diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000000..28403afd24 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,163 @@ +[alias] +# Collection of project-wide Clippy lints. This is done via aliases because +# Clippy does not currently support project-wide lint configuration in a +# dedicated config file. +# +# NOTE: Only lints that currently pass in this workspace are enabled. +# Additional lints can be enabled incrementally as the codebase is cleaned up. +# NOTE: for the `make lint` command to work well, this must contain the same +# list of lints as the xclippy-fix command below. +xclippy = [ + "clippy", + "--workspace", + "--all-targets", + "--all-features", + "--", + "-Dwarnings", + "-Wunused-qualifications", + "-Wclippy::all", + "-Wclippy::await_holding_lock", + "-Wclippy::char_lit_as_u8", + "-Wclippy::checked_conversions", + "-Wclippy::dbg_macro", + "-Wclippy::debug_assert_with_mut_call", + "-Wclippy::disallowed_methods", + "-Wclippy::empty_enums", + "-Wclippy::exit", + "-Wclippy::expl_impl_clone_on_copy", + "-Wclippy::explicit_deref_methods", + "-Wclippy::filter_map_next", + "-Wclippy::float_cmp_const", + "-Wclippy::fn_params_excessive_bools", + "-Wclippy::if_let_mutex", + "-Wclippy::imprecise_flops", + "-Wclippy::inefficient_to_string", + "-Wclippy::invalid_upcast_comparisons", + "-Wclippy::large_digit_groups", + "-Wclippy::large_stack_arrays", + "-Wclippy::large_types_passed_by_value", + "-Wclippy::let_unit_value", + "-Wclippy::linkedlist", + "-Wclippy::lossy_float_literal", + "-Wclippy::macro_use_imports", + "-Wclippy::manual_ok_or", + "-Wclippy::map_flatten", + "-Wclippy::mem_forget", + "-Wclippy::missing_enforced_import_renames", + "-Wclippy::mut_mut", + "-Wclippy::mutex_integer", + "-Wclippy::needless_borrow", + "-Wclippy::needless_collect", + "-Wclippy::option_option", + "-Wclippy::path_buf_push_overwrite", + "-Wclippy::rc_mutex", + "-Wclippy::redundant_clone", + "-Wclippy::redundant_closure_for_method_calls", + "-Wclippy::trivially_copy_pass_by_ref", + "-Wclippy::uninlined_format_args", + "-Wclippy::ref_option_ref", + "-Wclippy::rest_pat_in_fully_bound_structs", + "-Wclippy::same_functions_in_if_condition", + "-Wclippy::string_add", + "-Wclippy::string_add_assign", + "-Wclippy::todo", + "-Wclippy::trait_duplication_in_bounds", + "-Wclippy::unimplemented", + "-Wclippy::unnested_or_patterns", + "-Wclippy::useless_transmute", + "-Wclippy::verbose_file_reads", + "-Wclippy::zero_sized_map_values", + # The following lints are disabled because they still trigger warnings: + # -Wclippy::derive_partial_eq_without_eq (4 warnings) + # -Wclippy::doc_markdown (826 warnings) + # -Wclippy::enum_glob_use (5 warnings) + # -Wclippy::explicit_into_iter_loop (7 warnings) + # -Wclippy::fallible_impl_from (4 warnings) + # -Wclippy::flat_map_option (1 warning) + # -Wclippy::from_iter_instead_of_collect (6 warnings) + # -Wclippy::implicit_clone (2 warnings) + # -Wclippy::map_err_ignore (138 warnings) + # -Wclippy::map_unwrap_or (17 warnings) + # -Wclippy::manual_assert (6 warnings) + # -Wclippy::match_same_arms (16 warnings) + # -Wclippy::match_wild_err_arm (6 warnings) + # -Wclippy::match_wildcard_for_single_variants (2 warnings) + # -Wclippy::needless_continue (6 warnings) + # -Wclippy::needless_for_each (10 warnings) + # -Wclippy::needless_pass_by_value (37 warnings) + # -Wclippy::ptr_as_ptr (12 warnings) + # -Wclippy::semicolon_if_nothing_returned (159 warnings) + # -Wclippy::single_match_else (16 warnings) + # -Wclippy::string_lit_as_bytes (1 warning) + # -Wclippy::unnecessary_wraps (9 warnings) + # -Wclippy::unused_self (8 warnings) + # -Wclippy::cast_lossless (158 warnings) +] + +# Clippy fix with the same lints as xclippy. +# NOTE: for the `make lint` command to work well, this must contain the same +# list of lints as the xclippy setting above. +xclippy-fix = [ + "clippy", + "--fix", + "--allow-staged", + "--allow-dirty", + "--workspace", + "--all-targets", + "--all-features", + "--", + "-Dwarnings", + "-Wunused-qualifications", + "-Wclippy::all", + "-Wclippy::await_holding_lock", + "-Wclippy::char_lit_as_u8", + "-Wclippy::checked_conversions", + "-Wclippy::dbg_macro", + "-Wclippy::debug_assert_with_mut_call", + "-Wclippy::disallowed_methods", + "-Wclippy::empty_enums", + "-Wclippy::exit", + "-Wclippy::expl_impl_clone_on_copy", + "-Wclippy::explicit_deref_methods", + "-Wclippy::filter_map_next", + "-Wclippy::float_cmp_const", + "-Wclippy::fn_params_excessive_bools", + "-Wclippy::if_let_mutex", + "-Wclippy::imprecise_flops", + "-Wclippy::inefficient_to_string", + "-Wclippy::invalid_upcast_comparisons", + "-Wclippy::large_digit_groups", + "-Wclippy::large_stack_arrays", + "-Wclippy::large_types_passed_by_value", + "-Wclippy::let_unit_value", + "-Wclippy::linkedlist", + "-Wclippy::lossy_float_literal", + "-Wclippy::macro_use_imports", + "-Wclippy::manual_ok_or", + "-Wclippy::map_flatten", + "-Wclippy::mem_forget", + "-Wclippy::missing_enforced_import_renames", + "-Wclippy::mut_mut", + "-Wclippy::mutex_integer", + "-Wclippy::needless_borrow", + "-Wclippy::needless_collect", + "-Wclippy::option_option", + "-Wclippy::path_buf_push_overwrite", + "-Wclippy::rc_mutex", + "-Wclippy::redundant_clone", + "-Wclippy::redundant_closure_for_method_calls", + "-Wclippy::trivially_copy_pass_by_ref", + "-Wclippy::uninlined_format_args", + "-Wclippy::ref_option_ref", + "-Wclippy::rest_pat_in_fully_bound_structs", + "-Wclippy::same_functions_in_if_condition", + "-Wclippy::string_add", + "-Wclippy::string_add_assign", + "-Wclippy::todo", + "-Wclippy::trait_duplication_in_bounds", + "-Wclippy::unimplemented", + "-Wclippy::unnested_or_patterns", + "-Wclippy::useless_transmute", + "-Wclippy::verbose_file_reads", + "-Wclippy::zero_sized_map_values", +] diff --git a/.config/nextest.toml b/.config/nextest.toml index 9268e014ec..0b019d6727 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -1,5 +1,6 @@ [profile.default] -default-filter = 'not (test(merkle::smt::full::concurrent) or test(merkle::smt::full::large) or binary(rocksdb_large_smt))' +# The large-SMT module path is merkle::smt::large::... +default-filter = 'not (test(merkle::smt::full::concurrent) or test(merkle::smt::large) or binary(rocksdb_large_smt))' fail-fast = false failure-output = "immediate-final" @@ -9,6 +10,6 @@ fail-fast = false failure-output = "immediate-final" [profile.large-smt] -default-filter = '(test(merkle::smt::full::large) or binary(rocksdb_large_smt))' +default-filter = '(test(merkle::smt::large) or binary(rocksdb_large_smt))' fail-fast = false failure-output = "immediate-final" diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 0000000000..64e3e59094 --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,3 @@ +self-hosted-runner: + labels: + - warp-ubuntu-latest-x64-4x diff --git a/.github/actions/install-llvm/action.yml b/.github/actions/install-llvm/action.yml index 18703f362a..99012f50cb 100644 --- a/.github/actions/install-llvm/action.yml +++ b/.github/actions/install-llvm/action.yml @@ -9,35 +9,44 @@ runs: using: "composite" steps: - shell: bash - run: | + env: + LLVM_VERSION: ${{ inputs.version }} + run: | # zizmor: ignore[github-env] this composite action intentionally exports LLVM paths for later steps set -eux + case "${LLVM_VERSION}" in + ''|*[!0-9]*) + echo "::error::LLVM version must be a numeric major version" + exit 1 + ;; + esac + CODENAME="$(. /etc/os-release && echo "$UBUNTU_CODENAME")" # Add apt.llvm.org repo curl -fsSL https://apt.llvm.org/llvm-snapshot.gpg.key | \ sudo gpg --dearmor -o /usr/share/keyrings/llvm-archive-keyring.gpg echo "deb [signed-by=/usr/share/keyrings/llvm-archive-keyring.gpg] \ - http://apt.llvm.org/${CODENAME}/ llvm-toolchain-${CODENAME}-${{ inputs.version }} main" | \ + http://apt.llvm.org/${CODENAME}/ llvm-toolchain-${CODENAME}-${LLVM_VERSION} main" | \ sudo tee /etc/apt/sources.list.d/llvm.list sudo apt-get update sudo apt-get install -y \ - clang-17 \ - lld-17 \ - llvm-17-dev \ - libclang-17-dev + clang-"${LLVM_VERSION}" \ + lld-"${LLVM_VERSION}" \ + llvm-"${LLVM_VERSION}"-dev \ + libclang-"${LLVM_VERSION}"-dev - sudo ln -sf /usr/lib/llvm-${{ inputs.version }}/lib/libclang-${{ inputs.version }}.so \ - /usr/lib/llvm-${{ inputs.version }}/lib/libclang.so + sudo ln -sf /usr/lib/llvm-"${LLVM_VERSION}"/lib/libclang-"${LLVM_VERSION}".so \ + /usr/lib/llvm-"${LLVM_VERSION}"/lib/libclang.so - echo "/usr/lib/llvm-${{ inputs.version }}/lib" | \ - sudo tee /etc/ld.so.conf.d/llvm-${{ inputs.version }}.conf + echo "/usr/lib/llvm-${LLVM_VERSION}/lib" | \ + sudo tee /etc/ld.so.conf.d/llvm-"${LLVM_VERSION}".conf sudo ldconfig # Env for build scripts & tools - echo "LIBCLANG_PATH=/usr/lib/llvm-${{ inputs.version }}/lib" >> "$GITHUB_ENV" - echo "LIBRARY_PATH=/usr/lib/llvm-${{ inputs.version }}/lib:${LIBRARY_PATH:-}" >> "$GITHUB_ENV" - echo "LD_LIBRARY_PATH=/usr/lib/llvm-${{ inputs.version }}/lib:${LD_LIBRARY_PATH:-}" >> "$GITHUB_ENV" - echo "/usr/lib/llvm-${{ inputs.version }}/bin" >> "$GITHUB_PATH" - echo "CC=clang-${{ inputs.version }}" >> "$GITHUB_ENV" - echo "CXX=clang++-${{ inputs.version }}" >> "$GITHUB_ENV" \ No newline at end of file + echo "LIBCLANG_PATH=/usr/lib/llvm-${LLVM_VERSION}/lib" >> "$GITHUB_ENV" + echo "LIBRARY_PATH=/usr/lib/llvm-${LLVM_VERSION}/lib:${LIBRARY_PATH:-}" >> "$GITHUB_ENV" + echo "LD_LIBRARY_PATH=/usr/lib/llvm-${LLVM_VERSION}/lib:${LD_LIBRARY_PATH:-}" >> "$GITHUB_ENV" + echo "/usr/lib/llvm-${LLVM_VERSION}/bin" >> "$GITHUB_PATH" + echo "CC=clang-${LLVM_VERSION}" >> "$GITHUB_ENV" + echo "CXX=clang++-${LLVM_VERSION}" >> "$GITHUB_ENV" diff --git a/.github/actions/workspace-release/action.yml b/.github/actions/workspace-release/action.yml index dadd90d123..dbd6eb21ca 100644 --- a/.github/actions/workspace-release/action.yml +++ b/.github/actions/workspace-release/action.yml @@ -31,12 +31,12 @@ runs: fi - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # pin@stable with: toolchain: "1.90" - name: Cache cargo registry and git index - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # pin@v4 with: path: | ~/.cargo/registry @@ -49,7 +49,7 @@ runs: uses: ./.github/actions/cleanup-runner - name: Install cargo-binstall - uses: taiki-e/install-action@v2 + uses: taiki-e/install-action@a2352fc6ce487f030a3aa709482d57823eadfb37 # pin@v2 with: tool: cargo-binstall @@ -57,14 +57,8 @@ runs: shell: bash run: cargo binstall --no-confirm --force cargo-msrv - - name: Install Rust 1.91 for cargo-msrv - shell: bash - run: rustup toolchain install 1.91 - - name: Check MSRV shell: bash - env: - RUSTUP_TOOLCHAIN: "1.91" run: | export PATH="$HOME/.cargo/bin:$PATH" ./scripts/check-msrv.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5950afdfc9..46f0460e93 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,7 +25,7 @@ jobs: matrix: toolchain: [stable, nightly] steps: - - uses: actions/checkout@main + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # pin@v4 - name: Cleanup large tools for build space uses: ./.github/actions/cleanup-runner - name: Build for no-std @@ -43,7 +43,7 @@ jobs: matrix: toolchain: [stable, nightly] steps: - - uses: actions/checkout@main + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # pin@v4 - name: Cleanup large tools for build space uses: ./.github/actions/cleanup-runner - name: Build miden-field for on-chain target diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index f001ad84c0..59e4984134 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@main + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # pin@v4 with: fetch-depth: 0 - name: Check for changes in changelog diff --git a/.github/workflows/contribution-quality.yml b/.github/workflows/contribution-quality.yml new file mode 100644 index 0000000000..da037f93b7 --- /dev/null +++ b/.github/workflows/contribution-quality.yml @@ -0,0 +1,31 @@ +name: Contribution Quality + +# Uses pull_request_target only to let the pinned shared workflow comment/label via API; +# it must not checkout or execute PR code. +on: + pull_request_target: # zizmor: ignore[dangerous-triggers] metadata-only shared workflow, no PR checkout + types: [opened, reopened, edited, synchronize, ready_for_review] + workflow_dispatch: + inputs: + pr_number: + description: "PR number to evaluate (manual runs)" + required: true + type: string + force_all: + description: "Skip trusted/draft checks" + required: false + default: "false" + type: choice + options: ["false", "true"] + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + contribution-quality: + uses: 0xMiden/.github/.github/workflows/contribution-quality.yml@385c1c10c311dc0508c71d83abb0be749a2c12ca # pin@main + with: + pr_number: ${{ github.event.inputs.pr_number || '' }} + force_all: ${{ github.event.inputs.force_all || 'false' }} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 444acfba6c..24ae1442fa 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -28,7 +28,7 @@ jobs: name: Generate and deploy crate documentation runs-on: ubuntu-latest steps: - - uses: actions/checkout@main + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # pin@v4 - name: Cleanup large tools for build space uses: ./.github/actions/cleanup-runner diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 36b6a56461..ee58c03c5a 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -28,13 +28,13 @@ jobs: target: [primitives, collections, string, vint64, goldilocks, budgeted] timeout-minutes: 15 steps: - - uses: actions/checkout@main + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # pin@v4 - name: Cleanup large tools for build space uses: ./.github/actions/cleanup-runner - - uses: dtolnay/rust-toolchain@nightly + - uses: dtolnay/rust-toolchain@5b842231ba77f5c045dba54ac5560fed2db780e2 # pin@nightly with: toolchain: nightly - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # pin@v2 - name: Install cargo-fuzz run: cargo +nightly install cargo-fuzz --locked - name: Run fuzz target (smoke test) @@ -52,13 +52,13 @@ jobs: target: [word, merkle, merkle_store, smt_serde, mmr, crypto, aead, signatures] timeout-minutes: 15 steps: - - uses: actions/checkout@main + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # pin@v4 - name: Cleanup large tools for build space uses: ./.github/actions/cleanup-runner - - uses: dtolnay/rust-toolchain@nightly + - uses: dtolnay/rust-toolchain@5b842231ba77f5c045dba54ac5560fed2db780e2 # pin@nightly with: toolchain: nightly - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # pin@v2 - name: Install cargo-fuzz run: cargo +nightly install cargo-fuzz --locked - name: Run fuzz target (smoke test) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 5c1b31403d..f466d5b3b6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,8 +16,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: actions/checkout@v4 - - uses: taiki-e/install-action@v2 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # pin@v4 + - uses: taiki-e/install-action@a2352fc6ce487f030a3aa709482d57823eadfb37 # pin@v2 with: tool: typos - run: make typos-check @@ -26,14 +26,14 @@ jobs: name: rustfmt runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # pin@v4 - name: Rustup run: | rustup update --no-self-update nightly rustup +nightly component add rustfmt - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # pin@v2 with: - save-if: ${{ github.event_name == 'push' && github.ref == format('refs/heads/{0}', inputs.target_branch) }} + save-if: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/next') }} - name: Fmt run: make format-check @@ -41,7 +41,7 @@ jobs: name: clippy runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # pin@v4 - name: Cleanup large tools for build space uses: ./.github/actions/cleanup-runner # Added: LLVM/Clang for RocksDB/bindgen @@ -53,9 +53,9 @@ jobs: run: | rustup update --no-self-update rustup component add clippy - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # pin@v2 with: - save-if: ${{ github.event_name == 'push' && github.ref == format('refs/heads/{0}', inputs.target_branch) }} + save-if: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/next') }} - name: Clippy run: make clippy @@ -63,8 +63,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: actions/checkout@v4 - - uses: taiki-e/install-action@v2 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # pin@v4 + - uses: taiki-e/install-action@a2352fc6ce487f030a3aa709482d57823eadfb37 # pin@v2 with: tool: taplo-cli - run: make toml-check @@ -73,8 +73,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: actions/checkout@v4 - - uses: taiki-e/install-action@v2 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # pin@v4 + - uses: taiki-e/install-action@a2352fc6ce487f030a3aa709482d57823eadfb37 # pin@v2 with: tool: cargo-workspace-lints - run: | @@ -84,7 +84,7 @@ jobs: name: doc runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # pin@v4 - name: Cleanup large tools for build space uses: ./.github/actions/cleanup-runner # Added: LLVM/Clang for RocksDB/bindgen @@ -94,26 +94,31 @@ jobs: version: "17" - name: Rustup run: rustup update --no-self-update - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # pin@v2 with: - save-if: ${{ github.event_name == 'push' && github.ref == format('refs/heads/{0}', inputs.target_branch) }} + save-if: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/next') }} - name: Build docs run: make doc - unused_deps: - name: check for unused dependencies + shear: + name: cargo-shear runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: machete - uses: bnjbvr/cargo-machete@main + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # pin@v4 + with: + persist-credentials: false + - uses: taiki-e/install-action@b8be7f5e140177087325943c4a8e169d01c59b3d # pin@v2 + with: + tool: cargo-shear + - name: Check Cargo manifests for unused dependencies + run: make shear cargo-deny: name: check dependencies with cargo-deny runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: taiki-e/install-action@v2 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # pin@v4 + - uses: taiki-e/install-action@a2352fc6ce487f030a3aa709482d57823eadfb37 # pin@v2 with: tool: cargo-deny - run: make cargo-deny @@ -122,7 +127,7 @@ jobs: name: zeroize-audit runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # pin@v4 - name: Rustup run: | rustup update --no-self-update nightly @@ -133,14 +138,14 @@ jobs: name: check fuzz crate runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # pin@v4 - run: make check-fuzz check-features: name: check all feature combinations runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # pin@v4 - name: Cleanup large tools for build space uses: ./.github/actions/cleanup-runner # Added: LLVM/Clang for RocksDB/bindgen (needed for rocksdb feature) @@ -150,11 +155,11 @@ jobs: version: "17" - name: Rustup run: rustup update --no-self-update - - uses: taiki-e/install-action@v2 + - uses: taiki-e/install-action@a2352fc6ce487f030a3aa709482d57823eadfb37 # pin@v2 with: tool: cargo-hack - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # pin@v2 with: - save-if: ${{ github.event_name == 'push' && github.ref == format('refs/heads/{0}', inputs.target_branch) }} + save-if: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/next') }} - name: Check all feature combinations run: ./scripts/check-features.sh diff --git a/.github/workflows/signed-commits.yml b/.github/workflows/signed-commits.yml new file mode 100644 index 0000000000..c3434f529a --- /dev/null +++ b/.github/workflows/signed-commits.yml @@ -0,0 +1,19 @@ +# Verifies that all commits in a PR are signed (GPG or SSH). +# Delegates the actual check to the organization-level reusable workflow. + +name: signed commits + +permissions: + contents: read + pull-requests: write + +on: + pull_request_target: # zizmor: ignore[dangerous-triggers] metadata-only shared workflow, no PR checkout + branches: + - main + - next + types: [opened, reopened, synchronize] + +jobs: + check-signed-commits: + uses: 0xMiden/.github/.github/workflows/signed-commits.yml@385c1c10c311dc0508c71d83abb0be749a2c12ca # pin@main diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 01c777f170..57a0125355 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,20 +18,22 @@ permissions: jobs: test: - name: test ${{matrix.toolchain}} on ${{matrix.os}} with ${{matrix.args}} - runs-on: ${{matrix.os}}-latest + name: test ${{matrix.toolchain}} on ubuntu with ${{matrix.args}} + runs-on: warp-ubuntu-latest-x64-4x strategy: fail-fast: false matrix: - toolchain: [stable, nightly] - os: [ubuntu] + toolchain: [stable] args: [default, no-std, large-smt] timeout-minutes: 30 steps: - - uses: actions/checkout@main + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # pin@v4 - name: Cleanup large tools for build space uses: ./.github/actions/cleanup-runner - - uses: taiki-e/install-action@nextest + - uses: taiki-e/install-action@dd81e62c198605dea8507fed3fb6834da354ded4 # pin@nextest + - uses: WarpBuilds/rust-cache@9d0cc3090d9c87de74ea67617b246e978735b1a1 # pin@v2 + with: + save-if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/next' }} - name: Install LLVM/Clang if: ${{ matrix.args == 'large-smt' }} uses: ./.github/actions/install-llvm @@ -45,18 +47,21 @@ jobs: test-smt-concurrent: name: test-smt-concurrent ${{ matrix.toolchain }} - runs-on: ubuntu-latest + runs-on: warp-ubuntu-latest-x64-4x if: ${{ github.event_name == 'pull_request' && (github.base_ref == 'main' || github.base_ref == 'next') }} strategy: fail-fast: false matrix: - toolchain: [stable, nightly] + toolchain: [stable] timeout-minutes: 30 steps: - - uses: actions/checkout@main + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # pin@v4 - name: Cleanup large tools for build space uses: ./.github/actions/cleanup-runner - - uses: taiki-e/install-action@nextest + - uses: taiki-e/install-action@dd81e62c198605dea8507fed3fb6834da354ded4 # pin@nextest + - uses: WarpBuilds/rust-cache@9d0cc3090d9c87de74ea67617b246e978735b1a1 # pin@v2 + with: + save-if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/next' }} - name: Perform concurrent SMT tests run: | rustup update --no-self-update ${{matrix.toolchain}} @@ -72,10 +77,10 @@ jobs: matrix: toolchain: [stable] steps: - - uses: actions/checkout@main + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # pin@v4 - name: Cleanup large tools for build space uses: ./.github/actions/cleanup-runner - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # pin@v2 - name: Install LLVM/Clang uses: ./.github/actions/install-llvm with: @@ -91,7 +96,7 @@ jobs: runs-on: ubuntu-latest if: ${{ github.event_name == 'pull_request' && (github.base_ref == 'main' || github.base_ref == 'next') }} steps: - - uses: actions/checkout@main + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # pin@v4 - name: Cleanup large tools for build space uses: ./.github/actions/cleanup-runner - name: Install LLVM/Clang @@ -103,3 +108,21 @@ jobs: rustup update --no-self-update nightly rustup default nightly make doc + + test-p3-parallel: + name: test Miden STARK crates parallel ${{ matrix.toolchain }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + toolchain: [stable, nightly] + timeout-minutes: 30 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # pin@v4 + - name: Cleanup large tools for build space + uses: ./.github/actions/cleanup-runner + - name: Run Miden STARK crate parallel tests + run: | + rustup update --no-self-update ${{ matrix.toolchain }} + rustup default ${{ matrix.toolchain }} + make test-p3-parallel diff --git a/.github/workflows/workspace-dry-run.yml b/.github/workflows/workspace-dry-run.yml index 2787ddc851..681352d866 100644 --- a/.github/workflows/workspace-dry-run.yml +++ b/.github/workflows/workspace-dry-run.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # pin@v4 with: fetch-depth: 0 diff --git a/.github/workflows/workspace-publish.yml b/.github/workflows/workspace-publish.yml index e04f90d2bd..491495dc45 100644 --- a/.github/workflows/workspace-publish.yml +++ b/.github/workflows/workspace-publish.yml @@ -16,13 +16,13 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # pin@v4 with: fetch-depth: 0 ref: main - name: Authenticate with crates.io - uses: rust-lang/crates-io-auth-action@v1 + uses: rust-lang/crates-io-auth-action@b7e9a28eded4986ec6b1fa40eeee8f8f165559ec # pin@v1 id: auth - name: Publish workspace crates diff --git a/.taplo.toml b/.taplo.toml index b735451f6e..78e0a605f9 100644 --- a/.taplo.toml +++ b/.taplo.toml @@ -1,3 +1,8 @@ +exclude = [ + # Keep Cargo alias argv order stable; reordering this file breaks `cargo xclippy`. + ".cargo/config.toml", +] + [formatting] align_entries = true column_width = 120 diff --git a/CHANGELOG.md b/CHANGELOG.md index 10163fb8b7..6082385d60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,26 @@ +## 0.24.0 (2026-04-19) + +- [BREAKING] Removed `AlgebraicSponge::merge_with_int()` method ([#894](https://github.com/0xMiden/crypto/pull/894)). +- [BREAKING] Updated `Poseidon2` instance to match Plonky3 one ([#905](https://github.com/0xMiden/crypto/pull/905)). +- Added `LargeSmtForest::add_lineages` which provides an efficient means of adding multiple new lineages at once ([#910](https://github.com/0xMiden/crypto/pull/910)). +- Added the ability to configure the sync-to-disk behavior of the persistent backend using its config ([#912](https://github.com/0xMiden/crypto/pull/912)). +- [BREAKING] Removed `WORD_SIZE_FELTS` and `WORD_SIZE_BYTES` from `miden-field` in favor of `Word::NUM_ELEMENTS` and `Word::SERIALIZED_SIZE`, respectively. The values remain the same ([#917](https://github.com/0xMiden/crypto/pull/917)). +- [BREAKING] Removed `WORD_SIZE` from `miden-crypto` in favor of `Word::NUM_ELEMENTS`. Clients will need to update references to the constant, but `Word` will already be in scope as it is re-exported from `miden-crypto` ([#917](https://github.com/0xMiden/crypto/pull/917)). +- [BREAKING] Removed `LexicographicWord` as `Word` itself now implements the correct comparison behavior. Any place where the former is used should be able to seamlessly swap to the latter ([#918](https://github.com/0xMiden/crypto/pull/918)). +- [BREAKING] Removed implementations of `Deref` and `DerefMut` for `Felt` ([#919](https://github.com/0xMiden/crypto/pull/919)). +- Added `Serializable` and `Deserializable` instances for `Arc` ([#920](https://github.com/0xMiden/crypto/pull/920)). +- Optimized batch inversion to use per-chunk scratch space ([#933](https://github.com/0xMiden/crypto/pull/933)). +- [BREAKING] Changed the signature of `Felt::new` to perform reduction, and raise an error if the input is invalid. Retained the old behavior as `Felt::new_unchecked`, as its usage may lead to incorrect results ([#924](https://github.com/0xMiden/crypto/pull/924)). +- Optimized field operations for `Goldilocks` ([#926](https://github.com/0xMiden/crypto/pull/926)). +- [BREAKING] Moved per-instance log trace heights from `AirInstance` into `StarkProof`; `prove_multi` / `verify_multi` now observe them into the Fiat-Shamir challenger internally ([#956](https://github.com/0xMiden/crypto/pull/956)). Consumers on the temporary `(log_trace_height, proof)` serialization path must drop the wrapper and stop pre-observing the height, or it will be bound twice. `StarkProof` no longer exposes per-instance heights directly — parse the proof with `StarkTranscript::from_proof` to read them; `num_traces()` is available for the count. +- [BREAKING] `prove_multi` / `verify_multi` no longer require instances in ascending trace-height order; the prover sorts internally and the proof carries an `air_order` permutation ([#941](https://github.com/0xMiden/crypto/issues/941)). `InstanceShapes::from_trace_heights` now sorts internally and embeds the AIR ordering. `InstanceShapes::observe` renamed to `observe_heights`. The `NotAscending` error variant is removed; `InvalidAirOrder` and `AirOrderLengthMismatch` are added. `AirWitness` now derives `Clone + Copy`. Callers must bind AIR configurations and `air_order` into the Fiat-Shamir challenger — see the prover module-level docs. +- [BREAKING] Split the `SecretKey` type for both ECDSA-k256 and EdDSA-25519 into `SigningKey` and `KeyExchangeKey` to help enforce better practices around key reuse. `SecretKey` is no longer available in the public API; all usages should be moved to one of the new key types ([#965](https://github.com/0xMiden/crypto/pull/965)). +- Reduce repeated history scans in historical `LargeSmtForest::open()` queries ([#971](https://github.com/0xMiden/crypto/pull/971)). + ## 0.23.0 (2026-03-11) - Replaced `Subtree` internal storage with bitmask layout ([#784](https://github.com/0xMiden/crypto/pull/784)). +- [BREAKING] Enforced a maximum MMR forest size and made MMR/forest constructors and appends fallible to reject oversized inputs ([#857](https://github.com/0xMiden/crypto/pull/857)). - [BREAKING] `PartialMmr::open()` now returns `Option` instead of `Option` ([#787](https://github.com/0xMiden/crypto/pull/787)). - [BREAKING] Refactored BLAKE3 to use `Digest` struct, added `Digest192` type alias ([#811](https://github.com/0xMiden/crypto/pull/811)). - [BREAKING] Added validation to `PartialMmr::from_parts()` and `Deserializable` implementation, added `from_parts_unchecked()` for performance-critical code ([#812](https://github.com/0xMiden/crypto/pull/812)). @@ -20,7 +40,10 @@ - [BREAKING] Fixed `NodeIndex::to_scalar_index()` overflow at depth 64 by returning `Result` ([#865](https://github.com/0xMiden/crypto/issues/865)). - [BREAKING] Removed `RpoRandomCoin` and `RpxRandomCoin` and introduced a Poseidon2-based `RandomCoin` ([#871](https://github.com/0xMiden/crypto/pull/871)). - Harden MerkleStore deserialization and fuzz coverage ([#878](https://github.com/0xMiden/crypto/pull/878)). -- [BREAKING] Upgraded Plonky3 from 0.4.2 to 0.5.0 and replaced `p3-miden-air`, `p3-miden-fri`, and `p3-miden-prover` with the unified `p3-miden-lifted-stark` crate. The `stark` module now re-exports the Lifted STARK proving system from [p3-miden](https://github.com/0xMiden/p3-miden). +- [BREAKING] Upgraded Plonky3 from 0.4.2 to 0.5.0 and replaced `p3-miden-air`, `p3-miden-fri`, and `p3-miden-prover` with the unified `miden-lifted-stark` crate. The `stark` module now re-exports the Lifted STARK proving system from [p3-miden](https://github.com/0xMiden/p3-miden). +- [BREAKING] Changed the `LargeSmtForest::entries` iterator to be fallible by explicitly returning `Result` as the iterator item. +- [BREAKING] Updated `SparseMerkleTree` and its implementations to reject batches of key-value pairs that contain more than one instance of any given key. This may cause previously successful operations to now fail if your input batch is not de-duplicated. +- [BREAKING] `SimpleSmt::compute_mutations` now returns a result so it can fail gracefully if the input batch contains duplicate keys. ## 0.22.4 (2026-03-03) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c7dfdfdbf6..aa9608d205 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,6 +11,20 @@ We want to make contributing to this project as easy and transparent as possible   +## Contribution Quality + +To keep review time focused on meaningful improvements, we generally do not accept: +- Trivial typo fixes +- Minor code or documentation changes that don't materially improve clarity or completeness + +Contributions should: +- Include clear reasoning for the change +- Be linked to an issue the author has been assigned to +- Be testable / reviewable without unnecessary overhead +- Pass all CI tests + +**We reserve the right to close PRs at our discretion, or batch trivial valid fixes into internal commits.** + ## Flow We are using [Github Flow](https://docs.github.com/en/get-started/quickstart/github-flow), so all code changes happen through pull requests from a [forked repo](https://docs.github.com/en/get-started/quickstart/fork-a-repo). @@ -43,6 +57,10 @@ i.e. this branches state: More about rebase [here](https://git-scm.com/docs/git-rebase) and [here](https://www.atlassian.com/git/tutorials/rewriting-history/git-rebase#:~:text=What%20is%20git%20rebase%3F,of%20a%20feature%20branching%20workflow.) +### Signing commits + +We require all commits to be [signed](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification#ssh-commit-signature-verification). + ### Commit messages - Commit messages should be written in a short, descriptive manner and be prefixed with tags for the change type and scope (if possible) according to the [semantic commit](https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716) scheme. @@ -84,11 +102,12 @@ We use [semver](https://semver.org/) naming convention. ## Pre-PR checklist 1. Repo forked and branch created from `next` according to the naming convention. -2. Commit messages and code style follow conventions. -3. Tests added for new functionality. -4. Documentation/comments updated for all changes according to our documentation convention. -5. Clippy and Rustfmt linting passed. -6. New branch rebased from `next`. +2. Every commit is [signed](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification#ssh-commit-signature-verification). +3. Commit messages and code style follow conventions. +4. Tests added for new functionality. +5. Documentation/comments updated for all changes according to our documentation convention. +6. Clippy and Rustfmt linting passed. +7. New branch rebased from `next`.   diff --git a/Cargo.lock b/Cargo.lock index 8096f0328a..7f02c385d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,17 +21,41 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "anes" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -44,15 +68,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -139,22 +163,22 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "blake3" -version = "1.8.3" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +checksum = "4d2d5991425dfd0785aed03aedcf0b321d61975c9b5b3689c774a2610ae0b51e" dependencies = [ "arrayref", "arrayvec", "cc", "cfg-if", "constant_time_eq", - "cpufeatures", + "cpufeatures 0.3.0", ] [[package]] @@ -190,9 +214,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.56" +version = "1.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", "jobserver", @@ -223,7 +247,7 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -290,9 +314,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.60" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -300,9 +324,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -312,9 +336,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -324,15 +348,24 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cobs" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "const-oid" @@ -355,12 +388,22 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "criterion" -version = "0.7.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1c047a62b0cc3e145fa84415a3191f628e980b194c2755aa12300a4e6cbd928" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" dependencies = [ + "alloca", "anes", "cast", "ciborium", @@ -369,6 +412,7 @@ dependencies = [ "itertools 0.13.0", "num-traits", "oorandom", + "page_size", "plotters", "rayon", "regex", @@ -380,14 +424,20 @@ dependencies = [ [[package]] name = "criterion-plot" -version = "0.6.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b1bcc0dc7dfae599d84ad0b1a55f80cde8af3725da8313b528da95ef783e338" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" dependencies = [ "cast", "itertools 0.13.0", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -449,7 +499,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest", "fiat-crypto", @@ -555,6 +605,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "equivalent" version = "1.0.2" @@ -573,9 +635,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "ff" @@ -617,6 +679,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "futures-core" version = "0.3.32" @@ -748,7 +816,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -756,6 +824,17 @@ 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "heck" @@ -795,12 +874,12 @@ checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "serde", "serde_core", ] @@ -840,9 +919,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jobserver" @@ -856,9 +935,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ "once_cell", "wasm-bindgen", @@ -884,9 +963,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "leb128fmt" version = "0.1.0" @@ -895,9 +980,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.183" +version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] name = "libloading" @@ -931,9 +1016,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.25" +version = "1.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52f4c29e2a68ac30c9087e1b772dc9f44a2b66ed44edf2266cf2be9b03dafc1" +checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" dependencies = [ "cc", "pkg-config", @@ -971,15 +1056,54 @@ dependencies = [ "libc", ] +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "miden-bench" +version = "0.24.0" +dependencies = [ + "clap", + "miden-lifted-stark", + "p3-air", + "p3-batch-stark", + "p3-blake3", + "p3-blake3-air", + "p3-commit", + "p3-dft", + "p3-field", + "p3-fri", + "p3-goldilocks", + "p3-keccak", + "p3-keccak-air", + "p3-lookup", + "p3-matrix", + "p3-merkle-tree", + "p3-poseidon2-air", + "p3-symmetric", + "p3-uni-stark", + "postcard", + "rand 0.10.1", + "tracing", + "tracing-forest", + "tracing-subscriber", +] + [[package]] name = "miden-crypto" -version = "0.23.0" +version = "0.24.0" dependencies = [ "assert_matches", "blake3", @@ -990,16 +1114,17 @@ dependencies = [ "curve25519-dalek", "ed25519-dalek", "flume", - "glob", "hex", "hkdf", "itertools 0.14.0", "k256", "miden-crypto-derive", "miden-field", + "miden-lifted-stark", "miden-serde-utils", "num", "num-complex", + "once_cell", "p3-blake3", "p3-challenger", "p3-dft", @@ -1007,31 +1132,28 @@ dependencies = [ "p3-keccak", "p3-matrix", "p3-maybe-rayon", - "p3-miden-lifted-stark", "p3-symmetric", "p3-util", "proptest", - "rand 0.9.2", + "rand 0.9.4", "rand_chacha", "rand_core 0.9.5", "rand_hc", "rayon", "rocksdb", - "rstest", "seq-macro", "serde", "sha2", "sha3", "subtle", "tempfile", - "thiserror", - "winter-rand-utils", + "thiserror 2.0.18", "x25519-dalek", ] [[package]] name = "miden-crypto-derive" -version = "0.23.0" +version = "0.24.0" dependencies = [ "quote", "syn", @@ -1039,30 +1161,95 @@ dependencies = [ [[package]] name = "miden-field" -version = "0.23.0" +version = "0.24.0" dependencies = [ "miden-serde-utils", "num-bigint", "p3-challenger", "p3-field", "p3-goldilocks", + "p3-util", "paste", "proptest", - "rand 0.10.0", + "rand 0.10.1", "rstest", "serde", "subtle", - "thiserror", + "thiserror 2.0.18", +] + +[[package]] +name = "miden-lifted-air" +version = "0.24.0" +dependencies = [ + "p3-air", + "p3-field", + "p3-matrix", + "p3-util", + "thiserror 2.0.18", +] + +[[package]] +name = "miden-lifted-stark" +version = "0.24.0" +dependencies = [ + "criterion", + "miden-lifted-air", + "miden-stark-transcript", + "miden-stateful-hasher", + "p3-blake3", + "p3-blake3-air", + "p3-challenger", + "p3-commit", + "p3-dft", + "p3-field", + "p3-fri", + "p3-goldilocks", + "p3-interpolation", + "p3-keccak", + "p3-keccak-air", + "p3-matrix", + "p3-maybe-rayon", + "p3-merkle-tree", + "p3-poseidon2-air", + "p3-symmetric", + "p3-util", + "rand 0.10.1", + "serde", + "thiserror 2.0.18", + "tracing", + "tracing-subscriber", ] [[package]] name = "miden-serde-utils" -version = "0.23.0" +version = "0.24.0" dependencies = [ "p3-field", "p3-goldilocks", ] +[[package]] +name = "miden-stark-transcript" +version = "0.24.0" +dependencies = [ + "p3-challenger", + "p3-field", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "miden-stateful-hasher" +version = "0.24.0" +dependencies = [ + "p3-bn254", + "p3-field", + "p3-goldilocks", + "p3-mersenne-31", + "p3-symmetric", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1088,6 +1275,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + [[package]] name = "num" version = "0.4.3" @@ -1164,9 +1360,13 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "critical-section", + "portable-atomic", +] [[package]] name = "once_cell_polyfill" @@ -1188,31 +1388,82 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "p3-air" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091da36040f5a4e974f8254884859d335559fef75ec287eb6a7467dfd83501de" +checksum = "9f2ec9cbfc642fc5173817287c3f8b789d07743b5f7e812d058b7a03e344f9ab" dependencies = [ "p3-field", "p3-matrix", "tracing", ] +[[package]] +name = "p3-batch-stark" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3360f0bd705cf9eb0dbe152f7da2de4096ef8c13022c309f923344da516c1aa9" +dependencies = [ + "hashbrown 0.16.1", + "p3-air", + "p3-challenger", + "p3-commit", + "p3-field", + "p3-lookup", + "p3-matrix", + "p3-maybe-rayon", + "p3-uni-stark", + "p3-util", + "serde", + "tracing", +] + [[package]] name = "p3-blake3" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bb4e967e9af91413233d8b252c620228cab168c398b8887ad84a7bcee16015a" +checksum = "b667f43b19499dd939c9e2553aa95688936a88360d50117dae3c8848d07dbc70" dependencies = [ "blake3", "p3-symmetric", "p3-util", ] +[[package]] +name = "p3-blake3-air" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3344e24bb7a0302b38b6025a914f24fc748406ffa694b79f289e41fcf8280945" +dependencies = [ + "itertools 0.14.0", + "p3-air", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "rand 0.10.1", + "tracing", +] + +[[package]] +name = "p3-bn254" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c582646adc8a1014a48a20e703294118cef583da035e17f873465d2a6c62e13" +dependencies = [ + "num-bigint", + "p3-field", + "p3-poseidon2", + "p3-symmetric", + "p3-util", + "paste", + "rand 0.10.1", + "serde", +] + [[package]] name = "p3-challenger" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685fe948f1c038881f814556d3d9658ea47766e4d16a687aa09e001df2f3b126" +checksum = "4a0b490c745a7d2adeeafff06411814c8078c432740162332b3cd71be0158a76" dependencies = [ "p3-field", "p3-maybe-rayon", @@ -1224,11 +1475,13 @@ dependencies = [ [[package]] name = "p3-commit" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f10c38c88fd90dedb4cd66eb7ba7d4ce43f3adf46d82aee40178a5bbf7e71589" +checksum = "916ae7989d5c3b49f887f5c55b2f9826bdbb81aaebf834503c4145d8b267c829" dependencies = [ "itertools 0.14.0", + "p3-challenger", + "p3-dft", "p3-field", "p3-matrix", "p3-util", @@ -1237,9 +1490,9 @@ dependencies = [ [[package]] name = "p3-dft" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e08de44b5c241e7a4c038df1540e37038c83767e2f4b9fa4f5fd627c2916879" +checksum = "55301e91544440254977108b85c32c09d7ea05f2f0dd61092a2825339906a4a7" dependencies = [ "itertools 0.14.0", "p3-field", @@ -1252,189 +1505,198 @@ dependencies = [ [[package]] name = "p3-field" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb3bfc3a1ba51a757e9a7a7bd753a27886e8c05c38c4b6a43d59cd86c20a53f" +checksum = "85affca7fc983889f260655c4cf74163eebb94605f702e4b6809ead707cba54f" dependencies = [ "itertools 0.14.0", "num-bigint", "p3-maybe-rayon", "p3-util", "paste", - "rand 0.10.0", + "rand 0.10.1", "serde", "tracing", ] [[package]] -name = "p3-goldilocks" -version = "0.5.0" +name = "p3-fri" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a40340068bada658dc42df69bf9409f0faed1940d6494f3cf9ca20f8fe54dc3a" +checksum = "0ac25574ed306b4c9ad1969faaecc0fe6081d45ad7e1ec236661a6e0e37b39e1" dependencies = [ - "num-bigint", + "itertools 0.14.0", "p3-challenger", + "p3-commit", "p3-dft", "p3-field", - "p3-mds", - "p3-poseidon1", - "p3-poseidon2", - "p3-symmetric", + "p3-interpolation", + "p3-matrix", + "p3-maybe-rayon", "p3-util", - "paste", - "rand 0.10.0", + "rand 0.10.1", "serde", + "spin 0.10.0", + "thiserror 2.0.18", + "tracing", ] [[package]] -name = "p3-keccak" -version = "0.5.0" +name = "p3-goldilocks" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b880adadc200c8d1bef00ec6eab61766c743647afbc06bc1cdafc7d87a2fe1d" +checksum = "0ca1081f5c47b940f2d75a11c04f62ea1cc58a5d480dd465fef3861c045c63cd" dependencies = [ + "num-bigint", + "p3-challenger", + "p3-dft", + "p3-field", + "p3-mds", + "p3-poseidon1", + "p3-poseidon2", "p3-symmetric", "p3-util", - "tiny-keccak", + "paste", + "rand 0.10.1", + "serde", ] [[package]] -name = "p3-matrix" -version = "0.5.0" +name = "p3-interpolation" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbb6f8655e789cf8eebd5ef821311f97030e39d00ca59bd085aa66aca297d35a" +checksum = "14fd48db63ff15f5e96dc46e6991dbc2d39431b82dcb154bad90f4579236e328" dependencies = [ - "itertools 0.14.0", "p3-field", + "p3-matrix", "p3-maybe-rayon", "p3-util", - "rand 0.10.0", - "serde", - "tracing", ] [[package]] -name = "p3-maybe-rayon" -version = "0.5.0" +name = "p3-keccak" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3494f57fb29307865df3cda8974dbff9a21a2dfe67309ff9b8f4397e05dce67" +checksum = "ebcf27615ece1995e4fcf4c69740f1cf515d1481367a20b4b3ce7f4f1b8d70f7" dependencies = [ - "rayon", + "p3-symmetric", + "p3-util", + "tiny-keccak", ] [[package]] -name = "p3-mds" -version = "0.5.0" +name = "p3-keccak-air" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712356f8cb7ee1a44004f7d31960a3ab28728f5da6b41acb373844ff477a78dd" +checksum = "8252bf89de1c6620cde84c69132271f9d987c58ad240f052014568d5c01f7293" dependencies = [ - "p3-dft", + "p3-air", "p3-field", - "p3-symmetric", + "p3-matrix", + "p3-maybe-rayon", "p3-util", - "rand 0.10.0", + "rand 0.10.1", + "tracing", ] [[package]] -name = "p3-miden-lifted-air" -version = "0.5.0" +name = "p3-lookup" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5c31c65fdc88952d7b301546add9670676e5b878aa0066dd929f107c203b006" +checksum = "bb2fa218ece267137b199aed8a5c1e9053c39094e69b4a94b12d83e612bf42c6" dependencies = [ + "hashbrown 0.16.1", "p3-air", "p3-field", "p3-matrix", - "p3-util", - "thiserror", + "p3-maybe-rayon", + "p3-uni-stark", + "serde", + "tracing", ] [[package]] -name = "p3-miden-lifted-fri" -version = "0.5.0" +name = "p3-matrix" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab9932f1b0a16609a45cd4ee10a4d35412728bc4b38837c7979d7c85d8dcc9fc" +checksum = "53428126b009071563d1d07305a9de8be0d21de00b57d2475289ee32ffca6577" dependencies = [ - "p3-challenger", - "p3-commit", - "p3-dft", + "itertools 0.14.0", "p3-field", - "p3-matrix", "p3-maybe-rayon", - "p3-miden-lmcs", - "p3-miden-transcript", "p3-util", - "rand 0.10.0", - "thiserror", + "rand 0.10.1", + "serde", "tracing", ] [[package]] -name = "p3-miden-lifted-stark" -version = "0.5.0" +name = "p3-maybe-rayon" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3956ab7270c3cdd53ca9796d39ae1821984eb977415b0672110f9666bff5d8" +checksum = "082bf467011c06c768c579ec6eb9accb5e1e62108891634cc770396e917f978a" +dependencies = [ + "rayon", +] + +[[package]] +name = "p3-mds" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35209e6214102ea6ec6b8cb1b9c15a9b8e597a39f9173597c957f123bced81b3" dependencies = [ - "p3-challenger", "p3-dft", "p3-field", - "p3-matrix", - "p3-maybe-rayon", - "p3-miden-lifted-air", - "p3-miden-lifted-fri", - "p3-miden-lmcs", - "p3-miden-stateful-hasher", - "p3-miden-transcript", + "p3-symmetric", "p3-util", - "thiserror", - "tracing", + "rand 0.10.1", ] [[package]] -name = "p3-miden-lmcs" -version = "0.5.0" +name = "p3-merkle-tree" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c46791c983e772136db3d48f102431457451447abb9087deb6c8ce3c1efc86" +checksum = "182a5383a54c50f47866f819946d28d95262f69967902734de8fdecb0d70c774" dependencies = [ + "itertools 0.14.0", "p3-commit", "p3-field", "p3-matrix", "p3-maybe-rayon", - "p3-miden-stateful-hasher", - "p3-miden-transcript", "p3-symmetric", "p3-util", - "rand 0.10.0", + "rand 0.10.1", "serde", - "thiserror", + "thiserror 2.0.18", "tracing", ] [[package]] -name = "p3-miden-stateful-hasher" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec47a9d9615eb3d9d2a59b00d19751d9ad85384b55886827913d680d912eac6a" -dependencies = [ - "p3-field", - "p3-symmetric", -] - -[[package]] -name = "p3-miden-transcript" -version = "0.5.0" +name = "p3-mersenne-31" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c565647487e4a949f67e6f115b0391d6cb82ac8e561165789939bab23d0ae7" +checksum = "f71ab5340a5b15a613e637f48046723dcd3cf14fa23598586a1691e1dcf33185" dependencies = [ + "itertools 0.14.0", + "num-bigint", "p3-challenger", + "p3-dft", "p3-field", + "p3-matrix", + "p3-mds", + "p3-poseidon2", + "p3-symmetric", + "p3-util", + "paste", + "rand 0.10.1", "serde", - "thiserror", ] [[package]] name = "p3-monty-31" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136c964a854ff2c466a8c3d37b9575e0ef39ba6592e31c134f2557a777039094" +checksum = "ffa8c99ec50c035020bbf5457c6a729ba6a975719c1a8dd3f16421081e4f650c" dependencies = [ "itertools 0.14.0", "num-bigint", @@ -1448,7 +1710,7 @@ dependencies = [ "p3-symmetric", "p3-util", "paste", - "rand 0.10.0", + "rand 0.10.1", "serde", "spin 0.10.0", "tracing", @@ -1456,33 +1718,48 @@ dependencies = [ [[package]] name = "p3-poseidon1" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe427e925ad0e85fd0e36ba53a3ab162dbeadc8507c31b7a513531df42d73e9" +checksum = "6a018b618e3fa0aec8be933b1d8e404edd23f46991f6bf3f5c2f3f95e9413fe9" dependencies = [ "p3-field", "p3-symmetric", - "rand 0.10.0", + "rand 0.10.1", ] [[package]] name = "p3-poseidon2" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "530f98868eeb1c776cabba3f96d363142f90745f53fe508c86edce8ba11f3c5a" +checksum = "256a668a9ba916f8767552f13d0ba50d18968bc74a623bfdafa41e2970c944d0" dependencies = [ "p3-field", "p3-mds", "p3-symmetric", "p3-util", - "rand 0.10.0", + "rand 0.10.1", +] + +[[package]] +name = "p3-poseidon2-air" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6d248d93db7258e4f9837f94e5e4260909ca85bf544a4fe0fe56f0154c826a" +dependencies = [ + "p3-air", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-poseidon2", + "rand 0.10.1", + "tracing", ] [[package]] name = "p3-symmetric" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "631318d0025f045682a8508ccfd9227604b62fe76b7ebb7d0e5cf9c43e3d590c" +checksum = "6c60a71a1507c13611b0f2b0b6e83669fd5b76f8e3115bcbced5ccfdf3ca7807" dependencies = [ "itertools 0.14.0", "p3-field", @@ -1490,17 +1767,46 @@ dependencies = [ "serde", ] +[[package]] +name = "p3-uni-stark" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c4ecaad8a7b4cf0fc711278c7a29fdc6d14239157866b17feaf14061834bc51" +dependencies = [ + "itertools 0.14.0", + "p3-air", + "p3-challenger", + "p3-commit", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-util", + "serde", + "thiserror 2.0.18", + "tracing", +] + [[package]] name = "p3-util" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46ae22dbaa5e923be05afc0cc57f4b27ef80cdcabad5bfbc62250a9672151211" +checksum = "f8b766b9e9254bf3fa98d76e42cf8a5b30628c182dfd5272d270076ee12f0fc0" dependencies = [ "rayon", "serde", "transpose", ] +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "paste" version = "1.0.15" @@ -1525,9 +1831,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plotters" @@ -1563,11 +1869,29 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1607,13 +1931,13 @@ dependencies = [ [[package]] name = "proptest" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" dependencies = [ "bitflags", "num-traits", - "rand 0.9.2", + "rand 0.9.4", "rand_chacha", "rand_xorshift", "regex-syntax", @@ -1643,9 +1967,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha", "rand_core 0.9.5", @@ -1653,11 +1977,11 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ - "rand_core 0.10.0", + "rand_core 0.10.1", ] [[package]] @@ -1690,9 +2014,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "rand_hc" @@ -1714,9 +2038,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -1818,9 +2142,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" @@ -1881,9 +2205,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "seq-macro" @@ -1941,7 +2265,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -1955,6 +2279,15 @@ dependencies = [ "keccak", ] +[[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 = "shlex" version = "1.3.0" @@ -1977,6 +2310,12 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + [[package]] name = "spin" version = "0.9.8" @@ -2036,9 +2375,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.26.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom 0.4.2", @@ -2047,13 +2386,33 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -2067,6 +2426,15 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -2088,18 +2456,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.0.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.25.4+spec-1.1.0" +version = "0.25.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ "indexmap", "toml_datetime", @@ -2109,9 +2477,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.9+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ "winnow", ] @@ -2143,6 +2511,52 @@ name = "tracing-core" version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-forest" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee40835db14ddd1e3ba414292272eddde9dad04d3d4b65509656414d1c42592f" +dependencies = [ + "ansi_term", + "smallvec", + "thiserror 1.0.69", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] [[package]] name = "transpose" @@ -2194,6 +2608,12 @@ 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" @@ -2224,11 +2644,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -2237,14 +2657,14 @@ 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", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", @@ -2255,9 +2675,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2265,9 +2685,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", @@ -2278,9 +2698,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] @@ -2321,14 +2741,30 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.91" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -2338,6 +2774,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-link" version = "0.2.1" @@ -2355,29 +2797,13 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.15" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" dependencies = [ "memchr", ] -[[package]] -name = "winter-rand-utils" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ff3b651754a7bd216f959764d0a5ab6f4b551c9a3a08fb9ccecbed594b614a" -dependencies = [ - "rand 0.9.2", - "winter-utils", -] - -[[package]] -name = "winter-utils" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9951263ef5317740cd0f49e618db00c72fabb70b75756ea26c4d5efe462c04dd" - [[package]] name = "wit-bindgen" version = "0.51.0" @@ -2387,6 +2813,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" @@ -2478,18 +2910,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.42" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.42" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 7b1bc6514c..2d5dee0a9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,16 @@ [workspace] -exclude = ["miden-crypto-fuzz"] -members = ["miden-crypto", "miden-crypto-derive", "miden-field", "miden-serde-utils"] +exclude = ["miden-crypto-fuzz"] +members = [ + "miden-bench", + "miden-crypto", + "miden-crypto-derive", + "miden-field", + "miden-serde-utils", + "stark/miden-lifted-air", + "stark/miden-lifted-stark", + "stark/miden-stark-transcript", + "stark/miden-stateful-hasher", +] resolver = "3" [workspace.package] @@ -11,12 +21,83 @@ keywords = ["crypto", "hash", "merkle", "miden"] license = "MIT OR Apache-2.0" repository = "https://github.com/0xMiden/crypto" rust-version = "1.90" -version = "0.23.0" +version = "0.24.0" [workspace.dependencies] -miden-crypto-derive = { path = "miden-crypto-derive", version = "0.23" } -miden-field = { path = "miden-field", version = "0.23" } -miden-serde-utils = { path = "miden-serde-utils", version = "0.23" } +miden-crypto-derive = { path = "miden-crypto-derive", version = "0.24" } +miden-field = { path = "miden-field", version = "0.24" } +miden-lifted-air = { default-features = false, path = "stark/miden-lifted-air", version = "0.24" } +miden-lifted-stark = { default-features = false, path = "stark/miden-lifted-stark", version = "0.24" } +miden-serde-utils = { path = "miden-serde-utils", version = "0.24" } +miden-stark-transcript = { default-features = false, path = "stark/miden-stark-transcript", version = "0.24" } +miden-stateful-hasher = { default-features = false, path = "stark/miden-stateful-hasher", version = "0.24" } + +# Plonky3 +p3-air = { default-features = false, version = "0.5" } +p3-batch-stark = { default-features = false, version = "0.5" } +p3-blake3 = { default-features = false, version = "0.5" } +p3-blake3-air = { default-features = false, version = "0.5" } +p3-bn254 = { default-features = false, version = "0.5" } +p3-challenger = { default-features = false, version = "0.5" } +p3-commit = { default-features = false, version = "0.5" } +p3-dft = { default-features = false, version = "0.5" } +p3-field = { default-features = false, version = "0.5" } +p3-fri = { default-features = false, version = "0.5" } +p3-goldilocks = { default-features = false, version = "0.5" } +p3-interpolation = { default-features = false, version = "0.5" } +p3-keccak = { default-features = false, version = "0.5" } +p3-keccak-air = { default-features = false, version = "0.5" } +p3-lookup = { default-features = false, version = "0.5" } +p3-matrix = { default-features = false, version = "0.5" } +p3-maybe-rayon = { default-features = false, version = "0.5" } +p3-merkle-tree = { default-features = false, version = "0.5" } +p3-mersenne-31 = { default-features = false, version = "0.5" } +p3-poseidon2-air = { default-features = false, version = "0.5" } +p3-symmetric = { default-features = false, version = "0.5" } +p3-uni-stark = { default-features = false, version = "0.5" } +p3-util = { default-features = false, version = "0.5" } + +# Shared third-party dependencies used across workspace crates +assert_matches = { default-features = false, version = "1.5" } +blake3 = { default-features = false, version = "1.8" } +cc = "1.2" +chacha20poly1305 = "0.10" +curve25519-dalek = { default-features = false, version = "4" } +ed25519-dalek = "2" +flume = "0.11.1" +hex = { default-features = false, version = "0.4" } +hkdf = { default-features = false, version = "0.12" } +itertools = "0.14" +k256 = "0.13" +num = { default-features = false, version = "0.4" } +num-complex = { default-features = false, version = "0.4" } +once_cell = { default-features = false, version = "1.21" } +proptest = { default-features = false, version = "1.7" } +quote = "1.0" +rand_chacha = { default-features = false, version = "0.9" } +rand_core = { default-features = false, version = "0.9" } +rand_hc = "0.3" +rayon = "1.10" +rocksdb = { default-features = false, version = "0.24" } +rstest = "0.26" +seq-macro = "0.3" +sha2 = { default-features = false, version = "0.10" } +sha3 = { default-features = false, version = "0.10" } +subtle = { default-features = false, version = "2.6" } +syn = "2.0" +tempfile = "3.20" +x25519-dalek = { default-features = false, version = "2.0" } + +# Shared third-party dependencies used by imported Miden STARK crates +clap = { features = ["derive"], version = "4" } +criterion = { features = ["html_reports"], version = "0.8" } +postcard = { default-features = false, features = ["alloc"], version = "1" } +rand = { default-features = false, version = "0.10" } +serde = { default-features = false, features = ["alloc", "derive"], version = "1.0" } +thiserror = { default-features = false, version = "2.0" } +tracing = { default-features = false, features = ["attributes"], version = "0.1.37" } +tracing-forest = "0.1.6" +tracing-subscriber = { features = ["env-filter", "fmt", "std"], version = "0.3.17" } [workspace.lints.rust] # Suppress warnings about `cfg(fuzzing)`, which is automatically set when using `cargo-fuzz`. diff --git a/Makefile b/Makefile index 2b205aacd6..f46530b807 100644 --- a/Makefile +++ b/Makefile @@ -7,18 +7,26 @@ help: # -- variables -------------------------------------------------------------------------------------- ALL_FEATURES_EXCEPT_ROCKSDB="concurrent executable internal serde std" +MIDEN_STARK_TEST_PACKAGES=-p miden-lifted-air -p miden-lifted-stark -p miden-stateful-hasher -p miden-stark-transcript WARNINGS=RUSTDOCFLAGS="-D warnings" # -- linting -------------------------------------------------------------------------------------- .PHONY: clippy -clippy: ## Run Clippy with configs - cargo clippy --workspace --all-targets --all-features -- -D warnings +clippy: ## Run Clippy with configs (alias for xclippy) + cargo xclippy +.PHONY: xclippy +xclippy: ## Run Clippy with the curated workspace lint set + cargo xclippy .PHONY: fix -fix: ## Run Fix with configs - cargo +nightly fix --allow-staged --allow-dirty --all-targets --all-features +fix: ## Run Fix with configs (alias for xclippy-fix) + cargo xclippy-fix + +.PHONY: xclippy-fix +xclippy-fix: ## Run Clippy with --fix using the same lint set as xclippy + cargo xclippy-fix .PHONY: format @@ -30,9 +38,9 @@ format: ## Run Format using nightly toolchain format-check: ## Run Format using nightly toolchain but only in check mode cargo +nightly fmt --all --check -.PHONY: machete -machete: ## Runs machete to find unused dependencies - cargo machete +.PHONY: shear +shear: ## Runs cargo-shear to find unused or misplaced dependencies + cargo shear --deny-warnings .PHONY: toml toml: ## Runs Format for all TOML files @@ -62,7 +70,7 @@ zeroize-audit: ## Run Zeroize audit using rustdoc JSON cargo run --quiet --manifest-path tools/zeroize-audit/Cargo.toml -- "$$target_dir/doc/miden_crypto.json" .PHONY: lint -lint: format fix clippy toml typos-check machete cargo-deny ## Run all linting tasks at once (Clippy, fixing, formatting, machete, cargo-deny) +lint: clippy fix format toml typos-check shear cargo-deny ## Run all linting tasks at once (Clippy, fixing, formatting, cargo-shear, cargo-deny) # --- docs ---------------------------------------------------------------------------------------- @@ -89,8 +97,12 @@ test-smt-concurrent: ## Run only concurrent SMT tests test-docs: cargo test --doc --all-features --profile test-release +.PHONY: test-p3-parallel +test-p3-parallel: ## Run Miden STARK crate tests with the parallel feature enabled + cargo test $(MIDEN_STARK_TEST_PACKAGES) -F miden-lifted-stark/parallel + .PHONY: test-large-smt -test-large-smt: ## Run only large SMT tests +test-large-smt: ## Run large SMT unit tests and RocksDB integration tests cargo nextest run --success-output immediate --profile large-smt --cargo-profile test-release --features rocksdb .PHONY: test @@ -103,9 +115,8 @@ check: ## Check all targets and features for errors without code generation cargo check --all-targets --all-features .PHONY: check-features -check-features: ## Check miden-crypto feature combinations - cargo check -p miden-crypto --all-targets --no-default-features - cargo check -p miden-crypto --all-targets --features ${ALL_FEATURES_EXCEPT_ROCKSDB} +check-features: ## Check curated feature combinations across the integrated workspace + ./scripts/check-features.sh .PHONY: check-fuzz check-fuzz: ## Check miden-crypto-fuzz compilation @@ -145,19 +156,19 @@ bench: ## Run crypto benchmarks .PHONY: bench-smt-concurrent bench-smt-concurrent: ## Run SMT benchmarks with concurrent feature - cargo run --release --features concurrent,executable -- --size 1000000 + cargo run --bin miden-crypto --release --features concurrent,executable -- --size 1000000 .PHONY: bench-large-smt-memory bench-large-smt-memory: ## Run large SMT benchmarks with memory storage - cargo run --release --features concurrent,executable -- --size 1000000 + cargo run --bin miden-crypto --release --features concurrent,executable -- --size 1000000 .PHONY: bench-large-smt-rocksdb bench-large-smt-rocksdb: ## Run large SMT benchmarks with rocksdb storage - cargo run --release --features concurrent,rocksdb,executable -- --storage rocksdb --size 1000000 + cargo run --bin miden-crypto --release --features concurrent,rocksdb,executable -- --storage rocksdb --size 1000000 .PHONY: bench-large-smt-rocksdb-open bench-large-smt-rocksdb-open: ## Run large SMT benchmarks with rocksdb storage and open existing database - cargo run --release --features concurrent,rocksdb,executable -- --storage rocksdb --open + cargo run --bin miden-crypto --release --features concurrent,rocksdb,executable -- --storage rocksdb --open # --- fuzzing -------------------------------------------------------------------------------- @@ -205,15 +216,15 @@ check-tools: ## Checks if development tools are installed @command -v typos >/dev/null 2>&1 && echo "[OK] typos is installed" || echo "[MISSING] typos is not installed (run: make install-tools)" @command -v cargo nextest >/dev/null 2>&1 && echo "[OK] nextest is installed" || echo "[MISSING] nextest is not installed (run: make install-tools)" @command -v taplo >/dev/null 2>&1 && echo "[OK] taplo is installed" || echo "[MISSING] taplo is not installed (run: make install-tools)" - @command -v cargo machete >/dev/null 2>&1 && echo "[OK] machete is installed" || echo "[MISSING] machete is not installed (run: make install-tools)" + @command -v cargo-shear >/dev/null 2>&1 && echo "[OK] cargo-shear is installed" || echo "[MISSING] cargo-shear is not installed (run: make install-tools)" @command -v cargo deny >/dev/null 2>&1 && echo "[OK] cargo-deny is installed" || echo "[MISSING] cargo-deny is not installed (run: make install-tools)" .PHONY: install-tools -install-tools: ## Installs development tools required by the Makefile (typos, nextest, taplo, machete, cargo-deny) +install-tools: ## Installs development tools required by the Makefile (typos, nextest, taplo, cargo-shear, cargo-deny) @echo "Installing development tools..." cargo install typos-cli --locked cargo install cargo-nextest --locked cargo install taplo-cli --locked - cargo install cargo-machete --locked + cargo install cargo-shear --locked cargo install cargo-deny --locked @echo "Development tools installation complete!" diff --git a/README.md b/README.md index f6bd083a03..0b475c76c1 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ Messages sealed as one type must be unsealed using the corresponding method, oth ## STARK proving system -[STARK module](./miden-crypto/src/lib.rs) provides the Lifted STARK proving system, implemented in [p3-miden](https://github.com/0xMiden/p3-miden) and built on top of the [Plonky3](https://github.com/Plonky3/Plonky3) framework. It includes: +[STARK module](./miden-crypto/src/lib.rs) provides the Lifted STARK proving system, built on top of the [Plonky3](https://github.com/Plonky3/Plonky3) framework. The crates that used to live in the upstream `p3-miden` repo now live in this repo under [`stark/`](./stark/). It includes: - AIR traits and builders for defining algebraic constraints. - A Lifted Merkle commitment scheme and FRI-based polynomial commitments. diff --git a/deny.toml b/deny.toml index ec25b7f29e..5a7bd7db0e 100644 --- a/deny.toml +++ b/deny.toml @@ -11,6 +11,8 @@ db-urls = ["https://github.com/rustsec/advisory-db"] ignore = [ # paste crate is unmaintained but still functional; transitively used by Plonky3 "RUSTSEC-2024-0436", + # tracing-forest is only used by the non-published miden-bench utility crate + "RUSTSEC-2021-0139", ] yanked = "warn" @@ -37,13 +39,18 @@ multiple-versions = "deny" skip = [ # Allow duplicate spin versions - transitively pulled by p3-dft and flume { name = "spin" }, + # tracing-forest in miden-bench still depends on thiserror 1.x + { name = "thiserror" }, + { name = "thiserror-impl" }, ] skip-tree = [ # miden-crypto uses rand 0.9 while Plonky3 0.5 uses rand 0.10, # causing duplicate rand / rand_core / getrandom trees + { name = "cpufeatures", version = "0.2.17" }, { name = "getrandom", version = "=0.2.17" }, - { name = "rand", version = "=0.9.2" }, + { name = "rand", version = "=0.9.4" }, { name = "rand_core", version = "=0.6.4" }, + { name = "rand_core", version = "=0.9.5" }, ] wildcards = "allow" diff --git a/miden-bench/Cargo.toml b/miden-bench/Cargo.toml new file mode 100644 index 0000000000..9537ff493e --- /dev/null +++ b/miden-bench/Cargo.toml @@ -0,0 +1,49 @@ +[package] +description = "Profiling binary for lifted STARK and batch STARK provers." +edition = "2024" +homepage = "https://github.com/0xMiden/crypto" +license = "MIT OR Apache-2.0" +name = "miden-bench" +publish = false +readme = "../README.md" +repository = "https://github.com/0xMiden/crypto" +rust-version.workspace = true +version.workspace = true + +[dependencies] +# Internal +miden-lifted-stark = { features = ["testing"], workspace = true } + +# Plonky3 +p3-air.workspace = true +p3-batch-stark.workspace = true +p3-blake3.workspace = true +p3-blake3-air.workspace = true +p3-commit.workspace = true +p3-dft.workspace = true +p3-field.workspace = true +p3-fri.workspace = true +p3-goldilocks.workspace = true +p3-keccak.workspace = true +p3-keccak-air.workspace = true +p3-lookup.workspace = true +p3-matrix.workspace = true +p3-merkle-tree.workspace = true +p3-poseidon2-air.workspace = true +p3-symmetric.workspace = true +p3-uni-stark.workspace = true + +# Third-party +clap.workspace = true +postcard.workspace = true +rand.workspace = true +tracing.workspace = true +tracing-forest = { features = ["ansi", "smallvec"], workspace = true } +tracing-subscriber.workspace = true + +[features] +default = [] +parallel = ["miden-lifted-stark/parallel"] + +[lints] +workspace = true diff --git a/miden-bench/src/batch.rs b/miden-bench/src/batch.rs new file mode 100644 index 0000000000..0285c600b0 --- /dev/null +++ b/miden-bench/src/batch.rs @@ -0,0 +1,386 @@ +//! Batch STARK AIR wrappers, lookup implementations, and prove/verify runner. + +use miden_lifted_stark::{ + air::{BaseAir, log2_strict_u8}, + testing::airs::poseidon2::NUM_POSEIDON2_COLS, +}; +use p3_air::{Air, AirBuilder, AirLayout, BaseLeaf, SymbolicExpression, WindowAccess}; +use p3_batch_stark::{ProverData, StarkInstance, prove_batch, verify_batch}; +use p3_blake3_air::{Blake3Air, NUM_BLAKE3_COLS}; +use p3_commit::ExtensionMmcs; +use p3_field::{Field, PrimeCharacteristicRing}; +use p3_fri::{FriParameters, TwoAdicFriPcs}; +use p3_keccak_air::{KeccakAir, NUM_KECCAK_COLS}; +use p3_lookup::{ + LookupAir, + lookup_traits::{Direction, Kind, Lookup}, +}; +use p3_matrix::{Matrix, dense::RowMajorMatrix}; +use p3_merkle_tree::MerkleTreeMmcs; +use p3_uni_stark::SymbolicAirBuilder; +use tracing::info_span; + +use crate::{ + BatchPoseidon2Air, Felt, GlRoundConstants, QuadFelt, RunResult, + cli::{AirType, Cli, TraceSpec}, +}; + +// ═══════════════════════════════════════════════════════════════════════════════ +// Keccak wrapper with a single local lookup +// ═══════════════════════════════════════════════════════════════════════════════ + +/// Wraps [`KeccakAir`] and adds a single local LogUp lookup producing one +/// extension-field permutation column. Matches the lifted prover's unconditional +/// 1-column EF aux trace. +#[derive(Clone)] +struct KeccakWithLookup { + num_lookups: usize, +} + +impl BaseAir for KeccakWithLookup { + fn width(&self) -> usize { + NUM_KECCAK_COLS + } +} + +impl Air for KeccakWithLookup { + fn eval(&self, builder: &mut AB) { + Air::eval(&KeccakAir {}, builder); + } +} + +impl LookupAir for KeccakWithLookup { + fn add_lookup_columns(&mut self) -> Vec { + let idx = self.num_lookups; + self.num_lookups += 1; + vec![idx] + } + + fn get_lookups(&mut self) -> Vec> { + self.num_lookups = 0; + let col0 = SymbolicExpression::Leaf(BaseLeaf::Constant(F::ONE)); + let one = SymbolicExpression::Leaf(BaseLeaf::Constant(F::ONE)); + let lookup_inputs = vec![ + (vec![col0.clone()], one.clone(), Direction::Send), + (vec![col0], one, Direction::Receive), + ]; + vec![LookupAir::register_lookup(self, Kind::Local, &lookup_inputs)] + } +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// Miden wrapper with N local lookups +// ═══════════════════════════════════════════════════════════════════════════════ + +/// Wraps the Miden degree-9 constraint and adds `num_lookups_target` local LogUp +/// lookups, each producing one EF permutation column. +#[derive(Clone)] +struct MidenWithLookups { + width: usize, + num_lookups_target: usize, + num_lookups: usize, +} + +impl BaseAir for MidenWithLookups { + fn width(&self) -> usize { + self.width + } +} + +impl Air for MidenWithLookups { + fn eval(&self, builder: &mut AB) { + // Same degree-9 constraint as DummyMidenAir. + let main = builder.main(); + let local = main.current_slice(); + let product = (0..9).fold(AB::Expr::ONE, |acc, j| acc * local[j].into()); + builder.assert_zero(product); + } +} + +impl LookupAir for MidenWithLookups { + fn add_lookup_columns(&mut self) -> Vec { + let idx = self.num_lookups; + self.num_lookups += 1; + vec![idx] + } + + fn get_lookups(&mut self) -> Vec> { + self.num_lookups = 0; + let symbolic = SymbolicAirBuilder::::new(AirLayout { + main_width: self.width, + ..AirLayout::default() + }); + let main = symbolic.main(); + let local = main.current_slice(); + let col0: SymbolicExpression = local[0].into(); + let one = SymbolicExpression::Leaf(BaseLeaf::Constant(F::ONE)); + let lookup_inputs = vec![ + (vec![col0.clone()], one.clone(), Direction::Send), + (vec![col0], one, Direction::Receive), + ]; + (0..self.num_lookups_target) + .map(|_| LookupAir::register_lookup(self, Kind::Local, &lookup_inputs)) + .collect() + } +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// Batch AIR enum +// ═══════════════════════════════════════════════════════════════════════════════ + +#[derive(Clone)] +enum BatchBenchAir { + Keccak(KeccakWithLookup), + Poseidon2(Box), + Blake3, + Miden(MidenWithLookups), +} + +impl BaseAir for BatchBenchAir { + fn width(&self) -> usize { + match self { + Self::Keccak(a) => BaseAir::::width(a), + Self::Poseidon2(_) => NUM_POSEIDON2_COLS, + Self::Blake3 => NUM_BLAKE3_COLS, + Self::Miden(a) => BaseAir::::width(a), + } + } +} + +impl> Air for BatchBenchAir { + fn eval(&self, builder: &mut AB) { + match self { + Self::Keccak(a) => Air::eval(a, builder), + Self::Poseidon2(a) => Air::eval(a.as_ref(), builder), + Self::Blake3 => Air::eval(&Blake3Air {}, builder), + Self::Miden(a) => Air::eval(a, builder), + } + } +} + +impl LookupAir for BatchBenchAir { + fn add_lookup_columns(&mut self) -> Vec { + match self { + Self::Keccak(a) => >::add_lookup_columns(a), + Self::Miden(a) => >::add_lookup_columns(a), + Self::Poseidon2(_) | Self::Blake3 => vec![], + } + } + + fn get_lookups(&mut self) -> Vec> { + match self { + Self::Keccak(a) => >::get_lookups(a), + Self::Miden(a) => >::get_lookups(a), + Self::Poseidon2(_) | Self::Blake3 => vec![], + } + } +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// Batch config macro +// ═══════════════════════════════════════════════════════════════════════════════ + +/// Build a `p3_uni_stark::StarkConfig` for batch-STARK from MMCS components. +/// +/// Parameterized over packed field/digest types and digest size, since these +/// differ per hash function and cannot be inferred from the constructor. +macro_rules! batch_config { + ( + $P:ty, + $PD:ty, + $DIGEST:expr, + $leaf:expr, + $compress:expr, + $challenger:expr, + $log_blowup:expr, + $cli:expr + ) => {{ + type Dft = p3_dft::Radix2DitParallel; + let mmcs: MerkleTreeMmcs<$P, $PD, _, _, 2, $DIGEST> = + MerkleTreeMmcs::new($leaf, $compress, 0); + let challenge_mmcs = ExtensionMmcs::::new(mmcs.clone()); + let fri_params = FriParameters { + log_blowup: $log_blowup as usize, + log_final_poly_len: $cli.log_final_degree as usize, + max_log_arity: $cli.log_folding_arity as usize, + num_queries: $cli.num_queries, + commit_proof_of_work_bits: $cli.folding_pow_bits, + query_proof_of_work_bits: $cli.query_pow_bits, + mmcs: challenge_mmcs, + }; + let pcs = TwoAdicFriPcs::new(Dft::default(), mmcs, fri_params); + p3_uni_stark::StarkConfig::new(pcs, $challenger) + }}; +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// Runner +// ═══════════════════════════════════════════════════════════════════════════════ + +pub(crate) fn run_batch( + config: &SC, + specs: &[TraceSpec], + traces: &[RowMajorMatrix], + constants: &Option, + cli: &Cli, +) -> RunResult +where + SC: p3_uni_stark::StarkGenericConfig, + >::Domain: + p3_commit::PolynomialSpace, +{ + let mut airs: Vec = specs + .iter() + .map(|spec| match spec.air_type { + AirType::Keccak => BatchBenchAir::Keccak(KeccakWithLookup { num_lookups: 0 }), + AirType::Poseidon2 => { + let c = constants.as_ref().expect("poseidon2 constants required"); + BatchBenchAir::Poseidon2(Box::new(BatchPoseidon2Air::new(c.clone()))) + }, + AirType::Blake3 => BatchBenchAir::Blake3, + AirType::Miden => BatchBenchAir::Miden(MidenWithLookups { + width: spec.width, + num_lookups_target: spec.num_aux_cols, + num_lookups: 0, + }), + }) + .collect(); + + let degree_bits: Vec = + traces.iter().map(|t| log2_strict_u8(t.height()) as usize).collect(); + let prover_data = ProverData::from_airs_and_degrees(config, &mut airs, °ree_bits); + let common = &prover_data.common; + + let trace_refs: Vec<&RowMajorMatrix> = traces.iter().collect(); + let pvs: Vec> = specs.iter().map(|_| vec![]).collect(); + + let instances = StarkInstance::new_multiple(&airs, &trace_refs, &pvs, common); + + let proof = info_span!("prove").in_scope(|| prove_batch(config, &instances, &prover_data)); + + let result = RunResult { + proof_size_bytes: postcard::to_allocvec(&proof).expect("serialization failed").len(), + field_elems: 0, + commitments: 0, + }; + + if !cli.no_verify { + info_span!("verify").in_scope(|| { + verify_batch(config, &airs, &proof, &pvs, common) + .expect("batch-stark verification failed"); + }); + } + + result +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// Batch config constructors (called from main) +// ═══════════════════════════════════════════════════════════════════════════════ + +pub(crate) fn run_batch_poseidon2( + specs: &[TraceSpec], + traces: &[RowMajorMatrix], + constants: &Option, + log_blowup: u8, + cli: &Cli, +) -> RunResult { + use miden_lifted_stark::testing::configs::goldilocks_poseidon2 as gl; + use p3_symmetric::PaddingFreeSponge; + + let (perm, _, compress) = gl::test_components(); + let leaf = PaddingFreeSponge::<_, { gl::WIDTH }, { gl::RATE }, { gl::DIGEST }>::new(perm); + let config = batch_config!( + gl::PackedFelt, + gl::PackedFelt, + { gl::DIGEST }, + leaf, + compress, + gl::test_challenger(), + log_blowup, + cli + ); + run_batch(&config, specs, traces, constants, cli) +} + +pub(crate) fn run_batch_keccak( + specs: &[TraceSpec], + traces: &[RowMajorMatrix], + constants: &Option, + log_blowup: u8, + cli: &Cli, +) -> RunResult { + use miden_lifted_stark::testing::configs::goldilocks_keccak as keccak; + use p3_keccak::KeccakF; + use p3_symmetric::{CompressionFunctionFromHasher, PaddingFreeSponge, SerializingHasher}; + + type U64Hash = PaddingFreeSponge; + let u64_hash = U64Hash::new(KeccakF); + let leaf = SerializingHasher::new(u64_hash); + let compress = CompressionFunctionFromHasher::::new(u64_hash); + let config = batch_config!( + [Felt; p3_keccak::VECTOR_LEN], + [u64; p3_keccak::VECTOR_LEN], + 4, + leaf, + compress, + keccak::test_challenger(), + log_blowup, + cli + ); + run_batch(&config, specs, traces, constants, cli) +} + +pub(crate) fn run_batch_blake3( + specs: &[TraceSpec], + traces: &[RowMajorMatrix], + constants: &Option, + log_blowup: u8, + cli: &Cli, +) -> RunResult { + use miden_lifted_stark::testing::configs::goldilocks_blake3 as blake3; + use p3_symmetric::{CompressionFunctionFromHasher, SerializingHasher}; + + let leaf = SerializingHasher::new(p3_blake3::Blake3); + let compress = CompressionFunctionFromHasher::::new( + p3_blake3::Blake3, + ); + let config = batch_config!( + Felt, + u8, + { blake3::DIGEST }, + leaf, + compress, + blake3::test_challenger(), + log_blowup, + cli + ); + run_batch(&config, specs, traces, constants, cli) +} + +pub(crate) fn run_batch_blake3_192( + specs: &[TraceSpec], + traces: &[RowMajorMatrix], + constants: &Option, + log_blowup: u8, + cli: &Cli, +) -> RunResult { + use miden_lifted_stark::testing::configs::goldilocks_blake3_192 as blake3_192; + use p3_symmetric::{CompressionFunctionFromHasher, SerializingHasher}; + + let h = blake3_192::Blake3_192::new(p3_blake3::Blake3); + let leaf = SerializingHasher::new(h); + let compress = + CompressionFunctionFromHasher::::new(h); + let config = batch_config!( + Felt, + u8, + { blake3_192::DIGEST }, + leaf, + compress, + blake3_192::test_challenger(), + log_blowup, + cli + ); + run_batch(&config, specs, traces, constants, cli) +} diff --git a/miden-bench/src/cli.rs b/miden-bench/src/cli.rs new file mode 100644 index 0000000000..80e65b91d1 --- /dev/null +++ b/miden-bench/src/cli.rs @@ -0,0 +1,206 @@ +//! Command-line interface types and parsing. + +use std::{fmt, str::FromStr}; + +use clap::{Parser, ValueEnum}; + +const DEFAULT_NUM_QUERIES: usize = 100; +const DEFAULT_POW_BITS: usize = 16; + +pub(crate) const DEFAULT_MIDEN_WIDTH: usize = 51; +pub(crate) const DEFAULT_MIDEN_AUX_COLS: usize = 8; + +/// Prove and verify a set of AIR instances with the lifted or batch STARK prover. +/// +/// Prints a configuration summary, proof size, and total time. +/// Pass -v for the full hierarchical tracing tree. +/// +/// Trace spec format: `AIR:LOG_HEIGHT[:WIDTH[:AUX_COLS]]` +/// +/// Available AIR types (with short aliases): +/// +/// keccak (k) Keccak-f(1600) permutation, 24 rows/hash +/// poseidon2 (p) Poseidon2 permutation (Goldilocks), 1 row/hash +/// blake3 (b) Blake3 compression, 1 row/hash +/// miden (m) Dummy degree-9 constraint (Miden VM shape) +/// +/// WIDTH and AUX_COLS only apply to `miden` (defaults: 51, 8). +/// +/// Examples: +/// +/// bench # default: blake3:15 keccak:18 poseidon2:19 +/// bench keccak:15 keccak:18 keccak:19 # 3x Keccak at different heights +/// bench -v keccak:15 # full tracing tree +/// bench miden:18:51 miden:19:20 # two Miden-shaped traces (auto blowup=3) +/// bench -m batch keccak:15 keccak:18 keccak:19 # batch-STARK comparison +/// bench -H keccak keccak:15 # use Keccak hash for commitments +/// bench -H blake3 keccak:15 # use BLAKE3 (32B) hash for commitments +/// bench -H blake3-192 keccak:15 # use BLAKE3-192 (24B) hash for commitments +/// bench --log-blowup 2 --num-queries 50 keccak:18 # override PCS parameters +#[derive(Parser)] +#[command(name = "bench", verbatim_doc_comment)] +pub(crate) struct Cli { + /// Trace specs (`AIR:LOG_HEIGHT[:WIDTH[:AUX_COLS]]`). + /// + /// When omitted, defaults to: blake3:15 keccak:18 poseidon2:19 + #[arg(value_name = "TRACE")] + pub(crate) traces: Vec, + + /// Prover backend: `lifted` (LMCS-based) or `batch` (Plonky3 batch-STARK). + #[arg(long, short = 'm', value_enum, default_value_t = Mode::Lifted)] + pub(crate) mode: Mode, + + /// Hash function for the commitment scheme. + /// + /// Only applies to lifted mode; batch mode always uses poseidon2. + #[arg(long, short = 'H', value_enum, default_value_t = HashFn::Poseidon2)] + pub(crate) hash: HashFn, + + /// Print the full hierarchical tracing tree (default: summary only). + /// + /// RUST_LOG overrides this when set. + #[arg(long, short = 'v')] + pub(crate) verbose: bool, + + /// RNG seed for reproducible trace generation. + #[arg(long, short = 's', default_value_t = 1)] + pub(crate) seed: u64, + + /// Skip proof verification (prover-only profiling). + #[arg(long)] + pub(crate) no_verify: bool, + + // ── PCS Parameters ────────────────────────────────────────────────── + /// Log₂ blowup factor for the LDE domain extension. + /// + /// Auto-detected when omitted: 1 for hash-only workloads, 3 when any + /// `miden` trace is present (degree-9 constraints need more blowup). + #[arg(long, help_heading = "PCS Parameters")] + pub(crate) log_blowup: Option, + + /// Number of FRI query repetitions (higher = more soundness). + #[arg(long, default_value_t = DEFAULT_NUM_QUERIES, help_heading = "PCS Parameters")] + pub(crate) num_queries: usize, + + /// Proof-of-work grinding bits for the DEEP challenge (lifted mode only). + #[arg(long, default_value_t = DEFAULT_POW_BITS, help_heading = "PCS Parameters")] + pub(crate) deep_pow_bits: usize, + + /// Log₂ FRI folding arity (1, 2, or 3 for fold-by-2/4/8). + #[arg(long, default_value_t = 2, help_heading = "PCS Parameters")] + pub(crate) log_folding_arity: u8, + + /// Log₂ final polynomial degree bound. + #[arg(long, default_value_t = 0, help_heading = "PCS Parameters")] + pub(crate) log_final_degree: u8, + + /// Proof-of-work grinding bits per FRI folding round. + #[arg(long, default_value_t = 0, help_heading = "PCS Parameters")] + pub(crate) folding_pow_bits: usize, + + /// Proof-of-work grinding bits before query index sampling. + #[arg(long, default_value_t = 0, help_heading = "PCS Parameters")] + pub(crate) query_pow_bits: usize, +} + +#[derive(Clone, Copy, ValueEnum)] +pub(crate) enum Mode { + Lifted, + Batch, +} + +#[derive(Clone, Copy, PartialEq, Eq, ValueEnum)] +pub(crate) enum HashFn { + Poseidon2, + Keccak, + Blake3, + #[value(name = "blake3-192")] + Blake3_192, +} + +impl fmt::Display for HashFn { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + HashFn::Poseidon2 => write!(f, "poseidon2"), + HashFn::Keccak => write!(f, "keccak"), + HashFn::Blake3 => write!(f, "blake3"), + HashFn::Blake3_192 => write!(f, "blake3-192"), + } + } +} + +// ─── Trace spec parsing ────────────────────────────────────────────────────── + +#[derive(Clone)] +pub(crate) struct TraceSpec { + pub(crate) air_type: AirType, + pub(crate) log_height: u8, + /// Main trace width (miden only). + pub(crate) width: usize, + /// Extension-field auxiliary columns (miden only). + pub(crate) num_aux_cols: usize, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum AirType { + Keccak, + Poseidon2, + Blake3, + Miden, +} + +impl fmt::Display for AirType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Keccak => write!(f, "keccak"), + Self::Poseidon2 => write!(f, "poseidon2"), + Self::Blake3 => write!(f, "blake3"), + Self::Miden => write!(f, "miden"), + } + } +} + +impl FromStr for TraceSpec { + type Err = String; + + fn from_str(s: &str) -> Result { + let parts: Vec<&str> = s.split(':').collect(); + if parts.len() < 2 { + return Err(format!("expected :[:[:]], got '{s}'")); + } + + let air_type = match parts[0] { + "keccak" | "k" => AirType::Keccak, + "poseidon2" | "p" => AirType::Poseidon2, + "blake3" | "b" => AirType::Blake3, + "miden" | "m" => AirType::Miden, + other => return Err(format!("unknown AIR type '{other}'")), + }; + + let log_height: u8 = + parts[1].parse().map_err(|_| format!("invalid log_height '{}'", parts[1]))?; + + let width = if parts.len() > 2 { + parts[2].parse().map_err(|_| format!("invalid width '{}'", parts[2]))? + } else { + DEFAULT_MIDEN_WIDTH + }; + + let num_aux_cols = if parts.len() > 3 { + parts[3].parse().map_err(|_| format!("invalid aux_cols '{}'", parts[3]))? + } else { + DEFAULT_MIDEN_AUX_COLS + }; + + if air_type == AirType::Miden && width < 9 { + return Err("miden width must be at least 9".to_string()); + } + + Ok(Self { + air_type, + log_height, + width, + num_aux_cols, + }) + } +} diff --git a/miden-bench/src/lifted.rs b/miden-bench/src/lifted.rs new file mode 100644 index 0000000000..77915221af --- /dev/null +++ b/miden-bench/src/lifted.rs @@ -0,0 +1,161 @@ +//! Lifted STARK AIR enum and prove/verify runner. + +use std::fmt; + +use miden_lifted_stark::{ + AirInstance, AirWitness, StarkConfig, + air::{BaseAir, LiftedAir, LiftedAirBuilder}, + prove_multi, + testing::airs::{ + ZeroAuxBuilder, blake3::LiftedBlake3Air, keccak::LiftedKeccakAir, miden::DummyMidenAir, + poseidon2::LiftedPoseidon2Air, + }, + verify_multi, +}; +use p3_field::Field; +use p3_matrix::dense::RowMajorMatrix; +use tracing::info_span; + +use crate::{ + Felt, GlRoundConstants, QuadFelt, RunResult, + cli::{AirType, Cli, TraceSpec}, +}; + +// ═══════════════════════════════════════════════════════════════════════════════ +// AIR enum +// ═══════════════════════════════════════════════════════════════════════════════ + +pub(crate) enum LiftedBenchAir { + Keccak(LiftedKeccakAir), + Poseidon2(Box), + Blake3(LiftedBlake3Air), + Miden(DummyMidenAir), +} + +impl BaseAir for LiftedBenchAir { + fn width(&self) -> usize { + match self { + Self::Keccak(a) => BaseAir::::width(a), + Self::Poseidon2(a) => BaseAir::::width(a.as_ref()), + Self::Blake3(a) => BaseAir::::width(a), + Self::Miden(a) => BaseAir::::width(a), + } + } +} + +impl LiftedAir for LiftedBenchAir { + fn num_randomness(&self) -> usize { + match self { + Self::Miden(a) => LiftedAir::::num_randomness(a), + _ => 1, + } + } + + fn aux_width(&self) -> usize { + match self { + Self::Miden(a) => LiftedAir::::aux_width(a), + _ => 1, + } + } + + fn num_aux_values(&self) -> usize { + match self { + Self::Miden(a) => LiftedAir::::num_aux_values(a), + _ => 0, + } + } + + fn num_var_len_public_inputs(&self) -> usize { + 0 + } + + fn eval>(&self, builder: &mut AB) { + match self { + Self::Keccak(a) => LiftedAir::::eval(a, builder), + Self::Poseidon2(a) => LiftedAir::::eval(a.as_ref(), builder), + Self::Blake3(a) => LiftedAir::::eval(a, builder), + Self::Miden(a) => LiftedAir::::eval(a, builder), + } + } +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// Runner +// ═══════════════════════════════════════════════════════════════════════════════ + +pub(crate) fn run_lifted( + config: &SC, + specs: &[TraceSpec], + traces: &[RowMajorMatrix], + constants: &Option, + cli: &Cli, +) -> RunResult +where + SC: StarkConfig, + miden_lifted_stark::StarkDigest: PartialEq + fmt::Debug, +{ + let airs: Vec = specs + .iter() + .map(|spec| match spec.air_type { + AirType::Keccak => LiftedBenchAir::Keccak(LiftedKeccakAir), + AirType::Poseidon2 => { + let c = constants.as_ref().expect("poseidon2 constants required"); + LiftedBenchAir::Poseidon2(Box::new(LiftedPoseidon2Air::new(c.clone()))) + }, + AirType::Blake3 => LiftedBenchAir::Blake3(LiftedBlake3Air), + AirType::Miden => { + LiftedBenchAir::Miden(DummyMidenAir::new(spec.width, spec.num_aux_cols)) + }, + }) + .collect(); + + let aux_builders: Vec = specs + .iter() + .map(|spec| match spec.air_type { + AirType::Miden => ZeroAuxBuilder { + num_aux_cols: spec.num_aux_cols, + num_aux_values: spec.num_aux_cols, + }, + _ => ZeroAuxBuilder::dummy(), + }) + .collect(); + + let instances: Vec<_> = airs + .iter() + .zip(traces) + .zip(&aux_builders) + .map(|((air, trace), aux)| (air, AirWitness::new(trace, &[], &[]), aux)) + .collect(); + + let output = info_span!("prove") + .in_scope(|| prove_multi(config, &instances, config.challenger()).expect("proving failed")); + + let result = RunResult { + proof_size_bytes: output.proof.size_in_bytes(), + field_elems: output.proof.num_field_elements(), + commitments: output.proof.num_commitments(), + }; + + if !cli.no_verify { + info_span!("verify").in_scope(|| { + let verifier_instances: Vec<_> = airs + .iter() + .map(|air| { + ( + air, + AirInstance { + public_values: &[], + var_len_public_inputs: &[], + }, + ) + }) + .collect(); + let digest = + verify_multi(config, &verifier_instances, &output.proof, config.challenger()) + .expect("verification failed"); + assert_eq!(output.digest, digest); + }); + } + + result +} diff --git a/miden-bench/src/main.rs b/miden-bench/src/main.rs new file mode 100644 index 0000000000..94c0f55d6a --- /dev/null +++ b/miden-bench/src/main.rs @@ -0,0 +1,335 @@ +//! Unified STARK prove/verify profiling binary. +//! +//! Proves and verifies a set of AIR instances, printing a configuration summary, +//! proof size, and total time. Trace specifications are passed as positional +//! arguments; all parameters have sensible defaults. +//! +//! By default the output is concise (config header + proof size + total time). +//! Pass `-v` for the full hierarchical tracing tree, or set `RUST_LOG` for +//! fine-grained control. +//! +//! ```bash +//! # Quick run with defaults (blake3:15 keccak:18 poseidon2:19): +//! cargo run -p miden-bench --features parallel --release +//! +//! # Custom traces: +//! cargo run -p miden-bench --features parallel --release -- keccak:15 keccak:18 keccak:19 +//! +//! # Full tracing tree: +//! cargo run -p miden-bench --features parallel --release -- -v keccak:15 +//! +//! # Multi-iteration with warm-up (reports min/median/mean/max): +//! cargo run -p miden-bench --features parallel --release -- -n 5 keccak:15 +//! +//! # Miden-shaped AIR (auto log_blowup=3): +//! cargo run -p miden-bench --features parallel --release -- miden:18:51 miden:19:20 +//! +//! # Batch-STARK comparison: +//! cargo run -p miden-bench --features parallel --release -- -m batch keccak:15 keccak:18 +//! ``` + +mod batch; +mod cli; +mod lifted; + +use std::{fmt, time::Instant}; + +use clap::Parser; +use miden_lifted_stark::{ + GenericStarkConfig, PcsParams, + testing::{ + airs::{ + blake3::generate_blake3_trace, + keccak::generate_keccak_trace, + miden::generate_dummy_trace, + poseidon2::{HALF_FULL_ROUNDS, PARTIAL_ROUNDS, WIDTH, generate_poseidon2_trace}, + }, + configs::{ + Felt, QuadFelt, goldilocks_blake3 as blake3, goldilocks_blake3_192 as blake3_192, + goldilocks_keccak as keccak, goldilocks_poseidon2 as gl, + }, + }, +}; +use p3_goldilocks::GenericPoseidon2LinearLayersGoldilocks; +use p3_matrix::{Matrix, dense::RowMajorMatrix}; +use p3_poseidon2_air::RoundConstants; +use rand::{RngExt, SeedableRng, rngs::SmallRng}; +use tracing::info_span; +use tracing_subscriber::{Layer, Registry, layer::SubscriberExt, util::SubscriberInitExt}; + +use crate::cli::{AirType, Cli, HashFn, Mode, TraceSpec}; + +// ─── Type aliases ───────────────────────────────────────────────────────────��� + +type Gl = p3_goldilocks::Goldilocks; +type GlRoundConstants = RoundConstants; + +type BatchPoseidon2Air = p3_poseidon2_air::Poseidon2Air< + Felt, + GenericPoseidon2LinearLayersGoldilocks, + WIDTH, + { miden_lifted_stark::testing::airs::poseidon2::SBOX_DEGREE }, + { miden_lifted_stark::testing::airs::poseidon2::SBOX_REGISTERS }, + HALF_FULL_ROUNDS, + PARTIAL_ROUNDS, +>; + +const KECCAK_ROWS_PER_HASH: usize = 24; + +// ════════════════════════════════════════════════════════════════════════════��══ +// Run result +// ═══════════════════════════════════════════════════════════════════════════════ + +/// Captured output from a single prove/verify invocation. +pub(crate) struct RunResult { + pub(crate) proof_size_bytes: usize, + /// Number of field elements in the proof (lifted only, 0 for batch). + pub(crate) field_elems: usize, + /// Number of commitments in the proof (lifted only, 0 for batch). + pub(crate) commitments: usize, +} + +impl fmt::Display for RunResult { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "proof size: {}", format_bytes(self.proof_size_bytes))?; + if self.field_elems > 0 || self.commitments > 0 { + write!(f, " ({} field elems, {} commitments)", self.field_elems, self.commitments,)?; + } + Ok(()) + } +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// Configuration summary +// ═══════════════════════════════════════════════════════════════════════════════ + +fn print_config(cli: &Cli, specs: &[TraceSpec], traces: &[RowMajorMatrix], log_blowup: u8) { + let mode = match cli.mode { + Mode::Lifted => "lifted", + Mode::Batch => "batch", + }; + eprintln!("{:<20} {mode}", "mode:"); + eprintln!("{:<20} {}", "hash:", cli.hash); + eprintln!("{:<20} {}", "seed:", cli.seed); + for (i, (spec, trace)) in specs.iter().zip(traces).enumerate() { + let label = if i == 0 { "traces:" } else { "" }; + eprintln!( + "{:<20} {}:{} (width={}, 2^{} = {} rows)", + label, + spec.air_type, + spec.log_height, + trace.width(), + spec.log_height, + trace.height(), + ); + } + eprintln!("{:<20} {}", "log_blowup:", log_blowup); + eprintln!("{:<20} {}", "log_folding_arity:", cli.log_folding_arity); + eprintln!("{:<20} {}", "log_final_degree:", cli.log_final_degree); + eprintln!("{:<20} {}", "num_queries:", cli.num_queries); + eprintln!("{:<20} {}", "deep_pow_bits:", cli.deep_pow_bits); + eprintln!("{:<20} {}", "folding_pow_bits:", cli.folding_pow_bits); + eprintln!("{:<20} {}", "query_pow_bits:", cli.query_pow_bits); + eprintln!(); +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// Trace generation (shared between modes) +// ═══════════════════════════════════════════════════════════════════════════════ + +fn generate_traces( + specs: &[TraceSpec], + rng: &mut SmallRng, + constants: Option<&GlRoundConstants>, +) -> Vec> { + specs + .iter() + .map(|spec| { + info_span!("generate trace", air = %spec.air_type, log_height = spec.log_height) + .in_scope(|| match spec.air_type { + AirType::Keccak => { + let n = (1usize << spec.log_height) / KECCAK_ROWS_PER_HASH; + let inputs: Vec<[u64; 25]> = (0..n).map(|_| rng.random()).collect(); + generate_keccak_trace(inputs) + }, + AirType::Poseidon2 => { + let n = 1usize << spec.log_height; + let inputs: Vec<[Felt; 12]> = (0..n).map(|_| rng.random()).collect(); + generate_poseidon2_trace( + inputs, + constants.expect("poseidon2 constants required"), + ) + }, + AirType::Blake3 => { + let n = 1usize << spec.log_height; + let inputs: Vec<[u32; 24]> = (0..n).map(|_| rng.random()).collect(); + generate_blake3_trace(inputs) + }, + AirType::Miden => generate_dummy_trace(spec.width, spec.log_height, rng), + }) + }) + .collect() +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// Main +// ═══════════════════════════════════════════════════════════════════════════════ + +fn main() { + let cli = Cli::parse(); + + // Apply defaults. + let mut specs = if cli.traces.is_empty() { + vec![ + TraceSpec { + air_type: AirType::Blake3, + log_height: 15, + width: 0, + num_aux_cols: 0, + }, + TraceSpec { + air_type: AirType::Keccak, + log_height: 18, + width: 0, + num_aux_cols: 0, + }, + TraceSpec { + air_type: AirType::Poseidon2, + log_height: 19, + width: 0, + num_aux_cols: 0, + }, + ] + } else { + cli.traces.clone() + }; + + // Sort by ascending height for deterministic output. + specs.sort_by_key(|s| s.log_height); + + let has_miden = specs.iter().any(|s| s.air_type == AirType::Miden); + let log_blowup = cli.log_blowup.unwrap_or(if has_miden { 3 } else { 1 }); + + // Set up tracing subscriber (quiet by default, -v for full tree). + init_tracing(cli.verbose); + + // Generate Poseidon2 round constants (from RNG, before trace inputs). + let mut rng = SmallRng::seed_from_u64(cli.seed); + let poseidon2_constants: Option = + if specs.iter().any(|s| s.air_type == AirType::Poseidon2) { + Some(RoundConstants::from_rng(&mut rng)) + } else { + None + }; + + // Generate traces. + let traces = generate_traces(&specs, &mut rng, poseidon2_constants.as_ref()); + + // Print configuration summary. + print_config(&cli, &specs, &traces, log_blowup); + + // Build PCS params (shared across hash functions). + let pcs = PcsParams::new( + log_blowup, + cli.log_folding_arity, + cli.log_final_degree, + cli.folding_pow_bits, + cli.deep_pow_bits, + cli.num_queries, + cli.query_pow_bits, + ) + .expect("invalid PCS params"); + + type Dft = p3_dft::Radix2DitParallel; + + // Run prove/verify. + let start = Instant::now(); + let result = match cli.mode { + Mode::Lifted => match cli.hash { + HashFn::Poseidon2 => { + let config = GenericStarkConfig::new( + pcs, + gl::test_lmcs(), + Dft::default(), + gl::test_challenger(), + ); + lifted::run_lifted(&config, &specs, &traces, &poseidon2_constants, &cli) + }, + HashFn::Keccak => { + let config = GenericStarkConfig::new( + pcs, + keccak::test_lmcs(), + Dft::default(), + keccak::test_challenger(), + ); + lifted::run_lifted(&config, &specs, &traces, &poseidon2_constants, &cli) + }, + HashFn::Blake3 => { + let config = GenericStarkConfig::new( + pcs, + blake3::test_lmcs(), + Dft::default(), + blake3::test_challenger(), + ); + lifted::run_lifted(&config, &specs, &traces, &poseidon2_constants, &cli) + }, + HashFn::Blake3_192 => { + let config = GenericStarkConfig::new( + pcs, + blake3_192::test_lmcs(), + Dft::default(), + blake3_192::test_challenger(), + ); + lifted::run_lifted(&config, &specs, &traces, &poseidon2_constants, &cli) + }, + }, + Mode::Batch => match cli.hash { + HashFn::Poseidon2 => { + batch::run_batch_poseidon2(&specs, &traces, &poseidon2_constants, log_blowup, &cli) + }, + HashFn::Keccak => { + batch::run_batch_keccak(&specs, &traces, &poseidon2_constants, log_blowup, &cli) + }, + HashFn::Blake3 => { + batch::run_batch_blake3(&specs, &traces, &poseidon2_constants, log_blowup, &cli) + }, + HashFn::Blake3_192 => { + batch::run_batch_blake3_192(&specs, &traces, &poseidon2_constants, log_blowup, &cli) + }, + }, + }; + let elapsed = start.elapsed(); + + println!("{result}"); + println!("total time: {:.3} s", elapsed.as_secs_f64()); +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// Helpers +// ═══════════════════════════════════════════════════════════════════════════════ + +fn init_tracing(verbose: bool) { + let default_level = if verbose { + tracing_forest::util::LevelFilter::DEBUG + } else { + tracing_forest::util::LevelFilter::WARN + }; + + let env_filter = tracing_subscriber::EnvFilter::builder() + .with_default_directive(default_level.into()) + .from_env_lossy(); + + Registry::default() + .with(tracing_forest::ForestLayer::default().with_filter(env_filter)) + .init(); +} + +pub(crate) fn format_bytes(bytes: usize) -> String { + if bytes < 1024 { + format!("{bytes} B") + } else if bytes < 1024 * 1024 { + format!("{:.1} KiB", bytes as f64 / 1024.0) + } else { + format!("{:.2} MiB", bytes as f64 / (1024.0 * 1024.0)) + } +} diff --git a/miden-crypto-derive/Cargo.toml b/miden-crypto-derive/Cargo.toml index e902f7b9f2..b31cecd6f8 100644 --- a/miden-crypto-derive/Cargo.toml +++ b/miden-crypto-derive/Cargo.toml @@ -12,11 +12,13 @@ rust-version.workspace = true version.workspace = true [lib] +doctest = false proc-macro = true +test = false [dependencies] -quote = "1.0" -syn = { features = ["full"], version = "2.0" } +quote = { workspace = true } +syn = { features = ["full"], workspace = true } [lints] workspace = true diff --git a/miden-crypto-fuzz/Cargo.lock b/miden-crypto-fuzz/Cargo.lock index 5bd2cafeb9..85b4b43859 100644 --- a/miden-crypto-fuzz/Cargo.lock +++ b/miden-crypto-fuzz/Cargo.lock @@ -150,6 +150,12 @@ dependencies = [ "libc", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -399,12 +405,6 @@ dependencies = [ "wasip2", ] -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - [[package]] name = "group" version = "0.13.0" @@ -528,7 +528,7 @@ dependencies = [ [[package]] name = "miden-crypto" -version = "0.23.0" +version = "0.24.0" dependencies = [ "blake3", "cc", @@ -536,31 +536,25 @@ dependencies = [ "curve25519-dalek", "ed25519-dalek", "flume", - "glob", "hkdf", "k256", "miden-crypto-derive", "miden-field", + "miden-lifted-stark", "miden-serde-utils", "num", "num-complex", - "p3-air", + "once_cell", "p3-blake3", "p3-challenger", - "p3-commit", "p3-dft", - "p3-field", "p3-goldilocks", "p3-keccak", "p3-matrix", "p3-maybe-rayon", - "p3-merkle-tree", - "p3-miden-air", - "p3-miden-fri", - "p3-miden-prover", "p3-symmetric", "p3-util", - "rand", + "rand 0.9.3", "rand_chacha", "rand_core 0.9.3", "rand_hc", @@ -575,7 +569,7 @@ dependencies = [ [[package]] name = "miden-crypto-derive" -version = "0.23.0" +version = "0.24.0" dependencies = [ "quote", "syn", @@ -587,33 +581,84 @@ version = "0.0.0" dependencies = [ "libfuzzer-sys", "miden-crypto", - "rand", + "rand 0.9.3", ] [[package]] name = "miden-field" -version = "0.23.0" +version = "0.24.0" dependencies = [ "miden-serde-utils", "num-bigint", "p3-challenger", "p3-field", "p3-goldilocks", + "p3-util", "paste", - "rand", + "rand 0.10.1", "serde", "subtle", "thiserror", ] +[[package]] +name = "miden-lifted-air" +version = "0.24.0" +dependencies = [ + "p3-air", + "p3-field", + "p3-matrix", + "p3-util", + "thiserror", +] + +[[package]] +name = "miden-lifted-stark" +version = "0.24.0" +dependencies = [ + "miden-lifted-air", + "miden-stark-transcript", + "miden-stateful-hasher", + "p3-challenger", + "p3-dft", + "p3-field", + "p3-goldilocks", + "p3-matrix", + "p3-maybe-rayon", + "p3-symmetric", + "p3-util", + "rand 0.10.1", + "serde", + "thiserror", + "tracing", +] + [[package]] name = "miden-serde-utils" -version = "0.23.0" +version = "0.24.0" dependencies = [ "p3-field", "p3-goldilocks", ] +[[package]] +name = "miden-stark-transcript" +version = "0.24.0" +dependencies = [ + "p3-challenger", + "p3-field", + "serde", + "thiserror", +] + +[[package]] +name = "miden-stateful-hasher" +version = "0.24.0" +dependencies = [ + "p3-field", + "p3-symmetric", +] + [[package]] name = "nanorand" version = "0.7.0" @@ -702,6 +747,10 @@ name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "critical-section", + "portable-atomic", +] [[package]] name = "opaque-debug" @@ -711,19 +760,20 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "p3-air" -version = "0.4.2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0141a56ed9924ce0265e7e91cd29bbcd230262744b7a7f0c448bfbf212f73182" +checksum = "9f2ec9cbfc642fc5173817287c3f8b789d07743b5f7e812d058b7a03e344f9ab" dependencies = [ "p3-field", "p3-matrix", + "tracing", ] [[package]] name = "p3-blake3" -version = "0.4.2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "006330bae15fdda0d460e73e03e7ebf06e8848dfda8355f9d568a7fed7c37719" +checksum = "b667f43b19499dd939c9e2553aa95688936a88360d50117dae3c8848d07dbc70" dependencies = [ "blake3", "p3-symmetric", @@ -732,9 +782,9 @@ dependencies = [ [[package]] name = "p3-challenger" -version = "0.4.2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20e42ba74a49c08c6e99f74cd9b343bfa31aa5721fea55079b18e3fd65f1dcbc" +checksum = "4a0b490c745a7d2adeeafff06411814c8078c432740162332b3cd71be0158a76" dependencies = [ "p3-field", "p3-maybe-rayon", @@ -744,26 +794,11 @@ dependencies = [ "tracing", ] -[[package]] -name = "p3-commit" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "498211e7b9a0f8366b410b4a9283ae82ff2fc91f473b1c5816aa6e90e74b125d" -dependencies = [ - "itertools", - "p3-challenger", - "p3-dft", - "p3-field", - "p3-matrix", - "p3-util", - "serde", -] - [[package]] name = "p3-dft" -version = "0.4.2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e63fa5eb1bd12a240089e72ae3fe10350944d9c166d00a3bfd2a1794db65cf5c" +checksum = "55301e91544440254977108b85c32c09d7ea05f2f0dd61092a2825339906a4a7" dependencies = [ "itertools", "p3-field", @@ -776,58 +811,46 @@ dependencies = [ [[package]] name = "p3-field" -version = "0.4.2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ebfdb6ef992ae64e9e8f449ac46516ffa584f11afbdf9ee244288c2a633cdf4" +checksum = "85affca7fc983889f260655c4cf74163eebb94605f702e4b6809ead707cba54f" dependencies = [ "itertools", "num-bigint", "p3-maybe-rayon", "p3-util", "paste", - "rand", + "rand 0.10.1", "serde", "tracing", ] [[package]] name = "p3-goldilocks" -version = "0.4.2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64716244b5612622d4e78a4f48b74f6d3bb7b4085b7b6b25364b1dfca7198c66" +checksum = "0ca1081f5c47b940f2d75a11c04f62ea1cc58a5d480dd465fef3861c045c63cd" dependencies = [ "num-bigint", "p3-challenger", "p3-dft", "p3-field", "p3-mds", + "p3-poseidon1", "p3-poseidon2", "p3-symmetric", "p3-util", "paste", - "rand", + "rand 0.10.1", "serde", ] -[[package]] -name = "p3-interpolation" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d877565a94a527c89459fc8ccb0eb58769d8c86456575d1315a1651bd24616d" -dependencies = [ - "p3-field", - "p3-matrix", - "p3-maybe-rayon", - "p3-util", -] - [[package]] name = "p3-keccak" -version = "0.4.2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d57334537d10316e0f1cda622f0a5b3239f219a5dcd2a95ea87e41e00df6a92" +checksum = "ebcf27615ece1995e4fcf4c69740f1cf515d1481367a20b4b3ce7f4f1b8d70f7" dependencies = [ - "p3-field", "p3-symmetric", "p3-util", "tiny-keccak", @@ -835,139 +858,46 @@ dependencies = [ [[package]] name = "p3-matrix" -version = "0.4.2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5542f96504dae8100c91398fb1e3f5ec669eb9c73d9e0b018a93b5fe32bad230" +checksum = "53428126b009071563d1d07305a9de8be0d21de00b57d2475289ee32ffca6577" dependencies = [ "itertools", "p3-field", "p3-maybe-rayon", "p3-util", - "rand", + "rand 0.10.1", "serde", "tracing", - "transpose", ] [[package]] name = "p3-maybe-rayon" -version = "0.4.2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e5669ca75645f99cd001e9d0289a4eeff2bc2cd9dc3c6c3aaf22643966e83df" +checksum = "082bf467011c06c768c579ec6eb9accb5e1e62108891634cc770396e917f978a" dependencies = [ "rayon", ] [[package]] name = "p3-mds" -version = "0.4.2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038763af23df9da653065867fd85b38626079031576c86fd537097e5be6a0da0" +checksum = "35209e6214102ea6ec6b8cb1b9c15a9b8e597a39f9173597c957f123bced81b3" dependencies = [ "p3-dft", "p3-field", "p3-symmetric", "p3-util", - "rand", -] - -[[package]] -name = "p3-merkle-tree" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d93625a3041effddc72ee2511c919f710b7f91fd0f9931ab8a70aeba586fd6e" -dependencies = [ - "itertools", - "p3-commit", - "p3-field", - "p3-matrix", - "p3-maybe-rayon", - "p3-symmetric", - "p3-util", - "rand", - "serde", - "thiserror", - "tracing", -] - -[[package]] -name = "p3-miden-air" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a88e6ee9c92ff6c0b64f1ec0d61eda72fb432bda45337d876c46bd43748508" -dependencies = [ - "p3-air", - "p3-field", - "p3-matrix", -] - -[[package]] -name = "p3-miden-fri" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e282998bc1d12dceaa0ed8979fa507b8369d663fa377da695d578f5f3a035935" -dependencies = [ - "itertools", - "p3-challenger", - "p3-commit", - "p3-dft", - "p3-field", - "p3-interpolation", - "p3-matrix", - "p3-maybe-rayon", - "p3-util", - "rand", - "serde", - "tracing", -] - -[[package]] -name = "p3-miden-prover" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05a61c10cc2d6a73e192ac34a9884e4f26bd877f3eaea441d7b7ebfdffdf6c7" -dependencies = [ - "itertools", - "p3-challenger", - "p3-commit", - "p3-dft", - "p3-field", - "p3-interpolation", - "p3-matrix", - "p3-maybe-rayon", - "p3-miden-air", - "p3-miden-uni-stark", - "p3-util", - "serde", - "tracing", -] - -[[package]] -name = "p3-miden-uni-stark" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a78b6a5b5f6bdc55439d343d2a0a2a8e7cb6544b03296f54d2214a84e91e130" -dependencies = [ - "itertools", - "p3-air", - "p3-challenger", - "p3-commit", - "p3-dft", - "p3-field", - "p3-matrix", - "p3-maybe-rayon", - "p3-uni-stark", - "p3-util", - "serde", - "thiserror", - "tracing", + "rand 0.10.1", ] [[package]] name = "p3-monty-31" -version = "0.4.2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a981d60da3d8cbf8561014e2c186068578405fd69098fa75b43d4afb364a47" +checksum = "ffa8c99ec50c035020bbf5457c6a729ba6a975719c1a8dd3f16421081e4f650c" dependencies = [ "itertools", "num-bigint", @@ -976,69 +906,62 @@ dependencies = [ "p3-matrix", "p3-maybe-rayon", "p3-mds", + "p3-poseidon1", "p3-poseidon2", "p3-symmetric", "p3-util", "paste", - "rand", + "rand 0.10.1", "serde", "spin 0.10.0", "tracing", - "transpose", ] [[package]] -name = "p3-poseidon2" -version = "0.4.2" +name = "p3-poseidon1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "903b73e4f9a7781a18561c74dc169cf03333497b57a8dd02aaeb130c0f386599" +checksum = "6a018b618e3fa0aec8be933b1d8e404edd23f46991f6bf3f5c2f3f95e9413fe9" dependencies = [ "p3-field", - "p3-mds", "p3-symmetric", - "p3-util", - "rand", + "rand 0.10.1", ] [[package]] -name = "p3-symmetric" -version = "0.4.2" +name = "p3-poseidon2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd788f04e86dd5c35dd87cad29eefdb6371d2fd5f7664451382eeacae3c3ed0" +checksum = "256a668a9ba916f8767552f13d0ba50d18968bc74a623bfdafa41e2970c944d0" dependencies = [ - "itertools", "p3-field", - "serde", + "p3-mds", + "p3-symmetric", + "p3-util", + "rand 0.10.1", ] [[package]] -name = "p3-uni-stark" -version = "0.4.2" +name = "p3-symmetric" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68d409704a8cbdb6c77f6b83a05c6b16a3c8a2c00d880146fa34181977a0d3ac" +checksum = "6c60a71a1507c13611b0f2b0b6e83669fd5b76f8e3115bcbced5ccfdf3ca7807" dependencies = [ "itertools", - "p3-air", - "p3-challenger", - "p3-commit", - "p3-dft", "p3-field", - "p3-matrix", - "p3-maybe-rayon", "p3-util", "serde", - "thiserror", - "tracing", ] [[package]] name = "p3-util" -version = "0.4.2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "663b16021930bc600ecada915c6c3965730a3b9d6a6c23434ccf70bfc29d6881" +checksum = "f8b766b9e9254bf3fa98d76e42cf8a5b30628c182dfd5272d270076ee12f0fc0" dependencies = [ "rayon", "serde", + "transpose", ] [[package]] @@ -1074,6 +997,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1109,14 +1038,23 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "7ec095654a25171c2124e9e3393a930bddbffdc939556c914957a4c3e0a87166" dependencies = [ "rand_chacha", "rand_core 0.9.3", ] +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "rand_core 0.10.0", +] + [[package]] name = "rand_chacha" version = "0.9.0" @@ -1145,6 +1083,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + [[package]] name = "rand_hc" version = "0.3.2" diff --git a/miden-crypto-fuzz/fuzz_targets/smt.rs b/miden-crypto-fuzz/fuzz_targets/smt.rs index f48b70de92..b62439c2cd 100644 --- a/miden-crypto-fuzz/fuzz_targets/smt.rs +++ b/miden-crypto-fuzz/fuzz_targets/smt.rs @@ -1,7 +1,7 @@ #![no_main] use libfuzzer_sys::fuzz_target; -use miden_crypto::{Felt, ONE, Word, merkle::smt::Smt}; +use miden_crypto::{merkle::smt::Smt, Felt, Word, ONE}; use rand::Rng; // Needed for randomizing the split percentage struct FuzzInput { @@ -29,14 +29,18 @@ impl FuzzInput { for chunk in data.chunks_exact(40).take(num_entries) { let key = Word::new([ - Felt::new(u64::from_le_bytes(chunk[0..8].try_into().unwrap())), - Felt::new(u64::from_le_bytes(chunk[8..16].try_into().unwrap())), - Felt::new(u64::from_le_bytes(chunk[16..24].try_into().unwrap())), - Felt::new(u64::from_le_bytes(chunk[24..32].try_into().unwrap())), + Felt::new_unchecked(u64::from_le_bytes(chunk[0..8].try_into().unwrap())), + Felt::new_unchecked(u64::from_le_bytes(chunk[8..16].try_into().unwrap())), + Felt::new_unchecked(u64::from_le_bytes(chunk[16..24].try_into().unwrap())), + Felt::new_unchecked(u64::from_le_bytes(chunk[24..32].try_into().unwrap())), ]); - let value = - [ONE, ONE, ONE, Felt::new(u64::from_le_bytes(chunk[32..40].try_into().unwrap()))] - .into(); + let value = [ + ONE, + ONE, + ONE, + Felt::new_unchecked(u64::from_le_bytes(chunk[32..40].try_into().unwrap())), + ] + .into(); entries.push((key, value)); } diff --git a/miden-crypto/Cargo.toml b/miden-crypto/Cargo.toml index 8c1402ffe4..141470a49e 100644 --- a/miden-crypto/Cargo.toml +++ b/miden-crypto/Cargo.toml @@ -80,9 +80,9 @@ name = "transpose" required-features = ["std"] [features] -concurrent = ["dep:rayon", "p3-maybe-rayon/parallel", "p3-miden-lifted-stark/parallel", "p3-util/parallel", "std"] +concurrent = ["dep:rayon", "miden-lifted-stark/parallel", "p3-maybe-rayon/parallel", "p3-util/parallel", "std"] default = ["concurrent", "std"] -executable = ["concurrent", "dep:clap", "dep:rand-utils"] +executable = ["concurrent", "dep:clap"] fuzzing = [] internal = ["concurrent"] persistent-forest = ["rocksdb", "serde"] @@ -93,72 +93,64 @@ testing = ["dep:proptest", "miden-field/testing"] [dependencies] # Miden dependencies -miden-crypto-derive.workspace = true -miden-field.workspace = true -miden-serde-utils.workspace = true +miden-crypto-derive = { workspace = true } +miden-field = { workspace = true } +miden-lifted-stark = { default-features = false, workspace = true } +miden-serde-utils = { workspace = true } # External dependencies -blake3 = { default-features = false, version = "1.8" } -chacha20poly1305 = { features = ["alloc", "stream"], version = "0.10" } -clap = { features = ["derive"], optional = true, version = "4.5" } -curve25519-dalek = { default-features = false, version = "4" } -ed25519-dalek = { features = ["zeroize"], version = "2" } -flume = { version = "0.11.1" } -hkdf = { default-features = false, version = "0.12" } -k256 = { features = ["ecdh", "ecdsa", "pkcs8"], version = "0.13" } -num = { default-features = false, features = ["alloc", "libm"], version = "0.4" } -num-complex = { default-features = false, version = "0.4" } -proptest = { default-features = false, features = ["alloc"], optional = true, version = "1.7" } +blake3 = { workspace = true } +chacha20poly1305 = { features = ["alloc", "stream"], workspace = true } +clap = { features = ["derive"], optional = true, workspace = true } +curve25519-dalek = { workspace = true } +ed25519-dalek = { features = ["zeroize"], workspace = true } +flume = { workspace = true } +hkdf = { workspace = true } +k256 = { features = ["ecdh", "ecdsa", "pkcs8"], workspace = true } +num = { features = ["alloc", "libm"], workspace = true } +num-complex = { workspace = true } +once_cell = { features = ["alloc", "critical-section"], workspace = true } +proptest = { features = ["alloc"], optional = true, workspace = true } rand = { default-features = false, version = "0.9" } -rand-utils = { optional = true, package = "winter-rand-utils", version = "0.13" } -rand_chacha = { default-features = false, version = "0.9" } -rand_core = { default-features = false, version = "0.9" } -rand_hc = { version = "0.3" } -rayon = { optional = true, version = "1.10" } -rocksdb = { default-features = false, features = ["bindgen-runtime", "lz4"], optional = true, version = "0.24" } -serde = { default-features = false, features = ["derive"], optional = true, version = "1.0" } -sha2 = { default-features = false, version = "0.10" } -sha3 = { default-features = false, version = "0.10" } -subtle = { default-features = false, version = "2.6" } -thiserror = { default-features = false, version = "2.0" } -x25519-dalek = { default-features = false, features = ["static_secrets"], version = "2.0" } - -# Upstream Plonky3 dependencies (matching p3-miden's published 0.5.0) -p3-blake3 = { default-features = false, version = "0.5" } -p3-challenger = { default-features = false, version = "0.5" } -p3-dft = { default-features = false, version = "0.5" } -p3-goldilocks = { default-features = false, version = "0.5" } -p3-keccak = { default-features = false, version = "0.5" } -p3-matrix = { default-features = false, version = "0.5" } -p3-maybe-rayon = { default-features = false, version = "0.5" } -p3-symmetric = { default-features = false, version = "0.5" } -p3-util = { default-features = false, version = "0.5" } - -# Miden-specific Plonky3 crates -p3-miden-lifted-stark = { default-features = false, version = "0.5" } +rand_chacha = { workspace = true } +rand_core = { workspace = true } +rand_hc = { workspace = true } +rayon = { optional = true, workspace = true } +rocksdb = { features = ["bindgen-runtime", "lz4"], optional = true, workspace = true } +serde = { features = ["derive"], optional = true, workspace = true } +sha2 = { workspace = true } +sha3 = { workspace = true } +subtle = { workspace = true } +thiserror = { workspace = true } +x25519-dalek = { features = ["static_secrets"], workspace = true } + +# Upstream Plonky3 dependencies +p3-blake3.workspace = true +p3-challenger.workspace = true +p3-dft.workspace = true +p3-goldilocks.workspace = true +p3-keccak.workspace = true +p3-matrix.workspace = true +p3-maybe-rayon.workspace = true +p3-symmetric.workspace = true +p3-util.workspace = true [dev-dependencies] -assert_matches = { default-features = false, version = "1.5" } -criterion = { features = ["html_reports"], version = "0.7" } -hex = { default-features = false, features = ["alloc"], version = "0.4" } -itertools = { version = "0.14" } +assert_matches = { workspace = true } +criterion = { workspace = true } +hex = { features = ["alloc"], workspace = true } +itertools = { workspace = true } miden-field = { features = ["testing"], workspace = true } -proptest = { default-features = false, features = ["alloc"], version = "1.7" } -rand-utils = { package = "winter-rand-utils", version = "0.13" } -rstest = { version = "0.26" } -seq-macro = { version = "0.3" } -tempfile = { version = "3.20" } +proptest = { features = ["alloc"], workspace = true } +seq-macro = { workspace = true } +tempfile = { workspace = true } [build-dependencies] -cc = { features = ["parallel"], optional = true, version = "1.2" } -glob = "0.3" +cc = { features = ["parallel"], optional = true, workspace = true } [lints] workspace = true -[package.metadata.cargo-machete] -ignored = ["getrandom", "rand-utils"] - [[test]] name = "rocksdb_large_smt" required-features = ["concurrent", "rocksdb"] diff --git a/miden-crypto/benches/README.md b/miden-crypto/benches/README.md index 28b4d98484..83480d0e58 100644 --- a/miden-crypto/benches/README.md +++ b/miden-crypto/benches/README.md @@ -21,7 +21,7 @@ We benchmark the above hash functions using two scenarios. The first is a 2-to-1 | ------------------- | :----: | :----: | :-------: | :-------: | :------: | :------: | | Apple M1 Pro | 76 ns | 245 ns | | | *5.2 µs | *2.7 µs | | Apple M2 Max | 71 ns | 233 ns | | | *4.6 µs | *2.4 µs | -| Apple M4 Max | 48 ns | | 149 ns | 0.7 µs | 2.5 µs | 1.3 µs | +| Apple M4 Max | 48 ns | | 149 ns | 0.47 µs | 2.5 µs | 1.3 µs | | Amazon Graviton 3 | 108 ns | | | | *5.3 µs | *3.1 µs | | Amazon Graviton 4 | 96 ns | | | | *5.1 µs | *2.8 µs | | AMD Ryzen 9 9950X | 49 ns | | 375 ns | 0.65 µs | 3.1 µs | 1.6 µs | @@ -35,7 +35,7 @@ We benchmark the above hash functions using two scenarios. The first is a 2-to-1 | ------------------- | :----: | :----: | :-------: | :-------: | :------: | :------: | | Apple M1 Pro | 1.0 µs | 1.5 µs | | | *69 µs | *35 µs | | Apple M2 Max | 0.9 µs | 1.5 µs | | | *60 µs | *31 µs | -| Apple M4 Max | 0.7 µs | | 0.7 µs | 8.7 µs | 32 µs | 17 µs | +| Apple M4 Max | 0.7 µs | | 0.7 µs | 6.1 µs | 32 µs | 17 µs | | Amazon Graviton 3 | 1.4 µs | | | | *69 µs | *41 µs | | Amazon Graviton 4 | 1.2 µs | | | | *67 µs | *36 µs | | AMD Ryzen 9 9950X | 0.8 µs | | 2.2 µs | 8.7 µs | 40 µs | 22 µs | diff --git a/miden-crypto/benches/common/config.rs b/miden-crypto/benches/common/config.rs index adaaf6f244..2db8757c71 100644 --- a/miden-crypto/benches/common/config.rs +++ b/miden-crypto/benches/common/config.rs @@ -54,14 +54,6 @@ pub const MERGE_INPUT_SIZES: &[usize] = &[ 512, // 512 bytes ]; -/// Integer sizes for merge_with_int tests -pub const MERGE_INT_SIZES: &[usize] = &[ - 1, // Single byte integer - 2, // 16-bit integer - 4, // 32-bit integer - 8, // 64-bit integer -]; - // === Field Operations Configuration === /// Field element counts for batch operations pub const FIELD_BATCH_SIZES: &[usize] = &[ diff --git a/miden-crypto/benches/common/data.rs b/miden-crypto/benches/common/data.rs index de9c9efb07..52c21214c8 100644 --- a/miden-crypto/benches/common/data.rs +++ b/miden-crypto/benches/common/data.rs @@ -50,12 +50,14 @@ pub fn generate_byte_array_random(size: usize) -> Vec { /// Generate field element array with sequential values pub fn generate_felt_array_sequential(size: usize) -> Vec { - (0..size).map(|i| Felt::new(i as u64)).collect() + (0..size).map(|i| Felt::new_unchecked(i as u64)).collect() } /// Generate byte array of specified size with random data pub fn generate_felt_array_random(size: usize) -> Vec { - iter::from_fn(|| Some(Felt::new(rand_value::()))).take(size).collect() + iter::from_fn(|| Some(Felt::new_unchecked(rand_value::()))) + .take(size) + .collect() } // === Word and Value Generation === @@ -77,7 +79,12 @@ pub enum WordPattern { pub fn generate_word(seed: &mut [u8; 32]) -> Word { *seed = prng_array(*seed); let nums: [u64; 4] = prng_array(*seed); - Word::new([Felt::new(nums[0]), Felt::new(nums[1]), Felt::new(nums[2]), Felt::new(nums[3])]) + Word::new([ + Felt::new_unchecked(nums[0]), + Felt::new_unchecked(nums[1]), + Felt::new_unchecked(nums[2]), + Felt::new_unchecked(nums[3]), + ]) } /// Generate a generic value from seed using PRNG @@ -85,20 +92,28 @@ pub fn generate_value T { *seed = prng_array(*seed); - let value: [T; 1] = miden_crypto::rand::test_utils::prng_array(*seed); + let value: [T; 1] = prng_array(*seed); value[0].clone() } /// Generate word using specified pattern pub fn generate_word_pattern(i: u64, pattern: WordPattern) -> Word { match pattern { - WordPattern::MerkleStandard => Word::new([Felt::new(i), ONE, ONE, Felt::new(i)]), - WordPattern::Sequential => { - Word::new([Felt::new(i), Felt::new(i + 1), Felt::new(i + 2), Felt::new(i + 3)]) - }, - WordPattern::SpreadSequential => { - Word::new([Felt::new(i), Felt::new(i + 4), Felt::new(i + 8), Felt::new(i + 12)]) + WordPattern::MerkleStandard => { + Word::new([Felt::new_unchecked(i), ONE, ONE, Felt::new_unchecked(i)]) }, + WordPattern::Sequential => Word::new([ + Felt::new_unchecked(i), + Felt::new_unchecked(i + 1), + Felt::new_unchecked(i + 2), + Felt::new_unchecked(i + 3), + ]), + WordPattern::SpreadSequential => Word::new([ + Felt::new_unchecked(i), + Felt::new_unchecked(i + 4), + Felt::new_unchecked(i + 8), + Felt::new_unchecked(i + 12), + ]), WordPattern::Random => { let mut seed = [0u8; 32]; seed[0..8].copy_from_slice(&i.to_le_bytes()); @@ -123,7 +138,12 @@ pub fn prepare_smt_entries(pair_count: u64, seed: &mut [u8; 32]) -> Vec<(Word, W .map(|i| { let count = pair_count as f64; let idx = ((i as f64 / count) * (count)) as u64; - let key = Word::new([generate_value(seed), ONE, Felt::new(i), Felt::new(idx)]); + let key = Word::new([ + generate_value(seed), + ONE, + Felt::new_unchecked(i), + Felt::new_unchecked(idx), + ]); let value = generate_word(seed); (key, value) }) diff --git a/miden-crypto/benches/common/macros.rs b/miden-crypto/benches/common/macros.rs index bdea61752f..130eb97cc5 100644 --- a/miden-crypto/benches/common/macros.rs +++ b/miden-crypto/benches/common/macros.rs @@ -258,34 +258,6 @@ macro_rules! benchmark_hash_merge_domain { }; } -// Creates a benchmark for hash merge with int operations -#[macro_export] -macro_rules! benchmark_hash_merge_with_int { - ($func_name:ident, $hasher_name:literal, $sizes:expr, $int_sizes:expr, $closure:expr) => { - fn $func_name(c: &mut Criterion) { - let mut group = c.benchmark_group(concat!("hash-", $hasher_name, "-merge-int")); - group.sample_size(10); - - for size_ref in $sizes { - let size = *size_ref; - for int_size_ref in $int_sizes { - let int_size = *int_size_ref; - group.bench_with_input( - criterion::BenchmarkId::new("merge_with_int", format!("{size}_{int_size}")), - &(size, int_size), - |b: &mut criterion::Bencher, param_ref: &(usize, usize)| { - let (size_param, int_size_param) = *param_ref; - $closure(b, (size_param, int_size_param)) - }, - ); - } - } - - group.finish(); - } - }; -} - // Creates a benchmark for hash merge many operations #[macro_export] macro_rules! benchmark_hash_merge_many { @@ -705,7 +677,9 @@ macro_rules! benchmark_rand_reseed { let mut coin = <$coin_type>::new($seed); let new_seeds: Vec = (0u64..10) - .map(|i| miden_crypto::Word::new([miden_crypto::Felt::new((i + 1) as u64); 4])) + .map(|i| { + miden_crypto::Word::new([miden_crypto::Felt::new_unchecked((i + 1) as u64); 4]) + }) .collect(); group.bench_function("reseed", |b| { diff --git a/miden-crypto/benches/dsa.rs b/miden-crypto/benches/dsa.rs index 4ab567c9db..70866a8ad2 100644 --- a/miden-crypto/benches/dsa.rs +++ b/miden-crypto/benches/dsa.rs @@ -112,7 +112,7 @@ benchmark_with_setup_data! { || { let secret_keys: Vec = (0..KEYGEN_ITERATIONS).map(|_| Falcon512SecretKey::new()).collect(); let messages: Vec = - (0..KEYGEN_ITERATIONS).map(|i| Word::new([Felt::new(i as u64); 4])).collect(); + (0..KEYGEN_ITERATIONS).map(|i| Word::new([Felt::new_unchecked(i as u64); 4])).collect(); (secret_keys, messages) }, |b: &mut criterion::Bencher, (secret_keys, messages): &(Vec, Vec)| { @@ -133,7 +133,7 @@ benchmark_with_setup_data! { || { let secret_keys: Vec = (0..KEYGEN_ITERATIONS).map(|_| Falcon512SecretKey::new()).collect(); let messages: Vec = - (0..KEYGEN_ITERATIONS).map(|i| Word::new([Felt::new(i as u64); 4])).collect(); + (0..KEYGEN_ITERATIONS).map(|i| Word::new([Felt::new_unchecked(i as u64); 4])).collect(); let rngs: Vec<_> = (0..KEYGEN_ITERATIONS).map(|_| rng()).collect(); (secret_keys, messages, rngs) }, @@ -161,9 +161,9 @@ benchmark_with_setup_data! { let mut rng = rand::rngs::ThreadRng::default(); let secret_keys: Vec = (0..KEYGEN_ITERATIONS).map(|_| Falcon512SecretKey::with_rng(&mut rng)).collect(); - let public_keys: Vec = secret_keys.iter().map(|sk| sk.public_key()).collect(); + let public_keys: Vec = secret_keys.iter().map(falcon512_poseidon2::SecretKey::public_key).collect(); let messages: Vec = - (0..KEYGEN_ITERATIONS).map(|i| Word::new([Felt::new(i as u64); 4])).collect(); + (0..KEYGEN_ITERATIONS).map(|i| Word::new([Felt::new_unchecked(i as u64); 4])).collect(); let signatures: Vec = secret_keys .iter() .zip(messages.iter()) @@ -196,7 +196,7 @@ benchmark_with_setup! { || {}, |b: &mut criterion::Bencher| { b.iter(|| { - let _secret_key = ecdsa_k256_keccak::SecretKey::new(); + let _secret_key = ecdsa_k256_keccak::SigningKey::new(); }) }, } @@ -212,7 +212,7 @@ benchmark_with_setup_data! { |b: &mut criterion::Bencher, rng: &rand::rngs::ThreadRng| { b.iter(|| { let mut rng_clone = rng.clone(); - let _secret_key = ecdsa_k256_keccak::SecretKey::with_rng(&mut rng_clone); + let _secret_key = ecdsa_k256_keccak::SigningKey::with_rng(&mut rng_clone); }) }, } @@ -223,10 +223,10 @@ benchmark_with_setup_data! { DEFAULT_SAMPLE_SIZE, "ecdsa_k256_keygen_public", || { - let secret_keys: Vec = (0..KEYGEN_ITERATIONS).map(|_| ecdsa_k256_keccak::SecretKey::new()).collect(); + let secret_keys: Vec = (0..KEYGEN_ITERATIONS).map(|_| ecdsa_k256_keccak::SigningKey::new()).collect(); secret_keys }, - |b: &mut criterion::Bencher, secret_keys: &Vec| { + |b: &mut criterion::Bencher, secret_keys: &Vec| { b.iter(|| { for secret_key in secret_keys { let _public_key = secret_key.public_key(); @@ -243,12 +243,12 @@ benchmark_with_setup_data! { DEFAULT_SAMPLE_SIZE, "ecdsa_k256_sign", || { - let secret_keys: Vec = (0..KEYGEN_ITERATIONS).map(|_| ecdsa_k256_keccak::SecretKey::new()).collect(); + let secret_keys: Vec = (0..KEYGEN_ITERATIONS).map(|_| ecdsa_k256_keccak::SigningKey::new()).collect(); let messages: Vec = - (0..KEYGEN_ITERATIONS).map(|i| Word::new([Felt::new(i as u64); 4])).collect(); + (0..KEYGEN_ITERATIONS).map(|i| Word::new([Felt::new_unchecked(i as u64); 4])).collect(); (secret_keys, messages) }, - |b: &mut criterion::Bencher, (secret_keys, messages): &(Vec, Vec)| { + |b: &mut criterion::Bencher, (secret_keys, messages): &(Vec, Vec)| { b.iter(|| { // Clone secret keys since sign() needs &mut self let mut secret_keys_local = secret_keys.clone(); @@ -268,11 +268,11 @@ benchmark_with_setup_data! { "ecdsa_k256_verify", || { let mut rng = rand::rngs::ThreadRng::default(); - let mut secret_keys: Vec = - (0..KEYGEN_ITERATIONS).map(|_| ecdsa_k256_keccak::SecretKey::with_rng(&mut rng)).collect(); - let public_keys: Vec = secret_keys.iter().map(|sk| sk.public_key()).collect(); + let mut secret_keys: Vec = + (0..KEYGEN_ITERATIONS).map(|_| ecdsa_k256_keccak::SigningKey::with_rng(&mut rng)).collect(); + let public_keys: Vec = secret_keys.iter().map(ecdsa_k256_keccak::SigningKey::public_key).collect(); let messages: Vec = - (0..KEYGEN_ITERATIONS).map(|i| Word::new([Felt::new(i as u64); 4])).collect(); + (0..KEYGEN_ITERATIONS).map(|i| Word::new([Felt::new_unchecked(i as u64); 4])).collect(); let signatures: Vec = secret_keys .iter_mut() .zip(messages.iter()) @@ -305,7 +305,7 @@ benchmark_with_setup! { || {}, |b: &mut criterion::Bencher| { b.iter(|| { - let _secret_key = eddsa_25519_sha512::SecretKey::new(); + let _secret_key = eddsa_25519_sha512::SigningKey::new(); }) }, } @@ -321,7 +321,7 @@ benchmark_with_setup_data! { |b: &mut criterion::Bencher, rng: &rand::rngs::ThreadRng| { b.iter(|| { let mut rng_clone = rng.clone(); - let _secret_key = eddsa_25519_sha512::SecretKey::with_rng(&mut rng_clone); + let _secret_key = eddsa_25519_sha512::SigningKey::with_rng(&mut rng_clone); }) }, } @@ -332,10 +332,10 @@ benchmark_with_setup_data! { DEFAULT_SAMPLE_SIZE, "eddsa_25519_sha512_keygen_public", || { - let secret_keys: Vec = (0..KEYGEN_ITERATIONS).map(|_| eddsa_25519_sha512::SecretKey::new()).collect(); + let secret_keys: Vec = (0..KEYGEN_ITERATIONS).map(|_| eddsa_25519_sha512::SigningKey::new()).collect(); secret_keys }, - |b: &mut criterion::Bencher, secret_keys: &Vec| { + |b: &mut criterion::Bencher, secret_keys: &Vec| { b.iter(|| { for secret_key in secret_keys { let _public_key = secret_key.public_key(); @@ -352,12 +352,12 @@ benchmark_with_setup_data! { DEFAULT_SAMPLE_SIZE, "eddsa_25519_sha512_sign", || { - let secret_keys: Vec = (0..KEYGEN_ITERATIONS).map(|_| eddsa_25519_sha512::SecretKey::new()).collect(); + let secret_keys: Vec = (0..KEYGEN_ITERATIONS).map(|_| eddsa_25519_sha512::SigningKey::new()).collect(); let messages: Vec = - (0..KEYGEN_ITERATIONS).map(|i| Word::new([Felt::new(i as u64); 4])).collect(); + (0..KEYGEN_ITERATIONS).map(|i| Word::new([Felt::new_unchecked(i as u64); 4])).collect(); (secret_keys, messages) }, - |b: &mut criterion::Bencher, (secret_keys, messages): &(Vec, Vec)| { + |b: &mut criterion::Bencher, (secret_keys, messages): &(Vec, Vec)| { b.iter(|| { for (secret_key, message) in secret_keys.iter().zip(messages.iter()) { let _signature = secret_key.sign(black_box(*message)); @@ -375,11 +375,11 @@ benchmark_with_setup_data! { "eddsa_25519_sha512_verify", || { let mut rng = rand::rngs::ThreadRng::default(); - let secret_keys: Vec = - (0..KEYGEN_ITERATIONS).map(|_| eddsa_25519_sha512::SecretKey::with_rng(&mut rng)).collect(); - let public_keys: Vec = secret_keys.iter().map(|sk| sk.public_key()).collect(); + let secret_keys: Vec = + (0..KEYGEN_ITERATIONS).map(|_| eddsa_25519_sha512::SigningKey::with_rng(&mut rng)).collect(); + let public_keys: Vec = secret_keys.iter().map(eddsa_25519_sha512::SigningKey::public_key).collect(); let messages: Vec = - (0..KEYGEN_ITERATIONS).map(|i| Word::new([Felt::new(i as u64); 4])).collect(); + (0..KEYGEN_ITERATIONS).map(|i| Word::new([Felt::new_unchecked(i as u64); 4])).collect(); let signatures: Vec = secret_keys .iter() .zip(messages.iter()) diff --git a/miden-crypto/benches/large_smt.rs b/miden-crypto/benches/large_smt.rs index 74e0312a4d..397f2e497b 100644 --- a/miden-crypto/benches/large_smt.rs +++ b/miden-crypto/benches/large_smt.rs @@ -48,7 +48,12 @@ fn create_sparse_subtree() -> Subtree { let root_index = NodeIndex::new(ROOT_DEPTH, 0).unwrap(); let mut subtree = Subtree::new(root_index); - let mut child_hash: Word = Word::new([Felt::new(1), Felt::new(1), Felt::new(1), Felt::new(1)]); + let mut child_hash: Word = Word::new([ + Felt::new_unchecked(1), + Felt::new_unchecked(1), + Felt::new_unchecked(1), + Felt::new_unchecked(1), + ]); let mut current_idx = NodeIndex::new(ROOT_DEPTH + SUBTREE_DEPTH - 1, 0).unwrap(); for _ in 0..SUBTREE_DEPTH { diff --git a/miden-crypto/benches/large_smt_forest.rs b/miden-crypto/benches/large_smt_forest.rs index 90cc675e92..05aea7055d 100644 --- a/miden-crypto/benches/large_smt_forest.rs +++ b/miden-crypto/benches/large_smt_forest.rs @@ -7,6 +7,7 @@ use std::hint; use criterion::{BatchSize, Criterion, criterion_group, criterion_main}; use miden_crypto::{ + Map, merkle::smt::{ Backend, ForestPersistentBackend, LargeSmtForest, LineageId, PersistentBackendConfig, SmtForestUpdateBatch, SmtUpdateBatch, TreeId, @@ -134,6 +135,155 @@ benchmark_with_setup_data! { }, } +// Measures iteration over the latest version of a tree (the WithoutHistory fast path). +benchmark_with_setup_data! { + large_smt_forest_persistent_entries_current_tree, + DEFAULT_MEASUREMENT_TIME, + DEFAULT_SAMPLE_SIZE, + "large_smt_forest_persistent_entries_current_tree", + || { + let mut setup = ForestSetup::new_persistent(); + let batch = generate_tree_update_batch(BATCH_SIZE); + let lineage = LineageId::new([0x42; 32]); + let version = 0; + setup.forest.add_lineage(lineage, version, batch).unwrap(); + let tree = TreeId::new(lineage, version); + (setup, tree) + }, + |b: &mut criterion::Bencher, (setup, tree): &(ForestSetup<_>, TreeId)| { + b.iter(|| { + hint::black_box( + setup.forest.entries(*tree).unwrap().map(|e| e.unwrap()).collect::>() + ); + }) + } +} + +// Measures iteration over a historical version of a tree (the WithHistory state machine path). +benchmark_with_setup_data! { + large_smt_forest_persistent_entries_historical_tree, + DEFAULT_MEASUREMENT_TIME, + DEFAULT_SAMPLE_SIZE, + "large_smt_forest_persistent_entries_historical_tree", + || { + let mut setup = ForestSetup::new_persistent(); + let initial_batch = generate_tree_update_batch(BATCH_SIZE); + let lineage = LineageId::new([0x42; 32]); + let version = 0; + setup.forest.add_lineage(lineage, version, initial_batch).unwrap(); + let update_batch = generate_tree_update_batch(BATCH_SIZE); + setup.forest.update_tree(lineage, version + 1, update_batch).unwrap(); + let tree = TreeId::new(lineage, version); + (setup, tree) + }, + |b: &mut criterion::Bencher, (setup, tree): &(ForestSetup<_>, TreeId)| { + b.iter(|| { + hint::black_box( + setup.forest.entries(*tree).unwrap().map(|e| e.unwrap()).collect::>() + ); + }) + }, +} + +// Roughly equivalent to large_smt::large_smt_get in functionality. +benchmark_with_setup_data! { + large_smt_forest_persistent_get_full_tree, + DEFAULT_MEASUREMENT_TIME, + DEFAULT_SAMPLE_SIZE, + "large_smt_forest_persistent_get_full_tree", + || { + let mut setup = ForestSetup::new_persistent(); + let batch = generate_tree_update_batch(BATCH_SIZE); + let lineage = LineageId::new([0x42; 32]); + let version = 0; + setup.forest.add_lineage(lineage, version, batch).unwrap(); + let keys = generate_test_keys_sequential(10); + let tree = TreeId::new(lineage, version); + (setup, keys, tree) + }, + |b: &mut criterion::Bencher, (setup, keys, tree): &(ForestSetup<_>, Vec, TreeId)| { + b.iter(|| { + for key in keys { + hint::black_box(setup.forest.get(*tree, *key).unwrap()); + } + }) + } +} + +// Benchmarks `get` on a historical tree version. +benchmark_with_setup_data! { + large_smt_forest_persistent_get_historical_tree, + DEFAULT_MEASUREMENT_TIME, + DEFAULT_SAMPLE_SIZE, + "large_smt_forest_persistent_get_historical_tree", + || { + let mut setup = ForestSetup::new_persistent(); + let initial_batch = generate_tree_update_batch(BATCH_SIZE); + let lineage = LineageId::new([0x42; 32]); + let version = 0; + setup.forest.add_lineage(lineage, version, initial_batch).unwrap(); + let update_batch = generate_tree_update_batch(BATCH_SIZE); + setup.forest.update_tree(lineage, 1, update_batch).unwrap(); + + let keys = generate_test_keys_sequential(10); + let tree = TreeId::new(lineage, version); + (setup, keys, tree) + }, + |b: &mut criterion::Bencher, (setup, keys, tree): &(ForestSetup<_>, Vec, TreeId)| { + b.iter(|| { + for key in keys { + hint::black_box(setup.forest.get(*tree, *key).unwrap()); + } + }) + }, +} + +// Measures `entry_count` on the latest version of a tree. +benchmark_with_setup_data! { + large_smt_forest_persistent_entry_count_current_tree, + DEFAULT_MEASUREMENT_TIME, + DEFAULT_SAMPLE_SIZE, + "large_smt_forest_persistent_entry_count_current_tree", + || { + let mut setup = ForestSetup::new_persistent(); + let batch = generate_tree_update_batch(BATCH_SIZE); + let lineage = LineageId::new([0x42; 32]); + let version = 0; + setup.forest.add_lineage(lineage, version, batch).unwrap(); + let tree = TreeId::new(lineage, version); + (setup, tree) + }, + |b: &mut criterion::Bencher, (setup, tree): &(ForestSetup<_>, TreeId)| { + b.iter(|| { + hint::black_box(setup.forest.entry_count(*tree).unwrap()); + }) + } +} + +// Measures `entry_count` on a historical version of a tree. +benchmark_with_setup_data! { + large_smt_forest_persistent_entry_count_historical_tree, + DEFAULT_MEASUREMENT_TIME, + DEFAULT_SAMPLE_SIZE, + "large_smt_forest_persistent_entry_count_historical_tree", + || { + let mut setup = ForestSetup::new_persistent(); + let initial_batch = generate_tree_update_batch(BATCH_SIZE); + let lineage = LineageId::new([0x42; 32]); + let version = 0; + setup.forest.add_lineage(lineage, version, initial_batch).unwrap(); + let update_batch = generate_tree_update_batch(BATCH_SIZE); + setup.forest.update_tree(lineage, version + 1, update_batch).unwrap(); + let tree = TreeId::new(lineage, version); + (setup, tree) + }, + |b: &mut criterion::Bencher, (setup, tree): &(ForestSetup<_>, TreeId)| { + b.iter(|| { + hint::black_box(setup.forest.entry_count(*tree).unwrap()); + }) + }, +} + // Roughly equivalent to large_smt::large_smt_insert_batch_to_empty_tree in functionality. benchmark_batch! { large_smt_forest_persistent_add_lineage, @@ -182,7 +332,59 @@ benchmark_batch! { |size| Some(criterion::Throughput::Elements(size as u64)) } -// Has no direct equivalent in the large smt, but should be broadly equivalent workwise to the +// Benchmarks the addition of multiple lineages in a single batch through repeated calls to +// `add_lineage` and thereby provides a point of comparison for the actual parallel `add_lineage` +// implementation. +benchmark_batch! { + large_smt_forest_persistent_add_lineages_sequential, + &[10, 100, 1000], + |b: &mut criterion::Bencher, lineage_count: usize| { + let lineages = generate_lineages(lineage_count); + + b.iter_batched( + || { + let setup = ForestSetup::new_persistent(); + let batch = generate_forest_update_batch(&lineages, BATCH_SIZE) + .consume() + .into_iter() + .map(|(l, b)| (l, SmtUpdateBatch::new(b.into_iter()))) + .collect::>(); + (setup, batch) + }, + |(mut setup, batches)| { + for (lineage, batch) in batches { + hint::black_box(setup.forest.add_lineage(lineage, 0, batch).unwrap()); + } + }, + BatchSize::LargeInput + ) + }, + |size| Some(criterion::Throughput::Elements(size as u64)) +} + +// Benchmarks the addition of multiple lineages in a single batch. +benchmark_batch! { + large_smt_forest_persistent_add_lineages, + &[10, 100, 1000], + |b: &mut criterion::Bencher, lineage_count: usize| { + let lineages = generate_lineages(lineage_count); + + b.iter_batched( + || { + let setup = ForestSetup::new_persistent(); + let batch = generate_forest_update_batch(&lineages, BATCH_SIZE); + (setup, batch) + }, + |(mut setup, batch)| { + hint::black_box(setup.forest.add_lineages(0, batch).unwrap()) + }, + BatchSize::LargeInput + ) + }, + |size| Some(criterion::Throughput::Elements(size as u64)) +} + +// Has no direct equivalent in the large smt, but should be broadly equivalent work-wise to the // large_smt_forest_persistent_update_tree above in time as we try and do as much in parallel as // possible. benchmark_batch! { @@ -220,7 +422,15 @@ criterion_group!( large_smt_forest_persistent_group, large_smt_forest_persistent_open_full_tree, large_smt_forest_persistent_open_historical_tree, + large_smt_forest_persistent_get_full_tree, + large_smt_forest_persistent_get_historical_tree, + large_smt_forest_persistent_entry_count_current_tree, + large_smt_forest_persistent_entry_count_historical_tree, + large_smt_forest_persistent_entries_current_tree, + large_smt_forest_persistent_entries_historical_tree, large_smt_forest_persistent_add_lineage, + large_smt_forest_persistent_add_lineages_sequential, + large_smt_forest_persistent_add_lineages, large_smt_forest_persistent_update_tree, large_smt_forest_persistent_update_forest, ); diff --git a/miden-crypto/benches/merkle.rs b/miden-crypto/benches/merkle.rs index 052827d543..6b410386a6 100644 --- a/miden-crypto/benches/merkle.rs +++ b/miden-crypto/benches/merkle.rs @@ -66,7 +66,7 @@ benchmark_with_setup_data!( let entries = generate_words_merkle_std(256); MerkleTree::new(&entries).unwrap() }, - |b: &mut criterion::Bencher<'_>, tree: &MerkleTree| { + |b: &mut Bencher<'_>, tree: &MerkleTree| { b.iter(|| { hint::black_box(tree.root()); }); @@ -85,7 +85,7 @@ benchmark_with_setup_data!( let index = NodeIndex::new(8, 128).unwrap(); (tree, index) }, - |b: &mut criterion::Bencher<'_>, (tree, index): &(MerkleTree, NodeIndex)| { + |b: &mut Bencher<'_>, (tree, index): &(MerkleTree, NodeIndex)| { b.iter(|| { let _path = hint::black_box(tree.get_path(*index)).unwrap(); }) @@ -117,7 +117,7 @@ benchmark_with_setup_data!( let entries = generate_words_merkle_std(256); MerkleTree::new(&entries).unwrap() }, - |b: &mut criterion::Bencher<'_>, tree: &MerkleTree| { + |b: &mut Bencher<'_>, tree: &MerkleTree| { b.iter(|| { hint::black_box(tree.leaves().collect::>()); }); @@ -133,7 +133,7 @@ benchmark_with_setup_data!( let entries = generate_words_merkle_std(256); MerkleTree::new(&entries).unwrap() }, - |b: &mut criterion::Bencher<'_>, tree: &MerkleTree| { + |b: &mut Bencher<'_>, tree: &MerkleTree| { b.iter(|| { hint::black_box(tree.inner_nodes().collect::>()); }); @@ -158,7 +158,7 @@ benchmark_with_setup_data!( let root = tree.root(); (path, leaf_index, leaf, root) }, - |b: &mut criterion::Bencher<'_>, (path, index, leaf, root): &(MerklePath, u64, Word, Word)| { + |b: &mut Bencher<'_>, (path, index, leaf, root): &(MerklePath, u64, Word, Word)| { b.iter(|| { let _ = path.verify(*index, hint::black_box(*leaf), hint::black_box(root)); }) diff --git a/miden-crypto/benches/smt.rs b/miden-crypto/benches/smt.rs index f08be1473a..f1e67c27d7 100644 --- a/miden-crypto/benches/smt.rs +++ b/miden-crypto/benches/smt.rs @@ -128,8 +128,8 @@ benchmark_batch! { |b: &mut criterion::Bencher, insert_count: usize| { let entries = generate_smt_entries(256); let mut smt = Smt::with_entries(entries).unwrap(); - let new_key = Word::new([Felt::new(999), Felt::new(1000), Felt::new(1001), Felt::new(1002)]); - let new_value = Word::new([Felt::new(1003), Felt::new(1004), Felt::new(1005), Felt::new(1006)]); + let new_key = Word::new([Felt::new_unchecked(999), Felt::new_unchecked(1000), Felt::new_unchecked(1001), Felt::new_unchecked(1002)]); + let new_value = Word::new([Felt::new_unchecked(1003), Felt::new_unchecked(1004), Felt::new_unchecked(1005), Felt::new_unchecked(1006)]); b.iter(|| { for _ in 0..insert_count { @@ -284,7 +284,7 @@ benchmark_batch! { let entries = generate_simple_smt_entries(256); let mut smt = SimpleSmt::<32>::with_leaves(entries).unwrap(); let new_index = LeafIndex::<32>::new(999).unwrap(); - let new_value = Word::new([Felt::new(1000), Felt::new(1001), Felt::new(1002), Felt::new(1003)]); + let new_value = Word::new([Felt::new_unchecked(1000), Felt::new_unchecked(1001), Felt::new_unchecked(1002), Felt::new_unchecked(1003)]); b.iter(|| { for _ in 0..insert_count { @@ -413,10 +413,10 @@ benchmark_batch! { .map(|i| { let index = LeafIndex::<32>::new(1000u64 + i as u64).unwrap(); let value = Word::new([ - Felt::new((1000 + i) as u64), - Felt::new((1001 + i) as u64), - Felt::new((1002 + i) as u64), - Felt::new((1003 + i) as u64), + Felt::new_unchecked((1000 + i) as u64), + Felt::new_unchecked((1001 + i) as u64), + Felt::new_unchecked((1002 + i) as u64), + Felt::new_unchecked((1003 + i) as u64), ]); (index, value) }) @@ -452,8 +452,8 @@ benchmark_multi! { let key = Word::new([ generate_value(&mut seed), ONE, - Felt::new(n), - Felt::new(leaf_index), + Felt::new_unchecked(n), + Felt::new_unchecked(leaf_index), ]); let value = generate_word(&mut seed); (key, value) @@ -499,7 +499,7 @@ benchmark_multi! { let entries: Vec<(Word, Word)> = (0..pair_count) .map(|i| { let leaf_index: u8 = generate_value(&mut seed); - let key = Word::new([ONE, ONE, Felt::new(i), Felt::new(leaf_index as u64)]); + let key = Word::new([ONE, ONE, Felt::new_unchecked(i), Felt::new_unchecked(leaf_index as u64)]); let value = generate_word(&mut seed); (key, value) }) diff --git a/miden-crypto/benches/sparse_path.rs b/miden-crypto/benches/sparse_path.rs index f99c5cd24a..9c3298c90b 100644 --- a/miden-crypto/benches/sparse_path.rs +++ b/miden-crypto/benches/sparse_path.rs @@ -76,7 +76,9 @@ benchmark_with_setup_data!( }, |b: &mut Bencher<'_>, (merkle_path, sparse_path): &(MerklePath, miden_crypto::merkle::SparseMerklePath)| { + #[expect(clippy::needless_collect)] b.iter(|| { + // Collect to exercise the full iterator and verify counts let merkle_nodes: Vec<_> = hint::black_box(merkle_path.iter()).collect(); let sparse_nodes: Vec<_> = hint::black_box(sparse_path.iter()).collect(); // Ensure both iterators produce the same number of nodes @@ -101,7 +103,12 @@ benchmark_with_setup_data!( // Insert a few sparse entries for i in [0u64, 100, 500, 1000] { let leaf_idx = LeafIndex::new(i).unwrap(); - let value = Word::new([Felt::new(i), Felt::new(i), Felt::new(i), Felt::new(i)]); + let value = Word::new([ + Felt::new_unchecked(i), + Felt::new_unchecked(i), + Felt::new_unchecked(i), + Felt::new_unchecked(i), + ]); tree.insert(leaf_idx, value); indices.push(i); values.push(value); diff --git a/miden-crypto/benches/transpose.rs b/miden-crypto/benches/transpose.rs index 636655f6ee..88b23d698d 100644 --- a/miden-crypto/benches/transpose.rs +++ b/miden-crypto/benches/transpose.rs @@ -12,7 +12,7 @@ mod common; const MATRIX_SIZES: &[usize] = &[64, 256, 1024]; fn generate_felt_matrix(size: usize) -> Vec { - (0..(size * size)).map(|i| Felt::new(i as u64)).collect() + (0..(size * size)).map(|i| Felt::new_unchecked(i as u64)).collect() } /// Unsafe transpose using uninit_vector diff --git a/miden-crypto/benches/word.rs b/miden-crypto/benches/word.rs index a3e6666b4f..e834458db2 100644 --- a/miden-crypto/benches/word.rs +++ b/miden-crypto/benches/word.rs @@ -1,7 +1,7 @@ //! Comprehensive Word operation benchmarks //! //! This module benchmarks all Word operations implemented in the library -//! with a focus on type conversions, serialization, and lexicographic ordering. +//! with a focus on type conversions, serialization, and ordering. //! //! # Organization //! @@ -9,7 +9,7 @@ //! 1. Word creation and basic operations //! 2. Type conversions (bool, u8, u16, u32, u64) //! 3. Serialization and deserialization -//! 4. Lexicographic ordering +//! 4. Ordering (lexicographic) //! 5. Batch operations //! //! # Adding New Word Benchmarks @@ -22,7 +22,7 @@ use criterion::{Criterion, criterion_group, criterion_main}; // Import Word modules -use miden_crypto::{Felt, LexicographicWord, Word}; +use miden_crypto::{Felt, Word}; // Import common utilities mod common; @@ -33,16 +33,66 @@ use crate::config::{DEFAULT_MEASUREMENT_TIME, DEFAULT_SAMPLE_SIZE, FIELD_BATCH_S /// Configuration for Word testing const TEST_WORDS: [Word; 10] = [ - Word::new([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(0)]), - Word::new([Felt::new(1), Felt::new(0), Felt::new(0), Felt::new(0)]), - Word::new([Felt::new(0), Felt::new(1), Felt::new(0), Felt::new(0)]), - Word::new([Felt::new(0), Felt::new(0), Felt::new(1), Felt::new(0)]), - Word::new([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(1)]), - Word::new([Felt::new(1), Felt::new(1), Felt::new(1), Felt::new(1)]), - Word::new([Felt::new(u64::MAX), Felt::new(0), Felt::new(0), Felt::new(0)]), - Word::new([Felt::new(0), Felt::new(u64::MAX), Felt::new(0), Felt::new(0)]), - Word::new([Felt::new(0), Felt::new(0), Felt::new(u64::MAX), Felt::new(0)]), - Word::new([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(u64::MAX)]), + Word::new([ + Felt::new_unchecked(0), + Felt::new_unchecked(0), + Felt::new_unchecked(0), + Felt::new_unchecked(0), + ]), + Word::new([ + Felt::new_unchecked(1), + Felt::new_unchecked(0), + Felt::new_unchecked(0), + Felt::new_unchecked(0), + ]), + Word::new([ + Felt::new_unchecked(0), + Felt::new_unchecked(1), + Felt::new_unchecked(0), + Felt::new_unchecked(0), + ]), + Word::new([ + Felt::new_unchecked(0), + Felt::new_unchecked(0), + Felt::new_unchecked(1), + Felt::new_unchecked(0), + ]), + Word::new([ + Felt::new_unchecked(0), + Felt::new_unchecked(0), + Felt::new_unchecked(0), + Felt::new_unchecked(1), + ]), + Word::new([ + Felt::new_unchecked(1), + Felt::new_unchecked(1), + Felt::new_unchecked(1), + Felt::new_unchecked(1), + ]), + Word::new([ + Felt::new_unchecked(u64::MAX), + Felt::new_unchecked(0), + Felt::new_unchecked(0), + Felt::new_unchecked(0), + ]), + Word::new([ + Felt::new_unchecked(0), + Felt::new_unchecked(u64::MAX), + Felt::new_unchecked(0), + Felt::new_unchecked(0), + ]), + Word::new([ + Felt::new_unchecked(0), + Felt::new_unchecked(0), + Felt::new_unchecked(u64::MAX), + Felt::new_unchecked(0), + ]), + Word::new([ + Felt::new_unchecked(0), + Felt::new_unchecked(0), + Felt::new_unchecked(0), + Felt::new_unchecked(u64::MAX), + ]), ]; // === Word Creation and Basic Operations === @@ -57,10 +107,10 @@ benchmark_with_setup_data! { let test_elements: Vec<[Felt; 4]> = (0u64..100) .map(|i| { [ - Felt::new(i), - Felt::new(i + 1), - Felt::new(i + 2), - Felt::new(i + 3), + Felt::new_unchecked(i), + Felt::new_unchecked(i + 1), + Felt::new_unchecked(i + 2), + Felt::new_unchecked(i + 3), ] }) .collect(); @@ -198,82 +248,42 @@ benchmark_with_setup! { }, } -// === Lexicographic Ordering Benchmarks === +// === Ordering Benchmarks === -// Lexicographic word creation -benchmark_with_setup! { - word_lexicographic_new, - DEFAULT_MEASUREMENT_TIME, - DEFAULT_SAMPLE_SIZE, - "new", - || {}, - |b: &mut criterion::Bencher| { - b.iter(|| { - for word in &TEST_WORDS { - let _lex_word = LexicographicWord::new(*word); - } - }) - }, -} - -// Lexicographic word access -benchmark_with_setup_data! { - word_lexicographic_access, - DEFAULT_MEASUREMENT_TIME, - DEFAULT_SAMPLE_SIZE, - "inner_access", - || { - let lex_words: Vec = - TEST_WORDS.iter().map(|w| LexicographicWord::new(*w)).collect(); - lex_words - }, - |b: &mut criterion::Bencher, lex_words: &Vec| { - b.iter(|| { - for lex_word in lex_words { - let _inner = lex_word.inner(); - } - }) - }, -} - -// Lexicographic ordering comparisons +// Word ordering comparisons (lexicographic) benchmark_with_setup_data! { - word_lexicographic_ordering, + word_cmp, DEFAULT_MEASUREMENT_TIME, DEFAULT_SAMPLE_SIZE, - "partial_cmp", + "cmp", || { - let lex_words: Vec = - TEST_WORDS.iter().map(|w| LexicographicWord::new(*w)).collect(); - lex_words + TEST_WORDS.to_vec() }, - |b: &mut criterion::Bencher, lex_words: &Vec| { + |b: &mut criterion::Bencher, words: &Vec| { b.iter(|| { - for i in 0..lex_words.len() { - for j in i..lex_words.len() { - let _result = lex_words[i].partial_cmp(&lex_words[j]); + for i in 0..words.len() { + for j in i..words.len() { + let _result = words[i].cmp(&words[j]); } } }) }, } -// Lexicographic equality comparison +// Word equality comparison benchmark_with_setup_data! { - word_lexicographic_eq, + word_eq, DEFAULT_MEASUREMENT_TIME, DEFAULT_SAMPLE_SIZE, "eq", || { - let lex_words: Vec = - TEST_WORDS.iter().map(|w| LexicographicWord::new(*w)).collect(); - lex_words + TEST_WORDS.to_vec() }, - |b: &mut criterion::Bencher, lex_words: &Vec| { + |b: &mut criterion::Bencher, words: &Vec| { b.iter(|| { - for i in 0..lex_words.len() { - for j in 0..lex_words.len() { - let _result = lex_words[i] == lex_words[j]; + for i in 0..words.len() { + for j in 0..words.len() { + let _result = words[i] == words[j]; } } }) @@ -290,10 +300,10 @@ benchmark_batch! { let words: Vec = (0..count) .map(|i| { Word::new([ - Felt::new(i as u64), - Felt::new((i + 1) as u64), - Felt::new((i + 2) as u64), - Felt::new((i + 3) as u64), + Felt::new_unchecked(i as u64), + Felt::new_unchecked((i + 1) as u64), + Felt::new_unchecked((i + 2) as u64), + Felt::new_unchecked((i + 3) as u64), ]) }) .collect(); @@ -321,11 +331,9 @@ criterion_group!( // Serialization benchmarks word_to_hex, word_to_vec, - // Lexicographic ordering benchmarks - word_lexicographic_new, - word_lexicographic_access, - word_lexicographic_ordering, - word_lexicographic_eq, + // Ordering benchmarks + word_cmp, + word_eq, // Batch operations benchmarks word_batch_elements, ); diff --git a/miden-crypto/src/aead/aead_poseidon2/mod.rs b/miden-crypto/src/aead/aead_poseidon2/mod.rs index 18de8ab1df..70e443a2e1 100644 --- a/miden-crypto/src/aead/aead_poseidon2/mod.rs +++ b/miden-crypto/src/aead/aead_poseidon2/mod.rs @@ -435,13 +435,13 @@ impl SecretKey { } impl Distribution for StandardUniform { - fn sample(&self, rng: &mut R) -> SecretKey { + fn sample(&self, rng: &mut R) -> SecretKey { let mut res = [ZERO; SECRET_KEY_SIZE]; let uni_dist = Uniform::new(0, Felt::ORDER).expect("should not fail given the size of the field"); for r in res.iter_mut() { let sampled_integer = uni_dist.sample(rng); - *r = Felt::new(sampled_integer); + *r = Felt::new_unchecked(sampled_integer); } SecretKey(res) } @@ -605,13 +605,13 @@ impl From for [Felt; NONCE_SIZE] { } impl Distribution for StandardUniform { - fn sample(&self, rng: &mut R) -> Nonce { + fn sample(&self, rng: &mut R) -> Nonce { let mut res = [ZERO; NONCE_SIZE]; let uni_dist = Uniform::new(0, Felt::ORDER).expect("should not fail given the size of the field"); for r in res.iter_mut() { let sampled_integer = uni_dist.sample(rng); - *r = Felt::new(sampled_integer); + *r = Felt::new_unchecked(sampled_integer); } Nonce(res) } diff --git a/miden-crypto/src/aead/aead_poseidon2/test.rs b/miden-crypto/src/aead/aead_poseidon2/test.rs index cc1df19cab..e0b33dc723 100644 --- a/miden-crypto/src/aead/aead_poseidon2/test.rs +++ b/miden-crypto/src/aead/aead_poseidon2/test.rs @@ -15,13 +15,13 @@ proptest! { #[test] fn prop_bytes_felts_roundtrip(bytes in prop::collection::vec(any::(), 0..500)) { // bytes -> felts -> bytes - let felts = super::bytes_to_elements_with_padding(&bytes); - let back = super::padded_elements_to_bytes(&felts).unwrap(); + let felts = bytes_to_elements_with_padding(&bytes); + let back = padded_elements_to_bytes(&felts).unwrap(); prop_assert_eq!(bytes, back); // And the other direction on valid encodings: felts come from bytes_to_felts, // so they must satisfy the padding invariant expected by felts_to_bytes. - let felts_roundtrip = super::bytes_to_elements_with_padding(&super::padded_elements_to_bytes(&felts).unwrap()); + let felts_roundtrip = bytes_to_elements_with_padding(&padded_elements_to_bytes(&felts).unwrap()); prop_assert_eq!(felts, felts_roundtrip); } @@ -37,10 +37,10 @@ proptest! { // Generate random field elements let associated_data: Vec = (0..associated_data_len) - .map(|_| Felt::new(rng.next_u64())) + .map(|_| Felt::new_unchecked(rng.next_u64())) .collect(); let data: Vec = (0..data_len) - .map(|_| Felt::new(rng.next_u64())) + .map(|_| Felt::new_unchecked(rng.next_u64())) .collect(); let encrypted = key.encrypt_elements_with_nonce(&data, &associated_data, nonce).unwrap(); @@ -62,10 +62,10 @@ proptest! { // Generate random field elements let associated_data: Vec = (0..associated_data_len) - .map(|_| Felt::new(rng.next_u64())) + .map(|_| Felt::new_unchecked(rng.next_u64())) .collect(); let data: Vec = (0..data_len) - .map(|_| Felt::new(rng.next_u64())) + .map(|_| Felt::new_unchecked(rng.next_u64())) .collect(); let encrypted = key.encrypt_elements_with_nonce(&data, &associated_data, nonce).unwrap(); @@ -117,10 +117,10 @@ proptest! { let nonce2 = Nonce::from(nonce_word); let associated_data: Vec = associated_data.into_iter() - .map(Felt::new) + .map(Felt::new_unchecked) .collect(); let data: Vec = data.into_iter() - .map(Felt::new) + .map(Felt::new_unchecked) .collect(); let encrypted1 = key1.encrypt_elements_with_nonce(&data, &associated_data, nonce1).unwrap(); @@ -143,10 +143,10 @@ proptest! { let nonce2 = Nonce::from([ONE; 4]); let associated_data: Vec = associated_data.into_iter() - .map(Felt::new) + .map(Felt::new_unchecked) .collect(); let data: Vec = data.into_iter() - .map(Felt::new) + .map(Felt::new_unchecked) .collect(); let encrypted1 = key.encrypt_elements_with_nonce(&data,&associated_data, nonce1).unwrap(); @@ -218,7 +218,7 @@ fn test_single_element_encryption() { let nonce = Nonce::with_rng(&mut rng); let associated_data: Vec = vec![ZERO; 8]; - let data = vec![Felt::new(42)]; + let data = vec![Felt::new_unchecked(42)]; let encrypted = key.encrypt_elements_with_nonce(&data, &associated_data, nonce).unwrap(); let decrypted = key.decrypt_elements_with_associated_data(&encrypted, &associated_data).unwrap(); @@ -237,7 +237,7 @@ fn test_large_data_encryption() { let associated_data: Vec = vec![ONE; 8]; // Test with data larger than rate - let data: Vec = (0..100).map(|i| Felt::new(i as u64)).collect(); + let data: Vec = (0..100).map(|i| Felt::new_unchecked(i as u64)).collect(); let encrypted = key.encrypt_elements_with_nonce(&data, &associated_data, nonce).unwrap(); let decrypted = @@ -254,7 +254,7 @@ fn test_encryption_various_lengths() { let associated_data: Vec = vec![ONE; 8]; for len in [1, 7, 8, 9, 15, 16, 17, 31, 32, 35, 39, 54, 67, 100, 1000] { - let data: Vec = (0..len).map(|i| Felt::new(i as u64)).collect(); + let data: Vec = (0..len).map(|i| Felt::new_unchecked(i as u64)).collect(); let nonce = Nonce::with_rng(&mut rng); let encrypted = key.encrypt_elements_with_nonce(&data, &associated_data, nonce).unwrap(); @@ -294,7 +294,7 @@ fn test_ciphertext_tampering_detection() { let nonce = Nonce::with_rng(&mut rng); let associated_data: Vec = vec![ONE; 8]; - let data = vec![Felt::new(123), Felt::new(456)]; + let data = vec![Felt::new_unchecked(123), Felt::new_unchecked(456)]; let mut encrypted = key.encrypt_elements_with_nonce(&data, &associated_data, nonce).unwrap(); // Tamper with ciphertext @@ -312,7 +312,7 @@ fn test_auth_tag_tampering_detection() { let nonce = Nonce::with_rng(&mut rng); let associated_data: Vec = vec![ONE; 8]; - let data = vec![Felt::new(123), Felt::new(456)]; + let data = vec![Felt::new_unchecked(123), Felt::new_unchecked(456)]; let mut encrypted = key.encrypt_elements_with_nonce(&data, &associated_data, nonce).unwrap(); // Tamper with auth tag @@ -333,7 +333,7 @@ fn test_wrong_key_detection() { let nonce = Nonce::with_rng(&mut rng); let associated_data: Vec = vec![ONE; 8]; - let data = vec![Felt::new(123), Felt::new(456)]; + let data = vec![Felt::new_unchecked(123), Felt::new_unchecked(456)]; let encrypted = key1.encrypt_elements_with_nonce(&data, &associated_data, nonce).unwrap(); // Try to decrypt with wrong key @@ -350,7 +350,7 @@ fn test_wrong_nonce_detection() { let nonce2 = Nonce::with_rng(&mut rng); let associated_data: Vec = vec![ONE; 8]; - let data = vec![Felt::new(123), Felt::new(456)]; + let data = vec![Felt::new_unchecked(123), Felt::new_unchecked(456)]; let mut encrypted = key.encrypt_elements_with_nonce(&data, &associated_data, nonce1).unwrap(); // Try to decrypt with wrong nonce @@ -453,7 +453,7 @@ mod security_tests { assert_eq!(key1, key2); // Should produce same ciphertext - let plaintext = vec![Felt::new(42), Felt::new(100)]; + let plaintext = vec![Felt::new_unchecked(42), Felt::new_unchecked(100)]; let nonce = Nonce::with_rng(&mut rng); let encrypted1 = key1.encrypt_elements_with_nonce(&plaintext, &[], nonce.clone()).unwrap(); diff --git a/miden-crypto/src/aead/xchacha/mod.rs b/miden-crypto/src/aead/xchacha/mod.rs index b2966c0924..1d3b58af33 100644 --- a/miden-crypto/src/aead/xchacha/mod.rs +++ b/miden-crypto/src/aead/xchacha/mod.rs @@ -73,7 +73,7 @@ impl Nonce { // the `rand` dependency matching ours use chacha20poly1305::aead::rand_core::SeedableRng; let mut seed = [0_u8; 32]; - rand::RngCore::fill_bytes(rng, &mut seed); + RngCore::fill_bytes(rng, &mut seed); let rng = rand_hc::Hc128Rng::from_seed(seed); Nonce { @@ -123,7 +123,7 @@ impl SecretKey { // the `rand` dependency matching ours use chacha20poly1305::aead::rand_core::SeedableRng; let mut seed = [0_u8; 32]; - rand::RngCore::fill_bytes(rng, &mut seed); + RngCore::fill_bytes(rng, &mut seed); let rng = rand_hc::Hc128Rng::from_seed(seed); let key = XChaCha20Poly1305::generate_key(rng); @@ -348,7 +348,7 @@ impl AeadScheme for XChaCha { .map_err(|_| EncryptionError::FailedOperation) } - fn encrypt_bytes( + fn encrypt_bytes( key: &Self::Key, rng: &mut R, plaintext: &[u8], diff --git a/miden-crypto/src/aead/xchacha/test.rs b/miden-crypto/src/aead/xchacha/test.rs index e5e5e6017c..0e768c10e3 100644 --- a/miden-crypto/src/aead/xchacha/test.rs +++ b/miden-crypto/src/aead/xchacha/test.rs @@ -23,7 +23,7 @@ proptest! { // Generate random field elements let data: Vec = (0..data_len) - .map(|_| Felt::new(rng.random::())) + .map(|_| Felt::new_unchecked(rng.random::())) .collect(); let encrypted = key.encrypt_elements_with_nonce(&data, &[], nonce).unwrap(); @@ -44,10 +44,10 @@ proptest! { // Generate random field elements let associated_data: Vec = (0..associated_data_len) - .map(|_| Felt::new(rng.random::())) + .map(|_| Felt::new_unchecked(rng.random::())) .collect(); let data: Vec = (0..data_len) - .map(|_| Felt::new(rng.random::())) + .map(|_| Felt::new_unchecked(rng.random::())) .collect(); let encrypted = key.encrypt_elements_with_nonce(&data, &associated_data, nonce).unwrap(); diff --git a/miden-crypto/src/dsa/ecdsa_k256_keccak/mod.rs b/miden-crypto/src/dsa/ecdsa_k256_keccak/mod.rs index 47a116b841..e0ec2469e9 100644 --- a/miden-crypto/src/dsa/ecdsa_k256_keccak/mod.rs +++ b/miden-crypto/src/dsa/ecdsa_k256_keccak/mod.rs @@ -5,7 +5,8 @@ use alloc::{string::ToString, vec::Vec}; use k256::{ ecdh::diffie_hellman, - ecdsa::{RecoveryId, SigningKey, VerifyingKey, signature::hazmat::PrehashVerifier}, + ecdsa, + ecdsa::{RecoveryId, VerifyingKey, signature::hazmat::PrehashVerifier}, pkcs8::DecodePublicKey, }; use miden_crypto_derive::{SilentDebug, SilentDisplay}; @@ -22,7 +23,6 @@ use crate::{ }, }; -#[cfg(test)] mod tests; // CONSTANTS @@ -44,32 +44,23 @@ const SCALARS_SIZE_BYTES: usize = 32; /// Secret key for ECDSA signature verification over secp256k1 curve. #[derive(Clone, SilentDebug, SilentDisplay)] -pub struct SecretKey { - inner: SigningKey, +struct SecretKey { + inner: ecdsa::SigningKey, } impl SecretKey { - /// Generates a new random secret key using the OS random number generator. - #[cfg(feature = "std")] - #[allow(clippy::new_without_default)] - pub fn new() -> Self { - let mut rng = rand::rng(); - - Self::with_rng(&mut rng) - } - /// Generates a new secret key using the provided random number generator. - pub fn with_rng(rng: &mut R) -> Self { + fn with_rng(rng: &mut R) -> Self { // we use a seedable CSPRNG and seed it with `rng` // this is a work around the fact that the version of the `rand` dependency in our crate // is different than the one used in the `k256` one. This solution will no longer be needed // once `k256` gets a new release with a version of the `rand` dependency matching ours use k256::elliptic_curve::rand_core::SeedableRng; let mut seed = [0_u8; 32]; - rand::RngCore::fill_bytes(rng, &mut seed); + RngCore::fill_bytes(rng, &mut seed); let mut rng = rand_hc::Hc128Rng::from_seed(seed); - let signing_key = SigningKey::random(&mut rng); + let signing_key = ecdsa::SigningKey::random(&mut rng); // Zeroize the seed to prevent leaking secret material seed.zeroize(); @@ -78,19 +69,19 @@ impl SecretKey { } /// Gets the corresponding public key for this secret key. - pub fn public_key(&self) -> PublicKey { + fn public_key(&self) -> PublicKey { let verifying_key = self.inner.verifying_key(); PublicKey { inner: *verifying_key } } /// Signs a message (represented as a Word) with this secret key. - pub fn sign(&self, message: Word) -> Signature { + fn sign(&self, message: Word) -> Signature { let message_digest = hash_message(message); self.sign_prehash(message_digest) } /// Signs a pre-hashed message with this secret key. - pub fn sign_prehash(&self, message_digest: [u8; 32]) -> Signature { + fn sign_prehash(&self, message_digest: [u8; 32]) -> Signature { let (signature_inner, recovery_id) = self .inner .sign_prehash_recoverable(&message_digest) @@ -107,7 +98,7 @@ impl SecretKey { /// Computes a Diffie-Hellman shared secret from this secret key and the ephemeral public key /// generated by the other party. - pub fn get_shared_secret(&self, pk_e: EphemeralPublicKey) -> SharedSecret { + fn get_shared_secret(&self, pk_e: EphemeralPublicKey) -> SharedSecret { let shared_secret_inner = diffie_hellman(self.inner.as_nonzero_scalar(), pk_e.as_affine()); SharedSecret::new(shared_secret_inner) @@ -127,6 +118,124 @@ impl PartialEq for SecretKey { impl Eq for SecretKey {} +// SIGNING KEY +// ================================================================================================ + +/// A secret key for ECDSA signature verification over the secp256k1 curve. +#[derive(Clone, Eq, PartialEq, SilentDebug, SilentDisplay)] // Safe as SecretKey has const-time eq +pub struct SigningKey(SecretKey); + +impl SigningKey { + /// Generates a new random signing key using the OS random number generator. + /// + /// This is cryptographically secure as long as [`rand::rng`] remains so. + #[cfg(feature = "std")] + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + let mut rng = rand::rng(); + Self::with_rng(&mut rng) + } + + /// Generates a new signing key using the provided random number generator. + pub fn with_rng(rng: &mut R) -> Self { + Self(SecretKey::with_rng(rng)) + } + + /// Gets the public key that corresponds to this signing key. + pub fn public_key(&self) -> PublicKey { + self.0.public_key() + } + + /// Signs a message (represented as a word) with this signing key. + pub fn sign(&self, message: Word) -> Signature { + self.0.sign(message) + } + + /// Signs a pre-hashed message with this signing key. + pub fn sign_prehash(&self, message_digest: [u8; 32]) -> Signature { + self.0.sign_prehash(message_digest) + } +} + +impl From for SigningKey { + fn from(secret_key: SecretKey) -> Self { + Self(secret_key) + } +} + +// SAFETY: The inner `SecretKey` already implements `ZeroizeOnDrop` which ensures that the secret +// key material is securely zeroized when dropped. +impl ZeroizeOnDrop for SigningKey {} + +impl Serializable for SigningKey { + fn write_into(&self, target: &mut W) { + self.0.write_into(target); + } +} + +impl Deserializable for SigningKey { + fn read_from(source: &mut R) -> Result { + Ok(Self(SecretKey::read_from(source)?)) + } +} + +// KEY EXCHANGE KEY +// ================================================================================================ + +/// A secret key for ECDH key-exchange over the secp256k1 curve. +#[derive(Clone, Eq, PartialEq, SilentDebug, SilentDisplay)] // Safe as SecretKey has const-time eq +pub struct KeyExchangeKey(SecretKey); + +impl KeyExchangeKey { + /// Generates a new random key exchange key using the OS random number generator. + /// + /// This is cryptographically secure as long as [`rand::rng`] remains so. + #[cfg(feature = "std")] + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + let mut rng = rand::rng(); + Self::with_rng(&mut rng) + } + + /// Generates a new signing key using the provided random number generator. + pub fn with_rng(rng: &mut R) -> Self { + Self(SecretKey::with_rng(rng)) + } + + /// Gets the public key that corresponds to this key exchange key. + pub fn public_key(&self) -> PublicKey { + self.0.public_key() + } + + /// Computes a Diffie-Hellman shared secret from this secret key and the ephemeral public key + /// generated by the other party. + pub fn get_shared_secret(&self, pk_e: EphemeralPublicKey) -> SharedSecret { + self.0.get_shared_secret(pk_e) + } +} + +impl From for KeyExchangeKey { + fn from(value: SecretKey) -> Self { + Self(value) + } +} + +// SAFETY: The inner `SecretKey` already implements `ZeroizeOnDrop` which ensures that the secret +// key material is securely zeroized when dropped. +impl ZeroizeOnDrop for KeyExchangeKey {} + +impl Serializable for KeyExchangeKey { + fn write_into(&self, target: &mut W) { + self.0.write_into(target); + } +} + +impl Deserializable for KeyExchangeKey { + fn read_from(source: &mut R) -> Result { + Ok(Self(SecretKey::read_from(source)?)) + } +} + // PUBLIC KEY // ================================================================================================ @@ -153,7 +262,7 @@ impl PublicKey { /// Verifies a signature against this public key and pre-hashed message. pub fn verify_prehash(&self, message_digest: [u8; 32], signature: &Signature) -> bool { - let signature_inner = k256::ecdsa::Signature::from_scalars(*signature.r(), *signature.s()); + let signature_inner = ecdsa::Signature::from_scalars(*signature.r(), *signature.s()); match signature_inner { Ok(signature) => self.inner.verify_prehash(&message_digest, &signature).is_ok(), @@ -165,10 +274,10 @@ impl PublicKey { /// message. pub fn recover_from(message: Word, signature: &Signature) -> Result { let message_digest = hash_message(message); - let signature_data = k256::ecdsa::Signature::from_scalars(*signature.r(), *signature.s()) + let signature_data = ecdsa::Signature::from_scalars(*signature.r(), *signature.s()) .map_err(|_| PublicKeyError::RecoveryFailed)?; - let verifying_key = k256::ecdsa::VerifyingKey::recover_from_prehash( + let verifying_key = VerifyingKey::recover_from_prehash( &message_digest, &signature_data, RecoveryId::from_byte(signature.v()).ok_or(PublicKeyError::RecoveryFailed)?, @@ -290,7 +399,7 @@ impl Signature { return Err(DeserializationError::InvalidValue(r#"Invalid recovery ID"#.to_string())); } - let sig = k256::ecdsa::Signature::from_der(bytes) + let sig = ecdsa::Signature::from_der(bytes) .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?; // Normalize signature into "low s" form. @@ -333,7 +442,7 @@ impl Deserializable for SecretKey { fn read_from(source: &mut R) -> Result { let mut bytes: [u8; SECRET_KEY_BYTES] = source.read_array()?; - let signing_key = SigningKey::from_slice(&bytes) + let signing_key = ecdsa::SigningKey::from_slice(&bytes) .map_err(|_| DeserializationError::InvalidValue("Invalid secret key".to_string()))?; bytes.zeroize(); diff --git a/miden-crypto/src/dsa/ecdsa_k256_keccak/tests.rs b/miden-crypto/src/dsa/ecdsa_k256_keccak/tests.rs index fce5fb2691..9cf7f6ede4 100644 --- a/miden-crypto/src/dsa/ecdsa_k256_keccak/tests.rs +++ b/miden-crypto/src/dsa/ecdsa_k256_keccak/tests.rs @@ -1,415 +1,519 @@ -use super::*; -use crate::{Felt, rand::test_utils::seeded_rng}; - -#[test] -fn test_key_generation() { - let mut rng = seeded_rng([0u8; 32]); - - let secret_key = SecretKey::with_rng(&mut rng); - let public_key = secret_key.public_key(); - - // Test that we can convert to/from bytes - let sk_bytes = secret_key.to_bytes(); - let recovered_sk = SecretKey::read_from_bytes(&sk_bytes).unwrap(); - assert_eq!(secret_key.to_bytes(), recovered_sk.to_bytes()); - - let pk_bytes = public_key.to_bytes(); - let recovered_pk = PublicKey::read_from_bytes(&pk_bytes).unwrap(); - assert_eq!(public_key, recovered_pk); -} - -#[test] -fn test_public_key_recovery() { - let mut rng = seeded_rng([1u8; 32]); - - let secret_key = SecretKey::with_rng(&mut rng); - let public_key = secret_key.public_key(); - - // Generate a signature using the secret key - let message = [Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)].into(); - let signature = secret_key.sign(message); - - // Recover the public key - let recovered_pk = PublicKey::recover_from(message, &signature).unwrap(); - assert_eq!(public_key, recovered_pk); +#![cfg(test)] +mod signing_key { + use miden_field::Felt; + #[cfg(feature = "std")] + use miden_field::Word; + #[cfg(feature = "std")] + use miden_serde_utils::ByteReader; + use miden_serde_utils::{Deserializable, Serializable}; + + use crate::{ + dsa::ecdsa_k256_keccak::{PublicKey, Signature, SigningKey}, + rand::test_utils::seeded_rng, + }; + + #[test] + fn test_key_generation() { + let mut rng = seeded_rng([0u8; 32]); + + let secret_key = SigningKey::with_rng(&mut rng); + let public_key = secret_key.public_key(); + + // Test that we can convert to/from bytes + let sk_bytes = secret_key.to_bytes(); + let recovered_sk = SigningKey::read_from_bytes(&sk_bytes).unwrap(); + assert_eq!(secret_key.to_bytes(), recovered_sk.to_bytes()); + + let pk_bytes = public_key.to_bytes(); + let recovered_pk = PublicKey::read_from_bytes(&pk_bytes).unwrap(); + assert_eq!(public_key, recovered_pk); + } - // Using the wrong message, we shouldn't be able to recover the public key - let message = [Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(5)].into(); - let recovered_pk = PublicKey::recover_from(message, &signature).unwrap(); - assert!(public_key != recovered_pk); -} + #[test] + fn test_public_key_recovery() { + let mut rng = seeded_rng([1u8; 32]); + + let secret_key = SigningKey::with_rng(&mut rng); + let public_key = secret_key.public_key(); + + // Generate a signature using the secret key + let message = [ + Felt::new_unchecked(1), + Felt::new_unchecked(2), + Felt::new_unchecked(3), + Felt::new_unchecked(4), + ] + .into(); + let signature = secret_key.sign(message); + + // Recover the public key + let recovered_pk = PublicKey::recover_from(message, &signature).unwrap(); + assert_eq!(public_key, recovered_pk); + + // Using the wrong message, we shouldn't be able to recover the public key + let message = [ + Felt::new_unchecked(1), + Felt::new_unchecked(2), + Felt::new_unchecked(3), + Felt::new_unchecked(5), + ] + .into(); + let recovered_pk = PublicKey::recover_from(message, &signature).unwrap(); + assert!(public_key != recovered_pk); + } -#[test] -fn test_sign_and_verify() { - let mut rng = seeded_rng([2u8; 32]); + #[test] + fn test_sign_and_verify() { + let mut rng = seeded_rng([2u8; 32]); + + let secret_key = SigningKey::with_rng(&mut rng); + let public_key = secret_key.public_key(); + + let message = [ + Felt::new_unchecked(1), + Felt::new_unchecked(2), + Felt::new_unchecked(3), + Felt::new_unchecked(4), + ] + .into(); + let signature = secret_key.sign(message); + + // Verify using public key method + assert!(public_key.verify(message, &signature)); + + // Verify using signature method + assert!(signature.verify(message, &public_key)); + + // Test with wrong message + let wrong_message = [ + Felt::new_unchecked(5), + Felt::new_unchecked(6), + Felt::new_unchecked(7), + Felt::new_unchecked(8), + ] + .into(); + assert!(!public_key.verify(wrong_message, &signature)); + } - let secret_key = SecretKey::with_rng(&mut rng); - let public_key = secret_key.public_key(); + #[test] + fn test_signature_serialization_default() { + let mut rng = seeded_rng([3u8; 32]); - let message = [Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)].into(); - let signature = secret_key.sign(message); + let secret_key = SigningKey::with_rng(&mut rng); + let message = [ + Felt::new_unchecked(1), + Felt::new_unchecked(2), + Felt::new_unchecked(3), + Felt::new_unchecked(4), + ] + .into(); + let signature = secret_key.sign(message); - // Verify using public key method - assert!(public_key.verify(message, &signature)); + let sig_bytes = signature.to_bytes(); + let recovered_sig = Signature::read_from_bytes(&sig_bytes).unwrap(); - // Verify using signature method - assert!(signature.verify(message, &public_key)); + assert_eq!(signature, recovered_sig); + } - // Test with wrong message - let wrong_message = [Felt::new(5), Felt::new(6), Felt::new(7), Felt::new(8)].into(); - assert!(!public_key.verify(wrong_message, &signature)); -} + #[test] + fn test_signature_serialization() { + let mut rng = seeded_rng([4u8; 32]); + + let secret_key = SigningKey::with_rng(&mut rng); + let message = [ + Felt::new_unchecked(1), + Felt::new_unchecked(2), + Felt::new_unchecked(3), + Felt::new_unchecked(4), + ] + .into(); + let signature = secret_key.sign(message); + let recovery_id = signature.v(); + + let sig_bytes = signature.to_sec1_bytes(); + let recovered_sig = + Signature::from_sec1_bytes_and_recovery_id(sig_bytes, recovery_id).unwrap(); + + assert_eq!(signature, recovered_sig); + + let recovery_id = (recovery_id + 1) % 4; + let recovered_sig = + Signature::from_sec1_bytes_and_recovery_id(sig_bytes, recovery_id).unwrap(); + assert_ne!(signature, recovered_sig); + + let recovered_sig = + Signature::from_sec1_bytes_and_recovery_id(sig_bytes, recovery_id).unwrap(); + assert_ne!(signature, recovered_sig); + } -#[test] -fn test_signature_serialization_default() { - let mut rng = seeded_rng([3u8; 32]); + #[test] + fn test_secret_key_debug_redaction() { + let mut rng = seeded_rng([5u8; 32]); + let secret_key = SigningKey::with_rng(&mut rng); - let secret_key = SecretKey::with_rng(&mut rng); - let message = [Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)].into(); - let signature = secret_key.sign(message); + // Verify Debug impl produces expected redacted output + let debug_output = format!("{secret_key:?}"); + assert_eq!(debug_output, ""); - let sig_bytes = signature.to_bytes(); - let recovered_sig = Signature::read_from_bytes(&sig_bytes).unwrap(); + // Verify Display impl also elides + let display_output = format!("{secret_key}"); + assert_eq!(display_output, ""); + } - assert_eq!(signature, recovered_sig); + #[cfg(feature = "std")] + #[test] + fn test_signature_serde() { + use crate::utils::SliceReader; + let sig0 = SigningKey::new().sign(Word::from([5, 0, 0, 0u32])); + let sig_bytes = sig0.to_bytes(); + let mut slice_reader = SliceReader::new(&sig_bytes); + let sig0_deserialized = Signature::read_from(&mut slice_reader).unwrap(); + + assert!(!slice_reader.has_more_bytes()); + assert_eq!(sig0, sig0_deserialized); + } } -#[test] -fn test_signature_serialization() { - let mut rng = seeded_rng([4u8; 32]); +mod key_exchange_key { + use miden_serde_utils::{Deserializable, Serializable}; - let secret_key = SecretKey::with_rng(&mut rng); - let message = [Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)].into(); - let signature = secret_key.sign(message); - let recovery_id = signature.v(); + use crate::{ + dsa::ecdsa_k256_keccak::{KeyExchangeKey, PublicKey}, + rand::test_utils::seeded_rng, + }; - let sig_bytes = signature.to_sec1_bytes(); - let recovered_sig = Signature::from_sec1_bytes_and_recovery_id(sig_bytes, recovery_id).unwrap(); + #[test] + fn test_key_generation() { + let mut rng = seeded_rng([0u8; 32]); - assert_eq!(signature, recovered_sig); + let secret_key = KeyExchangeKey::with_rng(&mut rng); + let public_key = secret_key.public_key(); - let recovery_id = (recovery_id + 1) % 4; - let recovered_sig = Signature::from_sec1_bytes_and_recovery_id(sig_bytes, recovery_id).unwrap(); - assert_ne!(signature, recovered_sig); + // Test that we can convert to/from bytes + let sk_bytes = secret_key.to_bytes(); + let recovered_sk = KeyExchangeKey::read_from_bytes(&sk_bytes).unwrap(); + assert_eq!(secret_key.to_bytes(), recovered_sk.to_bytes()); - let recovered_sig = Signature::from_sec1_bytes_and_recovery_id(sig_bytes, recovery_id).unwrap(); - assert_ne!(signature, recovered_sig); -} + let pk_bytes = public_key.to_bytes(); + let recovered_pk = PublicKey::read_from_bytes(&pk_bytes).unwrap(); + assert_eq!(public_key, recovered_pk); + } -#[test] -fn test_secret_key_debug_redaction() { - let mut rng = seeded_rng([5u8; 32]); - let secret_key = SecretKey::with_rng(&mut rng); + #[test] + fn test_secret_key_debug_redaction() { + let mut rng = seeded_rng([5u8; 32]); + let secret_key = KeyExchangeKey::with_rng(&mut rng); - // Verify Debug impl produces expected redacted output - let debug_output = format!("{secret_key:?}"); - assert_eq!(debug_output, ""); + // Verify Debug impl produces expected redacted output + let debug_output = format!("{secret_key:?}"); + assert_eq!(debug_output, ""); - // Verify Display impl also elides - let display_output = format!("{secret_key}"); - assert_eq!(display_output, ""); -} - -#[cfg(feature = "std")] -#[test] -fn test_signature_serde() { - use crate::utils::SliceReader; - let sig0 = SecretKey::new().sign(Word::from([5, 0, 0, 0u32])); - let sig_bytes = sig0.to_bytes(); - let mut slice_reader = SliceReader::new(&sig_bytes); - let sig0_deserialized = Signature::read_from(&mut slice_reader).unwrap(); - - assert!(!slice_reader.has_more_bytes()); - assert_eq!(sig0, sig0_deserialized); -} - -#[test] -fn test_signature_from_der_success() { - // DER-encoded form of an ASN.1 SEQUENCE containing two INTEGER values. - let der: [u8; 8] = [ - 0x30, 0x06, // Sequence tag and length of sequence contents. - 0x02, 0x01, 0x01, // Integer 1. - 0x02, 0x01, 0x09, // Integer 2. - ]; - let v = 2u8; - - let sig = Signature::from_der(&der, v).expect("from_der should parse valid DER"); - - // Expect r = 1 and s = 9 in 32-byte big-endian form. - let mut expected_r = [0u8; 32]; - expected_r[31] = 1; - let mut expected_s = [0u8; 32]; - expected_s[31] = 9; - - assert_eq!(sig.r(), &expected_r); - assert_eq!(sig.s(), &expected_s); - assert_eq!(sig.v(), v); + // Verify Display impl also elides + let display_output = format!("{secret_key}"); + assert_eq!(display_output, ""); + } } -#[test] -fn test_signature_from_der_recovery_id_variation() { - // DER encoding with two integers both equal to 1. - let der: [u8; 8] = [0x30, 0x06, 0x02, 0x01, 0x01, 0x02, 0x01, 0x01]; +mod signature { + use alloc::vec::Vec; + + use miden_serde_utils::{DeserializationError, Serializable}; + + use crate::{ + dsa::ecdsa_k256_keccak::{PublicKey, SecretKey, Signature, SigningKey}, + rand::test_utils::seeded_rng, + }; + + #[test] + fn test_signature_from_der_success() { + // DER-encoded form of an ASN.1 SEQUENCE containing two INTEGER values. + let der: [u8; 8] = [ + 0x30, 0x06, // Sequence tag and length of sequence contents. + 0x02, 0x01, 0x01, // Integer 1. + 0x02, 0x01, 0x09, // Integer 2. + ]; + let v = 2u8; + + let sig = Signature::from_der(&der, v).expect("from_der should parse valid DER"); + + // Expect r = 1 and s = 9 in 32-byte big-endian form. + let mut expected_r = [0u8; 32]; + expected_r[31] = 1; + let mut expected_s = [0u8; 32]; + expected_s[31] = 9; + + assert_eq!(sig.r(), &expected_r); + assert_eq!(sig.s(), &expected_s); + assert_eq!(sig.v(), v); + } - let sig_v0 = Signature::from_der(&der, 0).unwrap(); - let sig_v3 = Signature::from_der(&der, 3).unwrap(); + #[test] + fn test_signature_from_der_recovery_id_variation() { + // DER encoding with two integers both equal to 1. + let der: [u8; 8] = [0x30, 0x06, 0x02, 0x01, 0x01, 0x02, 0x01, 0x01]; - // r and s must be identical; v differs, so signatures should not be equal. - assert_eq!(sig_v0.r(), sig_v3.r()); - assert_eq!(sig_v0.s(), sig_v3.s()); - assert_ne!(sig_v0.v(), sig_v3.v()); - assert_ne!(sig_v0, sig_v3); -} + let sig_v0 = Signature::from_der(&der, 0).unwrap(); + let sig_v3 = Signature::from_der(&der, 3).unwrap(); -#[test] -fn test_signature_from_der_invalid() { - // Empty input should fail at DER parsing stage (der error). - match Signature::from_der(&[], 0) { - Err(super::DeserializationError::InvalidValue(_)) => {}, - other => panic!("expected InvalidValue for empty DER, got {:?}", other), + // r and s must be identical; v differs, so signatures should not be equal. + assert_eq!(sig_v0.r(), sig_v3.r()); + assert_eq!(sig_v0.s(), sig_v3.s()); + assert_ne!(sig_v0.v(), sig_v3.v()); + assert_ne!(sig_v0, sig_v3); } - // Malformed/truncated DER should also fail. - let der_bad: [u8; 2] = [0x30, 0x01]; - match Signature::from_der(&der_bad, 0) { - Err(super::DeserializationError::InvalidValue(_)) => {}, - other => panic!("expected InvalidValue for malformed DER, got {:?}", other), + #[test] + fn test_signature_from_der_invalid() { + // Empty input should fail at DER parsing stage (der error). + match Signature::from_der(&[], 0) { + Err(DeserializationError::InvalidValue(_)) => {}, + other => panic!("expected InvalidValue for empty DER, got {:?}", other), + } + + // Malformed/truncated DER should also fail. + let der_bad: [u8; 2] = [0x30, 0x01]; + match Signature::from_der(&der_bad, 0) { + Err(DeserializationError::InvalidValue(_)) => {}, + other => panic!("expected InvalidValue for malformed DER, got {:?}", other), + } } -} - -#[test] -fn test_signature_from_der_high_s_normalizes_and_flips_v() { - // Construct a DER signature with r = 3 and s = n - 2 (high-S), which requires a leading 0x00 - // in DER to force a positive INTEGER. - // - // secp256k1 curve order (n): - // n = FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141 - // We set s = n - 2 = ... D036413F (> n/2), so normalize_s() should trigger and flip recovery - // id. - let der: [u8; 40] = [ - 0x30, 0x26, // SEQUENCE, length 38 - 0x02, 0x01, 0x03, // INTEGER r = 3 - 0x02, 0x21, 0x00, // INTEGER s, length 33 with leading 0x00 to keep positive - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0xfe, 0xba, 0xae, 0xdc, 0xe6, 0xaf, 0x48, 0xa0, 0x3b, 0xbf, 0xd2, 0x5e, 0x8c, 0xd0, 0x36, - 0x41, 0x3f, - ]; - let v_initial: u8 = 2; - let sig = Signature::from_der(&der, v_initial).expect("from_der should parse valid high-S DER"); - - // After normalization: - // - v should have its parity bit flipped (XOR with 1). - // - s should be normalized to low-s; since s = n - 2, the normalized s is 2. - let mut expected_r = [0u8; 32]; - expected_r[31] = 3; - let mut expected_s_low = [0u8; 32]; - expected_s_low[31] = 2; - - assert_eq!(sig.r(), &expected_r); - assert_eq!(sig.s(), &expected_s_low); - assert_eq!(sig.v(), v_initial ^ 1); -} -#[test] -fn test_public_key_from_der_success() { - // Build a valid SPKI DER for the compressed SEC1 point of our generated key. - let mut rng = seeded_rng([9u8; 32]); - let secret_key = SecretKey::with_rng(&mut rng); - let public_key = secret_key.public_key(); - let public_key_bytes = public_key.to_bytes(); // compressed SEC1 (33 bytes). - - // AlgorithmIdentifier: id-ecPublicKey + secp256k1 - let algo: [u8; 18] = [ - 0x30, 0x10, // SEQUENCE, length 16 - 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, // OID 1.2.840.10045.2.1 - 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x0a, // OID 1.3.132.0.10 (secp256k1) - ]; - - // subjectPublicKey BIT STRING: 0 unused bits + compressed SEC1. - let mut spk = Vec::with_capacity(2 + 1 + public_key_bytes.len()); - spk.push(0x03); // BIT STRING - spk.push((1 + public_key_bytes.len()) as u8); // length - spk.push(0x00); // unused bits = 0 - spk.extend_from_slice(&public_key_bytes); - - // Outer SEQUENCE. - let mut der = Vec::with_capacity(2 + algo.len() + spk.len()); - der.push(0x30); // SEQUENCE - der.push((algo.len() + spk.len()) as u8); // total length - der.extend_from_slice(&algo); - der.extend_from_slice(&spk); - - let parsed = PublicKey::from_der(&der).expect("should parse valid SPKI DER"); - assert_eq!(parsed, public_key); -} + #[test] + fn test_signature_from_der_high_s_normalizes_and_flips_v() { + // Construct a DER signature with r = 3 and s = n - 2 (high-S), which requires a leading + // 0x00 in DER to force a positive INTEGER. + // + // secp256k1 curve order (n): + // n = FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141 + // We set s = n - 2 = ... D036413F (> n/2), so normalize_s() should trigger and flip + // recovery id. + let der: [u8; 40] = [ + 0x30, 0x26, // SEQUENCE, length 38 + 0x02, 0x01, 0x03, // INTEGER r = 3 + 0x02, 0x21, 0x00, // INTEGER s, length 33 with leading 0x00 to keep positive + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xfe, 0xba, 0xae, 0xdc, 0xe6, 0xaf, 0x48, 0xa0, 0x3b, 0xbf, 0xd2, 0x5e, 0x8c, + 0xd0, 0x36, 0x41, 0x3f, + ]; + let v_initial: u8 = 2; + let sig = + Signature::from_der(&der, v_initial).expect("from_der should parse valid high-S DER"); + + // After normalization: + // - v should have its parity bit flipped (XOR with 1). + // - s should be normalized to low-s; since s = n - 2, the normalized s is 2. + let mut expected_r = [0u8; 32]; + expected_r[31] = 3; + let mut expected_s_low = [0u8; 32]; + expected_s_low[31] = 2; + + assert_eq!(sig.r(), &expected_r); + assert_eq!(sig.s(), &expected_s_low); + assert_eq!(sig.v(), v_initial ^ 1); + } -#[test] -fn test_public_key_from_der_invalid() { - // Empty DER. - match PublicKey::from_der(&[]) { - Err(super::DeserializationError::InvalidValue(_)) => {}, - other => panic!("expected InvalidValue for empty DER, got {:?}", other), + #[test] + fn test_public_key_from_der_success() { + // Build a valid SPKI DER for the compressed SEC1 point of our generated key. + let mut rng = seeded_rng([9u8; 32]); + let secret_key = SigningKey::with_rng(&mut rng); + let public_key = secret_key.public_key(); + let public_key_bytes = public_key.to_bytes(); // compressed SEC1 (33 bytes). + + // AlgorithmIdentifier: id-ecPublicKey + secp256k1 + let algo: [u8; 18] = [ + 0x30, 0x10, // SEQUENCE, length 16 + 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, // OID 1.2.840.10045.2.1 + 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x0a, // OID 1.3.132.0.10 (secp256k1) + ]; + + // subjectPublicKey BIT STRING: 0 unused bits + compressed SEC1. + let mut spk = Vec::with_capacity(2 + 1 + public_key_bytes.len()); + spk.push(0x03); // BIT STRING + spk.push((1 + public_key_bytes.len()) as u8); // length + spk.push(0x00); // unused bits = 0 + spk.extend_from_slice(&public_key_bytes); + + // Outer SEQUENCE. + let mut der = Vec::with_capacity(2 + algo.len() + spk.len()); + der.push(0x30); // SEQUENCE + der.push((algo.len() + spk.len()) as u8); // total length + der.extend_from_slice(&algo); + der.extend_from_slice(&spk); + + let parsed = PublicKey::from_der(&der).expect("should parse valid SPKI DER"); + assert_eq!(parsed, public_key); } - // Malformed: SEQUENCE with zero length (missing fields). - let der_bad: [u8; 2] = [0x30, 0x00]; - match PublicKey::from_der(&der_bad) { - Err(super::DeserializationError::InvalidValue(_)) => {}, - other => panic!("expected InvalidValue for malformed DER, got {:?}", other), + #[test] + fn test_public_key_from_der_invalid() { + // Empty DER. + match PublicKey::from_der(&[]) { + Err(DeserializationError::InvalidValue(_)) => {}, + other => panic!("expected InvalidValue for empty DER, got {:?}", other), + } + + // Malformed: SEQUENCE with zero length (missing fields). + let der_bad: [u8; 2] = [0x30, 0x00]; + match PublicKey::from_der(&der_bad) { + Err(DeserializationError::InvalidValue(_)) => {}, + other => panic!("expected InvalidValue for malformed DER, got {:?}", other), + } } -} -#[test] -fn test_public_key_from_der_rejects_non_canonical_long_form_length() { - // Build a valid SPKI structure but encode the outer SEQUENCE length using non-canonical - // long-form (0x81 ) even though the length < 128. DER should reject this. - let mut rng = seeded_rng([10u8; 32]); - let secret_key = SecretKey::with_rng(&mut rng); - let public_key = secret_key.public_key(); - let public_key_bytes = public_key.to_bytes(); // compressed SEC1 (33 bytes) - - // AlgorithmIdentifier: id-ecPublicKey + secp256k1 - let algo: [u8; 18] = [ - 0x30, 0x10, // SEQUENCE, length 16 - 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, // OID 1.2.840.10045.2.1 - 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x0a, // OID 1.3.132.0.10 (secp256k1) - ]; - - // subjectPublicKey BIT STRING: 0 unused bits + compressed SEC1 - let mut spk = Vec::with_capacity(2 + 1 + public_key_bytes.len()); - spk.push(0x03); // BIT STRING - spk.push((1 + public_key_bytes.len()) as u8); // length - spk.push(0x00); // unused bits = 0 - spk.extend_from_slice(&public_key_bytes); - - // Outer SEQUENCE using non-canonical long-form length (0x81) - let total_len = (algo.len() + spk.len()) as u8; // fits in one byte - let mut der = Vec::with_capacity(3 + algo.len() + spk.len()); - der.push(0x30); // SEQUENCE - der.push(0x81); // long-form length marker with one subsequent length byte - der.push(total_len); - der.extend_from_slice(&algo); - der.extend_from_slice(&spk); - - match PublicKey::from_der(&der) { - Err(super::DeserializationError::InvalidValue(_)) => {}, - other => { - panic!("expected InvalidValue for non-canonical long-form length, got {:?}", other) - }, + #[test] + fn test_public_key_from_der_rejects_non_canonical_long_form_length() { + // Build a valid SPKI structure but encode the outer SEQUENCE length using non-canonical + // long-form (0x81 ) even though the length < 128. DER should reject this. + let mut rng = seeded_rng([10u8; 32]); + let secret_key = SecretKey::with_rng(&mut rng); + let public_key = secret_key.public_key(); + let public_key_bytes = public_key.to_bytes(); // compressed SEC1 (33 bytes) + + // AlgorithmIdentifier: id-ecPublicKey + secp256k1 + let algo: [u8; 18] = [ + 0x30, 0x10, // SEQUENCE, length 16 + 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, // OID 1.2.840.10045.2.1 + 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x0a, // OID 1.3.132.0.10 (secp256k1) + ]; + + // subjectPublicKey BIT STRING: 0 unused bits + compressed SEC1 + let mut spk = Vec::with_capacity(2 + 1 + public_key_bytes.len()); + spk.push(0x03); // BIT STRING + spk.push((1 + public_key_bytes.len()) as u8); // length + spk.push(0x00); // unused bits = 0 + spk.extend_from_slice(&public_key_bytes); + + // Outer SEQUENCE using non-canonical long-form length (0x81) + let total_len = (algo.len() + spk.len()) as u8; // fits in one byte + let mut der = Vec::with_capacity(3 + algo.len() + spk.len()); + der.push(0x30); // SEQUENCE + der.push(0x81); // long-form length marker with one subsequent length byte + der.push(total_len); + der.extend_from_slice(&algo); + der.extend_from_slice(&spk); + + match PublicKey::from_der(&der) { + Err(DeserializationError::InvalidValue(_)) => {}, + other => { + panic!("expected InvalidValue for non-canonical long-form length, got {:?}", other) + }, + } } -} -#[test] -fn test_public_key_from_der_rejects_trailing_bytes() { - // Build a valid SPKI DER but append trailing bytes after the sequence; DER should reject. - let mut rng = seeded_rng([11u8; 32]); - let secret_key = SecretKey::with_rng(&mut rng); - let public_key = secret_key.public_key(); - let public_key_bytes = public_key.to_bytes(); // compressed SEC1 (33 bytes) - - // AlgorithmIdentifier: id-ecPublicKey + secp256k1. - let algo: [u8; 18] = [ - 0x30, 0x10, // SEQUENCE, length 16 - 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, // OID 1.2.840.10045.2.1 - 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x0a, // OID 1.3.132.0.10 (secp256k1) - ]; - - // subjectPublicKey BIT STRING: 0 unused bits + compressed SEC1. - let mut spk = Vec::with_capacity(2 + 1 + public_key_bytes.len()); - spk.push(0x03); // BIT STRING - spk.push((1 + public_key_bytes.len()) as u8); // length - spk.push(0x00); // unused bits = 0 - spk.extend_from_slice(&public_key_bytes); - - // Outer SEQUENCE with short-form length. - let total_len = (algo.len() + spk.len()) as u8; - let mut der = Vec::with_capacity(2 + algo.len() + spk.len() + 2); - der.push(0x30); // SEQUENCE - der.push(total_len); - der.extend_from_slice(&algo); - der.extend_from_slice(&spk); - - // Append trailing junk. - der.push(0x00); - der.push(0x00); - - match PublicKey::from_der(&der) { - Err(super::DeserializationError::InvalidValue(_)) => {}, - other => panic!("expected InvalidValue for DER with trailing bytes, got {:?}", other), + #[test] + fn test_public_key_from_der_rejects_trailing_bytes() { + // Build a valid SPKI DER but append trailing bytes after the sequence; DER should reject. + let mut rng = seeded_rng([11u8; 32]); + let secret_key = SecretKey::with_rng(&mut rng); + let public_key = secret_key.public_key(); + let public_key_bytes = public_key.to_bytes(); // compressed SEC1 (33 bytes) + + // AlgorithmIdentifier: id-ecPublicKey + secp256k1. + let algo: [u8; 18] = [ + 0x30, 0x10, // SEQUENCE, length 16 + 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, // OID 1.2.840.10045.2.1 + 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x0a, // OID 1.3.132.0.10 (secp256k1) + ]; + + // subjectPublicKey BIT STRING: 0 unused bits + compressed SEC1. + let mut spk = Vec::with_capacity(2 + 1 + public_key_bytes.len()); + spk.push(0x03); // BIT STRING + spk.push((1 + public_key_bytes.len()) as u8); // length + spk.push(0x00); // unused bits = 0 + spk.extend_from_slice(&public_key_bytes); + + // Outer SEQUENCE with short-form length. + let total_len = (algo.len() + spk.len()) as u8; + let mut der = Vec::with_capacity(2 + algo.len() + spk.len() + 2); + der.push(0x30); // SEQUENCE + der.push(total_len); + der.extend_from_slice(&algo); + der.extend_from_slice(&spk); + + // Append trailing junk. + der.push(0x00); + der.push(0x00); + + match PublicKey::from_der(&der) { + Err(DeserializationError::InvalidValue(_)) => {}, + other => panic!("expected InvalidValue for DER with trailing bytes, got {:?}", other), + } } -} -#[test] -fn test_public_key_from_der_rejects_wrong_curve_oid() { - // Same structure but with prime256v1 (P-256) curve OID instead of secp256k1. - let mut rng = seeded_rng([12u8; 32]); - let secret_key = SecretKey::with_rng(&mut rng); - let public_key = secret_key.public_key(); - let public_key_bytes = public_key.to_bytes(); // compressed SEC1 (33 bytes) - - // AlgorithmIdentifier: id-ecPublicKey + prime256v1 (1.2.840.10045.3.1.7). - // Completed prime256v1 OID tail for correctness - // Full DER OID bytes for 1.2.840.10045.3.1.7 are: 06 08 2A 86 48 CE 3D 03 01 07 - // We'll encode properly below with 8 length, then adjust the outer lengths accordingly. - - // AlgorithmIdentifier with correct OID encoding but wrong curve: - let algo_full: [u8; 21] = [ - 0x30, 0x12, // SEQUENCE, length 18 - 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, // id-ecPublicKey - 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, // prime256v1 - ]; - - // subjectPublicKey BIT STRING. - let mut spk = Vec::with_capacity(2 + 1 + public_key_bytes.len()); - spk.push(0x03); - spk.push((1 + public_key_bytes.len()) as u8); - spk.push(0x00); - spk.extend_from_slice(&public_key_bytes); - - let mut der = Vec::with_capacity(2 + algo_full.len() + spk.len()); - der.push(0x30); - der.push((algo_full.len() + spk.len()) as u8); - der.extend_from_slice(&algo_full); - der.extend_from_slice(&spk); - - match PublicKey::from_der(&der) { - Err(super::DeserializationError::InvalidValue(_)) => {}, - other => panic!("expected InvalidValue for wrong curve OID, got {:?}", other), + #[test] + fn test_public_key_from_der_rejects_wrong_curve_oid() { + // Same structure but with prime256v1 (P-256) curve OID instead of secp256k1. + let mut rng = seeded_rng([12u8; 32]); + let secret_key = SecretKey::with_rng(&mut rng); + let public_key = secret_key.public_key(); + let public_key_bytes = public_key.to_bytes(); // compressed SEC1 (33 bytes) + + // AlgorithmIdentifier: id-ecPublicKey + prime256v1 (1.2.840.10045.3.1.7). + // Completed prime256v1 OID tail for correctness + // Full DER OID bytes for 1.2.840.10045.3.1.7 are: 06 08 2A 86 48 CE 3D 03 01 07 + // We'll encode properly below with 8 length, then adjust the outer lengths accordingly. + + // AlgorithmIdentifier with correct OID encoding but wrong curve: + let algo_full: [u8; 21] = [ + 0x30, 0x12, // SEQUENCE, length 18 + 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, // id-ecPublicKey + 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, // prime256v1 + ]; + + // subjectPublicKey BIT STRING. + let mut spk = Vec::with_capacity(2 + 1 + public_key_bytes.len()); + spk.push(0x03); + spk.push((1 + public_key_bytes.len()) as u8); + spk.push(0x00); + spk.extend_from_slice(&public_key_bytes); + + let mut der = Vec::with_capacity(2 + algo_full.len() + spk.len()); + der.push(0x30); + der.push((algo_full.len() + spk.len()) as u8); + der.extend_from_slice(&algo_full); + der.extend_from_slice(&spk); + + match PublicKey::from_der(&der) { + Err(DeserializationError::InvalidValue(_)) => {}, + other => panic!("expected InvalidValue for wrong curve OID, got {:?}", other), + } } -} -#[test] -fn test_public_key_from_der_rejects_wrong_algorithm_oid() { - // Use rsaEncryption (1.2.840.113549.1.1.1) instead of id-ecPublicKey. - let mut rng = seeded_rng([13u8; 32]); - let secret_key = SecretKey::with_rng(&mut rng); - let public_key = secret_key.public_key(); - let public_key_bytes = public_key.to_bytes(); - - // AlgorithmIdentifier: rsaEncryption + NULL parameter. - // OID bytes for 1.2.840.113549.1.1.1: 06 09 2A 86 48 86 F7 0D 01 01 01. - // NULL parameter: 05 00. - let algo_rsa: [u8; 15] = [ - 0x30, 0x0d, // SEQUENCE, length 13 - 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, // rsaEncryption - 0x05, 0x00, // NULL - ]; - - // subjectPublicKey BIT STRING with EC compressed point (intentionally mismatched with algo). - let mut spk = Vec::with_capacity(2 + 1 + public_key_bytes.len()); - spk.push(0x03); - spk.push((1 + public_key_bytes.len()) as u8); - spk.push(0x00); - spk.extend_from_slice(&public_key_bytes); - - let mut der = Vec::with_capacity(2 + algo_rsa.len() + spk.len()); - der.push(0x30); - der.push((algo_rsa.len() + spk.len()) as u8); - der.extend_from_slice(&algo_rsa); - der.extend_from_slice(&spk); - - match PublicKey::from_der(&der) { - Err(super::DeserializationError::InvalidValue(_)) => {}, - other => panic!("expected InvalidValue for wrong algorithm OID, got {:?}", other), + #[test] + fn test_public_key_from_der_rejects_wrong_algorithm_oid() { + // Use rsaEncryption (1.2.840.113549.1.1.1) instead of id-ecPublicKey. + let mut rng = seeded_rng([13u8; 32]); + let secret_key = SecretKey::with_rng(&mut rng); + let public_key = secret_key.public_key(); + let public_key_bytes = public_key.to_bytes(); + + // AlgorithmIdentifier: rsaEncryption + NULL parameter. + // OID bytes for 1.2.840.113549.1.1.1: 06 09 2A 86 48 86 F7 0D 01 01 01. + // NULL parameter: 05 00. + let algo_rsa: [u8; 15] = [ + 0x30, 0x0d, // SEQUENCE, length 13 + 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, // rsaEncryption + 0x05, 0x00, // NULL + ]; + + // subjectPublicKey BIT STRING with EC compressed point (intentionally mismatched with + // algo). + let mut spk = Vec::with_capacity(2 + 1 + public_key_bytes.len()); + spk.push(0x03); + spk.push((1 + public_key_bytes.len()) as u8); + spk.push(0x00); + spk.extend_from_slice(&public_key_bytes); + + let mut der = Vec::with_capacity(2 + algo_rsa.len() + spk.len()); + der.push(0x30); + der.push((algo_rsa.len() + spk.len()) as u8); + der.extend_from_slice(&algo_rsa); + der.extend_from_slice(&spk); + + match PublicKey::from_der(&der) { + Err(DeserializationError::InvalidValue(_)) => {}, + other => panic!("expected InvalidValue for wrong algorithm OID, got {:?}", other), + } } } diff --git a/miden-crypto/src/dsa/eddsa_25519_sha512/mod.rs b/miden-crypto/src/dsa/eddsa_25519_sha512/mod.rs index 0e17ff6d79..29c8f445e7 100644 --- a/miden-crypto/src/dsa/eddsa_25519_sha512/mod.rs +++ b/miden-crypto/src/dsa/eddsa_25519_sha512/mod.rs @@ -18,7 +18,6 @@ use crate::{ }, }; -#[cfg(test)] mod tests; // CONSTANTS @@ -36,24 +35,15 @@ const SIGNATURE_BYTES: usize = 64; /// Secret key for EdDSA (Ed25519) signature verification over Curve25519. #[derive(Clone, SilentDebug, SilentDisplay)] -pub struct SecretKey { +struct SecretKey { inner: ed25519_dalek::SigningKey, } impl SecretKey { - /// Generates a new random secret key using the OS random number generator. - #[cfg(feature = "std")] - #[allow(clippy::new_without_default)] - pub fn new() -> Self { - let mut rng = rand::rng(); - - Self::with_rng(&mut rng) - } - /// Generates a new secret key using RNG. - pub fn with_rng(rng: &mut R) -> Self { + fn with_rng(rng: &mut R) -> Self { let mut seed = [0u8; SECRET_KEY_BYTES]; - rand::RngCore::fill_bytes(rng, &mut seed); + RngCore::fill_bytes(rng, &mut seed); let inner = ed25519_dalek::SigningKey::from_bytes(&seed); @@ -64,12 +54,12 @@ impl SecretKey { } /// Gets the corresponding public key for this secret key. - pub fn public_key(&self) -> PublicKey { + fn public_key(&self) -> PublicKey { PublicKey { inner: self.inner.verifying_key() } } /// Signs a message (Word) with this secret key. - pub fn sign(&self, message: Word) -> Signature { + fn sign(&self, message: Word) -> Signature { let message_bytes: [u8; 32] = message.into(); let sig = self.inner.sign(&message_bytes); Signature { inner: sig } @@ -77,7 +67,7 @@ impl SecretKey { /// Computes a Diffie-Hellman shared secret from this secret key and the ephemeral public key /// generated by the other party. - pub fn get_shared_secret(&self, pk_e: EphemeralPublicKey) -> SharedSecret { + fn get_shared_secret(&self, pk_e: EphemeralPublicKey) -> SharedSecret { let shared = self.to_x25519().diffie_hellman(&pk_e.inner); SharedSecret::new(shared) } @@ -112,6 +102,119 @@ impl PartialEq for SecretKey { impl Eq for SecretKey {} +// SIGNING KEY +// ================================================================================================ + +/// A secret key for EdDSA (Ed25519) signature verification over Curve25519. +#[derive(Clone, Eq, PartialEq, SilentDebug, SilentDisplay)] // Safe as SecretKey has const-time eq +pub struct SigningKey(SecretKey); + +impl SigningKey { + /// Generates a new random signing key using the OS random number generator. + /// + /// This is cryptographically secure as long as [`rand::rng`] remains so. + #[cfg(feature = "std")] + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + let mut rng = rand::rng(); + Self::with_rng(&mut rng) + } + + /// Generates a new secret key using RNG. + pub fn with_rng(rng: &mut R) -> Self { + Self(SecretKey::with_rng(rng)) + } + + /// Gets the corresponding public key for this secret key. + pub fn public_key(&self) -> PublicKey { + self.0.public_key() + } + + /// Signs a message (Word) with this secret key. + pub fn sign(&self, message: Word) -> Signature { + self.0.sign(message) + } +} + +impl From for SigningKey { + fn from(secret_key: SecretKey) -> Self { + Self(secret_key) + } +} + +// SAFETY: The inner `SecretKey` already implements `ZeroizeOnDrop` which ensures that the secret +// key material is securely zeroized when dropped. +impl ZeroizeOnDrop for SigningKey {} + +impl Serializable for SigningKey { + fn write_into(&self, target: &mut W) { + self.0.write_into(target); + } +} + +impl Deserializable for SigningKey { + fn read_from(source: &mut R) -> Result { + Ok(Self(SecretKey::read_from(source)?)) + } +} + +// KEY EXCHANGE KEY +// ================================================================================================ + +/// A key for ECDH key exchange over Curve25519 +#[derive(Clone, Eq, PartialEq, SilentDebug, SilentDisplay)] // Safe as SecretKey has const-time eq +pub struct KeyExchangeKey(SecretKey); + +impl KeyExchangeKey { + /// Generates a new random key exchange key using the OS random number generator. + /// + /// This is cryptographically secure as long as [`rand::rng`] remains so. + #[cfg(feature = "std")] + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + let mut rng = rand::rng(); + Self::with_rng(&mut rng) + } + + /// Generates a new secret key using RNG. + pub fn with_rng(rng: &mut R) -> Self { + Self(SecretKey::with_rng(rng)) + } + + /// Gets the corresponding public key for this secret key. + pub fn public_key(&self) -> PublicKey { + self.0.public_key() + } + + /// Computes a Diffie-Hellman shared secret from this secret key and the ephemeral public key + /// generated by the other party. + pub fn get_shared_secret(&self, pk_e: EphemeralPublicKey) -> SharedSecret { + self.0.get_shared_secret(pk_e) + } +} + +impl From for KeyExchangeKey { + fn from(secret_key: SecretKey) -> Self { + Self(secret_key) + } +} + +// SAFETY: The inner `SecretKey` already implements `ZeroizeOnDrop` which ensures that the secret +// key material is securely zeroized when dropped. +impl ZeroizeOnDrop for KeyExchangeKey {} + +impl Serializable for KeyExchangeKey { + fn write_into(&self, target: &mut W) { + self.0.write_into(target); + } +} + +impl Deserializable for KeyExchangeKey { + fn read_from(source: &mut R) -> Result { + Ok(Self(SecretKey::read_from(source)?)) + } +} + // PUBLIC KEY // ================================================================================================ diff --git a/miden-crypto/src/dsa/eddsa_25519_sha512/tests.rs b/miden-crypto/src/dsa/eddsa_25519_sha512/tests.rs index e0adfc5bcd..3a31d650eb 100644 --- a/miden-crypto/src/dsa/eddsa_25519_sha512/tests.rs +++ b/miden-crypto/src/dsa/eddsa_25519_sha512/tests.rs @@ -1,90 +1,155 @@ -use super::*; -use crate::rand::test_utils::seeded_rng; +#![cfg(test)] +mod signing_key { + use miden_field::{Felt, Word}; + use miden_serde_utils::{Deserializable, Serializable}; + + use crate::{ + dsa::eddsa_25519_sha512::{PublicKey, SigningKey, UncheckedVerificationError}, + rand::test_utils::seeded_rng, + }; + + #[test] + fn sign_and_verify_roundtrip() { + let mut rng = seeded_rng([0u8; 32]); + let sk = SigningKey::with_rng(&mut rng); + let pk = sk.public_key(); + + let msg = Word::default(); // all zeros + let sig = sk.sign(msg); + + assert!(pk.verify(msg, &sig)); + } -#[test] -fn sign_and_verify_roundtrip() { - let mut rng = seeded_rng([0u8; 32]); - let sk = SecretKey::with_rng(&mut rng); - let pk = sk.public_key(); + #[test] + fn test_key_generation_serialization() { + let mut rng = seeded_rng([1u8; 32]); - let msg = Word::default(); // all zeros - let sig = sk.sign(msg); + let sk = SigningKey::with_rng(&mut rng); + let pk = sk.public_key(); - assert!(pk.verify(msg, &sig)); -} + // Secret key -> bytes -> recovered secret key + let sk_bytes = sk.to_bytes(); + let serialized_sk = SigningKey::read_from_bytes(&sk_bytes) + .expect("deserialization of valid secret key bytes should succeed"); + assert_eq!(sk.to_bytes(), serialized_sk.to_bytes()); -#[test] -fn test_key_generation_serialization() { - let mut rng = seeded_rng([1u8; 32]); + // Public key -> bytes -> recovered public key + let pk_bytes = pk.to_bytes(); + let serialized_pk = PublicKey::read_from_bytes(&pk_bytes) + .expect("deserialization of valid public key bytes should succeed"); + assert_eq!(pk, serialized_pk); + } - let sk = SecretKey::with_rng(&mut rng); - let pk = sk.public_key(); + #[test] + fn test_secret_key_debug_redaction() { + let mut rng = seeded_rng([2u8; 32]); + let sk = SigningKey::with_rng(&mut rng); - // Secret key -> bytes -> recovered secret key - let sk_bytes = sk.to_bytes(); - let serialized_sk = SecretKey::read_from_bytes(&sk_bytes) - .expect("deserialization of valid secret key bytes should succeed"); - assert_eq!(sk.to_bytes(), serialized_sk.to_bytes()); + // Verify Debug impl produces expected redacted output + let debug_output = format!("{sk:?}"); + assert_eq!(debug_output, ""); - // Public key -> bytes -> recovered public key - let pk_bytes = pk.to_bytes(); - let serialized_pk = PublicKey::read_from_bytes(&pk_bytes) - .expect("deserialization of valid public key bytes should succeed"); - assert_eq!(pk, serialized_pk); + // Verify Display impl also elides + let display_output = format!("{sk}"); + assert_eq!(display_output, ""); + } + + #[test] + fn test_compute_challenge_k_equivalence() { + let mut rng = seeded_rng([3u8; 32]); + let sk = SigningKey::with_rng(&mut rng); + let pk = sk.public_key(); + + // Test with multiple different messages + let messages = [ + Word::default(), + Word::from([ + Felt::new_unchecked(1), + Felt::new_unchecked(2), + Felt::new_unchecked(3), + Felt::new_unchecked(4), + ]), + Word::from([ + Felt::new_unchecked(42), + Felt::new_unchecked(100), + Felt::new_unchecked(255), + Felt::new_unchecked(1000), + ]), + ]; + + for message in messages { + let signature = sk.sign(message); + + // Compute the challenge hash using the helper method + let k_hash = pk.compute_challenge_k(message, &signature); + + // Verify using verify_with_unchecked_k should give the same result as verify() + let result_with_k = pk.verify_with_unchecked_k(k_hash, &signature).is_ok(); + let result_standard = pk.verify(message, &signature); + + assert_eq!( + result_with_k, result_standard, + "verify_with_unchecked_k(compute_challenge_k(...)) should equal verify()" + ); + assert!(result_standard, "Signature should be valid"); + + // Test with wrong message - both should fail + let wrong_message = Word::from([ + Felt::new_unchecked(999), + Felt::new_unchecked(888), + Felt::new_unchecked(777), + Felt::new_unchecked(666), + ]); + let wrong_k_hash = pk.compute_challenge_k(wrong_message, &signature); + + assert!(matches!( + pk.verify_with_unchecked_k(wrong_k_hash, &signature), + Err(UncheckedVerificationError::EquationMismatch) + )); + assert!(!pk.verify(wrong_message, &signature), "verify with wrong message should fail"); + } + } } -#[test] -fn test_secret_key_debug_redaction() { - let mut rng = seeded_rng([2u8; 32]); - let sk = SecretKey::with_rng(&mut rng); +mod key_exchange_key { + use miden_serde_utils::{Deserializable, Serializable}; - // Verify Debug impl produces expected redacted output - let debug_output = format!("{sk:?}"); - assert_eq!(debug_output, ""); + use crate::{ + dsa::eddsa_25519_sha512::{KeyExchangeKey, PublicKey}, + rand::test_utils::seeded_rng, + }; - // Verify Display impl also elides - let display_output = format!("{sk}"); - assert_eq!(display_output, ""); -} + #[test] + fn test_key_generation_serialization() { + let mut rng = seeded_rng([1u8; 32]); + + let sk = KeyExchangeKey::with_rng(&mut rng); + let pk = sk.public_key(); + + // Secret key -> bytes -> recovered secret key + let sk_bytes = sk.to_bytes(); + let serialized_sk = KeyExchangeKey::read_from_bytes(&sk_bytes) + .expect("deserialization of valid secret key bytes should succeed"); + assert_eq!(sk.to_bytes(), serialized_sk.to_bytes()); + + // Public key -> bytes -> recovered public key + let pk_bytes = pk.to_bytes(); + let serialized_pk = PublicKey::read_from_bytes(&pk_bytes) + .expect("deserialization of valid public key bytes should succeed"); + assert_eq!(pk, serialized_pk); + } + + #[test] + fn test_secret_key_debug_redaction() { + let mut rng = seeded_rng([2u8; 32]); + let sk = KeyExchangeKey::with_rng(&mut rng); + + // Verify Debug impl produces expected redacted output + let debug_output = format!("{sk:?}"); + assert_eq!(debug_output, ""); -#[test] -fn test_compute_challenge_k_equivalence() { - let mut rng = seeded_rng([3u8; 32]); - let sk = SecretKey::with_rng(&mut rng); - let pk = sk.public_key(); - - // Test with multiple different messages - let messages = [ - Word::default(), - Word::from([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]), - Word::from([Felt::new(42), Felt::new(100), Felt::new(255), Felt::new(1000)]), - ]; - - for message in messages { - let signature = sk.sign(message); - - // Compute the challenge hash using the helper method - let k_hash = pk.compute_challenge_k(message, &signature); - - // Verify using verify_with_unchecked_k should give the same result as verify() - let result_with_k = pk.verify_with_unchecked_k(k_hash, &signature).is_ok(); - let result_standard = pk.verify(message, &signature); - - assert_eq!( - result_with_k, result_standard, - "verify_with_unchecked_k(compute_challenge_k(...)) should equal verify()" - ); - assert!(result_standard, "Signature should be valid"); - - // Test with wrong message - both should fail - let wrong_message = - Word::from([Felt::new(999), Felt::new(888), Felt::new(777), Felt::new(666)]); - let wrong_k_hash = pk.compute_challenge_k(wrong_message, &signature); - - assert!(matches!( - pk.verify_with_unchecked_k(wrong_k_hash, &signature), - Err(UncheckedVerificationError::EquationMismatch) - )); - assert!(!pk.verify(wrong_message, &signature), "verify with wrong message should fail"); + // Verify Display impl also elides + let display_output = format!("{sk}"); + assert_eq!(display_output, ""); } } diff --git a/miden-crypto/src/dsa/falcon512_poseidon2/keys/secret_key.rs b/miden-crypto/src/dsa/falcon512_poseidon2/keys/secret_key.rs index c9ce4db355..d07f5b1203 100644 --- a/miden-crypto/src/dsa/falcon512_poseidon2/keys/secret_key.rs +++ b/miden-crypto/src/dsa/falcon512_poseidon2/keys/secret_key.rs @@ -132,7 +132,7 @@ impl SecretKey { // -------------------------------------------------------------------------------------------- /// Signs a message with this secret key. - pub fn sign(&self, message: crate::Word) -> Signature { + pub fn sign(&self, message: Word) -> Signature { use rand::SeedableRng; use rand_chacha::ChaCha20Rng; @@ -389,10 +389,10 @@ impl Deserializable for SecretKey { // big_g * f - g * big_f = p (mod X^n + 1) let big_g = g.fft().hadamard_div(&f.fft()).hadamard_mul(&big_f.fft()).ifft(); let basis = [ - g.map(|f| f.balanced_value()), - -f.map(|f| f.balanced_value()), - big_g.map(|f| f.balanced_value()), - -big_f.map(|f| f.balanced_value()), + Polynomial::new(g.to_balanced_values()), + -Polynomial::new(f.to_balanced_values()), + Polynomial::new(big_g.to_balanced_values()), + -Polynomial::new(big_f.to_balanced_values()), ]; Ok(Self::from_short_lattice_basis(basis)) } diff --git a/miden-crypto/src/dsa/falcon512_poseidon2/math/ffsampling.rs b/miden-crypto/src/dsa/falcon512_poseidon2/math/ffsampling.rs index 629d4bb1a6..38ab228cd1 100644 --- a/miden-crypto/src/dsa/falcon512_poseidon2/math/ffsampling.rs +++ b/miden-crypto/src/dsa/falcon512_poseidon2/math/ffsampling.rs @@ -20,7 +20,7 @@ pub fn gram(b: [Polynomial; 4]) -> [Polynomial; 4] { for j in 0..N { for k in 0..N { g[N * i + j] = g[N * i + j].clone() - + b[N * i + k].hadamard_mul(&b[N * j + k].map(|c| c.conj())); + + b[N * i + k].hadamard_mul(&b[N * j + k].map(Complex::conj)); } } } @@ -133,8 +133,8 @@ pub fn ffldl(gram_matrix: [Polynomial; 4]) -> LdlTree { if n > 2 { let (d00_left, d00_right) = d00.split_fft(); let (d11_left, d11_right) = d11.split_fft(); - let g0 = [d00_left.clone(), d00_right.clone(), d00_right.map(|c| c.conj()), d00_left]; - let g1 = [d11_left.clone(), d11_right.clone(), d11_right.map(|c| c.conj()), d11_left]; + let g0 = [d00_left.clone(), d00_right.clone(), d00_right.map(Complex::conj), d00_left]; + let g1 = [d11_left.clone(), d11_right.clone(), d11_right.map(Complex::conj), d11_left]; LdlTree::Branch(l10, Box::new(ffldl(g0)), Box::new(ffldl(g1))) } else { LdlTree::Branch( @@ -219,7 +219,7 @@ mod tests { d11: &Polynomial, ) -> [Polynomial; 4] { // Compute conj(l10) for use in L* - let l10_conj = l10.map(|c| c.conj()); + let l10_conj = l10.map(Complex::conj); // Compute G = L·D·L* using Hadamard operations (FFT domain) // G[0,0] = 1*d00*1 + 0*d11*0 = d00 @@ -258,7 +258,7 @@ mod tests { } // Ensure Hermitian property: g10 = conj(g01) - let g10 = g01.iter().map(|c| c.conj()).collect(); + let g10 = g01.iter().map(Complex::conj).collect(); [ Polynomial::new(g00), @@ -307,23 +307,19 @@ mod tests { // Verify reconstruction matches original (L·D·L* = G) assert!( polynomials_approx_eq(&g_reconstructed[0], &g[0], 1e-10), - "degree {}: G[0,0] mismatch", - degree + "degree {degree}: G[0,0] mismatch" ); assert!( polynomials_approx_eq(&g_reconstructed[1], &g[1], 1e-10), - "degree {}: G[0,1] mismatch", - degree + "degree {degree}: G[0,1] mismatch" ); assert!( polynomials_approx_eq(&g_reconstructed[2], &g[2], 1e-10), - "degree {}: G[1,0] mismatch", - degree + "degree {degree}: G[1,0] mismatch" ); assert!( polynomials_approx_eq(&g_reconstructed[3], &g[3], 1e-10), - "degree {}: G[1,1] mismatch", - degree + "degree {degree}: G[1,1] mismatch" ); } } diff --git a/miden-crypto/src/dsa/falcon512_poseidon2/math/field.rs b/miden-crypto/src/dsa/falcon512_poseidon2/math/field.rs index f79a785ba9..874048b3cc 100644 --- a/miden-crypto/src/dsa/falcon512_poseidon2/math/field.rs +++ b/miden-crypto/src/dsa/falcon512_poseidon2/math/field.rs @@ -18,17 +18,17 @@ impl FalconFelt { FalconFelt(canonical_representative) } - pub const fn value(&self) -> i16 { + pub const fn value(self) -> i16 { self.0 as i16 } - pub fn balanced_value(&self) -> i16 { + pub fn balanced_value(self) -> i16 { let value = self.value(); let g = (value > ((MODULUS) / 2)) as i16; value - (MODULUS) * g } - pub const fn multiply(&self, other: Self) -> Self { + pub const fn multiply(self, other: Self) -> Self { FalconFelt((self.0 * other.0) % MODULUS as u32) } } diff --git a/miden-crypto/src/dsa/falcon512_poseidon2/math/mod.rs b/miden-crypto/src/dsa/falcon512_poseidon2/math/mod.rs index b0f12234d4..db2a4b95de 100644 --- a/miden-crypto/src/dsa/falcon512_poseidon2/math/mod.rs +++ b/miden-crypto/src/dsa/falcon512_poseidon2/math/mod.rs @@ -99,7 +99,7 @@ pub(crate) fn ntru_gen(n: usize, rng: &mut R) -> [Polynomial; 4] { } let f_ntt = f.map(|&i| FalconFelt::new(i)).fft(); - if f_ntt.coefficients.iter().any(|e| e.is_zero()) { + if f_ntt.coefficients.iter().any(Zero::is_zero) { continue; } let gamma = gram_schmidt_norm_squared(&f, &g); @@ -190,8 +190,8 @@ fn gram_schmidt_norm_squared(f: &Polynomial, g: &Polynomial) -> f64 { let f_fft = f.map(|i| Complex64::new(*i as f64, 0.0)).fft(); let g_fft = g.map(|i| Complex64::new(*i as f64, 0.0)).fft(); - let f_adj_fft = f_fft.map(|c| c.conj()); - let g_adj_fft = g_fft.map(|c| c.conj()); + let f_adj_fft = f_fft.map(num::Complex::conj); + let g_adj_fft = g_fft.map(num::Complex::conj); let ffgg_fft = f_fft.hadamard_mul(&f_adj_fft) + g_fft.hadamard_mul(&g_adj_fft); let ffgg_fft_inverse = ffgg_fft.hadamard_inv(); let qf_over_ffgg_fft = f_adj_fft.map(|c| c * (MODULUS as f64)).hadamard_mul(&ffgg_fft_inverse); @@ -240,8 +240,8 @@ fn babai_reduce( .map(|bi| Complex64::new(i64::try_from(bi >> shift).unwrap() as f64, 0.0)) .fft(); - let f_star_adjusted = f_adjusted.map(|c| c.conj()); - let g_star_adjusted = g_adjusted.map(|c| c.conj()); + let f_star_adjusted = f_adjusted.map(num::Complex::conj); + let g_star_adjusted = g_adjusted.map(num::Complex::conj); let denominator_fft = f_adjusted.hadamard_mul(&f_star_adjusted) + g_adjusted.hadamard_mul(&g_star_adjusted); diff --git a/miden-crypto/src/dsa/falcon512_poseidon2/math/polynomial.rs b/miden-crypto/src/dsa/falcon512_poseidon2/math/polynomial.rs index 7174259636..63e1deb0bc 100644 --- a/miden-crypto/src/dsa/falcon512_poseidon2/math/polynomial.rs +++ b/miden-crypto/src/dsa/falcon512_poseidon2/math/polynomial.rs @@ -229,13 +229,13 @@ where type Output = Polynomial; fn add(self, rhs: Self) -> Self::Output { let coefficients = if self.coefficients.len() >= rhs.coefficients.len() { - let mut coefficients = self.coefficients.clone(); + let mut coefficients = self.coefficients; for (i, c) in rhs.coefficients.into_iter().enumerate() { coefficients[i] += c; } coefficients } else { - let mut coefficients = rhs.coefficients.clone(); + let mut coefficients = rhs.coefficients; for (i, c) in self.coefficients.into_iter().enumerate() { coefficients[i] += c; } @@ -255,7 +255,7 @@ where self.coefficients[i] += c; } } else { - let mut coefficients = rhs.coefficients.clone(); + let mut coefficients = rhs.coefficients; for (i, c) in self.coefficients.iter().enumerate() { coefficients[i] += c.clone(); } @@ -457,7 +457,7 @@ where if self.is_zero() { Self::zero(); } - let mut remainder = self.clone(); + let mut remainder = self; let mut quotient = Polynomial::::zero(); while remainder.degree().unwrap() >= denominator.degree().unwrap() { let shift = remainder.degree().unwrap() - denominator.degree().unwrap(); @@ -582,6 +582,11 @@ impl Polynomial { self.coefficients.iter().map(|&a| Felt::from_u16(a.value() as u16)).collect() } + /// Returns the coefficients of this polynomial as balanced signed values. + pub fn to_balanced_values(&self) -> Vec { + self.coefficients.iter().copied().map(FalconFelt::balanced_value).collect() + } + // POLYNOMIAL OPERATIONS // -------------------------------------------------------------------------------------------- diff --git a/miden-crypto/src/dsa/falcon512_poseidon2/mod.rs b/miden-crypto/src/dsa/falcon512_poseidon2/mod.rs index ba32da810c..23d453627d 100644 --- a/miden-crypto/src/dsa/falcon512_poseidon2/mod.rs +++ b/miden-crypto/src/dsa/falcon512_poseidon2/mod.rs @@ -175,7 +175,7 @@ impl Nonce { buffer[..5].copy_from_slice(bytes); // we can safely (without overflow) create a new Felt from u64 value here since this // value contains at most 5 bytes - result[i] = Felt::new(u64::from_le_bytes(buffer)); + result[i] = Felt::new_unchecked(u64::from_le_bytes(buffer)); } result diff --git a/miden-crypto/src/dsa/falcon512_poseidon2/signature.rs b/miden-crypto/src/dsa/falcon512_poseidon2/signature.rs index 99f2cf3d9a..64de2e0d9c 100644 --- a/miden-crypto/src/dsa/falcon512_poseidon2/signature.rs +++ b/miden-crypto/src/dsa/falcon512_poseidon2/signature.rs @@ -1,4 +1,4 @@ -use alloc::{string::ToString, vec::Vec}; +use alloc::string::ToString; use core::ops::Deref; use num::Zero; @@ -225,7 +225,7 @@ impl TryFrom<&[i16; N]> for SignaturePoly { impl Serializable for &SignaturePoly { fn write_into(&self, target: &mut W) { - let sig_coeff: Vec = self.0.coefficients.iter().map(|a| a.balanced_value()).collect(); + let sig_coeff = self.0.to_balanced_values(); let mut sk_bytes = vec![0_u8; SIG_POLY_BYTE_LEN]; let mut acc = 0; diff --git a/miden-crypto/src/dsa/falcon512_poseidon2/tests/data.rs b/miden-crypto/src/dsa/falcon512_poseidon2/tests/data.rs index d34cb61331..3419aba88e 100644 --- a/miden-crypto/src/dsa/falcon512_poseidon2/tests/data.rs +++ b/miden-crypto/src/dsa/falcon512_poseidon2/tests/data.rs @@ -1755,80 +1755,80 @@ pub(crate) static SK_POLYS: [[[i16; 512]; 4]; NUM_TEST_VECTORS] = [ /// Serialized deterministic Falcon512-Poseidon2 signature intended for use as a test vector /// for the determinism in the signing procedure across platforms. /// -/// This was generated on an `M4` running on `Sequoia 15.7` and built with Rust `1.90.0`. +/// This was generated on an `M4` running on `Tahoe 26.3` and built with Rust `1.90.0`. pub(crate) const DETERMINISTIC_SIGNATURE: [u8; SIG_SERIALIZED_LEN] = [ - 185, 1, 82, 207, 113, 136, 65, 29, 193, 18, 114, 48, 81, 49, 255, 49, 144, 220, 214, 13, 145, - 231, 109, 189, 95, 108, 39, 47, 27, 221, 142, 39, 69, 219, 5, 21, 106, 116, 2, 87, 124, 167, - 180, 84, 173, 207, 127, 225, 135, 85, 220, 211, 107, 144, 79, 231, 176, 131, 75, 26, 138, 60, - 56, 247, 65, 172, 150, 238, 214, 20, 201, 211, 152, 46, 68, 119, 55, 106, 98, 29, 155, 173, 8, - 237, 108, 80, 202, 86, 228, 206, 235, 219, 104, 59, 206, 120, 43, 60, 120, 140, 97, 220, 224, - 60, 113, 106, 228, 201, 71, 131, 144, 148, 10, 169, 96, 60, 184, 37, 207, 56, 122, 157, 49, 24, - 210, 9, 44, 98, 69, 222, 151, 253, 144, 232, 84, 54, 81, 115, 159, 47, 121, 56, 122, 84, 186, - 121, 166, 18, 188, 180, 40, 207, 248, 155, 30, 29, 120, 144, 238, 253, 179, 15, 58, 157, 200, - 216, 171, 199, 45, 250, 80, 203, 150, 204, 199, 43, 188, 159, 131, 60, 157, 182, 84, 138, 187, - 108, 102, 30, 222, 193, 118, 239, 81, 175, 48, 4, 106, 60, 219, 240, 95, 74, 148, 209, 133, - 129, 120, 62, 166, 97, 247, 135, 188, 25, 173, 57, 146, 121, 166, 243, 218, 26, 33, 163, 119, - 162, 53, 167, 131, 75, 117, 97, 62, 173, 109, 17, 233, 32, 220, 184, 146, 32, 58, 36, 253, 159, - 152, 126, 166, 218, 179, 139, 134, 96, 154, 169, 130, 36, 154, 7, 6, 121, 248, 124, 83, 85, 14, - 132, 108, 21, 26, 187, 49, 116, 78, 92, 107, 49, 13, 227, 186, 60, 182, 220, 158, 237, 188, 75, - 92, 182, 240, 130, 28, 52, 144, 78, 221, 22, 67, 91, 206, 78, 245, 59, 35, 217, 53, 155, 238, - 93, 103, 207, 81, 230, 225, 114, 18, 180, 219, 27, 160, 38, 171, 20, 97, 216, 129, 187, 151, - 219, 82, 157, 187, 177, 36, 149, 22, 29, 214, 65, 46, 179, 154, 228, 139, 151, 212, 62, 213, - 172, 107, 214, 192, 155, 129, 176, 150, 45, 176, 190, 148, 146, 75, 68, 150, 27, 30, 69, 174, - 207, 249, 61, 153, 119, 175, 199, 142, 91, 187, 63, 127, 197, 143, 165, 206, 172, 244, 72, 126, - 154, 74, 194, 108, 171, 61, 104, 12, 224, 178, 122, 220, 106, 83, 91, 15, 45, 73, 162, 115, - 186, 137, 76, 123, 202, 175, 126, 52, 128, 241, 208, 57, 202, 155, 90, 81, 84, 40, 58, 117, - 107, 101, 176, 169, 58, 80, 156, 254, 22, 55, 217, 158, 16, 216, 156, 58, 110, 80, 233, 111, - 89, 11, 36, 136, 49, 100, 250, 206, 26, 222, 156, 161, 27, 149, 180, 11, 2, 145, 14, 81, 20, - 201, 252, 197, 173, 156, 50, 232, 44, 161, 171, 36, 163, 245, 132, 239, 127, 26, 4, 21, 199, - 212, 140, 146, 37, 193, 72, 88, 164, 129, 38, 8, 37, 20, 68, 238, 28, 221, 206, 24, 246, 215, - 253, 200, 241, 8, 242, 227, 78, 227, 94, 182, 210, 24, 229, 194, 10, 197, 77, 177, 113, 249, - 130, 73, 155, 85, 248, 62, 66, 176, 204, 178, 3, 99, 67, 213, 181, 29, 126, 122, 22, 37, 12, - 26, 23, 29, 46, 183, 54, 9, 251, 210, 124, 180, 56, 70, 10, 109, 229, 174, 65, 160, 81, 37, 29, - 148, 49, 235, 111, 77, 96, 197, 154, 158, 196, 6, 163, 38, 213, 25, 6, 218, 211, 149, 150, 224, - 153, 114, 49, 36, 118, 38, 210, 51, 227, 45, 140, 161, 104, 244, 169, 7, 215, 57, 232, 213, - 199, 221, 10, 128, 0, 0, 0, 0, 0, 0, 0, 0, 9, 155, 125, 185, 64, 84, 225, 93, 95, 10, 178, 100, - 198, 160, 180, 110, 66, 53, 41, 212, 204, 170, 160, 237, 167, 160, 122, 168, 168, 68, 93, 180, - 2, 255, 84, 191, 48, 157, 91, 57, 228, 201, 192, 75, 145, 62, 104, 175, 135, 156, 9, 128, 122, - 1, 210, 73, 150, 34, 200, 59, 228, 99, 89, 40, 54, 25, 217, 86, 245, 170, 187, 28, 224, 144, - 102, 208, 225, 180, 106, 60, 184, 144, 29, 213, 222, 166, 45, 212, 135, 24, 164, 201, 83, 26, - 181, 36, 130, 38, 173, 109, 97, 235, 192, 26, 43, 248, 157, 36, 62, 182, 118, 28, 234, 32, 6, - 171, 179, 224, 30, 170, 75, 80, 101, 228, 221, 195, 238, 144, 170, 6, 57, 109, 171, 41, 157, - 234, 0, 103, 243, 207, 105, 76, 164, 83, 35, 27, 73, 134, 177, 241, 38, 98, 86, 16, 200, 61, - 190, 53, 115, 49, 157, 169, 143, 109, 119, 196, 109, 151, 31, 54, 94, 22, 246, 174, 164, 162, - 173, 60, 74, 18, 220, 166, 122, 176, 32, 166, 107, 100, 229, 32, 161, 185, 210, 8, 49, 230, 61, - 184, 212, 197, 41, 114, 239, 214, 114, 177, 9, 39, 254, 197, 24, 151, 86, 141, 25, 206, 200, - 146, 167, 36, 29, 224, 66, 141, 123, 73, 246, 49, 80, 207, 109, 160, 72, 249, 70, 164, 10, 211, - 190, 15, 104, 147, 186, 216, 202, 251, 27, 246, 250, 104, 57, 91, 119, 19, 98, 173, 247, 70, - 85, 8, 13, 70, 69, 120, 52, 21, 87, 112, 50, 11, 75, 213, 167, 79, 42, 106, 58, 250, 77, 12, - 133, 174, 108, 113, 82, 17, 17, 98, 126, 97, 172, 87, 218, 221, 79, 84, 113, 33, 148, 62, 105, - 150, 66, 152, 153, 39, 237, 96, 75, 81, 1, 56, 6, 98, 92, 138, 114, 242, 189, 40, 38, 197, 118, - 96, 130, 145, 229, 138, 153, 44, 49, 89, 120, 209, 167, 205, 202, 28, 65, 174, 219, 125, 99, - 31, 88, 48, 254, 227, 34, 88, 138, 138, 60, 144, 106, 148, 158, 248, 154, 181, 53, 3, 45, 233, - 164, 68, 80, 207, 42, 209, 157, 159, 128, 94, 241, 55, 166, 231, 115, 130, 41, 132, 19, 135, - 225, 120, 36, 101, 204, 210, 161, 84, 197, 63, 5, 36, 178, 4, 229, 237, 43, 49, 212, 80, 219, - 20, 172, 182, 189, 9, 193, 112, 73, 63, 37, 148, 148, 184, 201, 96, 83, 62, 32, 186, 249, 54, - 103, 208, 112, 216, 216, 217, 97, 70, 4, 18, 42, 182, 117, 21, 222, 204, 168, 164, 123, 1, 189, - 145, 70, 80, 218, 192, 136, 81, 22, 159, 137, 194, 70, 246, 187, 150, 50, 54, 154, 203, 214, - 73, 174, 205, 44, 192, 105, 138, 192, 109, 238, 21, 64, 232, 181, 218, 129, 125, 92, 145, 87, - 64, 222, 169, 183, 57, 25, 220, 56, 92, 95, 107, 128, 117, 34, 88, 177, 235, 247, 115, 248, - 208, 139, 215, 206, 133, 153, 146, 133, 18, 219, 194, 85, 118, 162, 148, 33, 70, 46, 21, 175, - 86, 230, 182, 233, 31, 127, 188, 200, 2, 21, 203, 4, 82, 19, 185, 79, 195, 149, 207, 234, 145, - 240, 155, 74, 21, 94, 207, 38, 138, 136, 118, 45, 85, 239, 35, 210, 120, 55, 183, 88, 79, 26, - 98, 215, 198, 93, 79, 137, 27, 0, 253, 131, 54, 234, 89, 147, 30, 0, 161, 87, 69, 116, 171, - 156, 11, 53, 205, 148, 66, 106, 202, 224, 234, 155, 225, 149, 13, 40, 254, 217, 69, 232, 212, - 152, 253, 119, 80, 137, 205, 196, 98, 0, 163, 57, 144, 228, 31, 112, 141, 133, 54, 204, 155, - 16, 178, 246, 44, 138, 228, 218, 203, 49, 70, 10, 181, 7, 221, 30, 137, 14, 176, 25, 133, 174, - 24, 146, 184, 191, 54, 35, 140, 154, 45, 158, 230, 243, 4, 26, 23, 41, 210, 21, 117, 35, 136, - 74, 97, 229, 76, 217, 243, 214, 32, 2, 64, 109, 52, 243, 94, 80, 12, 238, 20, 37, 157, 159, 48, - 110, 21, 243, 154, 239, 42, 91, 34, 234, 158, 56, 194, 218, 242, 105, 244, 125, 64, 241, 13, - 102, 44, 78, 23, 56, 23, 193, 108, 97, 217, 44, 186, 3, 98, 94, 110, 94, 129, 128, 19, 3, 198, - 13, 129, 46, 117, 100, 92, 82, 50, 25, 195, 83, 39, 231, 87, 134, 238, 108, 22, 165, 101, 102, - 21, 200, 31, 247, 189, 47, 246, 128, 101, 4, 137, 74, 11, 17, 65, 192, 116, 16, 165, 47, 245, - 148, 122, 75, 251, 193, 47, 172, 28, 229, 144, 129, 39, 207, 156, 36, 180, 73, 102, 92, 109, - 65, 150, 86, 22, 144, 69, 4, 128, 184, 225, 97, 202, 86, 89, 103, 144, 165, 29, 197, 28, 139, - 86, 191, 161, 122, 137, 235, 224, 184, 130, 157, 242, 225, 96, 194, 119, 25, 222, 66, 98, 11, - 34, 26, 147, 208, 199, 195, 58, 230, 93, 49, 52, 221, 131, 2, 96, 7, 64, 242, 230, 87, 178, - 141, 154, 39, 56, 108, 54, 156, 109, 10, 91, 88, 43, 247, 88, 217, 84, 106, 36, 114, 241, 199, - 15, 208, 122, 160, 66, 248, 4, 8, 189, 5, 189, 25, 169, 107, 175, 228, + 185, 1, 115, 242, 40, 84, 78, 192, 133, 17, 36, 22, 121, 221, 210, 192, 18, 115, 247, 158, 101, + 244, 218, 244, 67, 180, 138, 156, 20, 127, 229, 151, 100, 19, 154, 42, 141, 19, 163, 94, 250, + 217, 83, 253, 72, 98, 167, 215, 228, 57, 149, 105, 211, 239, 79, 230, 142, 228, 86, 19, 142, + 251, 35, 139, 246, 95, 117, 140, 235, 202, 154, 114, 221, 13, 145, 147, 131, 101, 80, 45, 19, + 139, 51, 169, 25, 175, 51, 71, 98, 179, 161, 190, 220, 113, 51, 150, 166, 211, 22, 135, 22, + 100, 181, 74, 138, 121, 140, 254, 48, 149, 184, 18, 76, 127, 216, 179, 4, 51, 119, 36, 18, 67, + 13, 144, 48, 140, 99, 201, 164, 22, 118, 59, 42, 185, 162, 141, 76, 31, 186, 242, 53, 15, 7, + 179, 131, 39, 209, 170, 230, 141, 129, 240, 28, 6, 73, 186, 218, 70, 34, 49, 212, 255, 149, + 129, 134, 238, 211, 58, 198, 182, 9, 135, 169, 34, 110, 14, 252, 199, 201, 167, 218, 255, 161, + 190, 140, 96, 171, 12, 162, 1, 36, 68, 155, 220, 17, 156, 143, 122, 49, 238, 164, 89, 227, 73, + 182, 219, 11, 198, 64, 231, 73, 107, 30, 186, 78, 118, 25, 183, 171, 72, 137, 181, 239, 172, + 216, 107, 225, 70, 65, 14, 98, 140, 47, 34, 85, 90, 53, 197, 149, 178, 46, 177, 41, 81, 212, + 182, 188, 242, 218, 148, 203, 189, 200, 104, 202, 231, 32, 168, 148, 68, 197, 247, 128, 142, + 27, 123, 28, 63, 145, 141, 103, 98, 95, 151, 248, 218, 145, 179, 13, 48, 97, 147, 141, 155, 76, + 123, 11, 175, 64, 235, 208, 80, 228, 205, 54, 217, 204, 35, 139, 138, 113, 78, 148, 58, 149, + 52, 19, 92, 186, 198, 190, 204, 141, 102, 157, 184, 48, 236, 54, 163, 217, 242, 161, 89, 215, + 250, 140, 25, 83, 184, 198, 229, 4, 55, 154, 124, 179, 8, 241, 153, 224, 238, 201, 148, 169, + 176, 27, 29, 131, 203, 34, 184, 72, 181, 167, 83, 233, 175, 217, 149, 100, 233, 163, 194, 100, + 165, 108, 156, 181, 18, 202, 33, 141, 222, 57, 102, 191, 37, 205, 39, 74, 30, 195, 80, 250, 83, + 142, 138, 82, 88, 88, 46, 182, 235, 78, 193, 44, 232, 143, 139, 211, 169, 111, 61, 233, 252, + 111, 57, 5, 252, 105, 227, 170, 250, 158, 161, 69, 200, 238, 43, 229, 145, 28, 85, 147, 203, + 178, 197, 188, 56, 67, 220, 221, 102, 103, 251, 212, 137, 111, 99, 221, 159, 74, 92, 206, 180, + 155, 55, 26, 50, 75, 119, 241, 38, 14, 153, 176, 140, 68, 206, 170, 217, 232, 205, 149, 239, + 33, 74, 145, 150, 111, 117, 150, 26, 147, 62, 191, 83, 194, 145, 171, 186, 6, 31, 111, 245, + 210, 24, 55, 49, 149, 92, 225, 241, 22, 145, 128, 108, 201, 53, 192, 188, 210, 28, 89, 94, 142, + 0, 131, 177, 22, 12, 172, 85, 192, 81, 124, 245, 73, 35, 119, 158, 238, 63, 181, 186, 204, 42, + 32, 132, 236, 112, 55, 250, 124, 17, 116, 84, 82, 70, 66, 171, 16, 249, 84, 156, 6, 198, 29, + 45, 72, 138, 109, 34, 247, 26, 158, 33, 18, 201, 70, 97, 105, 65, 214, 55, 151, 156, 250, 199, + 216, 85, 226, 145, 5, 50, 13, 198, 191, 253, 182, 150, 148, 86, 159, 23, 130, 89, 208, 148, 25, + 167, 129, 251, 210, 108, 86, 103, 61, 250, 32, 101, 206, 0, 216, 48, 120, 103, 58, 251, 77, 58, + 90, 238, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 155, 125, 185, 64, 84, 225, 93, 95, 10, + 178, 100, 198, 160, 180, 110, 66, 53, 41, 212, 204, 170, 160, 237, 167, 160, 122, 168, 168, 68, + 93, 180, 2, 255, 84, 191, 48, 157, 91, 57, 228, 201, 192, 75, 145, 62, 104, 175, 135, 156, 9, + 128, 122, 1, 210, 73, 150, 34, 200, 59, 228, 99, 89, 40, 54, 25, 217, 86, 245, 170, 187, 28, + 224, 144, 102, 208, 225, 180, 106, 60, 184, 144, 29, 213, 222, 166, 45, 212, 135, 24, 164, 201, + 83, 26, 181, 36, 130, 38, 173, 109, 97, 235, 192, 26, 43, 248, 157, 36, 62, 182, 118, 28, 234, + 32, 6, 171, 179, 224, 30, 170, 75, 80, 101, 228, 221, 195, 238, 144, 170, 6, 57, 109, 171, 41, + 157, 234, 0, 103, 243, 207, 105, 76, 164, 83, 35, 27, 73, 134, 177, 241, 38, 98, 86, 16, 200, + 61, 190, 53, 115, 49, 157, 169, 143, 109, 119, 196, 109, 151, 31, 54, 94, 22, 246, 174, 164, + 162, 173, 60, 74, 18, 220, 166, 122, 176, 32, 166, 107, 100, 229, 32, 161, 185, 210, 8, 49, + 230, 61, 184, 212, 197, 41, 114, 239, 214, 114, 177, 9, 39, 254, 197, 24, 151, 86, 141, 25, + 206, 200, 146, 167, 36, 29, 224, 66, 141, 123, 73, 246, 49, 80, 207, 109, 160, 72, 249, 70, + 164, 10, 211, 190, 15, 104, 147, 186, 216, 202, 251, 27, 246, 250, 104, 57, 91, 119, 19, 98, + 173, 247, 70, 85, 8, 13, 70, 69, 120, 52, 21, 87, 112, 50, 11, 75, 213, 167, 79, 42, 106, 58, + 250, 77, 12, 133, 174, 108, 113, 82, 17, 17, 98, 126, 97, 172, 87, 218, 221, 79, 84, 113, 33, + 148, 62, 105, 150, 66, 152, 153, 39, 237, 96, 75, 81, 1, 56, 6, 98, 92, 138, 114, 242, 189, 40, + 38, 197, 118, 96, 130, 145, 229, 138, 153, 44, 49, 89, 120, 209, 167, 205, 202, 28, 65, 174, + 219, 125, 99, 31, 88, 48, 254, 227, 34, 88, 138, 138, 60, 144, 106, 148, 158, 248, 154, 181, + 53, 3, 45, 233, 164, 68, 80, 207, 42, 209, 157, 159, 128, 94, 241, 55, 166, 231, 115, 130, 41, + 132, 19, 135, 225, 120, 36, 101, 204, 210, 161, 84, 197, 63, 5, 36, 178, 4, 229, 237, 43, 49, + 212, 80, 219, 20, 172, 182, 189, 9, 193, 112, 73, 63, 37, 148, 148, 184, 201, 96, 83, 62, 32, + 186, 249, 54, 103, 208, 112, 216, 216, 217, 97, 70, 4, 18, 42, 182, 117, 21, 222, 204, 168, + 164, 123, 1, 189, 145, 70, 80, 218, 192, 136, 81, 22, 159, 137, 194, 70, 246, 187, 150, 50, 54, + 154, 203, 214, 73, 174, 205, 44, 192, 105, 138, 192, 109, 238, 21, 64, 232, 181, 218, 129, 125, + 92, 145, 87, 64, 222, 169, 183, 57, 25, 220, 56, 92, 95, 107, 128, 117, 34, 88, 177, 235, 247, + 115, 248, 208, 139, 215, 206, 133, 153, 146, 133, 18, 219, 194, 85, 118, 162, 148, 33, 70, 46, + 21, 175, 86, 230, 182, 233, 31, 127, 188, 200, 2, 21, 203, 4, 82, 19, 185, 79, 195, 149, 207, + 234, 145, 240, 155, 74, 21, 94, 207, 38, 138, 136, 118, 45, 85, 239, 35, 210, 120, 55, 183, 88, + 79, 26, 98, 215, 198, 93, 79, 137, 27, 0, 253, 131, 54, 234, 89, 147, 30, 0, 161, 87, 69, 116, + 171, 156, 11, 53, 205, 148, 66, 106, 202, 224, 234, 155, 225, 149, 13, 40, 254, 217, 69, 232, + 212, 152, 253, 119, 80, 137, 205, 196, 98, 0, 163, 57, 144, 228, 31, 112, 141, 133, 54, 204, + 155, 16, 178, 246, 44, 138, 228, 218, 203, 49, 70, 10, 181, 7, 221, 30, 137, 14, 176, 25, 133, + 174, 24, 146, 184, 191, 54, 35, 140, 154, 45, 158, 230, 243, 4, 26, 23, 41, 210, 21, 117, 35, + 136, 74, 97, 229, 76, 217, 243, 214, 32, 2, 64, 109, 52, 243, 94, 80, 12, 238, 20, 37, 157, + 159, 48, 110, 21, 243, 154, 239, 42, 91, 34, 234, 158, 56, 194, 218, 242, 105, 244, 125, 64, + 241, 13, 102, 44, 78, 23, 56, 23, 193, 108, 97, 217, 44, 186, 3, 98, 94, 110, 94, 129, 128, 19, + 3, 198, 13, 129, 46, 117, 100, 92, 82, 50, 25, 195, 83, 39, 231, 87, 134, 238, 108, 22, 165, + 101, 102, 21, 200, 31, 247, 189, 47, 246, 128, 101, 4, 137, 74, 11, 17, 65, 192, 116, 16, 165, + 47, 245, 148, 122, 75, 251, 193, 47, 172, 28, 229, 144, 129, 39, 207, 156, 36, 180, 73, 102, + 92, 109, 65, 150, 86, 22, 144, 69, 4, 128, 184, 225, 97, 202, 86, 89, 103, 144, 165, 29, 197, + 28, 139, 86, 191, 161, 122, 137, 235, 224, 184, 130, 157, 242, 225, 96, 194, 119, 25, 222, 66, + 98, 11, 34, 26, 147, 208, 199, 195, 58, 230, 93, 49, 52, 221, 131, 2, 96, 7, 64, 242, 230, 87, + 178, 141, 154, 39, 56, 108, 54, 156, 109, 10, 91, 88, 43, 247, 88, 217, 84, 106, 36, 114, 241, + 199, 15, 208, 122, 160, 66, 248, 4, 8, 189, 5, 189, 25, 169, 107, 175, 228, ]; diff --git a/miden-crypto/src/dsa/falcon512_poseidon2/tests/mod.rs b/miden-crypto/src/dsa/falcon512_poseidon2/tests/mod.rs index 8434cc3f68..aa902c6827 100644 --- a/miden-crypto/src/dsa/falcon512_poseidon2/tests/mod.rs +++ b/miden-crypto/src/dsa/falcon512_poseidon2/tests/mod.rs @@ -1,5 +1,3 @@ -use alloc::vec::Vec; - use data::{ DETERMINISTIC_SIGNATURE, EXPECTED_SIG, EXPECTED_SIG_POLYS, NUM_TEST_VECTORS, SK_POLYS, SYNC_DATA_FOR_TEST_VECTOR, @@ -62,8 +60,7 @@ fn test_signature_gen_reference_impl() { let signature = sk.sign_with_rng_testing(message, &mut rng_shake); // 3. compare against the expected signature - let sig_coef: Vec = - signature.sig_poly().coefficients.iter().map(|c| c.balanced_value()).collect(); + let sig_coef = signature.sig_poly().to_balanced_values(); assert_eq!(sig_coef, EXPECTED_SIG_POLYS[i]); // 4. compare the encoded signatures including the nonce diff --git a/miden-crypto/src/ecdh/k256.rs b/miden-crypto/src/ecdh/k256.rs index e8442b4baf..0771cd6a2a 100644 --- a/miden-crypto/src/ecdh/k256.rs +++ b/miden-crypto/src/ecdh/k256.rs @@ -19,14 +19,13 @@ use k256::{AffinePoint, elliptic_curve::sec1::ToEncodedPoint, sha2::Sha256}; use rand::{CryptoRng, RngCore}; use crate::{ - dsa::ecdsa_k256_keccak::{PUBLIC_KEY_BYTES, PublicKey, SecretKey}, + dsa::ecdsa_k256_keccak::{KeyExchangeKey, PUBLIC_KEY_BYTES, PublicKey}, ecdh::KeyAgreementScheme, utils::{ ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable, zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}, }, }; - // SHARED SECRET // ================================================================================================ @@ -117,7 +116,7 @@ impl EphemeralSecretKey { // once `k256` gets a new release with a version of the `rand` dependency matching ours use k256::elliptic_curve::rand_core::SeedableRng; let mut seed = Zeroizing::new([0_u8; 32]); - rand::RngCore::fill_bytes(rng, &mut *seed); + RngCore::fill_bytes(rng, &mut *seed); let mut rng = rand_hc::Hc128Rng::from_seed(*seed); let sk_e = k256::ecdh::EphemeralSecret::random(&mut rng); @@ -187,7 +186,7 @@ impl KeyAgreementScheme for K256 { type EphemeralSecretKey = EphemeralSecretKey; type EphemeralPublicKey = EphemeralPublicKey; - type SecretKey = SecretKey; + type SecretKey = KeyExchangeKey; type PublicKey = PublicKey; type SharedSecret = SharedSecret; @@ -235,7 +234,7 @@ impl KeyAgreementScheme for K256 { mod test { use super::{EphemeralPublicKey, EphemeralSecretKey}; use crate::{ - dsa::ecdsa_k256_keccak::SecretKey, + dsa::ecdsa_k256_keccak::KeyExchangeKey, rand::test_utils::seeded_rng, utils::{Deserializable, Serializable}, }; @@ -245,7 +244,7 @@ mod test { let mut rng = seeded_rng([0u8; 32]); // 1. Generate the static key-pair for Alice - let sk = SecretKey::with_rng(&mut rng); + let sk = KeyExchangeKey::with_rng(&mut rng); let pk = sk.public_key(); // 2. Generate the ephemeral key-pair for Bob diff --git a/miden-crypto/src/ecdh/x25519.rs b/miden-crypto/src/ecdh/x25519.rs index 1f53266295..f522ac1bde 100644 --- a/miden-crypto/src/ecdh/x25519.rs +++ b/miden-crypto/src/ecdh/x25519.rs @@ -20,14 +20,13 @@ use rand::{CryptoRng, RngCore}; use subtle::ConstantTimeEq; use crate::{ - dsa::eddsa_25519_sha512::{PublicKey, SecretKey}, + dsa::eddsa_25519_sha512::{KeyExchangeKey, PublicKey}, ecdh::KeyAgreementScheme, utils::{ ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable, zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}, }, }; - // SHARED SECRETE // ================================================================================================ @@ -116,7 +115,7 @@ impl EphemeralSecretKey { // dependency matching ours use k256::elliptic_curve::rand_core::SeedableRng; let mut seed = Zeroizing::new([0_u8; 32]); - rand::RngCore::fill_bytes(rng, &mut *seed); + RngCore::fill_bytes(rng, &mut *seed); let rng = rand_hc::Hc128Rng::from_seed(*seed); let sk = x25519_dalek::EphemeralSecret::random_from_rng(rng); @@ -182,7 +181,7 @@ impl KeyAgreementScheme for X25519 { type EphemeralSecretKey = EphemeralSecretKey; type EphemeralPublicKey = EphemeralPublicKey; - type SecretKey = SecretKey; + type SecretKey = KeyExchangeKey; type PublicKey = PublicKey; type SharedSecret = SharedSecret; @@ -249,8 +248,8 @@ mod tests { use super::*; use crate::{ - dsa::eddsa_25519_sha512::SecretKey, ecdh::KeyAgreementError, rand::test_utils::seeded_rng, - utils::Deserializable, + dsa::eddsa_25519_sha512::KeyExchangeKey, ecdh::KeyAgreementError, + rand::test_utils::seeded_rng, utils::Deserializable, }; #[test] @@ -258,7 +257,7 @@ mod tests { let mut rng = seeded_rng([0u8; 32]); // 1. Generate the static key-pair for Alice - let sk = SecretKey::with_rng(&mut rng); + let sk = KeyExchangeKey::with_rng(&mut rng); let pk = sk.public_key(); // 2. Generate the ephemeral key-pair for Bob @@ -295,7 +294,7 @@ mod tests { #[test] fn exchange_static_ephemeral_rejects_zero_shared_secret() { let mut rng = seeded_rng([0u8; 32]); - let static_sk = SecretKey::with_rng(&mut rng); + let static_sk = KeyExchangeKey::with_rng(&mut rng); let low_order_bytes = EIGHT_TORSION[0].to_montgomery().to_bytes(); let low_order_pk = EphemeralPublicKey { diff --git a/miden-crypto/src/hash/algebraic_sponge/mod.rs b/miden-crypto/src/hash/algebraic_sponge/mod.rs index da89857ac3..58a09c6038 100644 --- a/miden-crypto/src/hash/algebraic_sponge/mod.rs +++ b/miden-crypto/src/hash/algebraic_sponge/mod.rs @@ -144,7 +144,7 @@ pub(crate) trait AlgebraicSponge { // set the current rate element to the input. since we take at most 7 bytes, we are // guaranteed that the inputs data will fit into a single field element. - state[RATE_RANGE.start + rate_pos] = Felt::new(u64::from_le_bytes(buf)); + state[RATE_RANGE.start + rate_pos] = Felt::new_unchecked(u64::from_le_bytes(buf)); // proceed filling the range. if it's full, then we apply a permutation and reset the // counter to the beginning of the range. @@ -192,29 +192,6 @@ pub(crate) trait AlgebraicSponge { Self::hash_elements(elements) } - /// Returns hash(`seed` || `value`). This method is intended for use in PRNG and PoW contexts. - fn merge_with_int(seed: Word, value: u64) -> Word { - // initialize the state as follows: - // - seed is copied into the first 4 elements of the rate portion of the state. - // - if the value fits into a single field element, copy it into the fifth rate element and - // set the first capacity element to 5. - // - if the value doesn't fit into a single field element, split it into two field elements, - // copy them into rate elements 5 and 6 and set the first capacity element to 6. - let mut state = [ZERO; STATE_WIDTH]; - state[RATE0_RANGE].copy_from_slice(seed.as_elements()); - state[RATE1_RANGE.start] = Felt::new(value); - if value < Felt::ORDER { - state[CAPACITY_RANGE.start] = Felt::from_u8(5_u8); - } else { - state[RATE1_RANGE.start + 1] = Felt::new(value / Felt::ORDER); - state[CAPACITY_RANGE.start] = Felt::from_u8(6_u8); - } - - // apply the permutation and return the digest portion of the rate - Self::apply_permutation(&mut state); - Word::new(state[DIGEST_RANGE].try_into().unwrap()) - } - // DOMAIN IDENTIFIER HASHING // -------------------------------------------------------------------------------------------- diff --git a/miden-crypto/src/hash/algebraic_sponge/poseidon2/constants.rs b/miden-crypto/src/hash/algebraic_sponge/poseidon2/constants.rs index 247a1a2a1f..1336449ff2 100644 --- a/miden-crypto/src/hash/algebraic_sponge/poseidon2/constants.rs +++ b/miden-crypto/src/hash/algebraic_sponge/poseidon2/constants.rs @@ -1,31 +1,33 @@ -use super::{Felt, STATE_WIDTH}; - // HASH FUNCTION DEFINING CONSTANTS // ================================================================================================ +use miden_field::Felt; +use p3_goldilocks::{ + GOLDILOCKS_POSEIDON2_HALF_FULL_ROUNDS, GOLDILOCKS_POSEIDON2_PARTIAL_ROUNDS_12, +}; + +use super::STATE_WIDTH; -/// Number of external rounds. -pub(crate) const NUM_EXTERNAL_ROUNDS: usize = 8; /// Number of either initial or terminal external rounds. -pub(crate) const NUM_EXTERNAL_ROUNDS_HALF: usize = NUM_EXTERNAL_ROUNDS / 2; +pub(crate) const NUM_EXTERNAL_ROUNDS_HALF: usize = GOLDILOCKS_POSEIDON2_HALF_FULL_ROUNDS; /// Number of internal rounds. -pub(crate) const NUM_INTERNAL_ROUNDS: usize = 22; +pub(crate) const NUM_INTERNAL_ROUNDS: usize = GOLDILOCKS_POSEIDON2_PARTIAL_ROUNDS_12; // DIAGONAL MATRIX USED IN INTERNAL ROUNDS // ================================================================================================ pub(crate) const MAT_DIAG: [Felt; STATE_WIDTH] = [ - Felt::new(0xc3b6c08e23ba9300), - Felt::new(0xd84b5de94a324fb6), - Felt::new(0x0d0c371c5b35b84f), - Felt::new(0x7964f570e7188037), - Felt::new(0x5daf18bbd996604b), - Felt::new(0x6743bc47b9595257), - Felt::new(0x5528b9362c59bb70), - Felt::new(0xac45e25b7127b68b), - Felt::new(0xa2077d7dfbb606b5), - Felt::new(0xf3faac6faee378ae), - Felt::new(0x0c6388b51545e883), - Felt::new(0xd27dbb6944917b60), + Felt::new_unchecked(0xfffffffeffffffff), + Felt::new_unchecked(0x0000000000000001), + Felt::new_unchecked(0x0000000000000002), + Felt::new_unchecked(0x7fffffff80000001), + Felt::new_unchecked(0x0000000000000003), + Felt::new_unchecked(0x0000000000000004), + Felt::new_unchecked(0x7fffffff80000000), + Felt::new_unchecked(0xfffffffefffffffe), + Felt::new_unchecked(0xfffffffefffffffd), + Felt::new_unchecked(0xbfffffff40000001), + Felt::new_unchecked(0x3fffffffc0000000), + Felt::new_unchecked(0xdfffffff20000001), ]; // ROUND CONSTANTS @@ -33,143 +35,177 @@ pub(crate) const MAT_DIAG: [Felt; STATE_WIDTH] = [ pub(crate) const ARK_EXT_INITIAL: [[Felt; 12]; 4] = [ [ - Felt::new(0x13dcf33aba214f46), - Felt::new(0x30b3b654a1da6d83), - Felt::new(0x1fc634ada6159b56), - Felt::new(0x937459964dc03466), - Felt::new(0xedd2ef2ca7949924), - Felt::new(0xede9affde0e22f68), - Felt::new(0x8515b9d6bac9282d), - Felt::new(0x6b5c07b4e9e900d8), - Felt::new(0x1ec66368838c8a08), - Felt::new(0x9042367d80d1fbab), - Felt::new(0x400283564a3c3799), - Felt::new(0x4a00be0466bca75e), + Felt::new_unchecked(0x13dcf33aba214f46), + Felt::new_unchecked(0x30b3b654a1da6d83), + Felt::new_unchecked(0x1fc634ada6159b56), + Felt::new_unchecked(0x937459964dc03466), + Felt::new_unchecked(0xedd2ef2ca7949924), + Felt::new_unchecked(0xede9affde0e22f68), + Felt::new_unchecked(0x8515b9d6bac9282d), + Felt::new_unchecked(0x6b5c07b4e9e900d8), + Felt::new_unchecked(0x1ec66368838c8a08), + Felt::new_unchecked(0x9042367d80d1fbab), + Felt::new_unchecked(0x400283564a3c3799), + Felt::new_unchecked(0x4a00be0466bca75e), ], [ - Felt::new(0x7913beee58e3817f), - Felt::new(0xf545e88532237d90), - Felt::new(0x22f8cb8736042005), - Felt::new(0x6f04990e247a2623), - Felt::new(0xfe22e87ba37c38cd), - Felt::new(0xd20e32c85ffe2815), - Felt::new(0x117227674048fe73), - Felt::new(0x4e9fb7ea98a6b145), - Felt::new(0xe0866c232b8af08b), - Felt::new(0x00bbc77916884964), - Felt::new(0x7031c0fb990d7116), - Felt::new(0x240a9e87cf35108f), + Felt::new_unchecked(0x7913beee58e3817f), + Felt::new_unchecked(0xf545e88532237d90), + Felt::new_unchecked(0x22f8cb8736042005), + Felt::new_unchecked(0x6f04990e247a2623), + Felt::new_unchecked(0xfe22e87ba37c38cd), + Felt::new_unchecked(0xd20e32c85ffe2815), + Felt::new_unchecked(0x117227674048fe73), + Felt::new_unchecked(0x4e9fb7ea98a6b145), + Felt::new_unchecked(0xe0866c232b8af08b), + Felt::new_unchecked(0x00bbc77916884964), + Felt::new_unchecked(0x7031c0fb990d7116), + Felt::new_unchecked(0x240a9e87cf35108f), ], [ - Felt::new(0x2e6363a5a12244b3), - Felt::new(0x5e1c3787d1b5011c), - Felt::new(0x4132660e2a196e8b), - Felt::new(0x3a013b648d3d4327), - Felt::new(0xf79839f49888ea43), - Felt::new(0xfe85658ebafe1439), - Felt::new(0xb6889825a14240bd), - Felt::new(0x578453605541382b), - Felt::new(0x4508cda8f6b63ce9), - Felt::new(0x9c3ef35848684c91), - Felt::new(0x0812bde23c87178c), - Felt::new(0xfe49638f7f722c14), + Felt::new_unchecked(0x2e6363a5a12244b3), + Felt::new_unchecked(0x5e1c3787d1b5011c), + Felt::new_unchecked(0x4132660e2a196e8b), + Felt::new_unchecked(0x3a013b648d3d4327), + Felt::new_unchecked(0xf79839f49888ea43), + Felt::new_unchecked(0xfe85658ebafe1439), + Felt::new_unchecked(0xb6889825a14240bd), + Felt::new_unchecked(0x578453605541382b), + Felt::new_unchecked(0x4508cda8f6b63ce9), + Felt::new_unchecked(0x9c3ef35848684c91), + Felt::new_unchecked(0x0812bde23c87178c), + Felt::new_unchecked(0xfe49638f7f722c14), ], [ - Felt::new(0x8e3f688ce885cbf5), - Felt::new(0xb8e110acf746a87d), - Felt::new(0xb4b2e8973a6dabef), - Felt::new(0x9e714c5da3d462ec), - Felt::new(0x6438f9033d3d0c15), - Felt::new(0x24312f7cf1a27199), - Felt::new(0x23f843bb47acbf71), - Felt::new(0x9183f11a34be9f01), - Felt::new(0x839062fbb9d45dbf), - Felt::new(0x24b56e7e6c2e43fa), - Felt::new(0xe1683da61c962a72), - Felt::new(0xa95c63971a19bfa7), + Felt::new_unchecked(0x8e3f688ce885cbf5), + Felt::new_unchecked(0xb8e110acf746a87d), + Felt::new_unchecked(0xb4b2e8973a6dabef), + Felt::new_unchecked(0x9e714c5da3d462ec), + Felt::new_unchecked(0x6438f9033d3d0c15), + Felt::new_unchecked(0x24312f7cf1a27199), + Felt::new_unchecked(0x23f843bb47acbf71), + Felt::new_unchecked(0x9183f11a34be9f01), + Felt::new_unchecked(0x839062fbb9d45dbf), + Felt::new_unchecked(0x24b56e7e6c2e43fa), + Felt::new_unchecked(0xe1683da61c962a72), + Felt::new_unchecked(0xa95c63971a19bfa7), ], ]; pub(crate) const ARK_INT: [Felt; 22] = [ - Felt::new(0x4adf842aa75d4316), - Felt::new(0xf8fbb871aa4ab4eb), - Felt::new(0x68e85b6eb2dd6aeb), - Felt::new(0x07a0b06b2d270380), - Felt::new(0xd94e0228bd282de4), - Felt::new(0x8bdd91d3250c5278), - Felt::new(0x209c68b88bba778f), - Felt::new(0xb5e18cdab77f3877), - Felt::new(0xb296a3e808da93fa), - Felt::new(0x8370ecbda11a327e), - Felt::new(0x3f9075283775dad8), - Felt::new(0xb78095bb23c6aa84), - Felt::new(0x3f36b9fe72ad4e5f), - Felt::new(0x69bc96780b10b553), - Felt::new(0x3f1d341f2eb7b881), - Felt::new(0x4e939e9815838818), - Felt::new(0xda366b3ae2a31604), - Felt::new(0xbc89db1e7287d509), - Felt::new(0x6102f411f9ef5659), - Felt::new(0x58725c5e7ac1f0ab), - Felt::new(0x0df5856c798883e7), - Felt::new(0xf7bb62a8da4c961b), + Felt::new_unchecked(0x4adf842aa75d4316), + Felt::new_unchecked(0xf8fbb871aa4ab4eb), + Felt::new_unchecked(0x68e85b6eb2dd6aeb), + Felt::new_unchecked(0x07a0b06b2d270380), + Felt::new_unchecked(0xd94e0228bd282de4), + Felt::new_unchecked(0x8bdd91d3250c5278), + Felt::new_unchecked(0x209c68b88bba778f), + Felt::new_unchecked(0xb5e18cdab77f3877), + Felt::new_unchecked(0xb296a3e808da93fa), + Felt::new_unchecked(0x8370ecbda11a327e), + Felt::new_unchecked(0x3f9075283775dad8), + Felt::new_unchecked(0xb78095bb23c6aa84), + Felt::new_unchecked(0x3f36b9fe72ad4e5f), + Felt::new_unchecked(0x69bc96780b10b553), + Felt::new_unchecked(0x3f1d341f2eb7b881), + Felt::new_unchecked(0x4e939e9815838818), + Felt::new_unchecked(0xda366b3ae2a31604), + Felt::new_unchecked(0xbc89db1e7287d509), + Felt::new_unchecked(0x6102f411f9ef5659), + Felt::new_unchecked(0x58725c5e7ac1f0ab), + Felt::new_unchecked(0x0df5856c798883e7), + Felt::new_unchecked(0xf7bb62a8da4c961b), ]; pub(crate) const ARK_EXT_TERMINAL: [[Felt; STATE_WIDTH]; 4] = [ [ - Felt::new(0xc68be7c94882a24d), - Felt::new(0xaf996d5d5cdaedd9), - Felt::new(0x9717f025e7daf6a5), - Felt::new(0x6436679e6e7216f4), - Felt::new(0x8a223d99047af267), - Felt::new(0xbb512e35a133ba9a), - Felt::new(0xfbbf44097671aa03), - Felt::new(0xf04058ebf6811e61), - Felt::new(0x5cca84703fac7ffb), - Felt::new(0x9b55c7945de6469f), - Felt::new(0x8e05bf09808e934f), - Felt::new(0x2ea900de876307d7), + Felt::new_unchecked(0xc68be7c94882a24d), + Felt::new_unchecked(0xaf996d5d5cdaedd9), + Felt::new_unchecked(0x9717f025e7daf6a5), + Felt::new_unchecked(0x6436679e6e7216f4), + Felt::new_unchecked(0x8a223d99047af267), + Felt::new_unchecked(0xbb512e35a133ba9a), + Felt::new_unchecked(0xfbbf44097671aa03), + Felt::new_unchecked(0xf04058ebf6811e61), + Felt::new_unchecked(0x5cca84703fac7ffb), + Felt::new_unchecked(0x9b55c7945de6469f), + Felt::new_unchecked(0x8e05bf09808e934f), + Felt::new_unchecked(0x2ea900de876307d7), ], [ - Felt::new(0x7748fff2b38dfb89), - Felt::new(0x6b99a676dd3b5d81), - Felt::new(0xac4bb7c627cf7c13), - Felt::new(0xadb6ebe5e9e2f5ba), - Felt::new(0x2d33378cafa24ae3), - Felt::new(0x1e5b73807543f8c2), - Felt::new(0x09208814bfebb10f), - Felt::new(0x782e64b6bb5b93dd), - Felt::new(0xadd5a48eac90b50f), - Felt::new(0xadd4c54c736ea4b1), - Felt::new(0xd58dbb86ed817fd8), - Felt::new(0x6d5ed1a533f34ddd), + Felt::new_unchecked(0x7748fff2b38dfb89), + Felt::new_unchecked(0x6b99a676dd3b5d81), + Felt::new_unchecked(0xac4bb7c627cf7c13), + Felt::new_unchecked(0xadb6ebe5e9e2f5ba), + Felt::new_unchecked(0x2d33378cafa24ae3), + Felt::new_unchecked(0x1e5b73807543f8c2), + Felt::new_unchecked(0x09208814bfebb10f), + Felt::new_unchecked(0x782e64b6bb5b93dd), + Felt::new_unchecked(0xadd5a48eac90b50f), + Felt::new_unchecked(0xadd4c54c736ea4b1), + Felt::new_unchecked(0xd58dbb86ed817fd8), + Felt::new_unchecked(0x6d5ed1a533f34ddd), ], [ - Felt::new(0x28686aa3e36b7cb9), - Felt::new(0x591abd3476689f36), - Felt::new(0x047d766678f13875), - Felt::new(0xa2a11112625f5b49), - Felt::new(0x21fd10a3f8304958), - Felt::new(0xf9b40711443b0280), - Felt::new(0xd2697eb8b2bde88e), - Felt::new(0x3493790b51731b3f), - Felt::new(0x11caf9dd73764023), - Felt::new(0x7acfb8f72878164e), - Felt::new(0x744ec4db23cefc26), - Felt::new(0x1e00e58f422c6340), + Felt::new_unchecked(0x28686aa3e36b7cb9), + Felt::new_unchecked(0x591abd3476689f36), + Felt::new_unchecked(0x047d766678f13875), + Felt::new_unchecked(0xa2a11112625f5b49), + Felt::new_unchecked(0x21fd10a3f8304958), + Felt::new_unchecked(0xf9b40711443b0280), + Felt::new_unchecked(0xd2697eb8b2bde88e), + Felt::new_unchecked(0x3493790b51731b3f), + Felt::new_unchecked(0x11caf9dd73764023), + Felt::new_unchecked(0x7acfb8f72878164e), + Felt::new_unchecked(0x744ec4db23cefc26), + Felt::new_unchecked(0x1e00e58f422c6340), ], [ - Felt::new(0x21dd28d906a62dda), - Felt::new(0xf32a46ab5f465b5f), - Felt::new(0xbfce13201f3f7e6b), - Felt::new(0xf30d2e7adb5304e2), - Felt::new(0xecdf4ee4abad48e9), - Felt::new(0xf94e82182d395019), - Felt::new(0x4ee52e3744d887c5), - Felt::new(0xa1341c7cac0083b2), - Felt::new(0x2302fb26c30c834a), - Felt::new(0xaea3c587273bf7d3), - Felt::new(0xf798e24961823ec7), - Felt::new(0x962deba3e9a2cd94), + Felt::new_unchecked(0x21dd28d906a62dda), + Felt::new_unchecked(0xf32a46ab5f465b5f), + Felt::new_unchecked(0xbfce13201f3f7e6b), + Felt::new_unchecked(0xf30d2e7adb5304e2), + Felt::new_unchecked(0xecdf4ee4abad48e9), + Felt::new_unchecked(0xf94e82182d395019), + Felt::new_unchecked(0x4ee52e3744d887c5), + Felt::new_unchecked(0xa1341c7cac0083b2), + Felt::new_unchecked(0x2302fb26c30c834a), + Felt::new_unchecked(0xaea3c587273bf7d3), + Felt::new_unchecked(0xf798e24961823ec7), + Felt::new_unchecked(0x962deba3e9a2cd94), ], ]; + +#[cfg(test)] +mod tests { + use p3_goldilocks::{ + GOLDILOCKS_POSEIDON2_RC_12_EXTERNAL_FINAL, GOLDILOCKS_POSEIDON2_RC_12_EXTERNAL_INITIAL, + GOLDILOCKS_POSEIDON2_RC_12_INTERNAL, MATRIX_DIAG_12_GOLDILOCKS, + }; + + use super::*; + + #[test] + fn test_mat_diag() { + assert_eq!(MAT_DIAG, MATRIX_DIAG_12_GOLDILOCKS.map(Felt::from)); + } + + #[test] + fn test_ark_ext_initial() { + for (i, &ark) in ARK_EXT_INITIAL.iter().enumerate() { + assert_eq!(ark, GOLDILOCKS_POSEIDON2_RC_12_EXTERNAL_INITIAL[i].map(Felt::from)); + } + } + + #[test] + fn test_ark_int() { + assert_eq!(ARK_INT, GOLDILOCKS_POSEIDON2_RC_12_INTERNAL.map(Felt::from)); + } + + #[test] + fn test_ark_ext_terminal() { + for (i, &ark) in ARK_EXT_TERMINAL.iter().enumerate() { + assert_eq!(ark, GOLDILOCKS_POSEIDON2_RC_12_EXTERNAL_FINAL[i].map(Felt::from)); + } + } +} diff --git a/miden-crypto/src/hash/algebraic_sponge/poseidon2/mod.rs b/miden-crypto/src/hash/algebraic_sponge/poseidon2/mod.rs index db483f0c56..df63c2a1bb 100644 --- a/miden-crypto/src/hash/algebraic_sponge/poseidon2/mod.rs +++ b/miden-crypto/src/hash/algebraic_sponge/poseidon2/mod.rs @@ -1,21 +1,48 @@ +use once_cell::sync::Lazy; +use p3_goldilocks::Goldilocks; +use p3_symmetric::Permutation; + use super::{ AlgebraicSponge, CAPACITY_RANGE, DIGEST_RANGE, Felt, RATE_RANGE, RATE0_RANGE, RATE1_RANGE, - Range, STATE_WIDTH, Word, ZERO, + Range, STATE_WIDTH, Word, +}; +use crate::{ + ZERO, + hash::algebraic_sponge::poseidon2::constants::{ + ARK_EXT_INITIAL, ARK_EXT_TERMINAL, ARK_INT, MAT_DIAG, + }, }; mod constants; -use constants::{ - ARK_EXT_INITIAL, ARK_EXT_TERMINAL, ARK_INT, MAT_DIAG, NUM_EXTERNAL_ROUNDS_HALF, - NUM_INTERNAL_ROUNDS, -}; +use constants::{NUM_EXTERNAL_ROUNDS_HALF, NUM_INTERNAL_ROUNDS}; #[cfg(test)] mod test; +static P3_POSEIDON2: Lazy> = + Lazy::new(p3_goldilocks::default_goldilocks_poseidon2_12); + +/// Applies Plonky3's optimized Poseidon2 permutation to a `[Felt; 12]` state. +/// +/// `Felt` is `#[repr(transparent)]` over `Goldilocks`, so the transmute is safe. +/// A process-global lazy static holds the permutation so round constants are not reallocated on +/// every call (including `no_std`, via `once_cell` and the `critical-section` crate). +#[inline(always)] +fn p3_permute(state: &mut [Felt; STATE_WIDTH]) { + // SAFETY: Felt is #[repr(transparent)] over Goldilocks. + let gl_state = + unsafe { &mut *(state as *mut [Felt; STATE_WIDTH] as *mut [Goldilocks; STATE_WIDTH]) }; + + P3_POSEIDON2.permute_mut(gl_state); +} + /// Implementation of the Poseidon2 hash function with 256-bit output. /// -/// The implementation follows the original [specification](https://eprint.iacr.org/2023/323) and -/// its accompanying reference [implementation](https://github.com/HorizenLabs/poseidon2). +/// The permutation is delegated to Plonky3's optimized `Poseidon2Goldilocks<12>`, which provides +/// hardware-accelerated implementations on aarch64 (NEON inline assembly) and an optimized generic +/// implementation on other architectures. The internal MDS diagonal uses small special values +/// (-2, 1, 2, 1/2, 3, 4, ...) that enable multiplication via shifts and halves rather than full +/// field multiplications. /// /// The parameters used to instantiate the function are: /// * Field: 64-bit prime field with modulus 2^64 - 2^32 + 1. @@ -35,12 +62,11 @@ mod test; /// and it can be serialized into 32 bytes (256 bits). /// /// ## Hash output consistency -/// Functions [hash_elements()](Poseidon2::hash_elements), [merge()](Poseidon2::merge), and -/// [merge_with_int()](Poseidon2::merge_with_int) are internally consistent. That is, computing -/// a hash for the same set of elements using these functions will always produce the same -/// result. For example, merging two digests using [merge()](Poseidon2::merge) will produce the -/// same result as hashing 8 elements which make up these digests using -/// [hash_elements()](Poseidon2::hash_elements) function. +/// Functions [hash_elements()](Poseidon2::hash_elements), and [merge()](Poseidon2::merge), are +/// internally consistent. That is, computing a hash for the same set of elements using these +/// functions will always produce the same result. For example, merging two digests using +/// [merge()](Poseidon2::merge) will produce the same result as hashing 8 elements which make up +/// these digests using [hash_elements()](Poseidon2::hash_elements) function. /// /// However, [hash()](Poseidon2::hash) function is not consistent with functions mentioned above. /// For example, if we take two field elements, serialize them to bytes and hash them using @@ -76,17 +102,7 @@ pub struct Poseidon2(); impl AlgebraicSponge for Poseidon2 { fn apply_permutation(state: &mut [Felt; STATE_WIDTH]) { - // 1. Apply (external) linear layer to the input - Self::apply_matmul_external(state); - - // 2. Apply initial external rounds to the state - Self::initial_external_rounds(state); - - // 3. Apply internal rounds to the state - Self::internal_rounds(state); - - // 4. Apply terminal external rounds to the state - Self::terminal_external_rounds(state); + p3_permute(state); } } @@ -164,12 +180,6 @@ impl Poseidon2 { ::merge_many(values) } - /// Returns a hash of a digest and a u64 value. - #[inline(always)] - pub fn merge_with_int(seed: Word, value: u64) -> Word { - ::merge_with_int(seed, value) - } - /// Returns a hash of two digests and a domain identifier. #[inline(always)] pub fn merge_in_domain(values: &[Word; 2], domain: Felt) -> Word { @@ -179,39 +189,6 @@ impl Poseidon2 { // POSEIDON2 PERMUTATION // -------------------------------------------------------------------------------------------- - /// Applies the initial external rounds of the permutation. - #[allow(clippy::needless_range_loop)] - #[inline(always)] - fn initial_external_rounds(state: &mut [Felt; STATE_WIDTH]) { - for r in 0..NUM_EXTERNAL_ROUNDS_HALF { - Self::add_rc(state, &ARK_EXT_INITIAL[r]); - Self::apply_sbox(state); - Self::apply_matmul_external(state); - } - } - - /// Applies the internal rounds of the permutation. - #[allow(clippy::needless_range_loop)] - #[inline(always)] - fn internal_rounds(state: &mut [Felt; STATE_WIDTH]) { - for r in 0..NUM_INTERNAL_ROUNDS { - state[0] += ARK_INT[r]; - state[0] = state[0].exp_const_u64::<7>(); - Self::matmul_internal(state, MAT_DIAG); - } - } - - /// Applies the terminal external rounds of the permutation. - #[inline(always)] - #[allow(clippy::needless_range_loop)] - fn terminal_external_rounds(state: &mut [Felt; STATE_WIDTH]) { - for r in 0..NUM_EXTERNAL_ROUNDS_HALF { - Self::add_rc(state, &ARK_EXT_TERMINAL[r]); - Self::apply_sbox(state); - Self::apply_matmul_external(state); - } - } - /// Applies the M_E (external) linear layer to the state in-place. /// /// This basically takes any 4 x 4 MDS matrix M and computes the matrix-vector product with @@ -240,37 +217,30 @@ impl Poseidon2 { } } - /// Multiplies the state block-wise with a 4 x 4 MDS matrix. + /// Multiply a 4-element vector x by: + /// [ 2 3 1 1 ] + /// [ 1 2 3 1 ] + /// [ 1 1 2 3 ] + /// [ 3 1 1 2 ]. #[inline(always)] fn matmul_m4(state: &mut [Felt; STATE_WIDTH]) { - let t4 = STATE_WIDTH / 4; - - for i in 0..t4 { - let idx = i * 4; - - let a = state[idx]; - let b = state[idx + 1]; - let c = state[idx + 2]; - let d = state[idx + 3]; - - let t0 = a + b; - let t1 = c + d; - let two_b = b.double(); - let two_d = d.double(); - - let t2 = two_b + t1; - let t3 = two_d + t0; - - let t4 = t1.double().double() + t3; - let t5 = t0.double().double() + t2; - - let t6 = t3 + t5; - let t7 = t2 + t4; - - state[idx] = t6; - state[idx + 1] = t5; - state[idx + 2] = t7; - state[idx + 3] = t4; + const N_CHUNKS: usize = STATE_WIDTH / 4; + + for i in 0..N_CHUNKS { + let base = i * 4; + let x = &mut state[base..base + 4]; + + let t01 = x[0] + x[1]; + let t23 = x[2] + x[3]; + let t0123 = t01 + t23; + let t01123 = t0123 + x[1]; + let t01233 = t0123 + x[3]; + + // The order here is important. Need to overwrite x[0] and x[2] after x[1] and x[3]. + x[3] = t01233 + x[0].double(); // 3*x[0] + x[1] + x[2] + 2*x[3] + x[1] = t01123 + x[2].double(); // x[0] + 2*x[1] + 3*x[2] + x[3] + x[0] = t01123 + t01; // 2*x[0] + 3*x[1] + x[2] + x[3] + x[2] = t01233 + t23; // x[0] + x[1] + 2*x[2] + 3*x[3] } } @@ -318,27 +288,13 @@ impl Poseidon2 { // PLONKY3 INTEGRATION // ================================================================================================ -/// Plonky3-compatible Poseidon2 permutation implementation. -/// -/// This module provides a Plonky3-compatible interface to the Poseidon2 hash function, -/// implementing the `Permutation` and `CryptographicPermutation` traits from Plonky3. -/// -/// This allows Poseidon2 to be used with Plonky3's cryptographic infrastructure, including: -/// - PaddingFreeSponge for hashing -/// - TruncatedPermutation for compression -/// - DuplexChallenger for Fiat-Shamir transforms use p3_challenger::DuplexChallenger; -use p3_symmetric::{ - CryptographicPermutation, PaddingFreeSponge, Permutation, TruncatedPermutation, -}; - -// POSEIDON2 PERMUTATION FOR PLONKY3 -// ================================================================================================ +use p3_symmetric::{CryptographicPermutation, PaddingFreeSponge, TruncatedPermutation}; /// Plonky3-compatible Poseidon2 permutation. /// -/// This struct wraps the Poseidon2 permutation and implements Plonky3's `Permutation` and -/// `CryptographicPermutation` traits, allowing Poseidon2 to be used within the Plonky3 ecosystem. +/// This zero-sized wrapper delegates to Plonky3's optimized `Poseidon2Goldilocks<12>` and +/// implements the `Permutation` and `CryptographicPermutation` traits. /// /// The permutation operates on a state of 12 field elements (STATE_WIDTH = 12), with: /// - Rate: 8 elements (positions 0-7) @@ -387,7 +343,7 @@ impl Poseidon2Permutation256 { impl Permutation<[Felt; STATE_WIDTH]> for Poseidon2Permutation256 { fn permute_mut(&self, state: &mut [Felt; STATE_WIDTH]) { - Self::apply_permutation(state); + p3_permute(state); } } diff --git a/miden-crypto/src/hash/algebraic_sponge/poseidon2/test.rs b/miden-crypto/src/hash/algebraic_sponge/poseidon2/test.rs index af9bf504cb..3f85afe997 100644 --- a/miden-crypto/src/hash/algebraic_sponge/poseidon2/test.rs +++ b/miden-crypto/src/hash/algebraic_sponge/poseidon2/test.rs @@ -1,60 +1,59 @@ use p3_symmetric::{CryptographicHasher, PseudoCompressionFunction}; use super::*; -use crate::hash::poseidon2::Poseidon2; +use crate::{ZERO, hash::poseidon2::Poseidon2}; #[test] fn permutation_test_vector() { - // tests that the current implementation is consistent with - // the reference [implementation](https://github.com/HorizenLabs/poseidon2) and uses - // the test vectors provided therein let mut elements = [ ZERO, - Felt::new(1), - Felt::new(2), - Felt::new(3), - Felt::new(4), - Felt::new(5), - Felt::new(6), - Felt::new(7), - Felt::new(8), - Felt::new(9), - Felt::new(10), - Felt::new(11), + Felt::new_unchecked(1), + Felt::new_unchecked(2), + Felt::new_unchecked(3), + Felt::new_unchecked(4), + Felt::new_unchecked(5), + Felt::new_unchecked(6), + Felt::new_unchecked(7), + Felt::new_unchecked(8), + Felt::new_unchecked(9), + Felt::new_unchecked(10), + Felt::new_unchecked(11), ]; Poseidon2::apply_permutation(&mut elements); let perm = elements; - assert_eq!(perm[0], Felt::new(0x01eaef96bdf1c0c1)); - assert_eq!(perm[1], Felt::new(0x1f0d2cc525b2540c)); - assert_eq!(perm[2], Felt::new(0x6282c1dfe1e0358d)); - assert_eq!(perm[3], Felt::new(0xe780d721f698e1e6)); - assert_eq!(perm[4], Felt::new(0x280c0b6f753d833b)); - assert_eq!(perm[5], Felt::new(0x1b942dd5023156ab)); - assert_eq!(perm[6], Felt::new(0x43f0df3fcccb8398)); - assert_eq!(perm[7], Felt::new(0xe8e8190585489025)); - assert_eq!(perm[8], Felt::new(0x56bdbf72f77ada22)); - assert_eq!(perm[9], Felt::new(0x7911c32bf9dcd705)); - assert_eq!(perm[10], Felt::new(0xec467926508fbe67)); - assert_eq!(perm[11], Felt::new(0x6a50450ddf85a6ed)); + + // Expected values from Plonky3's `test_default_goldilocks_poseidon2_width_12` + assert_eq!(perm[0], Felt::new_unchecked(0xf292ab67c0f14b03)); + assert_eq!(perm[1], Felt::new_unchecked(0x0a32f1b37656544c)); + assert_eq!(perm[2], Felt::new_unchecked(0x053c61ab895498de)); + assert_eq!(perm[3], Felt::new_unchecked(0x02ff92e55b196ffb)); + assert_eq!(perm[4], Felt::new_unchecked(0x58176e8f6f58cab2)); + assert_eq!(perm[5], Felt::new_unchecked(0xb0aa1206e7aec0f8)); + assert_eq!(perm[6], Felt::new_unchecked(0xe90c13f3dce83ca4)); + assert_eq!(perm[7], Felt::new_unchecked(0xf4da15333edf39c2)); + assert_eq!(perm[8], Felt::new_unchecked(0x23b701c053c2ca6c)); + assert_eq!(perm[9], Felt::new_unchecked(0xd233d593dcdfbf58)); + assert_eq!(perm[10], Felt::new_unchecked(0x4effa5f9516fb52e)); + assert_eq!(perm[11], Felt::new_unchecked(0x0aaf4489f1f40166)); } #[test] fn test_poseidon2_permutation_basic() { - let mut state = [Felt::new(0); STATE_WIDTH]; + let mut state = [Felt::new_unchecked(0); STATE_WIDTH]; // Apply permutation let perm = Poseidon2Permutation256; perm.permute_mut(&mut state); // State should be different from all zeros after permutation - assert_ne!(state, [Felt::new(0); STATE_WIDTH]); + assert_ne!(state, [Felt::new_unchecked(0); STATE_WIDTH]); } #[test] fn test_poseidon2_permutation_consistency() { - let mut state1 = [Felt::new(0); STATE_WIDTH]; - let mut state2 = [Felt::new(0); STATE_WIDTH]; + let mut state1 = [Felt::new_unchecked(0); STATE_WIDTH]; + let mut state2 = [Felt::new_unchecked(0); STATE_WIDTH]; // Apply permutation using the trait let perm = Poseidon2Permutation256; @@ -70,18 +69,18 @@ fn test_poseidon2_permutation_consistency() { #[test] fn test_poseidon2_permutation_deterministic() { let input = [ - Felt::new(1), - Felt::new(2), - Felt::new(3), - Felt::new(4), - Felt::new(5), - Felt::new(6), - Felt::new(7), - Felt::new(8), - Felt::new(9), - Felt::new(10), - Felt::new(11), - Felt::new(12), + Felt::new_unchecked(1), + Felt::new_unchecked(2), + Felt::new_unchecked(3), + Felt::new_unchecked(4), + Felt::new_unchecked(5), + Felt::new_unchecked(6), + Felt::new_unchecked(7), + Felt::new_unchecked(8), + Felt::new_unchecked(9), + Felt::new_unchecked(10), + Felt::new_unchecked(11), + Felt::new_unchecked(12), ]; let mut state1 = input; @@ -101,14 +100,14 @@ fn test_poseidon2_hasher_vs_hash_elements() { // Test with 8 elements (exactly one rate) let input8 = [ - Felt::new(1), - Felt::new(2), - Felt::new(3), - Felt::new(4), - Felt::new(5), - Felt::new(6), - Felt::new(7), - Felt::new(8), + Felt::new_unchecked(1), + Felt::new_unchecked(2), + Felt::new_unchecked(3), + Felt::new_unchecked(4), + Felt::new_unchecked(5), + Felt::new_unchecked(6), + Felt::new_unchecked(7), + Felt::new_unchecked(8), ]; let expected: [Felt; 4] = Poseidon2::hash_elements(&input8).into(); let result = hasher.hash_iter(input8); @@ -116,22 +115,22 @@ fn test_poseidon2_hasher_vs_hash_elements() { // Test with 16 elements (two rates) let input16 = [ - Felt::new(1), - Felt::new(2), - Felt::new(3), - Felt::new(4), - Felt::new(5), - Felt::new(6), - Felt::new(7), - Felt::new(8), - Felt::new(9), - Felt::new(10), - Felt::new(11), - Felt::new(12), - Felt::new(13), - Felt::new(14), - Felt::new(15), - Felt::new(16), + Felt::new_unchecked(1), + Felt::new_unchecked(2), + Felt::new_unchecked(3), + Felt::new_unchecked(4), + Felt::new_unchecked(5), + Felt::new_unchecked(6), + Felt::new_unchecked(7), + Felt::new_unchecked(8), + Felt::new_unchecked(9), + Felt::new_unchecked(10), + Felt::new_unchecked(11), + Felt::new_unchecked(12), + Felt::new_unchecked(13), + Felt::new_unchecked(14), + Felt::new_unchecked(15), + Felt::new_unchecked(16), ]; let expected: [Felt; 4] = Poseidon2::hash_elements(&input16).into(); let result = hasher.hash_iter(input16); @@ -140,8 +139,18 @@ fn test_poseidon2_hasher_vs_hash_elements() { #[test] fn test_poseidon2_compression_vs_merge() { - let digest1 = [Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]; - let digest2 = [Felt::new(5), Felt::new(6), Felt::new(7), Felt::new(8)]; + let digest1 = [ + Felt::new_unchecked(1), + Felt::new_unchecked(2), + Felt::new_unchecked(3), + Felt::new_unchecked(4), + ]; + let digest2 = [ + Felt::new_unchecked(5), + Felt::new_unchecked(6), + Felt::new_unchecked(7), + Felt::new_unchecked(8), + ]; // Poseidon2::merge expects &[Word; 2] let expected: [Felt; 4] = Poseidon2::merge(&[digest1.into(), digest2.into()]).into(); diff --git a/miden-crypto/src/hash/algebraic_sponge/rescue/mds/freq.rs b/miden-crypto/src/hash/algebraic_sponge/rescue/mds/freq.rs index ed2e512af9..75ef3dd3c5 100644 --- a/miden-crypto/src/hash/algebraic_sponge/rescue/mds/freq.rs +++ b/miden-crypto/src/hash/algebraic_sponge/rescue/mds/freq.rs @@ -184,7 +184,7 @@ mod tests { let mut v2; for i in 0..STATE_WIDTH { - v1[i] = Felt::new(a[i]); + v1[i] = Felt::new_unchecked(a[i]); } v2 = v1; diff --git a/miden-crypto/src/hash/algebraic_sponge/rescue/mds/mod.rs b/miden-crypto/src/hash/algebraic_sponge/rescue/mds/mod.rs index 9bc4678cb3..626d148401 100644 --- a/miden-crypto/src/hash/algebraic_sponge/rescue/mds/mod.rs +++ b/miden-crypto/src/hash/algebraic_sponge/rescue/mds/mod.rs @@ -32,7 +32,7 @@ pub fn apply_mds(state: &mut [Felt; STATE_WIDTH]) { let z = (s_hi << 32) - s_hi; let (res, over) = s_lo.overflowing_add(z); - result[r] = Felt::new(res.wrapping_add(0u32.wrapping_sub(over as u32) as u64)); + result[r] = Felt::new_unchecked(res.wrapping_add(0u32.wrapping_sub(over as u32) as u64)); } *state = result; } @@ -43,171 +43,171 @@ pub fn apply_mds(state: &mut [Felt; STATE_WIDTH]) { /// RPO MDS matrix pub const MDS: [[Felt; STATE_WIDTH]; STATE_WIDTH] = [ [ - Felt::new(7), - Felt::new(23), - Felt::new(8), - Felt::new(26), - Felt::new(13), - Felt::new(10), - Felt::new(9), - Felt::new(7), - Felt::new(6), - Felt::new(22), - Felt::new(21), - Felt::new(8), + Felt::new_unchecked(7), + Felt::new_unchecked(23), + Felt::new_unchecked(8), + Felt::new_unchecked(26), + Felt::new_unchecked(13), + Felt::new_unchecked(10), + Felt::new_unchecked(9), + Felt::new_unchecked(7), + Felt::new_unchecked(6), + Felt::new_unchecked(22), + Felt::new_unchecked(21), + Felt::new_unchecked(8), ], [ - Felt::new(8), - Felt::new(7), - Felt::new(23), - Felt::new(8), - Felt::new(26), - Felt::new(13), - Felt::new(10), - Felt::new(9), - Felt::new(7), - Felt::new(6), - Felt::new(22), - Felt::new(21), + Felt::new_unchecked(8), + Felt::new_unchecked(7), + Felt::new_unchecked(23), + Felt::new_unchecked(8), + Felt::new_unchecked(26), + Felt::new_unchecked(13), + Felt::new_unchecked(10), + Felt::new_unchecked(9), + Felt::new_unchecked(7), + Felt::new_unchecked(6), + Felt::new_unchecked(22), + Felt::new_unchecked(21), ], [ - Felt::new(21), - Felt::new(8), - Felt::new(7), - Felt::new(23), - Felt::new(8), - Felt::new(26), - Felt::new(13), - Felt::new(10), - Felt::new(9), - Felt::new(7), - Felt::new(6), - Felt::new(22), + Felt::new_unchecked(21), + Felt::new_unchecked(8), + Felt::new_unchecked(7), + Felt::new_unchecked(23), + Felt::new_unchecked(8), + Felt::new_unchecked(26), + Felt::new_unchecked(13), + Felt::new_unchecked(10), + Felt::new_unchecked(9), + Felt::new_unchecked(7), + Felt::new_unchecked(6), + Felt::new_unchecked(22), ], [ - Felt::new(22), - Felt::new(21), - Felt::new(8), - Felt::new(7), - Felt::new(23), - Felt::new(8), - Felt::new(26), - Felt::new(13), - Felt::new(10), - Felt::new(9), - Felt::new(7), - Felt::new(6), + Felt::new_unchecked(22), + Felt::new_unchecked(21), + Felt::new_unchecked(8), + Felt::new_unchecked(7), + Felt::new_unchecked(23), + Felt::new_unchecked(8), + Felt::new_unchecked(26), + Felt::new_unchecked(13), + Felt::new_unchecked(10), + Felt::new_unchecked(9), + Felt::new_unchecked(7), + Felt::new_unchecked(6), ], [ - Felt::new(6), - Felt::new(22), - Felt::new(21), - Felt::new(8), - Felt::new(7), - Felt::new(23), - Felt::new(8), - Felt::new(26), - Felt::new(13), - Felt::new(10), - Felt::new(9), - Felt::new(7), + Felt::new_unchecked(6), + Felt::new_unchecked(22), + Felt::new_unchecked(21), + Felt::new_unchecked(8), + Felt::new_unchecked(7), + Felt::new_unchecked(23), + Felt::new_unchecked(8), + Felt::new_unchecked(26), + Felt::new_unchecked(13), + Felt::new_unchecked(10), + Felt::new_unchecked(9), + Felt::new_unchecked(7), ], [ - Felt::new(7), - Felt::new(6), - Felt::new(22), - Felt::new(21), - Felt::new(8), - Felt::new(7), - Felt::new(23), - Felt::new(8), - Felt::new(26), - Felt::new(13), - Felt::new(10), - Felt::new(9), + Felt::new_unchecked(7), + Felt::new_unchecked(6), + Felt::new_unchecked(22), + Felt::new_unchecked(21), + Felt::new_unchecked(8), + Felt::new_unchecked(7), + Felt::new_unchecked(23), + Felt::new_unchecked(8), + Felt::new_unchecked(26), + Felt::new_unchecked(13), + Felt::new_unchecked(10), + Felt::new_unchecked(9), ], [ - Felt::new(9), - Felt::new(7), - Felt::new(6), - Felt::new(22), - Felt::new(21), - Felt::new(8), - Felt::new(7), - Felt::new(23), - Felt::new(8), - Felt::new(26), - Felt::new(13), - Felt::new(10), + Felt::new_unchecked(9), + Felt::new_unchecked(7), + Felt::new_unchecked(6), + Felt::new_unchecked(22), + Felt::new_unchecked(21), + Felt::new_unchecked(8), + Felt::new_unchecked(7), + Felt::new_unchecked(23), + Felt::new_unchecked(8), + Felt::new_unchecked(26), + Felt::new_unchecked(13), + Felt::new_unchecked(10), ], [ - Felt::new(10), - Felt::new(9), - Felt::new(7), - Felt::new(6), - Felt::new(22), - Felt::new(21), - Felt::new(8), - Felt::new(7), - Felt::new(23), - Felt::new(8), - Felt::new(26), - Felt::new(13), + Felt::new_unchecked(10), + Felt::new_unchecked(9), + Felt::new_unchecked(7), + Felt::new_unchecked(6), + Felt::new_unchecked(22), + Felt::new_unchecked(21), + Felt::new_unchecked(8), + Felt::new_unchecked(7), + Felt::new_unchecked(23), + Felt::new_unchecked(8), + Felt::new_unchecked(26), + Felt::new_unchecked(13), ], [ - Felt::new(13), - Felt::new(10), - Felt::new(9), - Felt::new(7), - Felt::new(6), - Felt::new(22), - Felt::new(21), - Felt::new(8), - Felt::new(7), - Felt::new(23), - Felt::new(8), - Felt::new(26), + Felt::new_unchecked(13), + Felt::new_unchecked(10), + Felt::new_unchecked(9), + Felt::new_unchecked(7), + Felt::new_unchecked(6), + Felt::new_unchecked(22), + Felt::new_unchecked(21), + Felt::new_unchecked(8), + Felt::new_unchecked(7), + Felt::new_unchecked(23), + Felt::new_unchecked(8), + Felt::new_unchecked(26), ], [ - Felt::new(26), - Felt::new(13), - Felt::new(10), - Felt::new(9), - Felt::new(7), - Felt::new(6), - Felt::new(22), - Felt::new(21), - Felt::new(8), - Felt::new(7), - Felt::new(23), - Felt::new(8), + Felt::new_unchecked(26), + Felt::new_unchecked(13), + Felt::new_unchecked(10), + Felt::new_unchecked(9), + Felt::new_unchecked(7), + Felt::new_unchecked(6), + Felt::new_unchecked(22), + Felt::new_unchecked(21), + Felt::new_unchecked(8), + Felt::new_unchecked(7), + Felt::new_unchecked(23), + Felt::new_unchecked(8), ], [ - Felt::new(8), - Felt::new(26), - Felt::new(13), - Felt::new(10), - Felt::new(9), - Felt::new(7), - Felt::new(6), - Felt::new(22), - Felt::new(21), - Felt::new(8), - Felt::new(7), - Felt::new(23), + Felt::new_unchecked(8), + Felt::new_unchecked(26), + Felt::new_unchecked(13), + Felt::new_unchecked(10), + Felt::new_unchecked(9), + Felt::new_unchecked(7), + Felt::new_unchecked(6), + Felt::new_unchecked(22), + Felt::new_unchecked(21), + Felt::new_unchecked(8), + Felt::new_unchecked(7), + Felt::new_unchecked(23), ], [ - Felt::new(23), - Felt::new(8), - Felt::new(26), - Felt::new(13), - Felt::new(10), - Felt::new(9), - Felt::new(7), - Felt::new(6), - Felt::new(22), - Felt::new(21), - Felt::new(8), - Felt::new(7), + Felt::new_unchecked(23), + Felt::new_unchecked(8), + Felt::new_unchecked(26), + Felt::new_unchecked(13), + Felt::new_unchecked(10), + Felt::new_unchecked(9), + Felt::new_unchecked(7), + Felt::new_unchecked(6), + Felt::new_unchecked(22), + Felt::new_unchecked(21), + Felt::new_unchecked(8), + Felt::new_unchecked(7), ], ]; diff --git a/miden-crypto/src/hash/algebraic_sponge/rescue/mod.rs b/miden-crypto/src/hash/algebraic_sponge/rescue/mod.rs index a19ef82769..eab7adaa93 100644 --- a/miden-crypto/src/hash/algebraic_sponge/rescue/mod.rs +++ b/miden-crypto/src/hash/algebraic_sponge/rescue/mod.rs @@ -109,202 +109,202 @@ fn add_constants(state: &mut [Felt; STATE_WIDTH], ark: &[Felt; STATE_WIDTH]) { /// first half of RPO round, and ARK2 contains constants for the second half of RPO round. const ARK1: [[Felt; STATE_WIDTH]; NUM_ROUNDS] = [ [ - Felt::new(5789762306288267392), - Felt::new(6522564764413701783), - Felt::new(17809893479458208203), - Felt::new(107145243989736508), - Felt::new(6388978042437517382), - Felt::new(15844067734406016715), - Felt::new(9975000513555218239), - Felt::new(3344984123768313364), - Felt::new(9959189626657347191), - Felt::new(12960773468763563665), - Felt::new(9602914297752488475), - Felt::new(16657542370200465908), + Felt::new_unchecked(5789762306288267392), + Felt::new_unchecked(6522564764413701783), + Felt::new_unchecked(17809893479458208203), + Felt::new_unchecked(107145243989736508), + Felt::new_unchecked(6388978042437517382), + Felt::new_unchecked(15844067734406016715), + Felt::new_unchecked(9975000513555218239), + Felt::new_unchecked(3344984123768313364), + Felt::new_unchecked(9959189626657347191), + Felt::new_unchecked(12960773468763563665), + Felt::new_unchecked(9602914297752488475), + Felt::new_unchecked(16657542370200465908), ], [ - Felt::new(12987190162843096997), - Felt::new(653957632802705281), - Felt::new(4441654670647621225), - Felt::new(4038207883745915761), - Felt::new(5613464648874830118), - Felt::new(13222989726778338773), - Felt::new(3037761201230264149), - Felt::new(16683759727265180203), - Felt::new(8337364536491240715), - Felt::new(3227397518293416448), - Felt::new(8110510111539674682), - Felt::new(2872078294163232137), + Felt::new_unchecked(12987190162843096997), + Felt::new_unchecked(653957632802705281), + Felt::new_unchecked(4441654670647621225), + Felt::new_unchecked(4038207883745915761), + Felt::new_unchecked(5613464648874830118), + Felt::new_unchecked(13222989726778338773), + Felt::new_unchecked(3037761201230264149), + Felt::new_unchecked(16683759727265180203), + Felt::new_unchecked(8337364536491240715), + Felt::new_unchecked(3227397518293416448), + Felt::new_unchecked(8110510111539674682), + Felt::new_unchecked(2872078294163232137), ], [ - Felt::new(18072785500942327487), - Felt::new(6200974112677013481), - Felt::new(17682092219085884187), - Felt::new(10599526828986756440), - Felt::new(975003873302957338), - Felt::new(8264241093196931281), - Felt::new(10065763900435475170), - Felt::new(2181131744534710197), - Felt::new(6317303992309418647), - Felt::new(1401440938888741532), - Felt::new(8884468225181997494), - Felt::new(13066900325715521532), + Felt::new_unchecked(18072785500942327487), + Felt::new_unchecked(6200974112677013481), + Felt::new_unchecked(17682092219085884187), + Felt::new_unchecked(10599526828986756440), + Felt::new_unchecked(975003873302957338), + Felt::new_unchecked(8264241093196931281), + Felt::new_unchecked(10065763900435475170), + Felt::new_unchecked(2181131744534710197), + Felt::new_unchecked(6317303992309418647), + Felt::new_unchecked(1401440938888741532), + Felt::new_unchecked(8884468225181997494), + Felt::new_unchecked(13066900325715521532), ], [ - Felt::new(5674685213610121970), - Felt::new(5759084860419474071), - Felt::new(13943282657648897737), - Felt::new(1352748651966375394), - Felt::new(17110913224029905221), - Felt::new(1003883795902368422), - Felt::new(4141870621881018291), - Felt::new(8121410972417424656), - Felt::new(14300518605864919529), - Felt::new(13712227150607670181), - Felt::new(17021852944633065291), - Felt::new(6252096473787587650), + Felt::new_unchecked(5674685213610121970), + Felt::new_unchecked(5759084860419474071), + Felt::new_unchecked(13943282657648897737), + Felt::new_unchecked(1352748651966375394), + Felt::new_unchecked(17110913224029905221), + Felt::new_unchecked(1003883795902368422), + Felt::new_unchecked(4141870621881018291), + Felt::new_unchecked(8121410972417424656), + Felt::new_unchecked(14300518605864919529), + Felt::new_unchecked(13712227150607670181), + Felt::new_unchecked(17021852944633065291), + Felt::new_unchecked(6252096473787587650), ], [ - Felt::new(4887609836208846458), - Felt::new(3027115137917284492), - Felt::new(9595098600469470675), - Felt::new(10528569829048484079), - Felt::new(7864689113198939815), - Felt::new(17533723827845969040), - Felt::new(5781638039037710951), - Felt::new(17024078752430719006), - Felt::new(109659393484013511), - Felt::new(7158933660534805869), - Felt::new(2955076958026921730), - Felt::new(7433723648458773977), + Felt::new_unchecked(4887609836208846458), + Felt::new_unchecked(3027115137917284492), + Felt::new_unchecked(9595098600469470675), + Felt::new_unchecked(10528569829048484079), + Felt::new_unchecked(7864689113198939815), + Felt::new_unchecked(17533723827845969040), + Felt::new_unchecked(5781638039037710951), + Felt::new_unchecked(17024078752430719006), + Felt::new_unchecked(109659393484013511), + Felt::new_unchecked(7158933660534805869), + Felt::new_unchecked(2955076958026921730), + Felt::new_unchecked(7433723648458773977), ], [ - Felt::new(16308865189192447297), - Felt::new(11977192855656444890), - Felt::new(12532242556065780287), - Felt::new(14594890931430968898), - Felt::new(7291784239689209784), - Felt::new(5514718540551361949), - Felt::new(10025733853830934803), - Felt::new(7293794580341021693), - Felt::new(6728552937464861756), - Felt::new(6332385040983343262), - Felt::new(13277683694236792804), - Felt::new(2600778905124452676), + Felt::new_unchecked(16308865189192447297), + Felt::new_unchecked(11977192855656444890), + Felt::new_unchecked(12532242556065780287), + Felt::new_unchecked(14594890931430968898), + Felt::new_unchecked(7291784239689209784), + Felt::new_unchecked(5514718540551361949), + Felt::new_unchecked(10025733853830934803), + Felt::new_unchecked(7293794580341021693), + Felt::new_unchecked(6728552937464861756), + Felt::new_unchecked(6332385040983343262), + Felt::new_unchecked(13277683694236792804), + Felt::new_unchecked(2600778905124452676), ], [ - Felt::new(7123075680859040534), - Felt::new(1034205548717903090), - Felt::new(7717824418247931797), - Felt::new(3019070937878604058), - Felt::new(11403792746066867460), - Felt::new(10280580802233112374), - Felt::new(337153209462421218), - Felt::new(13333398568519923717), - Felt::new(3596153696935337464), - Felt::new(8104208463525993784), - Felt::new(14345062289456085693), - Felt::new(17036731477169661256), + Felt::new_unchecked(7123075680859040534), + Felt::new_unchecked(1034205548717903090), + Felt::new_unchecked(7717824418247931797), + Felt::new_unchecked(3019070937878604058), + Felt::new_unchecked(11403792746066867460), + Felt::new_unchecked(10280580802233112374), + Felt::new_unchecked(337153209462421218), + Felt::new_unchecked(13333398568519923717), + Felt::new_unchecked(3596153696935337464), + Felt::new_unchecked(8104208463525993784), + Felt::new_unchecked(14345062289456085693), + Felt::new_unchecked(17036731477169661256), ], ]; const ARK2: [[Felt; STATE_WIDTH]; NUM_ROUNDS] = [ [ - Felt::new(6077062762357204287), - Felt::new(15277620170502011191), - Felt::new(5358738125714196705), - Felt::new(14233283787297595718), - Felt::new(13792579614346651365), - Felt::new(11614812331536767105), - Felt::new(14871063686742261166), - Felt::new(10148237148793043499), - Felt::new(4457428952329675767), - Felt::new(15590786458219172475), - Felt::new(10063319113072092615), - Felt::new(14200078843431360086), + Felt::new_unchecked(6077062762357204287), + Felt::new_unchecked(15277620170502011191), + Felt::new_unchecked(5358738125714196705), + Felt::new_unchecked(14233283787297595718), + Felt::new_unchecked(13792579614346651365), + Felt::new_unchecked(11614812331536767105), + Felt::new_unchecked(14871063686742261166), + Felt::new_unchecked(10148237148793043499), + Felt::new_unchecked(4457428952329675767), + Felt::new_unchecked(15590786458219172475), + Felt::new_unchecked(10063319113072092615), + Felt::new_unchecked(14200078843431360086), ], [ - Felt::new(6202948458916099932), - Felt::new(17690140365333231091), - Felt::new(3595001575307484651), - Felt::new(373995945117666487), - Felt::new(1235734395091296013), - Felt::new(14172757457833931602), - Felt::new(707573103686350224), - Felt::new(15453217512188187135), - Felt::new(219777875004506018), - Felt::new(17876696346199469008), - Felt::new(17731621626449383378), - Felt::new(2897136237748376248), + Felt::new_unchecked(6202948458916099932), + Felt::new_unchecked(17690140365333231091), + Felt::new_unchecked(3595001575307484651), + Felt::new_unchecked(373995945117666487), + Felt::new_unchecked(1235734395091296013), + Felt::new_unchecked(14172757457833931602), + Felt::new_unchecked(707573103686350224), + Felt::new_unchecked(15453217512188187135), + Felt::new_unchecked(219777875004506018), + Felt::new_unchecked(17876696346199469008), + Felt::new_unchecked(17731621626449383378), + Felt::new_unchecked(2897136237748376248), ], [ - Felt::new(8023374565629191455), - Felt::new(15013690343205953430), - Felt::new(4485500052507912973), - Felt::new(12489737547229155153), - Felt::new(9500452585969030576), - Felt::new(2054001340201038870), - Felt::new(12420704059284934186), - Felt::new(355990932618543755), - Felt::new(9071225051243523860), - Felt::new(12766199826003448536), - Felt::new(9045979173463556963), - Felt::new(12934431667190679898), + Felt::new_unchecked(8023374565629191455), + Felt::new_unchecked(15013690343205953430), + Felt::new_unchecked(4485500052507912973), + Felt::new_unchecked(12489737547229155153), + Felt::new_unchecked(9500452585969030576), + Felt::new_unchecked(2054001340201038870), + Felt::new_unchecked(12420704059284934186), + Felt::new_unchecked(355990932618543755), + Felt::new_unchecked(9071225051243523860), + Felt::new_unchecked(12766199826003448536), + Felt::new_unchecked(9045979173463556963), + Felt::new_unchecked(12934431667190679898), ], [ - Felt::new(18389244934624494276), - Felt::new(16731736864863925227), - Felt::new(4440209734760478192), - Felt::new(17208448209698888938), - Felt::new(8739495587021565984), - Felt::new(17000774922218161967), - Felt::new(13533282547195532087), - Felt::new(525402848358706231), - Felt::new(16987541523062161972), - Felt::new(5466806524462797102), - Felt::new(14512769585918244983), - Felt::new(10973956031244051118), + Felt::new_unchecked(18389244934624494276), + Felt::new_unchecked(16731736864863925227), + Felt::new_unchecked(4440209734760478192), + Felt::new_unchecked(17208448209698888938), + Felt::new_unchecked(8739495587021565984), + Felt::new_unchecked(17000774922218161967), + Felt::new_unchecked(13533282547195532087), + Felt::new_unchecked(525402848358706231), + Felt::new_unchecked(16987541523062161972), + Felt::new_unchecked(5466806524462797102), + Felt::new_unchecked(14512769585918244983), + Felt::new_unchecked(10973956031244051118), ], [ - Felt::new(6982293561042362913), - Felt::new(14065426295947720331), - Felt::new(16451845770444974180), - Felt::new(7139138592091306727), - Felt::new(9012006439959783127), - Felt::new(14619614108529063361), - Felt::new(1394813199588124371), - Felt::new(4635111139507788575), - Felt::new(16217473952264203365), - Felt::new(10782018226466330683), - Felt::new(6844229992533662050), - Felt::new(7446486531695178711), + Felt::new_unchecked(6982293561042362913), + Felt::new_unchecked(14065426295947720331), + Felt::new_unchecked(16451845770444974180), + Felt::new_unchecked(7139138592091306727), + Felt::new_unchecked(9012006439959783127), + Felt::new_unchecked(14619614108529063361), + Felt::new_unchecked(1394813199588124371), + Felt::new_unchecked(4635111139507788575), + Felt::new_unchecked(16217473952264203365), + Felt::new_unchecked(10782018226466330683), + Felt::new_unchecked(6844229992533662050), + Felt::new_unchecked(7446486531695178711), ], [ - Felt::new(3736792340494631448), - Felt::new(577852220195055341), - Felt::new(6689998335515779805), - Felt::new(13886063479078013492), - Felt::new(14358505101923202168), - Felt::new(7744142531772274164), - Felt::new(16135070735728404443), - Felt::new(12290902521256031137), - Felt::new(12059913662657709804), - Felt::new(16456018495793751911), - Felt::new(4571485474751953524), - Felt::new(17200392109565783176), + Felt::new_unchecked(3736792340494631448), + Felt::new_unchecked(577852220195055341), + Felt::new_unchecked(6689998335515779805), + Felt::new_unchecked(13886063479078013492), + Felt::new_unchecked(14358505101923202168), + Felt::new_unchecked(7744142531772274164), + Felt::new_unchecked(16135070735728404443), + Felt::new_unchecked(12290902521256031137), + Felt::new_unchecked(12059913662657709804), + Felt::new_unchecked(16456018495793751911), + Felt::new_unchecked(4571485474751953524), + Felt::new_unchecked(17200392109565783176), ], [ - Felt::new(17130398059294018733), - Felt::new(519782857322261988), - Felt::new(9625384390925085478), - Felt::new(1664893052631119222), - Felt::new(7629576092524553570), - Felt::new(3485239601103661425), - Felt::new(9755891797164033838), - Felt::new(15218148195153269027), - Felt::new(16460604813734957368), - Felt::new(9643968136937729763), - Felt::new(3611348709641382851), - Felt::new(18256379591337759196), + Felt::new_unchecked(17130398059294018733), + Felt::new_unchecked(519782857322261988), + Felt::new_unchecked(9625384390925085478), + Felt::new_unchecked(1664893052631119222), + Felt::new_unchecked(7629576092524553570), + Felt::new_unchecked(3485239601103661425), + Felt::new_unchecked(9755891797164033838), + Felt::new_unchecked(15218148195153269027), + Felt::new_unchecked(16460604813734957368), + Felt::new_unchecked(9643968136937729763), + Felt::new_unchecked(3611348709641382851), + Felt::new_unchecked(18256379591337759196), ], ]; diff --git a/miden-crypto/src/hash/algebraic_sponge/rescue/rpo/mod.rs b/miden-crypto/src/hash/algebraic_sponge/rescue/rpo/mod.rs index f122f05b3b..74297b7141 100644 --- a/miden-crypto/src/hash/algebraic_sponge/rescue/rpo/mod.rs +++ b/miden-crypto/src/hash/algebraic_sponge/rescue/rpo/mod.rs @@ -29,11 +29,10 @@ mod tests; /// and it can be serialized into 32 bytes (256 bits). /// /// ## Hash output consistency -/// Functions [hash_elements()](Rpo256::hash_elements), [merge()](Rpo256::merge), and -/// [merge_with_int()](Rpo256::merge_with_int) are internally consistent. That is, computing -/// a hash for the same set of elements using these functions will always produce the same -/// result. For example, merging two digests using [merge()](Rpo256::merge) will produce the -/// same result as hashing 8 elements which make up these digests using +/// Functions [hash_elements()](Rpo256::hash_elements), and [merge()](Rpo256::merge), are internally +/// consistent. That is, computing a hash for the same set of elements using these functions will +/// always produce the same result. For example, merging two digests using [merge()](Rpo256::merge) +/// will produce the same result as hashing 8 elements which make up these digests using /// [hash_elements()](Rpo256::hash_elements) function. /// /// However, [hash()](Rpo256::hash) function is not consistent with functions mentioned above. @@ -148,12 +147,6 @@ impl Rpo256 { ::merge_many(values) } - /// Returns a hash of a digest and a u64 value. - #[inline(always)] - pub fn merge_with_int(seed: Word, value: u64) -> Word { - ::merge_with_int(seed, value) - } - /// Returns a hash of two digests and a domain identifier. #[inline(always)] pub fn merge_in_domain(values: &[Word; 2], domain: Felt) -> Word { diff --git a/miden-crypto/src/hash/algebraic_sponge/rescue/rpo/tests.rs b/miden-crypto/src/hash/algebraic_sponge/rescue/rpo/tests.rs index 3ac1126060..de2461a8e5 100644 --- a/miden-crypto/src/hash/algebraic_sponge/rescue/rpo/tests.rs +++ b/miden-crypto/src/hash/algebraic_sponge/rescue/rpo/tests.rs @@ -14,15 +14,13 @@ const ALPHA: u64 = 7; const INV_ALPHA: u64 = 10540996611094048183; use crate::{ ONE, Word, ZERO, - hash::algebraic_sponge::{ - AlgebraicSponge, BINARY_CHUNK_SIZE, CAPACITY_RANGE, RATE_RANGE, RATE_WIDTH, - }, + hash::algebraic_sponge::{BINARY_CHUNK_SIZE, CAPACITY_RANGE, RATE_RANGE, RATE_WIDTH}, rand::test_utils::rand_value, }; #[test] fn test_sbox() { - let state = [Felt::new(rand_value()); STATE_WIDTH]; + let state = [Felt::new_unchecked(rand_value()); STATE_WIDTH]; let mut expected = state; expected.iter_mut().for_each(|v| *v = v.exp_const_u64::()); @@ -35,7 +33,7 @@ fn test_sbox() { #[test] fn test_inv_sbox() { - let state = [Felt::new(rand_value()); STATE_WIDTH]; + let state = [Felt::new_unchecked(rand_value()); STATE_WIDTH]; let mut expected = state; expected.iter_mut().for_each(|v| *v = v.exp_const_u64::()); @@ -48,7 +46,7 @@ fn test_inv_sbox() { #[test] fn hash_elements_vs_merge() { - let elements = [Felt::new(rand_value()); 8]; + let elements = [Felt::new_unchecked(rand_value()); 8]; let digests: [Word; 2] = [ Word::new(elements[..4].try_into().unwrap()), @@ -62,7 +60,7 @@ fn hash_elements_vs_merge() { #[test] fn merge_vs_merge_in_domain() { - let elements = [Felt::new(rand_value()); 8]; + let elements = [Felt::new_unchecked(rand_value()); 8]; let digests: [Word; 2] = [ Word::new(elements[..4].try_into().unwrap()), @@ -87,33 +85,6 @@ fn merge_vs_merge_in_domain() { assert_ne!(merge_result, merge_in_domain_result); } -#[test] -fn hash_elements_vs_merge_with_int() { - let tmp = [Felt::new(rand_value()); 4]; - let seed = Word::new(tmp); - - // ----- value fits into a field element ------------------------------------------------------ - let val: Felt = Felt::new(rand_value()); - let m_result = ::merge_with_int(seed, val.as_canonical_u64()); - - let mut elements = seed.as_elements().to_vec(); - elements.push(val); - let h_result = Rpo256::hash_elements(&elements); - - assert_eq!(m_result, h_result); - - // ----- value does not fit into a field element ---------------------------------------------- - let val = Felt::ORDER + 2; - let m_result = ::merge_with_int(seed, val); - - let mut elements = seed.as_elements().to_vec(); - elements.push(Felt::new(val)); - elements.push(ONE); - let h_result = Rpo256::hash_elements(&elements); - - assert_eq!(m_result, h_result); -} - #[test] fn hash_padding() { // adding a zero bytes at the end of a byte string should result in a different hash @@ -153,7 +124,7 @@ fn hash_padding_no_extra_permutation_call() { // padding when hashing bytes state[CAPACITY_RANGE.start] = Felt::from_u8(RATE_WIDTH as u8); // place the final padded chunk into the last rate element - state[RATE_RANGE.end - 1] = Felt::new(u64::from_le_bytes(final_chunk)); + state[RATE_RANGE.end - 1] = Felt::new_unchecked(u64::from_le_bytes(final_chunk)); Rpo256::apply_permutation(&mut state); assert_eq!(&r1[0..4], &state[DIGEST_RANGE]); @@ -161,7 +132,7 @@ fn hash_padding_no_extra_permutation_call() { #[test] fn hash_elements_padding() { - let e1 = [Felt::new(rand_value()); 2]; + let e1 = [Felt::new_unchecked(rand_value()); 2]; let e2 = [e1[0], e1[1], ZERO]; let r1 = Rpo256::hash_elements(&e1); @@ -174,12 +145,12 @@ fn hash_elements() { let elements = [ ZERO, ONE, - Felt::new(2), - Felt::new(3), - Felt::new(4), - Felt::new(5), - Felt::new(6), - Felt::new(7), + Felt::new_unchecked(2), + Felt::new_unchecked(3), + Felt::new_unchecked(4), + Felt::new_unchecked(5), + Felt::new_unchecked(6), + Felt::new_unchecked(7), ]; let digests: [Word; 2] = [ @@ -215,23 +186,23 @@ fn hash_test_vectors() { let elements = [ ZERO, ONE, - Felt::new(2), - Felt::new(3), - Felt::new(4), - Felt::new(5), - Felt::new(6), - Felt::new(7), - Felt::new(8), - Felt::new(9), - Felt::new(10), - Felt::new(11), - Felt::new(12), - Felt::new(13), - Felt::new(14), - Felt::new(15), - Felt::new(16), - Felt::new(17), - Felt::new(18), + Felt::new_unchecked(2), + Felt::new_unchecked(3), + Felt::new_unchecked(4), + Felt::new_unchecked(5), + Felt::new_unchecked(6), + Felt::new_unchecked(7), + Felt::new_unchecked(8), + Felt::new_unchecked(9), + Felt::new_unchecked(10), + Felt::new_unchecked(11), + Felt::new_unchecked(12), + Felt::new_unchecked(13), + Felt::new_unchecked(14), + Felt::new_unchecked(15), + Felt::new_unchecked(16), + Felt::new_unchecked(17), + Felt::new_unchecked(18), ]; for (i, expected) in EXPECTED.iter().enumerate() { @@ -288,118 +259,118 @@ proptest! { /// The verification script is located at `generate_test_vectors.py` in this directory. const EXPECTED: [Word; 19] = [ Word::new([ - Felt::new(8563248028282119176), - Felt::new(14757918088501470722), - Felt::new(14042820149444308297), - Felt::new(7607140247535155355), + Felt::new_unchecked(8563248028282119176), + Felt::new_unchecked(14757918088501470722), + Felt::new_unchecked(14042820149444308297), + Felt::new_unchecked(7607140247535155355), ]), Word::new([ - Felt::new(8762449007102993687), - Felt::new(4386081033660325954), - Felt::new(5000814629424193749), - Felt::new(8171580292230495897), + Felt::new_unchecked(8762449007102993687), + Felt::new_unchecked(4386081033660325954), + Felt::new_unchecked(5000814629424193749), + Felt::new_unchecked(8171580292230495897), ]), Word::new([ - Felt::new(16710087681096729759), - Felt::new(10808706421914121430), - Felt::new(14661356949236585983), - Felt::new(5683478730832134441), + Felt::new_unchecked(16710087681096729759), + Felt::new_unchecked(10808706421914121430), + Felt::new_unchecked(14661356949236585983), + Felt::new_unchecked(5683478730832134441), ]), Word::new([ - Felt::new(5309818427047650994), - Felt::new(17172251659920546244), - Felt::new(8288476618870804357), - Felt::new(18080473279382182941), + Felt::new_unchecked(5309818427047650994), + Felt::new_unchecked(17172251659920546244), + Felt::new_unchecked(8288476618870804357), + Felt::new_unchecked(18080473279382182941), ]), Word::new([ - Felt::new(3647545403045515695), - Felt::new(3358383208908083302), - Felt::new(8797161010298072910), - Felt::new(2412100201132087248), + Felt::new_unchecked(3647545403045515695), + Felt::new_unchecked(3358383208908083302), + Felt::new_unchecked(8797161010298072910), + Felt::new_unchecked(2412100201132087248), ]), Word::new([ - Felt::new(8409780526028662686), - Felt::new(214479528340808320), - Felt::new(13626616722984122219), - Felt::new(13991752159726061594), + Felt::new_unchecked(8409780526028662686), + Felt::new_unchecked(214479528340808320), + Felt::new_unchecked(13626616722984122219), + Felt::new_unchecked(13991752159726061594), ]), Word::new([ - Felt::new(4800410126693035096), - Felt::new(8293686005479024958), - Felt::new(16849389505608627981), - Felt::new(12129312715917897796), + Felt::new_unchecked(4800410126693035096), + Felt::new_unchecked(8293686005479024958), + Felt::new_unchecked(16849389505608627981), + Felt::new_unchecked(12129312715917897796), ]), Word::new([ - Felt::new(5421234586123900205), - Felt::new(9738602082989433872), - Felt::new(7017816005734536787), - Felt::new(8635896173743411073), + Felt::new_unchecked(5421234586123900205), + Felt::new_unchecked(9738602082989433872), + Felt::new_unchecked(7017816005734536787), + Felt::new_unchecked(8635896173743411073), ]), Word::new([ - Felt::new(11707446879505873182), - Felt::new(7588005580730590001), - Felt::new(4664404372972250366), - Felt::new(17613162115550587316), + Felt::new_unchecked(11707446879505873182), + Felt::new_unchecked(7588005580730590001), + Felt::new_unchecked(4664404372972250366), + Felt::new_unchecked(17613162115550587316), ]), Word::new([ - Felt::new(6991094187713033844), - Felt::new(10140064581418506488), - Felt::new(1235093741254112241), - Felt::new(16755357411831959519), + Felt::new_unchecked(6991094187713033844), + Felt::new_unchecked(10140064581418506488), + Felt::new_unchecked(1235093741254112241), + Felt::new_unchecked(16755357411831959519), ]), Word::new([ - Felt::new(18007834547781860956), - Felt::new(5262789089508245576), - Felt::new(4752286606024269423), - Felt::new(15626544383301396533), + Felt::new_unchecked(18007834547781860956), + Felt::new_unchecked(5262789089508245576), + Felt::new_unchecked(4752286606024269423), + Felt::new_unchecked(15626544383301396533), ]), Word::new([ - Felt::new(5419895278045886802), - Felt::new(10747737918518643252), - Felt::new(14861255521757514163), - Felt::new(3291029997369465426), + Felt::new_unchecked(5419895278045886802), + Felt::new_unchecked(10747737918518643252), + Felt::new_unchecked(14861255521757514163), + Felt::new_unchecked(3291029997369465426), ]), Word::new([ - Felt::new(16916426112258580265), - Felt::new(8714377345140065340), - Felt::new(14207246102129706649), - Felt::new(6226142825442954311), + Felt::new_unchecked(16916426112258580265), + Felt::new_unchecked(8714377345140065340), + Felt::new_unchecked(14207246102129706649), + Felt::new_unchecked(6226142825442954311), ]), Word::new([ - Felt::new(7320977330193495928), - Felt::new(15630435616748408136), - Felt::new(10194509925259146809), - Felt::new(15938750299626487367), + Felt::new_unchecked(7320977330193495928), + Felt::new_unchecked(15630435616748408136), + Felt::new_unchecked(10194509925259146809), + Felt::new_unchecked(15938750299626487367), ]), Word::new([ - Felt::new(9872217233988117092), - Felt::new(5336302253150565952), - Felt::new(9650742686075483437), - Felt::new(8725445618118634861), + Felt::new_unchecked(9872217233988117092), + Felt::new_unchecked(5336302253150565952), + Felt::new_unchecked(9650742686075483437), + Felt::new_unchecked(8725445618118634861), ]), Word::new([ - Felt::new(12539853708112793207), - Felt::new(10831674032088582545), - Felt::new(11090804155187202889), - Felt::new(105068293543772992), + Felt::new_unchecked(12539853708112793207), + Felt::new_unchecked(10831674032088582545), + Felt::new_unchecked(11090804155187202889), + Felt::new_unchecked(105068293543772992), ]), Word::new([ - Felt::new(7287113073032114129), - Felt::new(6373434548664566745), - Felt::new(8097061424355177769), - Felt::new(14780666619112596652), + Felt::new_unchecked(7287113073032114129), + Felt::new_unchecked(6373434548664566745), + Felt::new_unchecked(8097061424355177769), + Felt::new_unchecked(14780666619112596652), ]), Word::new([ - Felt::new(17147873541222871127), - Felt::new(17350918081193545524), - Felt::new(5785390176806607444), - Felt::new(12480094913955467088), + Felt::new_unchecked(17147873541222871127), + Felt::new_unchecked(17350918081193545524), + Felt::new_unchecked(5785390176806607444), + Felt::new_unchecked(12480094913955467088), ]), Word::new([ - Felt::new(17273934282489765074), - Felt::new(8007352780590012415), - Felt::new(16690624932024962846), - Felt::new(8137543572359747206), + Felt::new_unchecked(17273934282489765074), + Felt::new_unchecked(8007352780590012415), + Felt::new_unchecked(16690624932024962846), + Felt::new_unchecked(8137543572359747206), ]), ]; @@ -416,20 +387,20 @@ mod p3_tests { #[test] fn test_rpo_permutation_basic() { - let mut state = [Felt::new(0); STATE_WIDTH]; + let mut state = [Felt::new_unchecked(0); STATE_WIDTH]; // Apply permutation let perm = RpoPermutation256; perm.permute_mut(&mut state); // State should be different from all zeros after permutation - assert_ne!(state, [Felt::new(0); STATE_WIDTH]); + assert_ne!(state, [Felt::new_unchecked(0); STATE_WIDTH]); } #[test] fn test_rpo_permutation_consistency() { - let mut state1 = [Felt::new(0); STATE_WIDTH]; - let mut state2 = [Felt::new(0); STATE_WIDTH]; + let mut state1 = [Felt::new_unchecked(0); STATE_WIDTH]; + let mut state2 = [Felt::new_unchecked(0); STATE_WIDTH]; // Apply permutation using the trait let perm = RpoPermutation256; @@ -445,18 +416,18 @@ mod p3_tests { #[test] fn test_rpo_permutation_deterministic() { let input = [ - Felt::new(1), - Felt::new(2), - Felt::new(3), - Felt::new(4), - Felt::new(5), - Felt::new(6), - Felt::new(7), - Felt::new(8), - Felt::new(9), - Felt::new(10), - Felt::new(11), - Felt::new(12), + Felt::new_unchecked(1), + Felt::new_unchecked(2), + Felt::new_unchecked(3), + Felt::new_unchecked(4), + Felt::new_unchecked(5), + Felt::new_unchecked(6), + Felt::new_unchecked(7), + Felt::new_unchecked(8), + Felt::new_unchecked(9), + Felt::new_unchecked(10), + Felt::new_unchecked(11), + Felt::new_unchecked(12), ]; let mut state1 = input; @@ -476,14 +447,14 @@ mod p3_tests { // Test with 8 elements (one rate) let input12 = [ - Felt::new(1), - Felt::new(2), - Felt::new(3), - Felt::new(4), - Felt::new(5), - Felt::new(6), - Felt::new(7), - Felt::new(8), + Felt::new_unchecked(1), + Felt::new_unchecked(2), + Felt::new_unchecked(3), + Felt::new_unchecked(4), + Felt::new_unchecked(5), + Felt::new_unchecked(6), + Felt::new_unchecked(7), + Felt::new_unchecked(8), ]; let expected: [Felt; 4] = Rpo256::hash_elements(&input12).into(); let result = hasher.hash_iter(input12); @@ -491,22 +462,22 @@ mod p3_tests { // Test with 16 elements (two rates) let input16 = [ - Felt::new(1), - Felt::new(2), - Felt::new(3), - Felt::new(4), - Felt::new(5), - Felt::new(6), - Felt::new(7), - Felt::new(8), - Felt::new(9), - Felt::new(10), - Felt::new(11), - Felt::new(12), - Felt::new(13), - Felt::new(14), - Felt::new(15), - Felt::new(16), + Felt::new_unchecked(1), + Felt::new_unchecked(2), + Felt::new_unchecked(3), + Felt::new_unchecked(4), + Felt::new_unchecked(5), + Felt::new_unchecked(6), + Felt::new_unchecked(7), + Felt::new_unchecked(8), + Felt::new_unchecked(9), + Felt::new_unchecked(10), + Felt::new_unchecked(11), + Felt::new_unchecked(12), + Felt::new_unchecked(13), + Felt::new_unchecked(14), + Felt::new_unchecked(15), + Felt::new_unchecked(16), ]; let expected: [Felt; 4] = Rpo256::hash_elements(&input16).into(); let result = hasher.hash_iter(input16); @@ -515,8 +486,18 @@ mod p3_tests { #[test] fn test_rpo_compression_vs_merge() { - let digest1 = [Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]; - let digest2 = [Felt::new(5), Felt::new(6), Felt::new(7), Felt::new(8)]; + let digest1 = [ + Felt::new_unchecked(1), + Felt::new_unchecked(2), + Felt::new_unchecked(3), + Felt::new_unchecked(4), + ]; + let digest2 = [ + Felt::new_unchecked(5), + Felt::new_unchecked(6), + Felt::new_unchecked(7), + Felt::new_unchecked(8), + ]; // Rpo256::merge expects &[Word; 2] let expected: [Felt; 4] = Rpo256::merge(&[digest1.into(), digest2.into()]).into(); diff --git a/miden-crypto/src/hash/algebraic_sponge/rescue/rpx/mod.rs b/miden-crypto/src/hash/algebraic_sponge/rescue/rpx/mod.rs index 9a22d63406..0e9d3c8711 100644 --- a/miden-crypto/src/hash/algebraic_sponge/rescue/rpx/mod.rs +++ b/miden-crypto/src/hash/algebraic_sponge/rescue/rpx/mod.rs @@ -32,11 +32,10 @@ mod tests; /// and it can be serialized into 32 bytes (256 bits). /// /// ## Hash output consistency -/// Functions [hash_elements()](Rpx256::hash_elements), [merge()](Rpx256::merge), and -/// [merge_with_int()](Rpx256::merge_with_int) are internally consistent. That is, computing -/// a hash for the same set of elements using these functions will always produce the same -/// result. For example, merging two digests using [merge()](Rpx256::merge) will produce the -/// same result as hashing 8 elements which make up these digests using +/// Functions [hash_elements()](Rpx256::hash_elements), and [merge()](Rpx256::merge), are internally +/// consistent. That is, computing a hash for the same set of elements using these functions will +/// always produce the same result. For example, merging two digests using [merge()](Rpx256::merge) +/// will produce the same result as hashing 8 elements which make up these digests using /// [hash_elements()](Rpx256::hash_elements) function. /// /// However, [hash()](Rpx256::hash) function is not consistent with functions mentioned above. @@ -149,12 +148,6 @@ impl Rpx256 { ::merge_many(values) } - /// Returns a hash of a digest and a u64 value. - #[inline(always)] - pub fn merge_with_int(seed: Word, value: u64) -> Word { - ::merge_with_int(seed, value) - } - /// Returns a hash of two digests and a domain identifier. #[inline(always)] pub fn merge_in_domain(values: &[Word; 2], domain: Felt) -> Word { diff --git a/miden-crypto/src/hash/algebraic_sponge/rescue/rpx/tests.rs b/miden-crypto/src/hash/algebraic_sponge/rescue/rpx/tests.rs index 6ea066966c..936f3ca0c1 100644 --- a/miden-crypto/src/hash/algebraic_sponge/rescue/rpx/tests.rs +++ b/miden-crypto/src/hash/algebraic_sponge/rescue/rpx/tests.rs @@ -4,9 +4,7 @@ use alloc::{collections::BTreeSet, vec::Vec}; use proptest::prelude::*; use super::{Felt, Rpx256}; -use crate::{ - ONE, Word, ZERO, hash::algebraic_sponge::AlgebraicSponge, rand::test_utils::rand_value, -}; +use crate::{ONE, Word, ZERO, rand::test_utils::rand_value}; // The number of iterations to run the `ext_round_matches_reference_many` test. #[cfg(all( @@ -20,7 +18,7 @@ const EXT_ROUND_TEST_ITERS: usize = 5_000_000; #[test] fn hash_elements_vs_merge() { - let elements = [Felt::new(rand_value()); 8]; + let elements = [Felt::new_unchecked(rand_value()); 8]; let digests: [Word; 2] = [ Word::new(elements[..4].try_into().unwrap()), @@ -34,7 +32,7 @@ fn hash_elements_vs_merge() { #[test] fn merge_vs_merge_in_domain() { - let elements = [Felt::new(rand_value()); 8]; + let elements = [Felt::new_unchecked(rand_value()); 8]; let digests: [Word; 2] = [ Word::new(elements[..4].try_into().unwrap()), @@ -59,33 +57,6 @@ fn merge_vs_merge_in_domain() { assert_ne!(merge_result, merge_in_domain_result); } -#[test] -fn hash_elements_vs_merge_with_int() { - let tmp = [Felt::new(rand_value()); 4]; - let seed = Word::new(tmp); - - // ----- value fits into a field element ------------------------------------------------------ - let val: Felt = Felt::new(rand_value()); - let m_result = ::merge_with_int(seed, val.as_canonical_u64()); - - let mut elements = seed.as_elements().to_vec(); - elements.push(val); - let h_result = Rpx256::hash_elements(&elements); - - assert_eq!(m_result, h_result); - - // ----- value does not fit into a field element ---------------------------------------------- - let val = Felt::ORDER + 2; - let m_result = ::merge_with_int(seed, val); - - let mut elements = seed.as_elements().to_vec(); - elements.push(Felt::new(val)); - elements.push(ONE); - let h_result = Rpx256::hash_elements(&elements); - - assert_eq!(m_result, h_result); -} - #[test] fn hash_padding() { // adding a zero bytes at the end of a byte string should result in a different hash @@ -111,7 +82,7 @@ fn hash_padding() { #[test] fn hash_elements_padding() { - let e1 = [Felt::new(rand_value()); 2]; + let e1 = [Felt::new_unchecked(rand_value()); 2]; let e2 = [e1[0], e1[1], ZERO]; let r1 = Rpx256::hash_elements(&e1); @@ -124,12 +95,12 @@ fn hash_elements() { let elements = [ ZERO, ONE, - Felt::new(2), - Felt::new(3), - Felt::new(4), - Felt::new(5), - Felt::new(6), - Felt::new(7), + Felt::new_unchecked(2), + Felt::new_unchecked(3), + Felt::new_unchecked(4), + Felt::new_unchecked(5), + Felt::new_unchecked(6), + Felt::new_unchecked(7), ]; let digests: [Word; 2] = [ @@ -205,7 +176,7 @@ fn sponge_zeroes_collision() { #[test] fn ext_round_matches_reference_many() { for i in 0..EXT_ROUND_TEST_ITERS { - let mut state = core::array::from_fn(|_| Felt::new(rand_value())); + let mut state = core::array::from_fn(|_| Felt::new_unchecked(rand_value())); for round in 0..7 { let mut got = state; @@ -245,23 +216,23 @@ mod p3_tests { use cubic_ext::*; // Test with a simple element [1, 0, 0] - let x = [Felt::new(1), Felt::new(0), Felt::new(0)]; + let x = [Felt::new_unchecked(1), Felt::new_unchecked(0), Felt::new_unchecked(0)]; let x7 = power7(x); assert_eq!(x7, x, "1^7 should equal 1"); // Test with [0, 1, 0] (just φ) - let phi = [Felt::new(0), Felt::new(1), Felt::new(0)]; + let phi = [Felt::new_unchecked(0), Felt::new_unchecked(1), Felt::new_unchecked(0)]; let phi7 = power7(phi); // φ^7 should be some combination - verify it's computed correctly assert_ne!(phi7, phi, "φ^7 should not equal φ"); // Test with [1, 1, 1] - let x = [Felt::new(1), Felt::new(1), Felt::new(1)]; + let x = [Felt::new_unchecked(1), Felt::new_unchecked(1), Felt::new_unchecked(1)]; let x7 = power7(x); assert_ne!(x7, x, "(1+φ+φ²)^7 should not equal 1+φ+φ²"); // Verify power7 is consistent - let x = [Felt::new(42), Felt::new(17), Felt::new(99)]; + let x = [Felt::new_unchecked(42), Felt::new_unchecked(17), Felt::new_unchecked(99)]; let x7_a = power7(x); let x7_b = power7(x); assert_eq!(x7_a, x7_b, "power7 should be deterministic"); @@ -269,20 +240,20 @@ mod p3_tests { #[test] fn test_rpx_permutation_basic() { - let mut state = [Felt::new(0); STATE_WIDTH]; + let mut state = [Felt::new_unchecked(0); STATE_WIDTH]; // Apply permutation let perm = RpxPermutation256; perm.permute_mut(&mut state); // State should be different from all zeros after permutation - assert_ne!(state, [Felt::new(0); STATE_WIDTH]); + assert_ne!(state, [Felt::new_unchecked(0); STATE_WIDTH]); } #[test] fn test_rpx_permutation_consistency() { - let mut state1 = [Felt::new(0); STATE_WIDTH]; - let mut state2 = [Felt::new(0); STATE_WIDTH]; + let mut state1 = [Felt::new_unchecked(0); STATE_WIDTH]; + let mut state2 = [Felt::new_unchecked(0); STATE_WIDTH]; // Apply permutation using the trait let perm = RpxPermutation256; @@ -298,18 +269,18 @@ mod p3_tests { #[test] fn test_rpx_permutation_deterministic() { let input = [ - Felt::new(1), - Felt::new(2), - Felt::new(3), - Felt::new(4), - Felt::new(5), - Felt::new(6), - Felt::new(7), - Felt::new(8), - Felt::new(9), - Felt::new(10), - Felt::new(11), - Felt::new(12), + Felt::new_unchecked(1), + Felt::new_unchecked(2), + Felt::new_unchecked(3), + Felt::new_unchecked(4), + Felt::new_unchecked(5), + Felt::new_unchecked(6), + Felt::new_unchecked(7), + Felt::new_unchecked(8), + Felt::new_unchecked(9), + Felt::new_unchecked(10), + Felt::new_unchecked(11), + Felt::new_unchecked(12), ]; let mut state1 = input; @@ -329,14 +300,14 @@ mod p3_tests { // Test with 8 elements (exactly one rate) let input8 = [ - Felt::new(1), - Felt::new(2), - Felt::new(3), - Felt::new(4), - Felt::new(5), - Felt::new(6), - Felt::new(7), - Felt::new(8), + Felt::new_unchecked(1), + Felt::new_unchecked(2), + Felt::new_unchecked(3), + Felt::new_unchecked(4), + Felt::new_unchecked(5), + Felt::new_unchecked(6), + Felt::new_unchecked(7), + Felt::new_unchecked(8), ]; let expected: [Felt; 4] = Rpx256::hash_elements(&input8).into(); let result = hasher.hash_iter(input8); @@ -344,22 +315,22 @@ mod p3_tests { // Test with 16 elements (two rates) let input16 = [ - Felt::new(1), - Felt::new(2), - Felt::new(3), - Felt::new(4), - Felt::new(5), - Felt::new(6), - Felt::new(7), - Felt::new(8), - Felt::new(9), - Felt::new(10), - Felt::new(11), - Felt::new(12), - Felt::new(13), - Felt::new(14), - Felt::new(15), - Felt::new(16), + Felt::new_unchecked(1), + Felt::new_unchecked(2), + Felt::new_unchecked(3), + Felt::new_unchecked(4), + Felt::new_unchecked(5), + Felt::new_unchecked(6), + Felt::new_unchecked(7), + Felt::new_unchecked(8), + Felt::new_unchecked(9), + Felt::new_unchecked(10), + Felt::new_unchecked(11), + Felt::new_unchecked(12), + Felt::new_unchecked(13), + Felt::new_unchecked(14), + Felt::new_unchecked(15), + Felt::new_unchecked(16), ]; let expected: [Felt; 4] = Rpx256::hash_elements(&input16).into(); let result = hasher.hash_iter(input16); @@ -368,8 +339,18 @@ mod p3_tests { #[test] fn test_rpx_compression_vs_merge() { - let digest1 = [Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]; - let digest2 = [Felt::new(5), Felt::new(6), Felt::new(7), Felt::new(8)]; + let digest1 = [ + Felt::new_unchecked(1), + Felt::new_unchecked(2), + Felt::new_unchecked(3), + Felt::new_unchecked(4), + ]; + let digest2 = [ + Felt::new_unchecked(5), + Felt::new_unchecked(6), + Felt::new_unchecked(7), + Felt::new_unchecked(8), + ]; // Rpx256::merge expects &[Word; 2] let expected: [Felt; 4] = Rpx256::merge(&[digest1.into(), digest2.into()]).into(); diff --git a/miden-crypto/src/hash/algebraic_sponge/rescue/tests.rs b/miden-crypto/src/hash/algebraic_sponge/rescue/tests.rs index e77090050b..a04215d39f 100644 --- a/miden-crypto/src/hash/algebraic_sponge/rescue/tests.rs +++ b/miden-crypto/src/hash/algebraic_sponge/rescue/tests.rs @@ -10,7 +10,7 @@ const INV_ALPHA: u64 = 10540996611094048183; #[test] fn test_alphas() { - let e: Felt = Felt::new(rand_value()); + let e: Felt = Felt::new_unchecked(rand_value()); let e_exp = e.exp_u64(ALPHA); assert_eq!(e, e_exp.exp_u64(INV_ALPHA)); } diff --git a/miden-crypto/src/hash/blake/mod.rs b/miden-crypto/src/hash/blake/mod.rs index 4d4439e25e..9bfa177c3d 100644 --- a/miden-crypto/src/hash/blake/mod.rs +++ b/miden-crypto/src/hash/blake/mod.rs @@ -48,9 +48,6 @@ impl Blake3_256 { Digest::new(blake3::hash(bytes).into()) } - // Note: merge/merge_many/merge_with_int methods were previously trait delegations - // (::merge). They're now direct implementations as part of removing - // the Winterfell Hasher trait dependency. These are public API used in benchmarks. pub fn merge(values: &[Digest256; 2]) -> Digest256 { Self::hash(Digest::digests_as_bytes(values)) } @@ -59,13 +56,6 @@ impl Blake3_256 { Digest::new(blake3::hash(Digest::digests_as_bytes(values)).into()) } - pub fn merge_with_int(seed: Digest256, value: u64) -> Digest256 { - let mut hasher = blake3::Hasher::new(); - hasher.update(seed.as_bytes()); - hasher.update(&value.to_le_bytes()); - Digest::new(hasher.finalize().into()) - } - /// Returns a hash of the provided field elements. #[inline(always)] pub fn hash_elements>(elements: &[E]) -> Digest256 { @@ -116,13 +106,6 @@ impl Blake3_192 { Self::hash(Digest::digests_as_bytes(values)) } - pub fn merge_with_int(seed: Digest192, value: u64) -> Digest192 { - let mut hasher = blake3::Hasher::new(); - hasher.update(seed.as_bytes()); - hasher.update(&value.to_le_bytes()); - Digest::new(shrink_array(hasher.finalize().into())) - } - /// Returns a hash of the provided field elements. #[inline(always)] pub fn hash_elements>(elements: &[E]) -> Digest192 { diff --git a/miden-crypto/src/hash/blake/tests.rs b/miden-crypto/src/hash/blake/tests.rs index 460538a318..f5c99782f6 100644 --- a/miden-crypto/src/hash/blake/tests.rs +++ b/miden-crypto/src/hash/blake/tests.rs @@ -56,7 +56,7 @@ proptest! { let expected = Blake3_256::hash(&concatenated); // Test with the original iterator of slices (converting Vec to &[u8]) - let actual = Blake3_256::hash_iter(slices.iter().map(|v| v.as_slice())); + let actual = Blake3_256::hash_iter(slices.iter().map(Vec::as_slice)); assert_eq!(expected, actual); // Test with empty slices list (should produce hash of empty string) @@ -84,7 +84,7 @@ proptest! { let expected = Blake3_192::hash(&concatenated); // Test with the original iterator of slices (converting Vec to &[u8]) - let actual = Blake3_192::hash_iter(slices.iter().map(|v| v.as_slice())); + let actual = Blake3_192::hash_iter(slices.iter().map(Vec::as_slice)); assert_eq!(expected, actual); // Test with empty slices list (should produce hash of empty string) diff --git a/miden-crypto/src/hash/keccak/mod.rs b/miden-crypto/src/hash/keccak/mod.rs index 9e08e264d2..6f7cd12a04 100644 --- a/miden-crypto/src/hash/keccak/mod.rs +++ b/miden-crypto/src/hash/keccak/mod.rs @@ -65,13 +65,6 @@ impl Keccak256 { Keccak256Digest::from(<[u8; DIGEST256_BYTES]>::from(hasher.finalize())) } - pub fn merge_with_int(seed: Keccak256Digest, value: u64) -> Keccak256Digest { - let mut hasher = sha3::Keccak256::new(); - hasher.update(&*seed); - hasher.update(value.to_le_bytes()); - Keccak256Digest::from(<[u8; DIGEST256_BYTES]>::from(hasher.finalize())) - } - /// Returns a hash of the provided field elements. #[inline(always)] pub fn hash_elements(elements: &[E]) -> Keccak256Digest diff --git a/miden-crypto/src/hash/keccak/tests.rs b/miden-crypto/src/hash/keccak/tests.rs index 2b137bac86..8abfdd9ee1 100644 --- a/miden-crypto/src/hash/keccak/tests.rs +++ b/miden-crypto/src/hash/keccak/tests.rs @@ -39,7 +39,7 @@ proptest! { let expected = Keccak256::hash(&concatenated); // Test with the original iterator of slices - let actual = Keccak256::hash_iter(slices.iter().map(|v| v.as_slice())); + let actual = Keccak256::hash_iter(slices.iter().map(Vec::as_slice)); assert_eq!(expected, actual); // Test with empty slices list (should produce hash of empty string) diff --git a/miden-crypto/src/hash/sha2/mod.rs b/miden-crypto/src/hash/sha2/mod.rs index c196b2b792..1705fad8ee 100644 --- a/miden-crypto/src/hash/sha2/mod.rs +++ b/miden-crypto/src/hash/sha2/mod.rs @@ -61,13 +61,6 @@ impl Sha256 { Sha256Digest::from(<[u8; DIGEST256_BYTES]>::from(hasher.finalize())) } - pub fn merge_with_int(seed: Sha256Digest, value: u64) -> Sha256Digest { - let mut hasher = sha2::Sha256::new(); - hasher.update(&*seed); - hasher.update(value.to_le_bytes()); - Sha256Digest::from(<[u8; DIGEST256_BYTES]>::from(hasher.finalize())) - } - /// Returns a hash of the provided field elements. #[inline(always)] pub fn hash_elements>(elements: &[E]) -> Sha256Digest { diff --git a/miden-crypto/src/hash/sha2/tests.rs b/miden-crypto/src/hash/sha2/tests.rs index 547ce1fb90..5a78195b95 100644 --- a/miden-crypto/src/hash/sha2/tests.rs +++ b/miden-crypto/src/hash/sha2/tests.rs @@ -40,7 +40,7 @@ proptest! { let expected = Sha256::hash(&concatenated); // Test with iterator - let actual = Sha256::hash_iter(slices.iter().map(|v| v.as_slice())); + let actual = Sha256::hash_iter(slices.iter().map(Vec::as_slice)); assert_eq!(expected, actual); // Test with empty slices list @@ -106,7 +106,7 @@ proptest! { let expected = Sha512::hash(&concatenated); // Test with iterator - let actual = Sha512::hash_iter(slices.iter().map(|v| v.as_slice())); + let actual = Sha512::hash_iter(slices.iter().map(Vec::as_slice)); assert_eq!(expected, actual); // Test with empty slices list @@ -218,14 +218,14 @@ const SHA512_TEST_VECTORS: &[TestVector] = &[ #[test] fn test_memory_layout_assumptions() { // Verify struct size equals inner array size (required for safe pointer casting) - assert_eq!(core::mem::size_of::(), core::mem::size_of::<[u8; 32]>()); + assert_eq!(size_of::(), size_of::<[u8; 32]>()); // Verify alignment - assert_eq!(core::mem::align_of::(), core::mem::align_of::<[u8; 32]>()); + assert_eq!(align_of::(), align_of::<[u8; 32]>()); // Same for Sha512Digest - assert_eq!(core::mem::size_of::(), core::mem::size_of::<[u8; 64]>()); - assert_eq!(core::mem::align_of::(), core::mem::align_of::<[u8; 64]>()); + assert_eq!(size_of::(), size_of::<[u8; 64]>()); + assert_eq!(align_of::(), align_of::<[u8; 64]>()); } #[test] diff --git a/miden-crypto/src/ies/keys.rs b/miden-crypto/src/ies/keys.rs index 40c0ddd417..21b4afff36 100644 --- a/miden-crypto/src/ies/keys.rs +++ b/miden-crypto/src/ies/keys.rs @@ -306,10 +306,10 @@ impl Deserializable for SealingKey { /// Secret key for unsealing messages. pub enum UnsealingKey { - K256XChaCha20Poly1305(crate::dsa::ecdsa_k256_keccak::SecretKey), - X25519XChaCha20Poly1305(crate::dsa::eddsa_25519_sha512::SecretKey), - K256AeadPoseidon2(crate::dsa::ecdsa_k256_keccak::SecretKey), - X25519AeadPoseidon2(crate::dsa::eddsa_25519_sha512::SecretKey), + K256XChaCha20Poly1305(crate::dsa::ecdsa_k256_keccak::KeyExchangeKey), + X25519XChaCha20Poly1305(crate::dsa::eddsa_25519_sha512::KeyExchangeKey), + K256AeadPoseidon2(crate::dsa::ecdsa_k256_keccak::KeyExchangeKey), + X25519AeadPoseidon2(crate::dsa::eddsa_25519_sha512::KeyExchangeKey), } impl UnsealingKey { @@ -385,19 +385,19 @@ impl Deserializable for UnsealingKey { match scheme { IesScheme::K256XChaCha20Poly1305 => { - let key = crate::dsa::ecdsa_k256_keccak::SecretKey::read_from(source)?; + let key = crate::dsa::ecdsa_k256_keccak::KeyExchangeKey::read_from(source)?; Ok(UnsealingKey::K256XChaCha20Poly1305(key)) }, IesScheme::X25519XChaCha20Poly1305 => { - let key = crate::dsa::eddsa_25519_sha512::SecretKey::read_from(source)?; + let key = crate::dsa::eddsa_25519_sha512::KeyExchangeKey::read_from(source)?; Ok(UnsealingKey::X25519XChaCha20Poly1305(key)) }, IesScheme::K256AeadPoseidon2 => { - let key = crate::dsa::ecdsa_k256_keccak::SecretKey::read_from(source)?; + let key = crate::dsa::ecdsa_k256_keccak::KeyExchangeKey::read_from(source)?; Ok(UnsealingKey::K256AeadPoseidon2(key)) }, IesScheme::X25519AeadPoseidon2 => { - let key = crate::dsa::eddsa_25519_sha512::SecretKey::read_from(source)?; + let key = crate::dsa::eddsa_25519_sha512::KeyExchangeKey::read_from(source)?; Ok(UnsealingKey::X25519AeadPoseidon2(key)) }, } diff --git a/miden-crypto/src/ies/mod.rs b/miden-crypto/src/ies/mod.rs index 936dbb44fc..7c270b45b6 100644 --- a/miden-crypto/src/ies/mod.rs +++ b/miden-crypto/src/ies/mod.rs @@ -9,13 +9,13 @@ //! //! ``` //! use miden_crypto::{ -//! dsa::eddsa_25519_sha512::SecretKey, +//! dsa::eddsa_25519_sha512::KeyExchangeKey, //! ies::{SealingKey, UnsealingKey}, //! }; //! use rand::rng; //! //! let mut rng = rng(); -//! let secret_key = SecretKey::with_rng(&mut rng); +//! let secret_key = KeyExchangeKey::with_rng(&mut rng); //! let public_key = secret_key.public_key(); //! //! let sealing_key = SealingKey::X25519XChaCha20Poly1305(public_key); diff --git a/miden-crypto/src/ies/tests.rs b/miden-crypto/src/ies/tests.rs index 8b7e4cd0e1..b3a89ac895 100644 --- a/miden-crypto/src/ies/tests.rs +++ b/miden-crypto/src/ies/tests.rs @@ -8,9 +8,9 @@ use rand_chacha::ChaCha20Rng; use crate::{ dsa::{ - ecdsa_k256_keccak::{PUBLIC_KEY_BYTES as K256_PUBLIC_KEY_BYTES, SecretKey}, + ecdsa_k256_keccak::PUBLIC_KEY_BYTES as K256_PUBLIC_KEY_BYTES, eddsa_25519_sha512::{ - PUBLIC_KEY_BYTES as X25519_PUBLIC_KEY_BYTES, SecretKey as SecretKey25519, + KeyExchangeKey as KeyExchangeKey25519, PUBLIC_KEY_BYTES as X25519_PUBLIC_KEY_BYTES, }, }, ies::{keys::EphemeralPublicKey, *}, @@ -29,7 +29,7 @@ fn arbitrary_bytes() -> impl Strategy> { fn arbitrary_field_elements() -> impl Strategy> { (1usize..100, any::()).prop_map(|(len, seed)| { let mut rng = ChaCha20Rng::seed_from_u64(seed); - (0..len).map(|_| crate::Felt::new(rng.next_u64())).collect() + (0..len).map(|_| crate::Felt::new_unchecked(rng.next_u64())).collect() }) } @@ -99,12 +99,13 @@ macro_rules! test_basic_roundtrip { /// K256 + XChaCha20-Poly1305 test suite mod k256_xchacha_tests { use super::*; + use crate::dsa::ecdsa_k256_keccak::KeyExchangeKey; #[test] fn test_k256_xchacha_bytes_roundtrip() { let mut rng = rand::rng(); let plaintext = b"test bytes encryption"; - let secret_key = SecretKey::with_rng(&mut rng); + let secret_key = KeyExchangeKey::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::K256XChaCha20Poly1305(public_key); let unsealing_key = UnsealingKey::K256XChaCha20Poly1305(secret_key); @@ -116,7 +117,7 @@ mod k256_xchacha_tests { let mut rng = rand::rng(); let plaintext = b"test bytes with associated data"; let associated_data = b"authentication context"; - let secret_key = SecretKey::with_rng(&mut rng); + let secret_key = KeyExchangeKey::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::K256XChaCha20Poly1305(public_key); let unsealing_key = UnsealingKey::K256XChaCha20Poly1305(secret_key); @@ -133,8 +134,12 @@ mod k256_xchacha_tests { #[test] fn test_k256_xchacha_elements_roundtrip() { let mut rng = rand::rng(); - let plaintext = vec![crate::Felt::new(42), crate::Felt::new(1337), crate::Felt::new(9999)]; - let secret_key = SecretKey::with_rng(&mut rng); + let plaintext = vec![ + crate::Felt::new_unchecked(42), + crate::Felt::new_unchecked(1337), + crate::Felt::new_unchecked(9999), + ]; + let secret_key = KeyExchangeKey::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::K256XChaCha20Poly1305(public_key); let unsealing_key = UnsealingKey::K256XChaCha20Poly1305(secret_key); @@ -150,9 +155,14 @@ mod k256_xchacha_tests { #[test] fn test_k256_xchacha_elements_with_associated_data() { let mut rng = rand::rng(); - let plaintext = vec![crate::Felt::new(100), crate::Felt::new(200), crate::Felt::new(300)]; - let associated_data = vec![crate::Felt::new(999), crate::Felt::new(888)]; - let secret_key = SecretKey::with_rng(&mut rng); + let plaintext = vec![ + crate::Felt::new_unchecked(100), + crate::Felt::new_unchecked(200), + crate::Felt::new_unchecked(300), + ]; + let associated_data = + vec![crate::Felt::new_unchecked(999), crate::Felt::new_unchecked(888)]; + let secret_key = KeyExchangeKey::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::K256XChaCha20Poly1305(public_key); let unsealing_key = UnsealingKey::K256XChaCha20Poly1305(secret_key); @@ -172,7 +182,7 @@ mod k256_xchacha_tests { let plaintext = b"test invalid associated data"; let correct_ad = b"correct context"; let incorrect_ad = b"wrong context"; - let secret_key = SecretKey::with_rng(&mut rng); + let secret_key = KeyExchangeKey::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::K256XChaCha20Poly1305(public_key); let sealed = sealing_key @@ -190,7 +200,7 @@ mod k256_xchacha_tests { associated_data in arbitrary_bytes() ) { let mut rng = rand::rng(); - let secret_key = SecretKey::with_rng(&mut rng); + let secret_key = KeyExchangeKey::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::K256XChaCha20Poly1305(public_key); let unsealing_key = UnsealingKey::K256XChaCha20Poly1305(secret_key); @@ -203,7 +213,7 @@ mod k256_xchacha_tests { associated_data in arbitrary_field_elements() ) { let mut rng = rand::rng(); - let secret_key = SecretKey::with_rng(&mut rng); + let secret_key = KeyExchangeKey::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::K256XChaCha20Poly1305(public_key); let unsealing_key = UnsealingKey::K256XChaCha20Poly1305(secret_key); @@ -216,9 +226,9 @@ mod k256_xchacha_tests { ) { prop_assume!(!plaintext.is_empty()); let mut rng = rand::rng(); - let secret1 = SecretKey::with_rng(&mut rng); + let secret1 = KeyExchangeKey::with_rng(&mut rng); let public1 = secret1.public_key(); - let secret2 = SecretKey::with_rng(&mut rng); + let secret2 = KeyExchangeKey::with_rng(&mut rng); let sealing_key = SealingKey::K256XChaCha20Poly1305(public1); let sealed = sealing_key.seal_bytes(&mut rng, &plaintext).unwrap(); let unsealing_key = UnsealingKey::K256XChaCha20Poly1305(secret2); @@ -238,7 +248,7 @@ mod x25519_xchacha_tests { fn test_x25519_xchacha_bytes_roundtrip() { let mut rng = rand::rng(); let plaintext = b"test bytes encryption"; - let secret_key = SecretKey25519::with_rng(&mut rng); + let secret_key = KeyExchangeKey25519::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::X25519XChaCha20Poly1305(public_key); let unsealing_key = UnsealingKey::X25519XChaCha20Poly1305(secret_key); @@ -250,7 +260,7 @@ mod x25519_xchacha_tests { let mut rng = rand::rng(); let plaintext = b"test bytes with associated data"; let associated_data = b"authentication context"; - let secret_key = SecretKey25519::with_rng(&mut rng); + let secret_key = KeyExchangeKey25519::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::X25519XChaCha20Poly1305(public_key); let unsealing_key = UnsealingKey::X25519XChaCha20Poly1305(secret_key); @@ -267,8 +277,12 @@ mod x25519_xchacha_tests { #[test] fn test_x25519_xchacha_elements_roundtrip() { let mut rng = rand::rng(); - let plaintext = vec![crate::Felt::new(42), crate::Felt::new(1337), crate::Felt::new(9999)]; - let secret_key = SecretKey25519::with_rng(&mut rng); + let plaintext = vec![ + crate::Felt::new_unchecked(42), + crate::Felt::new_unchecked(1337), + crate::Felt::new_unchecked(9999), + ]; + let secret_key = KeyExchangeKey25519::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::X25519XChaCha20Poly1305(public_key); let unsealing_key = UnsealingKey::X25519XChaCha20Poly1305(secret_key); @@ -284,9 +298,14 @@ mod x25519_xchacha_tests { #[test] fn test_x25519_xchacha_elements_with_associated_data() { let mut rng = rand::rng(); - let plaintext = vec![crate::Felt::new(100), crate::Felt::new(200), crate::Felt::new(300)]; - let associated_data = vec![crate::Felt::new(999), crate::Felt::new(888)]; - let secret_key = SecretKey25519::with_rng(&mut rng); + let plaintext = vec![ + crate::Felt::new_unchecked(100), + crate::Felt::new_unchecked(200), + crate::Felt::new_unchecked(300), + ]; + let associated_data = + vec![crate::Felt::new_unchecked(999), crate::Felt::new_unchecked(888)]; + let secret_key = KeyExchangeKey25519::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::X25519XChaCha20Poly1305(public_key); let unsealing_key = UnsealingKey::X25519XChaCha20Poly1305(secret_key); @@ -306,7 +325,7 @@ mod x25519_xchacha_tests { let plaintext = b"test invalid associated data"; let correct_ad = b"correct context"; let incorrect_ad = b"wrong context"; - let secret_key = SecretKey25519::with_rng(&mut rng); + let secret_key = KeyExchangeKey25519::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::X25519XChaCha20Poly1305(public_key); let sealed = sealing_key @@ -324,7 +343,7 @@ mod x25519_xchacha_tests { let mut rng = rand::rng(); let plaintext = b"malleability check"; - let secret_key = SecretKey25519::with_rng(&mut rng); + let secret_key = KeyExchangeKey25519::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::X25519XChaCha20Poly1305(public_key); let unsealing_key = UnsealingKey::X25519XChaCha20Poly1305(secret_key); @@ -359,7 +378,7 @@ mod x25519_xchacha_tests { associated_data in arbitrary_bytes() ) { let mut rng = rand::rng(); - let secret_key = SecretKey25519::with_rng(&mut rng); + let secret_key = KeyExchangeKey25519::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::X25519XChaCha20Poly1305(public_key); let unsealing_key = UnsealingKey::X25519XChaCha20Poly1305(secret_key); @@ -372,7 +391,7 @@ mod x25519_xchacha_tests { associated_data in arbitrary_field_elements() ) { let mut rng = rand::rng(); - let secret_key = SecretKey25519::with_rng(&mut rng); + let secret_key = KeyExchangeKey25519::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::X25519XChaCha20Poly1305(public_key); let unsealing_key = UnsealingKey::X25519XChaCha20Poly1305(secret_key); @@ -385,9 +404,9 @@ mod x25519_xchacha_tests { ) { prop_assume!(!plaintext.is_empty()); let mut rng = rand::rng(); - let secret1 = SecretKey25519::with_rng(&mut rng); + let secret1 = KeyExchangeKey25519::with_rng(&mut rng); let public1 = secret1.public_key(); - let secret2 = SecretKey25519::with_rng(&mut rng); + let secret2 = KeyExchangeKey25519::with_rng(&mut rng); let sealing_key = SealingKey::X25519XChaCha20Poly1305(public1); let sealed = sealing_key.seal_bytes(&mut rng, &plaintext).unwrap(); let unsealing_key = UnsealingKey::X25519XChaCha20Poly1305(secret2); @@ -400,13 +419,14 @@ mod x25519_xchacha_tests { /// K256 + AeadPoseidon2 test suite mod k256_aead_poseidon2_tests { use super::*; + use crate::dsa::ecdsa_k256_keccak::KeyExchangeKey; // BYTES TESTS #[test] fn test_k256_aead_poseidon2_bytes_roundtrip() { let mut rng = rand::rng(); let plaintext = b"test bytes encryption"; - let secret_key = SecretKey::with_rng(&mut rng); + let secret_key = KeyExchangeKey::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::K256AeadPoseidon2(public_key); let unsealing_key = UnsealingKey::K256AeadPoseidon2(secret_key); @@ -418,7 +438,7 @@ mod k256_aead_poseidon2_tests { let mut rng = rand::rng(); let plaintext = b"test bytes with associated data"; let associated_data = b"authentication context"; - let secret_key = SecretKey::with_rng(&mut rng); + let secret_key = KeyExchangeKey::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::K256AeadPoseidon2(public_key); let unsealing_key = UnsealingKey::K256AeadPoseidon2(secret_key); @@ -438,7 +458,7 @@ mod k256_aead_poseidon2_tests { let plaintext = b"test invalid associated data"; let correct_ad = b"correct context"; let incorrect_ad = b"wrong context"; - let secret_key = SecretKey::with_rng(&mut rng); + let secret_key = KeyExchangeKey::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::K256AeadPoseidon2(public_key); let sealed = sealing_key @@ -454,8 +474,9 @@ mod k256_aead_poseidon2_tests { fn test_k256_aead_poseidon2_field_elements_roundtrip() { use crate::Felt; let mut rng = rand::rng(); - let plaintext = vec![Felt::new(1), Felt::new(2), Felt::new(3)]; - let secret_key = SecretKey::with_rng(&mut rng); + let plaintext = + vec![Felt::new_unchecked(1), Felt::new_unchecked(2), Felt::new_unchecked(3)]; + let secret_key = KeyExchangeKey::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::K256AeadPoseidon2(public_key); let unsealing_key = UnsealingKey::K256AeadPoseidon2(secret_key); @@ -472,9 +493,9 @@ mod k256_aead_poseidon2_tests { fn test_k256_aead_poseidon2_field_elements_with_associated_data() { use crate::Felt; let mut rng = rand::rng(); - let plaintext = vec![Felt::new(10), Felt::new(20)]; - let associated_data = vec![Felt::new(100), Felt::new(200)]; - let secret_key = SecretKey::with_rng(&mut rng); + let plaintext = vec![Felt::new_unchecked(10), Felt::new_unchecked(20)]; + let associated_data = vec![Felt::new_unchecked(100), Felt::new_unchecked(200)]; + let secret_key = KeyExchangeKey::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::K256AeadPoseidon2(public_key); let unsealing_key = UnsealingKey::K256AeadPoseidon2(secret_key); @@ -495,7 +516,7 @@ mod k256_aead_poseidon2_tests { associated_data in arbitrary_bytes() ) { let mut rng = rand::rng(); - let secret_key = SecretKey::with_rng(&mut rng); + let secret_key = KeyExchangeKey::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::K256AeadPoseidon2(public_key); let unsealing_key = UnsealingKey::K256AeadPoseidon2(secret_key); @@ -508,7 +529,7 @@ mod k256_aead_poseidon2_tests { associated_data in arbitrary_field_elements() ) { let mut rng = rand::rng(); - let secret_key = SecretKey::with_rng(&mut rng); + let secret_key = KeyExchangeKey::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::K256AeadPoseidon2(public_key); let unsealing_key = UnsealingKey::K256AeadPoseidon2(secret_key); @@ -521,9 +542,9 @@ mod k256_aead_poseidon2_tests { ) { prop_assume!(!plaintext.is_empty()); let mut rng = rand::rng(); - let secret1 = SecretKey::with_rng(&mut rng); + let secret1 = KeyExchangeKey::with_rng(&mut rng); let public1 = secret1.public_key(); - let secret2 = SecretKey::with_rng(&mut rng); + let secret2 = KeyExchangeKey::with_rng(&mut rng); let sealing_key = SealingKey::K256AeadPoseidon2(public1); let sealed = sealing_key.seal_bytes(&mut rng, &plaintext).unwrap(); let unsealing_key = UnsealingKey::K256AeadPoseidon2(secret2); @@ -542,7 +563,7 @@ mod x25519_aead_poseidon2_tests { fn test_x25519_aead_poseidon2_bytes_roundtrip() { let mut rng = rand::rng(); let plaintext = b"test bytes encryption"; - let secret_key = SecretKey25519::with_rng(&mut rng); + let secret_key = KeyExchangeKey25519::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::X25519AeadPoseidon2(public_key); let unsealing_key = UnsealingKey::X25519AeadPoseidon2(secret_key); @@ -554,7 +575,7 @@ mod x25519_aead_poseidon2_tests { let mut rng = rand::rng(); let plaintext = b"test bytes with associated data"; let associated_data = b"authentication context"; - let secret_key = SecretKey25519::with_rng(&mut rng); + let secret_key = KeyExchangeKey25519::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::X25519AeadPoseidon2(public_key); let unsealing_key = UnsealingKey::X25519AeadPoseidon2(secret_key); @@ -574,7 +595,7 @@ mod x25519_aead_poseidon2_tests { let plaintext = b"test invalid associated data"; let correct_ad = b"correct context"; let incorrect_ad = b"wrong context"; - let secret_key = SecretKey25519::with_rng(&mut rng); + let secret_key = KeyExchangeKey25519::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::X25519AeadPoseidon2(public_key); let sealed = sealing_key @@ -590,8 +611,9 @@ mod x25519_aead_poseidon2_tests { fn test_x25519_aead_poseidon2_field_elements_roundtrip() { use crate::Felt; let mut rng = rand::rng(); - let plaintext = vec![Felt::new(1), Felt::new(2), Felt::new(3)]; - let secret_key = SecretKey25519::with_rng(&mut rng); + let plaintext = + vec![Felt::new_unchecked(1), Felt::new_unchecked(2), Felt::new_unchecked(3)]; + let secret_key = KeyExchangeKey25519::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::X25519AeadPoseidon2(public_key); let unsealing_key = UnsealingKey::X25519AeadPoseidon2(secret_key); @@ -608,9 +630,9 @@ mod x25519_aead_poseidon2_tests { fn test_x25519_aead_poseidon2_field_elements_with_associated_data() { use crate::Felt; let mut rng = rand::rng(); - let plaintext = vec![Felt::new(10), Felt::new(20)]; - let associated_data = vec![Felt::new(100), Felt::new(200)]; - let secret_key = SecretKey25519::with_rng(&mut rng); + let plaintext = vec![Felt::new_unchecked(10), Felt::new_unchecked(20)]; + let associated_data = vec![Felt::new_unchecked(100), Felt::new_unchecked(200)]; + let secret_key = KeyExchangeKey25519::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::X25519AeadPoseidon2(public_key); let unsealing_key = UnsealingKey::X25519AeadPoseidon2(secret_key); @@ -631,7 +653,7 @@ mod x25519_aead_poseidon2_tests { associated_data in arbitrary_bytes() ) { let mut rng = rand::rng(); - let secret_key = SecretKey25519::with_rng(&mut rng); + let secret_key = KeyExchangeKey25519::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::X25519AeadPoseidon2(public_key); let unsealing_key = UnsealingKey::X25519AeadPoseidon2(secret_key); @@ -644,7 +666,7 @@ mod x25519_aead_poseidon2_tests { associated_data in arbitrary_field_elements() ) { let mut rng = rand::rng(); - let secret_key = SecretKey25519::with_rng(&mut rng); + let secret_key = KeyExchangeKey25519::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::X25519AeadPoseidon2(public_key); let unsealing_key = UnsealingKey::X25519AeadPoseidon2(secret_key); @@ -657,9 +679,9 @@ mod x25519_aead_poseidon2_tests { ) { prop_assume!(!plaintext.is_empty()); let mut rng = rand::rng(); - let secret1 = SecretKey25519::with_rng(&mut rng); + let secret1 = KeyExchangeKey25519::with_rng(&mut rng); let public1 = secret1.public_key(); - let secret2 = SecretKey25519::with_rng(&mut rng); + let secret2 = KeyExchangeKey25519::with_rng(&mut rng); let sealing_key = SealingKey::X25519AeadPoseidon2(public1); let sealed = sealing_key.seal_bytes(&mut rng, &plaintext).unwrap(); let unsealing_key = UnsealingKey::X25519AeadPoseidon2(secret2); @@ -694,6 +716,7 @@ mod ephemeral_public_key_tests { /// Tests scheme mismatch detection between different IES variants mod scheme_compatibility_tests { use super::*; + use crate::dsa::ecdsa_k256_keccak::KeyExchangeKey; #[test] fn test_scheme_mismatch_k256_xchacha_vs_aead_poseidon2() { @@ -701,13 +724,13 @@ mod scheme_compatibility_tests { let plaintext = b"test scheme mismatch"; // Seal with K256XChaCha20Poly1305 - let secret_key = SecretKey::with_rng(&mut rng); + let secret_key = KeyExchangeKey::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::K256XChaCha20Poly1305(public_key); let sealed = sealing_key.seal_bytes(&mut rng, plaintext).unwrap(); // Try to unseal with K256AeadPoseidon2 (should fail) - let secret_key2 = SecretKey::with_rng(&mut rng); + let secret_key2 = KeyExchangeKey::with_rng(&mut rng); let unsealing_key = UnsealingKey::K256AeadPoseidon2(secret_key2); let result = unsealing_key.unseal_bytes(sealed); assert!(result.is_err()); @@ -719,13 +742,13 @@ mod scheme_compatibility_tests { let plaintext = b"test scheme mismatch"; // Seal with X25519XChaCha20Poly1305 - let secret_key = SecretKey25519::with_rng(&mut rng); + let secret_key = KeyExchangeKey25519::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::X25519XChaCha20Poly1305(public_key); let sealed = sealing_key.seal_bytes(&mut rng, plaintext).unwrap(); // Try to unseal with X25519AeadPoseidon2 (should fail) - let secret_key2 = SecretKey25519::with_rng(&mut rng); + let secret_key2 = KeyExchangeKey25519::with_rng(&mut rng); let unsealing_key = UnsealingKey::X25519AeadPoseidon2(secret_key2); let result = unsealing_key.unseal_bytes(sealed); assert!(result.is_err()); @@ -737,13 +760,13 @@ mod scheme_compatibility_tests { let plaintext = b"test cross-curve mismatch"; // Seal with K256XChaCha20Poly1305 - let secret_k256 = SecretKey::with_rng(&mut rng); + let secret_k256 = KeyExchangeKey::with_rng(&mut rng); let public_k256 = secret_k256.public_key(); let sealing_key = SealingKey::K256XChaCha20Poly1305(public_k256); let sealed = sealing_key.seal_bytes(&mut rng, plaintext).unwrap(); // Try to unseal with X25519XChaCha20Poly1305 (should fail) - let secret_x25519 = SecretKey25519::with_rng(&mut rng); + let secret_x25519 = KeyExchangeKey25519::with_rng(&mut rng); let unsealing_key = UnsealingKey::X25519XChaCha20Poly1305(secret_x25519); let result = unsealing_key.unseal_bytes(sealed); assert!(result.is_err()); @@ -756,9 +779,9 @@ mod scheme_compatibility_tests { ) { let mut rng = rand::rng(); // Create keys for different schemes - let secret_k256 = SecretKey::with_rng(&mut rng); + let secret_k256 = KeyExchangeKey::with_rng(&mut rng); let public_k256 = secret_k256.public_key(); - let secret_x25519 = SecretKey25519::with_rng(&mut rng); + let secret_x25519 = KeyExchangeKey25519::with_rng(&mut rng); // Seal with K256XChaCha20Poly1305 let sealing_key = SealingKey::K256XChaCha20Poly1305(public_k256); @@ -779,11 +802,12 @@ mod scheme_compatibility_tests { /// Tests for IES protocol-level functionality mod protocol_tests { use super::*; + use crate::dsa::ecdsa_k256_keccak::KeyExchangeKey; #[test] fn test_ephemeral_key_serialization_k256() { let mut rng = rand::rng(); - let secret_key = SecretKey::with_rng(&mut rng); + let secret_key = KeyExchangeKey::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::K256XChaCha20Poly1305(public_key); let sealed = sealing_key.seal_bytes(&mut rng, b"test").unwrap(); @@ -800,7 +824,7 @@ mod protocol_tests { #[test] fn test_ephemeral_key_serialization_x25519() { let mut rng = rand::rng(); - let secret_key = SecretKey25519::with_rng(&mut rng); + let secret_key = KeyExchangeKey25519::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::X25519XChaCha20Poly1305(public_key); let sealed = sealing_key.seal_bytes(&mut rng, b"test").unwrap(); @@ -820,7 +844,7 @@ mod protocol_tests { plaintext in arbitrary_bytes() ) { let mut rng = rand::rng(); - let secret_key = SecretKey::with_rng(&mut rng); + let secret_key = KeyExchangeKey::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::K256XChaCha20Poly1305(public_key); let sealed = sealing_key.seal_bytes(&mut rng, &plaintext).unwrap(); @@ -841,7 +865,7 @@ mod protocol_tests { #[test] fn test_sealed_message_serialization_roundtrip_k256_xchacha() { let mut rng = rand::rng(); - let sk = crate::dsa::ecdsa_k256_keccak::SecretKey::with_rng(&mut rng); + let sk = KeyExchangeKey::with_rng(&mut rng); let pk = sk.public_key(); let sealing_key = SealingKey::K256XChaCha20Poly1305(pk); let unsealing_key = UnsealingKey::K256XChaCha20Poly1305(sk); @@ -850,8 +874,7 @@ mod protocol_tests { let sealed = sealing_key.seal_bytes(&mut rng, plaintext).unwrap(); let before = sealed.scheme_name(); let bytes = sealed.to_bytes(); - let sealed2 = - ::read_from_bytes(&bytes).unwrap(); + let sealed2 = ::read_from_bytes(&bytes).unwrap(); let after = sealed2.scheme_name(); assert_eq!(before, after); let opened = unsealing_key.unseal_bytes(sealed2).unwrap(); @@ -861,7 +884,7 @@ mod protocol_tests { #[test] fn test_sealed_message_serialization_roundtrip_x25519_xchacha() { let mut rng = rand::rng(); - let sk = crate::dsa::eddsa_25519_sha512::SecretKey::with_rng(&mut rng); + let sk = crate::dsa::eddsa_25519_sha512::KeyExchangeKey::with_rng(&mut rng); let pk = sk.public_key(); let sealing_key = SealingKey::X25519XChaCha20Poly1305(pk); let unsealing_key = UnsealingKey::X25519XChaCha20Poly1305(sk); @@ -870,8 +893,7 @@ mod protocol_tests { let sealed = sealing_key.seal_bytes(&mut rng, plaintext).unwrap(); let before = sealed.scheme_name(); let bytes = sealed.to_bytes(); - let sealed2 = - ::read_from_bytes(&bytes).unwrap(); + let sealed2 = ::read_from_bytes(&bytes).unwrap(); let after = sealed2.scheme_name(); assert_eq!(before, after); let opened = unsealing_key.unseal_bytes(sealed2).unwrap(); @@ -881,7 +903,7 @@ mod protocol_tests { #[test] fn test_sealed_message_serialization_roundtrip_k256_aeadrpo() { let mut rng = rand::rng(); - let sk = crate::dsa::ecdsa_k256_keccak::SecretKey::with_rng(&mut rng); + let sk = KeyExchangeKey::with_rng(&mut rng); let pk = sk.public_key(); let sealing_key = SealingKey::K256AeadPoseidon2(pk); let unsealing_key = UnsealingKey::K256AeadPoseidon2(sk); @@ -890,8 +912,7 @@ mod protocol_tests { let sealed = sealing_key.seal_bytes(&mut rng, plaintext).unwrap(); let before = sealed.scheme_name(); let bytes = sealed.to_bytes(); - let sealed2 = - ::read_from_bytes(&bytes).unwrap(); + let sealed2 = ::read_from_bytes(&bytes).unwrap(); let after = sealed2.scheme_name(); assert_eq!(before, after); let opened = unsealing_key.unseal_bytes(sealed2).unwrap(); @@ -901,7 +922,7 @@ mod protocol_tests { #[test] fn test_sealed_message_serialization_roundtrip_x25519_aeadrpo() { let mut rng = rand::rng(); - let sk = crate::dsa::eddsa_25519_sha512::SecretKey::with_rng(&mut rng); + let sk = crate::dsa::eddsa_25519_sha512::KeyExchangeKey::with_rng(&mut rng); let pk = sk.public_key(); let sealing_key = SealingKey::X25519AeadPoseidon2(pk); let unsealing_key = UnsealingKey::X25519AeadPoseidon2(sk); @@ -910,8 +931,7 @@ mod protocol_tests { let sealed = sealing_key.seal_bytes(&mut rng, plaintext).unwrap(); let before = sealed.scheme_name(); let bytes = sealed.to_bytes(); - let sealed2 = - ::read_from_bytes(&bytes).unwrap(); + let sealed2 = ::read_from_bytes(&bytes).unwrap(); let after = sealed2.scheme_name(); assert_eq!(before, after); let opened = unsealing_key.unseal_bytes(sealed2).unwrap(); @@ -926,6 +946,7 @@ mod protocol_tests { /// Integration and regression tests mod integration_tests { use super::*; + use crate::dsa::ecdsa_k256_keccak::KeyExchangeKey; proptest! { #[test] @@ -934,13 +955,13 @@ mod integration_tests { ) { use crate::Felt; let mut rng = rand::rng(); - let secret_key = SecretKey25519::with_rng(&mut rng); + let secret_key = KeyExchangeKey25519::with_rng(&mut rng); let public_key = secret_key.public_key(); let sealing_key = SealingKey::X25519AeadPoseidon2(public_key); let unsealing_key = UnsealingKey::X25519AeadPoseidon2(secret_key); // Test field elements encryption - let field_elements: Vec = field_values.iter().map(|&v| Felt::new(v)).collect(); + let field_elements: Vec = field_values.iter().map(|&v| Felt::new_unchecked(v)).collect(); let sealed_elements = sealing_key.seal_elements(&mut rng, &field_elements).unwrap(); let decrypted_elements = unsealing_key.unseal_elements(sealed_elements).unwrap(); prop_assert_eq!(field_elements.clone(), decrypted_elements); @@ -960,9 +981,9 @@ mod integration_tests { let mut rng = rand::rng(); // Create two different key pairs - let secret1 = SecretKey::with_rng(&mut rng); + let secret1 = KeyExchangeKey::with_rng(&mut rng); let public1 = secret1.public_key(); - let secret2 = SecretKey::with_rng(&mut rng); + let secret2 = KeyExchangeKey::with_rng(&mut rng); let public2 = secret2.public_key(); let sealing_key1 = SealingKey::K256AeadPoseidon2(public1); @@ -982,7 +1003,7 @@ mod integration_tests { mod keys_serialization_tests { use super::*; - use crate::utils::ByteReader; + use crate::{dsa::ecdsa_k256_keccak::KeyExchangeKey, utils::ByteReader}; fn assert_roundtrip(sealing_key: SealingKey) { let expected_scheme = sealing_key.scheme(); @@ -1030,20 +1051,22 @@ mod keys_serialization_tests { fn sample_sealing_keys() -> Vec { let mut rng = rand::rng(); vec![ - SealingKey::K256XChaCha20Poly1305(SecretKey::with_rng(&mut rng).public_key()), - SealingKey::X25519XChaCha20Poly1305(SecretKey25519::with_rng(&mut rng).public_key()), - SealingKey::K256AeadPoseidon2(SecretKey::with_rng(&mut rng).public_key()), - SealingKey::X25519AeadPoseidon2(SecretKey25519::with_rng(&mut rng).public_key()), + SealingKey::K256XChaCha20Poly1305(KeyExchangeKey::with_rng(&mut rng).public_key()), + SealingKey::X25519XChaCha20Poly1305( + KeyExchangeKey25519::with_rng(&mut rng).public_key(), + ), + SealingKey::K256AeadPoseidon2(KeyExchangeKey::with_rng(&mut rng).public_key()), + SealingKey::X25519AeadPoseidon2(KeyExchangeKey25519::with_rng(&mut rng).public_key()), ] } fn sample_unsealing_keys() -> Vec { let mut rng = rand::rng(); vec![ - UnsealingKey::K256XChaCha20Poly1305(SecretKey::with_rng(&mut rng)), - UnsealingKey::X25519XChaCha20Poly1305(SecretKey25519::with_rng(&mut rng)), - UnsealingKey::K256AeadPoseidon2(SecretKey::with_rng(&mut rng)), - UnsealingKey::X25519AeadPoseidon2(SecretKey25519::with_rng(&mut rng)), + UnsealingKey::K256XChaCha20Poly1305(KeyExchangeKey::with_rng(&mut rng)), + UnsealingKey::X25519XChaCha20Poly1305(KeyExchangeKey25519::with_rng(&mut rng)), + UnsealingKey::K256AeadPoseidon2(KeyExchangeKey::with_rng(&mut rng)), + UnsealingKey::X25519AeadPoseidon2(KeyExchangeKey25519::with_rng(&mut rng)), ] } diff --git a/miden-crypto/src/lib.rs b/miden-crypto/src/lib.rs index 2e06cf5925..cb10e6fb53 100644 --- a/miden-crypto/src/lib.rs +++ b/miden-crypto/src/lib.rs @@ -16,7 +16,7 @@ pub mod utils; // RE-EXPORTS // ================================================================================================ -pub use miden_field::{Felt, LexicographicWord, Word, WordError, word}; +pub use miden_field::{Felt, Word, WordError, word}; pub mod field { //! Traits and utilities for working with the Goldilocks finite field (i.e., @@ -45,7 +45,7 @@ pub mod parallel { pub mod stark { //! Lifted STARK proving system based on Plonky3. //! - //! Sub-modules from `p3-miden-lifted-stark`: + //! Sub-modules from `miden-lifted-stark`: //! - [`proof`] — [`proof::StarkProof`], [`proof::StarkDigest`], [`proof::StarkOutput`], //! [`proof::StarkTranscript`] //! - [`air`] — AIR traits, builders, symbolic types (includes all of `p3-air`) @@ -64,9 +64,9 @@ pub mod stark { //! - [`symmetric`] — Symmetric cryptographic primitives // Top-level types from lifted-stark - pub use p3_miden_lifted_stark::{GenericStarkConfig, StarkConfig}; + pub use miden_lifted_stark::{GenericStarkConfig, StarkConfig}; // Lifted-stark sub-modules (re-exported as-is) - pub use p3_miden_lifted_stark::{ + pub use miden_lifted_stark::{ air, debug, fri, hasher, lmcs, proof, prover, transcript, verifier, }; @@ -107,11 +107,6 @@ pub mod stark { #[cfg(feature = "std")] pub type Map = std::collections::HashMap; -#[cfg(feature = "std")] -pub use std::collections::hash_map::Entry as MapEntry; -#[cfg(feature = "std")] -pub use std::collections::hash_map::IntoIter as MapIntoIter; - /// An alias for a key-value map. /// /// When the `std` feature is enabled, this is an alias for [`std::collections::HashMap`]. @@ -123,6 +118,10 @@ pub type Map = alloc::collections::BTreeMap; pub use alloc::collections::btree_map::Entry as MapEntry; #[cfg(not(feature = "std"))] pub use alloc::collections::btree_map::IntoIter as MapIntoIter; +#[cfg(feature = "std")] +pub use std::collections::hash_map::Entry as MapEntry; +#[cfg(feature = "std")] +pub use std::collections::hash_map::IntoIter as MapIntoIter; /// An alias for a simple set. /// @@ -141,17 +140,14 @@ pub type Set = alloc::collections::BTreeSet; // CONSTANTS // ================================================================================================ -/// Number of field elements in a word. -pub const WORD_SIZE: usize = word::WORD_SIZE_FELTS; - -/// Field element representing ZERO in the Miden base filed. +/// Field element representing ZERO in the Miden base field. pub const ZERO: Felt = Felt::ZERO; -/// Field element representing ONE in the Miden base filed. +/// Field element representing ONE in the Miden base field. pub const ONE: Felt = Felt::ONE; /// Array of field elements representing word of ZEROs in the Miden base field. -pub const EMPTY_WORD: Word = Word::new([ZERO; WORD_SIZE]); +pub const EMPTY_WORD: Word = Word::new([ZERO; Word::NUM_ELEMENTS]); // TRAITS // ================================================================================================ @@ -177,8 +173,6 @@ pub trait SequentialCommit { // ================================================================================================ mod batch_inversion { - use alloc::vec::Vec; - use p3_maybe_rayon::prelude::*; use super::{Felt, ONE, ZERO, field::Field}; @@ -189,14 +183,12 @@ mod batch_inversion { pub fn batch_inversion_allow_zeros(values: &mut [Felt]) { const CHUNK_SIZE: usize = 1024; - // We need to work with a copy since we're modifying in place - let input: Vec = values.to_vec(); - - input.par_chunks(CHUNK_SIZE).zip(values.par_chunks_mut(CHUNK_SIZE)).for_each( - |(input_chunk, output_chunk)| { - batch_inversion_helper(input_chunk, output_chunk); - }, - ); + values.par_chunks_mut(CHUNK_SIZE).for_each(|output_chunk| { + let len = output_chunk.len(); + let mut scratch = [ZERO; CHUNK_SIZE]; + scratch[..len].copy_from_slice(output_chunk); + batch_inversion_helper(&scratch[..len], output_chunk); + }); } /// Montgomery's trick for batch inversion, handling zeros. @@ -232,17 +224,43 @@ mod batch_inversion { #[cfg(test)] mod tests { + use alloc::vec::Vec; + use super::*; #[test] fn test_batch_inversion_allow_zeros() { - let mut column = Vec::from([Felt::new(2), ZERO, Felt::new(4), Felt::new(5)]); + let mut column = Vec::from([ + Felt::new_unchecked(2), + ZERO, + Felt::new_unchecked(4), + Felt::new_unchecked(5), + ]); batch_inversion_allow_zeros(&mut column); - assert_eq!(column[0], Felt::new(2).inverse()); + assert_eq!(column[0], Felt::new_unchecked(2).inverse()); assert_eq!(column[1], ZERO); - assert_eq!(column[2], Felt::new(4).inverse()); - assert_eq!(column[3], Felt::new(5).inverse()); + assert_eq!(column[2], Felt::new_unchecked(4).inverse()); + assert_eq!(column[3], Felt::new_unchecked(5).inverse()); + } + + #[test] + fn test_batch_inversion_allow_zeros_spans_fixed_chunks() { + let mut v: Vec = (1_u64..=2050).map(Felt::new_unchecked).collect(); + let expected: Vec = v.iter().copied().map(|x| x.inverse()).collect(); + batch_inversion_allow_zeros(&mut v); + assert_eq!(v, expected); + } + + #[test] + fn test_batch_inversion_allow_zeros_zero_on_chunk_boundary() { + let mut v = vec![Felt::new_unchecked(7); 1025]; + v[1023] = ZERO; + batch_inversion_allow_zeros(&mut v); + assert_eq!(v[1023], ZERO); + for i in (0..1023).chain(1024..1025) { + assert_eq!(v[i], Felt::new_unchecked(7).inverse()); + } } } } diff --git a/miden-crypto/src/main.rs b/miden-crypto/src/main.rs index 52aa06cabf..9d3f285bd6 100644 --- a/miden-crypto/src/main.rs +++ b/miden-crypto/src/main.rs @@ -71,7 +71,7 @@ pub fn benchmark_smt() { let mut entries = Vec::new(); for i in 0..tree_size { let key = rand_value::(); - let value = Word::new([ONE, ONE, ONE, Felt::new(i as u64)]); + let value = Word::new([ONE, ONE, ONE, Felt::new_unchecked(i as u64)]); entries.push((key, value)); } @@ -127,7 +127,7 @@ pub fn insertion(tree: &mut LargeSmt, insertions: usize) -> Result<(), for i in 0..insertions { let test_key = Poseidon2::hash(&rand_value::().to_be_bytes()); - let test_value = Word::new([ONE, ONE, ONE, Felt::new((size + i) as u64)]); + let test_value = Word::new([ONE, ONE, ONE, Felt::new_unchecked((size + i) as u64)]); let now = Instant::now(); tree.insert(test_key, test_value)?; @@ -155,7 +155,7 @@ pub fn batched_insertion( let new_pairs: Vec<(Word, Word)> = (0..insertions) .map(|i| { let key = Poseidon2::hash(&rand_value::().to_be_bytes()); - let value = Word::new([ONE, ONE, ONE, Felt::new((size + i) as u64)]); + let value = Word::new([ONE, ONE, ONE, Felt::new_unchecked((size + i) as u64)]); (key, value) }) .collect(); @@ -207,7 +207,7 @@ pub fn batched_update( let value = if rng.random_bool(REMOVAL_PROBABILITY) { EMPTY_WORD } else { - Word::new([ONE, ONE, ONE, Felt::new(rng.random())]) + Word::new([ONE, ONE, ONE, Felt::new_unchecked(rng.random())]) }; (key, value) diff --git a/miden-crypto/src/merkle/empty_roots.rs b/miden-crypto/src/merkle/empty_roots.rs index 5cef9503b1..acb768bc66 100644 --- a/miden-crypto/src/merkle/empty_roots.rs +++ b/miden-crypto/src/merkle/empty_roots.rs @@ -48,1540 +48,1540 @@ impl EmptySubtreeRoots { // merging the empty word upward. const EMPTY_SUBTREES: [Word; 256] = [ Word::new([ - Felt::new(0xb70f49c4aabc102d), - Felt::new(0xf38a6b6579150a60), - Felt::new(0xd5fb29ed3d3cf828), - Felt::new(0x0a2415d68965735f), + Felt::new_unchecked(0x4c4f25c381e6b418), + Felt::new_unchecked(0x4efaef9f33cf00b3), + Felt::new_unchecked(0x9b4ccd92de23bca0), + Felt::new_unchecked(0x5cbbdf50c70326fb), ]), Word::new([ - Felt::new(0x60f370624956f997), - Felt::new(0x096971f3e695318a), - Felt::new(0x4905cc3882a2334a), - Felt::new(0x34e362786e6759f0), + Felt::new_unchecked(0x6f4f58b1fd683ac0), + Felt::new_unchecked(0x98b0d7cc08c74f3a), + Felt::new_unchecked(0x0ec6e4033bd6571f), + Felt::new_unchecked(0x5d8262a48141ef78), ]), Word::new([ - Felt::new(0x3824d067838b15d6), - Felt::new(0x30417bf4696bd6be), - Felt::new(0x57625234b7b657ba), - Felt::new(0x40c6d1b249951cda), + Felt::new_unchecked(0x6a63aa718072ee71), + Felt::new_unchecked(0xea6da6a9d34039b0), + Felt::new_unchecked(0x12b3466ee1dea119), + Felt::new_unchecked(0x554a18ea9e8eb63f), ]), Word::new([ - Felt::new(0xd9d317c3bad907d7), - Felt::new(0x8cd99ba6cf203a57), - Felt::new(0x47c499cb3d999340), - Felt::new(0x60b28fab42847875), + Felt::new_unchecked(0x8c9f6f0a5a929353), + Felt::new_unchecked(0xd77fdb28292001c6), + Felt::new_unchecked(0x915a07da255563e5), + Felt::new_unchecked(0x18e8401cfda4dc43), ]), Word::new([ - Felt::new(0xb8dcfdb6a66dacc9), - Felt::new(0x4859b0ed69894461), - Felt::new(0x5cf0768579ca2f98), - Felt::new(0x3358b0e52b7b1d34), + Felt::new_unchecked(0x85ba2b42cd9f245b), + Felt::new_unchecked(0xc115f22e9485e488), + Felt::new_unchecked(0xf3048ff1fc10cb7b), + Felt::new_unchecked(0xbb0dd42baec71f4f), ]), Word::new([ - Felt::new(0x13820bce6b9e9734), - Felt::new(0xc278575ef6c0ba33), - Felt::new(0x909216fec7fa7515), - Felt::new(0x277b09c7728bb7f5), + Felt::new_unchecked(0x58d2b955c592a555), + Felt::new_unchecked(0x059bab14b5e84168), + Felt::new_unchecked(0xb8bd990c0af52b1d), + Felt::new_unchecked(0x6914c44744a57a2b), ]), Word::new([ - Felt::new(0x57730f57b34fc978), - Felt::new(0xb6e64648ef9b842b), - Felt::new(0x02e4f56085c37b5f), - Felt::new(0xff5f1f2291d26346), + Felt::new_unchecked(0x91baf2cf95412a72), + Felt::new_unchecked(0xc72a7b4e3ef1d622), + Felt::new_unchecked(0x9a99975e5ab63e0b), + Felt::new_unchecked(0x9b16c5c66c7586ee), ]), Word::new([ - Felt::new(0xf40f1334ccbd3d5b), - Felt::new(0x6fabce118d0a5997), - Felt::new(0x77a68b875eb4fb6b), - Felt::new(0x85107a3ee95c821f), + Felt::new_unchecked(0x426b357fabf42949), + Felt::new_unchecked(0x8709ad884a7ddb13), + Felt::new_unchecked(0x5c7bee1ee2f68a87), + Felt::new_unchecked(0xa7da1212f0218184), ]), Word::new([ - Felt::new(0xc6262d4107930d53), - Felt::new(0x7a8c642a0af3c172), - Felt::new(0x3af26e9e442a707d), - Felt::new(0xac62031f2abab944), + Felt::new_unchecked(0xdb29c8aa1f19bce8), + Felt::new_unchecked(0x272ddbb96bb8a008), + Felt::new_unchecked(0x3e390b4c8d4bcb80), + Felt::new_unchecked(0xaf37f1594a9038a8), ]), Word::new([ - Felt::new(0x44605e433fd3c167), - Felt::new(0xc3e3b8b0f3236e69), - Felt::new(0xd736df3ce2d7b28b), - Felt::new(0xd42a7bb1513c6d3d), + Felt::new_unchecked(0x085a13128e900c0c), + Felt::new_unchecked(0x5784c3e83c6c2355), + Felt::new_unchecked(0x980aedea6a7e9fcd), + Felt::new_unchecked(0x7acc863dfba2ed8e), ]), Word::new([ - Felt::new(0x53c291792b3b77dc), - Felt::new(0x4c3fed368e306473), - Felt::new(0x45c6e6bcaea9eff7), - Felt::new(0x93c68160468a305e), + Felt::new_unchecked(0x5ea58f1598470e6d), + Felt::new_unchecked(0x400c8c0f92222b7d), + Felt::new_unchecked(0x0fbcbed8e27387f8), + Felt::new_unchecked(0x82bc62efd05c114c), ]), Word::new([ - Felt::new(0xbc6925f5b3017050), - Felt::new(0xde86e82ab374379e), - Felt::new(0x6623a8fb49a26e8b), - Felt::new(0x3f299face348f881), + Felt::new_unchecked(0x5b44914d0bf7b2c5), + Felt::new_unchecked(0xda1a4ff72d0d1e9a), + Felt::new_unchecked(0xc1ce4f8428eb0d50), + Felt::new_unchecked(0xf5ff337bc642e2e3), ]), Word::new([ - Felt::new(0x74af619f24f20403), - Felt::new(0x24f25d56523eab6f), - Felt::new(0x3aa725c3a2235b82), - Felt::new(0x14420f697109064c), + Felt::new_unchecked(0x8359bfcbca34cb46), + Felt::new_unchecked(0xe82f32b88c5666c2), + Felt::new_unchecked(0x66423a5bb08de622), + Felt::new_unchecked(0xc9652d509826e5f6), ]), Word::new([ - Felt::new(0xcbffe33be12e5fae), - Felt::new(0x41deb98f3e7b69aa), - Felt::new(0x0b4adfb7b431bd72), - Felt::new(0x0b408fdb684a306a), + Felt::new_unchecked(0x6e68f94374d4bc14), + Felt::new_unchecked(0x6e793711d79b0cfb), + Felt::new_unchecked(0x2f7b445e1dd0100b), + Felt::new_unchecked(0x723e3e45f2825c2f), ]), Word::new([ - Felt::new(0xef8d84d2a98b503c), - Felt::new(0xe069f292e333e21a), - Felt::new(0x66ac70af5d095046), - Felt::new(0x9c3f4a827756042b), + Felt::new_unchecked(0x77feeb0c5f52033c), + Felt::new_unchecked(0xf585a9f40bc6379c), + Felt::new_unchecked(0x761d43ca6808af7f), + Felt::new_unchecked(0x5bd50d39bf707d3b), ]), Word::new([ - Felt::new(0xb6621ea4d868959c), - Felt::new(0x6654f0371c7ab628), - Felt::new(0xa86f684ace4b22a3), - Felt::new(0xd3e15afa79689c3e), + Felt::new_unchecked(0xd517236676f152ce), + Felt::new_unchecked(0x0c8763bedf2d0a12), + Felt::new_unchecked(0xe8c118f6922937bd), + Felt::new_unchecked(0x4fe8bd8ef1acfa95), ]), Word::new([ - Felt::new(0x8998bdb1d4483bf9), - Felt::new(0x22fbe7289a50d070), - Felt::new(0x2a5382ef2162a65c), - Felt::new(0x5a3960393b8e7c61), + Felt::new_unchecked(0x96d944f4048b971e), + Felt::new_unchecked(0x60470c914ff5b86e), + Felt::new_unchecked(0x1d1b2780ccf538a7), + Felt::new_unchecked(0x79cca49fd17f1ea6), ]), Word::new([ - Felt::new(0x715e9a73a0f50f0b), - Felt::new(0x7265725a0d6b733b), - Felt::new(0x794591556f58d6cd), - Felt::new(0x77073707f8dd3c13), + Felt::new_unchecked(0x4db6c0e245289509), + Felt::new_unchecked(0x7e23e1237bb3b42b), + Felt::new_unchecked(0x26ed2be12edb3df8), + Felt::new_unchecked(0xc73fc0c321466ba0), ]), Word::new([ - Felt::new(0xead47b65b8cf7e93), - Felt::new(0x3db24824b44f2f8a), - Felt::new(0xd032f4c8141623a2), - Felt::new(0x7ffd33ca934df915), + Felt::new_unchecked(0xf4d102ff48eb8faf), + Felt::new_unchecked(0x8003b9d941b4a022), + Felt::new_unchecked(0x9e17f1edd40819a8), + Felt::new_unchecked(0x3b0f05835eeb4d7a), ]), Word::new([ - Felt::new(0x86e0f5f5c4ea97cb), - Felt::new(0x2ce565c6fcd84a5c), - Felt::new(0x8ce0883224280104), - Felt::new(0x3475d1d7f8124f22), + Felt::new_unchecked(0x3d1b8bd3071b2b98), + Felt::new_unchecked(0xa794c8cded26a3a9), + Felt::new_unchecked(0xd0173a1c4483a95b), + Felt::new_unchecked(0xf4cb877e2c41e10b), ]), Word::new([ - Felt::new(0x31ecf12b05f93adf), - Felt::new(0x17242240909b8925), - Felt::new(0xf021614f85a0fd8e), - Felt::new(0x0681e976da2da221), + Felt::new_unchecked(0xfac0e64c6f5a183e), + Felt::new_unchecked(0x1ce5a42204919c27), + Felt::new_unchecked(0xcc3e93ee7f5f5b68), + Felt::new_unchecked(0x40890d85ab73dbed), ]), Word::new([ - Felt::new(0x94b1324ed4e3d2c3), - Felt::new(0xb2f7e944db9b7fc3), - Felt::new(0xcb3b4289822ed449), - Felt::new(0x7add148186f57183), + Felt::new_unchecked(0xe4a5839f68d1428c), + Felt::new_unchecked(0xb04141af0bcd7c6e), + Felt::new_unchecked(0x15876f1a5c40d606), + Felt::new_unchecked(0x5e7e0f1546b8f66d), ]), Word::new([ - Felt::new(0xf599fba7930f1fb8), - Felt::new(0x52a8dbeb5a7f2277), - Felt::new(0xeb3e832df10049e2), - Felt::new(0x65a509471c608e4d), + Felt::new_unchecked(0x8440ee0558f7750e), + Felt::new_unchecked(0x8ab26896d8c22617), + Felt::new_unchecked(0x3301c974c37fc64a), + Felt::new_unchecked(0x39e8f07f3739d84b), ]), Word::new([ - Felt::new(0xe82a672f9d3d1939), - Felt::new(0x7e50791921de7cad), - Felt::new(0xfcf6af2550d444a6), - Felt::new(0xf59d4bd29be522c2), + Felt::new_unchecked(0xdea9bc5832fd621d), + Felt::new_unchecked(0x3cd34508a2a4aff5), + Felt::new_unchecked(0xfb0986c303d559f0), + Felt::new_unchecked(0xb3622c645327f880), ]), Word::new([ - Felt::new(0xc4fba9ae23020044), - Felt::new(0xdbe23428e91cf970), - Felt::new(0x8f4d9b4974bd6748), - Felt::new(0x010fb7ab752d50d4), + Felt::new_unchecked(0xd6c5f7a1531cf087), + Felt::new_unchecked(0x114cf2ac95e0cee3), + Felt::new_unchecked(0xd8835c2c0ff1dfe8), + Felt::new_unchecked(0x62ae27c77404a384), ]), Word::new([ - Felt::new(0x870999a63cf57309), - Felt::new(0xf295b3cecc17c7fa), - Felt::new(0x55daca97a971c755), - Felt::new(0xf3a11d1147e9bee5), + Felt::new_unchecked(0xe29f14d09f47b04b), + Felt::new_unchecked(0x533ea3fa6b298a76), + Felt::new_unchecked(0xb78b67033c9064de), + Felt::new_unchecked(0xd7c87b4821cbbfa0), ]), Word::new([ - Felt::new(0x154b008d1baa72b7), - Felt::new(0x0ecf570359fc91e1), - Felt::new(0xf1b7f9e2692e0b25), - Felt::new(0x8f6bbf0ad9867a77), + Felt::new_unchecked(0x93c7a74d0def216d), + Felt::new_unchecked(0xddb3170d3cbf7553), + Felt::new_unchecked(0x8c5470c0a538c7a8), + Felt::new_unchecked(0xae95587051aa3371), ]), Word::new([ - Felt::new(0x022cb18c0a5a203e), - Felt::new(0x24491dbefe48ef4d), - Felt::new(0xae9e8c66c44eec84), - Felt::new(0x1c53ce2fa81a8417), + Felt::new_unchecked(0x51fef34c69193b93), + Felt::new_unchecked(0xe10207f668aaabec), + Felt::new_unchecked(0xbf04a03680bd70ea), + Felt::new_unchecked(0x6ffd5e8ac06ab267), ]), Word::new([ - Felt::new(0xe1df227725b70010), - Felt::new(0x28fb719d8e33d565), - Felt::new(0x3742a4a680e5881a), - Felt::new(0xd07e4304605673d4), + Felt::new_unchecked(0x36c0cc1726d5f776), + Felt::new_unchecked(0xda699e36a057bf54), + Felt::new_unchecked(0xe50e1a16719ce9d7), + Felt::new_unchecked(0x0362ffdcd7166b33), ]), Word::new([ - Felt::new(0xdbd7f9c2b149bf22), - Felt::new(0xb61f1889e20354a8), - Felt::new(0x4bb9e22ab3e4d65a), - Felt::new(0x9cd1da3ff563f651), + Felt::new_unchecked(0xbda2e0822ceca3be), + Felt::new_unchecked(0x18d920168dd7cfd3), + Felt::new_unchecked(0xc9af295181c108f0), + Felt::new_unchecked(0x6f43459ca6286f64), ]), Word::new([ - Felt::new(0x8e1d357154c2634a), - Felt::new(0xa644c12376fa5cae), - Felt::new(0xdb83acd5f00c27ab), - Felt::new(0xb3e2376e846bd50e), + Felt::new_unchecked(0x6bcbded8665027ff), + Felt::new_unchecked(0x52c89ea7e2b2824c), + Felt::new_unchecked(0xef14967767d21f83), + Felt::new_unchecked(0x87f520ca8640f685), ]), Word::new([ - Felt::new(0xd85519b0e2c2f289), - Felt::new(0x29d5604abe1203e7), - Felt::new(0xeb92a378922a359e), - Felt::new(0xde6cf31064c0eb69), + Felt::new_unchecked(0x94e18c15e22f8a24), + Felt::new_unchecked(0x745f09ab5e3eeca5), + Felt::new_unchecked(0x6fc2fec9f65bc60b), + Felt::new_unchecked(0xd4e4aca63a43a809), ]), Word::new([ - Felt::new(0x682db793679dd5f4), - Felt::new(0x96ef3fe1870faff2), - Felt::new(0x73013ec444a4d7a5), - Felt::new(0xcaf9d122c3e38336), + Felt::new_unchecked(0xa72cd5e0a9dbd78a), + Felt::new_unchecked(0x1bd859442151023b), + Felt::new_unchecked(0xe93168e5153a73d3), + Felt::new_unchecked(0x1944bf967eaea104), ]), Word::new([ - Felt::new(0x2a50e766ac3244b0), - Felt::new(0x53a6802d0c699901), - Felt::new(0xc288971f330762c3), - Felt::new(0x8b6c0635d1773d45), + Felt::new_unchecked(0xc094c3946c426b72), + Felt::new_unchecked(0xe2b3d4652ec373f2), + Felt::new_unchecked(0x1627f44f3c70ba62), + Felt::new_unchecked(0xdd7ec3f1746bcb77), ]), Word::new([ - Felt::new(0x51c847dac636a5f1), - Felt::new(0x698969fca5682a41), - Felt::new(0xa2c7cb7e23da3352), - Felt::new(0x5fb4b42ad093656c), + Felt::new_unchecked(0x8335704932578d63), + Felt::new_unchecked(0x0a73e42cc7c17d84), + Felt::new_unchecked(0xf0d977e0b870de98), + Felt::new_unchecked(0xc3931cc44c9e9225), ]), Word::new([ - Felt::new(0xa008d253cd64a28d), - Felt::new(0x0724d0d35701b523), - Felt::new(0xaf366ea4c2d99dc7), - Felt::new(0xd3fac30a27fd1502), + Felt::new_unchecked(0x3dab0e77122f9016), + Felt::new_unchecked(0x931cf6c3711bf31f), + Felt::new_unchecked(0x3b007a30619f3df3), + Felt::new_unchecked(0x12892b2d3a64dbda), ]), Word::new([ - Felt::new(0x7fd83af788bb10bd), - Felt::new(0x98742683f67b7f27), - Felt::new(0xa117a29cd88e34d0), - Felt::new(0xc6ae5e698c779003), + Felt::new_unchecked(0x00eb98933ba2f6bb), + Felt::new_unchecked(0xb7002750b3367b93), + Felt::new_unchecked(0xda039ebdad7cb2fc), + Felt::new_unchecked(0x172ae08e17d4e8d4), ]), Word::new([ - Felt::new(0x4bbfd2b901b9dfef), - Felt::new(0xa9080fe3b22a0325), - Felt::new(0xc86530265f007301), - Felt::new(0x8e01ae5e6c8f615d), + Felt::new_unchecked(0xf2cb4906b28f35c2), + Felt::new_unchecked(0x4bb30dca93339b8f), + Felt::new_unchecked(0x89205b67d713298d), + Felt::new_unchecked(0x977eb680b5770040), ]), Word::new([ - Felt::new(0x43f162b5392273ed), - Felt::new(0xd6c79ba623643952), - Felt::new(0xf961ca425dce47a9), - Felt::new(0x6887d784400afb26), + Felt::new_unchecked(0x1c8bf29ccba294fe), + Felt::new_unchecked(0xfecaead56c11576c), + Felt::new_unchecked(0x189f3b1165497858), + Felt::new_unchecked(0xdf1b25359471086f), ]), Word::new([ - Felt::new(0x354c512d4ea52b8e), - Felt::new(0x5d91bebd21c6249a), - Felt::new(0xb0a276cfb81d599f), - Felt::new(0x1a7acd4a354baa3b), + Felt::new_unchecked(0x358ee429251b0900), + Felt::new_unchecked(0xa18acdc332bf723a), + Felt::new_unchecked(0x38ac0b7c9ca73880), + Felt::new_unchecked(0x34d4eaad1b5f0496), ]), Word::new([ - Felt::new(0x788a621898e13694), - Felt::new(0x004e2b5b0bb7fa90), - Felt::new(0x59cb4c714f14cbc8), - Felt::new(0x02402688fee3e69b), + Felt::new_unchecked(0xe9023a1544703b89), + Felt::new_unchecked(0x6db0aa42d2e16023), + Felt::new_unchecked(0x8122a04db9523900), + Felt::new_unchecked(0x9d01f08dc8a514e1), ]), Word::new([ - Felt::new(0xa9a4a6cc9e8ad4b3), - Felt::new(0x9eeb3b4ec6076c2b), - Felt::new(0x815b1ab01fef23a3), - Felt::new(0xf91e9d41209c1781), + Felt::new_unchecked(0x274f6fc77438bb26), + Felt::new_unchecked(0xc25ff1046af9c4cd), + Felt::new_unchecked(0x82a69843472e69d3), + Felt::new_unchecked(0xe5a626ddfec0d5ec), ]), Word::new([ - Felt::new(0xdbdf8c531cddc05b), - Felt::new(0x9c217c8ffc968205), - Felt::new(0xf23b50f1647c080b), - Felt::new(0x0fc6963460c88ce5), + Felt::new_unchecked(0xc7bc0974d4d1ebdc), + Felt::new_unchecked(0xa63ee049dbc1346a), + Felt::new_unchecked(0x59cce9fbd6216ecb), + Felt::new_unchecked(0x1506f9b3a667fc21), ]), Word::new([ - Felt::new(0x75fae0bbb7de0f22), - Felt::new(0xc490fee21d5a4ae7), - Felt::new(0xae08a07e4a727dee), - Felt::new(0xb814ac6a66e63c73), + Felt::new_unchecked(0x740975d66980e9b3), + Felt::new_unchecked(0x491071fde38ebcbc), + Felt::new_unchecked(0x51d27f9782b7aa71), + Felt::new_unchecked(0xe2cccee6a3058f81), ]), Word::new([ - Felt::new(0x5f9f6699b6ecc804), - Felt::new(0xe4808414b962660c), - Felt::new(0x15a5ba5ec4cd4f06), - Felt::new(0x3012b4afba294f4f), + Felt::new_unchecked(0xefafa0d1cca18f6b), + Felt::new_unchecked(0x21d01e1f4d3200fd), + Felt::new_unchecked(0x32543fb5cb1f1330), + Felt::new_unchecked(0x574e659ac1bd470a), ]), Word::new([ - Felt::new(0xd9f051c926775485), - Felt::new(0x86a4e6a7c2f3c239), - Felt::new(0xed7a0e2c5f58ceb9), - Felt::new(0x3f1b26a4da43cd0d), + Felt::new_unchecked(0xb8be198c6b934315), + Felt::new_unchecked(0x032729d41beabbd2), + Felt::new_unchecked(0xe8ae241489f2f7be), + Felt::new_unchecked(0x2f6be58f340ce431), ]), Word::new([ - Felt::new(0x2165cd0f28f3599c), - Felt::new(0xb50ee14aba3e7170), - Felt::new(0x407eaf93756a857f), - Felt::new(0xff39c5c2ee52a4f9), + Felt::new_unchecked(0x61192457c61235d5), + Felt::new_unchecked(0x12a955ce18e6c305), + Felt::new_unchecked(0xcdd90649a2bbe9ab), + Felt::new_unchecked(0x91b25426cf074b2e), ]), Word::new([ - Felt::new(0x335459b2e67f0067), - Felt::new(0x279b7c011b14108b), - Felt::new(0x86b22da12e45da48), - Felt::new(0xa42bccc7717df4ce), + Felt::new_unchecked(0x28179dce83b0d50e), + Felt::new_unchecked(0x93a7870c5eb916ed), + Felt::new_unchecked(0xdfe027afc4f5480a), + Felt::new_unchecked(0x9ab5b0ae17c54ace), ]), Word::new([ - Felt::new(0x694548f769f4895f), - Felt::new(0x2a4a24618314b209), - Felt::new(0xba6794f78b5c5b4d), - Felt::new(0x39ecedb9a601d9c6), + Felt::new_unchecked(0xcb3bbd3a71dab53e), + Felt::new_unchecked(0x4304ceec93bfb5ea), + Felt::new_unchecked(0x2f2e6eccc76b435e), + Felt::new_unchecked(0xef13f7ce5fc20766), ]), Word::new([ - Felt::new(0x5441c2d7a579649a), - Felt::new(0xb4f6e2babb6e22f4), - Felt::new(0x4e2e474a92b8b51c), - Felt::new(0x0ee364795d881cc1), + Felt::new_unchecked(0xf1cab7b14b2b4886), + Felt::new_unchecked(0x5c8a8f437fde7974), + Felt::new_unchecked(0x25caa4ead5c6f47f), + Felt::new_unchecked(0x968d3e942a727813), ]), Word::new([ - Felt::new(0x875901844acc837e), - Felt::new(0x337fdc8fe4306ec6), - Felt::new(0xfd3d7e42b2ef10cd), - Felt::new(0x79e8ff324da10dab), + Felt::new_unchecked(0xc8ca564e1fa15e70), + Felt::new_unchecked(0x3541a4393cced221), + Felt::new_unchecked(0x8e3e6294a5a3e70a), + Felt::new_unchecked(0x41fd4afd00ba830c), ]), Word::new([ - Felt::new(0x12ebb44ba4a44d5b), - Felt::new(0x4a2061b3541ef52f), - Felt::new(0x8e789cb40e742e0d), - Felt::new(0x171a423883764c09), + Felt::new_unchecked(0xc2cabf38295ecfc6), + Felt::new_unchecked(0xcd115741a673fa37), + Felt::new_unchecked(0xacb77a4ab17f55d7), + Felt::new_unchecked(0x7e1840e35c545330), ]), Word::new([ - Felt::new(0x67d23a623f20da69), - Felt::new(0xd9e625c05be6f041), - Felt::new(0x93068ae492586907), - Felt::new(0x11a5eb7ff2417b7c), + Felt::new_unchecked(0x750c303c0e6d0dab), + Felt::new_unchecked(0xcb66103f5bb48380), + Felt::new_unchecked(0x9eec24d54e17f633), + Felt::new_unchecked(0x4e553d6e7f94254c), ]), Word::new([ - Felt::new(0x9c3d6e2e97125ee3), - Felt::new(0x550479ac4d9b702c), - Felt::new(0xe059e7a2fa114053), - Felt::new(0x5abd975d30928636), + Felt::new_unchecked(0xfb1bc99cffe0024b), + Felt::new_unchecked(0xd0fa793ff9b9ec3f), + Felt::new_unchecked(0x6923727eedaf58ed), + Felt::new_unchecked(0x503b9a6dec12cb30), ]), Word::new([ - Felt::new(0x91859ed75bc15763), - Felt::new(0xba8b03049e91bee7), - Felt::new(0x545c6fd0d574bb1c), - Felt::new(0xf7eb3257db1f03e6), + Felt::new_unchecked(0x77fe721f644abb71), + Felt::new_unchecked(0x4d7856e2d36a578e), + Felt::new_unchecked(0x02bf228c6daa5349), + Felt::new_unchecked(0xad735e6136044c50), ]), Word::new([ - Felt::new(0x0546e4e18a472e57), - Felt::new(0xecb125d13bc5068e), - Felt::new(0xd20ef3e739a69fdd), - Felt::new(0x54da5ceb579efb00), + Felt::new_unchecked(0x1552e98b8399fee0), + Felt::new_unchecked(0x660aa729b5cef551), + Felt::new_unchecked(0xb00bcdaae79cbf64), + Felt::new_unchecked(0x0837b92aa8d6c16b), ]), Word::new([ - Felt::new(0x9e6d46b0ad9e1c3f), - Felt::new(0x3fc49f1d3b987ec3), - Felt::new(0xa89dba5622cbd29d), - Felt::new(0x7c2d052aa927f1a6), + Felt::new_unchecked(0x288a230cf0ae5b21), + Felt::new_unchecked(0xa47da6db58a203f0), + Felt::new_unchecked(0x127ccfa9e3e7ea72), + Felt::new_unchecked(0xd0a6ca2919ff6f3a), ]), Word::new([ - Felt::new(0xc987595852b3000c), - Felt::new(0xf4d145064450dca0), - Felt::new(0x3e489e33eb7a316d), - Felt::new(0x3d17bca9d577410e), + Felt::new_unchecked(0x7927c07d0e07ee1e), + Felt::new_unchecked(0x101f4053bd001276), + Felt::new_unchecked(0xccdfcc6d0a9a2582), + Felt::new_unchecked(0x33a309ccc919a439), ]), Word::new([ - Felt::new(0x32666c6229bfa4ab), - Felt::new(0x14891937dca422fb), - Felt::new(0x57a753c70b0ac3d9), - Felt::new(0xb32446493d5464ce), + Felt::new_unchecked(0x571070fa8529b634), + Felt::new_unchecked(0x6f9d97f1091906cc), + Felt::new_unchecked(0x2524aeca304cb9e4), + Felt::new_unchecked(0x4088d46b157038b8), ]), Word::new([ - Felt::new(0xbb2c31ac5a1a3719), - Felt::new(0x6d6a26ba7f3cc71b), - Felt::new(0x6c58ba151bf46fa1), - Felt::new(0x8a9ee566cb189e35), + Felt::new_unchecked(0x8624643f3c389919), + Felt::new_unchecked(0x17d782b4204db31c), + Felt::new_unchecked(0xe75a4060068ba11a), + Felt::new_unchecked(0x4fad0d4319891940), ]), Word::new([ - Felt::new(0xe2223d554d0c5af4), - Felt::new(0x1e6fcd33a2bd8055), - Felt::new(0x118b06a85a99be06), - Felt::new(0x07497b70207a9a50), + Felt::new_unchecked(0x574cb69ffca04275), + Felt::new_unchecked(0x493938a8bccb44db), + Felt::new_unchecked(0xfb7ffc6b92f6a3e2), + Felt::new_unchecked(0x534c9d53b229d295), ]), Word::new([ - Felt::new(0x8b703d584eb67496), - Felt::new(0x3edfc05cfabfe2e8), - Felt::new(0x4743b9b2b46f9544), - Felt::new(0x47489dffb5959b15), + Felt::new_unchecked(0x31ae7423925ed97b), + Felt::new_unchecked(0xd38e2640229f51d7), + Felt::new_unchecked(0xdf41da363b58b070), + Felt::new_unchecked(0x43fe9fc24daf4a62), ]), Word::new([ - Felt::new(0x666cc8c5f556b230), - Felt::new(0x5c662624781d1eda), - Felt::new(0x162e768abdbe8770), - Felt::new(0xd7ba85dd1e72d4a1), + Felt::new_unchecked(0x5ab7380fbcc148fd), + Felt::new_unchecked(0x8ce336f0167b134c), + Felt::new_unchecked(0x435f4ecdd3ba555e), + Felt::new_unchecked(0xbea93960c5fa8d26), ]), Word::new([ - Felt::new(0x86aebabad91fedae), - Felt::new(0x0c56212bfc3e07f2), - Felt::new(0x05fdaed4545675ea), - Felt::new(0xfbf1e5af0f29d30f), + Felt::new_unchecked(0x45f7ea2a8fcd752f), + Felt::new_unchecked(0x19735515777c9278), + Felt::new_unchecked(0x2f70d26769c7c5e4), + Felt::new_unchecked(0xdcdd31f5d5336d5b), ]), Word::new([ - Felt::new(0x07ead4607e3284b1), - Felt::new(0x19f892ec975e1aa0), - Felt::new(0xe31cb5d20d65b858), - Felt::new(0x0156637a0ba0630d), + Felt::new_unchecked(0x8105568c603d0a4f), + Felt::new_unchecked(0xf11521a9400f2d43), + Felt::new_unchecked(0x0343856906a3b205), + Felt::new_unchecked(0x658b1805b9e5a6b0), ]), Word::new([ - Felt::new(0xee94681b12771edc), - Felt::new(0xaeb787acd51af0a5), - Felt::new(0x97ea46f0e59979ac), - Felt::new(0x0aedd1a12ef5a3da), + Felt::new_unchecked(0xb8235afd656e68d1), + Felt::new_unchecked(0x8e9325b28c9db5b8), + Felt::new_unchecked(0xb8e9b84eafe92bf1), + Felt::new_unchecked(0xaaab7303aec7b932), ]), Word::new([ - Felt::new(0xcc4dde18f244c0a3), - Felt::new(0x75593abedbca1f75), - Felt::new(0x0745f7e503834bb5), - Felt::new(0xd1aa72f11080e143), + Felt::new_unchecked(0x522be39d1d039336), + Felt::new_unchecked(0x54778cf95a13a4b7), + Felt::new_unchecked(0xf686afe997af8e76), + Felt::new_unchecked(0x49d00abd183e9577), ]), Word::new([ - Felt::new(0x2b03d6780b868555), - Felt::new(0xca3486641ed48f15), - Felt::new(0xeed3ea4297d7c4ff), - Felt::new(0x4e374623b5f5d940), + Felt::new_unchecked(0x7a87110967de6216), + Felt::new_unchecked(0x4e6a5094cbcd350a), + Felt::new_unchecked(0x4d21aca4636aa2af), + Felt::new_unchecked(0x9baefa59ca318945), ]), Word::new([ - Felt::new(0x86a0f718a27d9508), - Felt::new(0x5fe00cf82985a4bf), - Felt::new(0xe70dab7244c1d756), - Felt::new(0xfe84713dc37216a7), + Felt::new_unchecked(0xce4215291a0ee341), + Felt::new_unchecked(0x681e92bc5d92c35f), + Felt::new_unchecked(0x9ab62118bae03dbd), + Felt::new_unchecked(0xd662cdec48a1cb55), ]), Word::new([ - Felt::new(0xab9b7d9d92dc8e92), - Felt::new(0xd367e47cb409abb9), - Felt::new(0xb0815c449b50d320), - Felt::new(0x8bf91e2671a502cc), + Felt::new_unchecked(0x444bff5dd5d99841), + Felt::new_unchecked(0xcbc0bad4128802a7), + Felt::new_unchecked(0x7dc627dd675321a9), + Felt::new_unchecked(0x12ab3cc573078686), ]), Word::new([ - Felt::new(0x37e21999cca2af2a), - Felt::new(0x5d4c3fdae52fd6e3), - Felt::new(0x7c2a63f243083424), - Felt::new(0x85bc46e3253ced12), + Felt::new_unchecked(0x8743c664828e6b76), + Felt::new_unchecked(0xfe5d9306c95b1713), + Felt::new_unchecked(0x73de4560e75e8062), + Felt::new_unchecked(0x25536f84dd8fa10e), ]), Word::new([ - Felt::new(0x359397ff99b106dc), - Felt::new(0x5af7da08fbd5ed37), - Felt::new(0xbf4bcfc7f30cddc9), - Felt::new(0xdf5e505deea050ae), + Felt::new_unchecked(0x0eb224b91daef640), + Felt::new_unchecked(0xbe21b47657f5137f), + Felt::new_unchecked(0xcdc4e271cd9d455b), + Felt::new_unchecked(0xf91afc1c595493db), ]), Word::new([ - Felt::new(0x567e60727d9e8a69), - Felt::new(0xaddb2e33493bb678), - Felt::new(0x63e8f4fd813a8717), - Felt::new(0x7e6f327033191fa1), + Felt::new_unchecked(0x0fef850ea1af29ee), + Felt::new_unchecked(0xf391f562226b0ced), + Felt::new_unchecked(0x7710b9c25a70cdc0), + Felt::new_unchecked(0x1aa67e3c7783e360), ]), Word::new([ - Felt::new(0xc493169726e44d47), - Felt::new(0xb0d73f7dec4405e2), - Felt::new(0x12ce496206bd1144), - Felt::new(0x0489ebc41b5ff277), + Felt::new_unchecked(0x9728bb0dc4399b39), + Felt::new_unchecked(0x6b4967135b05596b), + Felt::new_unchecked(0x98d77568369f548e), + Felt::new_unchecked(0x1003df8f565f3e5b), ]), Word::new([ - Felt::new(0x12eac3132c52f64c), - Felt::new(0xe9c1587c49a823b5), - Felt::new(0x9bc86b1fb88516e7), - Felt::new(0xa7e5a3ce7c7374b9), + Felt::new_unchecked(0x562424e7c70bfeb6), + Felt::new_unchecked(0x701766ac2bcb299c), + Felt::new_unchecked(0xea733a7d64bf2c86), + Felt::new_unchecked(0x9010902157097cfa), ]), Word::new([ - Felt::new(0xdab4168ab42c968d), - Felt::new(0x780f9ddf4a4e3571), - Felt::new(0x0dfbd49cc0ff60a9), - Felt::new(0x463559b7c08224e6), + Felt::new_unchecked(0x821b070d6522c246), + Felt::new_unchecked(0x92c01259b7e9b96b), + Felt::new_unchecked(0x49202f5a484e8b28), + Felt::new_unchecked(0x72928828b97dd37c), ]), Word::new([ - Felt::new(0xc6a2d68bc98659d8), - Felt::new(0x923f6036bde4a662), - Felt::new(0xf5ec79864915ff3e), - Felt::new(0xef3a9bdfd5fdde4a), + Felt::new_unchecked(0x06eb0da16018d870), + Felt::new_unchecked(0x1e56486cd6353a5b), + Felt::new_unchecked(0x5ce12d3f3dc19b05), + Felt::new_unchecked(0x9b80ef76e5fa3575), ]), Word::new([ - Felt::new(0x4d7b39df2a6682ab), - Felt::new(0xce7139c699c438a7), - Felt::new(0x1837e393fef489e9), - Felt::new(0x812d088b2b983f55), + Felt::new_unchecked(0xc35eee558722b546), + Felt::new_unchecked(0xdb166a796f0a02c2), + Felt::new_unchecked(0xf69e31d05695c58d), + Felt::new_unchecked(0x289bc5bc90544fcc), ]), Word::new([ - Felt::new(0x5720a4c993c56a60), - Felt::new(0xe25fcbb6156c7dba), - Felt::new(0x35c12e8b8141b466), - Felt::new(0x8eda168f7eff7b35), + Felt::new_unchecked(0x25b0e88c7012e834), + Felt::new_unchecked(0xa7224182373a6440), + Felt::new_unchecked(0x173777468abc3d44), + Felt::new_unchecked(0xe54cf20cffcf86ca), ]), Word::new([ - Felt::new(0xcf01f59c2e72b382), - Felt::new(0xe94386684f8ed2bf), - Felt::new(0xb6d7ba96cafc9994), - Felt::new(0xa8d5bbe975e059f0), + Felt::new_unchecked(0xc02e6a573c7ed292), + Felt::new_unchecked(0x35b6402d71d0adb9), + Felt::new_unchecked(0x5cd2c8a6ac3e7083), + Felt::new_unchecked(0x2388eda39acb5336), ]), Word::new([ - Felt::new(0x35f676b56b532441), - Felt::new(0xebe05922a3cc819c), - Felt::new(0x6284a94f8f871e5f), - Felt::new(0xf24d26cf4d21afb9), + Felt::new_unchecked(0x78c00bf96ea42bbf), + Felt::new_unchecked(0x1ec02d8b1dbc38ff), + Felt::new_unchecked(0xe7de2bfb53624b36), + Felt::new_unchecked(0x022aedad8ef3616b), ]), Word::new([ - Felt::new(0x27a4b6e402d091bf), - Felt::new(0xb14dbda6f732a9e4), - Felt::new(0xda4f85b0e8f66e2b), - Felt::new(0xf817d6f9d274e1ba), + Felt::new_unchecked(0x3ad714ca78bbe472), + Felt::new_unchecked(0x2a6ad98c25e3a428), + Felt::new_unchecked(0x75524799f1ce7b8c), + Felt::new_unchecked(0x7ee22afe3881d875), ]), Word::new([ - Felt::new(0xce4b5c3f48025345), - Felt::new(0x5d73b65785aab19f), - Felt::new(0x53b3ec2fbcb30503), - Felt::new(0xc4a5badd310cd88e), + Felt::new_unchecked(0x9b1d77b5f98a118a), + Felt::new_unchecked(0x5db8bc2ce3f61e6d), + Felt::new_unchecked(0xa683c9daa18110d8), + Felt::new_unchecked(0x753272e15a587968), ]), Word::new([ - Felt::new(0x88cc10e54f12c5ed), - Felt::new(0x411a746abc87ed3b), - Felt::new(0x2f1b94e4462119bc), - Felt::new(0x4b7e537ad6e5104b), + Felt::new_unchecked(0x060ca6e344060800), + Felt::new_unchecked(0xfae2f90213b86ff0), + Felt::new_unchecked(0x0606ac5f147b803d), + Felt::new_unchecked(0x434de966bd04f765), ]), Word::new([ - Felt::new(0x8260499fb2360775), - Felt::new(0xca531419eaab87d0), - Felt::new(0x2c8f6ffffbc730e3), - Felt::new(0x2a542eeb199e0188), + Felt::new_unchecked(0x8ef48ccfce07f406), + Felt::new_unchecked(0xf5a41d2111325bfe), + Felt::new_unchecked(0x17264f6eb5b4cdaa), + Felt::new_unchecked(0xec2310908b7b83d8), ]), Word::new([ - Felt::new(0xa3055a3f70793f26), - Felt::new(0x0a3b2cc605ef4732), - Felt::new(0xba8d01a69068b828), - Felt::new(0x810cefa092abdbbf), + Felt::new_unchecked(0xdb1b803fe8febe93), + Felt::new_unchecked(0xd9dfce0805d65336), + Felt::new_unchecked(0x6bb502b32f767b27), + Felt::new_unchecked(0xde85ac18157d6386), ]), Word::new([ - Felt::new(0x78c362cc4aaa4dd4), - Felt::new(0x7b4757319188651d), - Felt::new(0x181e324be417bd15), - Felt::new(0x155c8ed6282f7e86), + Felt::new_unchecked(0xf65dac4e4a996af3), + Felt::new_unchecked(0xfde7682301ddddd9), + Felt::new_unchecked(0xf3115bf873b0358c), + Felt::new_unchecked(0x6a8f813a7a583575), ]), Word::new([ - Felt::new(0x95dcdd0def238a6f), - Felt::new(0x559999f6fa5f08a4), - Felt::new(0x66d1e52cd3c59b7b), - Felt::new(0xec49070012294ee9), + Felt::new_unchecked(0xe8cb62a3817385f0), + Felt::new_unchecked(0xb183563b68457034), + Felt::new_unchecked(0x0504d9e6b8e8bb6b), + Felt::new_unchecked(0x36b2fafe5f506100), ]), Word::new([ - Felt::new(0x870ba5b4c7120439), - Felt::new(0x22b26a1f6dedb83c), - Felt::new(0x388be0894aab204a), - Felt::new(0xd352c8ac653b3add), + Felt::new_unchecked(0xf41e8ab106786bd4), + Felt::new_unchecked(0x1b2f564e57db0f66), + Felt::new_unchecked(0xc920d858114aed61), + Felt::new_unchecked(0xba4618226512e159), ]), Word::new([ - Felt::new(0x3b3b01bf43cb6ea2), - Felt::new(0xc95149e67675dc57), - Felt::new(0xbdb065f63b34a784), - Felt::new(0xb05d7b591c39496c), + Felt::new_unchecked(0x01befb99c9341601), + Felt::new_unchecked(0x900b096476ccc620), + Felt::new_unchecked(0xbb57d9835686b4bc), + Felt::new_unchecked(0x4c17d0a1558ae848), ]), Word::new([ - Felt::new(0x700765b29ce6a984), - Felt::new(0x487f2aad500ea0d8), - Felt::new(0xb19b89448f839bf4), - Felt::new(0xf087f0f19f7212cd), + Felt::new_unchecked(0xff222f8e8701f045), + Felt::new_unchecked(0x217dd307fededabf), + Felt::new_unchecked(0xfcf0c23bb1384f18), + Felt::new_unchecked(0x51f60bd42142dfd0), ]), Word::new([ - Felt::new(0xf6d142dcfc2dbdfd), - Felt::new(0xa90dc638201ba6ee), - Felt::new(0xcbf567a319c880b4), - Felt::new(0x7d9ff5233020a341), + Felt::new_unchecked(0x452212d261e584c4), + Felt::new_unchecked(0xeebdb4a2ccebd4a9), + Felt::new_unchecked(0x79cb25167abd229d), + Felt::new_unchecked(0x36a3793c743e1e30), ]), Word::new([ - Felt::new(0xa36d9cc70b13c4cf), - Felt::new(0xe6d9208c49661fe2), - Felt::new(0xa335db806dcc1072), - Felt::new(0x3f701c87e5664389), + Felt::new_unchecked(0x01caf9bb188b2f2f), + Felt::new_unchecked(0xaebb85879b3f6d6f), + Felt::new_unchecked(0x3d1e118a45ecaf5d), + Felt::new_unchecked(0xc8d325d158fb1d76), ]), Word::new([ - Felt::new(0x9b572bf042261f9b), - Felt::new(0x410dc7708306726e), - Felt::new(0x7e8fa17f0dad1f9b), - Felt::new(0xddd5d4cbdf070f10), + Felt::new_unchecked(0x459c4c9f4ef519ce), + Felt::new_unchecked(0xc91a01bd97fc5938), + Felt::new_unchecked(0x091183436933fcc3), + Felt::new_unchecked(0xd4824d71e41c687a), ]), Word::new([ - Felt::new(0xd5295c577641c9b2), - Felt::new(0xfe37beaaf966a996), - Felt::new(0x25324add6afc9701), - Felt::new(0x370c1b4b32e05189), + Felt::new_unchecked(0x5bea80fe6b9eccad), + Felt::new_unchecked(0x0094dc3df67db3c7), + Felt::new_unchecked(0x41df3c2193134951), + Felt::new_unchecked(0x4433faaebeedafdb), ]), Word::new([ - Felt::new(0x7052a5fcfb4b5f20), - Felt::new(0x5cc07e64180dba47), - Felt::new(0xe62a8d19ad29af7d), - Felt::new(0xcbfff28ef21ed355), + Felt::new_unchecked(0xd846b8d025899bbf), + Felt::new_unchecked(0xc88b0d0f914359be), + Felt::new_unchecked(0x1aa4242a605cec57), + Felt::new_unchecked(0x54a4c144cb6138ac), ]), Word::new([ - Felt::new(0xec08e75dd21ccba5), - Felt::new(0x0e453d4fbc8f33e4), - Felt::new(0xaf65358edc0dde3d), - Felt::new(0xedf1f85ea9557ffc), + Felt::new_unchecked(0xe78e15f937e23f9f), + Felt::new_unchecked(0xa2dd1699cda36767), + Felt::new_unchecked(0x0f72e41266047f3d), + Felt::new_unchecked(0x975be883eb3c1b59), ]), Word::new([ - Felt::new(0xbf0742d96d410028), - Felt::new(0x8cc49e4edb48965e), - Felt::new(0xf36263ffc2dd1b71), - Felt::new(0x7c8e5a64d9815b3b), + Felt::new_unchecked(0x35c61b05315ee566), + Felt::new_unchecked(0x431031de15b6bd94), + Felt::new_unchecked(0xac3cdcc29b203e9c), + Felt::new_unchecked(0x4ea89420b63a861b), ]), Word::new([ - Felt::new(0x7175b516eb9d97af), - Felt::new(0xd0f6f208f300a47c), - Felt::new(0x2d2ff1c7e93b28f4), - Felt::new(0xe5aa10f76b3d71f5), + Felt::new_unchecked(0x6e30a3f2468a73e8), + Felt::new_unchecked(0x13378272d987ee1b), + Felt::new_unchecked(0xb76724d0f023d9c5), + Felt::new_unchecked(0x01ece73b5f000a3b), ]), Word::new([ - Felt::new(0x47af3319e908137e), - Felt::new(0x77a6e80674004b8b), - Felt::new(0x9023839df1c9e1f9), - Felt::new(0x9b6ce63cc65d2757), + Felt::new_unchecked(0x0b1d5556f721b3fa), + Felt::new_unchecked(0x1e4eb49b7d09cdfa), + Felt::new_unchecked(0x25c8fbf2bbfa4037), + Felt::new_unchecked(0xdf4c7b917fad54d7), ]), Word::new([ - Felt::new(0x30742f2e5602e399), - Felt::new(0xfcdf70bba95b7358), - Felt::new(0xe2fd987d05ecdb8a), - Felt::new(0x155ac789fb89403d), + Felt::new_unchecked(0xafb7411d2986278d), + Felt::new_unchecked(0x52aff0e894aa5a93), + Felt::new_unchecked(0xb5ea358f35b12b1e), + Felt::new_unchecked(0xd935ee5912b2e915), ]), Word::new([ - Felt::new(0x5dffc0444bd3688b), - Felt::new(0x4b68b14225fd3604), - Felt::new(0xb967634b410b1a73), - Felt::new(0x6c0f6783006aaa4d), + Felt::new_unchecked(0x7d2fd3ca225e5354), + Felt::new_unchecked(0x7237154354e48d89), + Felt::new_unchecked(0xc333dd09608804ad), + Felt::new_unchecked(0xe9097f0416711084), ]), Word::new([ - Felt::new(0xbec989eda65bfd98), - Felt::new(0x10450a3aca653113), - Felt::new(0x09823d415301e260), - Felt::new(0x83c459041b044921), + Felt::new_unchecked(0xa1d950c36fd9f939), + Felt::new_unchecked(0xad28b64388bf93e1), + Felt::new_unchecked(0xc43be9724ea1d9d2), + Felt::new_unchecked(0x727307c17f2e1bfe), ]), Word::new([ - Felt::new(0x704ea1315a3c372b), - Felt::new(0xfe3ebe5b02b66532), - Felt::new(0xfe8e37e2c2a996cd), - Felt::new(0xc7e23f42d146aab8), + Felt::new_unchecked(0xb4c89f66d142e19b), + Felt::new_unchecked(0xf608ec08b6165621), + Felt::new_unchecked(0x10ece5bbbc02e0fd), + Felt::new_unchecked(0x8c9f28a65cf173e9), ]), Word::new([ - Felt::new(0x34518952a9c13963), - Felt::new(0x67a7efc16364b020), - Felt::new(0x05429574811e470c), - Felt::new(0x9f25ec1ebbebec78), + Felt::new_unchecked(0x1f631999cd61b4c3), + Felt::new_unchecked(0xe3c5a6cf71f4bae1), + Felt::new_unchecked(0x5c34e905be52ab53), + Felt::new_unchecked(0x653a911dd9c1d7dc), ]), Word::new([ - Felt::new(0xfd70b8e960ddb2cb), - Felt::new(0x235e94f4d0068cad), - Felt::new(0x57ba64f6fd33a07d), - Felt::new(0xf5e9be1498c2e243), + Felt::new_unchecked(0xa272f052fe6234ef), + Felt::new_unchecked(0x3a7a2b3f32c38206), + Felt::new_unchecked(0x2ecdfb73c120cc73), + Felt::new_unchecked(0xaa2fd92278513ebc), ]), Word::new([ - Felt::new(0x2d3d473a4c0f4022), - Felt::new(0x1b244b25a3ecadf1), - Felt::new(0xe855d144de4bd14d), - Felt::new(0x56282deb6a0ec7fe), + Felt::new_unchecked(0xf467afb92e2d6795), + Felt::new_unchecked(0xef30265ea67632c7), + Felt::new_unchecked(0x64ad8acfd6375081), + Felt::new_unchecked(0xadc4c536cb9bd2ab), ]), Word::new([ - Felt::new(0x5848e8580c68aa7c), - Felt::new(0xa3dcc518c329f378), - Felt::new(0x6ee02b80fdd99213), - Felt::new(0x2575ab55891f15ff), + Felt::new_unchecked(0x482ce2a7e663c83b), + Felt::new_unchecked(0x48e05f89c4d5be26), + Felt::new_unchecked(0x604eb9b913153b9d), + Felt::new_unchecked(0xfa9986b504865cc5), ]), Word::new([ - Felt::new(0xebada5cb5bb83bb3), - Felt::new(0xc9c76a48b826d274), - Felt::new(0x8412fd732379fb33), - Felt::new(0x07e2664d33d36fa4), + Felt::new_unchecked(0xaeb76174b9cdc11d), + Felt::new_unchecked(0xd3c91d07fabaf276), + Felt::new_unchecked(0x4582555f33ef461f), + Felt::new_unchecked(0xd744b889d614cc9f), ]), Word::new([ - Felt::new(0x988fd5a250b1b40a), - Felt::new(0x542d30d05090539e), - Felt::new(0x6e3504d6a03607cf), - Felt::new(0xf2246eee8300984f), + Felt::new_unchecked(0x9b3f64333c21296a), + Felt::new_unchecked(0x6fb3c324efd10a40), + Felt::new_unchecked(0x8c8cf62b454f4efb), + Felt::new_unchecked(0xacef39f5726e16d6), ]), Word::new([ - Felt::new(0x109b3acdc552fae8), - Felt::new(0xdd68d5a7226e4631), - Felt::new(0x5b88ec90433c1e7c), - Felt::new(0xf11315763e01912b), + Felt::new_unchecked(0x4e3b7bffad47bce6), + Felt::new_unchecked(0x6b56fb3ca25e2094), + Felt::new_unchecked(0xbc323b5db596de2f), + Felt::new_unchecked(0x6430b549346296f6), ]), Word::new([ - Felt::new(0xb6701421be588640), - Felt::new(0xc14dd230ceb161a2), - Felt::new(0x6893c74a74f31403), - Felt::new(0xea01a82649f6dcdc), + Felt::new_unchecked(0xeef784d4e8af60cc), + Felt::new_unchecked(0x62392952cf05b74d), + Felt::new_unchecked(0x4c70721202e81f91), + Felt::new_unchecked(0xc87f494c6dc691ee), ]), Word::new([ - Felt::new(0x10283c7df5f217a9), - Felt::new(0x6c3dca0ab6897e83), - Felt::new(0x9e7eec19b4184699), - Felt::new(0xd9727b457ccbc2b8), + Felt::new_unchecked(0x14cdbb3645c32ac1), + Felt::new_unchecked(0x63854ea3b584025f), + Felt::new_unchecked(0xb3d88642bc0171f4), + Felt::new_unchecked(0xd868c6895bb01153), ]), Word::new([ - Felt::new(0x5a5909a772bbb426), - Felt::new(0xc93992bc91b011c4), - Felt::new(0xd5eea93f55e1afca), - Felt::new(0xd0740d01853554f6), + Felt::new_unchecked(0x88a590242df9557d), + Felt::new_unchecked(0x64b807bf410bfde2), + Felt::new_unchecked(0x65b47f4e180efc80), + Felt::new_unchecked(0x0246f8de53fdd07e), ]), Word::new([ - Felt::new(0xe4b9fb13511684df), - Felt::new(0xb3e3778dd9ce82e7), - Felt::new(0x054e6fba0c8b873b), - Felt::new(0xc7db268ebb31f73a), + Felt::new_unchecked(0x202e98bf1e19ce05), + Felt::new_unchecked(0xd5f063d66ef11d6c), + Felt::new_unchecked(0x0aed03a3854e1a13), + Felt::new_unchecked(0xc74d8a8797926799), ]), Word::new([ - Felt::new(0x94ad755f91cfc69c), - Felt::new(0x364ad9b084460a84), - Felt::new(0x4225747075ff8e77), - Felt::new(0x71251602ae91069b), + Felt::new_unchecked(0x9b5fa4970f8e5aa5), + Felt::new_unchecked(0xdd35b0607ccf18e3), + Felt::new_unchecked(0x05e89c2cf1e443c8), + Felt::new_unchecked(0x5cebc8ae5a9d3646), ]), Word::new([ - Felt::new(0xd460369d74b2a756), - Felt::new(0xe68b74a4ce55495a), - Felt::new(0x5970fc0497d83b7e), - Felt::new(0x7febce94338a1449), + Felt::new_unchecked(0xf4ae5469926b2191), + Felt::new_unchecked(0xd7ffd83acb02c8ba), + Felt::new_unchecked(0xcc4f40c6c36bb791), + Felt::new_unchecked(0x9f698d2a6fc0d9bf), ]), Word::new([ - Felt::new(0xd43b3d1f844a1ead), - Felt::new(0xf0d5da85ce1c1861), - Felt::new(0xfd10bda41bfda3f4), - Felt::new(0xe503116a9c37c678), + Felt::new_unchecked(0x99b27009934262e2), + Felt::new_unchecked(0x393667c95ea35de8), + Felt::new_unchecked(0xf1cbf6b3ee65bc73), + Felt::new_unchecked(0xdf500c8e4c4e2dcf), ]), Word::new([ - Felt::new(0x2ba7ff5da227f5b1), - Felt::new(0x6d99d0ca2d6e2f20), - Felt::new(0x55f2fab50778d043), - Felt::new(0xebd80de8ffea5e79), + Felt::new_unchecked(0x20f52c072ad4a580), + Felt::new_unchecked(0xfd022f5d7a6ac0d2), + Felt::new_unchecked(0x9c389855acb74ab0), + Felt::new_unchecked(0xbabb009925c8b3fd), ]), Word::new([ - Felt::new(0x5ce5419daa27376e), - Felt::new(0x8a6c000961dbafca), - Felt::new(0xe94aab9d599d63e4), - Felt::new(0x83a2202191a77859), + Felt::new_unchecked(0x323b431386ba76d1), + Felt::new_unchecked(0x5dd6911ae570e2ee), + Felt::new_unchecked(0x08c231b8ff9f4a73), + Felt::new_unchecked(0x8bd6f86c6c291580), ]), Word::new([ - Felt::new(0x6e96352e7ad6d211), - Felt::new(0x17a0fd498269729c), - Felt::new(0xca3ee252b136307a), - Felt::new(0x95e3db23c78412cd), + Felt::new_unchecked(0x4a3ba00112507849), + Felt::new_unchecked(0xb1028b1610d6eed2), + Felt::new_unchecked(0xf039a251eb8f3690), + Felt::new_unchecked(0x269123f91b0cd017), ]), Word::new([ - Felt::new(0x9b074ce0916bb459), - Felt::new(0x47733a8262d8618a), - Felt::new(0xdc426c3f66253547), - Felt::new(0x5a003cdd6aefeaa4), + Felt::new_unchecked(0x24dc3153d23be189), + Felt::new_unchecked(0x61d889575042bf98), + Felt::new_unchecked(0x502059e5d751cf8a), + Felt::new_unchecked(0x5a1ac378f44c8063), ]), Word::new([ - Felt::new(0x6501daa92c53d863), - Felt::new(0x5d9c61a7bc7c12f6), - Felt::new(0x0e064127315e13df), - Felt::new(0x766824f9adabd9a9), + Felt::new_unchecked(0x9b0975a9c1efb2ce), + Felt::new_unchecked(0xa26cd01acb659cf2), + Felt::new_unchecked(0x2a7929ca8686292b), + Felt::new_unchecked(0xea2b392841ecc63e), ]), Word::new([ - Felt::new(0x4bbcc1f9bfc36037), - Felt::new(0x94592e04aef2047b), - Felt::new(0x1230ad895858c112), - Felt::new(0xd2a07c006c94050c), + Felt::new_unchecked(0xe791c55d25a490c4), + Felt::new_unchecked(0xeecf48698a684036), + Felt::new_unchecked(0x5d3540063eb35547), + Felt::new_unchecked(0xb2e9037e6b8aa5d1), ]), Word::new([ - Felt::new(0x38ee2f4837c28d2c), - Felt::new(0xe284a8c09189604c), - Felt::new(0xebd72086dca0ac2a), - Felt::new(0xc6dd43e307c6560f), + Felt::new_unchecked(0xa9127c67712cdb2b), + Felt::new_unchecked(0xd8e0c828f48e52db), + Felt::new_unchecked(0x1633d413bcc7af58), + Felt::new_unchecked(0x0809fc9b1368c0d4), ]), Word::new([ - Felt::new(0x5d80a21551fdb81c), - Felt::new(0xf47eb3d8bfce0fe2), - Felt::new(0x936aec656d34aca1), - Felt::new(0x3a9fa674bab063d0), + Felt::new_unchecked(0x543d5e5a9a354cdb), + Felt::new_unchecked(0x8efe9432a3bff23f), + Felt::new_unchecked(0x9c71988c5e808465), + Felt::new_unchecked(0xde3133d276c4154c), ]), Word::new([ - Felt::new(0x4584177925505f0c), - Felt::new(0x2c06394dc3c8b525), - Felt::new(0xb418bb11fc87f664), - Felt::new(0x2f2dd03d9035808a), + Felt::new_unchecked(0xd0292ad96a0420cf), + Felt::new_unchecked(0xb5598209c1f1c840), + Felt::new_unchecked(0x0aad93f39bb6aad5), + Felt::new_unchecked(0xfa2731146eb5ec8e), ]), Word::new([ - Felt::new(0xadd647fa0d07420e), - Felt::new(0x172f967db343e4ac), - Felt::new(0xfec7a4b2cd2ce36a), - Felt::new(0x11359b39c2c12eae), + Felt::new_unchecked(0x4db14fa9d981dbbd), + Felt::new_unchecked(0x22fd5e0be0a72322), + Felt::new_unchecked(0x962aef99d2b73f36), + Felt::new_unchecked(0x5cdd78b8c75ae55b), ]), Word::new([ - Felt::new(0xa53bf8e872ab17cf), - Felt::new(0x3e646eb81992887b), - Felt::new(0x8df625a197135d6c), - Felt::new(0xc9eb62cbf73b8afb), + Felt::new_unchecked(0x84b235264584bd50), + Felt::new_unchecked(0x112dd2d6f48ec693), + Felt::new_unchecked(0x1f0c9d23e85f4fc9), + Felt::new_unchecked(0x9d04f0c90a1171a5), ]), Word::new([ - Felt::new(0xb2fa941b228d6f3a), - Felt::new(0xc56418d9e7de11d0), - Felt::new(0x4898d1fce23143d4), - Felt::new(0xe2eb8742ddce4cdd), + Felt::new_unchecked(0x16f07a5ae66351fd), + Felt::new_unchecked(0xada478edb194ba48), + Felt::new_unchecked(0xa01d9a0643361e47), + Felt::new_unchecked(0xc2783e0dcf7f5d17), ]), Word::new([ - Felt::new(0x67e8ad66c18c6335), - Felt::new(0x1b25e193d55e3a3f), - Felt::new(0x01b5285d164fd026), - Felt::new(0xf32b4a5ebee92d50), + Felt::new_unchecked(0x5110b3efad0c529e), + Felt::new_unchecked(0x48b791b96bf4ea5c), + Felt::new_unchecked(0xabdf56400d294f79), + Felt::new_unchecked(0x724043dce50148bb), ]), Word::new([ - Felt::new(0x87e613a4c555ab74), - Felt::new(0x4a09621b5c561c96), - Felt::new(0x2b64ab15aab3666b), - Felt::new(0xfd04e14a8a0b6c09), + Felt::new_unchecked(0x03e8bffe1e1140ef), + Felt::new_unchecked(0xdc13aa83b5aae1a3), + Felt::new_unchecked(0x778a7c5dc01067b4), + Felt::new_unchecked(0x143d90ce1cde2b69), ]), Word::new([ - Felt::new(0x3f970c2db33f5242), - Felt::new(0x80a3fcb6661723a2), - Felt::new(0xa82ec8a677866efd), - Felt::new(0x5a82e39fdb2313f6), + Felt::new_unchecked(0xffa1b474293b1e6e), + Felt::new_unchecked(0xb6229a9c4d92e06f), + Felt::new_unchecked(0x49d61074cb93984e), + Felt::new_unchecked(0xd8f2e27fa43903fe), ]), Word::new([ - Felt::new(0x2797ceaaa67c8131), - Felt::new(0x26f3c1c666d3c505), - Felt::new(0xb7c0b86783c25696), - Felt::new(0x2280f9f50af34b3a), + Felt::new_unchecked(0xf1c88b3c6963e9c9), + Felt::new_unchecked(0x79b01206f4228f67), + Felt::new_unchecked(0x785331aed09e6417), + Felt::new_unchecked(0x14ebf0052c629ce4), ]), Word::new([ - Felt::new(0x4bc36a52bdf92a1a), - Felt::new(0x25af7d67f1426927), - Felt::new(0x78d32e715406b40d), - Felt::new(0xf5bd9122df7f8678), + Felt::new_unchecked(0x9e711763920a39ec), + Felt::new_unchecked(0x811261ad01af5ecc), + Felt::new_unchecked(0xe3b9780ef2bc32ef), + Felt::new_unchecked(0x6d8607b24c9a0fe1), ]), Word::new([ - Felt::new(0xf688c0c29f9437fa), - Felt::new(0x1f81eebf8878f6d4), - Felt::new(0x186a6d4490941249), - Felt::new(0xf63cda0d6f269065), + Felt::new_unchecked(0x4627a6abb9778b76), + Felt::new_unchecked(0xbb8f08e059cd41ae), + Felt::new_unchecked(0x8c7714f9a4607cf6), + Felt::new_unchecked(0xf0cff546e3ef1715), ]), Word::new([ - Felt::new(0x84f3d1ebcfaa1562), - Felt::new(0x9cce9d6adbb76865), - Felt::new(0x94fa01600dd11898), - Felt::new(0xd5458829b4130256), + Felt::new_unchecked(0xff3f03af9dc9b872), + Felt::new_unchecked(0xb97da931b2f5699e), + Felt::new_unchecked(0xc7fde13a1dacb964), + Felt::new_unchecked(0x36921c1f64e0be45), ]), Word::new([ - Felt::new(0x16a747be6063b1b5), - Felt::new(0xb9996c5b744aedbf), - Felt::new(0x8e8e4acaea70053c), - Felt::new(0xa7282c670831d43b), + Felt::new_unchecked(0x2215a5523302825b), + Felt::new_unchecked(0xff666312178e368d), + Felt::new_unchecked(0x90d132b0e5eeb0c8), + Felt::new_unchecked(0xfc4f22d00d823840), ]), Word::new([ - Felt::new(0x483ee342c011f7f9), - Felt::new(0x5c0c9fe86bc86b67), - Felt::new(0x4cddd40605793509), - Felt::new(0x381429922edcb020), + Felt::new_unchecked(0x7fe036297012e736), + Felt::new_unchecked(0x3b6c20423571b6f9), + Felt::new_unchecked(0x9df39bfa1dddd86d), + Felt::new_unchecked(0xd1f22c6f91ffd626), ]), Word::new([ - Felt::new(0xa59ed90c1ca60698), - Felt::new(0x09981a7f81544b7b), - Felt::new(0xf953c75c139fdc85), - Felt::new(0x260fb0632f3efab2), + Felt::new_unchecked(0xe4ce0c7900d5c400), + Felt::new_unchecked(0xdb50cd5902dbc524), + Felt::new_unchecked(0x19c5108d705b5d6e), + Felt::new_unchecked(0x0a5edbcf5f072095), ]), Word::new([ - Felt::new(0x4afefea9330f4c0e), - Felt::new(0x466621aa41ab021a), - Felt::new(0x1bdc06a9a25d619d), - Felt::new(0xd6e857e49b7cbfd4), + Felt::new_unchecked(0xa8635e8441c074d9), + Felt::new_unchecked(0x4d4a382fc9e24e90), + Felt::new_unchecked(0xa3a4f1e2d230e956), + Felt::new_unchecked(0xea9550e2f96b025c), ]), Word::new([ - Felt::new(0x25b3e8478af5493e), - Felt::new(0x0508193caa22a375), - Felt::new(0xc6261bed917be14b), - Felt::new(0xe03bea63fb71cbf5), + Felt::new_unchecked(0x85186edcf25307d3), + Felt::new_unchecked(0xdc41a7269053ae80), + Felt::new_unchecked(0x93af4e65c4adf29e), + Felt::new_unchecked(0x78ea5f3bd2b45729), ]), Word::new([ - Felt::new(0x8ebca1d27c35d685), - Felt::new(0x8fbf4f9ab6b18930), - Felt::new(0x21c8ff832ef2b4b6), - Felt::new(0x53a3363026b09a01), + Felt::new_unchecked(0x26e561b2419831eb), + Felt::new_unchecked(0x7f135d5f29089733), + Felt::new_unchecked(0x3850c48388c33f2d), + Felt::new_unchecked(0x72ac40cb157ae2fc), ]), Word::new([ - Felt::new(0x57253100597ad1ec), - Felt::new(0x9e3ad735bac054aa), - Felt::new(0x84bea83f5aff0950), - Felt::new(0x6cad0be6ccd7cf55), + Felt::new_unchecked(0xaf0446459af542b6), + Felt::new_unchecked(0x8b80ece90ad8d3d9), + Felt::new_unchecked(0x06dd02781b0fcb5c), + Felt::new_unchecked(0x241b136a83ec8e36), ]), Word::new([ - Felt::new(0x8abc91c5bfc02ecc), - Felt::new(0xaa0126e4c41988c1), - Felt::new(0xc9c83cfb8f4e08f2), - Felt::new(0x7d25ae8c2a18f4f2), + Felt::new_unchecked(0x7360e118d7c4c44b), + Felt::new_unchecked(0x85e005756a549eaa), + Felt::new_unchecked(0x94366e7d3ee2ca48), + Felt::new_unchecked(0x24a184b27fa785c6), ]), Word::new([ - Felt::new(0x5a9ccf2cb7b0993a), - Felt::new(0x20e0cc8e09a5584d), - Felt::new(0xccb156feb1d4b9b5), - Felt::new(0x0f3f16b2d1be0575), + Felt::new_unchecked(0x9cb5db6f8ba877f4), + Felt::new_unchecked(0x432d1947a9c888d0), + Felt::new_unchecked(0x6d27e4d577889f05), + Felt::new_unchecked(0x25dc7800934ef1ae), ]), Word::new([ - Felt::new(0xde12ff9ef674c614), - Felt::new(0x74c16547bbf8260c), - Felt::new(0xacc3eae06a9aef55), - Felt::new(0x2a2d94e77b6189c2), + Felt::new_unchecked(0x79100c8b724fb432), + Felt::new_unchecked(0xbff994b931dfe75b), + Felt::new_unchecked(0x27953506d29f90f9), + Felt::new_unchecked(0x90cc228b50167ce1), ]), Word::new([ - Felt::new(0x2fa228aeb96f2623), - Felt::new(0xf8a57adfcdccf641), - Felt::new(0xbf552b92065ac157), - Felt::new(0x5130f01fc96b62e2), + Felt::new_unchecked(0xf0a48a5fc2e1d9db), + Felt::new_unchecked(0x5c54aad80cc50dd9), + Felt::new_unchecked(0x13cad4ffa16a608c), + Felt::new_unchecked(0x7159e79c02364b4d), ]), Word::new([ - Felt::new(0x1bdc44006adb1698), - Felt::new(0x5151ca32eddac5d1), - Felt::new(0xfd72f666343f784c), - Felt::new(0xfd132ba4bb738826), + Felt::new_unchecked(0x2f1fa97d85531c87), + Felt::new_unchecked(0x5c0ec79fe63e1fb1), + Felt::new_unchecked(0x767e71a7fe617d25), + Felt::new_unchecked(0xa460b484a64a72d6), ]), Word::new([ - Felt::new(0xdd45068ab899fa71), - Felt::new(0x1367ac815e888410), - Felt::new(0x1a800820d67e7e55), - Felt::new(0x9960bb8fd886e2ae), + Felt::new_unchecked(0xd095e4e7ddbe11d9), + Felt::new_unchecked(0x453d9ac44b09a471), + Felt::new_unchecked(0x182494a14d004713), + Felt::new_unchecked(0xbc44a57e6559a878), ]), Word::new([ - Felt::new(0x802517284c772d85), - Felt::new(0x5c962d1d0a4e1c25), - Felt::new(0x08f61eebbe4fbe76), - Felt::new(0x85228c63c7a49efd), + Felt::new_unchecked(0xaff03a0d76dbf96e), + Felt::new_unchecked(0x47f6ce8ec92e94d5), + Felt::new_unchecked(0xf798359c1c4a03af), + Felt::new_unchecked(0x452903ccd4a9491c), ]), Word::new([ - Felt::new(0x9cb407455c288be8), - Felt::new(0x2e1043b2cc13875c), - Felt::new(0x8f3872b43eb40cb9), - Felt::new(0x2f5298b8d703c092), + Felt::new_unchecked(0x732e5861313fb854), + Felt::new_unchecked(0x272125f78566ed70), + Felt::new_unchecked(0xae9292d495ebd45a), + Felt::new_unchecked(0x86bdeb0a07d26c1d), ]), Word::new([ - Felt::new(0xf9b409b5dfa20237), - Felt::new(0x509cdf91d771c4c0), - Felt::new(0xa03399ef00003e9a), - Felt::new(0x137fee82f099bfdc), + Felt::new_unchecked(0x85204c664906b68a), + Felt::new_unchecked(0x9ad6a41ac55c8ca6), + Felt::new_unchecked(0x78ed8975fc245b38), + Felt::new_unchecked(0x04abc3716c66d8c7), ]), Word::new([ - Felt::new(0x6200c7bc52786a84), - Felt::new(0x07e208e9a6d8297b), - Felt::new(0x6d88fc6e7017c6ad), - Felt::new(0xb5c8fdacfa860fde), + Felt::new_unchecked(0x9147153b745abe81), + Felt::new_unchecked(0x335727d370b2952c), + Felt::new_unchecked(0xb998783bb0e788d4), + Felt::new_unchecked(0xd0b2538a8693485c), ]), Word::new([ - Felt::new(0x3fa86b8fc6371912), - Felt::new(0x4babf426ae37d40f), - Felt::new(0xd8ce6543bd8b9bbb), - Felt::new(0x47a7cb0b6b6a242d), + Felt::new_unchecked(0x293bf549fe01832a), + Felt::new_unchecked(0x3c2fcbf24b3df870), + Felt::new_unchecked(0xd89e2f0981e50703), + Felt::new_unchecked(0x923c1c2a5c86b530), ]), Word::new([ - Felt::new(0x8bdb240e415d744a), - Felt::new(0x35aaf2b9ecae3ebc), - Felt::new(0xcfdc0a1b426abeb3), - Felt::new(0x592fee4621dcc2bf), + Felt::new_unchecked(0x420f112e6e401f4c), + Felt::new_unchecked(0x56253ed049ca9ffc), + Felt::new_unchecked(0x23de3e2ac1d7c0f0), + Felt::new_unchecked(0xd915d95eebde33ad), ]), Word::new([ - Felt::new(0x71f086dc9626aa9d), - Felt::new(0xab58f57b141c9806), - Felt::new(0x7f47c3feea78e6a1), - Felt::new(0xf8ade27ff44ef11c), + Felt::new_unchecked(0x0ce48bc6b7f60997), + Felt::new_unchecked(0x14c1f28114ed20dc), + Felt::new_unchecked(0xc9b631257e9360f2), + Felt::new_unchecked(0x0dbb92b86a787d22), ]), Word::new([ - Felt::new(0x50ee5035b0d85667), - Felt::new(0x1bdcf2544d9cb2a7), - Felt::new(0x3ee4c339793a054b), - Felt::new(0x9e788acc61d1b6e8), + Felt::new_unchecked(0xa6090168b2137a1d), + Felt::new_unchecked(0x1704a3d034139f9a), + Felt::new_unchecked(0x59e95cc0e9d888e5), + Felt::new_unchecked(0x9d3399e08765af8d), ]), Word::new([ - Felt::new(0x37650cb60969e966), - Felt::new(0xfe9247256673163a), - Felt::new(0x240611a46d8024a7), - Felt::new(0x50496ee909404115), + Felt::new_unchecked(0xffdee1cf2986e9f7), + Felt::new_unchecked(0x5397b903c6b5ebda), + Felt::new_unchecked(0x1e43c3fcd46bc842), + Felt::new_unchecked(0x7ce3b02f30ce57fa), ]), Word::new([ - Felt::new(0x95f0de9ec639c51a), - Felt::new(0xc900fb33ff6c757b), - Felt::new(0x2ec20d0eaa39a048), - Felt::new(0x14362711cd47de96), + Felt::new_unchecked(0x47b832abef714aee), + Felt::new_unchecked(0xfffb987574d18093), + Felt::new_unchecked(0xe179cd82ceda459d), + Felt::new_unchecked(0xd8ab8964cbb76761), ]), Word::new([ - Felt::new(0x6d7ce36fe1803e4b), - Felt::new(0xca4b3ee42416ba15), - Felt::new(0xaa8f0737e9fc1f07), - Felt::new(0x5b726c41400665ca), + Felt::new_unchecked(0x5f262e1313906e57), + Felt::new_unchecked(0xead7d293a5ec34f7), + Felt::new_unchecked(0x71f00b646fe16b05), + Felt::new_unchecked(0x369383554b95ccff), ]), Word::new([ - Felt::new(0x7be99fbb68e1bc97), - Felt::new(0x2bb479bb52c6426e), - Felt::new(0x454d3aea7c6d06a7), - Felt::new(0xcd613f3f9841a48f), + Felt::new_unchecked(0x33afc010464d552c), + Felt::new_unchecked(0xbb3668a62b613767), + Felt::new_unchecked(0xe2f75b19d35b5ba5), + Felt::new_unchecked(0xc12ec8ce30145a34), ]), Word::new([ - Felt::new(0x4b4832c38acc0b61), - Felt::new(0x1ff6eba8026046a5), - Felt::new(0xab74b76254ac5c8c), - Felt::new(0x29cac392e4f3cc7f), + Felt::new_unchecked(0xd9a77ee302a7ef62), + Felt::new_unchecked(0xd430d0b64b15a046), + Felt::new_unchecked(0x3c703c28c36b480b), + Felt::new_unchecked(0xbbf1835b1712cf20), ]), Word::new([ - Felt::new(0x01fdbfd0b87f2e09), - Felt::new(0x4e074d6a472fd5b6), - Felt::new(0x628d930e44c871c8), - Felt::new(0x5a3e619eaed03283), + Felt::new_unchecked(0x8fd394d77b23871f), + Felt::new_unchecked(0x6d5f95be96bb7250), + Felt::new_unchecked(0x2e0fbd527f36cb80), + Felt::new_unchecked(0xc93f3d342bf426a2), ]), Word::new([ - Felt::new(0x8c19096097e3df12), - Felt::new(0xe8857578a1bdbb8c), - Felt::new(0x50689e0cb310f595), - Felt::new(0xbe1742c30b7e11a2), + Felt::new_unchecked(0x7e5bf5ee62826fa9), + Felt::new_unchecked(0x20e6c3a52f775fae), + Felt::new_unchecked(0x4eb2d1abb33e904d), + Felt::new_unchecked(0x72e1209fa99f708f), ]), Word::new([ - Felt::new(0x9fd9929dfd838547), - Felt::new(0x59ad7ecd6f8561e9), - Felt::new(0x9be7ed4a7d558164), - Felt::new(0x3cc5e790310cf87e), + Felt::new_unchecked(0xd86e6262761401e3), + Felt::new_unchecked(0x44e2113d07886377), + Felt::new_unchecked(0x3da5d19b1351a070), + Felt::new_unchecked(0xfd145bfce40317cf), ]), Word::new([ - Felt::new(0x5988ffa09e5dc662), - Felt::new(0xdf77e0b8bed90f25), - Felt::new(0x96f3e807bf0af8ee), - Felt::new(0x9efc143168113a8d), + Felt::new_unchecked(0xf1de7bc452f8a5e4), + Felt::new_unchecked(0xdfe809b30c00a82f), + Felt::new_unchecked(0x5401374848fa780c), + Felt::new_unchecked(0xb7772cc983cd0f1c), ]), Word::new([ - Felt::new(0xf03f6198aa069685), - Felt::new(0x862768943cc33dfe), - Felt::new(0x79fd7dce8353cc21), - Felt::new(0xa70883055a67410b), + Felt::new_unchecked(0x842bed45416a5375), + Felt::new_unchecked(0x5178af9038c0d528), + Felt::new_unchecked(0x00b52dad36b57a36), + Felt::new_unchecked(0xb182d8bcd3f318aa), ]), Word::new([ - Felt::new(0x958be73cd7b9b636), - Felt::new(0xda82710cf709d664), - Felt::new(0xfcc735123f454965), - Felt::new(0x1db6d0ca80ef235b), + Felt::new_unchecked(0xdffaab3f6934e1b4), + Felt::new_unchecked(0xddd03d1890a3b637), + Felt::new_unchecked(0xc9aa99613dce13af), + Felt::new_unchecked(0x70a96801d20b9bbf), ]), Word::new([ - Felt::new(0xd986e1c34a8e7e0a), - Felt::new(0xc8bc3f0f6d634738), - Felt::new(0x59c01743c8c45a1e), - Felt::new(0x30f70c57c98be4e1), + Felt::new_unchecked(0xa1800d8a2337584f), + Felt::new_unchecked(0x1fd72730d2609dbb), + Felt::new_unchecked(0x01a2a4b509765b2a), + Felt::new_unchecked(0x6e99776f568dfe7c), ]), Word::new([ - Felt::new(0xfa731977e87b88cb), - Felt::new(0x0211fc99064cf66d), - Felt::new(0x62f91f0452839651), - Felt::new(0xfd0459b3324291eb), + Felt::new_unchecked(0x05ad4de4955a46eb), + Felt::new_unchecked(0xdbdd6949031e33c7), + Felt::new_unchecked(0x8a7df981d720ff61), + Felt::new_unchecked(0x447325552a7235e7), ]), Word::new([ - Felt::new(0xdf34097258613c7f), - Felt::new(0x92777ff51538e1c1), - Felt::new(0x5d38328f0ab20b20), - Felt::new(0xea5bad665b2bdb69), + Felt::new_unchecked(0x5322892e1842df01), + Felt::new_unchecked(0x3841a93cfac9660e), + Felt::new_unchecked(0x5e26b5de4a4e3d37), + Felt::new_unchecked(0x388836b50d100120), ]), Word::new([ - Felt::new(0xce9415c1103d41c8), - Felt::new(0x33e56c7b98737652), - Felt::new(0x98cf8572aadbb142), - Felt::new(0x8cdd8dd5f33db9be), + Felt::new_unchecked(0xcce89e91d179eaec), + Felt::new_unchecked(0xe68ce121dcab4d44), + Felt::new_unchecked(0x0d56cbe0ace2d4ec), + Felt::new_unchecked(0x2329986c74293ce5), ]), Word::new([ - Felt::new(0xbad69843fd27a859), - Felt::new(0x1f01e096692c3c59), - Felt::new(0x5bfccc8353013d54), - Felt::new(0x273d211cdce77fb8), + Felt::new_unchecked(0x12358aee10ffa5fc), + Felt::new_unchecked(0xbe7a6293921468e6), + Felt::new_unchecked(0xe15970cc955d864b), + Felt::new_unchecked(0xbf37f6ae3e331aab), ]), Word::new([ - Felt::new(0x0ae0e050c52d9381), - Felt::new(0xec0d7b0182f942c9), - Felt::new(0x167d0024d0e180c2), - Felt::new(0xc3a4758a8e8ff5cb), + Felt::new_unchecked(0xb5f111968b241e6e), + Felt::new_unchecked(0x7cc928c1c338b4ad), + Felt::new_unchecked(0x26dde0cdcb31d42c), + Felt::new_unchecked(0xd0c319d0f0aa9512), ]), Word::new([ - Felt::new(0xb6c9793eaadcbc77), - Felt::new(0x44f0017595bfe256), - Felt::new(0xec78adf967a3214a), - Felt::new(0x4e3aa60340d68e65), + Felt::new_unchecked(0x87077ff16b8f3d8e), + Felt::new_unchecked(0x4e6be6654ec08848), + Felt::new_unchecked(0x71a9d5e1e3ab9085), + Felt::new_unchecked(0xf6bd087b7508d595), ]), Word::new([ - Felt::new(0x3faa82b5fbd1f467), - Felt::new(0xbce0c8978c146e9c), - Felt::new(0x7f47470c22e93fd4), - Felt::new(0x3fb93441b389d389), + Felt::new_unchecked(0x53dc74b301a7036e), + Felt::new_unchecked(0x4a431b1485944c51), + Felt::new_unchecked(0x50b33f4d61724832), + Felt::new_unchecked(0xda5f40612948fb0b), ]), Word::new([ - Felt::new(0x7193f11b620b1a0d), - Felt::new(0x49b062199d57ebe6), - Felt::new(0x0a31c04000656106), - Felt::new(0xaf7d7a07a6eb73e7), + Felt::new_unchecked(0x95c6101469bf6744), + Felt::new_unchecked(0x1a9980d26ef6569f), + Felt::new_unchecked(0x4613db415a103854), + Felt::new_unchecked(0x4107edef06f4b2e2), ]), Word::new([ - Felt::new(0xbfc8f3debeaaccb9), - Felt::new(0x027bb07371639273), - Felt::new(0xf8cd94ccd8829c21), - Felt::new(0x0d698f9d65d14061), + Felt::new_unchecked(0xdd4af77853f150b5), + Felt::new_unchecked(0x08eecf1e24bdeb59), + Felt::new_unchecked(0x32aef82f2bffd372), + Felt::new_unchecked(0x9b05edc6cca613c6), ]), Word::new([ - Felt::new(0x9bacc29ba7a25d59), - Felt::new(0x1168877e510ed38d), - Felt::new(0xa37e780648f00d11), - Felt::new(0xdd1419b32e808e01), + Felt::new_unchecked(0xc9925f2d3619d2f4), + Felt::new_unchecked(0x4facfbe75c1407ff), + Felt::new_unchecked(0x9862025a201309e9), + Felt::new_unchecked(0x71c2179aa2551d12), ]), Word::new([ - Felt::new(0x686e16bf96e0dffa), - Felt::new(0x2a66ea7ed019ac55), - Felt::new(0x4a13653de99f10ae), - Felt::new(0x90891fb044c67756), + Felt::new_unchecked(0xa0e3480ba8ac4ccb), + Felt::new_unchecked(0x2dad4a92bf7c2476), + Felt::new_unchecked(0xbb5c9dbd61176a1a), + Felt::new_unchecked(0xabfd55e2a7e15949), ]), Word::new([ - Felt::new(0xf7f4c880706aec96), - Felt::new(0xb962efabc033df77), - Felt::new(0x14f21eb75e04e815), - Felt::new(0xf6079d7fb6ee39f8), + Felt::new_unchecked(0x4553b43fba664b10), + Felt::new_unchecked(0xbdbd5f8d304338ad), + Felt::new_unchecked(0xa877020ae3ba30dd), + Felt::new_unchecked(0x7705853732414d3e), ]), Word::new([ - Felt::new(0xae3145790a30ba67), - Felt::new(0x6b7115acd8d16e1e), - Felt::new(0xec9651fea505b477), - Felt::new(0x341ebac3f7cd1a77), + Felt::new_unchecked(0x1af7a763a925fb31), + Felt::new_unchecked(0xabd019c3cd9cef0a), + Felt::new_unchecked(0x9798360eb35e9915), + Felt::new_unchecked(0xfa854bee8e192cc6), ]), Word::new([ - Felt::new(0xa7a693e65d7b485a), - Felt::new(0x07ecc806fb91d03b), - Felt::new(0x9925566ced31f668), - Felt::new(0x3d6cf66a05ad36d1), + Felt::new_unchecked(0x1c0219a536aa2654), + Felt::new_unchecked(0xab431a3352c51fff), + Felt::new_unchecked(0x64c431d78d5e7bf4), + Felt::new_unchecked(0xafb35aa8b2696229), ]), Word::new([ - Felt::new(0x310efab3f050e9cc), - Felt::new(0x6907608ddc657b5b), - Felt::new(0x31ee000b84e51290), - Felt::new(0x06ce4e8ef44a412a), + Felt::new_unchecked(0x947321f3ed487702), + Felt::new_unchecked(0xf7887258489ec613), + Felt::new_unchecked(0xbcfef077528b8ee1), + Felt::new_unchecked(0xa2d7cf3ec671bd26), ]), Word::new([ - Felt::new(0x5cffd24924aca24d), - Felt::new(0xe0f4f6bc74efc000), - Felt::new(0x81ffffb283f2091b), - Felt::new(0xad216176fa87fd9f), + Felt::new_unchecked(0xb71476ee7dfbf7be), + Felt::new_unchecked(0x159c699add5d8b81), + Felt::new_unchecked(0x3aa0817318acd24f), + Felt::new_unchecked(0x0002ba5068fbf7b7), ]), Word::new([ - Felt::new(0xdf7d4f7042a35716), - Felt::new(0x2efb3ba83af345b4), - Felt::new(0x382b6f6943ae6efd), - Felt::new(0x4953f27954478314), + Felt::new_unchecked(0xd687e4208f4c1da8), + Felt::new_unchecked(0x616d2ae62f438ddb), + Felt::new_unchecked(0x19afddf13d62ab5a), + Felt::new_unchecked(0xedb19ff512b83f28), ]), Word::new([ - Felt::new(0x87d503581afe38a7), - Felt::new(0x5cc6c0d05007c364), - Felt::new(0xf7142e232a4ebe1c), - Felt::new(0x5cb6382efbaa7b04), + Felt::new_unchecked(0x7b016fcc9c1e70ba), + Felt::new_unchecked(0x2c7b452f73a3bffc), + Felt::new_unchecked(0xec5a0392306813d7), + Felt::new_unchecked(0xae99a69220be5139), ]), Word::new([ - Felt::new(0x1b694df14813c23d), - Felt::new(0x018501129fd167ae), - Felt::new(0x16836cb666963beb), - Felt::new(0x9ccefcb3c111d2c1), + Felt::new_unchecked(0x0b197925db766537), + Felt::new_unchecked(0xfe666f05cdcf50e0), + Felt::new_unchecked(0xec34f13b232d627e), + Felt::new_unchecked(0xf41f55a319b54d35), ]), Word::new([ - Felt::new(0xc9aa885f6b1eebca), - Felt::new(0xc8ed5b8d25810804), - Felt::new(0xd8219dbbcc7da1e7), - Felt::new(0x337431a1eda8563f), + Felt::new_unchecked(0x0e73225571eb96e2), + Felt::new_unchecked(0x8df169b862333236), + Felt::new_unchecked(0x3f66b218ba56d6a7), + Felt::new_unchecked(0x21b48323f2629c74), ]), Word::new([ - Felt::new(0x31eba37c60bd1682), - Felt::new(0x6eca92986b0d2b8c), - Felt::new(0xbbf6f1248083a544), - Felt::new(0x384c22650b3be05a), + Felt::new_unchecked(0x2dab8971e1f636ea), + Felt::new_unchecked(0xf1a61cb7873dd513), + Felt::new_unchecked(0x24ce44bee61d79b4), + Felt::new_unchecked(0xe85ec3af93cfd356), ]), Word::new([ - Felt::new(0xa08dba24fe1fc506), - Felt::new(0x63bf0cea699af3b1), - Felt::new(0x738d154ea773b70f), - Felt::new(0x1f15fbc12a759dc1), + Felt::new_unchecked(0x372b613b73452a2d), + Felt::new_unchecked(0x2326929e7c83670c), + Felt::new_unchecked(0x24a6024d88efd716), + Felt::new_unchecked(0x9ada032226ee8f41), ]), Word::new([ - Felt::new(0x6ecb8966d3e3a1d2), - Felt::new(0x4ffe6eef4bde94c9), - Felt::new(0x4154d42d2951e5e1), - Felt::new(0x716b1d675b13f7c9), + Felt::new_unchecked(0x70e5fa8e228fe225), + Felt::new_unchecked(0x3fb83ff7de835a4c), + Felt::new_unchecked(0xba460412906dd260), + Felt::new_unchecked(0xb9af693a54538497), ]), Word::new([ - Felt::new(0x6b0b5916ff352bf4), - Felt::new(0xc7c47cffcf547b8a), - Felt::new(0x69ef74458e4f6d09), - Felt::new(0x94a5991fb68d83a6), + Felt::new_unchecked(0x8b4c559f5a1d954d), + Felt::new_unchecked(0x609f0803e2108430), + Felt::new_unchecked(0xc2bc9c612459417a), + Felt::new_unchecked(0x20c0c2fb3c4d3adc), ]), Word::new([ - Felt::new(0x6cdcb02959571505), - Felt::new(0x207b77cfbe672a42), - Felt::new(0xbdd76d575b1bf415), - Felt::new(0xfd2829a667bc6610), + Felt::new_unchecked(0x4480f11ea7c0eff0), + Felt::new_unchecked(0xfb5f2a5816b0b191), + Felt::new_unchecked(0x741a8e48d77b5c1f), + Felt::new_unchecked(0xee4de2c193b44fbc), ]), Word::new([ - Felt::new(0x27e655c213ab7557), - Felt::new(0xc498abff9684e414), - Felt::new(0xabd9b1f29e46169e), - Felt::new(0x2c3334a6d114ad33), + Felt::new_unchecked(0x77fc4d4d2f5c78bd), + Felt::new_unchecked(0xb73c33d13ae1a81f), + Felt::new_unchecked(0xb77676f6190217ad), + Felt::new_unchecked(0x4a6e3dce800ea807), ]), Word::new([ - Felt::new(0x2c4d010b6cdb496b), - Felt::new(0xa2c41e518763d815), - Felt::new(0x06d40f064444d6e6), - Felt::new(0x3ae548cb8d057afd), + Felt::new_unchecked(0x4d0c2f131ed8fdcb), + Felt::new_unchecked(0xa93c45c839d9d744), + Felt::new_unchecked(0x02099f58db023eb2), + Felt::new_unchecked(0x51f830a886d83c68), ]), Word::new([ - Felt::new(0x737e01ec9910cfda), - Felt::new(0xa2c941998d808f61), - Felt::new(0xdd6b4a8029796504), - Felt::new(0x2f68cfed183ccc34), + Felt::new_unchecked(0x49969ad712828350), + Felt::new_unchecked(0x43fbaea306d8503a), + Felt::new_unchecked(0xb29e4a5c3b708c23), + Felt::new_unchecked(0x996982a21d90c3a8), ]), Word::new([ - Felt::new(0x23f669d9ce07f077), - Felt::new(0x6f3dc43dc02c6f1d), - Felt::new(0xf3f1e3541c6117c7), - Felt::new(0x28a571e36abbaace), + Felt::new_unchecked(0x459ee8bb2709673f), + Felt::new_unchecked(0xa65a9f6a0774863d), + Felt::new_unchecked(0x35069377f2cd8aa6), + Felt::new_unchecked(0xdade69a1b1f0ed54), ]), Word::new([ - Felt::new(0x6da85ec3f33ba133), - Felt::new(0x252355a98a6cd911), - Felt::new(0x63b15077f053f4bc), - Felt::new(0x1ae62ec16fd7fdbc), + Felt::new_unchecked(0xba6232a69b2193e8), + Felt::new_unchecked(0xacb491cbe92045f3), + Felt::new_unchecked(0xb62f47a82bddccb0), + Felt::new_unchecked(0xf7eb0e2318aa52ef), ]), Word::new([ - Felt::new(0xf70f5db5aff03727), - Felt::new(0xc65b599e71d7caaa), - Felt::new(0x6044114442d565d2), - Felt::new(0x62ea7655d3d0c330), + Felt::new_unchecked(0x8816f48f1e32ea78), + Felt::new_unchecked(0xf8be81a5ff3885fd), + Felt::new_unchecked(0x78c2ebe575085888), + Felt::new_unchecked(0xf40367a41cc0f59f), ]), Word::new([ - Felt::new(0x0866c3e684c9e35f), - Felt::new(0x0f33c50c313e5bfa), - Felt::new(0x6dcf72e37d2dcca3), - Felt::new(0x3704807cd3c6de92), + Felt::new_unchecked(0xfb488e200bf9ca63), + Felt::new_unchecked(0x8f8f4d7278d0e78c), + Felt::new_unchecked(0x09b208e717282c66), + Felt::new_unchecked(0xc876de43c913a66c), ]), Word::new([ - Felt::new(0x2c417271018b4372), - Felt::new(0xf4d10ace12df9588), - Felt::new(0xcbdf0a4fb229b7c1), - Felt::new(0xcf5a4c3002e65541), + Felt::new_unchecked(0x877130417c2e5f36), + Felt::new_unchecked(0xd50485c08bd277e7), + Felt::new_unchecked(0xd0945268a2883728), + Felt::new_unchecked(0x718d24bc8c73f8f7), ]), Word::new([ - Felt::new(0xcfaac25cac74c895), - Felt::new(0xc9af6f14973154e6), - Felt::new(0x8b6041d1a7fbb3c0), - Felt::new(0x2fd4d6b578efee90), + Felt::new_unchecked(0x2505d387c8fdb218), + Felt::new_unchecked(0x92233caaab007401), + Felt::new_unchecked(0x11e6575e5cc339ed), + Felt::new_unchecked(0xf43bcccacfb04389), ]), Word::new([ - Felt::new(0x7ecbfb9c2c47516a), - Felt::new(0x53aee5487ab0c680), - Felt::new(0xac92711629c37a75), - Felt::new(0x2945f4166fd590c7), + Felt::new_unchecked(0x3e60e99a73b1614d), + Felt::new_unchecked(0xd964d5b908f4d69c), + Felt::new_unchecked(0x435ce6a2a2a1b505), + Felt::new_unchecked(0x5e99c0a0a9499af4), ]), Word::new([ - Felt::new(0xc67642914b052720), - Felt::new(0x91d323ce00bf1118), - Felt::new(0xdcb6c341847197f9), - Felt::new(0x3715874d9a4157ff), + Felt::new_unchecked(0x2f6395587ab70784), + Felt::new_unchecked(0xcc979c15c9b0f084), + Felt::new_unchecked(0x51af7bedd42eea2d), + Felt::new_unchecked(0x36a4868b22b43bc3), ]), Word::new([ - Felt::new(0x018dc9a4b47d35a4), - Felt::new(0x84578f992a4b57ae), - Felt::new(0x53dba2deb351f882), - Felt::new(0xeeb3a4e41e19d3ac), + Felt::new_unchecked(0x6a86ad5c47406d72), + Felt::new_unchecked(0xbb2ed4c37164785a), + Felt::new_unchecked(0x2381850a1cac59bd), + Felt::new_unchecked(0x080ccb801113a2ac), ]), Word::new([ - Felt::new(0x19f3ddef203b4eaa), - Felt::new(0xc330bc99572590b7), - Felt::new(0xc40ce83bfbf50a51), - Felt::new(0x88ae15846aa8c47e), + Felt::new_unchecked(0x2d18d649a43a5732), + Felt::new_unchecked(0xd110805fc5adda4b), + Felt::new_unchecked(0xae9472d633a0a594), + Felt::new_unchecked(0xa2eea0294cd6000c), ]), Word::new([ - Felt::new(0xea95b07de74db84c), - Felt::new(0x25f24c92bca74eec), - Felt::new(0x6adbf8918405066e), - Felt::new(0x3598a521e9dbc12f), + Felt::new_unchecked(0x8d19318f37b7ada1), + Felt::new_unchecked(0xd6086bb2699993a2), + Felt::new_unchecked(0xc9f0ea948bd6fb17), + Felt::new_unchecked(0x6a4b12267d002adb), ]), Word::new([ - Felt::new(0xd2b485237dc87035), - Felt::new(0x9b94fcefe9761871), - Felt::new(0x86b1fc36fb33059f), - Felt::new(0xd9a01c3d524a1f64), + Felt::new_unchecked(0x58e4e9ced3ba344f), + Felt::new_unchecked(0x4b764faa2ffb83d5), + Felt::new_unchecked(0xadd9ea03fd82865e), + Felt::new_unchecked(0x36a7d0fd5ce7128b), ]), Word::new([ - Felt::new(0x6bb88e64acf6e6d3), - Felt::new(0xd6218cb38e359a29), - Felt::new(0xa90945f8948f466e), - Felt::new(0xb5f232e3e5b6638d), + Felt::new_unchecked(0xb9e425390904a962), + Felt::new_unchecked(0x6248e00cf596f6ad), + Felt::new_unchecked(0x5bf008948a2a3a8c), + Felt::new_unchecked(0xd0a2993b00962d0d), ]), Word::new([ - Felt::new(0x18f9825adc30d124), - Felt::new(0x7badbe0ccb1bdf7c), - Felt::new(0x26734fb973862f42), - Felt::new(0x48cb3aa280a1b610), + Felt::new_unchecked(0x85eeb4a3202d1865), + Felt::new_unchecked(0xd16cf9ef8b96ef19), + Felt::new_unchecked(0x24bda888285c7727), + Felt::new_unchecked(0x4f3196ef8aa475cf), ]), Word::new([ - Felt::new(0x5a242e66851f3ce1), - Felt::new(0x46723ee9a9637f99), - Felt::new(0xaca5e19be991e988), - Felt::new(0xe6be5ec090670666), + Felt::new_unchecked(0x2e3905a28ab152c3), + Felt::new_unchecked(0x3d65c64fc03e1154), + Felt::new_unchecked(0x463924157589e96a), + Felt::new_unchecked(0xc35e18d4a80c7857), ]), Word::new([ - Felt::new(0xd3cac74e1c73ae96), - Felt::new(0x571f9c148f6c3a29), - Felt::new(0x3aab030fa1c17103), - Felt::new(0x12c01509ded56d2e), + Felt::new_unchecked(0x0e0dd8d6d834bfb0), + Felt::new_unchecked(0x258cea707ab121ae), + Felt::new_unchecked(0x8a3fe0de0f960a8a), + Felt::new_unchecked(0x5effb1253cd26baf), ]), Word::new([ - Felt::new(0xfa0211fb143cffac), - Felt::new(0xc4bc02a6e706d164), - Felt::new(0xcd4d2f875acf7384), - Felt::new(0xe1a028540085477e), + Felt::new_unchecked(0xb8ed0f83a58af02d), + Felt::new_unchecked(0x57ada4648830369e), + Felt::new_unchecked(0x2705908277842e7d), + Felt::new_unchecked(0x2de8c38841905254), ]), Word::new([ - Felt::new(0x5bee74efb6935aeb), - Felt::new(0xc238b7ef9daf0b79), - Felt::new(0x7a07d0ecad1f8e18), - Felt::new(0x84298e03581c3c73), + Felt::new_unchecked(0x01024b6c34916d38), + Felt::new_unchecked(0xc5d13622ac399a0f), + Felt::new_unchecked(0x1f7ec2453e58755d), + Felt::new_unchecked(0x914696bdc2f6337b), ]), Word::new([ - Felt::new(0xe8eba3dab0f4aaea), - Felt::new(0xe7884a653a1ec6f5), - Felt::new(0x1c46ca905461cef0), - Felt::new(0x8f6511bd67d1d26a), + Felt::new_unchecked(0x302bc4cbe410549f), + Felt::new_unchecked(0x0687682a5bfb498b), + Felt::new_unchecked(0x17f9642a6e1db393), + Felt::new_unchecked(0x7e121da3e183cbbf), ]), Word::new([ - Felt::new(0x9ce806ee666f4c7a), - Felt::new(0xc7756864a0c480f5), - Felt::new(0x8e5a941d0c8bb591), - Felt::new(0x65f92bc3c31cee43), + Felt::new_unchecked(0x301a5478e91a76ae), + Felt::new_unchecked(0x4de2b12e5e088b92), + Felt::new_unchecked(0x51273b62ecd5a68f), + Felt::new_unchecked(0x40279ae1e9935d83), ]), Word::new([ - Felt::new(0xf765992519ace8ec), - Felt::new(0x47f3875ee1e33e14), - Felt::new(0xc693a0f0c39d1f8b), - Felt::new(0x827d37c4becc7c23), + Felt::new_unchecked(0x481559529cca1ff6), + Felt::new_unchecked(0x8996fd99e0263131), + Felt::new_unchecked(0xb51d6c798e0a9618), + Felt::new_unchecked(0x02f201e4b0d84be5), ]), Word::new([ - Felt::new(0xe7841cba02153301), - Felt::new(0xae360d71d1f3f9bf), - Felt::new(0x0f9dc12dedc0b53e), - Felt::new(0x948e2e0f2d0feab9), + Felt::new_unchecked(0x261df526ace9314b), + Felt::new_unchecked(0x49a2b33ae0558f78), + Felt::new_unchecked(0x3e937ac29f1cb809), + Felt::new_unchecked(0xd4e69b8d954d1b83), ]), Word::new([ - Felt::new(0x15074c84d1e8e716), - Felt::new(0xbf8ba357416abc48), - Felt::new(0xc4719d062d5b2bba), - Felt::new(0x64bb509cb47edeb5), + Felt::new_unchecked(0x087aa9955e4d4a36), + Felt::new_unchecked(0x838e53729e90910b), + Felt::new_unchecked(0x779677482e323c3e), + Felt::new_unchecked(0xce7bdd3aa6fdc27c), ]), Word::new([ - Felt::new(0xe318dd6e3b65042a), - Felt::new(0x78787e1d76bef52e), - Felt::new(0x7a81debdfcc7bae7), - Felt::new(0xb345047c9e557153), + Felt::new_unchecked(0x0ad2740bdeede05c), + Felt::new_unchecked(0x4ddeb7bcb67cc62e), + Felt::new_unchecked(0x60ed83e6dac9a793), + Felt::new_unchecked(0xd80bb6bf2da311b4), ]), Word::new([ - Felt::new(0x8d382bf7623c09d2), - Felt::new(0xd26a688e52452b02), - Felt::new(0x1092db1d52000435), - Felt::new(0x08dc1b4e50d6d51a), + Felt::new_unchecked(0xc04a9481f0b29e1e), + Felt::new_unchecked(0x9a481b8ed93d1b77), + Felt::new_unchecked(0xd7e49f5922c9ead3), + Felt::new_unchecked(0x6953a58c463a59d0), ]), Word::new([ - Felt::new(0x16dc8cda8f04358d), - Felt::new(0x838de5cc2f70282c), - Felt::new(0x148f06fdae8ba71e), - Felt::new(0xd9dd7ae619acfdbc), + Felt::new_unchecked(0x67ee9f3c62f968c4), + Felt::new_unchecked(0xf00c80b729593732), + Felt::new_unchecked(0x7fe66eb7c570a24f), + Felt::new_unchecked(0x7ea3069a9803d86c), ]), Word::new([ - Felt::new(0x58b322ddbfbba1af), - Felt::new(0x9398f83ba548e30c), - Felt::new(0xf744167b9d6e6688), - Felt::new(0x14341ceab42a5b83), + Felt::new_unchecked(0x428450f886f972d0), + Felt::new_unchecked(0x4328af23a159c7a3), + Felt::new_unchecked(0x63071f75da7753cc), + Felt::new_unchecked(0x950419829efc0e10), ]), Word::new([ - Felt::new(0xe5ef74f758cc8000), - Felt::new(0xb83f6a02d87211ba), - Felt::new(0x58ae77415137763d), - Felt::new(0x3acb2e367a17ac83), + Felt::new_unchecked(0x1ba95e067e6c0008), + Felt::new_unchecked(0x4a77f23e46b4cddf), + Felt::new_unchecked(0xeb0e31fee924bfb5), + Felt::new_unchecked(0x0c85ab57bd415e0a), ]), Word::new([ - Felt::new(0x153f8bffd4816e8e), - Felt::new(0x904700f5e763ef37), - Felt::new(0x9e31bb0556795cdb), - Felt::new(0x225dbcb0580f60fa), + Felt::new_unchecked(0xcffc94c55d0d6e56), + Felt::new_unchecked(0xe9d3712ac7b68613), + Felt::new_unchecked(0x62480bf5b986f2e3), + Felt::new_unchecked(0x3bcc8d7b5eae8efb), ]), Word::new([ - Felt::new(0x4d3f9f5807ab5065), - Felt::new(0x286b1b5f4ac6bc49), - Felt::new(0xbae4d712df7c5291), - Felt::new(0x69073ff9e5c35fb3), + Felt::new_unchecked(0x775920b95e7970b7), + Felt::new_unchecked(0xf9332431a4cc3253), + Felt::new_unchecked(0xbba433c4d80ec75c), + Felt::new_unchecked(0x3fb3215e800c349d), ]), Word::new([ - Felt::new(0x9d77fcce5377e5f3), - Felt::new(0x0bec32771ab1ba8d), - Felt::new(0x2eda25b78c8b300a), - Felt::new(0x1d158b86263b5ab6), + Felt::new_unchecked(0xa184a7960e40c822), + Felt::new_unchecked(0xe03a49f308042948), + Felt::new_unchecked(0x1749bfbf216b538b), + Felt::new_unchecked(0x26fad80a8da486b1), ]), Word::new([ - Felt::new(0x228fe2fa2e3f5ed1), - Felt::new(0xe8495a53a74f093a), - Felt::new(0x257a753ba2e43af2), - Felt::new(0xfd538fa9da4e0f27), + Felt::new_unchecked(0xbd938e79c6cd6c29), + Felt::new_unchecked(0x2156b7c66b5c09cb), + Felt::new_unchecked(0x73b98936bf8bacd3), + Felt::new_unchecked(0x8ec375b7b5325a23), ]), Word::new([ - Felt::new(0x54d06c74a0a7db7f), - Felt::new(0x6ae8f49e798dc860), - Felt::new(0xdbc80936878e3a79), - Felt::new(0xc50fd000599bc191), + Felt::new_unchecked(0x19a33afa6ed1163f), + Felt::new_unchecked(0x0e814e995a9e1eab), + Felt::new_unchecked(0x43224eebc470703f), + Felt::new_unchecked(0xdc75121ef0b93a68), ]), Word::new([ - Felt::new(0xd868fa65ec044777), - Felt::new(0x5ecc61c057d8f828), - Felt::new(0xb5ffa6a30770c2e7), - Felt::new(0x2a603c07498f29c4), + Felt::new_unchecked(0xdbdb8aa056a94d1f), + Felt::new_unchecked(0x2f56fa5a63f2908f), + Felt::new_unchecked(0x85867da0a35026c3), + Felt::new_unchecked(0xb38df4dd02aa42d6), ]), Word::new([ - Felt::new(0x1eaddbff84d97639), - Felt::new(0x1b5a2ff8d2d97cc1), - Felt::new(0x869825924edef25f), - Felt::new(0xc5461a47f89eb46c), + Felt::new_unchecked(0x3150518851b78b42), + Felt::new_unchecked(0x8fdf76effe34149f), + Felt::new_unchecked(0x4b15085fd8ffda6d), + Felt::new_unchecked(0x351a1eb1cca3bc29), ]), Word::new([ - Felt::new(0x5d322dbb2f762f46), - Felt::new(0xf48ef8dc6daa8c2c), - Felt::new(0x27d4b670c82074c4), - Felt::new(0x4bd090499ff2587d), + Felt::new_unchecked(0xf61249b1f1bfe39c), + Felt::new_unchecked(0xcc8677045409f1ce), + Felt::new_unchecked(0x5f9c5495d5d927c4), + Felt::new_unchecked(0x0e5a2880b78e5dad), ]), Word::new([ - Felt::new(0x354e814aaae16fff), - Felt::new(0x308049c1a9cdb07d), - Felt::new(0x2ca3bd1ace6e7348), - Felt::new(0x6782667ba99b5d18), + Felt::new_unchecked(0x83dbe82a80826195), + Felt::new_unchecked(0xc34b1ff758f368ab), + Felt::new_unchecked(0x32be0ec28cd358d4), + Felt::new_unchecked(0x2c17f4ce7b7d2c5f), ]), Word::new([ - Felt::new(0x0aaec950fc0eb154), - Felt::new(0xc20bfdf465c8010e), - Felt::new(0x7f8e848bcac5abe2), - Felt::new(0xbb08ae9eede5b9be), + Felt::new_unchecked(0xe795cf57d11db27f), + Felt::new_unchecked(0x2cb18688e05bee75), + Felt::new_unchecked(0x9053996e66be08c7), + Felt::new_unchecked(0x5c4a7af36aeda2a6), ]), Word::new([ - Felt::new(0x40af30f55c36ba02), - Felt::new(0x6c3078ec8eeb5923), - Felt::new(0x6a2e537061eaed81), - Felt::new(0x002ad72bd2b6ec7b), + Felt::new_unchecked(0xd104cc98855f0782), + Felt::new_unchecked(0xf132ef3e1c524ce4), + Felt::new_unchecked(0x864ae826c01eafd4), + Felt::new_unchecked(0x7bf085d210e943b8), ]), Word::new([ - Felt::new(0xeeb3faff2d77b8ba), - Felt::new(0x638ef01f4b91eb98), - Felt::new(0x4444b42687a47eaf), - Felt::new(0x5330c4248c5a8c66), + Felt::new_unchecked(0x2547ce4b049c50c4), + Felt::new_unchecked(0xcee6d1972a3673b5), + Felt::new_unchecked(0x90783afc4609b5ec), + Felt::new_unchecked(0x89edcabb4405e9eb), ]), Word::new([ - Felt::new(0x6690920a6bcc72a7), - Felt::new(0x6010c282ae56eea0), - Felt::new(0x2fed049030b97533), - Felt::new(0x138b76e4f2bc47cb), + Felt::new_unchecked(0xc71c12484b9a29e8), + Felt::new_unchecked(0x52ba25685e80757d), + Felt::new_unchecked(0x7aec2a9d6afc3abb), + Felt::new_unchecked(0x9369baf4a41c91ff), ]), Word::new([ - Felt::new(0xaf5278ff99a628df), - Felt::new(0x595f052fecc9ab19), - Felt::new(0x6061913440182875), - Felt::new(0x53eab420a61b2bf8), + Felt::new_unchecked(0xb8df5b247fd4dba9), + Felt::new_unchecked(0xe5b3b8b8280421db), + Felt::new_unchecked(0xde35f32fe39ae8f6), + Felt::new_unchecked(0x3a52c20200b3e702), ]), Word::new([ - Felt::new(0x88209ac769d94dab), - Felt::new(0xa12d55043a6c48c1), - Felt::new(0x13e16385eb2b52b8), - Felt::new(0x85042f91b4193103), + Felt::new_unchecked(0xcd84755676b282a1), + Felt::new_unchecked(0x6be2b9eeceefdcf5), + Felt::new_unchecked(0x6330de9426d1ffc0), + Felt::new_unchecked(0x669dee285bf50868), ]), Word::new([ - Felt::new(0x13b0825a2f949def), - Felt::new(0x0f133761b751a266), - Felt::new(0xb90bd5bccb8122a7), - Felt::new(0xaf2b1414a5909241), + Felt::new_unchecked(0x6b762dd088805b01), + Felt::new_unchecked(0x69f8c41ca96e2d10), + Felt::new_unchecked(0x65facc7fe98db521), + Felt::new_unchecked(0x8e520142fc2b93cd), ]), Word::new([ - Felt::new(0xca9b15725ee26e84), - Felt::new(0x4c0654b5622b7d28), - Felt::new(0x50cf58c8b1ab4d01), - Felt::new(0x5252f94dcd689e7f), + Felt::new_unchecked(0x3a9e36c9855dae83), + Felt::new_unchecked(0xb7d8df3d2fdfc3cd), + Felt::new_unchecked(0x36f699d93e940b30), + Felt::new_unchecked(0xbcad4890a9a4f9e2), ]), Word::new([ - Felt::new(0x966b1ccc512c9110), - Felt::new(0xf06e95f61e5b0b9a), - Felt::new(0x21827fe65b538b7f), - Felt::new(0xfbc190d74e45917d), + Felt::new_unchecked(0xaa97020f9a848d8f), + Felt::new_unchecked(0xd19e3d9a74691527), + Felt::new_unchecked(0xb6031c44189c6601), + Felt::new_unchecked(0xb55918234e061ee0), ]), Word::new([ - Felt::new(0x6d5753cc036c18f9), - Felt::new(0xdaf6f79da89c0461), - Felt::new(0x42de26ac092a4e0b), - Felt::new(0x6c213c0062b64dba), + Felt::new_unchecked(0x675dd7d25620bde1), + Felt::new_unchecked(0xf1315af5fa0fdc97), + Felt::new_unchecked(0xec342842c30d7f44), + Felt::new_unchecked(0x09907b3a3d77cbb9), ]), Word::new([ - Felt::new(0x91b3c707ed17f648), - Felt::new(0x6e90170e9052b9a7), - Felt::new(0xb45972fdfd4c51dd), - Felt::new(0x27701793ecfc50b1), + Felt::new_unchecked(0x1201c4524996115f), + Felt::new_unchecked(0x298b7361e465e677), + Felt::new_unchecked(0xb5606ebdc0b21687), + Felt::new_unchecked(0xf76a4fb8c47d213d), ]), Word::new([ - Felt::new(0x1c0637c214e8ebac), - Felt::new(0x43bf3c2506e8dbae), - Felt::new(0x4420bee16606142f), - Felt::new(0xf27e1e3054b67fab), + Felt::new_unchecked(0xb9d89de99096f8ab), + Felt::new_unchecked(0x3ffeec3e3a0c7d26), + Felt::new_unchecked(0x05883a69fee8236c), + Felt::new_unchecked(0x2d9034ca2df17a19), ]), Word::new([ - Felt::new(0x883e08713a0c0afd), - Felt::new(0xf9cdb6c6fcff5e43), - Felt::new(0x791a72cd6f40d832), - Felt::new(0xa7f3e71ca960f613), + Felt::new_unchecked(0xdaf329acf17f9313), + Felt::new_unchecked(0x4c71884a628539e4), + Felt::new_unchecked(0x882aca9fdb79254f), + Felt::new_unchecked(0x6ddd1c729017bf62), ]), Word::new([ - Felt::new(0x39ac62e8a61aa45f), - Felt::new(0xbf109a4a60f5cbd0), - Felt::new(0x1d8b66b47b281470), - Felt::new(0xc62ecd5c4b3e2239), + Felt::new_unchecked(0xb871e32185a9f3e9), + Felt::new_unchecked(0x27504edc7ae3fd79), + Felt::new_unchecked(0xf7b9c0133737c08c), + Felt::new_unchecked(0xfc50ceef9c615643), ]), Word::new([ - Felt::new(0xf9047dd2b5b38650), - Felt::new(0x234c754760a7c97a), - Felt::new(0xa6ef6dcea7f0e615), - Felt::new(0x466e0ea54307045d), + Felt::new_unchecked(0x8d530c37efa3e5b4), + Felt::new_unchecked(0x37f32d830f306f6b), + Felt::new_unchecked(0x9601886ad8115670), + Felt::new_unchecked(0x7d80f7f8d5c63a62), ]), Word::new([ - Felt::new(0xf7b010a7b4775867), - Felt::new(0x60e66f53c5cbec31), - Felt::new(0x4a323fc08a401a16), - Felt::new(0x9030956e04fcb4c2), + Felt::new_unchecked(0x993bdf8dfa6a1edc), + Felt::new_unchecked(0x2ea3c247340dba0e), + Felt::new_unchecked(0x7c2e1f1fc79e2f21), + Felt::new_unchecked(0x8f42990e74d5d817), ]), Word::new([ - Felt::new(0x589bfdc28df24262), - Felt::new(0x9fe7efa3e57efc69), - Felt::new(0x0d7bf81f3877a1a5), - Felt::new(0xb8c1e6d103328f81), + Felt::new_unchecked(0xbf7073e84f3cddea), + Felt::new_unchecked(0x60f7c5ee9b3d932e), + Felt::new_unchecked(0x31d73e322198371f), + Felt::new_unchecked(0x8d5346bb34751ac8), ]), Word::new([ - Felt::new(0x0bd4df64ebeb2195), - Felt::new(0xd718c22709eca79d), - Felt::new(0x12a11a5383359344), - Felt::new(0xcd399effd24a2e6d), + Felt::new_unchecked(0xfbc3edafb6c813b9), + Felt::new_unchecked(0x35d8cfadb189f5af), + Felt::new_unchecked(0x9cc65f5d198e28f1), + Felt::new_unchecked(0x14a74616a8e04623), ]), Word::new([ - Felt::new(0xa95c68ebff06feb0), - Felt::new(0x631cfec23d027097), - Felt::new(0x06f4ef2a64b127db), - Felt::new(0x07d396ac3d7625b0), + Felt::new_unchecked(0xe0c4c734868c11c6), + Felt::new_unchecked(0xee1f9ed1da448050), + Felt::new_unchecked(0x19fd124dd8f24870), + Felt::new_unchecked(0xcdd77f41d7deff73), ]), Word::new([ - Felt::new(0xef311849263abcb4), - Felt::new(0x8bf04d36f9a01799), - Felt::new(0x9e570c4df0f2699f), - Felt::new(0x6927c3a96db0b2ad), + Felt::new_unchecked(0x5b31a8b9799ff836), + Felt::new_unchecked(0xe385174fe60f4b08), + Felt::new_unchecked(0xe82c6be88d50767c), + Felt::new_unchecked(0x2778f3b6a18981e2), ]), Word::new([ - Felt::new(0x0000000000000000), - Felt::new(0x0000000000000000), - Felt::new(0x0000000000000000), - Felt::new(0x0000000000000000), + Felt::new_unchecked(0x0000000000000000), + Felt::new_unchecked(0x0000000000000000), + Felt::new_unchecked(0x0000000000000000), + Felt::new_unchecked(0x0000000000000000), ]), ]; diff --git a/miden-crypto/src/merkle/merkle_tree.rs b/miden-crypto/src/merkle/merkle_tree.rs index 693871b484..9d28b9fa24 100644 --- a/miden-crypto/src/merkle/merkle_tree.rs +++ b/miden-crypto/src/merkle/merkle_tree.rs @@ -276,11 +276,11 @@ mod tests { use super::*; use crate::{ - Felt, WORD_SIZE, + Felt, merkle::{int_to_leaf, int_to_node}, }; - const LEAVES4: [Word; WORD_SIZE] = + const LEAVES4: [Word; Word::NUM_ELEMENTS] = [int_to_node(1), int_to_node(2), int_to_node(3), int_to_node(4)]; const LEAVES8: [Word; 8] = [ @@ -296,7 +296,7 @@ mod tests { #[test] fn build_merkle_tree() { - let tree = super::MerkleTree::new(LEAVES4).unwrap(); + let tree = MerkleTree::new(LEAVES4).unwrap(); assert_eq!(8, tree.nodes.len()); // leaves were copied correctly @@ -315,7 +315,7 @@ mod tests { #[test] fn get_leaf() { - let tree = super::MerkleTree::new(LEAVES4).unwrap(); + let tree = MerkleTree::new(LEAVES4).unwrap(); // check depth 2 assert_eq!(LEAVES4[0], tree.get_node(NodeIndex::make(2, 0)).unwrap()); @@ -332,7 +332,7 @@ mod tests { #[test] fn get_path() { - let tree = super::MerkleTree::new(LEAVES4).unwrap(); + let tree = MerkleTree::new(LEAVES4).unwrap(); let (_, node2, node3) = compute_internal_nodes(); @@ -349,14 +349,14 @@ mod tests { #[test] fn update_leaf() { - let mut tree = super::MerkleTree::new(LEAVES8).unwrap(); + let mut tree = MerkleTree::new(LEAVES8).unwrap(); // update one leaf let value = 3; let new_node = int_to_leaf(9); let mut expected_leaves = LEAVES8.to_vec(); expected_leaves[value as usize] = new_node; - let expected_tree = super::MerkleTree::new(expected_leaves.clone()).unwrap(); + let expected_tree = MerkleTree::new(expected_leaves.clone()).unwrap(); tree.update_leaf(value, new_node).unwrap(); assert_eq!(expected_tree.nodes, tree.nodes); @@ -365,7 +365,7 @@ mod tests { let value = 6; let new_node = int_to_leaf(10); expected_leaves[value as usize] = new_node; - let expected_tree = super::MerkleTree::new(expected_leaves.clone()).unwrap(); + let expected_tree = MerkleTree::new(expected_leaves.clone()).unwrap(); tree.update_leaf(value, new_node).unwrap(); assert_eq!(expected_tree.nodes, tree.nodes); @@ -373,7 +373,7 @@ mod tests { #[test] fn nodes() -> Result<(), MerkleError> { - let tree = super::MerkleTree::new(LEAVES4).unwrap(); + let tree = MerkleTree::new(LEAVES4).unwrap(); let root = tree.root(); let l1n0 = tree.get_node(NodeIndex::make(1, 0))?; let l1n1 = tree.get_node(NodeIndex::make(1, 1))?; @@ -406,7 +406,7 @@ mod tests { // that assumes this equivalence. // build a word and copy it to another address as digest - let word = [Felt::new(a), Felt::new(b), Felt::new(c), Felt::new(d)]; + let word = [Felt::new_unchecked(a), Felt::new_unchecked(b), Felt::new_unchecked(c), Felt::new_unchecked(d)]; let digest = Word::from(word); // assert the addresses are different diff --git a/miden-crypto/src/merkle/mmr/error.rs b/miden-crypto/src/merkle/mmr/error.rs index 214747572a..20a3dcf651 100644 --- a/miden-crypto/src/merkle/mmr/error.rs +++ b/miden-crypto/src/merkle/mmr/error.rs @@ -12,6 +12,8 @@ pub enum MmrError { InvalidPeaks(String), #[error("mmr forest is out of bounds: requested {0} > current {1}")] ForestOutOfBounds(usize, usize), + #[error("mmr forest size {requested} exceeds maximum {max}")] + ForestSizeExceeded { requested: usize, max: usize }, #[error("mmr peak does not match the computed merkle root of the provided authentication path")] PeakPathMismatch, #[error("requested peak index is {peak_idx} but the number of peaks is {peaks_len}")] diff --git a/miden-crypto/src/merkle/mmr/forest.rs b/miden-crypto/src/merkle/mmr/forest.rs index 0fb201613f..2102bb71a9 100644 --- a/miden-crypto/src/merkle/mmr/forest.rs +++ b/miden-crypto/src/merkle/mmr/forest.rs @@ -3,7 +3,7 @@ use core::{ ops::{BitAnd, BitOr, BitXor, BitXorAssign}, }; -use super::InOrderIndex; +use super::{InOrderIndex, MmrError}; use crate::{ Felt, utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}, @@ -29,31 +29,61 @@ use crate::{ /// - `Forest(0b1010)` is a forest with two trees: one with 8 leaves (15 nodes), one with 2 leaves /// (3 nodes). /// - `Forest(0b1000)` is a forest with one tree, which has 8 leaves (15 nodes). +/// +/// Forest sizes are capped at [`Forest::MAX_LEAVES`]. Use [`Forest::new`] or +/// [`Forest::append_leaf`] to enforce the limit. #[derive(Debug, Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] pub struct Forest(usize); impl Forest { + /// Maximum number of leaves supported by the forest. + /// + /// Rationale: + /// - We require `MAX_LEAVES <= usize::MAX / 2 + 1` so `num_nodes()` stays indexable via + /// `usize`. + /// - We choose `usize::MAX / 2` (hard cutoff) rather than `usize::MAX / 2 + 1` so the cap is + /// always of the form `2^k - 1` on all targets. + /// - With that shape, bitwise OR/XOR of valid forest values remains within bounds, so OR/XOR + /// does not need additional overflow protection. + pub const MAX_LEAVES: usize = if (u32::MAX as usize) < (usize::MAX / 2) { + u32::MAX as usize + } else { + usize::MAX / 2 + }; + /// Creates an empty forest (no trees). pub const fn empty() -> Self { Self(0) } - /// Creates a forest with `num_leaves` leaves. - pub const fn new(num_leaves: usize) -> Self { - Self(num_leaves) + /// Creates a forest with `num_leaves` leaves, returning an error if the value is too large. + pub fn new(num_leaves: usize) -> Result { + if !Self::is_valid_size(num_leaves) { + return Err(DeserializationError::InvalidValue(format!( + "forest size {} exceeds maximum {}", + num_leaves, + Self::MAX_LEAVES + ))); + } + Ok(Self(num_leaves)) } /// Creates a forest with a given height. /// - /// This is equivalent to `Forest::new(1 << height)`. + /// This is equivalent to creating a forest with `1 << height` leaves. /// /// # Panics /// /// This will panic if `height` is greater than `usize::BITS - 1`. - pub const fn with_height(height: usize) -> Self { + pub fn with_height(height: usize) -> Self { assert!(height < usize::BITS as usize); - Self::new(1 << height) + Self::new(1 << height).expect("forest height exceeds maximum") + } + + /// Returns true if `num_leaves` is within the supported bounds. + pub const fn is_valid_size(num_leaves: usize) -> bool { + num_leaves <= Self::MAX_LEAVES } /// Returns true if there are no trees in the forest. @@ -64,8 +94,15 @@ impl Forest { /// Adds exactly one more leaf to the capacity of this forest. /// /// Some smaller trees might be merged together. - pub fn append_leaf(&mut self) { + pub fn append_leaf(&mut self) -> Result<(), MmrError> { + if self.0 >= Self::MAX_LEAVES { + return Err(MmrError::ForestSizeExceeded { + requested: self.0.saturating_add(1), + max: Self::MAX_LEAVES, + }); + } self.0 += 1; + Ok(()) } /// Returns a count of leaves in the entire underlying forest (MMR). @@ -75,11 +112,11 @@ impl Forest { /// Return the total number of nodes of a given forest. /// - /// # Panics - /// - /// This will panic if the forest has size greater than `usize::MAX / 2 + 1`. + /// This relies on the `Forest` invariant that `num_leaves() <= Forest::MAX_LEAVES`. + /// The internal assertion is a defensive check and should be unreachable for values created + /// through validated constructors/deserializers. pub const fn num_nodes(self) -> usize { - assert!(self.0 <= usize::MAX / 2 + 1); + assert!(self.0 <= Self::MAX_LEAVES); if self.0 <= usize::MAX / 2 { self.0 * 2 - self.num_trees() } else { @@ -197,11 +234,12 @@ impl Forest { /// /// ``` /// # use miden_crypto::merkle::mmr::Forest; - /// let range = Forest::new(0b0101_0110); - /// assert_eq!(range.trees_larger_than(1), Forest::new(0b0101_0100)); + /// let range = Forest::new(0b0101_0110).unwrap(); + /// assert_eq!(range.trees_larger_than(1), Forest::new(0b0101_0100).unwrap()); /// ``` pub fn trees_larger_than(self, tree_idx: u32) -> Self { - self & high_bitmask(tree_idx + 1) + let mask = high_bitmask(tree_idx + 1); + Self::new(self.0 & mask).expect("forest size exceeds maximum") } /// Creates a new forest with all possible trees smaller than the smallest tree in this @@ -216,7 +254,7 @@ impl Forest { /// For a non-panicking version of this function, see [`Forest::all_smaller_trees()`]. pub fn all_smaller_trees_unchecked(self) -> Self { debug_assert_eq!(self.num_trees(), 1); - Self::new(self.0 - 1) + Self::new(self.0 - 1).expect("forest size exceeds maximum") } /// Creates a new forest with all possible trees smaller than the smallest tree in this @@ -232,9 +270,16 @@ impl Forest { } /// Returns a forest with exactly one tree, one size (depth) larger than the current one. - pub fn next_larger_tree(self) -> Self { + /// + /// # Errors + /// Returns an error if the resulting forest would exceed [`Forest::MAX_LEAVES`]. + pub(crate) fn next_larger_tree(self) -> Result { debug_assert_eq!(self.num_trees(), 1); - Forest(self.0 << 1) + let value = self.0.saturating_mul(2); + if value > Self::MAX_LEAVES { + return Err(MmrError::ForestSizeExceeded { requested: value, max: Self::MAX_LEAVES }); + } + Ok(Forest(value)) } /// Returns true if the forest contains a single-node tree. @@ -244,17 +289,20 @@ impl Forest { /// Add a single-node tree if not already present in the forest. pub fn with_single_leaf(self) -> Self { - Self::new(self.0 | 1) + // Setting the lowest bit cannot exceed MAX_LEAVES when MAX_LEAVES is 2^k - 1. + Self(self.0 | 1) } /// Remove the single-node tree if present in the forest. pub fn without_single_leaf(self) -> Self { - Self::new(self.0 & (usize::MAX - 1)) + // Clearing the lowest bit does not add leaves. + Self(self.0 & (usize::MAX - 1)) } /// Returns a new forest that does not have the trees that `other` has. pub fn without_trees(self, other: Forest) -> Self { - self ^ other + // Clearing bits does not add leaves. + Self(self.0 & !other.0) } /// Returns index of the forest tree for a specified leaf index. @@ -262,7 +310,8 @@ impl Forest { let root = self .leaf_to_corresponding_tree(leaf_idx) .expect("position must be part of the forest"); - let smaller_tree_mask = Self::new(2_usize.pow(root) - 1); + let smaller_tree_mask = + Self::new(2_usize.pow(root) - 1).expect("forest size exceeds maximum"); let num_smaller_trees = (*self & smaller_tree_mask).num_trees(); self.num_trees() - num_smaller_trees - 1 } @@ -350,8 +399,7 @@ impl Forest { /// Given a leaf index in the current forest, return the tree number responsible for the /// leaf. /// - /// Note: - /// The result is a tree position `p`, it has the following interpretations: + /// The result is a tree position `p`: /// - `p+1` is the depth of the tree. /// - Because the root element is not part of the proof, `p` is the length of the authentication /// path. @@ -396,8 +444,8 @@ impl Forest { /// the leaf belongs. pub(super) fn leaf_relative_position(self, leaf_idx: usize) -> Option { let tree_idx = self.leaf_to_corresponding_tree(leaf_idx)?; - let forest_before = self & high_bitmask(tree_idx + 1); - Some(leaf_idx - forest_before.0) + let mask = high_bitmask(tree_idx + 1); + Some(leaf_idx - (self.0 & mask)) } } @@ -417,15 +465,19 @@ impl BitAnd for Forest { type Output = Self; fn bitand(self, rhs: Self) -> Self::Output { - Self::new(self.0 & rhs.0) + Self::new(self.0 & rhs.0).expect("forest size exceeds maximum") } } +// Compile-time invariant: MAX_LEAVES must be exactly 2^k - 1. +const _: () = + assert!(Forest::MAX_LEAVES != 0 && (Forest::MAX_LEAVES & (Forest::MAX_LEAVES + 1)) == 0); + impl BitOr for Forest { type Output = Self; fn bitor(self, rhs: Self) -> Self::Output { - Self::new(self.0 | rhs.0) + Self(self.0 | rhs.0) } } @@ -433,7 +485,7 @@ impl BitXor for Forest { type Output = Self; fn bitxor(self, rhs: Self) -> Self::Output { - Self::new(self.0 ^ rhs.0) + Self(self.0 ^ rhs.0) } } @@ -443,25 +495,41 @@ impl BitXorAssign for Forest { } } -impl From for Forest { - fn from(value: Felt) -> Self { - Self::new(value.as_canonical_u64() as usize) +impl TryFrom for Forest { + type Error = MmrError; + + fn try_from(value: Felt) -> Result { + let value = usize::try_from(value.as_canonical_u64()).map_err(|_| { + MmrError::ForestSizeExceeded { + requested: usize::MAX, + max: Self::MAX_LEAVES, + } + })?; + if value > Self::MAX_LEAVES { + return Err(MmrError::ForestSizeExceeded { requested: value, max: Self::MAX_LEAVES }); + } + Ok(Self(value)) + } +} + +pub(crate) fn largest_tree_from_mask(mask: usize) -> Forest { + if mask == 0 { + Forest::empty() + } else { + let bit = mask.ilog2(); + Forest::new(1usize << bit).expect("forest size exceeds maximum") } } impl From for Felt { fn from(value: Forest) -> Self { - Felt::new(value.0 as u64) + Felt::new_unchecked(value.0 as u64) } } /// Return a bitmask for the bits including and above the given position. -pub(crate) const fn high_bitmask(bit: u32) -> Forest { - if bit > usize::BITS - 1 { - Forest::empty() - } else { - Forest::new(usize::MAX << bit) - } +pub(crate) fn high_bitmask(bit: u32) -> usize { + if bit > usize::BITS - 1 { 0 } else { usize::MAX << bit } } // SERIALIZATION @@ -476,7 +544,18 @@ impl Serializable for Forest { impl Deserializable for Forest { fn read_from(source: &mut R) -> Result { let value = source.read_usize()?; - Ok(Self::new(value)) + Self::new(value) + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for Forest { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = usize::deserialize(deserializer)?; + Self::new(value).map_err(serde::de::Error::custom) } } diff --git a/miden-crypto/src/merkle/mmr/full.rs b/miden-crypto/src/merkle/mmr/full.rs index 9bcfe6a62a..344b1aec28 100644 --- a/miden-crypto/src/merkle/mmr/full.rs +++ b/miden-crypto/src/merkle/mmr/full.rs @@ -16,6 +16,7 @@ use super::{ super::{InnerNodeInfo, MerklePath}, MmrDelta, MmrError, MmrPath, MmrPeaks, MmrProof, forest::{Forest, TreeSizeIterator}, + nodes_from_mask, }; use crate::{ Word, @@ -64,6 +65,35 @@ impl Mmr { } } + /// Constructs an MMR from an iterator of leaves. + /// + /// # Errors + /// Returns an error if the maximum forest size is exceeded. + pub fn try_from_iter>(values: T) -> Result { + Self::try_from_iter_with_limit(values, Forest::MAX_LEAVES) + } + + pub(crate) fn try_from_iter_with_limit>( + values: T, + max_leaves: usize, + ) -> Result { + let mut mmr = Mmr::new(); + let iter = values.into_iter(); + let (lower, _) = iter.size_hint(); + if lower > max_leaves { + return Err(MmrError::ForestSizeExceeded { requested: lower, max: max_leaves }); + } + let mut count = 0usize; + for v in iter { + count += 1; + if count > max_leaves { + return Err(MmrError::ForestSizeExceeded { requested: count, max: max_leaves }); + } + mmr.add(v)?; + } + Ok(mmr) + } + // ACCESSORS // ============================================================================================ @@ -121,7 +151,13 @@ impl Mmr { } /// Adds a new element to the MMR. - pub fn add(&mut self, el: Word) { + /// + /// # Errors + /// Returns an error if the MMR exceeds the maximum supported forest size. + pub fn add(&mut self, el: Word) -> Result<(), MmrError> { + // Fail early before mutating nodes. + let old_forest = self.forest; + self.forest.append_leaf()?; // Note: every node is also a tree of size 1, adding an element to the forest creates a new // rooted-tree of size 1. This may temporarily break the invariant that every tree in the // forest has different sizes, the loop below will eagerly merge trees of same size and @@ -130,16 +166,22 @@ impl Mmr { let mut left_offset = self.nodes.len().saturating_sub(2); let mut right = el; - let mut left_tree = 1; - while !(self.forest & Forest::new(left_tree)).is_empty() { + let mut left_tree = 1usize; + while (old_forest.num_leaves() & left_tree) != 0 { right = Poseidon2::merge(&[self.nodes[left_offset], right]); self.nodes.push(right); - left_offset = left_offset.saturating_sub(Forest::new(left_tree).num_nodes()); - left_tree <<= 1; + debug_assert!(left_tree <= Forest::MAX_LEAVES); + let left_nodes = left_tree * 2 - 1; + left_offset = left_offset.saturating_sub(left_nodes); + + match left_tree.checked_shl(1) { + Some(next) => left_tree = next, + None => break, + } } - self.forest.append_leaf(); + Ok(()) } /// Returns the current peaks of the MMR. @@ -158,7 +200,7 @@ impl Mmr { let peaks: Vec = TreeSizeIterator::new(forest) .rev() - .map(|tree| tree.num_nodes()) + .map(Forest::num_nodes) .scan(0, |offset, el| { *offset += el; Some(*offset) @@ -197,8 +239,8 @@ impl Mmr { let mut result = Vec::new(); // Find the largest tree in this [Mmr] which is new to `from_forest`. - let candidate_trees = to_forest ^ from_forest; - let mut new_high = candidate_trees.largest_tree_unchecked(); + let candidate_mask = to_forest.num_leaves() ^ from_forest.num_leaves(); + let mut new_high = super::forest::largest_tree_from_mask(candidate_mask); // Collect authentication nodes used for tree merges // ---------------------------------------------------------------------------------------- @@ -221,14 +263,16 @@ impl Mmr { // - target: tree from which to load the sibling. On the first iteration this is a // value known by the partial mmr, on subsequent iterations this value is to be // computed from the known peaks and provided authentication nodes. - let known = (common_trees | merges | target).num_nodes(); + let known_mask = + common_trees.num_leaves() | merges.num_leaves() | target.num_leaves(); + let known = nodes_from_mask(known_mask); let sibling = target.num_nodes(); result.push(self.nodes[known + sibling - 1]); // Update the target and account for tree merges - target = target.next_larger_tree(); + target = target.next_larger_tree()?; while !(merges & target).is_empty() { - target = target.next_larger_tree(); + target = target.next_larger_tree()?; } // Remove the merges done so far merges ^= merges & target.all_smaller_trees_unchecked(); @@ -298,7 +342,7 @@ impl Mmr { // The tree walk below goes from the root to the leaf, compute the root index to start let mut forest_target: usize = 1usize << tree_bit; - let mut index = Forest::new(forest_target).num_nodes() - 1; + let mut index = nodes_from_mask(forest_target) - 1; // Loop until the leaf is reached while forest_target > 1 { @@ -307,7 +351,7 @@ impl Mmr { // compute the indices of the right and left subtrees based on the post-order let right_offset = index - 1; - let left_offset = right_offset - Forest::new(forest_target).num_nodes(); + let left_offset = right_offset - nodes_from_mask(forest_target); let left_or_right = relative_pos & forest_target; let sibling = if left_or_right != 0 { @@ -337,18 +381,7 @@ impl Mmr { // CONVERSIONS // ================================================================================================ -impl From for Mmr -where - T: IntoIterator, -{ - fn from(values: T) -> Self { - let mut mmr = Mmr::new(); - for v in values { - mmr.add(v) - } - mmr - } -} +// No TryFrom impl: it conflicts with core’s blanket TryFrom where U: Into. // SERIALIZATION // ================================================================================================ @@ -415,7 +448,7 @@ impl Iterator for MmrNodes<'_> { // compute the number of nodes in the right tree, this is the offset to the // previous left parent - let right_nodes = Forest::new(self.last_right).num_nodes(); + let right_nodes = Forest::new(self.last_right).unwrap().num_nodes(); // the next parent position is one above the position of the pair let parent = self.last_right << 1; @@ -453,22 +486,41 @@ impl Iterator for MmrNodes<'_> { mod tests { use alloc::vec::Vec; + use super::super::nodes_from_mask; use crate::{ Felt, Word, ZERO, - merkle::mmr::Mmr, - utils::{Deserializable, Serializable}, + merkle::mmr::{Forest, Mmr}, + utils::{Deserializable, DeserializationError, Serializable}, }; #[test] fn test_serialization() { let nodes = (0u64..128u64) - .map(|value| Word::new([ZERO, ZERO, ZERO, Felt::new(value)])) + .map(|value| Word::new([ZERO, ZERO, ZERO, Felt::new_unchecked(value)])) .collect::>(); - let mmr = Mmr::from(nodes); + let mmr = Mmr::try_from_iter(nodes).unwrap(); let serialized = mmr.to_bytes(); let deserialized = Mmr::read_from_bytes(&serialized).unwrap(); assert_eq!(mmr.forest, deserialized.forest); assert_eq!(mmr.nodes, deserialized.nodes); } + + #[test] + fn test_deserialization_rejects_large_forest() { + let mut bytes = (Forest::MAX_LEAVES + 1).to_bytes(); + bytes.extend_from_slice(&0usize.to_bytes()); // empty nodes vector + + let result = Mmr::read_from_bytes(&bytes); + assert!(matches!(result, Err(DeserializationError::InvalidValue(_)))); + } + + #[test] + fn test_nodes_from_mask_at_max_leaves() { + let expected = (Forest::MAX_LEAVES as u128) + .saturating_mul(2) + .saturating_sub(Forest::MAX_LEAVES.count_ones() as u128); + assert!(expected <= usize::MAX as u128); + assert_eq!(nodes_from_mask(Forest::MAX_LEAVES), expected as usize); + } } diff --git a/miden-crypto/src/merkle/mmr/mod.rs b/miden-crypto/src/merkle/mmr/mod.rs index b2b024ab61..51192ed58d 100644 --- a/miden-crypto/src/merkle/mmr/mod.rs +++ b/miden-crypto/src/merkle/mmr/mod.rs @@ -12,6 +12,14 @@ mod proof; #[cfg(test)] mod tests; +/// Returns the number of nodes represented by a forest bitmask. +/// +/// `mask` is a forest-leaf mask (same encoding as [`Forest::num_leaves()`]): each set bit denotes +/// one peak/tree with leaf count `2^bit_position`. +fn nodes_from_mask(mask: usize) -> usize { + Forest::new(mask).expect("mask must encode a valid forest").num_nodes() +} + // REEXPORTS // ================================================================================================ pub use delta::MmrDelta; diff --git a/miden-crypto/src/merkle/mmr/partial.rs b/miden-crypto/src/merkle/mmr/partial.rs index ea7b402601..244a222f88 100644 --- a/miden-crypto/src/merkle/mmr/partial.rs +++ b/miden-crypto/src/merkle/mmr/partial.rs @@ -124,15 +124,13 @@ impl PartialMmr { for &pos in &tracked_leaves { if pos >= num_leaves { return Err(MmrError::InconsistentPartialMmr(format!( - "tracked leaf position {} is out of bounds (forest has {} leaves)", - pos, num_leaves + "tracked leaf position {pos} is out of bounds (forest has {num_leaves} leaves)" ))); } let leaf_idx = InOrderIndex::from_leaf_pos(pos); if !nodes.contains_key(&leaf_idx) { return Err(MmrError::InconsistentPartialMmr(format!( - "tracked leaf at position {} has no value in nodes", - pos + "tracked leaf at position {pos} has no value in nodes" ))); } } @@ -300,8 +298,12 @@ impl PartialMmr { /// inserted into this [PartialMmr] as a result of this operation. /// /// When `track` is `true` the new leaf is tracked and its value is stored. - pub fn add(&mut self, leaf: Word, track: bool) -> Vec<(InOrderIndex, Word)> { - self.forest.append_leaf(); + /// + /// # Errors + /// Returns an error if the MMR exceeds the maximum supported forest size. + pub fn add(&mut self, leaf: Word, track: bool) -> Result, MmrError> { + // Fail early before mutating nodes. + self.forest.append_leaf()?; // The smallest tree height equals the number of merges because adding a leaf is like // adding 1 in binary: each carry corresponds to a merge. For example, forest 3 (0b11) // + 1 = 4 (0b100) requires 2 carries/merges to form a tree of height 2. @@ -373,14 +375,14 @@ impl PartialMmr { // On the next iteration, a peak will be merged. If any of its children are tracked, // then we have to track the left side - track_left = self.is_tracked_node(&right_idx.sibling()); + track_left = self.is_tracked_node(right_idx.sibling()); } right }; self.peaks.push(peak); - new_nodes + Ok(new_nodes) } /// Adds the authentication path represented by [MerklePath] if it is valid. @@ -400,9 +402,12 @@ impl PartialMmr { ) -> Result<(), MmrError> { // Checks there is a tree with same depth as the authentication path, if not the path is // invalid. - let tree = Forest::new(1 << path.depth()); + let path_depth = path.depth(); + let tree_leaves = + 1usize.checked_shl(path_depth as u32).ok_or(MmrError::UnknownPeak(path_depth))?; + let tree = Forest::new(tree_leaves).map_err(|_| MmrError::UnknownPeak(path_depth))?; if (tree & self.forest).is_empty() { - return Err(MmrError::UnknownPeak(path.depth())); + return Err(MmrError::UnknownPeak(path_depth)); }; // ignore the trees smaller than the target (these elements are position after the current @@ -527,10 +532,10 @@ impl PartialMmr { } // find the trees to merge (bitmask of existing trees that will be combined) - let changes = self.forest ^ delta.forest; + let changes = self.forest.num_leaves() ^ delta.forest.num_leaves(); // `largest_tree_unchecked()` panics if `changes` is empty. `changes` cannot be empty // unless `self.forest == delta.forest`, which is guarded against above. - let largest = changes.largest_tree_unchecked(); + let largest = super::forest::largest_tree_from_mask(changes); // The largest tree itself also cannot be an empty forest, so this cannot panic either. let trees_to_merge = self.forest & largest.all_smaller_trees_unchecked(); @@ -546,7 +551,9 @@ impl PartialMmr { (merge_count, new_peaks) } else { - (0, changes) + let new_peaks = Forest::new(changes) + .expect("changes must be a valid forest under apply invariants"); + (0, new_peaks) }; // verify the delta size @@ -575,7 +582,7 @@ impl PartialMmr { // Check if either the left or right subtrees have nodes saved for authentication // paths. If so, turn tracking on to update those paths. if !track { - track = self.is_tracked_node(&peak_idx); + track = self.is_tracked_node(peak_idx); } // update data only contains the nodes from the right subtrees, left nodes are @@ -586,7 +593,7 @@ impl PartialMmr { // if the sibling peak is tracked, add this peaks to the set of // authentication nodes - if self.is_tracked_node(&sibling_idx) { + if self.is_tracked_node(sibling_idx) { self.nodes.insert(peak_idx, new); inserted_nodes.push((peak_idx, new)); } @@ -611,7 +618,7 @@ impl PartialMmr { peak_idx = peak_idx.parent(); new = Poseidon2::merge(&[left, right]); - target = target.next_larger_tree(); + target = target.next_larger_tree()?; } debug_assert!(peak_count == trees_to_merge.num_trees()); @@ -640,7 +647,7 @@ impl PartialMmr { /// Returns true if this [PartialMmr] tracks authentication path for the node at the specified /// index. - fn is_tracked_node(&self, node_index: &InOrderIndex) -> bool { + fn is_tracked_node(&self, node_index: InOrderIndex) -> bool { if let Some(leaf_pos) = node_index.to_leaf_pos() { // For leaf nodes, check if the leaf is in the tracked set. self.tracked_leaves.contains(&leaf_pos) @@ -746,7 +753,7 @@ impl Deserializable for PartialMmr { ) -> Result { use crate::utils::DeserializationError; - let forest = Forest::new(usize::read_from(source)?); + let forest = Forest::new(usize::read_from(source)?)?; let peaks_vec = Vec::::read_from(source)?; let nodes = NodeMap::read_from(source)?; if !source.has_more_bytes() { @@ -763,12 +770,12 @@ impl Deserializable for PartialMmr { // Construct MmrPeaks to validate forest/peaks consistency let peaks = MmrPeaks::new(forest, peaks_vec).map_err(|e| { - DeserializationError::InvalidValue(format!("invalid partial mmr peaks: {}", e)) + DeserializationError::InvalidValue(format!("invalid partial mmr peaks: {e}")) })?; // Use validating constructor Self::from_parts(peaks, nodes, tracked_leaves) - .map_err(|e| DeserializationError::InvalidValue(format!("invalid partial mmr: {}", e))) + .map_err(|e| DeserializationError::InvalidValue(format!("invalid partial mmr: {e}"))) } } @@ -790,7 +797,7 @@ mod tests { mmr::{InOrderIndex, Mmr, forest::Forest}, store::MerkleStore, }, - utils::{ByteWriter, Deserializable, Serializable}, + utils::{ByteWriter, Deserializable, DeserializationError, Serializable}, }; const LEAVES: [Word; 7] = [ @@ -807,7 +814,7 @@ mod tests { fn test_partial_mmr_apply_delta() { // build an MMR with 10 nodes (2 peaks) and a partial MMR based on it let mut mmr = Mmr::default(); - (0..10).for_each(|i| mmr.add(int_to_node(i))); + (0..10).for_each(|i| mmr.add(int_to_node(i)).unwrap()); let mut partial_mmr: PartialMmr = mmr.peaks().into(); // add authentication path for position 1 and 8 @@ -824,11 +831,11 @@ mod tests { } // add 2 more nodes into the MMR and validate apply_delta() - (10..12).for_each(|i| mmr.add(int_to_node(i))); + (10..12).for_each(|i| mmr.add(int_to_node(i)).unwrap()); validate_apply_delta(&mmr, &mut partial_mmr); // add 1 more node to the MMR, validate apply_delta() and start tracking the node - mmr.add(int_to_node(12)); + mmr.add(int_to_node(12)).unwrap(); validate_apply_delta(&mmr, &mut partial_mmr); { let node = mmr.get(12).unwrap(); @@ -841,7 +848,7 @@ mod tests { // by this point we are tracking authentication paths for positions: 1, 8, and 12 // add 3 more nodes to the MMR (collapses to 1 peak) and validate apply_delta() - (13..16).for_each(|i| mmr.add(int_to_node(i))); + (13..16).for_each(|i| mmr.add(int_to_node(i)).unwrap()); validate_apply_delta(&mmr, &mut partial_mmr); } @@ -877,7 +884,7 @@ mod tests { #[test] fn test_partial_mmr_inner_nodes_iterator() { // build the MMR - let mmr: Mmr = LEAVES.into(); + let mmr = Mmr::try_from_iter(LEAVES.iter().copied()).unwrap(); let first_peak = mmr.peaks().peaks()[0]; // -- test single tree ---------------------------- @@ -973,8 +980,8 @@ mod tests { let mut partial_mmr = PartialMmr::from_peaks(empty_peaks); for el in (0..256).map(int_to_node) { - mmr.add(el); - partial_mmr.add(el, false); + mmr.add(el).unwrap(); + partial_mmr.add(el, false).unwrap(); assert_eq!(mmr.peaks(), partial_mmr.peaks()); assert_eq!(mmr.forest(), partial_mmr.forest()); @@ -989,8 +996,8 @@ mod tests { for i in 0..256 { let el = int_to_node(i as u64); - mmr.add(el); - partial_mmr.add(el, true); + mmr.add(el).unwrap(); + partial_mmr.add(el, true).unwrap(); assert_eq!(mmr.peaks(), partial_mmr.peaks()); assert_eq!(mmr.forest(), partial_mmr.forest()); @@ -1005,7 +1012,7 @@ mod tests { #[test] fn test_partial_mmr_add_existing_track() { - let mut mmr = Mmr::from((0..7).map(int_to_node)); + let mut mmr = Mmr::try_from_iter((0..7).map(int_to_node)).unwrap(); // derive a partial Mmr from it which tracks authentication path to leaf 5 let mut partial_mmr = PartialMmr::from_peaks(mmr.peaks()); @@ -1015,8 +1022,8 @@ mod tests { // add a new leaf to both Mmr and partial Mmr let leaf_at_7 = int_to_node(7); - mmr.add(leaf_at_7); - partial_mmr.add(leaf_at_7, false); + mmr.add(leaf_at_7).unwrap(); + partial_mmr.add(leaf_at_7, false).unwrap(); // the openings should be the same assert_eq!(mmr.open(5).unwrap(), partial_mmr.open(5).unwrap().unwrap()); @@ -1031,16 +1038,16 @@ mod tests { // Add leaf 0 with tracking - it's a dangling leaf (forest=1) let leaf0 = int_to_node(0); - mmr.add(leaf0); - partial_mmr.add(leaf0, true); + mmr.add(leaf0).unwrap(); + partial_mmr.add(leaf0, true).unwrap(); // Both should produce the same proof (empty path, leaf is a peak) assert_eq!(mmr.open(0).unwrap(), partial_mmr.open(0).unwrap().unwrap()); // Add leaf 1 WITHOUT tracking - triggers merge, leaf 0 gets a sibling let leaf1 = int_to_node(1); - mmr.add(leaf1); - partial_mmr.add(leaf1, false); + mmr.add(leaf1).unwrap(); + partial_mmr.add(leaf1, false).unwrap(); // Leaf 0 should still be tracked with correct proof after merge assert!(partial_mmr.is_tracked(0)); @@ -1050,7 +1057,7 @@ mod tests { #[test] fn test_partial_mmr_serialization() { - let mmr = Mmr::from((0..7).map(int_to_node)); + let mmr = Mmr::try_from_iter((0..7).map(int_to_node)).unwrap(); let partial_mmr = PartialMmr::from_peaks(mmr.peaks()); let bytes = partial_mmr.to_bytes(); @@ -1059,10 +1066,21 @@ mod tests { assert_eq!(partial_mmr, decoded); } + #[test] + fn test_partial_mmr_deserialization_rejects_large_forest() { + let mut bytes = (Forest::MAX_LEAVES + 1).to_bytes(); + bytes.extend_from_slice(&0usize.to_bytes()); // empty peaks vec + bytes.extend_from_slice(&0usize.to_bytes()); // empty nodes map + bytes.extend_from_slice(&0usize.to_bytes()); // empty tracked vec + + let result = PartialMmr::read_from_bytes(&bytes); + assert!(matches!(result, Err(DeserializationError::InvalidValue(_)))); + } + #[test] fn test_partial_mmr_untrack() { // build the MMR - let mmr: Mmr = LEAVES.into(); + let mmr = Mmr::try_from_iter(LEAVES.iter().copied()).unwrap(); // get path and node for position 1 let node1 = mmr.get(1).unwrap(); @@ -1090,7 +1108,7 @@ mod tests { #[test] fn test_partial_mmr_untrack_returns_removed_nodes() { // build the MMR - let mmr: Mmr = LEAVES.into(); + let mmr = Mmr::try_from_iter(LEAVES.iter().copied()).unwrap(); // get path and node for position 1 let node1 = mmr.get(1).unwrap(); @@ -1120,7 +1138,7 @@ mod tests { #[test] fn test_partial_mmr_untrack_shared_nodes() { // build the MMR - let mmr: Mmr = LEAVES.into(); + let mmr = Mmr::try_from_iter(LEAVES.iter().copied()).unwrap(); // track two sibling leaves (positions 0 and 1) let node0 = mmr.get(0).unwrap(); @@ -1171,7 +1189,7 @@ mod tests { #[test] fn test_partial_mmr_untrack_preserves_upper_siblings() { let mut mmr = Mmr::default(); - (0..8).for_each(|i| mmr.add(int_to_node(i))); + (0..8).for_each(|i| mmr.add(int_to_node(i)).unwrap()); let mut partial_mmr: PartialMmr = mmr.peaks().into(); for pos in [0, 2] { @@ -1190,7 +1208,7 @@ mod tests { #[test] fn test_partial_mmr_deserialize_missing_marker_fails() { let mut mmr = Mmr::default(); - (0..3).for_each(|i| mmr.add(int_to_node(i))); + (0..3).for_each(|i| mmr.add(int_to_node(i)).unwrap()); let peaks = mmr.peaks(); let mut bytes = Vec::new(); @@ -1203,7 +1221,7 @@ mod tests { #[test] fn test_partial_mmr_deserialize_invalid_marker_fails() { let mut mmr = Mmr::default(); - (0..3).for_each(|i| mmr.add(int_to_node(i))); + (0..3).for_each(|i| mmr.add(int_to_node(i)).unwrap()); let peaks = mmr.peaks(); let mut bytes = Vec::new(); @@ -1219,7 +1237,7 @@ mod tests { #[test] fn test_partial_mmr_open_returns_proof_with_leaf() { // build the MMR - let mmr: Mmr = LEAVES.into(); + let mmr = Mmr::try_from_iter(LEAVES.iter().copied()).unwrap(); // get leaf and proof for position 1 let leaf1 = mmr.get(1).unwrap(); @@ -1249,9 +1267,9 @@ mod tests { let leaf1 = int_to_node(1); let leaf2 = int_to_node(2); - partial_mmr.add(leaf0, true); // track - partial_mmr.add(leaf1, false); // don't track - partial_mmr.add(leaf2, true); // track + partial_mmr.add(leaf0, true).unwrap(); // track + partial_mmr.add(leaf1, false).unwrap(); // don't track + partial_mmr.add(leaf2, true).unwrap(); // track // verify tracked leaves can be opened let proof0 = partial_mmr.open(0).unwrap(); @@ -1281,7 +1299,7 @@ mod tests { fn test_partial_mmr_track_dangling_leaf() { // Single-leaf MMR: forest = 1, leaf 0 is a peak with an empty path. let mut mmr = Mmr::default(); - mmr.add(int_to_node(0)); + mmr.add(int_to_node(0)).unwrap(); let mut partial_mmr: PartialMmr = mmr.peaks().into(); let leaf0 = mmr.get(0).unwrap(); @@ -1303,7 +1321,7 @@ mod tests { use super::InOrderIndex; // Build a valid MMR with 7 leaves - let mmr: Mmr = LEAVES.into(); + let mmr = Mmr::try_from_iter(LEAVES.iter().copied()).unwrap(); let peaks = mmr.peaks(); // Valid case: empty nodes and empty tracked_leaves @@ -1324,7 +1342,7 @@ mod tests { // Valid case: tracked leaf with its value in nodes let mut nodes_with_leaf = BTreeMap::new(); - let leaf_idx = super::InOrderIndex::from_leaf_pos(0); + let leaf_idx = InOrderIndex::from_leaf_pos(0); nodes_with_leaf.insert(leaf_idx, int_to_node(0)); let mut tracked_valid = BTreeSet::new(); tracked_valid.insert(0); @@ -1366,8 +1384,7 @@ mod tests { let mut nodes_with_separator_12 = BTreeMap::new(); let separator_idx_12 = InOrderIndex::read_from_bytes(&12usize.to_bytes()).unwrap(); nodes_with_separator_12.insert(separator_idx_12, int_to_node(0)); - let result = - PartialMmr::from_parts(peaks.clone(), nodes_with_separator_12, BTreeSet::new()); + let result = PartialMmr::from_parts(peaks, nodes_with_separator_12, BTreeSet::new()); assert!(result.is_err(), "separator index 12 should be rejected"); // Invalid case: nodes with empty forest @@ -1381,7 +1398,7 @@ mod tests { #[test] fn test_from_parts_validation_deserialization() { // Build an MMR with 7 leaves - let mmr: Mmr = LEAVES.into(); + let mmr = Mmr::try_from_iter(LEAVES.iter().copied()).unwrap(); let partial_mmr = PartialMmr::from_peaks(mmr.peaks()); // Valid serialization/deserialization @@ -1429,7 +1446,7 @@ mod tests { use alloc::collections::BTreeMap; // Build a valid MMR - let mmr: Mmr = LEAVES.into(); + let mmr = Mmr::try_from_iter(LEAVES.iter().copied()).unwrap(); let peaks = mmr.peaks(); // from_parts_unchecked should not validate and always succeed @@ -1440,8 +1457,7 @@ mod tests { // Even invalid combinations should work (no validation) let mut invalid_tracked = BTreeSet::new(); invalid_tracked.insert(999); - let partial = - PartialMmr::from_parts_unchecked(peaks.clone(), BTreeMap::new(), invalid_tracked); + let partial = PartialMmr::from_parts_unchecked(peaks, BTreeMap::new(), invalid_tracked); assert!(partial.tracked_leaves.contains(&999)); } } diff --git a/miden-crypto/src/merkle/mmr/peaks.rs b/miden-crypto/src/merkle/mmr/peaks.rs index 37f84af23b..f378b417ed 100644 --- a/miden-crypto/src/merkle/mmr/peaks.rs +++ b/miden-crypto/src/merkle/mmr/peaks.rs @@ -57,8 +57,15 @@ impl MmrPeaks { /// leaves in the underlying MMR. /// /// # Errors - /// Returns an error if the number of leaves and the number of peaks are inconsistent. + /// Returns an error if the number of leaves and the number of peaks are inconsistent, or if + /// the forest exceeds the maximum supported size. pub fn new(forest: Forest, peaks: Vec) -> Result { + if !Forest::is_valid_size(forest.num_leaves()) { + return Err(MmrError::ForestSizeExceeded { + requested: forest.num_leaves(), + max: Forest::MAX_LEAVES, + }); + } if forest.num_trees() != peaks.len() { return Err(MmrError::InvalidPeaks(format!( "number of one bits in leaves is {} which does not equal peak length {}", diff --git a/miden-crypto/src/merkle/mmr/proof.rs b/miden-crypto/src/merkle/mmr/proof.rs index c6c9b0ff3c..e4bdd2f2c1 100644 --- a/miden-crypto/src/merkle/mmr/proof.rs +++ b/miden-crypto/src/merkle/mmr/proof.rs @@ -180,7 +180,7 @@ mod tests { #[test] fn test_peak_index() { // --- single peak forest --------------------------------------------- - let forest = Forest::new(11); + let forest = Forest::new(11).unwrap(); // the first 4 leaves belong to peak 0 for position in 0..8 { @@ -189,7 +189,7 @@ mod tests { } // --- forest with non-consecutive peaks ------------------------------ - let forest = Forest::new(11); + let forest = Forest::new(11).unwrap(); // the first 8 leaves belong to peak 0 for position in 0..8 { @@ -208,7 +208,7 @@ mod tests { assert_eq!(proof.peak_index(), 2); // --- forest with consecutive peaks ---------------------------------- - let forest = Forest::new(7); + let forest = Forest::new(7).unwrap(); // the first 4 leaves belong to peak 0 for position in 0..4 { @@ -237,14 +237,14 @@ mod tests { // Create an MMR with 5 leaves let mut small_mmr = Mmr::new(); for i in 0..5 { - small_mmr.add(int_to_node(i)); + small_mmr.add(int_to_node(i)).unwrap(); } let small_forest = small_mmr.forest(); // Clone and add 5 more leaves to create larger MMR let mut large_mmr = small_mmr.clone(); for i in 5..10 { - large_mmr.add(int_to_node(i)); + large_mmr.add(int_to_node(i)).unwrap(); } // Get proof for position 2 from the larger MMR @@ -273,17 +273,17 @@ mod tests { // Create a MMR with 7 leaves let mut mmr = Mmr::new(); for i in 0..7 { - mmr.add(int_to_node(i)); + mmr.add(int_to_node(i)).unwrap(); } let proof = mmr.open(2).unwrap(); let path = proof.path(); // Error: target forest doesn't include position - let small_forest = Forest::new(2); + let small_forest = Forest::new(2).unwrap(); assert!(path.with_forest(small_forest).is_err()); // Error: target forest is larger than current - let large_forest = Forest::new(15); + let large_forest = Forest::new(15).unwrap(); assert!(path.with_forest(large_forest).is_err()); // Same forest should work diff --git a/miden-crypto/src/merkle/mmr/tests.rs b/miden-crypto/src/merkle/mmr/tests.rs index 89f8e2ff37..9cca7506ec 100644 --- a/miden-crypto/src/merkle/mmr/tests.rs +++ b/miden-crypto/src/merkle/mmr/tests.rs @@ -4,7 +4,7 @@ use assert_matches::assert_matches; use super::{ super::{InnerNodeInfo, Poseidon2, Word}, - Mmr, MmrError, MmrPeaks, PartialMmr, + Mmr, MmrError, MmrPeaks, PartialMmr, nodes_from_mask, }; use crate::{ Felt, @@ -91,11 +91,11 @@ fn test_leaf_to_corresponding_tree() { #[test] fn test_high_bitmask() { - assert_eq!(high_bitmask(0), Forest::new(usize::MAX)); - assert_eq!(high_bitmask(1), Forest::new(usize::MAX << 1)); - assert_eq!(high_bitmask(usize::BITS - 2), Forest::new(0b11usize.rotate_right(2))); - assert_eq!(high_bitmask(usize::BITS - 1), Forest::new(0b1usize.rotate_right(1))); - assert_eq!(high_bitmask(usize::BITS), Forest::empty(), "overflow should be handled"); + assert_eq!(high_bitmask(0), usize::MAX); + assert_eq!(high_bitmask(1), usize::MAX << 1); + assert_eq!(high_bitmask(usize::BITS - 2), 0b11usize.rotate_right(2)); + assert_eq!(high_bitmask(usize::BITS - 1), 0b1usize.rotate_right(1)); + assert_eq!(high_bitmask(usize::BITS), 0, "overflow should be handled"); } #[test] @@ -121,28 +121,213 @@ fn test_nodes_in_forest_single_bit() { assert_eq!(nodes_in_forest(2usize.pow(2)), 2usize.pow(3) - 1); assert_eq!(nodes_in_forest(2usize.pow(3)), 2usize.pow(4) - 1); - for bit in 0..(usize::BITS - 1) { - let size = 2usize.pow(bit + 1) - 1; - assert_eq!(nodes_in_forest(1usize << bit), size); + let mut bit = 0u32; + while let Some(leaves) = 1usize.checked_shl(bit) { + if leaves > Forest::MAX_LEAVES { + break; + } + + if leaves > usize::MAX / 2 { + break; + } + + let size = leaves * 2 - 1; + assert_eq!(nodes_in_forest(leaves), size); + bit += 1; + } + + if Forest::MAX_LEAVES.is_power_of_two() { + let expected = (Forest::MAX_LEAVES as u128).saturating_mul(2).saturating_sub(1); + let actual = nodes_in_forest(Forest::MAX_LEAVES) as u128; + assert_eq!(actual, expected); } } #[test] fn test_forest_largest_smallest_tree() { // largest_tree and smallest_tree return correct results - let forest = Forest::new(0b1101_0100); - let largest = Forest::new(0b1000_0000); - let smallest = Forest::new(0b0000_0100); + let forest = Forest::new(0b1101_0100).unwrap(); + let largest = Forest::new(0b1000_0000).unwrap(); + let smallest = Forest::new(0b0000_0100).unwrap(); assert_eq!(forest.largest_tree(), largest); assert_eq!(forest.smallest_tree(), smallest); // no trees in an empty forest - let empty_forest = Forest::new(0); + let empty_forest = Forest::new(0).unwrap(); assert_eq!(empty_forest.largest_tree(), empty_forest); assert_eq!(empty_forest.smallest_tree(), empty_forest); } +#[test] +fn test_forest_append_leaf_limit() { + let mut forest = Forest::new(Forest::MAX_LEAVES).unwrap(); + assert_matches!( + forest.append_leaf(), + Err(MmrError::ForestSizeExceeded { requested, max }) if + requested == Forest::MAX_LEAVES + 1 && max == Forest::MAX_LEAVES + ); +} + +#[test] +fn test_forest_new_limit() { + assert!(Forest::new(Forest::MAX_LEAVES).is_ok()); + assert!(Forest::new(Forest::MAX_LEAVES + 1).is_err()); +} + +#[cfg(feature = "serde")] +#[test] +fn test_forest_serde_rejects_large_value() { + use serde::{Deserialize, de::value::UsizeDeserializer}; + + let result = Forest::deserialize(UsizeDeserializer::::new( + Forest::MAX_LEAVES + 1, + )); + assert!(result.is_err()); +} + +#[test] +fn test_forest_with_single_leaf_limit() { + let forest = Forest::new(Forest::MAX_LEAVES).unwrap(); + let result = forest.with_single_leaf(); + assert_eq!(result.num_leaves(), Forest::MAX_LEAVES); +} + +#[test] +fn test_forest_bitxor_within_limit() { + let high = Forest::new(Forest::MAX_LEAVES).unwrap(); + let low = Forest::new(Forest::MAX_LEAVES - 1).unwrap(); + let result = high ^ low; + assert!(result.num_leaves() <= Forest::MAX_LEAVES); +} + +#[test] +fn test_forest_bitor_within_limit() { + let high = Forest::new(Forest::MAX_LEAVES).unwrap(); + let low = Forest::new(1).unwrap(); + let result = high | low; + assert!(result.num_leaves() <= Forest::MAX_LEAVES); +} + +#[test] +fn test_forest_without_trees_does_not_add_bits() { + let forest = Forest::new(0b1000).unwrap(); + let other = Forest::new(0b0001).unwrap(); + let result = forest.without_trees(other); + assert_eq!(result, forest); +} + +#[test] +fn test_mmr_add_limit_prevents_mutation() { + let mut mmr = Mmr { + forest: Forest::new(Forest::MAX_LEAVES).unwrap(), + nodes: Vec::new(), + }; + let result = mmr.add(Word::empty()); + assert_matches!(result, Err(MmrError::ForestSizeExceeded { .. })); + assert_eq!(mmr.nodes.len(), 0); + assert_eq!(mmr.forest.num_leaves(), Forest::MAX_LEAVES); +} + +#[test] +fn test_mmr_try_from_iter_accepts_oversize_upper_hint() { + struct OversizeIter; + + impl Iterator for OversizeIter { + type Item = Word; + + fn next(&mut self) -> Option { + None + } + + fn size_hint(&self) -> (usize, Option) { + (0, Some(Forest::MAX_LEAVES + 1)) + } + } + + let result = Mmr::try_from_iter(OversizeIter); + assert!(result.is_ok()); +} + +#[test] +fn test_mmr_try_from_iter_rejects_oversize_lower_bound() { + struct OversizeLowerIter { + remaining: usize, + } + + impl Iterator for OversizeLowerIter { + type Item = Word; + + fn next(&mut self) -> Option { + if self.remaining == 0 { + None + } else { + self.remaining -= 1; + Some(Word::empty()) + } + } + + fn size_hint(&self) -> (usize, Option) { + (self.remaining, Some(self.remaining)) + } + } + + let result = Mmr::try_from_iter(OversizeLowerIter { remaining: Forest::MAX_LEAVES + 1 }); + assert_matches!(result, Err(MmrError::ForestSizeExceeded { .. })); +} + +#[test] +fn test_mmr_try_from_iter_rejects_oversize_actual() { + let max_leaves = 8; + + struct OversizeActualIter { + remaining: usize, + } + + impl Iterator for OversizeActualIter { + type Item = Word; + + fn next(&mut self) -> Option { + if self.remaining == 0 { + None + } else { + self.remaining -= 1; + Some(Word::empty()) + } + } + + fn size_hint(&self) -> (usize, Option) { + (0, None) + } + } + + let result = + Mmr::try_from_iter_with_limit(OversizeActualIter { remaining: max_leaves + 1 }, max_leaves); + assert_matches!( + result, + Err(MmrError::ForestSizeExceeded { requested, max }) if + requested == max_leaves + 1 && max == max_leaves + ); +} + +#[test] +fn test_partial_mmr_add_limit_prevents_mutation() { + let mut partial = PartialMmr { + forest: Forest::new(Forest::MAX_LEAVES).unwrap(), + ..Default::default() + }; + let before_peaks = partial.peaks.clone(); + let before_nodes = partial.nodes.clone(); + let before_tracked = partial.tracked_leaves.clone(); + + let result = partial.add(Word::empty(), true); + assert_matches!(result, Err(MmrError::ForestSizeExceeded { .. })); + assert_eq!(partial.forest.num_leaves(), Forest::MAX_LEAVES); + assert_eq!(partial.peaks, before_peaks); + assert_eq!(partial.nodes, before_nodes); + assert_eq!(partial.tracked_leaves, before_tracked); +} + #[test] fn test_forest_to_root_index() { fn idx(pos: usize) -> InOrderIndex { @@ -151,22 +336,22 @@ fn test_forest_to_root_index() { // When there is a single tree in the forest, the index is equivalent to the number of // leaves in that tree, which is `2^n`. - assert_eq!(Forest::new(0b0001).root_in_order_index(), idx(1)); - assert_eq!(Forest::new(0b0010).root_in_order_index(), idx(2)); - assert_eq!(Forest::new(0b0100).root_in_order_index(), idx(4)); - assert_eq!(Forest::new(0b1000).root_in_order_index(), idx(8)); - - assert_eq!(Forest::new(0b0011).root_in_order_index(), idx(5)); - assert_eq!(Forest::new(0b0101).root_in_order_index(), idx(9)); - assert_eq!(Forest::new(0b1001).root_in_order_index(), idx(17)); - assert_eq!(Forest::new(0b0111).root_in_order_index(), idx(13)); - assert_eq!(Forest::new(0b1011).root_in_order_index(), idx(21)); - assert_eq!(Forest::new(0b1111).root_in_order_index(), idx(29)); - - assert_eq!(Forest::new(0b0110).root_in_order_index(), idx(10)); - assert_eq!(Forest::new(0b1010).root_in_order_index(), idx(18)); - assert_eq!(Forest::new(0b1100).root_in_order_index(), idx(20)); - assert_eq!(Forest::new(0b1110).root_in_order_index(), idx(26)); + assert_eq!(Forest::new(0b0001).unwrap().root_in_order_index(), idx(1)); + assert_eq!(Forest::new(0b0010).unwrap().root_in_order_index(), idx(2)); + assert_eq!(Forest::new(0b0100).unwrap().root_in_order_index(), idx(4)); + assert_eq!(Forest::new(0b1000).unwrap().root_in_order_index(), idx(8)); + + assert_eq!(Forest::new(0b0011).unwrap().root_in_order_index(), idx(5)); + assert_eq!(Forest::new(0b0101).unwrap().root_in_order_index(), idx(9)); + assert_eq!(Forest::new(0b1001).unwrap().root_in_order_index(), idx(17)); + assert_eq!(Forest::new(0b0111).unwrap().root_in_order_index(), idx(13)); + assert_eq!(Forest::new(0b1011).unwrap().root_in_order_index(), idx(21)); + assert_eq!(Forest::new(0b1111).unwrap().root_in_order_index(), idx(29)); + + assert_eq!(Forest::new(0b0110).unwrap().root_in_order_index(), idx(10)); + assert_eq!(Forest::new(0b1010).unwrap().root_in_order_index(), idx(18)); + assert_eq!(Forest::new(0b1100).unwrap().root_in_order_index(), idx(20)); + assert_eq!(Forest::new(0b1110).unwrap().root_in_order_index(), idx(26)); } #[test] @@ -177,26 +362,26 @@ fn test_forest_to_rightmost_index() { for forest in 1..256 { assert!( - Forest::new(forest).rightmost_in_order_index().inner() % 2 == 1, + Forest::new(forest).unwrap().rightmost_in_order_index().inner() % 2 == 1, "Leaves are always odd" ); } - assert_eq!(Forest::new(0b0001).rightmost_in_order_index(), idx(1)); - assert_eq!(Forest::new(0b0010).rightmost_in_order_index(), idx(3)); - assert_eq!(Forest::new(0b0011).rightmost_in_order_index(), idx(5)); - assert_eq!(Forest::new(0b0100).rightmost_in_order_index(), idx(7)); - assert_eq!(Forest::new(0b0101).rightmost_in_order_index(), idx(9)); - assert_eq!(Forest::new(0b0110).rightmost_in_order_index(), idx(11)); - assert_eq!(Forest::new(0b0111).rightmost_in_order_index(), idx(13)); - assert_eq!(Forest::new(0b1000).rightmost_in_order_index(), idx(15)); - assert_eq!(Forest::new(0b1001).rightmost_in_order_index(), idx(17)); - assert_eq!(Forest::new(0b1010).rightmost_in_order_index(), idx(19)); - assert_eq!(Forest::new(0b1011).rightmost_in_order_index(), idx(21)); - assert_eq!(Forest::new(0b1100).rightmost_in_order_index(), idx(23)); - assert_eq!(Forest::new(0b1101).rightmost_in_order_index(), idx(25)); - assert_eq!(Forest::new(0b1110).rightmost_in_order_index(), idx(27)); - assert_eq!(Forest::new(0b1111).rightmost_in_order_index(), idx(29)); + assert_eq!(Forest::new(0b0001).unwrap().rightmost_in_order_index(), idx(1)); + assert_eq!(Forest::new(0b0010).unwrap().rightmost_in_order_index(), idx(3)); + assert_eq!(Forest::new(0b0011).unwrap().rightmost_in_order_index(), idx(5)); + assert_eq!(Forest::new(0b0100).unwrap().rightmost_in_order_index(), idx(7)); + assert_eq!(Forest::new(0b0101).unwrap().rightmost_in_order_index(), idx(9)); + assert_eq!(Forest::new(0b0110).unwrap().rightmost_in_order_index(), idx(11)); + assert_eq!(Forest::new(0b0111).unwrap().rightmost_in_order_index(), idx(13)); + assert_eq!(Forest::new(0b1000).unwrap().rightmost_in_order_index(), idx(15)); + assert_eq!(Forest::new(0b1001).unwrap().rightmost_in_order_index(), idx(17)); + assert_eq!(Forest::new(0b1010).unwrap().rightmost_in_order_index(), idx(19)); + assert_eq!(Forest::new(0b1011).unwrap().rightmost_in_order_index(), idx(21)); + assert_eq!(Forest::new(0b1100).unwrap().rightmost_in_order_index(), idx(23)); + assert_eq!(Forest::new(0b1101).unwrap().rightmost_in_order_index(), idx(25)); + assert_eq!(Forest::new(0b1110).unwrap().rightmost_in_order_index(), idx(27)); + assert_eq!(Forest::new(0b1111).unwrap().rightmost_in_order_index(), idx(29)); } #[test] @@ -211,22 +396,22 @@ fn test_is_valid_in_order_index() { // Single tree forests (power of 2 leaves) have no separators // Forest with 1 leaf: valid indices are just 1 - let forest_1 = Forest::new(0b0001); + let forest_1 = Forest::new(0b0001).unwrap(); assert!(!forest_1.is_valid_in_order_index(&idx(2)), "index 2 is invalid"); assert!(forest_1.is_valid_in_order_index(&idx(1))); assert!(!forest_1.is_valid_in_order_index(&idx(2)), "beyond bounds"); // Forest with 2 leaves: valid indices are 1, 2, 3 - let forest_2 = Forest::new(0b0010); + let forest_2 = Forest::new(0b0010).unwrap(); assert!(forest_2.is_valid_in_order_index(&idx(1))); assert!(forest_2.is_valid_in_order_index(&idx(2))); assert!(forest_2.is_valid_in_order_index(&idx(3))); assert!(!forest_2.is_valid_in_order_index(&idx(4)), "beyond bounds"); // Forest with 4 leaves: valid indices are 1-7 - let forest_4 = Forest::new(0b0100); + let forest_4 = Forest::new(0b0100).unwrap(); for i in 1..=7 { - assert!(forest_4.is_valid_in_order_index(&idx(i)), "index {} should be valid", i); + assert!(forest_4.is_valid_in_order_index(&idx(i)), "index {i} should be valid"); } assert!(!forest_4.is_valid_in_order_index(&idx(8)), "beyond bounds"); @@ -236,14 +421,13 @@ fn test_is_valid_in_order_index() { // Tree 2 (2 leaves): indices 9-11 // Separator: index 12 // Tree 3 (1 leaf): index 13 - let forest_7 = Forest::new(0b0111); + let forest_7 = Forest::new(0b0111).unwrap(); // Valid indices in first tree (4 leaves, 7 nodes) for i in 1..=7 { assert!( forest_7.is_valid_in_order_index(&idx(i)), - "index {} should be valid in first tree", - i + "index {i} should be valid in first tree" ); } @@ -254,8 +438,7 @@ fn test_is_valid_in_order_index() { for i in 9..=11 { assert!( forest_7.is_valid_in_order_index(&idx(i)), - "index {} should be valid in second tree", - i + "index {i} should be valid in second tree" ); } @@ -275,13 +458,13 @@ fn test_is_valid_in_order_index() { // Tree 1 (4 leaves): indices 1-7 // Separator: index 8 // Tree 2 (2 leaves): indices 9-11 - let forest_6 = Forest::new(0b0110); + let forest_6 = Forest::new(0b0110).unwrap(); for i in 1..=7 { - assert!(forest_6.is_valid_in_order_index(&idx(i)), "index {} should be valid", i); + assert!(forest_6.is_valid_in_order_index(&idx(i)), "index {i} should be valid"); } assert!(!forest_6.is_valid_in_order_index(&idx(8)), "index 8 is a separator"); for i in 9..=11 { - assert!(forest_6.is_valid_in_order_index(&idx(i)), "index {} should be valid", i); + assert!(forest_6.is_valid_in_order_index(&idx(i)), "index {i} should be valid"); } assert!(!forest_6.is_valid_in_order_index(&idx(12)), "index 12 is beyond bounds"); } @@ -292,48 +475,50 @@ fn test_bit_position_iterator() { assert_eq!(TreeSizeIterator::new(Forest::empty()).rev().count(), 0); assert_eq!( - TreeSizeIterator::new(Forest::new(1)).collect::>(), - vec![Forest::new(1)] + TreeSizeIterator::new(Forest::new(1).unwrap()).collect::>(), + vec![Forest::new(1).unwrap()] ); assert_eq!( - TreeSizeIterator::new(Forest::new(1)).rev().collect::>(), - vec![Forest::new(1)], + TreeSizeIterator::new(Forest::new(1).unwrap()).rev().collect::>(), + vec![Forest::new(1).unwrap()], ); assert_eq!( - TreeSizeIterator::new(Forest::new(2)).collect::>(), - vec![Forest::new(2)] + TreeSizeIterator::new(Forest::new(2).unwrap()).collect::>(), + vec![Forest::new(2).unwrap()] ); assert_eq!( - TreeSizeIterator::new(Forest::new(2)).rev().collect::>(), - vec![Forest::new(2)], + TreeSizeIterator::new(Forest::new(2).unwrap()).rev().collect::>(), + vec![Forest::new(2).unwrap()], ); assert_eq!( - TreeSizeIterator::new(Forest::new(3)).collect::>(), - vec![Forest::new(1), Forest::new(2)], + TreeSizeIterator::new(Forest::new(3).unwrap()).collect::>(), + vec![Forest::new(1).unwrap(), Forest::new(2).unwrap()], ); assert_eq!( - TreeSizeIterator::new(Forest::new(3)).rev().collect::>(), - vec![Forest::new(2), Forest::new(1)], + TreeSizeIterator::new(Forest::new(3).unwrap()).rev().collect::>(), + vec![Forest::new(2).unwrap(), Forest::new(1).unwrap()], ); assert_eq!( - TreeSizeIterator::new(Forest::new(0b11010101)).collect::>(), + TreeSizeIterator::new(Forest::new(0b11010101).unwrap()).collect::>(), vec![0, 2, 4, 6, 7] .into_iter() - .map(|bit| Forest::new(1 << bit)) + .map(|bit| Forest::new(1 << bit).unwrap()) .collect::>() ); assert_eq!( - TreeSizeIterator::new(Forest::new(0b11010101)).rev().collect::>(), + TreeSizeIterator::new(Forest::new(0b11010101).unwrap()) + .rev() + .collect::>(), vec![7, 6, 4, 2, 0] .into_iter() - .map(|bit| Forest::new(1 << bit)) + .map(|bit| Forest::new(1 << bit).unwrap()) .collect::>() ); - let forest = Forest::new(0b1101_0101); + let forest = Forest::new(0b1101_0101).unwrap(); let mut it = TreeSizeIterator::new(forest); // 0b1101_0101 @@ -409,7 +594,7 @@ fn test_mmr_simple() { assert_eq!(mmr.forest().num_leaves(), 0); assert_eq!(mmr.nodes.len(), 0); - mmr.add(LEAVES[0]); + mmr.add(LEAVES[0]).unwrap(); assert_eq!(mmr.forest().num_leaves(), 1); assert_eq!(mmr.nodes.len(), 1); assert_eq!(mmr.nodes.as_slice(), &postorder[0..mmr.nodes.len()]); @@ -418,7 +603,7 @@ fn test_mmr_simple() { assert_eq!(acc.num_leaves(), 1); assert_eq!(acc.peaks(), &[postorder[0]]); - mmr.add(LEAVES[1]); + mmr.add(LEAVES[1]).unwrap(); assert_eq!(mmr.forest().num_leaves(), 2); assert_eq!(mmr.nodes.len(), 3); assert_eq!(mmr.nodes.as_slice(), &postorder[0..mmr.nodes.len()]); @@ -427,7 +612,7 @@ fn test_mmr_simple() { assert_eq!(acc.num_leaves(), 2); assert_eq!(acc.peaks(), &[postorder[2]]); - mmr.add(LEAVES[2]); + mmr.add(LEAVES[2]).unwrap(); assert_eq!(mmr.forest().num_leaves(), 3); assert_eq!(mmr.nodes.len(), 4); assert_eq!(mmr.nodes.as_slice(), &postorder[0..mmr.nodes.len()]); @@ -436,7 +621,7 @@ fn test_mmr_simple() { assert_eq!(acc.num_leaves(), 3); assert_eq!(acc.peaks(), &[postorder[2], postorder[3]]); - mmr.add(LEAVES[3]); + mmr.add(LEAVES[3]).unwrap(); assert_eq!(mmr.forest().num_leaves(), 4); assert_eq!(mmr.nodes.len(), 7); assert_eq!(mmr.nodes.as_slice(), &postorder[0..mmr.nodes.len()]); @@ -445,7 +630,7 @@ fn test_mmr_simple() { assert_eq!(acc.num_leaves(), 4); assert_eq!(acc.peaks(), &[postorder[6]]); - mmr.add(LEAVES[4]); + mmr.add(LEAVES[4]).unwrap(); assert_eq!(mmr.forest().num_leaves(), 5); assert_eq!(mmr.nodes.len(), 8); assert_eq!(mmr.nodes.as_slice(), &postorder[0..mmr.nodes.len()]); @@ -454,7 +639,7 @@ fn test_mmr_simple() { assert_eq!(acc.num_leaves(), 5); assert_eq!(acc.peaks(), &[postorder[6], postorder[7]]); - mmr.add(LEAVES[5]); + mmr.add(LEAVES[5]).unwrap(); assert_eq!(mmr.forest().num_leaves(), 6); assert_eq!(mmr.nodes.len(), 10); assert_eq!(mmr.nodes.as_slice(), &postorder[0..mmr.nodes.len()]); @@ -463,7 +648,7 @@ fn test_mmr_simple() { assert_eq!(acc.num_leaves(), 6); assert_eq!(acc.peaks(), &[postorder[6], postorder[9]]); - mmr.add(LEAVES[6]); + mmr.add(LEAVES[6]).unwrap(); assert_eq!(mmr.forest().num_leaves(), 7); assert_eq!(mmr.nodes.len(), 11); assert_eq!(mmr.nodes.as_slice(), &postorder[0..mmr.nodes.len()]); @@ -475,7 +660,7 @@ fn test_mmr_simple() { #[test] fn test_mmr_open() { - let mmr: Mmr = LEAVES.into(); + let mmr = Mmr::try_from_iter(LEAVES.iter().copied()).unwrap(); let h01 = merge(LEAVES[0], LEAVES[1]); let h23 = merge(LEAVES[2], LEAVES[3]); @@ -551,15 +736,15 @@ fn test_mmr_open() { #[test] fn test_mmr_open_older_version() { - let mmr: Mmr = LEAVES.into(); + let mmr = Mmr::try_from_iter(LEAVES.iter().copied()).unwrap(); - fn is_even(v: &usize) -> bool { + fn is_even(v: usize) -> bool { v & 1 == 0 } // merkle path of a node is empty if there are no elements to pair with it - for pos in (0..mmr.forest().num_leaves()).filter(is_even) { - let forest = Forest::new(pos + 1); + for pos in (0..mmr.forest().num_leaves()).filter(|&v| is_even(v)) { + let forest = Forest::new(pos + 1).unwrap(); let proof = mmr.open_at(pos, forest).unwrap(); assert_eq!(proof.path().forest(), forest); assert_eq!(proof.path().merkle_path().nodes(), []); @@ -569,7 +754,7 @@ fn test_mmr_open_older_version() { // openings match that of a merkle tree let mtree: MerkleTree = LEAVES[..4].try_into().unwrap(); for forest in 4..=LEAVES.len() { - let forest = Forest::new(forest); + let forest = Forest::new(forest).unwrap(); for pos in 0..4 { let idx = NodeIndex::new(2, pos).unwrap(); let path = mtree.get_path(idx).unwrap(); @@ -579,7 +764,7 @@ fn test_mmr_open_older_version() { } let mtree: MerkleTree = LEAVES[4..6].try_into().unwrap(); for forest in 6..=LEAVES.len() { - let forest = Forest::new(forest); + let forest = Forest::new(forest).unwrap(); for pos in 0..2 { let idx = NodeIndex::new(1, pos).unwrap(); let path = mtree.get_path(idx).unwrap(); @@ -606,8 +791,8 @@ fn test_mmr_open_eight() { ]; let mtree: MerkleTree = leaves.as_slice().try_into().unwrap(); - let forest = Forest::new(leaves.len()); - let mmr: Mmr = leaves.into(); + let forest = Forest::new(leaves.len()).unwrap(); + let mmr = Mmr::try_from_iter(leaves.iter().copied()).unwrap(); let root = mtree.root(); let position = 0; @@ -745,8 +930,8 @@ fn test_mmr_open_seven() { let mtree1: MerkleTree = LEAVES[..4].try_into().unwrap(); let mtree2: MerkleTree = LEAVES[4..6].try_into().unwrap(); - let forest = Forest::new(LEAVES.len()); - let mmr: Mmr = LEAVES.into(); + let forest = Forest::new(LEAVES.len()).unwrap(); + let mmr = Mmr::try_from_iter(LEAVES.iter().copied()).unwrap(); let position = 0; let proof = mmr.open(position).unwrap(); @@ -818,7 +1003,7 @@ fn test_mmr_open_seven() { #[test] fn test_mmr_get() { - let mmr: Mmr = LEAVES.into(); + let mmr = Mmr::try_from_iter(LEAVES.iter().copied()).unwrap(); assert_eq!(mmr.get(0).unwrap(), LEAVES[0], "value at pos 0 must correspond"); assert_eq!(mmr.get(1).unwrap(), LEAVES[1], "value at pos 1 must correspond"); assert_eq!(mmr.get(2).unwrap(), LEAVES[2], "value at pos 2 must correspond"); @@ -833,7 +1018,7 @@ fn test_mmr_get() { fn test_mmr_invariants() { let mut mmr = Mmr::new(); for v in 1..=1028 { - mmr.add(int_to_node(v)); + mmr.add(int_to_node(v)).unwrap(); let accumulator = mmr.peaks(); assert_eq!( v as usize, @@ -852,7 +1037,7 @@ fn test_mmr_invariants() { ); let expected_nodes: usize = - TreeSizeIterator::new(mmr.forest()).map(|tree| tree.num_nodes()).sum(); + TreeSizeIterator::new(mmr.forest()).map(Forest::num_nodes).sum(); assert_eq!( expected_nodes, @@ -865,7 +1050,7 @@ fn test_mmr_invariants() { #[test] fn test_mmr_inner_nodes() { - let mmr: Mmr = LEAVES.into(); + let mmr = Mmr::try_from_iter(LEAVES.iter().copied()).unwrap(); let nodes: Vec = mmr.inner_nodes().collect(); let h01 = Poseidon2::merge(&[LEAVES[0], LEAVES[1]]); @@ -896,39 +1081,39 @@ fn test_mmr_inner_nodes() { #[test] fn test_mmr_peaks() { - let mmr: Mmr = LEAVES.into(); + let mmr = Mmr::try_from_iter(LEAVES.iter().copied()).unwrap(); - let forest = Forest::new(0b0001); + let forest = Forest::new(0b0001).unwrap(); let acc = mmr.peaks_at(forest).unwrap(); assert_eq!(acc.num_leaves(), forest.num_leaves()); assert_eq!(acc.peaks(), &[mmr.nodes[0]]); - let forest = Forest::new(0b0010); + let forest = Forest::new(0b0010).unwrap(); let acc = mmr.peaks_at(forest).unwrap(); assert_eq!(acc.num_leaves(), forest.num_leaves()); assert_eq!(acc.peaks(), &[mmr.nodes[2]]); - let forest = Forest::new(0b0011); + let forest = Forest::new(0b0011).unwrap(); let acc = mmr.peaks_at(forest).unwrap(); assert_eq!(acc.num_leaves(), forest.num_leaves()); assert_eq!(acc.peaks(), &[mmr.nodes[2], mmr.nodes[3]]); - let forest = Forest::new(0b0100); + let forest = Forest::new(0b0100).unwrap(); let acc = mmr.peaks_at(forest).unwrap(); assert_eq!(acc.num_leaves(), forest.num_leaves()); assert_eq!(acc.peaks(), &[mmr.nodes[6]]); - let forest = Forest::new(0b0101); + let forest = Forest::new(0b0101).unwrap(); let acc = mmr.peaks_at(forest).unwrap(); assert_eq!(acc.num_leaves(), forest.num_leaves()); assert_eq!(acc.peaks(), &[mmr.nodes[6], mmr.nodes[7]]); - let forest = Forest::new(0b0110); + let forest = Forest::new(0b0110).unwrap(); let acc = mmr.peaks_at(forest).unwrap(); assert_eq!(acc.num_leaves(), forest.num_leaves()); assert_eq!(acc.peaks(), &[mmr.nodes[6], mmr.nodes[9]]); - let forest = Forest::new(0b0111); + let forest = Forest::new(0b0111).unwrap(); let acc = mmr.peaks_at(forest).unwrap(); assert_eq!(acc.num_leaves(), forest.num_leaves()); assert_eq!(acc.peaks(), &[mmr.nodes[6], mmr.nodes[9], mmr.nodes[10]]); @@ -936,7 +1121,7 @@ fn test_mmr_peaks() { #[test] fn test_mmr_hash_peaks() { - let mmr: Mmr = LEAVES.into(); + let mmr = Mmr::try_from_iter(LEAVES.iter().copied()).unwrap(); let peaks = mmr.peaks(); let first_peak = Poseidon2::merge(&[ @@ -962,7 +1147,7 @@ fn test_mmr_peaks_hash_less_than_16() { for i in 0..16 { peaks.push(int_to_node(i)); - let forest = Forest::new(1 << peaks.len()).all_smaller_trees().unwrap(); + let forest = Forest::new(1 << peaks.len()).unwrap().all_smaller_trees().unwrap(); let accumulator = MmrPeaks::new(forest, peaks.clone()).unwrap(); // minimum length is 16 @@ -979,7 +1164,7 @@ fn test_mmr_peaks_hash_less_than_16() { fn test_mmr_peaks_hash_odd() { let peaks: Vec<_> = (0..=17).map(int_to_node).collect(); - let forest = Forest::new(1 << peaks.len()).all_smaller_trees_unchecked(); + let forest = Forest::new(1 << peaks.len()).unwrap().all_smaller_trees_unchecked(); let accumulator = MmrPeaks::new(forest, peaks.clone()).unwrap(); // odd length bigger than 16 is padded to the next even number @@ -993,45 +1178,48 @@ fn test_mmr_peaks_hash_odd() { #[test] fn test_mmr_delta() { - let mmr: Mmr = LEAVES.into(); + let mmr = Mmr::try_from_iter(LEAVES.iter().copied()).unwrap(); let acc = mmr.peaks(); // original_forest can't have more elements assert!( - mmr.get_delta(Forest::new(LEAVES.len() + 1), mmr.forest()).is_err(), + mmr.get_delta(Forest::new(LEAVES.len() + 1).unwrap(), mmr.forest()).is_err(), "Can not provide updates for a newer Mmr" ); // if the number of elements is the same there is no change assert!( - mmr.get_delta(Forest::new(LEAVES.len()), mmr.forest()).unwrap().data.is_empty(), + mmr.get_delta(Forest::new(LEAVES.len()).unwrap(), mmr.forest()) + .unwrap() + .data + .is_empty(), "There are no updates for the same Mmr version" ); // missing the last element added, which is itself a tree peak assert_eq!( - mmr.get_delta(Forest::new(6), mmr.forest()).unwrap().data, + mmr.get_delta(Forest::new(6).unwrap(), mmr.forest()).unwrap().data, vec![acc.peaks()[2]], "one peak" ); // missing the sibling to complete the tree of depth 2, and the last element assert_eq!( - mmr.get_delta(Forest::new(5), mmr.forest()).unwrap().data, + mmr.get_delta(Forest::new(5).unwrap(), mmr.forest()).unwrap().data, vec![LEAVES[5], acc.peaks()[2]], "one sibling, one peak" ); // missing the whole last two trees, only send the peaks assert_eq!( - mmr.get_delta(Forest::new(4), mmr.forest()).unwrap().data, + mmr.get_delta(Forest::new(4).unwrap(), mmr.forest()).unwrap().data, vec![acc.peaks()[1], acc.peaks()[2]], "two peaks" ); // missing the sibling to complete the first tree, and the two last trees assert_eq!( - mmr.get_delta(Forest::new(3), mmr.forest()).unwrap().data, + mmr.get_delta(Forest::new(3).unwrap(), mmr.forest()).unwrap().data, vec![LEAVES[3], acc.peaks()[1], acc.peaks()[2]], "one sibling, two peaks" ); @@ -1039,13 +1227,13 @@ fn test_mmr_delta() { // missing half of the first tree, only send the computed element (not the leaves), and the new // peaks assert_eq!( - mmr.get_delta(Forest::new(2), mmr.forest()).unwrap().data, + mmr.get_delta(Forest::new(2).unwrap(), mmr.forest()).unwrap().data, vec![mmr.nodes[5], acc.peaks()[1], acc.peaks()[2]], "one sibling, two peaks" ); assert_eq!( - mmr.get_delta(Forest::new(1), mmr.forest()).unwrap().data, + mmr.get_delta(Forest::new(1).unwrap(), mmr.forest()).unwrap().data, vec![LEAVES[1], mmr.nodes[5], acc.peaks()[1], acc.peaks()[2]], "one sibling, two peaks" ); @@ -1059,16 +1247,19 @@ fn test_mmr_delta() { #[test] fn test_mmr_delta_old_forest() { - let mmr: Mmr = LEAVES.into(); + let mmr = Mmr::try_from_iter(LEAVES.iter().copied()).unwrap(); // from_forest must be smaller-or-equal to to_forest for version in 1..=mmr.forest().num_leaves() { - assert!(mmr.get_delta(Forest::new(version + 1), Forest::new(version)).is_err()); + assert!( + mmr.get_delta(Forest::new(version + 1).unwrap(), Forest::new(version).unwrap()) + .is_err() + ); } // when from_forest and to_forest are equal, there are no updates for version in 1..=mmr.forest().num_leaves() { - let version = Forest::new(version); + let version = Forest::new(version).unwrap(); let delta = mmr.get_delta(version, version).unwrap(); assert!(delta.data.is_empty()); assert_eq!(delta.forest, version); @@ -1078,8 +1269,8 @@ fn test_mmr_delta_old_forest() { for count in 0..(mmr.forest().num_leaves() / 2) { // *2 because every iteration tests a pair // +1 because the Mmr is 1-indexed - let from_forest = Forest::new((count * 2) + 1); - let to_forest = Forest::new((count * 2) + 2); + let from_forest = Forest::new((count * 2) + 1).unwrap(); + let to_forest = Forest::new((count * 2) + 2).unwrap(); let delta = mmr.get_delta(from_forest, to_forest).unwrap(); // *2 because every iteration tests a pair @@ -1089,20 +1280,20 @@ fn test_mmr_delta_old_forest() { assert_eq!(delta.forest, to_forest); } - let version = Forest::new(4); - let delta = mmr.get_delta(Forest::new(1), version).unwrap(); + let version = Forest::new(4).unwrap(); + let delta = mmr.get_delta(Forest::new(1).unwrap(), version).unwrap(); assert_eq!(delta.data, [mmr.nodes[1], mmr.nodes[5]]); assert_eq!(delta.forest, version); - let version = Forest::new(5); - let delta = mmr.get_delta(Forest::new(1), version).unwrap(); + let version = Forest::new(5).unwrap(); + let delta = mmr.get_delta(Forest::new(1).unwrap(), version).unwrap(); assert_eq!(delta.data, [mmr.nodes[1], mmr.nodes[5], mmr.nodes[7]]); assert_eq!(delta.forest, version); } #[test] fn test_partial_mmr_simple() { - let mmr: Mmr = LEAVES.into(); + let mmr = Mmr::try_from_iter(LEAVES.iter().copied()).unwrap(); let peaks = mmr.peaks(); let mut partial: PartialMmr = peaks.clone().into(); @@ -1153,7 +1344,7 @@ fn test_partial_mmr_simple() { fn test_partial_mmr_update_single() { let mut full = Mmr::new(); let zero = int_to_node(0); - full.add(zero); + full.add(zero).unwrap(); let mut partial: PartialMmr = full.peaks().into(); let proof = full.open(0).unwrap(); @@ -1163,7 +1354,7 @@ fn test_partial_mmr_update_single() { for i in 1..100 { let node = int_to_node(i as u64); - full.add(node); + full.add(node).unwrap(); let delta = full.get_delta(partial.forest(), full.forest()).unwrap(); partial.apply(delta).unwrap(); @@ -1181,9 +1372,9 @@ fn test_partial_mmr_update_single() { #[test] fn test_mmr_add_invalid_odd_leaf() { - let mmr: Mmr = LEAVES.into(); + let mmr = Mmr::try_from_iter(LEAVES.iter().copied()).unwrap(); let acc = mmr.peaks(); - let mut partial: PartialMmr = acc.clone().into(); + let mut partial: PartialMmr = acc.into(); let empty = MerklePath::new(Vec::new()); @@ -1197,16 +1388,57 @@ fn test_mmr_add_invalid_odd_leaf() { assert!(result.is_ok()); } +#[test] +fn test_partial_track_rejects_path_depth_too_large() { + let leaf = int_to_node(1); + let mut full = Mmr::new(); + full.add(leaf).unwrap(); + let mut partial: PartialMmr = full.peaks().into(); + + let deep_path = MerklePath::new(vec![Word::empty(); u8::MAX as usize]); + let result = partial.track(0, leaf, &deep_path); + assert_matches!(result, Err(MmrError::UnknownPeak(depth)) if depth == u8::MAX); +} + +#[test] +fn test_get_delta_and_apply_never_return_forest_size_exceeded_for_valid_forests() { + let mut full = Mmr::new(); + full.add(int_to_node(0)).unwrap(); + let mut partial: PartialMmr = full.peaks().into(); + + for i in 1..256 { + full.add(int_to_node(i as u64)).unwrap(); + + let delta = match full.get_delta(partial.forest(), full.forest()) { + Ok(delta) => delta, + Err(MmrError::ForestSizeExceeded { .. }) => { + panic!("valid get_delta range should not exceed forest bounds") + }, + Err(err) => panic!("unexpected get_delta error: {err}"), + }; + + if let Err(err) = partial.apply(delta) { + match err { + MmrError::ForestSizeExceeded { .. } => { + panic!("applying a valid delta should not exceed forest bounds") + }, + _ => panic!("unexpected apply error: {err}"), + } + } + } +} + /// Tests that a proof whose peak count exceeds the peak count of the MMR returns an error. /// /// Here we manipulate the proof to return a peak index of 1 while the MMR only has 1 peak (with /// index 0). #[test] fn test_mmr_proof_num_peaks_exceeds_current_num_peaks() { - let mmr: Mmr = LEAVES[0..4].iter().cloned().into(); + let mmr = Mmr::try_from_iter(LEAVES[0..4].iter().cloned()).unwrap(); let original_proof = mmr.open(3).unwrap(); // Create an invalid proof with wrong forest and position - let invalid_path = MmrPath::new(Forest::new(5), 4, original_proof.path().merkle_path().clone()); + let invalid_path = + MmrPath::new(Forest::new(5).unwrap(), 4, original_proof.path().merkle_path().clone()); let invalid_proof = MmrProof::new(invalid_path, original_proof.leaf()); let err = mmr.peaks().verify(LEAVES[3], invalid_proof).unwrap_err(); assert_matches!( @@ -1224,13 +1456,13 @@ fn test_mmr_proof_num_peaks_exceeds_current_num_peaks() { #[test] fn test_mmr_old_proof_num_peaks_exceeds_current_num_peaks() { let leaves_len = 3; - let mut mmr = Mmr::from(LEAVES[0..leaves_len].iter().cloned()); + let mut mmr = Mmr::try_from_iter(LEAVES[0..leaves_len].iter().cloned()).unwrap(); let leaf_idx = leaves_len - 1; let proof = mmr.open(leaf_idx).unwrap(); assert!(mmr.peaks().verify(LEAVES[leaf_idx], proof.clone()).is_ok()); - mmr.add(LEAVES[leaves_len]); + mmr.add(LEAVES[leaves_len]).unwrap(); let err = mmr.peaks().verify(LEAVES[leaf_idx], proof).unwrap_err(); assert_matches!( err, @@ -1242,11 +1474,11 @@ fn test_mmr_old_proof_num_peaks_exceeds_current_num_peaks() { mod property_tests { use proptest::prelude::*; - use super::leaf_to_corresponding_tree; + use super::{Forest, leaf_to_corresponding_tree}; proptest! { #[test] - fn test_last_position_is_always_contained_in_the_last_tree(leaves in any::().prop_filter("can't have an empty tree", |v| *v != 0)) { + fn test_last_position_is_always_contained_in_the_last_tree(leaves in 1..=Forest::MAX_LEAVES) { let last_pos = leaves - 1; let lowest_bit = leaves.trailing_zeros(); @@ -1259,7 +1491,7 @@ mod property_tests { proptest! { #[test] - fn test_contained_tree_is_always_power_of_two((leaves, pos) in any::().prop_flat_map(|v| (Just(v), 0..v))) { + fn test_contained_tree_is_always_power_of_two((leaves, pos) in (1..=Forest::MAX_LEAVES).prop_flat_map(|v| (Just(v), 0..v))) { let tree_bit = leaf_to_corresponding_tree(pos, leaves).expect("pos is smaller than leaves, there should always be a corresponding tree"); let mask = 1usize << tree_bit; @@ -1284,10 +1516,10 @@ fn merge(l: Word, r: Word) -> Word { /// Given a leaf index and the current forest, return the tree number responsible for /// the position. fn leaf_to_corresponding_tree(leaf_idx: usize, forest: usize) -> Option { - Forest::new(forest).leaf_to_corresponding_tree(leaf_idx) + Forest::new(forest).unwrap().leaf_to_corresponding_tree(leaf_idx) } /// Return the total number of nodes of a given forest -const fn nodes_in_forest(forest: usize) -> usize { - Forest::new(forest).num_nodes() +fn nodes_in_forest(forest: usize) -> usize { + nodes_from_mask(forest) } diff --git a/miden-crypto/src/merkle/mod.rs b/miden-crypto/src/merkle/mod.rs index 2458e3b69f..9d1539fafe 100644 --- a/miden-crypto/src/merkle/mod.rs +++ b/miden-crypto/src/merkle/mod.rs @@ -36,11 +36,11 @@ pub use sparse_path::SparseMerklePath; #[cfg(test)] const fn int_to_node(value: u64) -> Word { use super::ZERO; - Word::new([Felt::new(value), ZERO, ZERO, ZERO]) + Word::new([Felt::new_unchecked(value), ZERO, ZERO, ZERO]) } #[cfg(test)] const fn int_to_leaf(value: u64) -> Word { use super::ZERO; - Word::new([Felt::new(value), ZERO, ZERO, ZERO]) + Word::new([Felt::new_unchecked(value), ZERO, ZERO, ZERO]) } diff --git a/miden-crypto/src/merkle/smt/forest/mod.rs b/miden-crypto/src/merkle/smt/forest/mod.rs index efbc08213d..3cabb728d6 100644 --- a/miden-crypto/src/merkle/smt/forest/mod.rs +++ b/miden-crypto/src/merkle/smt/forest/mod.rs @@ -25,7 +25,7 @@ mod tests; /// /// ```rust /// use miden_crypto::{ -/// Felt, ONE, WORD_SIZE, Word, ZERO, +/// Felt, ONE, Word, ZERO, /// merkle::{ /// EmptySubtreeRoots, /// smt::{MAX_LEAF_ENTRIES, SMT_DEPTH, SmtForest}, @@ -37,15 +37,15 @@ mod tests; /// /// // Insert a key-value pair into an SMT with an empty root /// let empty_tree_root = *EmptySubtreeRoots::entry(SMT_DEPTH, 0); -/// let key = Word::new([ZERO; WORD_SIZE]); -/// let value = Word::new([ONE; WORD_SIZE]); +/// let key = Word::new([ZERO; Word::NUM_ELEMENTS]); +/// let value = Word::new([ONE; Word::NUM_ELEMENTS]); /// let new_root = forest.insert(empty_tree_root, key, value).unwrap(); /// /// // Insert multiple key-value pairs /// let mut entries = Vec::new(); /// for i in 0..MAX_LEAF_ENTRIES { -/// let key = Word::new([Felt::new(i as u64); WORD_SIZE]); -/// let value = Word::new([Felt::new((i + 1) as u64); WORD_SIZE]); +/// let key = Word::new([Felt::new_unchecked(i as u64); Word::NUM_ELEMENTS]); +/// let value = Word::new([Felt::new_unchecked((i + 1) as u64); Word::NUM_ELEMENTS]); /// entries.push((key, value)); /// } /// let new_root = forest.batch_insert(new_root, entries.into_iter()).unwrap(); diff --git a/miden-crypto/src/merkle/smt/forest/tests.rs b/miden-crypto/src/merkle/smt/forest/tests.rs index 5a2b067309..c878724bfd 100644 --- a/miden-crypto/src/merkle/smt/forest/tests.rs +++ b/miden-crypto/src/merkle/smt/forest/tests.rs @@ -3,7 +3,7 @@ use itertools::Itertools; use super::{EmptySubtreeRoots, MerkleError, SmtForest, Word}; use crate::{ - EMPTY_WORD, Felt, ONE, WORD_SIZE, ZERO, + EMPTY_WORD, Felt, ONE, ZERO, merkle::{ int_to_node, smt::{SMT_DEPTH, SmtProofError}, @@ -16,7 +16,7 @@ use crate::{ #[test] fn test_insert_root_not_in_store() -> Result<(), MerkleError> { let mut forest = SmtForest::new(); - let word = Word::new([ONE; WORD_SIZE]); + let word = Word::new([ONE; Word::NUM_ELEMENTS]); assert_matches!( forest.insert(word, word, word), Err(MerkleError::RootNotInStore(_)), @@ -30,15 +30,15 @@ fn test_insert_root_not_in_store() -> Result<(), MerkleError> { fn test_insert_root_empty() -> Result<(), MerkleError> { let mut forest = SmtForest::new(); let empty_tree_root = *EmptySubtreeRoots::entry(SMT_DEPTH, 0); - let key = Word::new([ZERO; WORD_SIZE]); - let value = Word::new([ONE; WORD_SIZE]); + let key = Word::new([ZERO; Word::NUM_ELEMENTS]); + let value = Word::new([ONE; Word::NUM_ELEMENTS]); assert_eq!( forest.insert(empty_tree_root, key, value)?, Word::new([ - Felt::new(10282719876465040359), - Felt::new(11409448441631035650), - Felt::new(5182120309170345651), - Felt::new(13734097025026540107), + Felt::new_unchecked(14568730562832515847), + Felt::new_unchecked(18252916646450022498), + Felt::new_unchecked(41434158889285279), + Felt::new_unchecked(9206344219167471937), ]), ); Ok(()) @@ -49,16 +49,16 @@ fn test_insert_multiple_values() -> Result<(), MerkleError> { let mut forest = SmtForest::new(); let empty_tree_root = *EmptySubtreeRoots::entry(SMT_DEPTH, 0); - let key = Word::new([ZERO; WORD_SIZE]); - let value = Word::new([ONE; WORD_SIZE]); + let key = Word::new([ZERO; Word::NUM_ELEMENTS]); + let value = Word::new([ONE; Word::NUM_ELEMENTS]); let new_root = forest.insert(empty_tree_root, key, value)?; assert_eq!( new_root, Word::new([ - Felt::new(10282719876465040359), - Felt::new(11409448441631035650), - Felt::new(5182120309170345651), - Felt::new(13734097025026540107), + Felt::new_unchecked(14568730562832515847), + Felt::new_unchecked(18252916646450022498), + Felt::new_unchecked(41434158889285279), + Felt::new_unchecked(9206344219167471937), ]), ); @@ -66,10 +66,10 @@ fn test_insert_multiple_values() -> Result<(), MerkleError> { assert_eq!( new_root, Word::new([ - Felt::new(10282719876465040359), - Felt::new(11409448441631035650), - Felt::new(5182120309170345651), - Felt::new(13734097025026540107), + Felt::new_unchecked(14568730562832515847), + Felt::new_unchecked(18252916646450022498), + Felt::new_unchecked(41434158889285279), + Felt::new_unchecked(9206344219167471937), ]), ); @@ -82,10 +82,10 @@ fn test_insert_multiple_values() -> Result<(), MerkleError> { assert_eq!( new_root, Word::new([ - Felt::new(13926725651708849655), - Felt::new(15133040102807981886), - Felt::new(13129040520743261049), - Felt::new(12502921173243968881), + Felt::new_unchecked(8331046026464464586), + Felt::new_unchecked(2235589849047307808), + Felt::new_unchecked(16989070170732558432), + Felt::new_unchecked(14827437307365892589), ]) ); @@ -99,9 +99,9 @@ fn test_batch_insert() -> Result<(), MerkleError> { let empty_tree_root = *EmptySubtreeRoots::entry(SMT_DEPTH, 0); let values = vec![ - (Word::new([ZERO; WORD_SIZE]), Word::new([ONE; WORD_SIZE])), - (Word::new([ZERO, ONE, ZERO, ONE]), Word::new([ONE; WORD_SIZE])), - (Word::new([ZERO, ONE, ZERO, ZERO]), Word::new([ONE; WORD_SIZE])), + (Word::new([ZERO; Word::NUM_ELEMENTS]), Word::new([ONE; Word::NUM_ELEMENTS])), + (Word::new([ZERO, ONE, ZERO, ONE]), Word::new([ONE; Word::NUM_ELEMENTS])), + (Word::new([ZERO, ONE, ZERO, ZERO]), Word::new([ONE; Word::NUM_ELEMENTS])), ]; values.into_iter().permutations(3).for_each(|values| { @@ -111,10 +111,10 @@ fn test_batch_insert() -> Result<(), MerkleError> { assert_eq!( new_root, Word::new([ - Felt::new(14549016523344139792), - Felt::new(9300503680013959685), - Felt::new(16332664079126957671), - Felt::new(13970928800244566339), + Felt::new_unchecked(10190519849202762248), + Felt::new_unchecked(435931819697066051), + Felt::new_unchecked(16151289788138594836), + Felt::new_unchecked(9391498722098326251), ]) ); @@ -130,7 +130,7 @@ fn test_batch_insert() -> Result<(), MerkleError> { #[test] fn test_open_root_not_in_store() -> Result<(), MerkleError> { let forest = SmtForest::new(); - let word = Word::new([ONE; WORD_SIZE]); + let word = Word::new([ONE; Word::NUM_ELEMENTS]); assert_matches!( forest.open(word, word), Err(MerkleError::RootNotInStore(_)), @@ -147,25 +147,52 @@ fn test_open_root_in_store() -> Result<(), MerkleError> { let root = *EmptySubtreeRoots::entry(SMT_DEPTH, 0); let root = forest.insert( root, - Word::new([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(0)]), + Word::new([ + Felt::new_unchecked(0), + Felt::new_unchecked(0), + Felt::new_unchecked(0), + Felt::new_unchecked(0), + ]), int_to_node(1), )?; let root = forest.insert( root, - Word::new([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(1)]), + Word::new([ + Felt::new_unchecked(0), + Felt::new_unchecked(0), + Felt::new_unchecked(0), + Felt::new_unchecked(1), + ]), int_to_node(2), )?; let root = forest.insert( root, - Word::new([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(2)]), + Word::new([ + Felt::new_unchecked(0), + Felt::new_unchecked(0), + Felt::new_unchecked(0), + Felt::new_unchecked(2), + ]), int_to_node(3), )?; - let proof = - forest.open(root, Word::new([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(2)]))?; + let proof = forest.open( + root, + Word::new([ + Felt::new_unchecked(0), + Felt::new_unchecked(0), + Felt::new_unchecked(0), + Felt::new_unchecked(2), + ]), + )?; proof .verify_presence( - &Word::new([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(2)]), + &Word::new([ + Felt::new_unchecked(0), + Felt::new_unchecked(0), + Felt::new_unchecked(0), + Felt::new_unchecked(2), + ]), &int_to_node(3), &root, ) @@ -178,8 +205,8 @@ fn test_open_root_in_store() -> Result<(), MerkleError> { fn test_empty_word_removes_key() -> Result<(), MerkleError> { let mut forest = SmtForest::new(); let empty_root = *EmptySubtreeRoots::entry(SMT_DEPTH, 0); - let key = Word::from([1_u32; WORD_SIZE]); - let value = Word::from([2_u32; WORD_SIZE]); + let key = Word::from([1_u32; Word::NUM_ELEMENTS]); + let value = Word::from([2_u32; Word::NUM_ELEMENTS]); let root_with_value = forest.insert(empty_root, key, value)?; let root_after_remove = forest.insert(root_with_value, key, EMPTY_WORD)?; @@ -200,16 +227,16 @@ fn test_multiple_versions_of_same_key() -> Result<(), MerkleError> { let mut forest = SmtForest::new(); let empty_tree_root = *EmptySubtreeRoots::entry(SMT_DEPTH, 0); - let key = Word::new([ZERO; WORD_SIZE]); + let key = Word::new([ZERO; Word::NUM_ELEMENTS]); // Insert the same key with different values, creating multiple roots - let value1 = Word::new([ONE; WORD_SIZE]); + let value1 = Word::new([ONE; Word::NUM_ELEMENTS]); let root1 = forest.insert(empty_tree_root, key, value1)?; - let value2 = Word::new([Felt::new(2); WORD_SIZE]); + let value2 = Word::new([Felt::new_unchecked(2); Word::NUM_ELEMENTS]); let root2 = forest.insert(root1, key, value2)?; - let value3 = Word::new([Felt::new(3); WORD_SIZE]); + let value3 = Word::new([Felt::new_unchecked(3); Word::NUM_ELEMENTS]); let root3 = forest.insert(root2, key, value3)?; // All three roots should be different @@ -254,8 +281,8 @@ fn test_pop_roots() -> Result<(), MerkleError> { let mut forest = SmtForest::new(); let empty_tree_root = *EmptySubtreeRoots::entry(SMT_DEPTH, 0); - let key = Word::new([ZERO; WORD_SIZE]); - let value = Word::new([ONE; WORD_SIZE]); + let key = Word::new([ZERO; Word::NUM_ELEMENTS]); + let value = Word::new([ONE; Word::NUM_ELEMENTS]); let root = forest.insert(empty_tree_root, key, value)?; assert_eq!(forest.roots.len(), 1); @@ -274,8 +301,8 @@ fn test_pop_and_reinsert_same_tree() -> Result<(), MerkleError> { let mut forest = SmtForest::new(); let empty_tree_root = *EmptySubtreeRoots::entry(SMT_DEPTH, 0); - let key = Word::new([ZERO; WORD_SIZE]); - let value = Word::new([ONE; WORD_SIZE]); + let key = Word::new([ZERO; Word::NUM_ELEMENTS]); + let value = Word::new([ONE; Word::NUM_ELEMENTS]); // Insert a key, then pop the tree let root1 = forest.insert(empty_tree_root, key, value)?; @@ -302,7 +329,7 @@ fn test_pop_and_reinsert_same_tree() -> Result<(), MerkleError> { fn test_removing_empty_smt_from_forest() { let mut forest = SmtForest::new(); let empty_tree_root = *EmptySubtreeRoots::entry(SMT_DEPTH, 0); - let non_empty_root = Word::new([ONE; WORD_SIZE]); + let non_empty_root = Word::new([ONE; Word::NUM_ELEMENTS]); // Popping zero SMTs from forest should be a no-op (no panic or error) forest.pop_smts(vec![]); @@ -320,8 +347,8 @@ fn test_empty_root_never_removed() -> Result<(), MerkleError> { // popping it does not corrupt the store. let mut forest = SmtForest::new(); let empty_root = *EmptySubtreeRoots::entry(SMT_DEPTH, 0); - let key = Word::new([ZERO; WORD_SIZE]); - let value = Word::new([ONE; WORD_SIZE]); + let key = Word::new([ZERO; Word::NUM_ELEMENTS]); + let value = Word::new([ONE; Word::NUM_ELEMENTS]); // batch_insert with no entries returns the empty root — it must not be registered let root = forest.batch_insert(empty_root, vec![])?; diff --git a/miden-crypto/src/merkle/smt/full/concurrent/mod.rs b/miden-crypto/src/merkle/smt/full/concurrent/mod.rs index d3c7a5b948..2a18b3ecaa 100644 --- a/miden-crypto/src/merkle/smt/full/concurrent/mod.rs +++ b/miden-crypto/src/merkle/smt/full/concurrent/mod.rs @@ -6,7 +6,7 @@ use p3_maybe_rayon::prelude::*; use super::{ EmptySubtreeRoots, InnerNode, InnerNodes, Leaves, MerkleError, MutationSet, NodeIndex, - SMT_DEPTH, Smt, SmtLeaf, SparseMerkleTree, Word, leaf, + SMT_DEPTH, Smt, SmtLeaf, SparseMerkleTree, Word, }; use crate::merkle::smt::{Map, NodeMutation, NodeMutations, SmtLeafError}; @@ -103,10 +103,13 @@ impl Smt { where Self: Sized + Sync, { - // Collect and sort key-value pairs by their corresponding leaf index + // Collect and sort key-value pairs by their corresponding leaf index and then their key + // value. let mut sorted_kv_pairs: Vec<_> = kv_pairs.into_iter().collect(); - sorted_kv_pairs - .par_sort_unstable_by_key(|(key, _)| Self::key_to_leaf_index(key).position()); + sorted_kv_pairs.par_sort_unstable_by_key(|(key, _)| *key); + + // After sorting, check for duplicate keys which are adjacent after the sort. + Self::check_for_duplicate_keys(&sorted_kv_pairs)?; // Convert sorted pairs into mutated leaves and capture any new pairs let (mut subtree_leaves, new_pairs) = @@ -276,13 +279,9 @@ impl Smt { assert!(!pairs.is_empty()); if pairs.len() > 1 { - pairs.sort_by(|(key_1, _), (key_2, _)| leaf::cmp_keys(*key_1, *key_2)); + pairs.sort_by(|(key_1, _), (key_2, _)| key_1.cmp(key_2)); // Check for duplicates in a sorted list by comparing adjacent pairs - if let Some(window) = pairs.windows(2).find(|window| window[0].0 == window[1].0) { - // If we find a duplicate, return an error - let col = Self::key_to_leaf_index(&window[0].0).index.position(); - return Err(MerkleError::DuplicateValuesForIndex(col)); - } + Self::check_for_duplicate_keys(&pairs)?; Ok(Some(SmtLeaf::new_multiple(pairs).unwrap())) } else { let (key, value) = pairs.pop().unwrap(); diff --git a/miden-crypto/src/merkle/smt/full/concurrent/tests.rs b/miden-crypto/src/merkle/smt/full/concurrent/tests.rs index 7ddc227c02..2dff3c4280 100644 --- a/miden-crypto/src/merkle/smt/full/concurrent/tests.rs +++ b/miden-crypto/src/merkle/smt/full/concurrent/tests.rs @@ -31,16 +31,22 @@ fn smtleaf_to_subtree_leaf(leaf: &SmtLeaf) -> SubtreeLeaf { fn test_sorted_pairs_to_leaves() { let entries: Vec<(Word, Word)> = vec![ // Subtree 0. - ([ONE, ONE, ONE, Felt::new(16)].into(), [ONE; 4].into()), - ([ONE, ONE, ONE, Felt::new(17)].into(), [ONE; 4].into()), + ([ONE, ONE, ONE, Felt::new_unchecked(16)].into(), [ONE; 4].into()), + ([ONE, ONE, ONE, Felt::new_unchecked(17)].into(), [ONE; 4].into()), // Leaf index collision. - ([ONE, ONE, Felt::new(10), Felt::new(20)].into(), [ONE; 4].into()), - ([ONE, ONE, Felt::new(20), Felt::new(20)].into(), [ONE; 4].into()), + ( + [ONE, ONE, Felt::new_unchecked(10), Felt::new_unchecked(20)].into(), + [ONE; 4].into(), + ), + ( + [ONE, ONE, Felt::new_unchecked(20), Felt::new_unchecked(20)].into(), + [ONE; 4].into(), + ), // Subtree 1. Normal single leaf again. - ([ONE, ONE, ONE, Felt::new(400)].into(), [ONE; 4].into()), // Subtree boundary. - ([ONE, ONE, ONE, Felt::new(401)].into(), [ONE; 4].into()), + ([ONE, ONE, ONE, Felt::new_unchecked(400)].into(), [ONE; 4].into()), // Subtree boundary. + ([ONE, ONE, ONE, Felt::new_unchecked(401)].into(), [ONE; 4].into()), // Subtree 2. Another normal leaf. - ([ONE, ONE, ONE, Felt::new(1024)].into(), [ONE; 4].into()), + ([ONE, ONE, ONE, Felt::new_unchecked(1024)].into(), [ONE; 4].into()), ]; let control = Smt::with_entries_sequential(entries.clone()).unwrap(); @@ -108,8 +114,9 @@ fn generate_entries(pair_count: u64) -> Vec<(Word, Word)> { (0..pair_count) .map(|i| { let leaf_index = ((i as f64 / pair_count as f64) * (pair_count as f64)) as u64; - let key = Word::new([ONE, ONE, Felt::new(i), Felt::new(leaf_index)]); - let value = Word::new([ONE, ONE, ONE, Felt::new(i)]); + let key = + Word::new([ONE, ONE, Felt::new_unchecked(i), Felt::new_unchecked(leaf_index)]); + let value = Word::new([ONE, ONE, ONE, Felt::new_unchecked(i)]); (key, value) }) .collect() @@ -131,7 +138,7 @@ fn generate_updates(entries: Vec<(Word, Word)>, updates: usize) -> Vec<(Word, Wo let value = if rng.random_bool(REMOVAL_PROBABILITY) { EMPTY_WORD } else { - Word::new([ONE, ONE, ONE, Felt::new(rng.random())]) + Word::new([ONE, ONE, ONE, Felt::new_unchecked(rng.random())]) }; (key, value) }) @@ -194,7 +201,7 @@ fn test_two_subtrees() { let total_computed = first_nodes.len() + second_nodes.len() + next_leaves.len(); assert_eq!(total_computed as u64, PAIR_COUNT); // Verify the computed nodes of both subtrees. - let computed_nodes = first_nodes.clone().into_iter().chain(second_nodes); + let computed_nodes = first_nodes.into_iter().chain(second_nodes); for (index, test_node) in computed_nodes { let control_node = control.get_inner_node(index); assert_eq!( @@ -402,7 +409,7 @@ fn test_singlethreaded_subtree_mutations() { const PAIR_COUNT: u64 = COLS_PER_SUBTREE * 64; let entries = generate_entries(PAIR_COUNT); let updates = generate_updates(entries.clone(), 1000); - let tree = Smt::with_entries_sequential(entries.clone()).unwrap(); + let tree = Smt::with_entries_sequential(entries).unwrap(); let control = tree.compute_mutations_sequential(updates.clone()).unwrap(); let mut node_mutations = NodeMutations::default(); let (mut subtree_leaves, new_pairs) = @@ -475,7 +482,7 @@ fn test_compute_mutations_parallel() { #[test] fn test_smt_construction_with_entries_unsorted() { let entries = [ - ([ONE, ONE, Felt::new(2_u64), ONE].into(), [ONE; 4].into()), + ([ONE, ONE, Felt::new_unchecked(2_u64), ONE].into(), [ONE; 4].into()), ([ONE; 4].into(), [ONE; 4].into()), ]; let control = Smt::with_entries_sequential(entries).unwrap(); @@ -487,9 +494,9 @@ fn test_smt_construction_with_entries_unsorted() { #[test] fn test_smt_construction_with_entries_duplicate_keys() { let entries = [ - ([ONE, ONE, ONE, Felt::new(16)].into(), [ONE; 4].into()), + ([ONE, ONE, ONE, Felt::new_unchecked(16)].into(), [ONE; 4].into()), ([ONE; 4].into(), [ONE; 4].into()), - ([ONE, ONE, ONE, Felt::new(16)].into(), [ONE; 4].into()), + ([ONE, ONE, ONE, Felt::new_unchecked(16)].into(), [ONE; 4].into()), ]; let expected_col = Smt::key_to_leaf_index(&entries[0].0).index.position(); let err = Smt::with_entries(entries).unwrap_err(); @@ -500,7 +507,7 @@ fn test_smt_construction_with_entries_duplicate_keys() { fn test_smt_construction_with_some_empty_values() { let entries = [ ([ONE, ONE, ONE, ONE].into(), Smt::EMPTY_VALUE), - ([ONE, ONE, ONE, Felt::new(2)].into(), [ONE; 4].into()), + ([ONE, ONE, ONE, Felt::new_unchecked(2)].into(), [ONE; 4].into()), ]; let result = Smt::with_entries(entries); @@ -543,7 +550,7 @@ fn test_smt_construction_with_no_entries() { } fn arb_felt() -> impl Strategy { - prop_oneof![any::().prop_map(Felt::new), Just(ZERO), Just(ONE),] + prop_oneof![any::().prop_map(Felt::new_unchecked), Just(ZERO), Just(ONE),] } /// Test that the debug assertion panics on unsorted entries. @@ -553,12 +560,34 @@ fn smt_with_sorted_entries_panics_on_unsorted_entries() { // Unsorted keys. let smt_leaves_2: [(Word, Word); 2] = [ ( - Word::new([Felt::new(105), Felt::new(106), Felt::new(107), Felt::new(108)]), - [Felt::new(5_u64), Felt::new(6_u64), Felt::new(7_u64), Felt::new(8_u64)].into(), + Word::new([ + Felt::new_unchecked(105), + Felt::new_unchecked(106), + Felt::new_unchecked(107), + Felt::new_unchecked(108), + ]), + [ + Felt::new_unchecked(5_u64), + Felt::new_unchecked(6_u64), + Felt::new_unchecked(7_u64), + Felt::new_unchecked(8_u64), + ] + .into(), ), ( - Word::new([Felt::new(101), Felt::new(102), Felt::new(103), Felt::new(104)]), - [Felt::new(1_u64), Felt::new(2_u64), Felt::new(3_u64), Felt::new(4_u64)].into(), + Word::new([ + Felt::new_unchecked(101), + Felt::new_unchecked(102), + Felt::new_unchecked(103), + Felt::new_unchecked(104), + ]), + [ + Felt::new_unchecked(1_u64), + Felt::new_unchecked(2_u64), + Felt::new_unchecked(3_u64), + Felt::new_unchecked(4_u64), + ] + .into(), ), ]; @@ -584,8 +613,8 @@ fn generate_cross_subtree_entries() -> impl Strategy> offsets .into_iter() .map(|base_col| { - let key = Word::new([ONE, ONE, ONE, Felt::new(base_col)]); - let value = Word::new([ONE, ONE, ONE, Felt::new(base_col)]); + let key = Word::new([ONE, ONE, ONE, Felt::new_unchecked(base_col)]); + let value = Word::new([ONE, ONE, ONE, Felt::new_unchecked(base_col)]); (key, value) }) .collect() @@ -605,8 +634,8 @@ fn arb_entries() -> impl Strategy> { ), // Edge case values ( - Just([ONE, ONE, ONE, Felt::new(0)].into()), - Just([ONE, ONE, ONE, Felt::new(u64::MAX)].into()) + Just([ONE, ONE, ONE, Felt::new_unchecked(0)].into()), + Just([ONE, ONE, ONE, Felt::new_unchecked(u64::MAX)].into()) ) ], 1..1000, @@ -650,7 +679,7 @@ proptest! { #[test] fn test_with_entries_consistency(entries in arb_entries()) { let sequential = Smt::with_entries_sequential(entries.clone()).unwrap(); - let concurrent = Smt::with_entries(entries.clone()).unwrap(); + let concurrent = Smt::with_entries(entries).unwrap(); prop_assert_eq!(concurrent, sequential); } @@ -672,7 +701,7 @@ proptest! { }); let sequential = tree.compute_mutations_sequential(update_entries.clone()).unwrap(); - let concurrent = tree.compute_mutations(update_entries.clone()).unwrap(); + let concurrent = tree.compute_mutations(update_entries).unwrap(); // If there are real changes, the root should change if has_real_changes { diff --git a/miden-crypto/src/merkle/smt/full/leaf.rs b/miden-crypto/src/merkle/smt/full/leaf.rs index 1894aca980..f5d8a40d6c 100644 --- a/miden-crypto/src/merkle/smt/full/leaf.rs +++ b/miden-crypto/src/merkle/smt/full/leaf.rs @@ -1,5 +1,4 @@ use alloc::{string::ToString, vec::Vec}; -use core::cmp::Ordering; use super::EMPTY_WORD; use crate::{ @@ -314,13 +313,13 @@ impl SmtLeaf { // This stays within MAX_LEAF_ENTRIES limit. We're only adding one entry to a // single leaf let mut pairs = vec![*kv_pair, (key, value)]; - pairs.sort_by(|(key_1, _), (key_2, _)| cmp_keys(*key_1, *key_2)); + pairs.sort_by(|(key_1, _), (key_2, _)| key_1.cmp(key_2)); *self = SmtLeaf::Multiple(pairs); Ok(None) } }, SmtLeaf::Multiple(kv_pairs) => { - match kv_pairs.binary_search_by(|kv_pair| cmp_keys(kv_pair.0, key)) { + match kv_pairs.binary_search_by(|kv_pair| kv_pair.0.cmp(&key)) { Ok(pos) => { let old_value = kv_pairs[pos].1; kv_pairs[pos].1 = value; @@ -363,7 +362,7 @@ impl SmtLeaf { } }, SmtLeaf::Multiple(kv_pairs) => { - match kv_pairs.binary_search_by(|kv_pair| cmp_keys(kv_pair.0, key)) { + match kv_pairs.binary_search_by(|kv_pair| kv_pair.0.cmp(&key)) { Ok(pos) => { let old_value = kv_pairs[pos].1; @@ -439,17 +438,3 @@ pub(crate) fn kv_to_elements((key, value): (Word, Word)) -> impl Iterator Ordering { - for (v1, v2) in key_1.iter().zip(key_2.iter()).rev() { - let v1 = (*v1).as_canonical_u64(); - let v2 = (*v2).as_canonical_u64(); - if v1 != v2 { - return v1.cmp(&v2); - } - } - - Ordering::Equal -} diff --git a/miden-crypto/src/merkle/smt/full/mod.rs b/miden-crypto/src/merkle/smt/full/mod.rs index bafe62d384..4d6f4863ae 100644 --- a/miden-crypto/src/merkle/smt/full/mod.rs +++ b/miden-crypto/src/merkle/smt/full/mod.rs @@ -336,7 +336,14 @@ impl Smt { /// [`Smt::apply_mutations()`] can be called in order to commit these changes to the Merkle /// tree, or [`drop()`] to discard them. /// + /// # Errors + /// + /// - [`MerkleError::DuplicateValuesForIndex`] if `kv_pairs` contains the same key more than + /// once. + /// - [`MerkleError::TooManyLeafEntries`] if mutations would exceed 1024 entries in a leaf. + /// /// # Example + /// /// ``` /// # use miden_crypto::{Felt, Word}; /// # use miden_crypto::merkle::{EmptySubtreeRoots, smt::{Smt, SMT_DEPTH}}; @@ -469,7 +476,7 @@ impl SparseMerkleTree for Smt { assert_eq!(root_node_hash, root); } - let num_entries = leaves.values().map(|leaf| leaf.num_entries()).sum(); + let num_entries = leaves.values().map(SmtLeaf::num_entries).sum(); Ok(Self { root, inner_nodes, leaves, num_entries }) } @@ -663,12 +670,34 @@ fn test_smt_serialization_deserialization() { // Smt with values let smt_leaves_2: [(Word, Word); 2] = [ ( - Word::new([Felt::new(105), Felt::new(106), Felt::new(107), Felt::new(108)]), - [Felt::new(5_u64), Felt::new(6_u64), Felt::new(7_u64), Felt::new(8_u64)].into(), + Word::new([ + Felt::new_unchecked(105), + Felt::new_unchecked(106), + Felt::new_unchecked(107), + Felt::new_unchecked(108), + ]), + [ + Felt::new_unchecked(5_u64), + Felt::new_unchecked(6_u64), + Felt::new_unchecked(7_u64), + Felt::new_unchecked(8_u64), + ] + .into(), ), ( - Word::new([Felt::new(101), Felt::new(102), Felt::new(103), Felt::new(104)]), - [Felt::new(1_u64), Felt::new(2_u64), Felt::new(3_u64), Felt::new(4_u64)].into(), + Word::new([ + Felt::new_unchecked(101), + Felt::new_unchecked(102), + Felt::new_unchecked(103), + Felt::new_unchecked(104), + ]), + [ + Felt::new_unchecked(1_u64), + Felt::new_unchecked(2_u64), + Felt::new_unchecked(3_u64), + Felt::new_unchecked(4_u64), + ] + .into(), ), ]; let smt = Smt::with_entries(smt_leaves_2).unwrap(); @@ -683,12 +712,34 @@ fn smt_with_sorted_entries() { // Smt with sorted values let smt_leaves_2: [(Word, Word); 2] = [ ( - Word::new([Felt::new(101), Felt::new(102), Felt::new(103), Felt::new(104)]), - [Felt::new(1_u64), Felt::new(2_u64), Felt::new(3_u64), Felt::new(4_u64)].into(), + Word::new([ + Felt::new_unchecked(101), + Felt::new_unchecked(102), + Felt::new_unchecked(103), + Felt::new_unchecked(104), + ]), + [ + Felt::new_unchecked(1_u64), + Felt::new_unchecked(2_u64), + Felt::new_unchecked(3_u64), + Felt::new_unchecked(4_u64), + ] + .into(), ), ( - Word::new([Felt::new(105), Felt::new(106), Felt::new(107), Felt::new(108)]), - [Felt::new(5_u64), Felt::new(6_u64), Felt::new(7_u64), Felt::new(8_u64)].into(), + Word::new([ + Felt::new_unchecked(105), + Felt::new_unchecked(106), + Felt::new_unchecked(107), + Felt::new_unchecked(108), + ]), + [ + Felt::new_unchecked(5_u64), + Felt::new_unchecked(6_u64), + Felt::new_unchecked(7_u64), + Felt::new_unchecked(8_u64), + ] + .into(), ), ]; diff --git a/miden-crypto/src/merkle/smt/full/tests.rs b/miden-crypto/src/merkle/smt/full/tests.rs index f72ff36095..29e5ee57e4 100644 --- a/miden-crypto/src/merkle/smt/full/tests.rs +++ b/miden-crypto/src/merkle/smt/full/tests.rs @@ -4,7 +4,7 @@ use assert_matches::assert_matches; use super::{EMPTY_WORD, LeafIndex, NodeIndex, SMT_DEPTH, Smt, SmtLeaf}; use crate::{ - Felt, ONE, WORD_SIZE, Word, + Felt, ONE, Word, hash::poseidon2::Poseidon2, merkle::{ EmptySubtreeRoots, @@ -32,12 +32,12 @@ fn test_smt_insert_at_same_key() { let key_1: Word = { let raw = 0b_01101001_01101100_00011111_11111111_10010110_10010011_11100000_00000000_u64; - Word::from([ONE, ONE, ONE, Felt::new(raw)]) + Word::from([ONE, ONE, ONE, Felt::new_unchecked(raw)]) }; let key_1_index: NodeIndex = LeafIndex::::from(key_1).into(); - let value_1 = Word::new([ONE; WORD_SIZE]); - let value_2 = Word::new([ONE + ONE; WORD_SIZE]); + let value_1 = Word::new([ONE; Word::NUM_ELEMENTS]); + let value_2 = Word::new([ONE + ONE; Word::NUM_ELEMENTS]); // Insert value 1 and ensure root is as expected { @@ -69,10 +69,10 @@ fn test_smt_insert_at_same_key_2() { // The most significant u64 used for both keys (to ensure they map to the same leaf) let key_msb: u64 = 42; - let key_already_present = Word::from([2_u64, 2_u64, 2_u64, key_msb].map(Felt::new)); + let key_already_present = Word::from([2_u64, 2_u64, 2_u64, key_msb].map(Felt::new_unchecked)); let key_already_present_index: NodeIndex = LeafIndex::::from(key_already_present).into(); - let value_already_present = Word::new([ONE + ONE + ONE; WORD_SIZE]); + let value_already_present = Word::new([ONE + ONE + ONE; Word::NUM_ELEMENTS]); let mut smt = Smt::with_entries(core::iter::once((key_already_present, value_already_present))).unwrap(); @@ -86,13 +86,13 @@ fn test_smt_insert_at_same_key_2() { store }; - let key_1: Word = Word::from([ONE, ONE, ONE, Felt::new(key_msb)]); + let key_1: Word = Word::from([ONE, ONE, ONE, Felt::new_unchecked(key_msb)]); let key_1_index: NodeIndex = LeafIndex::::from(key_1).into(); assert_eq!(key_1_index, key_already_present_index); - let value_1 = Word::new([ONE; WORD_SIZE]); - let value_2 = Word::new([ONE + ONE; WORD_SIZE]); + let value_1 = Word::new([ONE; Word::NUM_ELEMENTS]); + let value_2 = Word::new([ONE + ONE; Word::NUM_ELEMENTS]); // Insert value 1 and ensure root is as expected { @@ -155,24 +155,24 @@ fn test_smt_insert_and_remove_multiple_values() { let key_1: Word = { let raw = 0b_01101001_01101100_00011111_11111111_10010110_10010011_11100000_00000000_u64; - Word::from([ONE, ONE, ONE, Felt::new(raw)]) + Word::from([ONE, ONE, ONE, Felt::new_unchecked(raw)]) }; let key_2: Word = { let raw = 0b_11111111_11111111_11111111_11111111_11111111_11111111_11111111_11111111_u64; - Word::from([ONE, ONE, ONE, Felt::new(raw)]) + Word::from([ONE, ONE, ONE, Felt::new_unchecked(raw)]) }; let key_3: Word = { let raw = 0b_00000000_00000000_00000000_00000000_00000000_00000000_00000000_00000000_u64; - Word::from([ONE, ONE, ONE, Felt::new(raw)]) + Word::from([ONE, ONE, ONE, Felt::new_unchecked(raw)]) }; - let value_1 = Word::new([ONE; WORD_SIZE]); - let value_2 = Word::new([ONE + ONE; WORD_SIZE]); - let value_3 = Word::new([ONE + ONE + ONE; WORD_SIZE]); + let value_1 = Word::new([ONE; Word::NUM_ELEMENTS]); + let value_2 = Word::new([ONE + ONE; Word::NUM_ELEMENTS]); + let value_3 = Word::new([ONE + ONE + ONE; Word::NUM_ELEMENTS]); // Insert values in the tree let key_values = [(key_1, value_1), (key_2, value_2), (key_3, value_3)]; @@ -232,18 +232,23 @@ fn test_smt_removal() { let raw = 0b_01101001_01101100_00011111_11111111_10010110_10010011_11100000_00000000_u64; - let key_1: Word = Word::from([ONE, ONE, ONE, Felt::new(raw)]); - let key_2: Word = Word::from([Felt::new(2), Felt::new(2), Felt::new(2), Felt::new(raw)]); + let key_1: Word = Word::from([ONE, ONE, ONE, Felt::new_unchecked(raw)]); + let key_2: Word = Word::from([ + Felt::new_unchecked(2), + Felt::new_unchecked(2), + Felt::new_unchecked(2), + Felt::new_unchecked(raw), + ]); let key_3: Word = Word::from([ Felt::from_u32(3_u32), Felt::from_u32(3_u32), Felt::from_u32(3_u32), - Felt::new(raw), + Felt::new_unchecked(raw), ]); - let value_1 = Word::new([ONE; WORD_SIZE]); - let value_2 = Word::new([Felt::from_u32(2_u32); WORD_SIZE]); - let value_3 = Word::new([Felt::from_u32(3_u32); WORD_SIZE]); + let value_1 = Word::new([ONE; Word::NUM_ELEMENTS]); + let value_2 = Word::new([Felt::from_u32(2_u32); Word::NUM_ELEMENTS]); + let value_3 = Word::new([Felt::from_u32(3_u32); Word::NUM_ELEMENTS]); // insert key-value 1 { @@ -312,19 +317,24 @@ fn test_prospective_hash() { let raw = 0b_01101001_01101100_00011111_11111111_10010110_10010011_11100000_00000000_u64; - let key_1: Word = Word::from([ONE, ONE, ONE, Felt::new(raw)]); - let key_2: Word = Word::from([Felt::new(2), Felt::new(2), Felt::new(2), Felt::new(raw)]); + let key_1: Word = Word::from([ONE, ONE, ONE, Felt::new_unchecked(raw)]); + let key_2: Word = Word::from([ + Felt::new_unchecked(2), + Felt::new_unchecked(2), + Felt::new_unchecked(2), + Felt::new_unchecked(raw), + ]); // Sort key_3 before key_1, to test non-append insertion. let key_3: Word = Word::from([ Felt::from_u32(0_u32), Felt::from_u32(0_u32), Felt::from_u32(0_u32), - Felt::new(raw), + Felt::new_unchecked(raw), ]); - let value_1 = Word::new([ONE; WORD_SIZE]); - let value_2 = Word::new([Felt::from_u32(2_u32); WORD_SIZE]); - let value_3 = Word::new([Felt::from_u32(3_u32); WORD_SIZE]); + let value_1 = Word::new([ONE; Word::NUM_ELEMENTS]); + let value_2 = Word::new([Felt::from_u32(2_u32); Word::NUM_ELEMENTS]); + let value_3 = Word::new([Felt::from_u32(3_u32); Word::NUM_ELEMENTS]); // insert key-value 1 { @@ -435,19 +445,24 @@ fn test_prospective_insertion() { let raw = 0b_01101001_01101100_00011111_11111111_10010110_10010011_11100000_00000000_u64; - let key_1: Word = Word::from([ONE, ONE, ONE, Felt::new(raw)]); - let key_2: Word = Word::from([Felt::new(2), Felt::new(2), Felt::new(2), Felt::new(raw)]); + let key_1: Word = Word::from([ONE, ONE, ONE, Felt::new_unchecked(raw)]); + let key_2: Word = Word::from([ + Felt::new_unchecked(2), + Felt::new_unchecked(2), + Felt::new_unchecked(2), + Felt::new_unchecked(raw), + ]); // Sort key_3 before key_1, to test non-append insertion. let key_3: Word = Word::from([ Felt::from_u32(0_u32), Felt::from_u32(0_u32), Felt::from_u32(0_u32), - Felt::new(raw), + Felt::new_unchecked(raw), ]); - let value_1 = Word::new([ONE; WORD_SIZE]); - let value_2 = Word::new([Felt::from_u32(2_u32); WORD_SIZE]); - let value_3 = Word::new([Felt::from_u32(3_u32); WORD_SIZE]); + let value_1 = Word::new([ONE; Word::NUM_ELEMENTS]); + let value_2 = Word::new([Felt::from_u32(2_u32); Word::NUM_ELEMENTS]); + let value_3 = Word::new([Felt::from_u32(3_u32); Word::NUM_ELEMENTS]); let root_empty = smt.root(); @@ -489,9 +504,7 @@ fn test_prospective_insertion() { let mutations = smt.compute_mutations(vec![(key_2, value_2)]).unwrap(); assert_eq!(mutations.root(), root_2, "prospective root 2 did not match actual root 2"); - let mutations = smt - .compute_mutations(vec![(key_3, EMPTY_WORD), (key_2, value_2), (key_3, value_3)]) - .unwrap(); + let mutations = smt.compute_mutations(vec![(key_2, value_2), (key_3, value_3)]).unwrap(); assert_eq!(mutations.root(), root_3, "mutations before and after apply did not match"); let old_root = smt.root(); let revert = apply_mutations(&mut smt, mutations); @@ -503,23 +516,8 @@ fn test_prospective_insertion() { "reverse mutations pairs did not match" ); - // Edge case: multiple values at the same key, where a later pair restores the original value. - let mutations = smt.compute_mutations(vec![(key_3, EMPTY_WORD), (key_3, value_3)]).unwrap(); - assert_eq!(mutations.root(), root_3); - let old_root = smt.root(); - let revert = apply_mutations(&mut smt, mutations); - assert_eq!(smt.root(), root_3); - assert_eq!(revert.old_root, smt.root(), "reverse mutations old root did not match"); - assert_eq!(revert.root(), old_root, "reverse mutations new root did not match"); - assert_eq!( - revert.new_pairs, - Map::from_iter([(key_3, value_3)]), - "reverse mutations pairs did not match" - ); - // Test batch updates, and that the order doesn't matter. - let pairs = - vec![(key_3, value_2), (key_2, EMPTY_WORD), (key_1, EMPTY_WORD), (key_3, EMPTY_WORD)]; + let pairs = vec![(key_3, EMPTY_WORD), (key_2, EMPTY_WORD), (key_1, EMPTY_WORD)]; let mutations = smt.compute_mutations(pairs).unwrap(); assert_eq!( mutations.root(), @@ -547,7 +545,7 @@ fn test_prospective_insertion() { #[test] fn test_mutations_no_mutations() { let key = Word::from([ONE, ONE, ONE, ONE]); - let value = Word::new([ONE; WORD_SIZE]); + let value = Word::new([ONE; Word::NUM_ELEMENTS]); let entries = [(key, value)]; let tree = Smt::with_entries(entries).unwrap(); @@ -562,18 +560,23 @@ fn test_mutations_no_mutations() { fn test_mutations_revert() { let mut smt = Smt::default(); - let key_1: Word = Word::from([ONE, ONE, ONE, Felt::new(1)]); - let key_2: Word = Word::from([Felt::new(2), Felt::new(2), Felt::new(2), Felt::new(2)]); + let key_1: Word = Word::from([ONE, ONE, ONE, Felt::new_unchecked(1)]); + let key_2: Word = Word::from([ + Felt::new_unchecked(2), + Felt::new_unchecked(2), + Felt::new_unchecked(2), + Felt::new_unchecked(2), + ]); let key_3: Word = Word::from([ Felt::from_u32(0_u32), Felt::from_u32(0_u32), Felt::from_u32(0_u32), - Felt::new(3), + Felt::new_unchecked(3), ]); - let value_1 = Word::new([ONE; WORD_SIZE]); - let value_2 = Word::new([Felt::from_u32(2_u32); WORD_SIZE]); - let value_3 = Word::new([Felt::from_u32(3_u32); WORD_SIZE]); + let value_1 = Word::new([ONE; Word::NUM_ELEMENTS]); + let value_2 = Word::new([Felt::from_u32(2_u32); Word::NUM_ELEMENTS]); + let value_3 = Word::new([Felt::from_u32(3_u32); Word::NUM_ELEMENTS]); smt.insert(key_1, value_1).unwrap(); smt.insert(key_2, value_2).unwrap(); @@ -597,18 +600,23 @@ fn test_mutations_revert() { fn test_mutation_set_serialization() { let mut smt = Smt::default(); - let key_1: Word = Word::from([ONE, ONE, ONE, Felt::new(1)]); - let key_2: Word = Word::from([Felt::new(2), Felt::new(2), Felt::new(2), Felt::new(2)]); + let key_1: Word = Word::from([ONE, ONE, ONE, Felt::new_unchecked(1)]); + let key_2: Word = Word::from([ + Felt::new_unchecked(2), + Felt::new_unchecked(2), + Felt::new_unchecked(2), + Felt::new_unchecked(2), + ]); let key_3: Word = Word::from([ Felt::from_u32(0_u32), Felt::from_u32(0_u32), Felt::from_u32(0_u32), - Felt::new(3), + Felt::new_unchecked(3), ]); - let value_1 = Word::new([ONE; WORD_SIZE]); - let value_2 = Word::new([Felt::from_u32(2_u32); WORD_SIZE]); - let value_3 = Word::new([Felt::from_u32(3_u32); WORD_SIZE]); + let value_1 = Word::new([ONE; Word::NUM_ELEMENTS]); + let value_2 = Word::new([Felt::from_u32(2_u32); Word::NUM_ELEMENTS]); + let value_3 = Word::new([Felt::from_u32(3_u32); Word::NUM_ELEMENTS]); smt.insert(key_1, value_1).unwrap(); smt.insert(key_2, value_2).unwrap(); @@ -635,11 +643,16 @@ fn test_mutation_set_serialization() { fn test_smt_path_to_keys_in_same_leaf_are_equal() { let raw = 0b_01101001_01101100_00011111_11111111_10010110_10010011_11100000_00000000_u64; - let key_1: Word = Word::from([ONE, ONE, ONE, Felt::new(raw)]); - let key_2: Word = Word::from([Felt::new(2), Felt::new(2), Felt::new(2), Felt::new(raw)]); + let key_1: Word = Word::from([ONE, ONE, ONE, Felt::new_unchecked(raw)]); + let key_2: Word = Word::from([ + Felt::new_unchecked(2), + Felt::new_unchecked(2), + Felt::new_unchecked(2), + Felt::new_unchecked(raw), + ]); - let value_1 = Word::new([ONE; WORD_SIZE]); - let value_2 = Word::new([Felt::from_u32(2_u32); WORD_SIZE]); + let value_1 = Word::new([ONE; Word::NUM_ELEMENTS]); + let value_2 = Word::new([Felt::from_u32(2_u32); Word::NUM_ELEMENTS]); let smt = Smt::with_entries([(key_1, value_1), (key_2, value_2)]).unwrap(); @@ -661,8 +674,8 @@ fn test_smt_get_value() { let key_1: Word = Word::from([ONE, ONE, ONE, ONE]); let key_2: Word = Word::from([2_u32, 2_u32, 2_u32, 2_u32]); - let value_1 = Word::new([ONE; WORD_SIZE]); - let value_2 = Word::new([Felt::from_u32(2_u32); WORD_SIZE]); + let value_1 = Word::new([ONE; Word::NUM_ELEMENTS]); + let value_2 = Word::new([Felt::from_u32(2_u32); Word::NUM_ELEMENTS]); let smt = Smt::with_entries([(key_1, value_1), (key_2, value_2)]).unwrap(); @@ -684,8 +697,8 @@ fn test_smt_entries() { let key_1 = Word::from([ONE, ONE, ONE, ONE]); let key_2 = Word::from([2_u32, 2_u32, 2_u32, 2_u32]); - let value_1 = Word::new([ONE; WORD_SIZE]); - let value_2 = Word::new([Felt::from_u32(2_u32); WORD_SIZE]); + let value_1 = Word::new([ONE; Word::NUM_ELEMENTS]); + let value_2 = Word::new([Felt::from_u32(2_u32); Word::NUM_ELEMENTS]); let entries = [(key_1, value_1), (key_2, value_2)]; let smt = Smt::with_entries(entries).unwrap(); @@ -801,8 +814,8 @@ fn test_max_leaf_entries_validation() { let mut entries = Vec::new(); for i in 0..MAX_LEAF_ENTRIES { - let key = Word::new([ONE, ONE, Felt::new(i as u64), ONE]); - let value = Word::new([ONE, ONE, ONE, Felt::new(i as u64)]); + let key = Word::new([ONE, ONE, Felt::new_unchecked(i as u64), ONE]); + let value = Word::new([ONE, ONE, ONE, Felt::new_unchecked(i as u64)]); entries.push((key, value)); } @@ -810,8 +823,8 @@ fn test_max_leaf_entries_validation() { assert!(result.is_ok(), "Should allow exactly MAX_LEAF_ENTRIES entries"); // Test that creating a multiple leaf with more than MAX_LEAF_ENTRIES fails - let key = Word::new([ONE, ONE, Felt::new(MAX_LEAF_ENTRIES as u64), ONE]); - let value = Word::new([ONE, ONE, ONE, Felt::new(MAX_LEAF_ENTRIES as u64)]); + let key = Word::new([ONE, ONE, Felt::new_unchecked(MAX_LEAF_ENTRIES as u64), ONE]); + let value = Word::new([ONE, ONE, ONE, Felt::new_unchecked(MAX_LEAF_ENTRIES as u64)]); entries.push((key, value)); let error = SmtLeaf::new_multiple(entries).unwrap_err(); @@ -825,15 +838,15 @@ fn test_max_leaf_entries_validation() { /// Tests that verify_presence returns InvalidKeyForProof when key maps to different leaf index #[test] fn test_smt_proof_error_invalid_key_for_proof() { - let key = Word::from([ONE, ONE, ONE, Felt::new(42)]); - let value = Word::new([ONE; WORD_SIZE]); + let key = Word::from([ONE, ONE, ONE, Felt::new_unchecked(42)]); + let value = Word::new([ONE; Word::NUM_ELEMENTS]); let smt = Smt::with_entries([(key, value)]).unwrap(); let proof = smt.open(&key); let root = smt.root(); // Use a key that maps to a different leaf index (different most significant felt) - let different_index_key = Word::from([ONE, ONE, ONE, Felt::new(999)]); + let different_index_key = Word::from([ONE, ONE, ONE, Felt::new_unchecked(999)]); assert_matches!( proof.verify_presence(&different_index_key, &value, &root), @@ -844,15 +857,15 @@ fn test_smt_proof_error_invalid_key_for_proof() { /// Tests that verify_presence returns ValueMismatch when value doesn't match #[test] fn test_smt_proof_error_value_mismatch() { - let key = Word::from([ONE, ONE, ONE, Felt::new(42)]); - let value = Word::new([ONE; WORD_SIZE]); + let key = Word::from([ONE, ONE, ONE, Felt::new_unchecked(42)]); + let value = Word::new([ONE; Word::NUM_ELEMENTS]); let smt = Smt::with_entries([(key, value)]).unwrap(); let proof = smt.open(&key); let root = smt.root(); // Use the correct key but wrong value - let wrong_value = Word::new([Felt::new(999); WORD_SIZE]); + let wrong_value = Word::new([Felt::new_unchecked(999); Word::NUM_ELEMENTS]); assert_matches!( proof.verify_presence(&key, &wrong_value, &root), @@ -864,15 +877,15 @@ fn test_smt_proof_error_value_mismatch() { /// Tests that verify_presence returns ConflictingRoots when root doesn't match #[test] fn test_smt_proof_error_conflicting_roots() { - let key = Word::from([ONE, ONE, ONE, Felt::new(42)]); - let value = Word::new([ONE; WORD_SIZE]); + let key = Word::from([ONE, ONE, ONE, Felt::new_unchecked(42)]); + let value = Word::new([ONE; Word::NUM_ELEMENTS]); let smt = Smt::with_entries([(key, value)]).unwrap(); let proof = smt.open(&key); let actual_root = smt.root(); // Use a completely wrong root - let wrong_root = Word::new([Felt::new(999); WORD_SIZE]); + let wrong_root = Word::new([Felt::new_unchecked(999); Word::NUM_ELEMENTS]); assert_matches!( proof.verify_presence(&key, &value, &wrong_root), @@ -886,7 +899,7 @@ fn test_smt_proof_error_conflicting_roots() { fn test_smt_proof_verify_unset_success() { // Use an empty tree where no keys have values let smt = Smt::default(); - let key = Word::from([ONE, ONE, ONE, Felt::new(42)]); + let key = Word::from([ONE, ONE, ONE, Felt::new_unchecked(42)]); let proof = smt.open(&key); let root = smt.root(); @@ -897,8 +910,8 @@ fn test_smt_proof_verify_unset_success() { /// Tests that verify_unset returns ValueMismatch when key has a value #[test] fn test_smt_proof_verify_unset_fails_when_value_exists() { - let key = Word::from([ONE, ONE, ONE, Felt::new(42)]); - let value = Word::new([ONE; WORD_SIZE]); + let key = Word::from([ONE, ONE, ONE, Felt::new_unchecked(42)]); + let value = Word::new([ONE; Word::NUM_ELEMENTS]); let smt = Smt::with_entries([(key, value)]).unwrap(); let proof = smt.open(&key); @@ -911,15 +924,15 @@ fn test_smt_proof_verify_unset_fails_when_value_exists() { /// Tests that verify_absence returns Ok when the key has a different value #[test] fn test_smt_proof_verify_absence_success_different_value() { - let key = Word::from([ONE, ONE, ONE, Felt::new(42)]); - let actual_value = Word::new([ONE; WORD_SIZE]); + let key = Word::from([ONE, ONE, ONE, Felt::new_unchecked(42)]); + let actual_value = Word::new([ONE; Word::NUM_ELEMENTS]); let smt = Smt::with_entries([(key, actual_value)]).unwrap(); let proof = smt.open(&key); let root = smt.root(); // The key has a different value, so this pair is absent - let absent_value = Word::new([Felt::new(999); WORD_SIZE]); + let absent_value = Word::new([Felt::new_unchecked(999); Word::NUM_ELEMENTS]); proof.verify_absence(&key, &absent_value, &root).unwrap(); } @@ -928,20 +941,20 @@ fn test_smt_proof_verify_absence_success_different_value() { fn test_smt_proof_verify_absence_success_key_unset() { // Use an empty tree let smt = Smt::default(); - let key = Word::from([ONE, ONE, ONE, Felt::new(42)]); + let key = Word::from([ONE, ONE, ONE, Felt::new_unchecked(42)]); let proof = smt.open(&key); let root = smt.root(); // Any non-empty value should be absent since key is unset - let value = Word::new([ONE; WORD_SIZE]); + let value = Word::new([ONE; Word::NUM_ELEMENTS]); proof.verify_absence(&key, &value, &root).unwrap(); } /// Tests that verify_absence returns ValuePresent when the key-value pair exists #[test] fn test_smt_proof_error_value_present() { - let key = Word::from([ONE, ONE, ONE, Felt::new(42)]); - let value = Word::new([ONE; WORD_SIZE]); + let key = Word::from([ONE, ONE, ONE, Felt::new_unchecked(42)]); + let value = Word::new([ONE; Word::NUM_ELEMENTS]); let smt = Smt::with_entries([(key, value)]).unwrap(); let proof = smt.open(&key); @@ -957,15 +970,15 @@ fn test_smt_proof_error_value_present() { /// Tests that verify_absence returns InvalidKeyForProof for wrong leaf index #[test] fn test_smt_proof_verify_absence_invalid_key() { - let key = Word::from([ONE, ONE, ONE, Felt::new(42)]); - let value = Word::new([ONE; WORD_SIZE]); + let key = Word::from([ONE, ONE, ONE, Felt::new_unchecked(42)]); + let value = Word::new([ONE; Word::NUM_ELEMENTS]); let smt = Smt::with_entries([(key, value)]).unwrap(); let proof = smt.open(&key); let root = smt.root(); // Use a key that maps to a different leaf index - let different_index_key = Word::from([ONE, ONE, ONE, Felt::new(999)]); + let different_index_key = Word::from([ONE, ONE, ONE, Felt::new_unchecked(999)]); assert_matches!( proof.verify_absence(&different_index_key, &value, &root), @@ -976,14 +989,14 @@ fn test_smt_proof_verify_absence_invalid_key() { /// Tests that `get()` returns None for keys that don't map to the proof's leaf index. #[test] fn test_smt_proof_get_returns_none_for_different_leaf() { - let key = Word::from([ONE, ONE, ONE, Felt::new(42)]); - let value = Word::new([ONE; WORD_SIZE]); + let key = Word::from([ONE, ONE, ONE, Felt::new_unchecked(42)]); + let value = Word::new([ONE; Word::NUM_ELEMENTS]); let smt = Smt::with_entries([(key, value)]).unwrap(); let proof = smt.open(&key); // Key that maps to a different leaf index - let different_leaf_key = Word::from([ONE, ONE, ONE, Felt::new(999)]); + let different_leaf_key = Word::from([ONE, ONE, ONE, Felt::new_unchecked(999)]); assert!( proof.get(&different_leaf_key).is_none(), @@ -994,15 +1007,19 @@ fn test_smt_proof_get_returns_none_for_different_leaf() { /// Tests that `get()` returns EMPTY_WORD for keys that map to the proof's leaf but don't exist. #[test] fn test_smt_proof_get_returns_empty_for_absent_key_same_leaf() { - let key = Word::from([ONE, ONE, ONE, Felt::new(42)]); - let value = Word::new([ONE; WORD_SIZE]); + let key = Word::from([ONE, ONE, ONE, Felt::new_unchecked(42)]); + let value = Word::new([ONE; Word::NUM_ELEMENTS]); let smt = Smt::with_entries([(key, value)]).unwrap(); let proof = smt.open(&key); // Key that maps to the same leaf but doesn't exist (same most significant felt) - let absent_key_same_leaf = - Word::from([Felt::new(2), Felt::new(2), Felt::new(2), Felt::new(42)]); + let absent_key_same_leaf = Word::from([ + Felt::new_unchecked(2), + Felt::new_unchecked(2), + Felt::new_unchecked(2), + Felt::new_unchecked(42), + ]); let result = proof.get(&absent_key_same_leaf); assert_eq!( @@ -1064,6 +1081,131 @@ fn test_smt_leaf_try_from_elements_invalid_length() { assert_matches!(result, Err(SmtLeafError::DecodingError(_))); } +// DUPLICATE KEY DETECTION +// -------------------------------------------------------------------------------------------- + +/// Tests that `compute_mutations` rejects duplicate keys (same key, same value). +#[test] +fn test_compute_mutations_rejects_duplicate_keys() { + use crate::merkle::MerkleError; + + let smt = Smt::default(); + let key = Word::from([ONE, ONE, ONE, Felt::new_unchecked(42)]); + let value = Word::new([ONE; Word::NUM_ELEMENTS]); + + let result = smt.compute_mutations(vec![(key, value), (key, value)]); + + let expected_pos = Smt::key_to_leaf_index(&key).position(); + assert_matches!(result, Err(MerkleError::DuplicateValuesForIndex(pos)) if pos == expected_pos); +} + +/// Tests that `compute_mutations` rejects duplicate keys even with different values. +#[test] +fn test_compute_mutations_rejects_duplicate_keys_different_values() { + use crate::merkle::MerkleError; + + let smt = Smt::default(); + let key = Word::from([ONE, ONE, ONE, Felt::new_unchecked(42)]); + let value_1 = Word::new([ONE; Word::NUM_ELEMENTS]); + let value_2 = Word::new([Felt::from_u32(2_u32); Word::NUM_ELEMENTS]); + + let result = smt.compute_mutations(vec![(key, value_1), (key, value_2)]); + + let expected_pos = Smt::key_to_leaf_index(&key).position(); + assert_matches!(result, Err(MerkleError::DuplicateValuesForIndex(pos)) if pos == expected_pos); +} + +/// Tests that `compute_mutations` rejects duplicate keys even when interleaved with another key +/// that shares the same leaf index: `[(k1, v1), (k2, v2), (k1, v3)]`. +#[test] +fn test_compute_mutations_rejects_interleaved_duplicate_keys() { + use crate::merkle::MerkleError; + + let smt = Smt::default(); + + // Two different keys that map to the same leaf (same most significant felt) + let key_1 = Word::from([ONE, ONE, ONE, Felt::new_unchecked(42)]); + let key_2 = Word::from([ + Felt::new_unchecked(2), + Felt::new_unchecked(2), + Felt::new_unchecked(2), + Felt::new_unchecked(42), + ]); + + let value_1 = Word::new([ONE; Word::NUM_ELEMENTS]); + let value_2 = Word::new([Felt::from_u32(2_u32); Word::NUM_ELEMENTS]); + let value_3 = Word::new([Felt::from_u32(3_u32); Word::NUM_ELEMENTS]); + + // k1 appears at positions 0 and 2, interleaved with k2 + let result = smt.compute_mutations(vec![(key_1, value_1), (key_2, value_2), (key_1, value_3)]); + + let expected_pos = Smt::key_to_leaf_index(&key_1).position(); + assert_matches!(result, Err(MerkleError::DuplicateValuesForIndex(pos)) if pos == expected_pos); +} + +/// Tests that different keys mapping to the same leaf index do NOT trigger the duplicate error. +#[test] +fn test_compute_mutations_no_false_positives() { + let smt = Smt::default(); + + // Two different keys that map to the same leaf (same most significant felt) + let key_1 = Word::from([ONE, ONE, ONE, Felt::new_unchecked(42)]); + let key_2 = Word::from([ + Felt::new_unchecked(2), + Felt::new_unchecked(2), + Felt::new_unchecked(2), + Felt::new_unchecked(42), + ]); + + let value_1 = Word::new([ONE; Word::NUM_ELEMENTS]); + let value_2 = Word::new([Felt::from_u32(2_u32); Word::NUM_ELEMENTS]); + + // These are different keys (despite sharing a leaf index), so this should succeed. + let result = smt.compute_mutations(vec![(key_1, value_1), (key_2, value_2)]); + + assert!(result.is_ok(), "Different keys at the same leaf index should not be rejected"); +} + +/// Tests that `Smt::with_entries` rejects duplicate keys. +#[test] +fn test_with_entries_rejects_duplicate_keys() { + use crate::merkle::MerkleError; + + let key = Word::from([ONE, ONE, ONE, Felt::new_unchecked(42)]); + let value_1 = Word::new([ONE; Word::NUM_ELEMENTS]); + let value_2 = Word::new([Felt::from_u32(2_u32); Word::NUM_ELEMENTS]); + + let result = Smt::with_entries(vec![(key, value_1), (key, value_2)]); + + let expected_pos = Smt::key_to_leaf_index(&key).position(); + assert_matches!(result, Err(MerkleError::DuplicateValuesForIndex(pos)) if pos == expected_pos); +} + +/// Tests that `Smt::with_entries` rejects interleaved duplicate keys. +#[test] +fn test_with_entries_rejects_interleaved_duplicate_keys() { + use crate::merkle::MerkleError; + + // Two different keys that map to the same leaf (same most significant felt) + let key_1 = Word::from([ONE, ONE, ONE, Felt::new_unchecked(42)]); + let key_2 = Word::from([ + Felt::new_unchecked(2), + Felt::new_unchecked(2), + Felt::new_unchecked(2), + Felt::new_unchecked(42), + ]); + + let value_1 = Word::new([ONE; Word::NUM_ELEMENTS]); + let value_2 = Word::new([Felt::from_u32(2_u32); Word::NUM_ELEMENTS]); + let value_3 = Word::new([Felt::from_u32(3_u32); Word::NUM_ELEMENTS]); + + // k1 appears at positions 0 and 2, interleaved with k2 + let result = Smt::with_entries(vec![(key_1, value_1), (key_2, value_2), (key_1, value_3)]); + + let expected_pos = Smt::key_to_leaf_index(&key_1).position(); + assert_matches!(result, Err(MerkleError::DuplicateValuesForIndex(pos)) if pos == expected_pos); +} + // HELPERS // -------------------------------------------------------------------------------------------- diff --git a/miden-crypto/src/merkle/smt/large/batch_ops.rs b/miden-crypto/src/merkle/smt/large/batch_ops.rs index 9c3e93229c..f5d97d6918 100644 --- a/miden-crypto/src/merkle/smt/large/batch_ops.rs +++ b/miden-crypto/src/merkle/smt/large/batch_ops.rs @@ -104,13 +104,13 @@ impl LargeSmt { &self, sorted_kv_pairs: &[(Word, Word)], ) -> Result { - // Collect the unique leaf indices + // Collect the unique leaf indices. If the input is truly sorted, then we can dedup + // directly. let mut leaf_indices: Vec = sorted_kv_pairs .iter() .map(|(key, _)| Self::key_to_leaf_index(key).position()) .collect(); leaf_indices.dedup(); - leaf_indices.par_sort_unstable(); // Get leaves from storage let leaves_from_storage = self.storage.get_leaves(&leaf_indices)?; @@ -312,15 +312,43 @@ impl LargeSmt { /// let entries = vec![ /// // Insert new entries /// ( - /// Word::new([Felt::new(1), Felt::new(0), Felt::new(0), Felt::new(0)]), - /// Word::new([Felt::new(10), Felt::new(20), Felt::new(30), Felt::new(40)]), + /// Word::new([ + /// Felt::new_unchecked(1), + /// Felt::new_unchecked(0), + /// Felt::new_unchecked(0), + /// Felt::new_unchecked(0), + /// ]), + /// Word::new([ + /// Felt::new_unchecked(10), + /// Felt::new_unchecked(20), + /// Felt::new_unchecked(30), + /// Felt::new_unchecked(40), + /// ]), /// ), /// ( - /// Word::new([Felt::new(2), Felt::new(0), Felt::new(0), Felt::new(0)]), - /// Word::new([Felt::new(11), Felt::new(22), Felt::new(33), Felt::new(44)]), + /// Word::new([ + /// Felt::new_unchecked(2), + /// Felt::new_unchecked(0), + /// Felt::new_unchecked(0), + /// Felt::new_unchecked(0), + /// ]), + /// Word::new([ + /// Felt::new_unchecked(11), + /// Felt::new_unchecked(22), + /// Felt::new_unchecked(33), + /// Felt::new_unchecked(44), + /// ]), /// ), /// // Delete an entry - /// (Word::new([Felt::new(3), Felt::new(0), Felt::new(0), Felt::new(0)]), EMPTY_WORD), + /// ( + /// Word::new([ + /// Felt::new_unchecked(3), + /// Felt::new_unchecked(0), + /// Felt::new_unchecked(0), + /// Felt::new_unchecked(0), + /// ]), + /// EMPTY_WORD, + /// ), /// ]; /// /// let new_root = smt.insert_batch(entries)?; @@ -335,9 +363,12 @@ impl LargeSmt { where Self: Sized + Sync, { - // Sort key-value pairs by leaf index + // Sort key-value pairs by their corresponding leaf index and then the key value itself. let mut sorted_kv_pairs: Vec<_> = kv_pairs.into_iter().collect(); - sorted_kv_pairs.par_sort_by_key(|(key, _)| Self::key_to_leaf_index(key).position()); + sorted_kv_pairs.par_sort_unstable_by_key(|(key, _)| *key); + + // After sorting, check for duplicate keys which are adjacent after the sort. + Self::check_for_duplicate_keys(&sorted_kv_pairs)?; // Load leaves from storage let (_leaf_indices, leaf_map) = self.load_leaves_for_pairs(&sorted_kv_pairs)?; @@ -501,15 +532,14 @@ impl LargeSmt { // Collect and sort key-value pairs by their corresponding leaf index let mut sorted_kv_pairs: Vec<_> = new_pairs.iter().map(|(k, v)| (*k, *v)).collect(); - sorted_kv_pairs - .par_sort_by_key(|(key, _)| LargeSmt::::key_to_leaf_index(key).position()); + sorted_kv_pairs.par_sort_unstable_by_key(|(key, _)| *key); - // Collect the unique leaf indices + // Collect the unique leaf indices, relying on the global sort order given by the above + // sort. let mut leaf_indices: Vec = sorted_kv_pairs .iter() .map(|(key, _)| LargeSmt::::key_to_leaf_index(key).position()) .collect(); - leaf_indices.par_sort_unstable(); leaf_indices.dedup(); // Get leaves from storage @@ -599,7 +629,7 @@ impl LargeSmt { // Go through subtrees, see if any are empty, and if so remove them for (_index, subtree) in loaded_subtrees.iter_mut() { - if subtree.as_ref().is_some_and(|s| s.is_empty()) { + if subtree.as_ref().is_some_and(Subtree::is_empty) { *subtree = None; } } @@ -693,10 +723,12 @@ impl LargeSmt { where Self: Sized + Sync, { - // Collect and sort key-value pairs by their corresponding leaf index + // Sort key-value pairs by their corresponding leaf index and then the key value itself. let mut sorted_kv_pairs: Vec<_> = kv_pairs.into_iter().collect(); - sorted_kv_pairs - .par_sort_unstable_by_key(|(key, _)| LargeSmt::::key_to_leaf_index(key).position()); + sorted_kv_pairs.par_sort_unstable_by_key(|(key, _)| *key); + + // After sorting, check for duplicate keys which are adjacent after the sort. + Self::check_for_duplicate_keys(&sorted_kv_pairs)?; // Load leaves from storage using helper let (_leaf_indices, leaf_map) = self.load_leaves_for_pairs(&sorted_kv_pairs)?; diff --git a/miden-crypto/src/merkle/smt/large/construction.rs b/miden-crypto/src/merkle/smt/large/construction.rs index f2282efc2a..b56a9d05ed 100644 --- a/miden-crypto/src/merkle/smt/large/construction.rs +++ b/miden-crypto/src/merkle/smt/large/construction.rs @@ -11,7 +11,7 @@ use crate::{ EMPTY_WORD, Word, hash::poseidon2::Poseidon2, merkle::smt::{ - EmptySubtreeRoots, InnerNode, Map, MerkleError, NodeIndex, Smt, SparseMerkleTree, + EmptySubtreeRoots, InnerNode, Map, MerkleError, NodeIndex, Smt, SmtLeaf, SparseMerkleTree, full::concurrent::{ PairComputations, SUBTREE_DEPTH, SubtreeLeaf, SubtreeLeavesIter, build_subtree, }, @@ -250,7 +250,7 @@ impl LargeSmt { // Update cached counts before storing leaves self.leaf_count = initial_leaves.len(); - self.entry_count = initial_leaves.values().map(|leaf| leaf.num_entries()).sum(); + self.entry_count = initial_leaves.values().map(SmtLeaf::num_entries).sum(); // Store the initial leaves self.storage.set_leaves(initial_leaves).expect("Failed to store initial leaves"); diff --git a/miden-crypto/src/merkle/smt/large/iter.rs b/miden-crypto/src/merkle/smt/large/iter.rs index f1e36a3a9f..d1ff24c0a4 100644 --- a/miden-crypto/src/merkle/smt/large/iter.rs +++ b/miden-crypto/src/merkle/smt/large/iter.rs @@ -113,6 +113,10 @@ impl Iterator for LargeSmtInnerNodeIterator<'_, S> { // Current subtree exhausted, move to next subtree match subtree_iter.next() { Some(next_subtree) => { + // Collect is necessary here because iter_inner_node_info returns + // an iterator borrowing from next_subtree, which would outlive + // the subtree itself. We need to eagerly evaluate to owned data. + #[expect(clippy::needless_collect)] let infos: Vec = next_subtree.iter_inner_node_info().collect(); *current_subtree_node_iter = Some(Box::new(infos.into_iter())); diff --git a/miden-crypto/src/merkle/smt/large/mod.rs b/miden-crypto/src/merkle/smt/large/mod.rs index 6ce71ee4f7..7eb4c3efee 100644 --- a/miden-crypto/src/merkle/smt/large/mod.rs +++ b/miden-crypto/src/merkle/smt/large/mod.rs @@ -64,12 +64,32 @@ //! // Prepare initial entries //! let entries = vec![ //! ( -//! Word::new([Felt::new(1), Felt::new(0), Felt::new(0), Felt::new(0)]), -//! Word::new([Felt::new(10), Felt::new(20), Felt::new(30), Felt::new(40)]), +//! Word::new([ +//! Felt::new_unchecked(1), +//! Felt::new_unchecked(0), +//! Felt::new_unchecked(0), +//! Felt::new_unchecked(0), +//! ]), +//! Word::new([ +//! Felt::new_unchecked(10), +//! Felt::new_unchecked(20), +//! Felt::new_unchecked(30), +//! Felt::new_unchecked(40), +//! ]), //! ), //! ( -//! Word::new([Felt::new(2), Felt::new(0), Felt::new(0), Felt::new(0)]), -//! Word::new([Felt::new(11), Felt::new(22), Felt::new(33), Felt::new(44)]), +//! Word::new([ +//! Felt::new_unchecked(2), +//! Felt::new_unchecked(0), +//! Felt::new_unchecked(0), +//! Felt::new_unchecked(0), +//! ]), +//! Word::new([ +//! Felt::new_unchecked(11), +//! Felt::new_unchecked(22), +//! Felt::new_unchecked(33), +//! Felt::new_unchecked(44), +//! ]), //! ), //! ]; //! @@ -93,11 +113,36 @@ //! let storage = RocksDbStorage::open(RocksDbConfig::new("/path/to/db"))?; //! let mut smt = LargeSmt::load(storage)?; //! -//! let k1 = Word::new([Felt::new(101), Felt::new(0), Felt::new(0), Felt::new(0)]); -//! let v1 = Word::new([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]); -//! let k2 = Word::new([Felt::new(202), Felt::new(0), Felt::new(0), Felt::new(0)]); -//! let k3 = Word::new([Felt::new(303), Felt::new(0), Felt::new(0), Felt::new(0)]); -//! let v3 = Word::new([Felt::new(7), Felt::new(7), Felt::new(7), Felt::new(7)]); +//! let k1 = Word::new([ +//! Felt::new_unchecked(101), +//! Felt::new_unchecked(0), +//! Felt::new_unchecked(0), +//! Felt::new_unchecked(0), +//! ]); +//! let v1 = Word::new([ +//! Felt::new_unchecked(1), +//! Felt::new_unchecked(2), +//! Felt::new_unchecked(3), +//! Felt::new_unchecked(4), +//! ]); +//! let k2 = Word::new([ +//! Felt::new_unchecked(202), +//! Felt::new_unchecked(0), +//! Felt::new_unchecked(0), +//! Felt::new_unchecked(0), +//! ]); +//! let k3 = Word::new([ +//! Felt::new_unchecked(303), +//! Felt::new_unchecked(0), +//! Felt::new_unchecked(0), +//! Felt::new_unchecked(0), +//! ]); +//! let v3 = Word::new([ +//! Felt::new_unchecked(7), +//! Felt::new_unchecked(7), +//! Felt::new_unchecked(7), +//! Felt::new_unchecked(7), +//! ]); //! //! // EMPTY_WORD marks deletions //! let updates = vec![(k1, v1), (k2, EMPTY_WORD), (k3, v3)]; @@ -128,12 +173,32 @@ //! let storage = RocksDbStorage::open(RocksDbConfig::new(path))?; //! let entries = vec![ //! ( -//! Word::new([Felt::new(1), Felt::new(0), Felt::new(0), Felt::new(0)]), -//! Word::new([Felt::new(10), Felt::new(20), Felt::new(30), Felt::new(40)]), +//! Word::new([ +//! Felt::new_unchecked(1), +//! Felt::new_unchecked(0), +//! Felt::new_unchecked(0), +//! Felt::new_unchecked(0), +//! ]), +//! Word::new([ +//! Felt::new_unchecked(10), +//! Felt::new_unchecked(20), +//! Felt::new_unchecked(30), +//! Felt::new_unchecked(40), +//! ]), //! ), //! ( -//! Word::new([Felt::new(2), Felt::new(0), Felt::new(0), Felt::new(0)]), -//! Word::new([Felt::new(11), Felt::new(22), Felt::new(33), Felt::new(44)]), +//! Word::new([ +//! Felt::new_unchecked(2), +//! Felt::new_unchecked(0), +//! Felt::new_unchecked(0), +//! Felt::new_unchecked(0), +//! ]), +//! Word::new([ +//! Felt::new_unchecked(11), +//! Felt::new_unchecked(22), +//! Felt::new_unchecked(33), +//! Felt::new_unchecked(44), +//! ]), //! ), //! ]; //! let _smt = LargeSmt::with_entries(storage, entries)?; diff --git a/miden-crypto/src/merkle/smt/large/property_tests.rs b/miden-crypto/src/merkle/smt/large/property_tests.rs index 9314fcfb30..9b38064ec1 100644 --- a/miden-crypto/src/merkle/smt/large/property_tests.rs +++ b/miden-crypto/src/merkle/smt/large/property_tests.rs @@ -15,7 +15,7 @@ use crate::{ // ================================================================================================ fn arb_felt() -> impl Strategy { - prop_oneof![any::().prop_map(Felt::new), Just(ZERO), Just(ONE),] + prop_oneof![any::().prop_map(Felt::new_unchecked), Just(ZERO), Just(ONE),] } fn arb_word() -> impl Strategy { diff --git a/miden-crypto/src/merkle/smt/large/storage/memory.rs b/miden-crypto/src/merkle/smt/large/storage/memory.rs index 51d4a90174..a051d29f6d 100644 --- a/miden-crypto/src/merkle/smt/large/storage/memory.rs +++ b/miden-crypto/src/merkle/smt/large/storage/memory.rs @@ -51,7 +51,7 @@ impl SmtStorage for MemoryStorage { /// Gets the total number of key-value entries currently stored. fn entry_count(&self) -> Result { - Ok(self.leaves.values().map(|leaf| leaf.num_entries()).sum()) + Ok(self.leaves.values().map(SmtLeaf::num_entries).sum()) } /// Inserts a key-value pair into the leaf at the given index. @@ -288,16 +288,14 @@ impl SmtStorage for MemoryStorage { /// /// The iterator provides access to the current state of the leaves. fn iter_leaves(&self) -> Result + '_>, StorageError> { - let leaves_vec = self.leaves.iter().map(|(&k, v)| (k, v.clone())).collect::>(); - Ok(Box::new(leaves_vec.into_iter())) + Ok(Box::new(self.leaves.iter().map(|(&k, v)| (k, v.clone())))) } /// Returns an iterator over all Subtrees in the storage. /// /// The iterator provides access to the current subtrees from storage. fn iter_subtrees(&self) -> Result + '_>, StorageError> { - let subtrees_vec = self.subtrees.values().cloned().collect::>(); - Ok(Box::new(subtrees_vec.into_iter())) + Ok(Box::new(self.subtrees.values().cloned())) } /// Retrieves all depth 24 roots for fast tree rebuilding. diff --git a/miden-crypto/src/merkle/smt/large/tests.rs b/miden-crypto/src/merkle/smt/large/tests.rs index e9577a8e5d..a3182067d8 100644 --- a/miden-crypto/src/merkle/smt/large/tests.rs +++ b/miden-crypto/src/merkle/smt/large/tests.rs @@ -4,7 +4,7 @@ use rand::{Rng, prelude::IteratorRandom, rng}; use super::MemoryStorage; use crate::{ - EMPTY_WORD, Felt, ONE, WORD_SIZE, Word, + EMPTY_WORD, Felt, ONE, Word, merkle::{ InnerNodeInfo, smt::{ @@ -18,8 +18,9 @@ fn generate_entries(pair_count: u64) -> Vec<(Word, Word)> { (0..pair_count) .map(|i| { let leaf_index = ((i as f64 / pair_count as f64) * (pair_count as f64)) as u64; - let key = Word::new([ONE, ONE, Felt::new(i), Felt::new(leaf_index)]); - let value = Word::new([ONE, ONE, ONE, Felt::new(i)]); + let key = + Word::new([ONE, ONE, Felt::new_unchecked(i), Felt::new_unchecked(leaf_index)]); + let value = Word::new([ONE, ONE, ONE, Felt::new_unchecked(i)]); (key, value) }) .collect() @@ -40,7 +41,7 @@ fn generate_updates(entries: Vec<(Word, Word)>, updates: usize) -> Vec<(Word, Wo let value = if rng.random_bool(REMOVAL_PROBABILITY) { EMPTY_WORD } else { - Word::new([ONE, ONE, ONE, Felt::new(rng.random())]) + Word::new([ONE, ONE, ONE, Felt::new_unchecked(rng.random())]) }; (key, value) }) @@ -64,8 +65,8 @@ fn test_smt_get_value() { let key_1: Word = Word::from([ONE, ONE, ONE, ONE]); let key_2: Word = Word::from([2_u32, 2_u32, 2_u32, 2_u32]); - let value_1 = Word::new([ONE; WORD_SIZE]); - let value_2 = Word::new([Felt::from_u32(2_u32); WORD_SIZE]); + let value_1 = Word::new([ONE; Word::NUM_ELEMENTS]); + let value_2 = Word::new([Felt::from_u32(2_u32); Word::NUM_ELEMENTS]); let smt = LargeSmt::<_>::with_entries(storage, [(key_1, value_1), (key_2, value_2)]).unwrap(); let returned_value_1 = smt.get_value(&key_1); @@ -197,7 +198,7 @@ fn test_empty_smt() { fn test_single_entry_smt() { let storage = MemoryStorage::new(); let key = Word::new([ONE, ONE, ONE, ONE]); - let value = Word::new([ONE; WORD_SIZE]); + let value = Word::new([ONE; Word::NUM_ELEMENTS]); let mut smt = LargeSmt::<_>::with_entries(storage, [(key, value)]).unwrap(); @@ -213,7 +214,7 @@ fn test_single_entry_smt() { assert_eq!(entries.len(), 1, "Single entry SMT should have one entry"); assert_eq!(entries[0], (key, value), "Single entry SMT entry mismatch"); - let new_value = Word::new([Felt::from_u32(2_u32); WORD_SIZE]); + let new_value = Word::new([Felt::from_u32(2_u32); Word::NUM_ELEMENTS]); let mutations = smt.compute_mutations(vec![(key, new_value)]).unwrap(); assert_eq!( @@ -247,8 +248,8 @@ fn test_single_entry_smt() { fn test_duplicate_key_insertion() { let storage = MemoryStorage::new(); let key = Word::from([ONE, ONE, ONE, ONE]); - let value1 = Word::new([ONE; WORD_SIZE]); - let value2 = Word::new([Felt::from_u32(2_u32); WORD_SIZE]); + let value1 = Word::new([ONE; Word::NUM_ELEMENTS]); + let value2 = Word::new([Felt::from_u32(2_u32); Word::NUM_ELEMENTS]); let entries = vec![(key, value1), (key, value2)]; @@ -256,19 +257,76 @@ fn test_duplicate_key_insertion() { assert!(result.is_err(), "Expected an error when inserting duplicate keys"); } +#[test] +fn test_compute_mutations_rejects_duplicate_keys() { + let storage = MemoryStorage::new(); + let smt = LargeSmt::<_>::with_entries(storage, vec![]).unwrap(); + + let key = Word::from([ONE, ONE, ONE, ONE]); + let value = Word::new([ONE; Word::NUM_ELEMENTS]); + + let entries = vec![(key, value), (key, value)]; + let result = smt.compute_mutations(entries); + assert!( + result.is_err(), + "Expected an error when computing mutations with duplicate keys" + ); +} + +#[test] +fn test_compute_mutations_rejects_interleaved_duplicate_keys() { + let storage = MemoryStorage::new(); + let smt = LargeSmt::<_>::with_entries(storage, vec![]).unwrap(); + + // Two different keys that map to the same leaf (same most significant felt) + let key_1 = Word::from([ONE, ONE, ONE, Felt::new_unchecked(42)]); + let key_2 = Word::from([ + Felt::new_unchecked(2), + Felt::new_unchecked(2), + Felt::new_unchecked(2), + Felt::new_unchecked(42), + ]); + + let value_1 = Word::new([ONE; Word::NUM_ELEMENTS]); + let value_2 = Word::new([Felt::from_u32(2_u32); Word::NUM_ELEMENTS]); + let value_3 = Word::new([Felt::from_u32(3_u32); Word::NUM_ELEMENTS]); + + // k1 appears at positions 0 and 2, interleaved with k2 + let entries = vec![(key_1, value_1), (key_2, value_2), (key_1, value_3)]; + let result = smt.compute_mutations(entries); + assert!( + result.is_err(), + "Expected an error when computing mutations with interleaved duplicate keys" + ); +} + +#[test] +fn test_insert_batch_rejects_duplicate_keys() { + let storage = MemoryStorage::new(); + let mut smt = LargeSmt::<_>::with_entries(storage, vec![]).unwrap(); + + let key = Word::from([ONE, ONE, ONE, ONE]); + let value1 = Word::new([ONE; Word::NUM_ELEMENTS]); + let value2 = Word::new([Felt::from_u32(2_u32); Word::NUM_ELEMENTS]); + + let entries = vec![(key, value1), (key, value2)]; + let result = smt.insert_batch(entries); + assert!(result.is_err(), "Expected an error when inserting batch with duplicate keys"); +} + #[test] fn test_delete_entry() { let storage = MemoryStorage::new(); let key1 = Word::new([ONE, ONE, ONE, ONE]); - let value1 = Word::new([ONE; WORD_SIZE]); + let value1 = Word::new([ONE; Word::NUM_ELEMENTS]); let key2 = Word::from([2_u32, 2_u32, 2_u32, 2_u32]); - let value2 = Word::new([Felt::from_u32(2_u32); WORD_SIZE]); + let value2 = Word::new([Felt::from_u32(2_u32); Word::NUM_ELEMENTS]); let key3 = Word::from([3_u32, 3_u32, 3_u32, 3_u32]); - let value3 = Word::new([Felt::from_u32(3_u32); WORD_SIZE]); + let value3 = Word::new([Felt::from_u32(3_u32); Word::NUM_ELEMENTS]); let initial_entries = vec![(key1, value1), (key2, value2), (key3, value3)]; - let mut smt = LargeSmt::<_>::with_entries(storage, initial_entries.clone()).unwrap(); + let mut smt = LargeSmt::<_>::with_entries(storage, initial_entries).unwrap(); let mutations = smt.compute_mutations(vec![(key2, EMPTY_WORD)]).unwrap(); smt.apply_mutations(mutations).unwrap(); @@ -306,7 +364,7 @@ fn test_insert_entry() { assert_eq!(large_smt.num_leaves(), control_smt.num_leaves(), "Number of leaves mismatch"); let new_key = Word::from([100_u32, 100_u32, 100_u32, 100_u32]); - let new_value = Word::new([Felt::from_u32(100_u32); WORD_SIZE]); + let new_value = Word::new([Felt::from_u32(100_u32); Word::NUM_ELEMENTS]); let old_value = large_smt.insert(new_key, new_value).unwrap(); let control_old_value = control_smt.insert(new_key, new_value).unwrap(); @@ -337,13 +395,23 @@ fn test_mutations_revert() { let storage = MemoryStorage::new(); let mut smt = LargeSmt::<_>::new(storage).unwrap(); - let key_1: Word = Word::new([ONE, ONE, ONE, Felt::new(1)]); - let key_2: Word = Word::new([Felt::new(2), Felt::new(2), Felt::new(2), Felt::new(2)]); - let key_3: Word = Word::new([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(3)]); - - let value_1 = Word::new([ONE; WORD_SIZE]); - let value_2 = Word::new([Felt::from_u32(2_u32); WORD_SIZE]); - let value_3 = Word::new([Felt::from_u32(3_u32); WORD_SIZE]); + let key_1: Word = Word::new([ONE, ONE, ONE, Felt::new_unchecked(1)]); + let key_2: Word = Word::new([ + Felt::new_unchecked(2), + Felt::new_unchecked(2), + Felt::new_unchecked(2), + Felt::new_unchecked(2), + ]); + let key_3: Word = Word::new([ + Felt::new_unchecked(0), + Felt::new_unchecked(0), + Felt::new_unchecked(0), + Felt::new_unchecked(3), + ]); + + let value_1 = Word::new([ONE; Word::NUM_ELEMENTS]); + let value_2 = Word::new([Felt::from_u32(2_u32); Word::NUM_ELEMENTS]); + let value_3 = Word::new([Felt::from_u32(3_u32); Word::NUM_ELEMENTS]); smt.insert(key_1, value_1).unwrap(); smt.insert(key_2, value_2).unwrap(); @@ -414,12 +482,32 @@ fn test_insert_batch_empty_tree() { let entries = vec![ ( - crate::Word::new([Felt::new(1), Felt::new(0), Felt::new(0), Felt::new(0)]), - crate::Word::new([Felt::new(10), Felt::new(20), Felt::new(30), Felt::new(40)]), + Word::new([ + Felt::new_unchecked(1), + Felt::new_unchecked(0), + Felt::new_unchecked(0), + Felt::new_unchecked(0), + ]), + Word::new([ + Felt::new_unchecked(10), + Felt::new_unchecked(20), + Felt::new_unchecked(30), + Felt::new_unchecked(40), + ]), ), ( - crate::Word::new([Felt::new(2), Felt::new(0), Felt::new(0), Felt::new(0)]), - crate::Word::new([Felt::new(11), Felt::new(22), Felt::new(33), Felt::new(44)]), + Word::new([ + Felt::new_unchecked(2), + Felt::new_unchecked(0), + Felt::new_unchecked(0), + Felt::new_unchecked(0), + ]), + Word::new([ + Felt::new_unchecked(11), + Felt::new_unchecked(22), + Felt::new_unchecked(33), + Felt::new_unchecked(44), + ]), ), ]; @@ -439,13 +527,23 @@ fn test_insert_batch_with_deletions() { let mut smt = LargeSmt::new(storage).unwrap(); // Initial data - let key_1 = crate::Word::new([ONE, ONE, ONE, Felt::new(1)]); - let key_2 = crate::Word::new([Felt::new(2), Felt::new(2), Felt::new(2), Felt::new(2)]); - let key_3 = crate::Word::new([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(3)]); - - let value_1 = crate::Word::new([ONE; WORD_SIZE]); - let value_2 = crate::Word::new([Felt::new(2); WORD_SIZE]); - let value_3 = crate::Word::new([Felt::new(3); WORD_SIZE]); + let key_1 = Word::new([ONE, ONE, ONE, Felt::new_unchecked(1)]); + let key_2 = Word::new([ + Felt::new_unchecked(2), + Felt::new_unchecked(2), + Felt::new_unchecked(2), + Felt::new_unchecked(2), + ]); + let key_3 = Word::new([ + Felt::new_unchecked(0), + Felt::new_unchecked(0), + Felt::new_unchecked(0), + Felt::new_unchecked(3), + ]); + + let value_1 = Word::new([ONE; Word::NUM_ELEMENTS]); + let value_2 = Word::new([Felt::new_unchecked(2); Word::NUM_ELEMENTS]); + let value_3 = Word::new([Felt::new_unchecked(3); Word::NUM_ELEMENTS]); smt.insert(key_1, value_1).unwrap(); smt.insert(key_2, value_2).unwrap(); @@ -469,8 +567,8 @@ fn test_insert_batch_no_mutations() { let storage = MemoryStorage::new(); let mut smt = LargeSmt::new(storage).unwrap(); - let key_1 = crate::Word::new([ONE, ONE, ONE, Felt::new(1)]); - let value_1 = crate::Word::new([ONE; WORD_SIZE]); + let key_1 = Word::new([ONE, ONE, ONE, Felt::new_unchecked(1)]); + let value_1 = Word::new([ONE; Word::NUM_ELEMENTS]); smt.insert(key_1, value_1).unwrap(); let root_before = smt.root(); @@ -517,8 +615,13 @@ fn test_flat_layout_index_zero_unused_in_instance() { // Index 0 should always be EMPTY_WORD (unused) assert_eq!(in_memory_nodes[0], EMPTY_WORD, "Index 0 should be EMPTY_WORD (unused)"); - let key = Word::new([ONE, ONE, ONE, Felt::new(1)]); - let value = Word::new([Felt::new(42), Felt::new(43), Felt::new(44), Felt::new(45)]); + let key = Word::new([ONE, ONE, ONE, Felt::new_unchecked(1)]); + let value = Word::new([ + Felt::new_unchecked(42), + Felt::new_unchecked(43), + Felt::new_unchecked(44), + Felt::new_unchecked(45), + ]); smt.insert(key, value).unwrap(); let in_memory_nodes = smt.in_memory_nodes(); @@ -537,8 +640,13 @@ fn test_flat_layout_after_insertion() { let storage = MemoryStorage::new(); let mut smt = LargeSmt::<_>::new(storage).unwrap(); - let key = Word::new([ONE, ONE, ONE, Felt::new(1)]); - let value = Word::new([Felt::new(42), Felt::new(43), Felt::new(44), Felt::new(45)]); + let key = Word::new([ONE, ONE, ONE, Felt::new_unchecked(1)]); + let value = Word::new([ + Felt::new_unchecked(42), + Felt::new_unchecked(43), + Felt::new_unchecked(44), + Felt::new_unchecked(45), + ]); smt.insert(key, value).unwrap(); @@ -575,8 +683,8 @@ fn test_flat_layout_children_relationship() { (0..num_samples).map(|_| rng.random::() % (1 << 20)).collect(); for leaf_value in &leaf_indices { - let key = Word::new([ONE, ONE, ONE, Felt::new(*leaf_value)]); - let value = Word::new([Felt::new(*leaf_value * 10); 4]); + let key = Word::new([ONE, ONE, ONE, Felt::new_unchecked(*leaf_value)]); + let value = Word::new([Felt::new_unchecked(*leaf_value * 10); 4]); smt.insert(key, value).unwrap(); } @@ -610,16 +718,14 @@ fn test_flat_layout_children_relationship() { let child_on_path = if is_right_child { right_child } else { left_child }; assert_ne!( child_on_path, empty_hash, - "Child on path should be non-empty at depth {}, value {} (on path to leaf {})", - depth, node_value, leaf_value + "Child on path should be non-empty at depth {depth}, value {node_value} (on path to leaf {leaf_value})" ); // Verify the parent-child hash relationship let node_hash = Poseidon2::merge(&[left_child, right_child]); assert_eq!( in_memory_nodes[memory_idx], node_hash, - "Stored hash at memory_idx {} should match computed hash from children at depth {}, value {}", - memory_idx, depth, node_value + "Stored hash at memory_idx {memory_idx} should match computed hash from children at depth {depth}, value {node_value}" ); } } diff --git a/miden-crypto/src/merkle/smt/large_forest/backend/memory/mod.rs b/miden-crypto/src/merkle/smt/large_forest/backend/memory/mod.rs index 753eb030b2..0b6aa1a475 100644 --- a/miden-crypto/src/merkle/smt/large_forest/backend/memory/mod.rs +++ b/miden-crypto/src/merkle/smt/large_forest/backend/memory/mod.rs @@ -10,7 +10,7 @@ use alloc::vec::Vec; use crate::{ EMPTY_WORD, Map, Word, merkle::smt::{ - Smt, SmtProof, VersionId, + LeafIndex, SMT_DEPTH, Smt, SmtLeaf, SmtProof, VersionId, large_forest::{ Backend, backend::{BackendError, MutationSet, Result}, @@ -47,13 +47,30 @@ impl Backend for InMemoryBackend { /// /// # Errors /// - /// - [`BackendError::UnknownLineage`] If the provided `lineage` is one not known by this + /// - [`BackendError::UnknownLineage`] if the provided `lineage` is one not known by this /// backend. fn open(&self, lineage: LineageId, key: Word) -> Result { let tree = self.trees.get(&lineage).ok_or(BackendError::UnknownLineage(lineage))?; Ok(tree.tree.open(&key)) } + /// Returns the leaf stored at `leaf_index` in the SMT with the specified `lineage`. + /// + /// If no leaf is explicitly stored at the given index, an empty leaf for that index is + /// returned. + /// + /// # Errors + /// + /// - [`BackendError::UnknownLineage`] if the provided `lineage` is one not known by this + /// backend. + fn get_leaf(&self, lineage: LineageId, leaf_index: LeafIndex) -> Result { + let tree = self.trees.get(&lineage).ok_or(BackendError::UnknownLineage(lineage))?; + Ok(tree + .tree + .get_leaf_by_index(leaf_index) + .unwrap_or_else(|| SmtLeaf::new_empty(leaf_index))) + } + /// Returns the value associated with the provided `key` in the SMT with the specified /// `lineage`, or [`None`] if no such value exists. /// @@ -113,9 +130,9 @@ impl Backend for InMemoryBackend { /// /// - [`BackendError::UnknownLineage`] if the provided `lineage` is one not known by this /// backend. - fn entries(&self, lineage: LineageId) -> Result> { + fn entries(&self, lineage: LineageId) -> Result>> { let tree = self.trees.get(&lineage).ok_or(BackendError::UnknownLineage(lineage))?; - Ok(tree.tree.entries().map(|(k, v)| TreeEntry { key: *k, value: *v })) + Ok(tree.tree.entries().map(|(k, v)| Ok(TreeEntry { key: *k, value: *v }))) } /// Adds the provided `lineage` to the forest. @@ -141,7 +158,7 @@ impl Backend for InMemoryBackend { // A failure to compute mutations is a failure derived from user input, so we forward it as // appropriate. - let mutations = tree.compute_mutations(updates.into_iter().map(|o| o.into()))?; + let mutations = tree.compute_mutations(updates.into_iter().map(Into::into))?; // If computation of the mutations has succeeded but the application fails, then this should // be reported as an internal error, not a merkle error, to allow the caller to decide what @@ -181,7 +198,7 @@ impl Backend for InMemoryBackend { // We compute the mutations as a precondition check, which will leave the underlying tree in // the same state if anything errors. Any error this yields is considered to be derived from // user-input and hence is forwarded as-is. - let mutations = tree.compute_mutations(updates.into_iter().map(|o| o.into()))?; + let mutations = tree.compute_mutations(updates.into_iter().map(Into::into))?; // The invariants on this method given by the `Backend` trait states that no new allocations // should be performed if the updates do not change the tree. As a result, we can @@ -205,6 +222,70 @@ impl Backend for InMemoryBackend { Ok(reversion_set) } + /// Adds multiple new `lineages` to the tree, creating an empty tree for each and applying the + /// provided modifications to it, with the result being given the specified `version`. + /// + /// If the provide batch of modifications is empty for any given lineage, then the **empty tree + /// will be added** as the first version in that lineage. + /// + /// # Errors + /// + /// - [`BackendError::DuplicateLineage`] if any provided lineage conflicts with an already-known + /// lineage. No data is changed in this case. + /// - [`BackendError::Merkle`] if any of the provided updates cannot be applied on top of the + /// empty tree. + fn add_lineages( + &mut self, + version: VersionId, + lineages: SmtForestUpdateBatch, + ) -> Result> { + // We start by checking that all lineages referred to in the batch of `updates` are valid, + // failing early with an error if need be. + let updates = lineages + .into_iter() + .map(|(lineage, ops)| { + if self.trees.contains_key(&lineage) { + return Err(BackendError::DuplicateLineage(lineage)); + } + Ok((lineage, ops)) + }) + .collect::>>()?; + + // Next, we compute all the relevant mutations to each tree, also failing with an error + // where relevant. + let mutations = updates + .into_iter() + .map(|(lineage, ops)| { + let tree = Smt::new(); + let mutations = tree.compute_mutations(ops.into_iter().map(Into::into))?; + Ok((lineage, tree, mutations)) + }) + .collect::>>()?; + + // With the preconditions checked, we can unconditionally perform the changes on all trees. + // We apply all mutations first without modifying self.trees, so that if any mutation + // fails, no data is changed. + let applied = mutations + .into_iter() + .map(|(lineage, mut tree, mutations)| { + tree.apply_mutations(mutations).map_err(BackendError::internal_from)?; + Ok((lineage, tree)) + }) + .collect::>>()?; + + // Once they have all succeeded, we then modify the data in memory. + let results = applied + .into_iter() + .map(|(lineage, tree)| { + let root = tree.root(); + self.trees.insert(lineage, TreeData { version, tree }); + (lineage, TreeWithRoot::new(lineage, version, root)) + }) + .collect::>(); + + Ok(results) + } + /// Performs the provided `updates` on the entire forest, returning the mutation sets that would /// reverse the changes to each tree in the forest. /// @@ -244,7 +325,7 @@ impl Backend for InMemoryBackend { .into_iter() .map(|(lineage, ops)| { let tree = self.trees.get(&lineage).expect("Tree known to be present was not"); - let mutations = tree.tree.compute_mutations(ops.into_iter().map(|o| o.into()))?; + let mutations = tree.tree.compute_mutations(ops.into_iter().map(Into::into))?; Ok((lineage, mutations)) }) .collect::>>()?; diff --git a/miden-crypto/src/merkle/smt/large_forest/backend/memory/property_tests.rs b/miden-crypto/src/merkle/smt/large_forest/backend/memory/property_tests.rs index df378d7bb4..466ab48294 100644 --- a/miden-crypto/src/merkle/smt/large_forest/backend/memory/property_tests.rs +++ b/miden-crypto/src/merkle/smt/large_forest/backend/memory/property_tests.rs @@ -106,7 +106,7 @@ proptest! { // We're going to need an auxiliary tree to check the behavior. let mut tree = Smt::new(); let muts_1 = - tree.compute_mutations(Vec::from(entries_v1.clone()).into_iter()).map_err(to_fail)?; + tree.compute_mutations(Vec::from(entries_v1).into_iter()).map_err(to_fail)?; tree.apply_mutations(muts_1).map_err(to_fail)?; let muts_2 = tree.compute_mutations(Vec::from(entries_v2.clone()).into_iter()).map_err(to_fail)?; @@ -226,7 +226,10 @@ proptest! { let backend_entries = backend .entries(target_lineage) .map_err(to_fail)? - .map(|e| (e.key, e.value)) + .map(|e| e.map(|e| (e.key, e.value))) + .collect::, _>>() + .map_err(to_fail)? + .into_iter() .sorted() .collect_vec(); let tree_entries = tree.entries().copied().sorted().collect_vec(); @@ -247,7 +250,7 @@ proptest! { // And create a normal tree to compare against. let mut tree = Smt::new(); let tree_mutations = - tree.compute_mutations(Vec::from(entries.clone()).into_iter()).map_err(to_fail)?; + tree.compute_mutations(Vec::from(entries).into_iter()).map_err(to_fail)?; tree.apply_mutations(tree_mutations).map_err(to_fail)?; // The root should return the same results as that. @@ -260,6 +263,46 @@ proptest! { prop_assert!(backend.lineages().map_err(to_fail)?.contains(&lineage)); } + #[test] + fn add_lineages_correct( + lineages in prop::collection::vec(arbitrary_lineage(), 0..30), + version in arbitrary_version(), + entries in prop::collection::vec(arbitrary_batch(), 0..30), + ) { + let mut backend = InMemoryBackend::new(); + let pairs = lineages.into_iter().unique().zip(entries).collect_vec(); + let mut batch = SmtForestUpdateBatch::empty(); + for (lineage, entries) in &pairs { + *batch.operations(*lineage) = entries.clone(); + } + + // Add all lineages in one call. + let results = backend.add_lineages(version, batch).map_err(to_fail)?; + + // Verify each result against a reference tree. + for (lineage, entries) in &pairs { + let mut tree = Smt::new(); + let tree_mutations = + tree.compute_mutations(Vec::from(entries.clone()).into_iter()).map_err(to_fail)?; + tree.apply_mutations(tree_mutations).map_err(to_fail)?; + + let (_, result) = results.iter().find(|(l, _)| l == lineage).unwrap(); + prop_assert_eq!(result.root(), tree.root()); + prop_assert_eq!(result.version(), version); + prop_assert_eq!(result.lineage(), *lineage); + + // Verify cross-lineage gets. + for op in entries.clone().into_iter() { + let (key, value) = op.into(); + let backend_value = backend.get(*lineage, key).map_err(to_fail)?; + let expected = if value == EMPTY_WORD { None } else { Some(value) }; + prop_assert_eq!(backend_value, expected); + } + } + + prop_assert_eq!(backend.lineages().map_err(to_fail)?.count(), pairs.len()); + } + #[test] fn update_lineage_correct( lineage_1 in arbitrary_lineage(), diff --git a/miden-crypto/src/merkle/smt/large_forest/backend/memory/tests.rs b/miden-crypto/src/merkle/smt/large_forest/backend/memory/tests.rs index ac1c47e4af..3268043ae8 100644 --- a/miden-crypto/src/merkle/smt/large_forest/backend/memory/tests.rs +++ b/miden-crypto/src/merkle/smt/large_forest/backend/memory/tests.rs @@ -5,6 +5,8 @@ //! existing [`Smt`] implementation, comparing the results of the in-memory backend against it //! wherever relevant. +use alloc::vec::Vec; + use assert_matches::assert_matches; use itertools::Itertools; @@ -385,22 +387,11 @@ fn entries() -> Result<()> { backend.update_tree(lineage_1, version, operations)?; // Now, the iterator should yield the expected three items. - assert_eq!(backend.entries(lineage_1)?.count(), 3); - assert!( - backend - .entries(lineage_1)? - .contains(&TreeEntry { key: key_1_1, value: value_1_1 }), - ); - assert!( - backend - .entries(lineage_1)? - .contains(&TreeEntry { key: key_1_2, value: value_1_2 }), - ); - assert!( - backend - .entries(lineage_1)? - .contains(&TreeEntry { key: key_1_3, value: value_1_3 }), - ); + let entries = backend.entries(lineage_1)?.collect::>>()?; + assert_eq!(entries.len(), 3); + assert!(entries.contains(&TreeEntry { key: key_1_1, value: value_1_1 })); + assert!(entries.contains(&TreeEntry { key: key_1_2, value: value_1_2 })); + assert!(entries.contains(&TreeEntry { key: key_1_3, value: value_1_3 })); Ok(()) } @@ -445,6 +436,130 @@ fn add_lineage() -> Result<()> { Ok(()) } +#[test] +fn add_lineages() -> Result<()> { + let mut backend = InMemoryBackend::new(); + let mut rng = ContinuousRng::new([0xa1; 32]); + + // An empty batch should return an empty result and leave the backend unchanged. + let version: VersionId = rng.value(); + let result = backend.add_lineages(version, SmtForestUpdateBatch::empty())?; + assert!(result.is_empty()); + assert_eq!(backend.lineages()?.count(), 0); + assert_eq!(backend.trees()?.count(), 0); + + // A single lineage with two inserts should work correctly. + let lineage_1: LineageId = rng.value(); + let key_1_1: Word = rng.value(); + let value_1_1: Word = rng.value(); + let key_1_2: Word = rng.value(); + let value_1_2: Word = rng.value(); + + let mut batch = SmtForestUpdateBatch::empty(); + batch.operations(lineage_1).add_insert(key_1_1, value_1_1); + batch.operations(lineage_1).add_insert(key_1_2, value_1_2); + + let result = backend.add_lineages(version, batch)?; + assert_eq!(result.len(), 1); + + let mut ref_tree_1 = Smt::new(); + ref_tree_1.insert(key_1_1, value_1_1)?; + ref_tree_1.insert(key_1_2, value_1_2)?; + + assert_eq!(result[0].1.root(), ref_tree_1.root()); + assert_eq!(backend.get(lineage_1, key_1_1)?, Some(value_1_1)); + assert_eq!(backend.get(lineage_1, key_1_2)?, Some(value_1_2)); + + // Reset backend for multi-lineage test. + let mut backend = InMemoryBackend::new(); + + let lineage_a: LineageId = rng.value(); + let lineage_b: LineageId = rng.value(); + let lineage_c: LineageId = rng.value(); + + let key_a_1: Word = rng.value(); + let value_a_1: Word = rng.value(); + let key_a_2: Word = rng.value(); + let value_a_2: Word = rng.value(); + let key_b_1: Word = rng.value(); + let value_b_1: Word = rng.value(); + let key_b_2: Word = rng.value(); + let value_b_2: Word = rng.value(); + let key_c_1: Word = rng.value(); + let value_c_1: Word = rng.value(); + let key_c_2: Word = rng.value(); + let value_c_2: Word = rng.value(); + + let mut batch = SmtForestUpdateBatch::empty(); + batch.operations(lineage_a).add_insert(key_a_1, value_a_1); + batch.operations(lineage_a).add_insert(key_a_2, value_a_2); + batch.operations(lineage_b).add_insert(key_b_1, value_b_1); + batch.operations(lineage_b).add_insert(key_b_2, value_b_2); + batch.operations(lineage_c).add_insert(key_c_1, value_c_1); + batch.operations(lineage_c).add_insert(key_c_2, value_c_2); + + let result = backend.add_lineages(version, batch)?; + assert_eq!(result.len(), 3); + + // Build reference trees and verify roots. + let mut ref_a = Smt::new(); + ref_a.insert(key_a_1, value_a_1)?; + ref_a.insert(key_a_2, value_a_2)?; + + let mut ref_b = Smt::new(); + ref_b.insert(key_b_1, value_b_1)?; + ref_b.insert(key_b_2, value_b_2)?; + + let mut ref_c = Smt::new(); + ref_c.insert(key_c_1, value_c_1)?; + ref_c.insert(key_c_2, value_c_2)?; + + // Find each lineage in the results and check the root. + let root_a = result.iter().find(|(l, _)| *l == lineage_a).unwrap().1.root(); + let root_b = result.iter().find(|(l, _)| *l == lineage_b).unwrap().1.root(); + let root_c = result.iter().find(|(l, _)| *l == lineage_c).unwrap().1.root(); + assert_eq!(root_a, ref_a.root()); + assert_eq!(root_b, ref_b.root()); + assert_eq!(root_c, ref_c.root()); + assert_eq!(backend.lineages()?.count(), 3); + + // Cross-lineage gets should work correctly. + assert_eq!(backend.get(lineage_a, key_a_1)?, Some(value_a_1)); + assert_eq!(backend.get(lineage_b, key_b_2)?, Some(value_b_2)); + assert_eq!(backend.get(lineage_c, key_c_1)?, Some(value_c_1)); + + // Verify gets spanning all three lineages. + assert_eq!(backend.get(lineage_a, key_a_1)?, Some(value_a_1)); + assert_eq!(backend.get(lineage_b, key_b_1)?, Some(value_b_1)); + assert_eq!(backend.get(lineage_c, key_c_2)?, Some(value_c_2)); + + // Duplicate lineage error and atomicity: pre-add one lineage, then try a batch containing it. + let mut backend = InMemoryBackend::new(); + let existing_lineage: LineageId = rng.value(); + let new_lineage: LineageId = rng.value(); + + let mut ops = SmtUpdateBatch::default(); + ops.add_insert(rng.value(), rng.value()); + backend.add_lineage(existing_lineage, version, ops)?; + let backend_before = backend.clone(); + + let mut batch = SmtForestUpdateBatch::empty(); + batch.operations(existing_lineage).add_insert(rng.value(), rng.value()); + batch.operations(new_lineage).add_insert(rng.value(), rng.value()); + + let result = backend.add_lineages(version, batch); + assert!(result.is_err()); + assert_matches!( + result.unwrap_err(), + BackendError::DuplicateLineage(l) if l == existing_lineage + ); + + // Backend state should be unchanged (atomicity). + assert_eq!(backend, backend_before); + + Ok(()) +} + #[test] fn update_tree() -> Result<()> { let mut backend = InMemoryBackend::new(); diff --git a/miden-crypto/src/merkle/smt/large_forest/backend/mod.rs b/miden-crypto/src/merkle/smt/large_forest/backend/mod.rs index 772e500bd1..fc6076fcc4 100644 --- a/miden-crypto/src/merkle/smt/large_forest/backend/mod.rs +++ b/miden-crypto/src/merkle/smt/large_forest/backend/mod.rs @@ -15,7 +15,7 @@ use crate::{ merkle::{ MerkleError, smt::{ - SmtProof, + LeafIndex, SMT_DEPTH, SmtLeaf, SmtProof, large_forest::{ operation::{SmtForestUpdateBatch, SmtUpdateBatch}, root::{LineageId, TreeEntry, TreeWithRoot, VersionId}, @@ -83,6 +83,14 @@ where /// backend. The backend must return an error if the lineage does not exist. fn open(&self, lineage: LineageId, key: Word) -> Result; + /// Returns the leaf stored at the provided `leaf_index` in the SMT with the specified + /// `lineage`. If no leaf is explicitly stored at the given index, the backend must return + /// an empty leaf for that index. + /// + /// It is the responsibility of the forest to ensure lineage existence before querying the + /// backend. The backend must return an error if the lineage does not exist. + fn get_leaf(&self, lineage: LineageId, leaf_index: LeafIndex) -> Result; + /// Returns the value associated with the provided `key` in the SMT with the specified /// `lineage`, or [`None`] if no such value exists. /// @@ -111,6 +119,14 @@ where /// /// It is the responsibility of the forest to ensure lineage existence before querying the /// backend. The backend must return an error if the lineage does not exist. + /// + /// # Expected Behavior + /// + /// Implementations must guarantee the following behavior in addition to the global invariants: + /// + /// - This method must be **cheap** to call, not requiring network or disk I/O to service the + /// result. This usually implies in-memory caching of the data. + /// - This method must not return errors other than if the lineage does not exist. fn entry_count(&self, lineage: LineageId) -> Result; /// Returns an iterator that yields the populated (key-value) entries for the specified @@ -121,7 +137,17 @@ where /// /// The iterator may yield entries in any arbitrary order, but must not yield entries for which /// the value is the empty word. - fn entries(&self, lineage: LineageId) -> Result>; + /// + /// # Expected Behavior + /// + /// Implementations must guarantee the following behavior in addition to the global invariants: + /// + /// - If any kind of error occurs during iteration that should be signaled to the user, the + /// iterator must return `Some(Err(...))`. The caller should stop iteration after receiving an + /// error as the iterator state is no longer valid. + /// - `None` will be returned upon successful completion, or at any time after an error has been + /// returned. + fn entries(&self, lineage: LineageId) -> Result>>; // SINGLE-TREE MODIFIERS // ============================================================================================ @@ -163,6 +189,22 @@ where // MULTI-TREE MODIFIERS // ============================================================================================ + /// Adds multiple new `lineages` to the backend with the provided `version` and sets the + /// associated SMTs to have the value created by applying the provided updates to the empty + /// tree, returning the new root of that tree. + /// + /// # Expected Behavior + /// + /// Implementations must guarantee the following behavior in addition to the global invariants: + /// + /// - If any provided lineage conflicts with an already-existing lineage in the backend, it must + /// return [`BackendError::DuplicateLineage`]. + fn add_lineages( + &mut self, + version: VersionId, + lineages: SmtForestUpdateBatch, + ) -> Result>; + /// Performs the provided `updates` on the forest, setting all new tree states to have the /// provided `new_version` and returning a vector of the mutation sets that reverse the changes /// to each changed tree. diff --git a/miden-crypto/src/merkle/smt/large_forest/backend/persistent/config.rs b/miden-crypto/src/merkle/smt/large_forest/backend/persistent/config.rs index dc8b325636..9428812d96 100644 --- a/miden-crypto/src/merkle/smt/large_forest/backend/persistent/config.rs +++ b/miden-crypto/src/merkle/smt/large_forest/backend/persistent/config.rs @@ -23,6 +23,11 @@ const DEFAULT_BLOOM_FILTER_BITS_PER_KEY: c_double = 10.0; /// The default target file size for the files that make up the database (512 MiB). const DEFAULT_TARGET_FILE_SIZE: u64 = 512 << 20; +/// The default setting for synchronous writes (`false`). +/// +/// When `false`, writes are buffered and not immediately flushed to disk. +const DEFAULT_SYNC_WRITES: bool = false; + // CONFIG TYPE // ================================================================================================ @@ -76,6 +81,15 @@ pub struct Config { /// /// Defaults to [`DEFAULT_TARGET_FILE_SIZE`]. pub(super) target_file_size: u64, + + /// Whether each write should be synchronously flushed to disk. + /// + /// When `true`, every write is synchronously flushed to disk, providing stronger durability + /// guarantees but with reduced write performance. When `false` (the default), writes are + /// buffered and only flushed to disk when the buffers become full. + /// + /// Defaults to [`DEFAULT_SYNC_WRITES`]. + pub(super) sync_writes: bool, } impl Config { @@ -90,6 +104,7 @@ impl Config { /// - `cache_size_bytes`: 2 GiB /// - `max_open_files`: 512 /// - `max_wal_size`: 1 GiB + /// - `sync_writes`: `false` /// /// # Errors /// @@ -123,6 +138,7 @@ impl Config { max_wal_size: DEFAULT_MAX_TOTAL_WAL_SIZE_BYTES, bloom_filter_bits: DEFAULT_BLOOM_FILTER_BITS_PER_KEY, target_file_size: DEFAULT_TARGET_FILE_SIZE, + sync_writes: DEFAULT_SYNC_WRITES, }) } } @@ -189,6 +205,18 @@ impl Config { self.target_file_size = target_file_size; self } + + /// Sets whether writes should be synchronously flushed to disk. + /// + /// When `true`, every write is synchronously flushed to disk, providing stronger durability + /// guarantees but with reduced write performance. When `false` (the default), writes are + /// buffered only flushed to disk when the buffer becomes full. + /// + /// Defaults to `false`. + pub fn with_sync_writes(mut self, sync_writes: bool) -> Self { + self.sync_writes = sync_writes; + self + } } // TESTS @@ -210,6 +238,7 @@ mod test { assert_eq!(config.max_wal_size, DEFAULT_MAX_TOTAL_WAL_SIZE_BYTES); assert_eq!(config.bloom_filter_bits, DEFAULT_BLOOM_FILTER_BITS_PER_KEY); assert_eq!(config.target_file_size, DEFAULT_TARGET_FILE_SIZE); + assert_eq!(config.sync_writes, DEFAULT_SYNC_WRITES); Ok(()) } @@ -268,4 +297,15 @@ mod test { Ok(()) } + + #[test] + fn with_sync_writes() -> Result<()> { + let tempdir = tempdir()?; + let config = Config::new(tempdir.path())?; + let config = config.with_sync_writes(true); + + assert!(config.sync_writes); + + Ok(()) + } } diff --git a/miden-crypto/src/merkle/smt/large_forest/backend/persistent/iterator.rs b/miden-crypto/src/merkle/smt/large_forest/backend/persistent/iterator.rs index 64b2fe4239..0692dca07e 100644 --- a/miden-crypto/src/merkle/smt/large_forest/backend/persistent/iterator.rs +++ b/miden-crypto/src/merkle/smt/large_forest/backend/persistent/iterator.rs @@ -39,7 +39,7 @@ impl<'db> PersistentBackendEntriesIterator<'db> { /// Constructs a new such iterator in the starting state. /// /// The provided `iterator` must yield items where the key decodes to a `LeafKey` and the value - /// decodes to an `SmtLeaf`. If this is not the case, iteration will panic. + /// decodes to an `SmtLeaf`. If this is not the case, iteration will yield an error. /// /// For performance, this iterator should be passed a prefix iterator over the database with the /// correct prefix (corresponding to the provided `lineage`) set, but it will still function @@ -61,26 +61,38 @@ enum PersistentBackendEntriesIteratorState { /// The iterator over the current leaf's entries. leaf_entries: Box>, }, + + /// The iterator has encountered an error and will yield no further items. + Faulted, } impl<'db> Iterator for PersistentBackendEntriesIterator<'db> { - type Item = TreeEntry; + type Item = super::Result; /// Advances the iterator and returns the next item if present. - /// - /// # Panics - /// - /// - If unable to read from the underlying database. fn next(&mut self) -> Option { loop { match &mut self.state { + PersistentBackendEntriesIteratorState::Faulted => return None, PersistentBackendEntriesIteratorState::NotInLeaf => { // Here we are not in a leaf of the targeted tree, so we have to see if we _can_ // be. if let Some(entry) = self.iterator.next() { - let (key_bytes, value_bytes) = entry.expect("Able to read from the DB"); - let key = LeafKey::read_from_bytes(&key_bytes) - .expect("Leaf key data read from disk is not corrupt"); + let (key_bytes, value_bytes) = match entry { + Ok((key_bytes, value_bytes)) => (key_bytes, value_bytes), + Err(e) => { + self.state = PersistentBackendEntriesIteratorState::Faulted; + return Some(Err(e.into())); + }, + }; + + let key = match LeafKey::read_from_bytes(&key_bytes) { + Ok(key) => key, + Err(e) => { + self.state = PersistentBackendEntriesIteratorState::Faulted; + return Some(Err(e.into())); + }, + }; // If the key isn't for the correct lineage (which can happen even with the // bloom filter), we need to advance by returning to the loop. @@ -90,8 +102,13 @@ impl<'db> Iterator for PersistentBackendEntriesIterator<'db> { // If the key is valid, we need to read out the leaf itself and then start // iterating over that. - let leaf = SmtLeaf::read_from_bytes(&value_bytes) - .expect("Leaf data read from disk is not corrupt"); + let leaf = match SmtLeaf::read_from_bytes(&value_bytes) { + Ok(leaf) => leaf, + Err(e) => { + self.state = PersistentBackendEntriesIteratorState::Faulted; + return Some(Err(e.into())); + }, + }; let mut leaf_entries = leaf.into_entries(); leaf_entries.sort_by_key(|(k, _)| *k); @@ -106,7 +123,7 @@ impl<'db> Iterator for PersistentBackendEntriesIterator<'db> { PersistentBackendEntriesIteratorState::InLeaf { leaf_entries } => { if let Some((key, value)) = leaf_entries.next() { // Here we have an entry in the leaf, so we simply need to return it. - return Some(TreeEntry { key, value }); + return Some(Ok(TreeEntry { key, value })); } else { // If we've run out of entries in the leaf itself, we need to see if there // is another valid leaf. We do this by changing state and looping to use diff --git a/miden-crypto/src/merkle/smt/large_forest/backend/persistent/keys.rs b/miden-crypto/src/merkle/smt/large_forest/backend/persistent/keys.rs index 64aef7807c..9fff105624 100644 --- a/miden-crypto/src/merkle/smt/large_forest/backend/persistent/keys.rs +++ b/miden-crypto/src/merkle/smt/large_forest/backend/persistent/keys.rs @@ -12,7 +12,7 @@ use crate::merkle::{NodeIndex, smt::LineageId}; // ================================================================================================ /// A key that uniquely identifies a leaf in the database. -#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] pub struct LeafKey { /// The lineage (and hence tree) to which the leaf belongs. pub lineage: LineageId, @@ -45,7 +45,7 @@ impl Deserializable for LeafKey { // ================================================================================================ /// A key that uniquely identifies a subtree in the database. -#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] +#[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)] pub struct SubtreeKey { /// The lineage (and hence tree) to which the subtree belongs. pub lineage: LineageId, diff --git a/miden-crypto/src/merkle/smt/large_forest/backend/persistent/mod.rs b/miden-crypto/src/merkle/smt/large_forest/backend/persistent/mod.rs index 7a2b0cc98c..b03e1b7ce3 100644 --- a/miden-crypto/src/merkle/smt/large_forest/backend/persistent/mod.rs +++ b/miden-crypto/src/merkle/smt/large_forest/backend/persistent/mod.rs @@ -142,6 +142,9 @@ const L0_FILE_COMPACTION_TRIGGER: c_int = 8; /// combine their batches in parallel. const MIN_LINEAGES_IN_BATCH_TO_PARALLELIZE: usize = 5; +/// The minimum number of items per rayon chunk when parallelizing deserialization and extraction. +const CHUNKING_UNIT: usize = 100; + // PERSISTENT BACKEND // ================================================================================================ @@ -171,6 +174,12 @@ pub struct PersistentBackend { /// Care must be taken that this is _always_ kept in sync with the on-disk copy in the /// [`METADATA_CF`] column. lineages: HashMap, + + /// Whether writes should be synchronously flushed to disk. + /// + /// Setting this to true will result in reduced throughput but may result in higher durability + /// in the presence of crashes. + sync_writes: bool, } // CONSTRUCTION @@ -189,8 +198,9 @@ impl PersistentBackend { pub fn load(config: Config) -> Result { let db = Arc::new(Self::build_db_with_options(&config)?); let lineages = Self::read_all_metadata(db.clone())?; + let sync_writes = config.sync_writes; - Ok(Self { db, lineages }) + Ok(Self { db, lineages, sync_writes }) } } @@ -218,7 +228,7 @@ impl Backend for PersistentBackend { // We then have to load both the corresponding leaf, and the siblings for its path out of // storage. - let mut leaf_index: NodeIndex = LeafIndex::from(key).into(); + let leaf_index: NodeIndex = LeafIndex::from(key).into(); // We calculate the roots of the subtrees in order to know their keys for loading. As an // opening only ever needs to retrieve 8 subtrees we just do this sequentially. @@ -239,28 +249,28 @@ impl Backend for PersistentBackend { subtree_cache.insert(root, maybe_tree.unwrap_or_else(|| Subtree::new(root))); } - // Now we can read the necessary path from the cached subtree roots. - let mut path = Vec::with_capacity(SMT_DEPTH as usize); - - while leaf_index.depth() > 0 { - let is_right = leaf_index.is_position_odd(); - leaf_index = leaf_index.parent(); + let merkle_path = self.compute_path(leaf_index, &subtree_cache); - let root = Subtree::find_subtree_root(leaf_index); - let subtree = &subtree_cache[&root]; // Known to exist by construction. - let InnerNode { left, right } = - subtree.get_inner_node(leaf_index).unwrap_or_else(|| { - EmptySubtreeRoots::get_inner_node(SMT_DEPTH, leaf_index.depth()) - }); + // This is safe to do unchecked as we ensure that the path is valid by construction. + Ok(SmtProof::new_unchecked(merkle_path, leaf)) + } - path.push(if is_right { left } else { right }); + /// Returns the leaf stored at `leaf_index` in the SMT with the specified `lineage`. + /// + /// If no leaf is explicitly stored at the given index, an empty leaf for that index is + /// returned. + /// + /// # Errors + /// + /// - [`BackendError::UnknownLineage`] if the provided `lineage` is not known by the backend. + /// - [`BackendError::Internal`] if the backing database cannot be accessed for some reason. + fn get_leaf(&self, lineage: LineageId, leaf_index: LeafIndex) -> Result { + if !self.lineages.contains_key(&lineage) { + return Err(BackendError::UnknownLineage(lineage)); } - let merkle_path = - SparseMerklePath::from_sized_iter(path).expect("Always succeeds by construction"); - - // This is safe to do unchecked as we ensure that the path is valid by construction. - Ok(SmtProof::new_unchecked(merkle_path, leaf)) + let key = LeafKey { lineage, index: leaf_index.position() }; + Ok(self.load_leaf_raw(&key)?.unwrap_or_else(|| SmtLeaf::new_empty(leaf_index))) } /// Returns the value associated with the provided `key` in the specified `lineage`, or [`None`] @@ -340,7 +350,7 @@ impl Backend for PersistentBackend { /// /// - [`BackendError::UnknownLineage`] if the provided `lineage` is not one known by this /// backend. - fn entries(&self, lineage: LineageId) -> Result> { + fn entries(&self, lineage: LineageId) -> Result>> { if !self.lineages.contains_key(&lineage) { return Err(BackendError::UnknownLineage(lineage)); } @@ -468,6 +478,105 @@ impl Backend for PersistentBackend { Ok(reversion_set) } + /// Adds multiple new `lineages` to the tree, creating an empty tree for each and applying the + /// provided modifications to it, with the result being given the specified `version`. + /// + /// If the provide batch of modifications is empty for any given lineage, then the **empty tree + /// will be added** as the first version in that lineage. + /// + /// # Errors + /// + /// - [`BackendError::DuplicateLineage`] if any of the provided lineages already exists in the + /// backend. + /// - [`BackendError::Internal`] if the database cannot be accessed at any point. + /// - [`BackendError::Merkle`] if an error occurs with the merkle tree semantics. + fn add_lineages( + &mut self, + version: VersionId, + lineages: SmtForestUpdateBatch, + ) -> Result> { + // We start by checking that none of the lineages already exist, as we are expected by + // contract to error if any is a duplicate. + let updates = lineages + .into_iter() + .map(|(lineage, ops)| { + if self.lineages.contains_key(&lineage) { + return Err(BackendError::DuplicateLineage(lineage)); + } + Ok((lineage, ops)) + }) + .collect::>>()?; + let lineage_count = updates.len(); + + // If we have no lineages, then we can exit early. + if updates.is_empty() { + return Ok(Vec::new()); + } + + // Build the initial metadata and set up empty write batches for each new lineage + let updates_with_batch = updates + .into_iter() + .map(|(lineage, ops)| { + let new_meta = TreeMetadata { + version, + root_value: *EmptySubtreeRoots::entry(SMT_DEPTH, 0), + entry_count: 0, + }; + let batch = WriteBatch::default(); + (lineage, ops, new_meta, batch) + }) + .collect::>(); + + // Process lineages in parallel, updating each tree in its own write batch + let lineage_data = updates_with_batch + .into_par_iter() + .map(|(lineage, ops, new_meta, batch)| { + let ops = ops.into_iter().map(Into::into).collect(); + let (batch, reversion, tree_data) = + self.update_tree_in_write_batch(batch, lineage, new_meta, version, ops)?; + let batch = self.write_metadata(batch, lineage, &tree_data)?; + let root = tree_data.root_value; + + Ok((batch, (lineage, tree_data, reversion), (lineage, root))) + }) + .collect::>>()?; + let (batches, mutation_sets, roots): (Vec<_>, Vec<_>, Vec<_>) = + lineage_data.into_iter().fold( + ( + Vec::with_capacity(lineage_count), + Vec::with_capacity(lineage_count), + Vec::with_capacity(lineage_count), + ), + |(mut bs, mut ms, mut rs), (b, m, r)| { + bs.push(b); + ms.push(m); + rs.push(r); + (bs, ms, rs) + }, + ); + + // Merge all the write batches into one atomic batch + let final_batch = if lineage_count > MIN_LINEAGES_IN_BATCH_TO_PARALLELIZE { + batches + .into_par_iter() + .fold(WriteBatch::new, |l, r| merge_batches(l, &r)) + .reduce(WriteBatch::new, |l, r| merge_batches(l, &r)) + } else { + batches.into_iter().fold(WriteBatch::new(), |l, r| merge_batches(l, &r)) + }; + + // Atomically write to disk and update in-memory cache. + self.finalize_update(final_batch, mutation_sets.into_iter())?; + + // Build the return value from the captured roots. + let results = roots + .into_iter() + .map(|(lineage, root)| (lineage, TreeWithRoot::new(lineage, version, root))) + .collect(); + + Ok(results) + } + /// Performs the provided `updates` on the entire forest, returning the mutation sets that would /// reverse the changes to each lineage in the forest. /// @@ -487,23 +596,17 @@ impl Backend for PersistentBackend { ) -> Result> { // We first have to check our precondition that all lineages are valid, returning an error // as required by our contract if any lineage is unknown to the backend. - let updates = updates + let updates: Vec<_> = updates .into_iter() .map(|(lineage, ops)| { if !self.lineages.contains_key(&lineage) { return Err(BackendError::UnknownLineage(lineage)); } - - Ok((lineage, ops)) + let tree_data = self.lineages.get(&lineage).expect("Known to exist").clone(); + Ok((lineage, ops, tree_data)) }) .collect::>>()?; let lineage_count = updates.len(); - let updates = updates - .into_iter() - .map(|(lineage, ops)| { - (lineage, ops, self.lineages.get(&lineage).expect("Known to exist").clone()) - }) - .collect::>(); // We want to update all trees as part of an atomic update to the backing database, but we // also want to do this in parallel. As we cannot share a transaction directly, we instead @@ -520,7 +623,7 @@ impl Backend for PersistentBackend { let lineage_data = updates_with_batch .into_par_iter() .map(|(lineage, ops, tree_data, batch)| { - let ops = ops.into_iter().map(|op| op.into()).collect(); + let ops = ops.into_iter().map(Into::into).collect(); let (batch, reversion, tree_data) = self.update_tree_in_write_batch(batch, lineage, tree_data, new_version, ops)?; let batch = self.write_metadata(batch, lineage, &tree_data)?; @@ -555,6 +658,32 @@ impl Backend for PersistentBackend { /// This block contains methods for internal use only that provide useful functionality for the /// implementation of the backend. impl PersistentBackend { + /// Computes the merkle path for the provided `lineage` beginning at the provided `leaf_index` + /// using the pre-loaded `subtrees`. + fn compute_path( + &self, + mut leaf_index: NodeIndex, + subtrees: &HashMap, + ) -> SparseMerklePath { + let mut path = Vec::with_capacity(SMT_DEPTH as usize); + + while leaf_index.depth() > 0 { + let is_right = leaf_index.is_position_odd(); + leaf_index = leaf_index.parent(); + + let root = Subtree::find_subtree_root(leaf_index); + let subtree = &subtrees[&root]; // Known to exist by construction. + let InnerNode { left, right } = + subtree.get_inner_node(leaf_index).unwrap_or_else(|| { + EmptySubtreeRoots::get_inner_node(SMT_DEPTH, leaf_index.depth()) + }); + + path.push(if is_right { left } else { right }); + } + + SparseMerklePath::from_sized_iter(path).expect("Always succeeds by construction") + } + /// Performs `updates` on the tree in the specified lineage, assigning the new tree the /// provided `new_version`. /// @@ -578,9 +707,15 @@ impl PersistentBackend { // of various other operations. updates.sort_by_key(|(k, _)| LeafIndex::from(*k).position()); - // We then have to load the leaves that correspond to these pairs from storage. - let leaf_map = self - .get_leaves_for_keys(lineage, &updates.iter().map(|(k, _)| *k).collect::>())?; + // We then have to load the leaves that correspond to these pairs from storage. If the tree + // is known to be empty (entry_count == 0), we skip the disk read entirely as all leaves + // are guaranteed to not exist. This is primarily an optimization for the case of adding + // new trees. + let leaf_map = if tree_metadata.entry_count == 0 { + HashMap::new() + } else { + self.get_leaves_for_keys(lineage, &updates.iter().map(|(k, _)| *k).collect::>())? + }; // We then process the leaves in parallel to determine the mutations that we need to apply // to the full tree. @@ -961,7 +1096,7 @@ impl PersistentBackend { let leaf_index = LeafIndex::from(leaf_pairs[0].0); let maybe_old_leaf = leaf_map.get(&leaf_index.position()).and_then(Option::as_ref); - let old_entry_count = maybe_old_leaf.map(|leaf| leaf.num_entries()).unwrap_or_default(); + let old_entry_count = maybe_old_leaf.map(SmtLeaf::num_entries).unwrap_or_default(); // Whenever we change a value in the current leaf, we have to store the _old_ version of // that value in our reversion pairs. @@ -1079,15 +1214,43 @@ impl PersistentBackend { /// /// - [`BackendError::Internal`] if the data cannot be loaded from the database. fn load_leaves(&self, lineage: LineageId, indices: &[u64]) -> Result>> { - let col = self.cf(LEAVES_CF)?; let keys = indices .iter() - .map(|index| LeafKey { lineage, index: *index }.to_bytes()) - .collect::>>(); - let leaves = self.db.multi_get_cf(keys.iter().map(|k| (col, k.as_slice()))); + .map(|index| LeafKey { lineage, index: *index }) + .collect::>(); + + self.load_leaves_direct(keys.iter()) + } + + /// Loads the concrete leaves from disk corresponding to the provided `keys`. + /// + /// # Errors + /// + /// - [`BackendError::Internal`] if the data cannot be loaded from the database. + fn load_leaves_direct<'b>( + &self, + keys: impl Iterator, + ) -> Result>> { + let bytes = keys.map(Serializable::to_bytes).collect::>(); + self.load_leaves_raw(bytes.iter()) + } + + /// Loads the concrete leaves from disk corresponding to the provided `keys`. + /// + /// # Errors + /// + /// - [`BackendError::Internal`] if the data cannot be loaded from the database. + #[inline(always)] + fn load_leaves_raw<'b>( + &self, + key_bytes: impl Iterator>, + ) -> Result>> { + let col = self.cf(LEAVES_CF)?; + let leaves = self.db.multi_get_cf(key_bytes.map(|k| (col, k.as_slice()))); leaves - .into_iter() + .into_par_iter() + .with_min_len(CHUNKING_UNIT) .map(|result| match result { Ok(Some(bytes)) => { Ok(Some(SmtLeaf::read_from_bytes_with_budget(&bytes, bytes.len())?)) @@ -1098,14 +1261,15 @@ impl PersistentBackend { .collect() } - /// Gets the leaf from disk in the provided `lineage` that would contain `key`. - fn load_leaf_for(&self, lineage: LineageId, key: Word) -> Result> { + /// Gets the leaf with the provided `key` from disk, or returns [`None`] if it is not stored. + /// + /// # Errors + /// + /// - [`BackendError::Internal`] if the database cannot be successfully queried. + #[inline(always)] + fn load_leaf_raw(&self, key: &LeafKey) -> Result> { let col = self.cf(LEAVES_CF)?; - let key_bytes = LeafKey { - lineage, - index: LeafIndex::from(key).position(), - } - .to_bytes(); + let key_bytes = key.to_bytes(); let leaf_bytes = self.db.get_cf(col, key_bytes)?; let leaf = match leaf_bytes { Some(bytes) => Some(SmtLeaf::read_from_bytes_with_budget(&bytes, bytes.len())?), @@ -1115,6 +1279,19 @@ impl PersistentBackend { Ok(leaf) } + /// Gets the leaf from disk in the provided `lineage` that would contain `key`. + /// + /// # Errors + /// + /// - [`BackendError::Internal`] if the database cannot be successfully queried. + fn load_leaf_for(&self, lineage: LineageId, key: Word) -> Result> { + let key = LeafKey { + lineage, + index: LeafIndex::from(key).position(), + }; + self.load_leaf_raw(&key) + } + /// Gets the column family corresponding to the subtree with root index `index`. /// /// # Errors @@ -1122,7 +1299,17 @@ impl PersistentBackend { /// - [`BackendError::Internal`] if the database cannot be accessed to get the column family. #[inline(always)] fn subtree_cf(&self, index: NodeIndex) -> Result<&db::ColumnFamily> { - let cf_name = subtree_cf_name(index.depth()); + self.subtree_cf_depth(index.depth()) + } + + /// Gets the column family corresponding to the subtree with root index `index`. + /// + /// # Errors + /// + /// - [`BackendError::Internal`] if the database cannot be accessed to get the column family. + #[inline(always)] + fn subtree_cf_depth(&self, depth: u8) -> Result<&db::ColumnFamily> { + let cf_name = subtree_cf_name(depth); self.cf(cf_name) } @@ -1145,7 +1332,7 @@ impl PersistentBackend { /// - [`BackendError::Internal`] if writing to the database fails for any reason. fn write(&self, batch: WriteBatch) -> Result<()> { let mut write_opts = db::WriteOptions::default(); - write_opts.set_sync(false); + write_opts.set_sync(self.sync_writes); self.db.write_opt(batch, &write_opts)?; Ok(()) diff --git a/miden-crypto/src/merkle/smt/large_forest/backend/persistent/property_tests.rs b/miden-crypto/src/merkle/smt/large_forest/backend/persistent/property_tests.rs index 1831665b07..2f30129432 100644 --- a/miden-crypto/src/merkle/smt/large_forest/backend/persistent/property_tests.rs +++ b/miden-crypto/src/merkle/smt/large_forest/backend/persistent/property_tests.rs @@ -54,6 +54,7 @@ proptest! { } } + #[test] fn get_correct( lineage in arbitrary_lineage(), @@ -102,7 +103,7 @@ proptest! { // We're going to need an auxiliary tree to check the behavior. let mut tree = Smt::new(); - let muts_1 =tree.compute_mutations(Vec::from(entries_v1.clone()).into_iter())?; + let muts_1 =tree.compute_mutations(Vec::from(entries_v1).into_iter())?; tree.apply_mutations(muts_1)?; let muts_2 =tree.compute_mutations(Vec::from(entries_v2.clone()).into_iter())?; @@ -217,7 +218,9 @@ proptest! { // And we should have the same number of entries in each. let backend_entries = backend .entries(target_lineage)? - .map(|e| (e.key, e.value)) + .map(|e| e.map(|e| (e.key, e.value))) + .collect::, _>>()? + .into_iter() .sorted() .collect_vec(); let tree_entries = tree.entries().copied().sorted().collect_vec(); @@ -237,7 +240,7 @@ proptest! { // And create a normal tree to compare against. let mut tree = Smt::new(); - let tree_mutations =tree.compute_mutations(Vec::from(entries.clone()).into_iter())?; + let tree_mutations =tree.compute_mutations(Vec::from(entries).into_iter())?; tree.apply_mutations(tree_mutations)?; // The root should return the same results as that. diff --git a/miden-crypto/src/merkle/smt/large_forest/backend/persistent/tests.rs b/miden-crypto/src/merkle/smt/large_forest/backend/persistent/tests.rs index 8d74c41385..57645b96a4 100644 --- a/miden-crypto/src/merkle/smt/large_forest/backend/persistent/tests.rs +++ b/miden-crypto/src/merkle/smt/large_forest/backend/persistent/tests.rs @@ -5,6 +5,8 @@ //! existing [`Smt`] implementation, comparing the results of the persistent backend against it //! wherever relevant. +use alloc::vec::Vec; + use assert_matches::assert_matches; use itertools::Itertools; use tempfile::{TempDir, tempdir}; @@ -126,7 +128,12 @@ fn load_extant() -> Result<()> { assert_eq!(l2_value, Some(t2_value)); // ...and entries. - let l1_entries = backend.entries(lineage_1)?.sorted().collect_vec(); + let l1_entries = backend + .entries(lineage_1)? + .collect::, _>>()? + .into_iter() + .sorted() + .collect_vec(); let t1_entries = tree_1 .entries() .sorted() @@ -134,7 +141,12 @@ fn load_extant() -> Result<()> { .collect_vec(); assert_eq!(l1_entries, t1_entries); - let l2_entries = backend.entries(lineage_2)?.sorted().collect_vec(); + let l2_entries = backend + .entries(lineage_2)? + .collect::, _>>()? + .into_iter() + .sorted() + .collect_vec(); let t2_entries = tree_2 .entries() .sorted() @@ -493,22 +505,11 @@ fn entries() -> Result<()> { backend.update_tree(lineage_1, version, operations)?; // Now, the iterator should yield the expected three items. - assert_eq!(backend.entries(lineage_1)?.count(), 3); - assert!( - backend - .entries(lineage_1)? - .contains(&TreeEntry { key: key_1_1, value: value_1_1 }), - ); - assert!( - backend - .entries(lineage_1)? - .contains(&TreeEntry { key: key_1_2, value: value_1_2 }), - ); - assert!( - backend - .entries(lineage_1)? - .contains(&TreeEntry { key: key_1_3, value: value_1_3 }), - ); + let entries = backend.entries(lineage_1)?.collect::>>()?; + assert_eq!(entries.len(), 3); + assert!(entries.contains(&TreeEntry { key: key_1_1, value: value_1_1 })); + assert!(entries.contains(&TreeEntry { key: key_1_2, value: value_1_2 })); + assert!(entries.contains(&TreeEntry { key: key_1_3, value: value_1_3 })); Ok(()) } @@ -618,6 +619,130 @@ fn update_tree() -> Result<()> { Ok(()) } +#[test] +fn add_lineages() -> Result<()> { + let (_file, mut backend) = default_backend()?; + let mut rng = ContinuousRng::new([0xa1; 32]); + + // An empty batch should return an empty result and leave the backend unchanged. + let version: VersionId = rng.value(); + let result = backend.add_lineages(version, SmtForestUpdateBatch::empty())?; + assert!(result.is_empty()); + assert_eq!(backend.lineages()?.count(), 0); + assert_eq!(backend.trees()?.count(), 0); + + // A single lineage with two inserts should work correctly. + let lineage_1: LineageId = rng.value(); + let key_1_1: Word = rng.value(); + let value_1_1: Word = rng.value(); + let key_1_2: Word = rng.value(); + let value_1_2: Word = rng.value(); + + let mut batch = SmtForestUpdateBatch::empty(); + batch.operations(lineage_1).add_insert(key_1_1, value_1_1); + batch.operations(lineage_1).add_insert(key_1_2, value_1_2); + + let result = backend.add_lineages(version, batch)?; + assert_eq!(result.len(), 1); + + let mut ref_tree_1 = Smt::new(); + ref_tree_1.insert(key_1_1, value_1_1)?; + ref_tree_1.insert(key_1_2, value_1_2)?; + + assert_eq!(result[0].1.root(), ref_tree_1.root()); + assert_eq!(backend.get(lineage_1, key_1_1)?, Some(value_1_1)); + assert_eq!(backend.get(lineage_1, key_1_2)?, Some(value_1_2)); + + // Multi-lineage test with a fresh backend. + let (_file2, mut backend) = default_backend()?; + + let lineage_a: LineageId = rng.value(); + let lineage_b: LineageId = rng.value(); + let lineage_c: LineageId = rng.value(); + + let key_a_1: Word = rng.value(); + let value_a_1: Word = rng.value(); + let key_a_2: Word = rng.value(); + let value_a_2: Word = rng.value(); + let key_b_1: Word = rng.value(); + let value_b_1: Word = rng.value(); + let key_b_2: Word = rng.value(); + let value_b_2: Word = rng.value(); + let key_c_1: Word = rng.value(); + let value_c_1: Word = rng.value(); + let key_c_2: Word = rng.value(); + let value_c_2: Word = rng.value(); + + let mut batch = SmtForestUpdateBatch::empty(); + batch.operations(lineage_a).add_insert(key_a_1, value_a_1); + batch.operations(lineage_a).add_insert(key_a_2, value_a_2); + batch.operations(lineage_b).add_insert(key_b_1, value_b_1); + batch.operations(lineage_b).add_insert(key_b_2, value_b_2); + batch.operations(lineage_c).add_insert(key_c_1, value_c_1); + batch.operations(lineage_c).add_insert(key_c_2, value_c_2); + + let result = backend.add_lineages(version, batch)?; + assert_eq!(result.len(), 3); + + // Build reference trees and verify roots. + let mut ref_a = Smt::new(); + ref_a.insert(key_a_1, value_a_1)?; + ref_a.insert(key_a_2, value_a_2)?; + + let mut ref_b = Smt::new(); + ref_b.insert(key_b_1, value_b_1)?; + ref_b.insert(key_b_2, value_b_2)?; + + let mut ref_c = Smt::new(); + ref_c.insert(key_c_1, value_c_1)?; + ref_c.insert(key_c_2, value_c_2)?; + + let root_a = result.iter().find(|(l, _)| *l == lineage_a).unwrap().1.root(); + let root_b = result.iter().find(|(l, _)| *l == lineage_b).unwrap().1.root(); + let root_c = result.iter().find(|(l, _)| *l == lineage_c).unwrap().1.root(); + assert_eq!(root_a, ref_a.root()); + assert_eq!(root_b, ref_b.root()); + assert_eq!(root_c, ref_c.root()); + assert_eq!(backend.lineages()?.count(), 3); + + // Cross-lineage gets should work correctly. + assert_eq!(backend.get(lineage_a, key_a_1)?, Some(value_a_1)); + assert_eq!(backend.get(lineage_b, key_b_2)?, Some(value_b_2)); + assert_eq!(backend.get(lineage_c, key_c_1)?, Some(value_c_1)); + + // Verify gets spanning all three lineages. + assert_eq!(backend.get(lineage_a, key_a_1)?, Some(value_a_1)); + assert_eq!(backend.get(lineage_b, key_b_1)?, Some(value_b_1)); + assert_eq!(backend.get(lineage_c, key_c_2)?, Some(value_c_2)); + + // Duplicate lineage error: pre-add one lineage, then try a batch containing it. + let (_file3, mut backend) = default_backend()?; + let existing_lineage: LineageId = rng.value(); + let new_lineage: LineageId = rng.value(); + + let mut ops = SmtUpdateBatch::default(); + ops.add_insert(rng.value(), rng.value()); + backend.add_lineage(existing_lineage, version, ops)?; + + let lineage_count_before = backend.lineages()?.count(); + + let mut batch = SmtForestUpdateBatch::empty(); + batch.operations(existing_lineage).add_insert(rng.value(), rng.value()); + batch.operations(new_lineage).add_insert(rng.value(), rng.value()); + + let result = backend.add_lineages(version, batch); + assert!(result.is_err()); + assert_matches!( + result.unwrap_err(), + BackendError::DuplicateLineage(l) if l == existing_lineage + ); + + // Backend state should be unchanged (atomicity). + assert_eq!(backend.lineages()?.count(), lineage_count_before); + + Ok(()) +} + #[test] fn update_forest() -> Result<()> { let (_file, mut backend) = default_backend()?; diff --git a/miden-crypto/src/merkle/smt/large_forest/history/mod.rs b/miden-crypto/src/merkle/smt/large_forest/history/mod.rs index 0a8d1ab665..f2355a10f2 100644 --- a/miden-crypto/src/merkle/smt/large_forest/history/mod.rs +++ b/miden-crypto/src/merkle/smt/large_forest/history/mod.rs @@ -131,7 +131,8 @@ impl History { } /// Adds a version to the history with the provided `root` and represented by the changes from - /// the current tree given in `nodes` and `leaves`. + /// the current tree given in `nodes` and `changed_keys`, with `entry_count` as the total + /// number of entries for the tree that corresponds to this version. /// /// If adding this version would result in exceeding `self.max_count` historical versions, then /// the oldest of the versions is automatically removed. @@ -157,6 +158,7 @@ impl History { version_id: VersionId, nodes: NodeChanges, changed_keys: ChangedKeys, + entry_count: usize, ) -> Result<()> { // We need to fail early if the provided new version is not monotonic with respect to the // latest version in the history. @@ -185,13 +187,15 @@ impl History { } } - self.deltas.push_back(Delta::new(root, version_id, nodes, changed_keys)); + self.deltas + .push_back(Delta::new(root, version_id, nodes, changed_keys, entry_count)); Ok(()) } - /// Adds a version to the history and represented by the changes from the current tree given - /// `mutations`. + /// Adds a version to the history, represented by the changes from the current tree given + /// `mutations`, with `entry_count` corresponding to the number of entries in the full tree + /// corresponding to this version. /// /// If adding this version would result in exceeding `self.max_count` historical versions, then /// the oldest of the versions is automatically removed. @@ -214,6 +218,7 @@ impl History { &mut self, version_id: VersionId, mutations: MutationSet, + entry_count: usize, ) -> Result<()> { let mut changed_keys = ChangedKeys::default(); mutations.new_pairs.into_iter().for_each(|(k, v)| { @@ -232,7 +237,7 @@ impl History { .collect(); // Now we can simply delegate to the standard function. - self.add_version(mutations.new_root, version_id, node_changes, changed_keys) + self.add_version(mutations.new_root, version_id, node_changes, changed_keys, entry_count) } /// Returns the index in the sequence of deltas of the version that corresponds to the provided @@ -382,20 +387,26 @@ impl<'history> HistoryView<'history> { /// Queries the value of a specific `key` in a leaf in the overlay, returning the value for that /// `key` if it has been changed, and [`None`] otherwise. #[must_use] - pub fn value(&self, key: &Word) -> Option { + pub fn value(self, key: &Word) -> Option { self.delta.changed_keys.get(key).cloned() } /// Returns `true` if the key is removed by this delta, and `false` otherwise. #[must_use] - pub fn is_key_removed(&self, key: &Word) -> bool { + pub fn is_key_removed(self, key: &Word) -> bool { self.delta.changed_keys.get(key).map(Word::is_empty).unwrap_or(false) } /// Returns an iterator which yields the entries that are added by this view. - pub fn changed_keys(&self) -> impl Iterator + 'history { + pub fn changed_keys(self) -> impl Iterator + 'history { self.delta.changed_keys.iter().map(|(k, v)| (*k, *v)) } + + /// Returns the total number of entries in the tree at this historical version. + #[must_use] + pub fn entry_count(self) -> usize { + self.delta.entry_count + } } // DELTA @@ -436,19 +447,30 @@ struct Delta { /// represented by the delta. This includes pairs that either add or mutate a value under a /// key, and pairs where the value is the `EMPTY_WORD` and hence represent removals. changed_keys: ChangedKeys, + + /// The total number of entries that existed in the tree at the version represented by this + /// delta, stored eagerly to avoid recomputation. + entry_count: usize, } impl Delta { /// Creates a new delta with the provided `root`, representing the provided changes to the - /// `nodes` in the merkle tree, and using `added_keys` and `removed_keys` to represent the - /// changes to entries in the tree. + /// `nodes` in the merkle tree, using `changed_keys` to represent the changes to entries in + /// the tree, and storing `entry_count` as the total number of entries at this version. #[must_use] fn new( root: RootValue, version_id: VersionId, nodes: NodeChanges, changed_keys: ChangedKeys, + entry_count: usize, ) -> Self { - Self { root, version_id, nodes, changed_keys } + Self { + root, + version_id, + nodes, + changed_keys, + entry_count, + } } } diff --git a/miden-crypto/src/merkle/smt/large_forest/history/tests.rs b/miden-crypto/src/merkle/smt/large_forest/history/tests.rs index d0ed0a14e0..4b515e702a 100644 --- a/miden-crypto/src/merkle/smt/large_forest/history/tests.rs +++ b/miden-crypto/src/merkle/smt/large_forest/history/tests.rs @@ -3,7 +3,9 @@ use alloc::vec::Vec; -use super::{ChangedKeys, History, NodeChanges, error::Result}; +use super::{ + super::test_utils::UNUSED_ENTRY_COUNT, ChangedKeys, History, NodeChanges, error::Result, +}; use crate::{ EMPTY_WORD, Felt, Word, field::PrimeCharacteristicRing, @@ -34,8 +36,8 @@ fn roots() -> Result<()> { let mut history = History::empty(2); let root_1: Word = rng.value(); let root_2: Word = rng.value(); - history.add_version(root_1, 0, nodes.clone(), changed_keys.clone())?; - history.add_version(root_2, 1, nodes.clone(), changed_keys.clone())?; + history.add_version(root_1, 0, nodes.clone(), changed_keys.clone(), UNUSED_ENTRY_COUNT)?; + history.add_version(root_2, 1, nodes, changed_keys, UNUSED_ENTRY_COUNT)?; // We should be able to get all the roots. let roots = history.roots().collect::>(); @@ -61,11 +63,35 @@ fn find_latest_corresponding_version() -> Result<()> { let v4 = 31; let v5 = 45; - history.add_version(rng.value(), v1, nodes.clone(), changed_keys.clone())?; - history.add_version(rng.value(), v2, nodes.clone(), changed_keys.clone())?; - history.add_version(rng.value(), v3, nodes.clone(), changed_keys.clone())?; - history.add_version(rng.value(), v4, nodes.clone(), changed_keys.clone())?; - history.add_version(rng.value(), v5, nodes.clone(), changed_keys.clone())?; + history.add_version( + rng.value(), + v1, + nodes.clone(), + changed_keys.clone(), + UNUSED_ENTRY_COUNT, + )?; + history.add_version( + rng.value(), + v2, + nodes.clone(), + changed_keys.clone(), + UNUSED_ENTRY_COUNT, + )?; + history.add_version( + rng.value(), + v3, + nodes.clone(), + changed_keys.clone(), + UNUSED_ENTRY_COUNT, + )?; + history.add_version( + rng.value(), + v4, + nodes.clone(), + changed_keys.clone(), + UNUSED_ENTRY_COUNT, + )?; + history.add_version(rng.value(), v5, nodes, changed_keys, UNUSED_ENTRY_COUNT)?; // When we query for a version that is older than the oldest in the history we should get an // error. @@ -107,18 +133,18 @@ fn add_version() -> Result<()> { let root_1: Word = rng.value(); let id_1 = 0; - history.add_version(root_1, id_1, nodes.clone(), changed_keys.clone())?; + history.add_version(root_1, id_1, nodes.clone(), changed_keys.clone(), UNUSED_ENTRY_COUNT)?; assert_eq!(history.num_versions(), 1); let root_2: Word = rng.value(); let id_2 = 1; - history.add_version(root_2, id_2, nodes.clone(), changed_keys.clone())?; + history.add_version(root_2, id_2, nodes.clone(), changed_keys.clone(), UNUSED_ENTRY_COUNT)?; assert_eq!(history.num_versions(), 2); // At this point, adding any version should remove the oldest. let root_3: Word = rng.value(); let id_3 = 2; - history.add_version(root_3, id_3, nodes.clone(), changed_keys.clone())?; + history.add_version(root_3, id_3, nodes.clone(), changed_keys.clone(), UNUSED_ENTRY_COUNT)?; assert_eq!(history.num_versions(), 2); // If we then query for that first version it won't be there anymore, but the other two @@ -128,7 +154,11 @@ fn add_version() -> Result<()> { assert!(history.get_view_at(id_3).is_ok()); // If we try and add a version with a non-monotonic version number, we should see an error. - assert!(history.add_version(root_3, id_1, nodes, changed_keys.clone()).is_err()); + assert!( + history + .add_version(root_3, id_1, nodes, changed_keys, UNUSED_ENTRY_COUNT) + .is_err() + ); Ok(()) } @@ -163,7 +193,7 @@ fn add_version_from_mutation_set() -> Result<()> { let mut history = History::empty(2); let version: VersionId = rng.value(); - history.add_version_from_mutation_set(version, mutations)?; + history.add_version_from_mutation_set(version, mutations, UNUSED_ENTRY_COUNT)?; // Now we can check that it did things correctly. let view = history.get_view_at(version)?; @@ -187,19 +217,19 @@ fn truncate() -> Result<()> { let root_1: Word = rng.value(); let id_1 = 5; - history.add_version(root_1, id_1, nodes.clone(), changed_keys.clone())?; + history.add_version(root_1, id_1, nodes.clone(), changed_keys.clone(), UNUSED_ENTRY_COUNT)?; let root_2: Word = rng.value(); let id_2 = 10; - history.add_version(root_2, id_2, nodes.clone(), changed_keys.clone())?; + history.add_version(root_2, id_2, nodes.clone(), changed_keys.clone(), UNUSED_ENTRY_COUNT)?; let root_3: Word = rng.value(); let id_3 = 15; - history.add_version(root_3, id_3, nodes.clone(), changed_keys.clone())?; + history.add_version(root_3, id_3, nodes.clone(), changed_keys.clone(), UNUSED_ENTRY_COUNT)?; let root_4: Word = rng.value(); let id_4 = 20; - history.add_version(root_4, id_4, nodes.clone(), changed_keys.clone())?; + history.add_version(root_4, id_4, nodes, changed_keys, UNUSED_ENTRY_COUNT)?; assert_eq!(history.num_versions(), 4); @@ -239,11 +269,11 @@ fn clear() -> Result<()> { let root_1: Word = rng.value(); let id_1 = 0; - history.add_version(root_1, id_1, nodes.clone(), changed_keys.clone())?; + history.add_version(root_1, id_1, nodes.clone(), changed_keys.clone(), UNUSED_ENTRY_COUNT)?; let root_2: Word = rng.value(); let id_2 = 1; - history.add_version(root_2, id_2, nodes.clone(), changed_keys.clone())?; + history.add_version(root_2, id_2, nodes, changed_keys, UNUSED_ENTRY_COUNT)?; assert_eq!(history.num_versions(), 2); @@ -286,7 +316,7 @@ fn view_at() -> Result<()> { changed_1.insert(l2_e1_key, l2_e1_value); changed_1.insert(l2_e2_key, l2_e2_value); - history.add_version(root_1, id_1, nodes_1.clone(), changed_1.clone())?; + history.add_version(root_1, id_1, nodes_1.clone(), changed_1.clone(), 3)?; assert_eq!(history.num_versions(), 1); // We then add another version that overlaps with the older version. @@ -306,7 +336,7 @@ fn view_at() -> Result<()> { l3_e1_key[3] = Felt::from_u64(leaf_3_ix.position()); let l3_e1_value: Word = rng.value(); changed_2.insert(l3_e1_key, l3_e1_value); - history.add_version(root_2, id_2, nodes_2.clone(), changed_2.clone())?; + history.add_version(root_2, id_2, nodes_2.clone(), changed_2.clone(), 7)?; assert_eq!(history.num_versions(), 2); // And another version for the sake of the test. @@ -327,7 +357,7 @@ fn view_at() -> Result<()> { let l1n_e1_value: Word = rng.value(); changed_3.insert(l1n_e1_key, l1n_e1_value); - history.add_version(root_3, id_3, nodes_3.clone(), changed_3.clone())?; + history.add_version(root_3, id_3, nodes_3.clone(), changed_3.clone(), 15)?; assert_eq!(history.num_versions(), 3); // At this point, we can grab a view into the history. If we grab something older than the @@ -337,6 +367,7 @@ fn view_at() -> Result<()> { // If we grab something valid, then we should get the right results. Let's grab the oldest // possible version to test the overlay logic. let view = history.get_view_at(id_1)?; + assert_eq!(view.entry_count(), 3); // Getting a node in the targeted version should just return it. assert_eq!(view.node_value(&NodeIndex::new(2, 1).unwrap()), Some(&n1_value)); @@ -387,6 +418,7 @@ fn view_at() -> Result<()> { let view = history.get_view_at(7)?; assert_eq!(view.node_value(&NodeIndex::new_unchecked(30, 1)), Some(&n5_value)); assert!(view.node_value(&NodeIndex::new_unchecked(30, 2)).is_none()); + assert_eq!(view.entry_count(), 15); Ok(()) } @@ -420,14 +452,16 @@ fn history_from_smt_non_overlapping() -> Result<()> { let mutations_v0 = smt.compute_mutations(vec![(key_1, value_1)]).unwrap(); let reversion_set = smt.apply_mutations_with_reversion(mutations_v0).unwrap(); let root_v0 = smt.root(); - history.add_version_from_mutation_set(0, reversion_set)?; + // Before this mutation the tree was empty, so the entry count for version 0 is 0. + history.add_version_from_mutation_set(0, reversion_set, 0)?; assert_eq!(history.num_versions(), 1); // Version 1: Insert second key-value pair let mutations_v1 = smt.compute_mutations(vec![(key_2, value_2)]).unwrap(); let reversion_set = smt.apply_mutations_with_reversion(mutations_v1).unwrap(); let root_v1 = smt.root(); - history.add_version_from_mutation_set(1, reversion_set)?; + // Before this mutation the tree had 1 entry (key_1), so the entry count for version 1 is 1. + history.add_version_from_mutation_set(1, reversion_set, 1)?; // Verify the roots for older states are tracked correctly in the history. assert!(history.is_known_root(initial_root)); @@ -441,10 +475,12 @@ fn history_from_smt_non_overlapping() -> Result<()> { let view_v0 = history.get_view_at(0)?; assert_eq!(view_v0.value(&key_1), Some(EMPTY_WORD)); assert_eq!(view_v0.value(&key_2), Some(EMPTY_WORD)); + assert_eq!(view_v0.entry_count(), 0); // When we query version 1 it should only make revert one change on top of the current tree. let view_v1 = history.get_view_at(1)?; assert_eq!(view_v1.value(&key_2), Some(EMPTY_WORD)); + assert_eq!(view_v1.entry_count(), 1); // Verify querying a non-existent key returns None let nonexistent_key: Word = rng.value(); @@ -468,20 +504,116 @@ fn history_from_smt_overlapping() -> Result<()> { // Version 0: Insert initial value let mutations_v0 = smt.compute_mutations(vec![(key, value_v0)]).unwrap(); let reversion_set = smt.apply_mutations_with_reversion(mutations_v0).unwrap(); - history.add_version_from_mutation_set(0, reversion_set)?; + // Before this mutation the tree was empty, so the entry count for version 0 is 0. + history.add_version_from_mutation_set(0, reversion_set, 0)?; // Version 1: Update to new value let mutations_v1 = smt.compute_mutations(vec![(key, value_v1)]).unwrap(); let reversion_set = smt.apply_mutations_with_reversion(mutations_v1).unwrap(); - history.add_version_from_mutation_set(1, reversion_set)?; + // Before this mutation the tree had 1 entry (key), so the entry count for version 1 is 1. + history.add_version_from_mutation_set(1, reversion_set, 1)?; // In version 0 we should have the correct (empty) value when reverted. let view_v0 = history.get_view_at(0)?; assert_eq!(view_v0.value(&key), Some(EMPTY_WORD)); + assert_eq!(view_v0.entry_count(), 0); // In version 1 we should have the value set in the transition to version 0. let view_v1 = history.get_view_at(1)?; assert_eq!(view_v1.value(&key), Some(value_v0)); + assert_eq!(view_v1.entry_count(), 1); + + Ok(()) +} + +#[test] +fn entry_count_single_version() -> Result<()> { + let mut rng = ContinuousRng::new([0x1c; 32]); + let mut history = History::empty(3); + + let root: Word = rng.value(); + history.add_version(root, 0, NodeChanges::default(), ChangedKeys::default(), 42)?; + + let view = history.get_view_at(0)?; + assert_eq!(view.entry_count(), 42); + + Ok(()) +} + +#[test] +fn entry_count_multiple_versions() -> Result<()> { + let mut rng = ContinuousRng::new([0x1d; 32]); + let mut history = History::empty(5); + + // Add versions with different entry counts. + history.add_version(rng.value(), 0, NodeChanges::default(), ChangedKeys::default(), 0)?; + history.add_version(rng.value(), 1, NodeChanges::default(), ChangedKeys::default(), 5)?; + history.add_version(rng.value(), 2, NodeChanges::default(), ChangedKeys::default(), 3)?; + history.add_version(rng.value(), 3, NodeChanges::default(), ChangedKeys::default(), 10)?; + + assert_eq!(history.get_view_at(0)?.entry_count(), 0); + assert_eq!(history.get_view_at(1)?.entry_count(), 5); + assert_eq!(history.get_view_at(2)?.entry_count(), 3); + assert_eq!(history.get_view_at(3)?.entry_count(), 10); + + Ok(()) +} + +#[test] +fn entry_count_after_eviction() -> Result<()> { + let mut rng = ContinuousRng::new([0x1e; 32]); + let mut history = History::empty(2); + + // Add 3 versions to a history that can hold only 2, causing eviction of the oldest. + history.add_version(rng.value(), 0, NodeChanges::default(), ChangedKeys::default(), 1)?; + history.add_version(rng.value(), 1, NodeChanges::default(), ChangedKeys::default(), 5)?; + history.add_version(rng.value(), 2, NodeChanges::default(), ChangedKeys::default(), 10)?; + + // Version 0 should have been evicted. + assert!(history.get_view_at(0).is_err()); + + // The remaining versions should still have the correct entry counts. + assert_eq!(history.get_view_at(1)?.entry_count(), 5); + assert_eq!(history.get_view_at(2)?.entry_count(), 10); + + Ok(()) +} + +#[test] +fn entry_count_after_truncation() -> Result<()> { + let mut rng = ContinuousRng::new([0x1f; 32]); + let mut history = History::empty(4); + + history.add_version(rng.value(), 5, NodeChanges::default(), ChangedKeys::default(), 2)?; + history.add_version(rng.value(), 10, NodeChanges::default(), ChangedKeys::default(), 7)?; + history.add_version(rng.value(), 15, NodeChanges::default(), ChangedKeys::default(), 12)?; + + // Truncate to version 10, removing version 5. + history.truncate(10); + assert_eq!(history.num_versions(), 2); + + // The surviving versions should retain their entry counts. + assert_eq!(history.get_view_at(10)?.entry_count(), 7); + assert_eq!(history.get_view_at(15)?.entry_count(), 12); + + Ok(()) +} + +#[test] +fn entry_count_reaches_zero_through_removals() -> Result<()> { + let mut rng = ContinuousRng::new([0x20; 32]); + let mut history = History::empty(4); + + // Simulate a tree that gains entries and then has them all removed. + history.add_version(rng.value(), 0, NodeChanges::default(), ChangedKeys::default(), 0)?; + history.add_version(rng.value(), 1, NodeChanges::default(), ChangedKeys::default(), 3)?; + history.add_version(rng.value(), 2, NodeChanges::default(), ChangedKeys::default(), 1)?; + history.add_version(rng.value(), 3, NodeChanges::default(), ChangedKeys::default(), 0)?; + + assert_eq!(history.get_view_at(0)?.entry_count(), 0); + assert_eq!(history.get_view_at(1)?.entry_count(), 3); + assert_eq!(history.get_view_at(2)?.entry_count(), 1); + assert_eq!(history.get_view_at(3)?.entry_count(), 0); Ok(()) } diff --git a/miden-crypto/src/merkle/smt/large_forest/iterator.rs b/miden-crypto/src/merkle/smt/large_forest/iterator.rs index 562b7f68e2..7acbe930de 100644 --- a/miden-crypto/src/merkle/smt/large_forest/iterator.rs +++ b/miden-crypto/src/merkle/smt/large_forest/iterator.rs @@ -22,6 +22,7 @@ use core::iter::Peekable; use miden_field::Word; +use super::Result; use crate::{ Set, merkle::smt::large_forest::{history::HistoryView, root::TreeEntry}, @@ -33,6 +34,12 @@ use crate::{ /// An iterator over the entries of an arbitrary tree in the forest, yielding entries in an /// arbitrary order. /// +/// - If any error occurs during iteration, this is signaled to the user by the iterator yielding +/// `Some(Err(...))`. The user should stop on first error, as the iterator will be in an +/// inconsistent state afterward. +/// - `None` is returned if the true end of the iterator is reached successfully, or at any point +/// after an error has been yielded. +/// /// It is split into two variants for performance, as iterating over a full tree is significantly /// simpler than iterating over a historical tree. While it would be nice to be able to return one /// of two different iterators depending on the circumstances of construction, Rust's `impl Trait` @@ -48,7 +55,8 @@ pub(super) enum EntriesIterator<'forest> { /// The iterator over the entries in the full tree. /// /// This iterator should never yield any entries where `value == EMPTY_WORD`. - full_tree_iter: Peekable + 'forest>>, + full_tree_iter: + Peekable> + 'forest>>, /// The view into the history at the correct point. history_view: HistoryView<'forest>, @@ -64,7 +72,10 @@ pub(super) enum EntriesIterator<'forest> { /// An iterator over a tree in the forest that is simply an iterator over the full tree. WithoutHistory { /// The iterator over the entries in the full tree. - full_tree_iter: Box + 'forest>, + full_tree_iter: Box> + 'forest>, + + /// Whether the iterator has encountered an error and should yield no more items. + faulted: bool, }, } @@ -74,7 +85,7 @@ impl<'forest> EntriesIterator<'forest> { /// /// Note that it _does not_ perform checks as to the correctness of the provided iterators. pub(super) fn new_with_history( - full_tree_iter: impl Iterator + 'forest, + full_tree_iter: impl Iterator> + 'forest, history_view: HistoryView<'forest>, ) -> Self { // This type gymnastics is unfortunately necessary to let us easily store the `Peekable` @@ -95,10 +106,10 @@ impl<'forest> EntriesIterator<'forest> { /// Note that it _does not_ check whether `full_tree_iter` is actually an iterator over the /// full tree. If it is not, this iterator will yield invalid results. pub(super) fn new_without_history( - full_tree_iter: impl Iterator + 'forest, + full_tree_iter: impl Iterator> + 'forest, ) -> Self { let full_tree_iter = Box::new(full_tree_iter); - Self::WithoutHistory { full_tree_iter } + Self::WithoutHistory { full_tree_iter, faulted: false } } /// Advances the iterator and returns the next value in the case where it is iterating over a @@ -108,7 +119,7 @@ impl<'forest> EntriesIterator<'forest> { /// /// - If the method is called with a `self` that is not in the [`Self::WithHistory`] variant. #[inline(always)] // To help the optimizer eliminate the redundant check in Iterator::next() - fn next_with_history(&mut self) -> Option { + fn next_with_history(&mut self) -> Option> { let EntriesIterator::WithHistory { full_tree_iter, history_view, @@ -121,6 +132,7 @@ impl<'forest> EntriesIterator<'forest> { loop { match state { + EntriesIteratorState::Faulted => return None, EntriesIteratorState::Initial => { // In the initial state we need to advance to the appropriate next state. if full_tree_iter.peek().is_none() { @@ -157,6 +169,13 @@ impl<'forest> EntriesIterator<'forest> { let Some(entry) = full_tree_iter.next() else { unreachable!("The iterator is known to have another item available"); }; + let entry = match entry { + Ok(entry) => entry, + Err(e) => { + *state = EntriesIteratorState::Faulted; + return Some(Err(e.into())); + }, + }; let value = if let Some(v) = history_view.value(&entry.key) { // If the history has a value for this key, then we need to return that @@ -174,7 +193,7 @@ impl<'forest> EntriesIterator<'forest> { entry }; - return Some(value); + return Some(Ok(value)); }, EntriesIteratorState::ReadingHistory { iterator } => { // This is a terminal state. We cannot transition to any other state from here, @@ -182,7 +201,7 @@ impl<'forest> EntriesIterator<'forest> { // history items does. for entry in iterator.by_ref() { if !yielded_history_keys.contains(&entry.key) && !entry.value.is_empty() { - return Some(entry); + return Some(Ok(entry)); } // Here we have already returned this entry, or it is empty. In both cases @@ -201,12 +220,23 @@ impl<'forest> EntriesIterator<'forest> { /// /// - If the method is called with a `self` that is not the [`Self::WithoutHistory`] variant. #[inline(always)] // To help the optimizer eliminate the redundant check in Iterator::next() - fn next_without_history(&mut self) -> Option { - let EntriesIterator::WithoutHistory { full_tree_iter } = self else { + fn next_without_history(&mut self) -> Option> { + // Note that the inner iterator yields items of type `backend::Result` while this one + // yields the outer result. Conversion happening via the standard `Into::into` conversion + // for Result. + let EntriesIterator::WithoutHistory { full_tree_iter, faulted } = self else { panic!("EntriesIterator::next_without_history called with history") }; - full_tree_iter.next() + if *faulted { + return None; + } + + let result = full_tree_iter.next().map(|e| e.map_err(Into::into)); + if matches!(&result, Some(Err(_))) { + *faulted = true; + } + result } } @@ -214,7 +244,7 @@ impl<'forest> EntriesIterator<'forest> { // ================================================================================================ impl Iterator for EntriesIterator<'_> { - type Item = TreeEntry; + type Item = Result; fn next(&mut self) -> Option { match self { @@ -243,4 +273,7 @@ pub(super) enum EntriesIteratorState<'forest> { /// The iterator over the entries that are _added_ by the history. iterator: Box + 'forest>, }, + + /// The iterator has encountered an error and will yield no more items. + Faulted, } diff --git a/miden-crypto/src/merkle/smt/large_forest/mod.rs b/miden-crypto/src/merkle/smt/large_forest/mod.rs index 04e8f72bfa..7cde494901 100644 --- a/miden-crypto/src/merkle/smt/large_forest/mod.rs +++ b/miden-crypto/src/merkle/smt/large_forest/mod.rs @@ -282,8 +282,8 @@ //! assert!(forest.get(current_tree, key_1)?.is_none()); //! //! // We can also get an iterator over all the entries in the tree. -//! let entries_old: Vec<_> = forest.entries(old_tree)?.collect(); -//! let entries_current: Vec<_> = forest.entries(current_tree)?.collect(); +//! let entries_old = forest.entries(old_tree)?.collect::, _>>()?; +//! let entries_current = forest.entries(current_tree)?.collect::, _>>()?; //! assert!(entries_old.contains(&TreeEntry { key: key_1, value: value_1 })); //! assert!(entries_old.contains(&TreeEntry { key: key_2, value: value_2 })); //! assert!(!entries_old.contains(&TreeEntry { key: key_3, value: value_3 })); @@ -487,7 +487,7 @@ impl LargeSmtForest { /// advances, with earlier items being roots from versions closer to the present. The current /// root of the lineage will thus always be the first item yielded by the iterator. pub fn lineage_roots(&self, lineage: LineageId) -> Option> { - self.lineage_data.get(&lineage).map(|d| d.roots()) + self.lineage_data.get(&lineage).map(LineageData::roots) } /// Gets the value root of the newest tree in the provided `lineage`, if that lineage is in the @@ -572,7 +572,7 @@ impl LargeSmtForest { /// # Errors /// /// - [`LargeSmtForestError::Fatal`] if the backend fails to operate properly during the query. - /// - [`LargeSmtForestError::UnknownLineage`] If the provided `tree` specifies a lineage that is + /// - [`LargeSmtForestError::UnknownLineage`] if the provided `tree` specifies a lineage that is /// not one known by the forest. /// - [`LargeSmtForestError::UnknownTree`] if the provided `tree` refers to a tree that is not a /// member of the forest. @@ -614,10 +614,32 @@ impl LargeSmtForest { .open(tree.lineage(), key) .map_err(Into::::into)?; + // Pre-collect the changed keys relevant to the target leaf and its deepest sibling + // in a single pass over the history delta, avoiding repeated full scans. + let sibling_leaf_index = + LeafIndex::new_max_depth(NodeIndex::from(leaf_index).sibling().position()); + let mut target_leaf_changes = Vec::new(); + let mut sibling_leaf_changes = Vec::new(); + for (k, v) in view.changed_keys() { + let key_leaf = LeafIndex::from(k); + if key_leaf == leaf_index { + target_leaf_changes.push((k, v)); + } else if key_leaf == sibling_leaf_index { + sibling_leaf_changes.push((k, v)); + } + } + // We compute the new leaf and new path by applying any reversions from the history on // top of the current state. - let new_leaf = self.merge_leaves(opening.leaf(), view)?; - let new_path = self.merge_paths(leaf_index, opening.path(), view)?; + let new_leaf = Self::merge_leaves(opening.leaf(), view, &target_leaf_changes)?; + let new_path = Self::merge_paths( + &self.backend, + tree.lineage(), + leaf_index, + opening.path(), + view, + &sibling_leaf_changes, + )?; // Finally we can compose our combined opening. Ok(SmtProof::new(new_path, new_leaf)?) @@ -678,14 +700,9 @@ impl LargeSmtForest { /// /// # Performance /// - /// Due to the way that tree data is stored, this method exhibits a split performance profile. - /// - /// - If querying for a `tree` that is the latest in its lineage, the time to return a result - /// should be constant. - /// - If querying for a `tree` that is a historical version, the time to return a result will be - /// linear in the number of entries in the tree. This is because an overlaid iterator has to - /// be created to yield the correct entries for the historical version, and then queried for - /// its length. + /// This method should always return its result in constant time. The exact performance profile + /// to do this is dependent on the backend for the most recent tree, but for historical trees + /// will be the same regardless of the backend in use. /// /// # Errors /// @@ -715,13 +732,17 @@ impl LargeSmtForest { return Err(LargeSmtForestError::UnknownTree(tree)); }; - // In the general case there is no faster path than doing the iteration to merge the - // history with the full tree, so we just count the iterator. - Ok(EntriesIterator::new_with_history(self.backend.entries(tree.lineage())?, view).count()) + Ok(view.entry_count()) } /// Returns an iterator that yields the entries in the specified `tree`. /// + /// - If any error occurs during iteration, this is signaled to the user by the iterator + /// yielding `Some(Err(...))`. The user should stop on first error, as the iterator will be in + /// an inconsistent state afterward. + /// - `None` is returned if the true end of the iterator is reached successfully, or at any + /// point after an error has been yielded. + /// /// # Performance /// /// The performance of the iterator depends both on the choice of backend _and_ the type of tree @@ -736,7 +757,7 @@ impl LargeSmtForest { /// not one known by the forest. /// - [`LargeSmtForestError::UnknownTree`] if the provided `tree` refers to a tree that is not a /// member of the forest. - pub fn entries(&self, tree: TreeId) -> Result> { + pub fn entries(&self, tree: TreeId) -> Result>> { // We start by yielding an error if we cannot get the lineage data for the specified tree. let Some(lineage_data) = self.lineage_data.get(&tree.lineage()) else { return Err(LargeSmtForestError::UnknownLineage(tree.lineage())); @@ -849,6 +870,16 @@ impl LargeSmtForest { return Err(LargeSmtForestError::UnknownLineage(lineage)); }; + // We capture the entry count before the backend update, as this is the count for the + // version being pushed into history. This must precede `update_tree` because the backend + // entry count changes after that call. + // + // By the contract of the `Backend` trait, `entry_count` is expected to be extremely cheap, + // and only fail if the lineage in question is missing. Versions of this method that are + // expensive to call, or that can fail due to I/O or other such reasons, are considered + // non-conformant. + let old_entry_count = self.backend.entry_count(lineage)?; + // We now know that we have a valid lineage and a valid version, so we perform the update in // the backend. let reversion_set = self.backend.update_tree(lineage, new_version, updates)?; @@ -872,7 +903,11 @@ impl LargeSmtForest { // hence should only ever fail due to a programmer bug so we panic if it does fail. lineage_data .history - .add_version_from_mutation_set(lineage_data.latest_version, reversion_set) + .add_version_from_mutation_set( + lineage_data.latest_version, + reversion_set, + old_entry_count, + ) .unwrap_or_else(|_| { panic!("Unable to add valid version {} to history", lineage_data.latest_version) }); @@ -905,6 +940,61 @@ impl LargeSmtForest { /// Where anything more specific can be said about performance, the method documentation will /// contain more detail. impl LargeSmtForest { + /// Adds multiple new `lineages` to the tree, creating an empty tree for each and applying the + /// provided modifications to it, with the result being given the specified `version`. + /// + /// If the provide batch of modifications is empty for any given lineage, then the **empty tree + /// will be added** as the first version in that lineage. + /// + /// # Performance + /// + /// This method is intended to be a reliable choice if the caller needs to add more than one + /// new lineage at once. At _worst_, its performance should be no slower than repeating + /// [`Self::add_lineage`] in a loop, but in some cases it may be significantly more performant. + /// + /// The exact scope of any speed-up is determined by the backend in use, so it is worth reading + /// the documentation for the Backend's `add_lineages` method. + /// + /// # Errors + /// + /// - [`LargeSmtForestError::DuplicateLineage`] if any of the provided lineages share an ID with + /// an already-known lineage. + /// - [`LargeSmtForestError::Fatal`] if the backend fails while being accessed. + /// - [`BackendError::Merkle`] if the provided `updates` cannot be applied to the empty tree. + pub fn add_lineages( + &mut self, + version: VersionId, + lineages: SmtForestUpdateBatch, + ) -> Result> { + // We start by performing our precondition checks: none of the lineages in the batch should + // already exist in the forest. + for lineage in lineages.lineages() { + if self.lineage_data.contains_key(lineage) { + return Err(LargeSmtForestError::DuplicateLineage(*lineage)); + } + } + + // With the preconditions checked we can call into the backend to perform the additions, and + // we forward all errors as this will be correct for conformant backend implementations. + let results = self.backend.add_lineages(version, lineages)?; + + // Now we have to update the lineage data for each newly-added lineage. New lineages have + // empty histories, so we do not need to insert into `non_empty_histories`. + results + .into_iter() + .map(|(lineage, tree_info)| { + let lineage_data = LineageData { + history: History::empty(self.config.max_history_versions()), + latest_version: tree_info.version(), + latest_root: tree_info.root(), + }; + self.lineage_data.insert(lineage, lineage_data); + + Ok(tree_info) + }) + .collect() + } + /// Performs the provided `updates` on the forest, adding at most one new root with version /// `new_version` to the forest for each target root in `updates` and returning a mapping /// from old root to the new root data. @@ -947,6 +1037,22 @@ impl LargeSmtForest { }) .collect::>>()?; + // We capture the entry counts before the backend update, as these are the counts for the + // versions being pushed into history. This must precede `update_forest` because the + // backend entry counts change after that call. We capture for all lineages eagerly + // (including those that may produce no-op updates) to keep the logic simple and consistent + // with `update_tree`, and use a map to eliminate any accidental dependencies on iteration + // order. + // + // By the contract of the `Backend` trait, `entry_count` is expected to be extremely cheap, + // and only fail if the lineage in question is missing. Versions of this method that are + // expensive to call, or that can fail due to I/O or other such reasons, are considered + // non-conformant. + let old_entry_counts: Map = updates + .lineages() + .map(|lineage| Ok((*lineage, self.backend.entry_count(*lineage)?))) + .collect::>()?; + // With the preconditions checked we can call into the backend to perform the updates, and // we forward all errors as this will be correct for conformant backend implementations. let reversion_sets = self.backend.update_forest(new_version, updates)?; @@ -956,6 +1062,8 @@ impl LargeSmtForest { reversion_sets .into_iter() .map(|(lineage, reversion)| { + // Known to exist by construction, so the bare index is safe. + let old_entry_count = old_entry_counts[&lineage]; let lineage_data = self .lineage_data .get_mut(&lineage) @@ -978,7 +1086,11 @@ impl LargeSmtForest { // hence should only ever fail due to a programmer bug so we panic if it does fail. lineage_data .history - .add_version_from_mutation_set(lineage_data.latest_version, reversion) + .add_version_from_mutation_set( + lineage_data.latest_version, + reversion, + old_entry_count, + ) .unwrap_or_else(|_| { panic!( "Unable to add valid version {} to history", @@ -1007,7 +1119,19 @@ impl LargeSmtForest { impl LargeSmtForest { /// Applies the history delta given by `history_view` on top of the provided `full_tree_leaf` to /// produce the correct leaf for a historical opening. - fn merge_leaves(&self, full_tree_leaf: &SmtLeaf, history_view: HistoryView) -> Result { + /// + /// `leaf_changes` must contain exactly the entries from the history's changed keys that belong + /// to the same leaf index as `full_tree_leaf`. Providing pre-filtered changes avoids repeated + /// full scans of the history delta. + /// + /// # Errors + /// + /// - [`LargeSmtForestError::SmtLeafError`] if the combined leaf cannot be computed correctly + fn merge_leaves( + full_tree_leaf: &SmtLeaf, + history_view: HistoryView, + leaf_changes: &[(Word, Word)], + ) -> Result { // We apply the historical delta on top of the existing entries to perform the reversion // back to the previous state. let mut leaf_entries = Map::new(); @@ -1021,12 +1145,9 @@ impl LargeSmtForest { } // The delta may have added items that we do not have (due to later removals), so we have to - // add those back, but only the ones for the leaf we care about. - leaf_entries.extend( - history_view - .changed_keys() - .filter(|(k, v)| LeafIndex::from(*k) == full_tree_leaf.index() && !v.is_empty()), - ); + // add those back, but only the ones for the leaf we care about. The caller has already + // filtered `leaf_changes` to only contain entries for this leaf. + leaf_entries.extend(leaf_changes.iter().filter(|(_, v)| !v.is_empty()).copied()); // At this point we should not see any entries with empty values, so in debug builds let's // sanity check this. @@ -1035,17 +1156,26 @@ impl LargeSmtForest { "Leaf entries should not contain entries with empty values" ); - // Any entries that are still empty at this point should be removed. - Ok(SmtLeaf::new(leaf_entries.into_iter().collect(), full_tree_leaf.index())?) + // We sort the entries to ensure a consistent ordering, as the map above is a HashMap + // which does not guarantee iteration order. + let mut entries = leaf_entries.into_iter().collect::>(); + entries.sort_by_key(|(key, value)| (*key, *value)); + Ok(SmtLeaf::new(entries, full_tree_leaf.index())?) } /// Applies any historical changes contained in `history_view` on top of the merkle path /// obtained from the full tree to produce the correct path for a historical opening. + /// + /// # Errors + /// + /// - [`LargeSmtForestError::Merkle`] if the merkle path cannot be created properly. fn merge_paths( - &self, + backend: &B, + lineage: LineageId, leaf_index: LeafIndex, full_tree_path: &SparseMerklePath, history_view: HistoryView, + sibling_leaf_changes: &[(Word, Word)], ) -> Result { let mut path_elems = [EMPTY_WORD; SMT_DEPTH as usize]; let mut current_node_ix = NodeIndex::from(leaf_index); @@ -1058,6 +1188,19 @@ impl LargeSmtForest { // If there is a historical value we need to use it, and so we write it to the // correct slot in the path elements array. path_elems[depth as usize - 1] = *historical_value; + } else if path_node_ix.depth() == SMT_DEPTH { + // The caller has already collected the sibling leaf's changed keys, so we can + // check for changes without scanning the full delta again. + if !sibling_leaf_changes.is_empty() { + let sibling_leaf_index = LeafIndex::new_max_depth(path_node_ix.position()); + let sibling_leaf = backend.get_leaf(lineage, sibling_leaf_index)?; + let sibling_leaf = + Self::merge_leaves(&sibling_leaf, history_view, sibling_leaf_changes)?; + path_elems[depth as usize - 1] = sibling_leaf.hash(); + } else { + let bounded_depth = NonZeroU8::new(depth).expect("depth ∈ 1 ..= SMT_DEPTH]"); + path_elems[depth as usize - 1] = full_tree_path.at_depth(bounded_depth)?; + } } else { // If there isn't a historical value, we should delegate to the corresponding // element in the path from the full-tree opening. diff --git a/miden-crypto/src/merkle/smt/large_forest/operation.rs b/miden-crypto/src/merkle/smt/large_forest/operation.rs index ead396f347..f739a9fdc3 100644 --- a/miden-crypto/src/merkle/smt/large_forest/operation.rs +++ b/miden-crypto/src/merkle/smt/large_forest/operation.rs @@ -103,7 +103,7 @@ impl SmtUpdateBatch { .rev() .filter(|o| seen_keys.insert(o.key())) .collect::>(); - ops.sort_by_key(|o| o.key()); + ops.sort_by_key(ForestOperation::key); ops } } @@ -254,7 +254,7 @@ mod test { // If we then consume the batch, we should have the operations ordered by their key. let ops = batch.consume(); - assert!(ops.is_sorted_by_key(|o| o.key())); + assert!(ops.is_sorted_by_key(ForestOperation::key)); // Let's now make two additional operations with keys that overlay with keys from the first // three... @@ -274,7 +274,7 @@ mod test { let ops = batch.consume(); assert_eq!(ops.len(), 3); - assert!(ops.is_sorted_by_key(|o| o.key())); + assert!(ops.is_sorted_by_key(ForestOperation::key)); assert!(ops.contains(&o3)); assert!(ops.contains(&o4)); @@ -307,11 +307,11 @@ mod test { assert_eq!(ops.len(), 2); let t1_ops = ops.get(&t1_lineage).unwrap(); - assert!(t1_ops.is_sorted_by_key(|o| o.key())); + assert!(t1_ops.is_sorted_by_key(ForestOperation::key)); assert_eq!(t1_ops.iter().unique_by(|o| o.key()).count(), 2); let t2_ops = ops.get(&t2_lineage).unwrap(); - assert!(t2_ops.is_sorted_by_key(|o| o.key())); + assert!(t2_ops.is_sorted_by_key(ForestOperation::key)); assert_eq!(t2_ops.iter().unique_by(|o| o.key()).count(), 2); } } diff --git a/miden-crypto/src/merkle/smt/large_forest/property_tests.rs b/miden-crypto/src/merkle/smt/large_forest/property_tests.rs index 418c4e498b..d2af7a5807 100644 --- a/miden-crypto/src/merkle/smt/large_forest/property_tests.rs +++ b/miden-crypto/src/merkle/smt/large_forest/property_tests.rs @@ -7,21 +7,299 @@ use itertools::Itertools; use proptest::prelude::*; use crate::{ - EMPTY_WORD, + EMPTY_WORD, Word, merkle::smt::{ - ForestInMemoryBackend, LargeSmtForest, Smt, TreeEntry, TreeId, + Backend, ForestConfig, ForestInMemoryBackend, ForestOperation, LargeSmtForest, + LargeSmtForestError, LineageId, RootInfo, Smt, SmtForestUpdateBatch, SmtUpdateBatch, + TreeId, large_forest::test_utils::{ - arbitrary_batch, arbitrary_lineage, arbitrary_version, to_fail, + apply_batch, arbitrary_batch, arbitrary_distinct_lineages, arbitrary_lineage, + arbitrary_non_empty_word, arbitrary_version, arbitrary_word, assert_lineage_metadata, + assert_tree_queries_match, batch_keys, build_tree, sorted_forest_entries, + sorted_tree_entries, to_fail, }, }, }; -// ENTRIES +// PROPERTY TESTS // ================================================================================================ proptest! { #![proptest_config(ProptestConfig::with_cases(10))] + /// This test validates constructor behavior when loading from a pre-populated backend. The + /// forest should load the latest tree state, but not reconstruct historical versions. + #[test] + fn new_loads_latest_backend_state_without_history( + (lineage_1, lineage_2) in arbitrary_distinct_lineages(), + version in arbitrary_version(), + entries_1 in arbitrary_batch(), + entries_2 in arbitrary_batch(), + updates_1 in arbitrary_batch(), + query_key in arbitrary_word(), + ) { + let mut backend = ForestInMemoryBackend::new(); + backend.add_lineage(lineage_1, version, entries_1.clone()).map_err(to_fail)?; + backend.add_lineage(lineage_2, version, entries_2.clone()).map_err(to_fail)?; + backend.update_tree(lineage_1, version + 1, updates_1.clone()).map_err(to_fail)?; + + let forest = LargeSmtForest::new(backend).map_err(to_fail)?; + + let tree_1_v1 = build_tree(entries_1.clone())?; + let mut expected_tree_1 = tree_1_v1.clone(); + apply_batch(&mut expected_tree_1, updates_1.clone())?; + let expected_tree_2 = build_tree(entries_2.clone())?; + let latest_version_1 = if expected_tree_1.root() == tree_1_v1.root() { + version + } else { + version + 1 + }; + + let mut sample_keys = batch_keys(&entries_1); + sample_keys.extend(batch_keys(&entries_2)); + sample_keys.extend(batch_keys(&updates_1)); + sample_keys.push(query_key); + sample_keys.sort(); + sample_keys.dedup(); + + assert_tree_queries_match( + &forest, + TreeId::new(lineage_1, latest_version_1), + &expected_tree_1, + &sample_keys, + true, + )?; + assert_tree_queries_match( + &forest, + TreeId::new(lineage_2, version), + &expected_tree_2, + &sample_keys, + true, + )?; + prop_assert_eq!(forest.lineage_count(), 2); + prop_assert_eq!(forest.tree_count(), 2); + prop_assert_eq!(forest.latest_version(lineage_1), Some(latest_version_1)); + prop_assert_eq!(forest.latest_root(lineage_1), Some(expected_tree_1.root())); + let expected_root_info = if latest_version_1 == version { + RootInfo::Missing + } else { + RootInfo::LatestVersion(expected_tree_1.root()) + }; + prop_assert_eq!( + forest.root_info(TreeId::new(lineage_1, version + 1)), + expected_root_info + ); + } + + /// This test validates history retention under custom configuration and the semantics of + /// explicit truncation. + #[test] + fn with_config_and_truncate_limit_retained_versions( + lineage in arbitrary_lineage(), + version in arbitrary_version(), + key_1 in arbitrary_word(), + key_2 in arbitrary_word(), + key_3 in arbitrary_word(), + key_4 in arbitrary_word(), + value_1 in arbitrary_non_empty_word(), + value_2 in arbitrary_non_empty_word(), + value_3 in arbitrary_non_empty_word(), + value_4 in arbitrary_non_empty_word(), + ) { + prop_assume!(key_1 != key_2 && key_1 != key_3 && key_1 != key_4); + prop_assume!(key_2 != key_3 && key_2 != key_4); + prop_assume!(key_3 != key_4); + + let config = ForestConfig::default().with_max_history_versions(2); + let mut forest = + LargeSmtForest::with_config(ForestInMemoryBackend::new(), config).map_err(to_fail)?; + forest + .add_lineage( + lineage, + version, + SmtUpdateBatch::new([ForestOperation::insert(key_1, value_1)].into_iter()), + ) + .map_err(to_fail)?; + forest + .update_tree( + lineage, + version + 1, + SmtUpdateBatch::new([ForestOperation::insert(key_2, value_2)].into_iter()), + ) + .map_err(to_fail)?; + forest + .update_tree( + lineage, + version + 2, + SmtUpdateBatch::new([ForestOperation::insert(key_3, value_3)].into_iter()), + ) + .map_err(to_fail)?; + forest + .update_tree( + lineage, + version + 3, + SmtUpdateBatch::new([ForestOperation::insert(key_4, value_4)].into_iter()), + ) + .map_err(to_fail)?; + + let mut tree_v1 = Smt::new(); + apply_batch( + &mut tree_v1, + SmtUpdateBatch::new([ForestOperation::insert(key_1, value_1)].into_iter()), + )?; + let mut tree_v2 = tree_v1.clone(); + apply_batch( + &mut tree_v2, + SmtUpdateBatch::new([ForestOperation::insert(key_2, value_2)].into_iter()), + )?; + let mut tree_v3 = tree_v2.clone(); + apply_batch( + &mut tree_v3, + SmtUpdateBatch::new([ForestOperation::insert(key_3, value_3)].into_iter()), + )?; + let mut tree_v4 = tree_v3.clone(); + apply_batch( + &mut tree_v4, + SmtUpdateBatch::new([ForestOperation::insert(key_4, value_4)].into_iter()), + )?; + + let sample_keys = vec![key_1, key_2, key_3, key_4]; + assert_tree_queries_match( + &forest, + TreeId::new(lineage, version + 2), + &tree_v3, + &sample_keys, + true, + )?; + assert_tree_queries_match( + &forest, + TreeId::new(lineage, version + 3), + &tree_v4, + &sample_keys, + true, + )?; + prop_assert_eq!(forest.latest_version(lineage), Some(version + 3)); + prop_assert_eq!(forest.latest_root(lineage), Some(tree_v4.root())); + prop_assert_eq!( + forest.root_info(TreeId::new(lineage, version + 3)), + RootInfo::LatestVersion(tree_v4.root()) + ); + prop_assert_eq!( + forest.root_info(TreeId::new(lineage, version + 2)), + RootInfo::HistoricalVersion(tree_v3.root()) + ); + prop_assert_eq!(forest.root_info(TreeId::new(lineage, version)), RootInfo::Missing); + + forest.truncate(version + 2); + assert_tree_queries_match( + &forest, + TreeId::new(lineage, version + 2), + &tree_v3, + &sample_keys, + true, + )?; + assert_tree_queries_match( + &forest, + TreeId::new(lineage, version + 3), + &tree_v4, + &sample_keys, + true, + )?; + prop_assert_eq!(forest.latest_version(lineage), Some(version + 3)); + prop_assert_eq!( + forest.root_info(TreeId::new(lineage, version + 3)), + RootInfo::LatestVersion(tree_v4.root()) + ); + prop_assert_eq!( + forest.root_info(TreeId::new(lineage, version + 2)), + RootInfo::HistoricalVersion(tree_v3.root()) + ); + prop_assert_eq!(forest.root_info(TreeId::new(lineage, version + 1)), RootInfo::Missing); + + forest.truncate(version + 3); + assert_tree_queries_match( + &forest, + TreeId::new(lineage, version + 3), + &tree_v4, + &sample_keys, + true, + )?; + prop_assert_eq!(forest.latest_version(lineage), Some(version + 3)); + prop_assert_eq!(forest.latest_root(lineage), Some(tree_v4.root())); + prop_assert_eq!( + forest.root_info(TreeId::new(lineage, version + 3)), + RootInfo::LatestVersion(tree_v4.root()) + ); + prop_assert_eq!(forest.root_info(TreeId::new(lineage, version + 2)), RootInfo::Missing); + } + + /// This test cross-checks the core query APIs (`get`, `open`, `entries`, `entry_count`) and the + /// associated metadata APIs against a reference SMT model across current and historical versions. + #[test] + fn queries_and_metadata_match_reference_model( + lineage in arbitrary_lineage(), + version in arbitrary_version(), + entries_v1 in arbitrary_batch(), + entries_v2 in arbitrary_batch(), + random_key in arbitrary_word(), + ) { + let mut forest = LargeSmtForest::new(ForestInMemoryBackend::new()).map_err(to_fail)?; + let add_result = + forest.add_lineage(lineage, version, entries_v1.clone()).map_err(to_fail)?; + let update_result = + forest.update_tree(lineage, version + 1, entries_v2.clone()).map_err(to_fail)?; + + let tree_v1 = build_tree(entries_v1.clone())?; + let mut tree_current = tree_v1.clone(); + apply_batch(&mut tree_current, entries_v2.clone())?; + + let mut sample_keys = batch_keys(&entries_v1); + sample_keys.extend(batch_keys(&entries_v2)); + sample_keys.push(random_key); + sample_keys.sort(); + sample_keys.dedup(); + + assert_tree_queries_match( + &forest, + TreeId::new(lineage, version), + &tree_v1, + &sample_keys, + true, + )?; + assert_tree_queries_match( + &forest, + TreeId::new(lineage, update_result.version()), + &tree_current, + &sample_keys, + true, + )?; + + let expected_versions = if tree_current.root() == tree_v1.root() { + vec![(version, tree_v1.root())] + } else { + vec![(version, add_result.root()), (version + 1, tree_current.root())] + }; + + assert_lineage_metadata(&forest, lineage, &expected_versions)?; + prop_assert_eq!(forest.lineage_count(), 1); + prop_assert_eq!(forest.tree_count(), expected_versions.len()); + prop_assert_eq!( + forest.roots().map(|root| (root.lineage(), root.value())).sorted().collect_vec(), + expected_versions.iter().map(|(_, root)| (lineage, *root)).sorted().collect_vec() + ); + + let unknown_lineage = LineageId::new([0xAA; 32]); + prop_assume!(unknown_lineage != lineage); + prop_assert_eq!(forest.latest_version(unknown_lineage), None); + prop_assert_eq!(forest.latest_root(unknown_lineage), None); + prop_assert!(forest.lineage_roots(unknown_lineage).is_none()); + prop_assert_eq!(forest.root_info(TreeId::new(lineage, version + 2)), RootInfo::Missing); + prop_assert_eq!(forest.root_info(TreeId::new(unknown_lineage, version)), RootInfo::Missing); + } + + // ENTRIES + // ============================================================================================ + /// This test ensures that the `entries` iterator for the forest always returns the exact same /// values as the `entries` iterator over a basic SMT with the same state. #[test] @@ -31,48 +309,26 @@ proptest! { entries_v1 in arbitrary_batch(), entries_v2 in arbitrary_batch(), ) { - // We now create a forest and add the lineage to it using the first set of entries. let mut forest = LargeSmtForest::new(ForestInMemoryBackend::new()).map_err(to_fail)?; forest.add_lineage(lineage, version, entries_v1.clone()).map_err(to_fail)?; let tree_info = forest.update_tree(lineage, version + 1, entries_v2.clone()).map_err(to_fail)?; - // We then create two auxiliary trees to work with, to compare our results against. - let mut tree_v1 = Smt::new(); - let tree_v1_mutations = - tree_v1.compute_mutations(Vec::from(entries_v1).into_iter()).map_err(to_fail)?; - tree_v1.apply_mutations(tree_v1_mutations).map_err(to_fail)?; - + let tree_v1 = build_tree(entries_v1)?; let mut tree_v2 = tree_v1.clone(); - let tree_v2_mutations = - tree_v2.compute_mutations(Vec::from(entries_v2).into_iter()).map_err(to_fail)?; - tree_v2.apply_mutations(tree_v2_mutations.clone()).map_err(to_fail)?; + apply_batch(&mut tree_v2, entries_v2)?; - // Iterating over the historical version of the lineage in the forest should produce exactly - // the same entries as iterating over V1 of our test tree. let old_version = TreeId::new(lineage, version); - let forest_entries = forest.entries(old_version).map_err(to_fail)?.sorted().collect_vec(); - let tree_entries = tree_v1 - .entries() - .map(|(k, v)| TreeEntry { key: *k, value: *v }) - .sorted() - .collect_vec(); - prop_assert_eq!(forest_entries, tree_entries); + prop_assert_eq!( + sorted_forest_entries(&forest, old_version)?, + sorted_tree_entries(&tree_v1) + ); - // Iterating over the newest version of the lineage in the forest should provide exactly the - // same entries as iterating over V2 of our test tree. - let current_version = if tree_v2_mutations.is_empty() { - TreeId::new(lineage, version) - } else { - TreeId::new(lineage, tree_info.version()) - }; - let forest_entries = forest.entries(current_version).map_err(to_fail)?.sorted().collect_vec(); - let tree_entries = tree_v2 - .entries() - .map(|(k, v)| TreeEntry { key: *k, value: *v }) - .sorted() - .collect_vec(); - prop_assert_eq!(forest_entries, tree_entries); + let current_version = TreeId::new(lineage, tree_info.version()); + prop_assert_eq!( + sorted_forest_entries(&forest, current_version)?, + sorted_tree_entries(&tree_v2) + ); } /// This test ensures that the `entries` iterator for the forest will never return entries where @@ -84,23 +340,290 @@ proptest! { entries_v1 in arbitrary_batch(), entries_v2 in arbitrary_batch(), ) { - // We now create a forest and add the lineage to it using the first set of entries. let mut forest = LargeSmtForest::new(ForestInMemoryBackend::new()).map_err(to_fail)?; - let root_1 = forest.add_lineage(lineage, version, entries_v1.clone()).map_err(to_fail)?; - let root_2 = forest.update_tree(lineage, version + 1, entries_v2.clone()).map_err(to_fail)?; + forest.add_lineage(lineage, version, entries_v1).map_err(to_fail)?; + let tree_info = forest.update_tree(lineage, version + 1, entries_v2).map_err(to_fail)?; - // Iterating over the historical version of the lineage in the forest should produce exactly - // the same entries as iterating over V1 of our test tree. let old_version = TreeId::new(lineage, version); - prop_assert!(forest.entries(old_version).map_err(to_fail)?.all(|e| e.value != EMPTY_WORD)); + let old_entries = forest + .entries(old_version) + .map_err(to_fail)? + .collect::>>() + .map_err(to_fail)?; + prop_assert!(old_entries.iter().all(|entry| entry.value != EMPTY_WORD)); + + let current_version = TreeId::new(lineage, tree_info.version()); + let current_entries = forest + .entries(current_version) + .map_err(to_fail)? + .collect::>>() + .map_err(to_fail)?; + prop_assert!(current_entries.iter().all(|entry| entry.value != EMPTY_WORD)); + } + + /// This test validates single-lineage mutation semantics, including duplicate additions, bad + /// version updates, and no-op updates preserving the observable forest state. + #[test] + fn add_lineage_and_update_tree_preserve_state_on_failures( + lineage in arbitrary_lineage(), + version in arbitrary_version(), + initial_entries in arbitrary_batch(), + extra_entries in arbitrary_batch(), + random_key in arbitrary_word(), + ) { + let mut forest = LargeSmtForest::new(ForestInMemoryBackend::new()).map_err(to_fail)?; + forest.add_lineage(lineage, version, initial_entries.clone()).map_err(to_fail)?; + let reference = build_tree(initial_entries.clone())?; + + let mut sample_keys = batch_keys(&initial_entries); + sample_keys.extend(batch_keys(&extra_entries)); + sample_keys.push(random_key); + sample_keys.sort(); + sample_keys.dedup(); + + let duplicate = forest.add_lineage(lineage, version + 1, extra_entries.clone()); + let is_duplicate = matches!( + duplicate, + Err(LargeSmtForestError::DuplicateLineage(l)) if l == lineage + ); + prop_assert!(is_duplicate); + assert_lineage_metadata(&forest, lineage, &[(version, reference.root())])?; + assert_tree_queries_match( + &forest, + TreeId::new(lineage, version), + &reference, + &sample_keys, + true, + )?; + prop_assert_eq!(forest.lineage_count(), 1); + prop_assert_eq!(forest.tree_count(), 1); + prop_assert_eq!(forest.root_info(TreeId::new(lineage, version + 1)), RootInfo::Missing); + prop_assert_eq!( + forest.roots().map(|root| (root.lineage(), root.value())).collect_vec(), + vec![(lineage, reference.root())] + ); + + let bad_version = forest.update_tree(lineage, version, extra_entries); + let is_bad_version = matches!( + bad_version, + Err(LargeSmtForestError::BadVersion { provided, latest }) if provided == version && latest == version + ); + prop_assert!(is_bad_version); + assert_lineage_metadata(&forest, lineage, &[(version, reference.root())])?; + assert_tree_queries_match( + &forest, + TreeId::new(lineage, version), + &reference, + &sample_keys, + true, + )?; + prop_assert_eq!(forest.root_info(TreeId::new(lineage, version + 1)), RootInfo::Missing); + prop_assert_eq!( + forest.roots().map(|root| (root.lineage(), root.value())).collect_vec(), + vec![(lineage, reference.root())] + ); + + let no_op = forest + .update_tree(lineage, version + 1, SmtUpdateBatch::empty()) + .map_err(to_fail)?; + prop_assert_eq!(no_op.version(), version); + prop_assert_eq!(no_op.root(), reference.root()); + assert_lineage_metadata(&forest, lineage, &[(version, reference.root())])?; + prop_assert_eq!(forest.tree_count(), 1); + prop_assert_eq!(forest.root_info(TreeId::new(lineage, version + 1)), RootInfo::Missing); + } + + /// This test validates batch updates across multiple lineages and ensures invalid batches do + /// not partially modify forest state. + #[test] + fn update_forest_matches_reference_model_and_preserves_state_on_error( + (lineage_1, lineage_2) in arbitrary_distinct_lineages(), + version in arbitrary_version(), + entries_1 in arbitrary_batch(), + entries_2 in arbitrary_batch(), + updates_1 in arbitrary_batch(), + updates_2 in arbitrary_batch(), + query_key in arbitrary_word(), + ) { + let mut forest = LargeSmtForest::new(ForestInMemoryBackend::new()).map_err(to_fail)?; + forest.add_lineage(lineage_1, version, entries_1.clone()).map_err(to_fail)?; + forest.add_lineage(lineage_2, version, entries_2.clone()).map_err(to_fail)?; + + let tree_1_v1 = build_tree(entries_1.clone())?; + let tree_2_v1 = build_tree(entries_2.clone())?; + + let mut expected_tree_1 = tree_1_v1.clone(); + let mut expected_tree_2 = tree_2_v1.clone(); + apply_batch(&mut expected_tree_1, updates_1.clone())?; + apply_batch(&mut expected_tree_2, updates_2.clone())?; - // Iterating over the newest version of the lineage in the forest should provide exactly the - // same entries as iterating over V2 of our test tree. - let current_version = if root_1 == root_2 { - TreeId::new(lineage, version) + let mut forest_updates = SmtForestUpdateBatch::empty(); + forest_updates.add_operations( + lineage_1, + updates_1.clone().consume().into_iter(), + ); + forest_updates.add_operations( + lineage_2, + updates_2.clone().consume().into_iter(), + ); + let results = forest.update_forest(version + 1, forest_updates).map_err(to_fail)?; + prop_assert_eq!(results.len(), 2); + + let mut sample_keys = batch_keys(&entries_1); + sample_keys.extend(batch_keys(&entries_2)); + sample_keys.extend(batch_keys(&updates_1)); + sample_keys.extend(batch_keys(&updates_2)); + sample_keys.push(query_key); + sample_keys.sort(); + sample_keys.dedup(); + + let versions_1 = if expected_tree_1.root() == tree_1_v1.root() { + vec![(version, tree_1_v1.root())] + } else { + vec![(version, tree_1_v1.root()), (version + 1, expected_tree_1.root())] + }; + let versions_2 = if expected_tree_2.root() == tree_2_v1.root() { + vec![(version, tree_2_v1.root())] } else { - TreeId::new(lineage, root_2.version()) + vec![(version, tree_2_v1.root()), (version + 1, expected_tree_2.root())] }; - prop_assert!(forest.entries(current_version).map_err(to_fail)?.all(|e| e.value != EMPTY_WORD)); + + assert_tree_queries_match( + &forest, + TreeId::new(lineage_1, versions_1.last().expect("non-empty").0), + &expected_tree_1, + &sample_keys, + true, + )?; + assert_tree_queries_match( + &forest, + TreeId::new(lineage_2, versions_2.last().expect("non-empty").0), + &expected_tree_2, + &sample_keys, + true, + )?; + assert_lineage_metadata(&forest, lineage_1, &versions_1)?; + assert_lineage_metadata(&forest, lineage_2, &versions_2)?; + + let roots = forest + .roots() + .map(|root| (root.lineage(), root.value())) + .sorted() + .collect_vec(); + let mut expected_roots = + versions_1.iter().map(|(_, root)| (lineage_1, *root)).collect_vec(); + expected_roots.extend(versions_2.iter().map(|(_, root)| (lineage_2, *root))); + expected_roots.sort(); + prop_assert_eq!(roots, expected_roots.clone()); + prop_assert_eq!(forest.lineage_count(), 2); + prop_assert_eq!(forest.tree_count(), versions_1.len() + versions_2.len()); + + let unknown_lineage = LineageId::new([0x55; 32]); + prop_assume!(unknown_lineage != lineage_1 && unknown_lineage != lineage_2); + let mut invalid_updates = SmtForestUpdateBatch::empty(); + let invalid_value = Word::from([1u32, 1, 1, 1]); + invalid_updates.add_operations( + lineage_1, + SmtUpdateBatch::new([ForestOperation::insert(query_key, invalid_value)].into_iter()) + .consume() + .into_iter(), + ); + invalid_updates + .operations(unknown_lineage) + .add_insert(query_key, invalid_value); + let invalid_result = forest.update_forest(version + 2, invalid_updates); + prop_assert!(invalid_result.is_err()); + + assert_tree_queries_match( + &forest, + TreeId::new(lineage_1, versions_1.last().expect("non-empty").0), + &expected_tree_1, + &sample_keys, + true, + )?; + assert_tree_queries_match( + &forest, + TreeId::new(lineage_2, versions_2.last().expect("non-empty").0), + &expected_tree_2, + &sample_keys, + true, + )?; + assert_lineage_metadata(&forest, lineage_1, &versions_1)?; + assert_lineage_metadata(&forest, lineage_2, &versions_2)?; + prop_assert_eq!(forest.lineage_count(), 2); + prop_assert_eq!(forest.tree_count(), versions_1.len() + versions_2.len()); + let roots_after_error = forest + .roots() + .map(|root| (root.lineage(), root.value())) + .sorted() + .collect_vec(); + prop_assert_eq!(roots_after_error, expected_roots.clone()); + prop_assert_eq!( + forest.root_info(TreeId::new(lineage_1, version + 2)), + RootInfo::Missing + ); + prop_assert_eq!( + forest.root_info(TreeId::new(lineage_2, version + 2)), + RootInfo::Missing + ); + } + + // ADD LINEAGES + // ============================================================================================ + + /// This test ensures that `add_lineages` produces the same results as adding each lineage + /// individually via `add_lineage`. + #[test] + fn add_lineages_matches_repeated_add_lineage( + lineages in prop::collection::vec(arbitrary_lineage(), 0..10) + .prop_map(|v| v.into_iter().unique().collect::>()), + version in arbitrary_version(), + entries in prop::collection::vec(arbitrary_batch(), 0..10), + ) { + // Build a forest update batch containing all lineages with their respective entries. + let mut batch = SmtForestUpdateBatch::empty(); + for (i, lineage) in lineages.iter().enumerate() { + if let Some(entry_batch) = entries.get(i) { + *batch.operations(*lineage) = entry_batch.clone(); + } else { + batch.operations(*lineage); + } + } + + // Add all lineages at once via add_lineages. + let mut forest_batch = LargeSmtForest::new(ForestInMemoryBackend::new()).map_err(to_fail)?; + let batch_results = forest_batch.add_lineages(version, batch).map_err(to_fail)?; + + // Add each lineage individually via add_lineage. + let mut forest_individual = LargeSmtForest::new(ForestInMemoryBackend::new()).map_err(to_fail)?; + let mut individual_results = Vec::new(); + for (i, lineage) in lineages.iter().enumerate() { + let entry_batch = entries.get(i).cloned().unwrap_or_default(); + let result = forest_individual.add_lineage(*lineage, version, entry_batch).map_err(to_fail)?; + individual_results.push(result); + } + + // Both should yield the same number of results. + prop_assert_eq!(batch_results.len(), individual_results.len()); + + // For each lineage, verify the roots match and get returns the same values. + for (i, lineage) in lineages.iter().enumerate() { + let batch_root = batch_results.iter().find(|r| r.lineage() == *lineage); + let individual_root = &individual_results[i]; + + let batch_root = batch_root.unwrap(); + prop_assert_eq!(batch_root.root(), individual_root.root()); + prop_assert_eq!(batch_root.version(), individual_root.version()); + + // Verify get returns the same values for all keys in the entries. + let tree = TreeId::new(*lineage, version); + if let Some(entry_batch) = entries.get(i) { + for op in entry_batch.clone().into_iter() { + let batch_val = forest_batch.get(tree, op.key()).map_err(to_fail)?; + let individual_val = forest_individual.get(tree, op.key()).map_err(to_fail)?; + prop_assert_eq!(batch_val, individual_val); + } + } + } } } diff --git a/miden-crypto/src/merkle/smt/large_forest/test_utils.rs b/miden-crypto/src/merkle/smt/large_forest/test_utils.rs index 0ddc79b3d3..ea6aac4f05 100644 --- a/miden-crypto/src/merkle/smt/large_forest/test_utils.rs +++ b/miden-crypto/src/merkle/smt/large_forest/test_utils.rs @@ -1,17 +1,27 @@ #![cfg(test)] //! This module contains utility functions for testing the large forest. +/// Placeholder entry count for tests that do not exercise or assert entry count behavior. +pub const UNUSED_ENTRY_COUNT: usize = 0; + use alloc::{string::ToString, vec::Vec}; use core::error::Error; +use itertools::Itertools; use miden_field::{Felt, Word}; use proptest::prelude::*; use crate::{ EMPTY_WORD, Map, ONE, ZERO, merkle::smt::{ - ForestOperation, LeafIndex, LineageId, MAX_LEAF_ENTRIES, SMT_DEPTH, SmtUpdateBatch, - VersionId, + Backend, ForestInMemoryBackend, ForestOperation, LargeSmtForest, LeafIndex, LineageId, + MAX_LEAF_ENTRIES, RootInfo, SMT_DEPTH, Smt, SmtForestUpdateBatch, SmtProof, SmtUpdateBatch, + TreeId, VersionId, + large_forest::{ + backend::{BackendError, Result as BackendResult}, + root::{TreeEntry, TreeWithRoot}, + utils::MutationSet, + }, }, }; @@ -24,6 +34,9 @@ const MIN_BATCH_ENTRIES: usize = 0; /// The maximum number of entries that can be included in a batch. const MAX_BATCH_ENTRIES: usize = 300; +/// The message used by [`FallibleEntriesBackend`] to simulate an iteration failure. +pub const FALLIBLE_READ_FAILURE_MESSAGE: &str = "simulated read failure"; + // UTILS // ================================================================================================ @@ -43,6 +56,12 @@ pub fn arbitrary_lineage() -> impl Strategy { prop::array::uniform32(any::()).prop_map(LineageId::new) } +/// Generates two distinct lineage identifiers. +pub fn arbitrary_distinct_lineages() -> impl Strategy { + (arbitrary_lineage(), arbitrary_lineage()) + .prop_filter("lineages must be distinct", |(a, b)| a != b) +} + /// Generates an arbitrary version identifier. pub fn arbitrary_version() -> impl Strategy { // As the proptests occasionally increment the version they are given, we exclude u64::MAX just @@ -52,7 +71,7 @@ pub fn arbitrary_version() -> impl Strategy { /// Generates an arbitrary valid felt value. pub fn arbitrary_felt() -> impl Strategy { - prop_oneof![any::().prop_map(Felt::new), Just(ZERO), Just(ONE)] + prop_oneof![any::().prop_map(Felt::new_unchecked), Just(ZERO), Just(ONE)] } /// Generates an arbitrary valid word value. @@ -60,6 +79,11 @@ pub fn arbitrary_word() -> impl Strategy { prop_oneof![prop::array::uniform4(arbitrary_felt()).prop_map(Word::new), Just(Word::empty()),] } +/// Generates a non-empty word value. +pub fn arbitrary_non_empty_word() -> impl Strategy { + arbitrary_word().prop_filter("word must be non-empty", |word| *word != EMPTY_WORD) +} + /// Generates a random number of unique (non-overlapping) key-value pairs. /// /// Note that the generated pairs may well have the same leaf index. @@ -104,3 +128,225 @@ pub fn arbitrary_batch() -> impl Strategy { })) }) } + +/// Builds a reference [`Smt`] by applying `initial` to an empty tree. +pub fn build_tree(initial: SmtUpdateBatch) -> Result { + let mut tree = Smt::new(); + apply_batch(&mut tree, initial)?; + Ok(tree) +} + +/// Applies a batch to the provided reference [`Smt`]. +pub fn apply_batch(tree: &mut Smt, batch: SmtUpdateBatch) -> Result<(), TestCaseError> { + let mutations = tree + .compute_mutations(batch.consume().into_iter().map(Into::<(Word, Word)>::into)) + .map_err(to_fail)?; + tree.apply_mutations(mutations).map_err(to_fail) +} + +/// Collects the keys affected by a batch using the batch's canonicalized ordering and deduping. +pub fn batch_keys(batch: &SmtUpdateBatch) -> Vec { + batch.clone().consume().into_iter().map(|operation| operation.key()).collect() +} + +/// Sorts tree entries explicitly by `(key, value)` so tests compare sets without constraining +/// backend iteration order. +pub fn sorted_tree_entries(tree: &Smt) -> Vec { + let mut entries = tree + .entries() + .map(|(key, value)| TreeEntry { key: *key, value: *value }) + .collect_vec(); + entries.sort_by_key(|entry| (entry.key, entry.value)); + entries +} + +/// Sorts forest entries explicitly by `(key, value)` so tests compare observable contents rather +/// than relying on unspecified iterator ordering. +pub fn sorted_forest_entries( + forest: &LargeSmtForest, + tree: TreeId, +) -> Result, TestCaseError> { + let mut entries = forest + .entries(tree) + .map_err(to_fail)? + .collect::>>() + .map_err(to_fail)?; + entries.sort_by_key(|entry| (entry.key, entry.value)); + Ok(entries) +} + +fn word_to_option(value: Word) -> Option { + if value == EMPTY_WORD { None } else { Some(value) } +} + +/// Asserts that the forest and reference tree agree on entries, counts, key lookups, and openings. +pub fn assert_tree_queries_match( + forest: &LargeSmtForest, + tree_id: TreeId, + reference: &Smt, + sample_keys: &[Word], + assert_openings: bool, +) -> Result<(), TestCaseError> { + let forest_entries = sorted_forest_entries(forest, tree_id)?; + let reference_entries = sorted_tree_entries(reference); + let reference_entry_count = reference_entries.len(); + prop_assert_eq!(forest_entries, reference_entries); + prop_assert_eq!(forest.entry_count(tree_id).map_err(to_fail)?, reference_entry_count); + + for key in sample_keys { + prop_assert_eq!( + forest.get(tree_id, *key).map_err(to_fail)?, + word_to_option(reference.get_value(key)) + ); + if assert_openings { + prop_assert_eq!(forest.open(tree_id, *key).map_err(to_fail)?, reference.open(key)); + } + } + + Ok(()) +} + +/// Asserts that the forest metadata for `lineage` matches the provided sequence of versions. +pub fn assert_lineage_metadata( + forest: &LargeSmtForest, + lineage: LineageId, + versions: &[(VersionId, Word)], +) -> Result<(), TestCaseError> { + let (latest_version, latest_root) = + versions.last().copied().expect("lineage must be non-empty"); + + prop_assert_eq!(forest.latest_version(lineage), Some(latest_version)); + prop_assert_eq!(forest.latest_root(lineage), Some(latest_root)); + prop_assert_eq!( + forest.lineage_roots(lineage).expect("lineage must be present").collect_vec(), + versions.iter().rev().map(|(_, root)| *root).collect_vec() + ); + + for (idx, (version, root)) in versions.iter().enumerate() { + let tree = TreeId::new(lineage, *version); + let expected = if idx + 1 == versions.len() { + RootInfo::LatestVersion(*root) + } else { + RootInfo::HistoricalVersion(*root) + }; + prop_assert_eq!(forest.root_info(tree), expected); + } + + Ok(()) +} + +// FALLIBLE ENTRIES BACKEND +// ================================================================================================ + +/// A wrapper around [`ForestInMemoryBackend`] that injects an error on the 3rd item yielded by +/// the `entries` iterator, to exercise the error-propagation path. +#[derive(Debug)] +pub struct FallibleEntriesBackend { + inner: ForestInMemoryBackend, +} + +impl FallibleEntriesBackend { + /// Constructs a new `FallibleEntriesBackend` wrapping a fresh [`ForestInMemoryBackend`]. + pub fn new() -> Self { + Self { inner: ForestInMemoryBackend::new() } + } +} + +/// An iterator that yields the first 2 items from the inner iterator as `Ok(...)`, then yields +/// a single `Err(BackendError::Unspecified(...))`, then yields `None` forever. +pub struct FallibleIter { + inner: I, + count: usize, +} + +impl>> Iterator for FallibleIter { + type Item = BackendResult; + + fn next(&mut self) -> Option { + if self.count >= 3 { + return None; + } + self.count += 1; + if self.count <= 2 { + self.inner.next() + } else { + Some(Err(BackendError::Unspecified(FALLIBLE_READ_FAILURE_MESSAGE.into()))) + } + } +} + +impl Backend for FallibleEntriesBackend { + fn open(&self, lineage: LineageId, key: Word) -> BackendResult { + self.inner.open(lineage, key) + } + + fn get_leaf( + &self, + lineage: LineageId, + leaf_index: LeafIndex, + ) -> BackendResult { + self.inner.get_leaf(lineage, leaf_index) + } + + fn get(&self, lineage: LineageId, key: Word) -> BackendResult> { + self.inner.get(lineage, key) + } + + fn version(&self, lineage: LineageId) -> BackendResult { + self.inner.version(lineage) + } + + fn lineages(&self) -> BackendResult> { + self.inner.lineages() + } + + fn trees(&self) -> BackendResult> { + self.inner.trees() + } + + fn entry_count(&self, lineage: LineageId) -> BackendResult { + self.inner.entry_count(lineage) + } + + fn entries( + &self, + lineage: LineageId, + ) -> BackendResult>> { + let inner_iter = self.inner.entries(lineage)?; + Ok(FallibleIter { inner: inner_iter, count: 0 }) + } + + fn add_lineage( + &mut self, + lineage: LineageId, + version: VersionId, + updates: SmtUpdateBatch, + ) -> BackendResult { + self.inner.add_lineage(lineage, version, updates) + } + + fn update_tree( + &mut self, + lineage: LineageId, + new_version: VersionId, + updates: SmtUpdateBatch, + ) -> BackendResult { + self.inner.update_tree(lineage, new_version, updates) + } + + fn add_lineages( + &mut self, + version: VersionId, + lineages: SmtForestUpdateBatch, + ) -> BackendResult> { + self.inner.add_lineages(version, lineages) + } + + fn update_forest( + &mut self, + new_version: VersionId, + updates: SmtForestUpdateBatch, + ) -> BackendResult> { + self.inner.update_forest(new_version, updates) + } +} diff --git a/miden-crypto/src/merkle/smt/large_forest/tests.rs b/miden-crypto/src/merkle/smt/large_forest/tests.rs index 5e428bab95..ea09019101 100644 --- a/miden-crypto/src/merkle/smt/large_forest/tests.rs +++ b/miden-crypto/src/merkle/smt/large_forest/tests.rs @@ -11,9 +11,8 @@ use alloc::vec::Vec; use assert_matches::assert_matches; -use itertools::Itertools; -use super::{Config, Result}; +use super::{Config, Result, test_utils::UNUSED_ENTRY_COUNT}; use crate::{ EMPTY_WORD, Map, Set, Word, merkle::{ @@ -25,6 +24,7 @@ use crate::{ LineageData, history::{ChangedKeys, History, NodeChanges}, root::{LineageId, TreeEntry, TreeWithRoot}, + test_utils::{FALLIBLE_READ_FAILURE_MESSAGE, FallibleEntriesBackend}, }, }, }, @@ -630,6 +630,118 @@ fn entry_count() -> Result<()> { Ok(()) } +#[test] +fn entry_count_historical_across_versions() -> Result<()> { + let backend = ForestInMemoryBackend::new(); + let mut forest = Forest::new(backend)?; + let mut rng = ContinuousRng::new([0x23; 32]); + + let lineage: LineageId = rng.value(); + let version_1: VersionId = rng.value(); + + // Version 1: Insert 2 entries. + let key_1: Word = rng.value(); + let value_1: Word = rng.value(); + let key_2: Word = rng.value(); + let value_2: Word = rng.value(); + + let mut ops = SmtUpdateBatch::empty(); + ops.add_insert(key_1, value_1); + ops.add_insert(key_2, value_2); + forest.add_lineage(lineage, version_1, ops)?; + + // Version 2: Insert 1 more entry (total 3). + let version_2 = version_1 + 1; + let key_3: Word = rng.value(); + let value_3: Word = rng.value(); + + let mut ops = SmtUpdateBatch::empty(); + ops.add_insert(key_3, value_3); + forest.update_tree(lineage, version_2, ops)?; + + // Version 3: Remove 1 entry (total 2). + let version_3 = version_2 + 1; + let mut ops = SmtUpdateBatch::empty(); + ops.add_remove(key_1); + forest.update_tree(lineage, version_3, ops)?; + + // Verify entry counts for all versions. + assert_eq!(forest.entry_count(TreeId::new(lineage, version_1))?, 2); + assert_eq!(forest.entry_count(TreeId::new(lineage, version_2))?, 3); + assert_eq!(forest.entry_count(TreeId::new(lineage, version_3))?, 2); + + Ok(()) +} + +#[test] +fn entry_count_historical_across_versions_via_update_forest() -> Result<()> { + let backend = ForestInMemoryBackend::new(); + let mut forest = Forest::new(backend)?; + let mut rng = ContinuousRng::new([0x24; 32]); + + // Set up two lineages so we exercise the update_forest path (which updates multiple lineages + // in a single batch). + let lineage_a: LineageId = rng.value(); + let lineage_b: LineageId = rng.value(); + let version_1: VersionId = rng.value(); + + // Version 1: lineage_a gets 2 entries, lineage_b gets 1 entry. + let a_key_1: Word = rng.value(); + let a_value_1: Word = rng.value(); + let a_key_2: Word = rng.value(); + let a_value_2: Word = rng.value(); + let b_key_1: Word = rng.value(); + let b_value_1: Word = rng.value(); + + let mut ops_a = SmtUpdateBatch::empty(); + ops_a.add_insert(a_key_1, a_value_1); + ops_a.add_insert(a_key_2, a_value_2); + forest.add_lineage(lineage_a, version_1, ops_a)?; + + let mut ops_b = SmtUpdateBatch::empty(); + ops_b.add_insert(b_key_1, b_value_1); + forest.add_lineage(lineage_b, version_1, ops_b)?; + + // Version 2 via update_forest: add 1 entry to lineage_a (total 3), add 2 entries to + // lineage_b (total 3). + let version_2 = version_1 + 1; + let a_key_3: Word = rng.value(); + let a_value_3: Word = rng.value(); + let b_key_2: Word = rng.value(); + let b_value_2: Word = rng.value(); + let b_key_3: Word = rng.value(); + let b_value_3: Word = rng.value(); + + let mut batch = SmtForestUpdateBatch::empty(); + batch.operations(lineage_a).add_insert(a_key_3, a_value_3); + batch.operations(lineage_b).add_insert(b_key_2, b_value_2); + batch.operations(lineage_b).add_insert(b_key_3, b_value_3); + forest.update_forest(version_2, batch)?; + + // Version 3 via update_forest: remove 1 entry from lineage_a (total 2), add 1 entry to + // lineage_b (total 4). + let version_3 = version_2 + 1; + let b_key_4: Word = rng.value(); + let b_value_4: Word = rng.value(); + + let mut batch = SmtForestUpdateBatch::empty(); + batch.operations(lineage_a).add_remove(a_key_1); + batch.operations(lineage_b).add_insert(b_key_4, b_value_4); + forest.update_forest(version_3, batch)?; + + // Verify historical entry counts for lineage_a. + assert_eq!(forest.entry_count(TreeId::new(lineage_a, version_1))?, 2); + assert_eq!(forest.entry_count(TreeId::new(lineage_a, version_2))?, 3); + assert_eq!(forest.entry_count(TreeId::new(lineage_a, version_3))?, 2); + + // Verify historical entry counts for lineage_b. + assert_eq!(forest.entry_count(TreeId::new(lineage_b, version_1))?, 1); + assert_eq!(forest.entry_count(TreeId::new(lineage_b, version_2))?, 3); + assert_eq!(forest.entry_count(TreeId::new(lineage_b, version_3))?, 4); + + Ok(()) +} + #[test] fn entries() -> Result<()> { let backend = ForestInMemoryBackend::new(); @@ -694,51 +806,21 @@ fn entries() -> Result<()> { // Grabbing the entries for the latest version in a lineage should do the right thing. let current_tree = TreeId::new(lineage_1, version_2); - assert_eq!(forest.entries(current_tree)?.count(), 3); - assert!( - forest - .entries(current_tree)? - .contains(&TreeEntry { key: key_1, value: value_1_v2 }) - ); - assert!( - forest - .entries(current_tree)? - .contains(&TreeEntry { key: key_2, value: value_2_v1 }) - ); - assert!( - forest - .entries(current_tree)? - .contains(&TreeEntry { key: key_4, value: value_4_v1 }) - ); - assert!( - !forest - .entries(current_tree)? - .contains(&TreeEntry { key: key_3, value: value_3_v1 }) - ); + let current_entries = forest.entries(current_tree)?.collect::>>()?; + assert_eq!(current_entries.len(), 3); + assert!(current_entries.contains(&TreeEntry { key: key_1, value: value_1_v2 })); + assert!(current_entries.contains(&TreeEntry { key: key_2, value: value_2_v1 })); + assert!(current_entries.contains(&TreeEntry { key: key_4, value: value_4_v1 })); + assert!(!current_entries.contains(&TreeEntry { key: key_3, value: value_3_v1 })); // If we ask for a historical version, things are more complex but should still work. let historical_tree = TreeId::new(lineage_1, version_1); - assert_eq!(forest.entries(historical_tree)?.count(), 3); - assert!( - forest - .entries(historical_tree)? - .contains(&TreeEntry { key: key_1, value: value_1_v1 }) - ); - assert!( - forest - .entries(historical_tree)? - .contains(&TreeEntry { key: key_2, value: value_2_v1 }) - ); - assert!( - forest - .entries(historical_tree)? - .contains(&TreeEntry { key: key_3, value: value_3_v1 }) - ); - assert!( - !forest - .entries(historical_tree)? - .contains(&TreeEntry { key: key_4, value: value_4_v1 }) - ); + let historical_entries = forest.entries(historical_tree)?.collect::>>()?; + assert_eq!(historical_entries.len(), 3); + assert!(historical_entries.contains(&TreeEntry { key: key_1, value: value_1_v1 })); + assert!(historical_entries.contains(&TreeEntry { key: key_2, value: value_2_v1 })); + assert!(historical_entries.contains(&TreeEntry { key: key_3, value: value_3_v1 })); + assert!(!historical_entries.contains(&TreeEntry { key: key_4, value: value_4_v1 })); Ok(()) } @@ -795,8 +877,8 @@ fn forest_overlays_correctly() -> Result<()> { assert!(forest.get(current_tree, key_1)?.is_none()); // We can also get an iterator over all the entries in the tree. - let entries_old: Vec<_> = forest.entries(old_tree)?.collect(); - let entries_current: Vec<_> = forest.entries(current_tree)?.collect(); + let entries_old = forest.entries(old_tree)?.collect::>>()?; + let entries_current = forest.entries(current_tree)?.collect::>>()?; assert!(entries_old.contains(&TreeEntry { key: key_1, value: value_1 })); assert!(entries_old.contains(&TreeEntry { key: key_2, value: value_2 })); assert!(!entries_old.contains(&TreeEntry { key: key_3, value: value_3 })); @@ -863,8 +945,9 @@ fn entries_never_returns_empty_entry() -> Result<()> { // Now, when we query for entries on the historical version, we should only see one entry, and // no entries should be the empty word. let historical_tree = TreeId::new(lineage_2, version_1); - assert_eq!(forest.entries(historical_tree)?.count(), 1); - assert!(forest.entries(historical_tree)?.all(|e| e.value != EMPTY_WORD)); + let entries = forest.entries(historical_tree)?.collect::>>()?; + assert_eq!(entries.len(), 1); + assert!(entries.iter().all(|e| e.value != EMPTY_WORD)); // The third scenario is where entries are added within a shared leaf, where we should only see // the historical leaf entries and not their reversions. @@ -888,8 +971,9 @@ fn entries_never_returns_empty_entry() -> Result<()> { // Now when we query the historical version, we should only see one entry, and no reversions. let historical_tree = TreeId::new(lineage_3, version_1); - assert_eq!(forest.entries(historical_tree)?.count(), 1); - assert!(forest.entries(historical_tree)?.all(|e| e.value != EMPTY_WORD)); + let entries = forest.entries(historical_tree)?.collect::>>()?; + assert_eq!(entries.len(), 1); + assert!(entries.iter().all(|e| e.value != EMPTY_WORD)); Ok(()) } @@ -938,7 +1022,7 @@ fn entries_history_empty_values_do_not_reorder() -> Result<()> { )?; let historical_tree = TreeId::new(lineage, version_1); - let entries: Vec<_> = forest.entries(historical_tree)?.collect(); + let entries = forest.entries(historical_tree)?.collect::>>()?; assert_eq!(entries.len(), 2); assert_eq!(entries[0], TreeEntry { key: key_a, value: value_a }); assert_eq!(entries[1], TreeEntry { key: key_c, value: value_c_v1 }); @@ -1079,6 +1163,97 @@ fn update_tree() -> Result<()> { // MULTI-TREE MODIFIER TESTS // ================================================================================================ +#[test] +fn add_lineages() -> Result<()> { + let backend = ForestInMemoryBackend::new(); + let mut forest = Forest::new(backend)?; + let mut rng = ContinuousRng::new([0xa1; 32]); + + // An empty batch should return an empty result and leave the forest unchanged. + let version: VersionId = rng.value(); + let empty_batch = SmtForestUpdateBatch::empty(); + let results = forest.add_lineages(version, empty_batch)?; + assert!(results.is_empty()); + + // We can add multiple distinct lineages at once, each with their own data. + let lineage_1: LineageId = rng.value(); + let lineage_2: LineageId = rng.value(); + let lineage_3: LineageId = rng.value(); + + let l1_key: Word = rng.value(); + let l1_value: Word = rng.value(); + let l2_key: Word = rng.value(); + let l2_value: Word = rng.value(); + + let mut batch = SmtForestUpdateBatch::empty(); + batch.operations(lineage_1).add_insert(l1_key, l1_value); + batch.operations(lineage_2).add_insert(l2_key, l2_value); + batch.operations(lineage_3); // empty lineage — should still be added + + let results = forest.add_lineages(version, batch)?; + assert_eq!(results.len(), 3); + + // Verify roots match reference Smt trees. + let mut tree_1 = Smt::new(); + tree_1.insert(l1_key, l1_value)?; + let mut tree_2 = Smt::new(); + tree_2.insert(l2_key, l2_value)?; + let tree_3 = Smt::new(); + + assert!( + results.iter().any(|r| r.lineage() == lineage_1 + && r.root() == tree_1.root() + && r.version() == version) + ); + assert!( + results.iter().any(|r| r.lineage() == lineage_2 + && r.root() == tree_2.root() + && r.version() == version) + ); + assert!( + results.iter().any(|r| r.lineage() == lineage_3 + && r.root() == tree_3.root() + && r.version() == version) + ); + + // Verify lineage_data is populated via root_info. + assert_eq!( + forest.root_info(TreeId::new(lineage_1, version)), + RootInfo::LatestVersion(tree_1.root()) + ); + assert_eq!( + forest.root_info(TreeId::new(lineage_2, version)), + RootInfo::LatestVersion(tree_2.root()) + ); + assert_eq!( + forest.root_info(TreeId::new(lineage_3, version)), + RootInfo::LatestVersion(tree_3.root()) + ); + + // New lineages should have empty histories. + assert!(!forest.get_non_empty_histories().contains(&lineage_1)); + assert!(!forest.get_non_empty_histories().contains(&lineage_2)); + assert!(!forest.get_non_empty_histories().contains(&lineage_3)); + + // Adding a batch that contains an already-known lineage should fail with DuplicateLineage. + let lineage_4: LineageId = rng.value(); + let mut dup_batch = SmtForestUpdateBatch::empty(); + dup_batch.operations(lineage_1); // already exists + dup_batch.operations(lineage_4); // new + + let result = forest.add_lineages(version, dup_batch); + assert!(result.is_err()); + assert_matches!( + result.unwrap_err(), + LargeSmtForestError::DuplicateLineage(l) if l == lineage_1 + ); + + // The failed batch should not have added lineage_4. + assert_eq!(forest.root_info(TreeId::new(lineage_4, version)), RootInfo::Missing); + + Ok(()) +} + #[test] fn update_forest() -> Result<()> { let backend = ForestInMemoryBackend::new(); @@ -1248,7 +1423,9 @@ fn truncate_removes_emptied_lineages_from_non_empty_histories() { let mut history = History::empty(4); let nodes = NodeChanges::default(); let changed_keys = ChangedKeys::default(); - history.add_version(rand_value(), 5, nodes, changed_keys).unwrap(); + history + .add_version(rand_value(), 5, nodes, changed_keys, UNUSED_ENTRY_COUNT) + .unwrap(); assert_eq!(history.num_versions(), 1); let lineage_data = LineageData { @@ -1293,10 +1470,10 @@ fn truncate_retains_non_empty_lineages_in_non_empty_histories() { let nodes = NodeChanges::default(); let changed_keys = ChangedKeys::default(); history - .add_version(rand_value(), 5, nodes.clone(), changed_keys.clone()) + .add_version(rand_value(), 5, nodes.clone(), changed_keys.clone(), UNUSED_ENTRY_COUNT) .unwrap(); history - .add_version(rand_value(), 8, nodes.clone(), changed_keys.clone()) + .add_version(rand_value(), 8, nodes, changed_keys, UNUSED_ENTRY_COUNT) .unwrap(); assert_eq!(history.num_versions(), 2); @@ -1329,3 +1506,108 @@ fn truncate_retains_non_empty_lineages_in_non_empty_histories() { "lineage with remaining history must stay in non_empty_histories" ); } + +// ENTRIES UNHAPPY PATH TESTS +// ================================================================================================ + +#[test] +fn entries_with_fallible_backend() -> Result<()> { + let backend = FallibleEntriesBackend::new(); + let mut forest = LargeSmtForest::new(backend)?; + let mut rng = ContinuousRng::new([0xfa; 32]); + + // Add a lineage with more than 3 entries so we can verify that entries beyond the failure + // point are never returned. + let lineage: LineageId = rng.value(); + let version: VersionId = rng.value(); + let key_1: Word = rng.value(); + let value_1: Word = rng.value(); + let key_2: Word = rng.value(); + let value_2: Word = rng.value(); + let key_3: Word = rng.value(); + let value_3: Word = rng.value(); + let key_4: Word = rng.value(); + let value_4: Word = rng.value(); + let key_5: Word = rng.value(); + let value_5: Word = rng.value(); + + let mut operations = SmtUpdateBatch::empty(); + operations.add_insert(key_1, value_1); + operations.add_insert(key_2, value_2); + operations.add_insert(key_3, value_3); + operations.add_insert(key_4, value_4); + operations.add_insert(key_5, value_5); + + forest.add_lineage(lineage, version, operations)?; + + // Query entries on the current version (WithoutHistory path). + let tree_id = TreeId::new(lineage, version); + let mut iter = forest.entries(tree_id)?; + + // First two items should be Ok. + let first = iter.next(); + assert!(matches!(first, Some(Ok(_))), "expected first item to be Some(Ok(...))"); + let second = iter.next(); + assert!(matches!(second, Some(Ok(_))), "expected second item to be Some(Ok(...))"); + + // Third item should be the simulated error. + let third = iter.next(); + assert_matches!( + &third, + Some(Err(LargeSmtForestError::Unspecified(message))) + if message == FALLIBLE_READ_FAILURE_MESSAGE + ); + + // After faulting, the iterator must yield None — the remaining entries (4th, 5th) are never + // returned. + assert!(iter.next().is_none(), "expected None after error"); + assert!(iter.next().is_none(), "expected iterator to remain exhausted"); + + Ok(()) +} + +#[test] +fn entry_count_historical_bypasses_fallible_entries_iterator() -> Result<()> { + let backend = FallibleEntriesBackend::new(); + let mut forest = LargeSmtForest::new(backend)?; + let mut rng = ContinuousRng::new([0xfb; 32]); + + // Add a lineage with 5 entries at version V1. + let lineage: LineageId = rng.value(); + let version_1: VersionId = rng.value(); + let key_1: Word = rng.value(); + let value_1: Word = rng.value(); + let key_2: Word = rng.value(); + let value_2: Word = rng.value(); + let key_3: Word = rng.value(); + let value_3: Word = rng.value(); + let key_4: Word = rng.value(); + let value_4: Word = rng.value(); + let key_5: Word = rng.value(); + let value_5: Word = rng.value(); + + let mut operations = SmtUpdateBatch::empty(); + operations.add_insert(key_1, value_1); + operations.add_insert(key_2, value_2); + operations.add_insert(key_3, value_3); + operations.add_insert(key_4, value_4); + operations.add_insert(key_5, value_5); + + forest.add_lineage(lineage, version_1, operations)?; + + // Update the tree at V2 so V1 becomes historical. + let version_2: VersionId = version_1 + 1; + let key_6: Word = rng.value(); + let value_6: Word = rng.value(); + let mut operations = SmtUpdateBatch::empty(); + operations.add_insert(key_6, value_6); + forest.update_tree(lineage, version_2, operations)?; + + // Query entry_count for the historical version V1. + // With the stored entry count optimization, this no longer iterates through entries, + // so it succeeds even with a fallible backend. + let result = forest.entry_count(TreeId::new(lineage, version_1)); + assert_eq!(result?, 5); + + Ok(()) +} diff --git a/miden-crypto/src/merkle/smt/mod.rs b/miden-crypto/src/merkle/smt/mod.rs index d532c4c9eb..ac0ae128e6 100644 --- a/miden-crypto/src/merkle/smt/mod.rs +++ b/miden-crypto/src/merkle/smt/mod.rs @@ -8,7 +8,7 @@ use core::{ use super::{EmptySubtreeRoots, InnerNodeInfo, MerkleError, NodeIndex, SparseMerklePath}; use crate::{ - EMPTY_WORD, Map, Word, + EMPTY_WORD, Map, Set, Word, hash::poseidon2::Poseidon2, utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}, }; @@ -204,9 +204,9 @@ pub(crate) trait SparseMerkleTree { /// the Merkle tree, or [`drop()`] to discard them. /// /// # Errors - /// If mutations would exceed [`crate::merkle::smt::MAX_LEAF_ENTRIES`] (1024 entries) in a leaf, - /// returns - /// [`MerkleError::TooManyLeafEntries`]. + /// - Returns [`MerkleError::DuplicateValuesForIndex`] if `kv_pairs` contains duplicate keys. + /// - If mutations would exceed [`crate::merkle::smt::MAX_LEAF_ENTRIES`] (1024 entries) in a + /// leaf, returns [`MerkleError::TooManyLeafEntries`]. fn compute_mutations( &self, kv_pairs: impl IntoIterator, @@ -216,6 +216,13 @@ pub(crate) trait SparseMerkleTree { /// Sequential version of [`SparseMerkleTree::compute_mutations()`]. /// This is the default implementation. + /// + /// # Errors + /// + /// - [`MerkleError::DuplicateValuesForIndex`] if `kv_pairs` contains multiple pairs with the + /// same key. + /// - [`MerkleError::TooManyLeafEntries`] if the mutations would cause the tree to exceed + /// [`crate::merkle::smt::MAX_LEAF_ENTRIES`] in a single leaf. fn compute_mutations_sequential( &self, kv_pairs: impl IntoIterator, @@ -224,12 +231,18 @@ pub(crate) trait SparseMerkleTree { let mut new_root = self.root(); let mut new_pairs: Map = Default::default(); - let mut node_mutations: NodeMutations = Default::default(); + let mut node_mutations: NodeMutations = NodeMutations::new(); + let mut seen_keys: Set = Set::new(); for (key, value) in kv_pairs { + // Reject duplicate keys. + if !seen_keys.insert(key.clone()) { + return Err(MerkleError::DuplicateValuesForIndex( + Self::key_to_leaf_index(&key).position(), + )); + } + // If the old value and the new value are the same, there is nothing to update. - // For the unusual case that kv_pairs has multiple values at the same key, we'll have - // to check the key-value pairs we've already seen to get the "effective" old value. let old_value = new_pairs.get(&key).cloned().unwrap_or_else(|| self.get_value(&key)); if value == old_value { continue; @@ -249,7 +262,7 @@ pub(crate) trait SparseMerkleTree { pairs_at_index.fold(self.get_leaf(&key), |acc, (k, v)| { // Most of the time `pairs_at_index` should only contain a single entry (or // none at all), as multi-leaves should be really rare. - let existing_leaf = acc.clone(); + let existing_leaf = acc; self.construct_prospective_leaf(existing_leaf, k, v) .expect("current leaf should be valid") }) @@ -512,6 +525,27 @@ pub(crate) trait SparseMerkleTree { /// Maps a key to a leaf index fn key_to_leaf_index(key: &Self::Key) -> LeafIndex; + /// Checks a slice of key-value pairs (assumed sorted by key) for duplicate keys. + /// + /// The input `sorted_kv_pairs` must be sorted for this function to return correct results, as + /// it only performs checks of adjacent elements. + /// + /// # Errors + /// + /// - [`MerkleError::DuplicateValuesForIndex`] at the first duplicate key found in + /// `sorted_kv_pairs`. + #[cfg(feature = "concurrent")] // Currently only used in concurrent contexts + fn check_for_duplicate_keys( + sorted_kv_pairs: &[(Self::Key, Self::Value)], + ) -> Result<(), MerkleError> { + if let Some(window) = sorted_kv_pairs.windows(2).find(|w| w[0].0 == w[1].0) { + return Err(MerkleError::DuplicateValuesForIndex( + Self::key_to_leaf_index(&window[0].0).position(), + )); + } + Ok(()) + } + /// Maps a (SparseMerklePath, Self::Leaf) to an opening. /// /// The length `path` is guaranteed to be equal to `DEPTH` diff --git a/miden-crypto/src/merkle/smt/partial/mod.rs b/miden-crypto/src/merkle/smt/partial/mod.rs index 9a1dcb4f9d..e34b8e6ded 100644 --- a/miden-crypto/src/merkle/smt/partial/mod.rs +++ b/miden-crypto/src/merkle/smt/partial/mod.rs @@ -306,7 +306,7 @@ impl PartialSmt { let prev_entries = self .leaves .get(¤t_index.position()) - .map(|leaf| leaf.num_entries()) + .map(SmtLeaf::num_entries) .unwrap_or(0); let current_entries = leaf.num_entries(); // Only store non-empty leaves @@ -571,7 +571,7 @@ impl Deserializable for PartialSmt { inner_nodes.insert(idx, node); } - let num_entries = leaves.values().map(|leaf| leaf.num_entries()).sum(); + let num_entries = leaves.values().map(SmtLeaf::num_entries).sum(); let partial = Self { root, num_entries, leaves, inner_nodes }; partial.validate()?; diff --git a/miden-crypto/src/merkle/smt/partial/tests.rs b/miden-crypto/src/merkle/smt/partial/tests.rs index 84ee0d9ec5..cfecc5a628 100644 --- a/miden-crypto/src/merkle/smt/partial/tests.rs +++ b/miden-crypto/src/merkle/smt/partial/tests.rs @@ -22,10 +22,10 @@ use crate::{ /// This is used for deterministic tests that need reproducible sequences of random values. fn random_word(rng: &mut R) -> Word { Word::new([ - Felt::new(rng.random::() % Felt::ORDER), - Felt::new(rng.random::() % Felt::ORDER), - Felt::new(rng.random::() % Felt::ORDER), - Felt::new(rng.random::() % Felt::ORDER), + Felt::new_unchecked(rng.random::() % Felt::ORDER), + Felt::new_unchecked(rng.random::() % Felt::ORDER), + Felt::new_unchecked(rng.random::() % Felt::ORDER), + Felt::new_unchecked(rng.random::() % Felt::ORDER), ]) } @@ -429,7 +429,7 @@ fn partial_smt_add_proof_num_entries() { // key0 and key1 have the same felt at index 3 so they will be placed in the same leaf. let key0 = Word::from([ZERO, ZERO, ZERO, ONE]); let key1 = Word::from([ONE, ONE, ONE, ONE]); - let key2 = Word::from([ONE, ONE, ONE, Felt::new(5)]); + let key2 = Word::from([ONE, ONE, ONE, Felt::new_unchecked(5)]); let value0 = random_word(&mut rng); let value1 = random_word(&mut rng); let value2 = random_word(&mut rng); @@ -485,14 +485,14 @@ fn partial_smt_tracking_visualization() { const LEAF_6: u64 = (1 << 63) | (1 << 62); const LEAF_7: u64 = (1 << 63) | (1 << 62) | (1 << 61); - let key_0 = Word::from([ZERO, ZERO, ZERO, Felt::new(LEAF_0)]); - let key_1 = Word::from([ZERO, ZERO, ZERO, Felt::new(LEAF_1)]); - let key_2 = Word::from([ZERO, ZERO, ZERO, Felt::new(LEAF_2)]); - let key_3 = Word::from([ZERO, ZERO, ZERO, Felt::new(LEAF_3)]); - let key_4 = Word::from([ZERO, ZERO, ZERO, Felt::new(LEAF_4)]); - let key_5 = Word::from([ZERO, ZERO, ZERO, Felt::new(LEAF_5)]); - let key_6 = Word::from([ZERO, ZERO, ZERO, Felt::new(LEAF_6)]); - let key_7 = Word::from([ZERO, ZERO, ZERO, Felt::new(LEAF_7)]); + let key_0 = Word::from([ZERO, ZERO, ZERO, Felt::new_unchecked(LEAF_0)]); + let key_1 = Word::from([ZERO, ZERO, ZERO, Felt::new_unchecked(LEAF_1)]); + let key_2 = Word::from([ZERO, ZERO, ZERO, Felt::new_unchecked(LEAF_2)]); + let key_3 = Word::from([ZERO, ZERO, ZERO, Felt::new_unchecked(LEAF_3)]); + let key_4 = Word::from([ZERO, ZERO, ZERO, Felt::new_unchecked(LEAF_4)]); + let key_5 = Word::from([ZERO, ZERO, ZERO, Felt::new_unchecked(LEAF_5)]); + let key_6 = Word::from([ZERO, ZERO, ZERO, Felt::new_unchecked(LEAF_6)]); + let key_7 = Word::from([ZERO, ZERO, ZERO, Felt::new_unchecked(LEAF_7)]); let mut rng = ChaCha20Rng::from_seed([12u8; 32]); @@ -643,7 +643,7 @@ fn partial_smt_deserialize_invalid_leaf() { // Tamper with leaf value data (after position). // Byte position to flip. let leaf_value_offset = 32 + 8 + 8 + 10; - let mut tampered_bytes = bytes.clone(); + let mut tampered_bytes = bytes; // Flip a byte in the leaf value data to corrupt it. tampered_bytes[leaf_value_offset] ^= 0xff; diff --git a/miden-crypto/src/merkle/smt/simple/mod.rs b/miden-crypto/src/merkle/smt/simple/mod.rs index be9c8833ab..c4dd3c39c4 100644 --- a/miden-crypto/src/merkle/smt/simple/mod.rs +++ b/miden-crypto/src/merkle/smt/simple/mod.rs @@ -231,13 +231,18 @@ impl SimpleSmt { /// [`SimpleSmt::apply_mutations()`] can be called in order to commit these changes to the /// Merkle tree, or [`drop()`] to discard them. /// + /// # Errors + /// + /// - [`MerkleError::DuplicateValuesForIndex`] if the provided `kv_pairs` contain duplicate + /// keys. + /// /// # Example /// ``` /// # use miden_crypto::{Felt, Word}; /// # use miden_crypto::merkle::{smt::{LeafIndex, SimpleSmt, SMT_DEPTH}, EmptySubtreeRoots}; /// let mut smt: SimpleSmt<3> = SimpleSmt::new().unwrap(); /// let pair = (LeafIndex::default(), Word::default()); - /// let mutations = smt.compute_mutations(vec![pair]); + /// let mutations = smt.compute_mutations(vec![pair]).unwrap(); /// assert_eq!(mutations.root(), *EmptySubtreeRoots::entry(3, 0)); /// smt.apply_mutations(mutations).unwrap(); /// assert_eq!(smt.root(), *EmptySubtreeRoots::entry(3, 0)); @@ -245,12 +250,8 @@ impl SimpleSmt { pub fn compute_mutations( &self, kv_pairs: impl IntoIterator, Word)>, - ) -> MutationSet, Word> { - // SAFETY: a SimpleSmt does not contain multi-value leaves. The underlying - // SimpleSmt::construct_prospective_leaf does not return any errors so it's safe to unwrap - // here. + ) -> Result, Word>, MerkleError> { >::compute_mutations(self, kv_pairs) - .expect("computing mutations on a simple smt never returns an error") } /// Applies the prospective mutations computed with [`SimpleSmt::compute_mutations()`] to this diff --git a/miden-crypto/src/merkle/sparse_path.rs b/miden-crypto/src/merkle/sparse_path.rs index 7a5b318aa8..cc24b78925 100644 --- a/miden-crypto/src/merkle/sparse_path.rs +++ b/miden-crypto/src/merkle/sparse_path.rs @@ -471,7 +471,8 @@ mod tests { let entries: Vec<(Word, Word)> = (0..pair_count) .map(|n| { let leaf_index = ((n as f64 / pair_count as f64) * 255.0) as u64; - let key = Word::new([ONE, ONE, Felt::new(n), Felt::new(leaf_index)]); + let key = + Word::new([ONE, ONE, Felt::new_unchecked(n), Felt::new_unchecked(leaf_index)]); let value = Word::new([ONE, ONE, ONE, ONE]); (key, value) }) @@ -699,7 +700,7 @@ mod tests { fn merkle_path_roundtrip_equivalence(sparse in any::()) { // Convert SparseMerklePath to MerklePath and back let merkle = MerklePath::from(sparse.clone()); - let reconstructed = SparseMerklePath::try_from(merkle.clone()).unwrap(); + let reconstructed = SparseMerklePath::try_from(merkle).unwrap(); prop_assert_eq!(sparse, reconstructed); } } @@ -948,7 +949,9 @@ mod tests { .prop_flat_map(|num_entries| { prop::collection::vec((any::(), any::()), num_entries).prop_map( |indices_n_values| { - let entries: Vec<(Word, Word)> = indices_n_values + // Ensure unique keys to avoid duplicates as we build the entries + let mut seen = alloc::collections::BTreeSet::new(); + let unique_entries: Vec<(Word, Word)> = indices_n_values .into_iter() .enumerate() .map(|(n, (leaf_index, value))| { @@ -956,20 +959,16 @@ mod tests { // Ensure we use valid leaf indices for the SMT depth let valid_leaf_index = leaf_index % (1u64 << 60); // Use large but valid range let key = Word::new([ - Felt::new(n as u64), // element 0 - Felt::new(n as u64 + 1), // element 1 - Felt::new(n as u64 + 2), // element 2 - Felt::new(valid_leaf_index), // element 3 (leaf index) + Felt::new_unchecked(n as u64), // element 0 + Felt::new_unchecked(n as u64 + 1), // element 1 + Felt::new_unchecked(n as u64 + 2), // element 2 + Felt::new_unchecked(valid_leaf_index), // element 3 (leaf index) ]); (key, value) }) + .filter(|(key, _)| seen.insert(*key)) .collect(); - // Ensure unique keys to avoid duplicates - let mut seen = alloc::collections::BTreeSet::new(); - let unique_entries: Vec<_> = - entries.into_iter().filter(|(key, _)| seen.insert(*key)).collect(); - let tree = Smt::with_entries(unique_entries.clone()).unwrap(); RandomSmt { tree, entries: unique_entries } }, diff --git a/miden-crypto/src/merkle/store/mod.rs b/miden-crypto/src/merkle/store/mod.rs index 2da1929f8a..3c68699daf 100644 --- a/miden-crypto/src/merkle/store/mod.rs +++ b/miden-crypto/src/merkle/store/mod.rs @@ -41,7 +41,7 @@ pub struct StoreNode { /// # use miden_crypto::hash::poseidon2::Poseidon2; /// # use miden_crypto::field::PrimeCharacteristicRing; /// # const fn int_to_node(value: u64) -> Word { -/// # Word::new([Felt::new(value), ZERO, ZERO, ZERO]) +/// # Word::new([Felt::new_unchecked(value), ZERO, ZERO, ZERO]) /// # } /// # let A = int_to_node(1); /// # let B = int_to_node(2); diff --git a/miden-crypto/src/merkle/store/tests.rs b/miden-crypto/src/merkle/store/tests.rs index cea91fef6e..eadbbcfb10 100644 --- a/miden-crypto/src/merkle/store/tests.rs +++ b/miden-crypto/src/merkle/store/tests.rs @@ -14,7 +14,7 @@ use super::{ Poseidon2, Word, }; use crate::{ - Felt, ONE, WORD_SIZE, ZERO, + Felt, ONE, ZERO, merkle::{ MerkleTree, int_to_leaf, int_to_node, smt::{LeafIndex, SMT_MAX_DEPTH, SimpleSmt}, @@ -539,7 +539,7 @@ fn test_add_merkle_paths() -> Result<(), MerkleError> { fn wont_open_to_different_depth_root() { let empty = EmptySubtreeRoots::empty_hashes(64); let a = Word::new([ONE; 4]); - let b = Word::new([Felt::new(2); 4]); + let b = Word::new([Felt::new_unchecked(2); 4]); // Compute the root for a different depth. We cherry-pick this specific depth to prevent a // regression to a bug in the past that allowed the user to fetch a node at a depth lower than @@ -562,13 +562,13 @@ fn wont_open_to_different_depth_root() { #[test] fn store_path_opens_from_leaf() { let a = Word::new([ONE; 4]); - let b = Word::new([Felt::new(2); 4]); - let c = Word::new([Felt::new(3); 4]); - let d = Word::new([Felt::new(4); 4]); - let e = Word::new([Felt::new(5); 4]); - let f = Word::new([Felt::new(6); 4]); - let g = Word::new([Felt::new(7); 4]); - let h = Word::new([Felt::new(8); 4]); + let b = Word::new([Felt::new_unchecked(2); 4]); + let c = Word::new([Felt::new_unchecked(3); 4]); + let d = Word::new([Felt::new_unchecked(4); 4]); + let e = Word::new([Felt::new_unchecked(5); 4]); + let f = Word::new([Felt::new_unchecked(6); 4]); + let g = Word::new([Felt::new_unchecked(7); 4]); + let h = Word::new([Felt::new_unchecked(8); 4]); let i = Poseidon2::merge(&[a, b]); let j = Poseidon2::merge(&[c, d]); @@ -629,9 +629,7 @@ fn test_constructors() -> Result<(), MerkleError> { assert_eq!(smt.open(&LeafIndex::::new(key).unwrap()).path, value_path.path); assert!( store.has_path(smt.root(), index), - "path for key {} at depth {} must exist", - key, - DEPTH + "path for key {key} at depth {DEPTH} must exist" ); } @@ -663,15 +661,11 @@ fn test_constructors() -> Result<(), MerkleError> { assert_eq!(pmt.get_path(index)?, value_path1.path); assert!( store1.has_path(pmt.root(), index), - "path for key {} at depth {} must exist in store1", - key, - d + "path for key {key} at depth {d} must exist in store1" ); assert!( store2.has_path(pmt.root(), index), - "path for key {} at depth {} must exist in store2", - key, - d + "path for key {key} at depth {d} must exist in store2" ); } @@ -687,7 +681,7 @@ fn node_path_should_be_truncated_by_midtier_insert() { // insert first node - works as expected let depth = 64; - let node = Word::from([Felt::new(key); WORD_SIZE]); + let node = Word::from([Felt::new_unchecked(key); Word::NUM_ELEMENTS]); let index = NodeIndex::new(depth, key).unwrap(); let root = store.set_node(root, index, node).unwrap().root; let result = store.get_node(root, index).unwrap(); @@ -701,7 +695,7 @@ fn node_path_should_be_truncated_by_midtier_insert() { let key = key ^ (1 << 63); let key = key >> 8; let depth = 56; - let node = Word::from([Felt::new(key); WORD_SIZE]); + let node = Word::from([Felt::new_unchecked(key); Word::NUM_ELEMENTS]); let index = NodeIndex::new(depth, key).unwrap(); let root = store.set_node(root, index, node).unwrap().root; let result = store.get_node(root, index).unwrap(); @@ -730,7 +724,7 @@ fn get_leaf_depth_works_depth_64() { // this will create a rainbow tree and test all opening to depth 64 for d in 0..64 { let k = key & (u64::MAX >> d); - let node = Word::from([Felt::new(k); WORD_SIZE]); + let node = Word::from([Felt::new_unchecked(k); Word::NUM_ELEMENTS]); let index = NodeIndex::new(64, k).unwrap(); // assert the leaf doesn't exist before the insert. the returned depth should always @@ -754,7 +748,7 @@ fn get_leaf_depth_works_with_incremental_depth() { assert_eq!(0, store.get_leaf_depth(root, 64, key).unwrap()); let depth = 64; let index = NodeIndex::new(depth, key).unwrap(); - let node = Word::from([Felt::new(key); WORD_SIZE]); + let node = Word::from([Felt::new_unchecked(key); Word::NUM_ELEMENTS]); root = store.set_node(root, index, node).unwrap().root; assert_eq!(depth, store.get_leaf_depth(root, 64, key).unwrap()); @@ -763,7 +757,7 @@ fn get_leaf_depth_works_with_incremental_depth() { assert_eq!(1, store.get_leaf_depth(root, 64, key).unwrap()); let depth = 16; let index = NodeIndex::new(depth, key >> (64 - depth)).unwrap(); - let node = Word::from([Felt::new(key); WORD_SIZE]); + let node = Word::from([Felt::new_unchecked(key); Word::NUM_ELEMENTS]); root = store.set_node(root, index, node).unwrap().root; assert_eq!(depth, store.get_leaf_depth(root, 64, key).unwrap()); @@ -771,7 +765,7 @@ fn get_leaf_depth_works_with_incremental_depth() { let key = 0b11001011_10110111_00000000_00000000_00000000_00000000_00000000_00000000_u64; assert_eq!(16, store.get_leaf_depth(root, 64, key).unwrap()); let index = NodeIndex::new(depth, key >> (64 - depth)).unwrap(); - let node = Word::from([Felt::new(key); WORD_SIZE]); + let node = Word::from([Felt::new_unchecked(key); Word::NUM_ELEMENTS]); root = store.set_node(root, index, node).unwrap().root; assert_eq!(depth, store.get_leaf_depth(root, 64, key).unwrap()); @@ -780,7 +774,7 @@ fn get_leaf_depth_works_with_incremental_depth() { assert_eq!(15, store.get_leaf_depth(root, 64, key).unwrap()); let depth = 17; let index = NodeIndex::new(depth, key >> (64 - depth)).unwrap(); - let node = Word::from([Felt::new(key); WORD_SIZE]); + let node = Word::from([Felt::new_unchecked(key); Word::NUM_ELEMENTS]); root = store.set_node(root, index, node).unwrap().root; assert_eq!(depth, store.get_leaf_depth(root, 64, key).unwrap()); } @@ -798,7 +792,7 @@ fn get_leaf_depth_works_with_depth_8() { for k in [a, b, c, d] { let index = NodeIndex::new(8, k).unwrap(); - let node = Word::from([Felt::new(k); WORD_SIZE]); + let node = Word::from([Felt::new_unchecked(k); Word::NUM_ELEMENTS]); root = store.set_node(root, index, node).unwrap().root; } @@ -943,7 +937,7 @@ fn check_mstore_subtree(store: &MerkleStore, subtree: &MerkleTree) { let path2 = subtree.get_path(index).unwrap(); assert_eq!(path1.path, path2); - assert!(store.has_path(subtree.root(), index), "path for leaf {} must exist", i); + assert!(store.has_path(subtree.root(), index), "path for leaf {i} must exist"); } } diff --git a/miden-crypto/src/rand/mod.rs b/miden-crypto/src/rand/mod.rs index 073544f712..d4d62ee663 100644 --- a/miden-crypto/src/rand/mod.rs +++ b/miden-crypto/src/rand/mod.rs @@ -1,6 +1,5 @@ //! Pseudo-random element generation. -use miden_field::word::WORD_SIZE_BYTES; use rand::RngCore; use crate::{Felt, Word}; @@ -91,7 +90,7 @@ impl Randomizable for Felt { let value = u64::from_le_bytes(bytes); // Ensure the value is within the field modulus if value < Felt::ORDER { - Some(Felt::new(value)) + Some(Felt::new_unchecked(value)) } else { None } @@ -102,7 +101,7 @@ impl Randomizable for Felt { } impl Randomizable for Word { - const VALUE_SIZE: usize = WORD_SIZE_BYTES; + const VALUE_SIZE: usize = Word::SERIALIZED_SIZE; fn from_random_bytes(bytes: &[u8]) -> Option { let bytes_array: Option<[u8; 32]> = bytes.try_into().ok(); @@ -146,9 +145,13 @@ pub trait FeltRng: RngCore { pub fn random_felt() -> Felt { use rand::Rng; let mut rng = rand::rng(); - // Goldilocks field order is 2^64 - 2^32 + 1 - // Generate a random u64 and reduce modulo the field order - Felt::new(rng.random::()) + // We use the `Felt::new` constructor to do rejection sampling here. It should effectively + // never repeat, but nevertheless gives us the correct distribution. + loop { + if let Ok(felt) = Felt::new(rng.random::()) { + return felt; + } + } } /// Generates a random word (4 field elements) for testing purposes. diff --git a/miden-crypto/src/rand/test_utils.rs b/miden-crypto/src/rand/test_utils.rs index 1a5bef3ebb..22d52176a4 100644 --- a/miden-crypto/src/rand/test_utils.rs +++ b/miden-crypto/src/rand/test_utils.rs @@ -53,10 +53,20 @@ fn rng_value(rng: &mut impl Rng) -> T { /// let x: u64 = rand_value(); /// let y: u128 = rand_value(); /// ``` +#[cfg(feature = "std")] pub fn rand_value() -> T { rng_value(&mut rand::rng()) } +/// Generates a deterministic value of type `T` in `no_std` builds. +/// +/// This keeps tests and feature-matrix checks buildable without relying on +/// thread-local RNG support. +#[cfg(not(feature = "std"))] +pub fn rand_value() -> T { + prng_value([0u8; 32]) +} + /// Generates a random array of type T with N elements. /// /// # Examples diff --git a/miden-crypto/src/utils/mod.rs b/miden-crypto/src/utils/mod.rs index ffef91a267..0fabaae169 100644 --- a/miden-crypto/src/utils/mod.rs +++ b/miden-crypto/src/utils/mod.rs @@ -93,7 +93,7 @@ pub fn bytes_to_elements_with_padding(bytes: &[u8]) -> Vec { buf[chunk.len()] = 1; } - Felt::new(u64::from_le_bytes(buf)) + Felt::new_unchecked(u64::from_le_bytes(buf)) }) .collect() } @@ -208,10 +208,10 @@ pub fn bytes_to_elements_exact(bytes: &[u8]) -> Option> { /// /// let bytes = vec![0x01, 0x02, 0x03, 0x04, 0x05]; /// let felts = bytes_to_packed_u32_elements(&bytes); -/// assert_eq!(felts, vec![Felt::new(0x04030201), Felt::new(0x00000005)]); +/// assert_eq!(felts, vec![Felt::new_unchecked(0x04030201), Felt::new_unchecked(0x00000005)]); /// ``` pub fn bytes_to_packed_u32_elements(bytes: &[u8]) -> Vec { - const BYTES_PER_U32: usize = core::mem::size_of::(); + const BYTES_PER_U32: usize = size_of::(); bytes .chunks(BYTES_PER_U32) diff --git a/miden-crypto/tests/rocksdb_large_smt.rs b/miden-crypto/tests/rocksdb_large_smt.rs index 61d5e091df..c42dff8b61 100644 --- a/miden-crypto/tests/rocksdb_large_smt.rs +++ b/miden-crypto/tests/rocksdb_large_smt.rs @@ -1,5 +1,5 @@ use miden_crypto::{ - EMPTY_WORD, Felt, ONE, WORD_SIZE, Word, ZERO, + EMPTY_WORD, Felt, ONE, Word, ZERO, merkle::{ InnerNodeInfo, smt::{LargeSmt, LargeSmtError, RocksDbConfig, RocksDbStorage}, @@ -23,8 +23,13 @@ fn setup_storage() -> (RocksDbStorage, TempDir) { fn generate_entries(pair_count: usize) -> Vec<(Word, Word)> { (0..pair_count) .map(|i| { - let key = Word::new([ONE, ONE, Felt::new(i as u64), Felt::new(i as u64 % 1000)]); - let value = Word::new([ONE, ONE, ONE, Felt::new(i as u64)]); + let key = Word::new([ + ONE, + ONE, + Felt::new_unchecked(i as u64), + Felt::new_unchecked(i as u64 % 1000), + ]); + let value = Word::new([ONE, ONE, ONE, Felt::new_unchecked(i as u64)]); (key, value) }) .collect() @@ -36,7 +41,7 @@ fn rocksdb_sanity_insert_and_get() { let mut smt = LargeSmt::::new(storage).unwrap(); let key = Word::new([ONE, ONE, ONE, ONE]); - let val = Word::new([ONE; WORD_SIZE]); + let val = Word::new([ONE; Word::NUM_ELEMENTS]); let prev = smt.insert(key, val).unwrap(); assert_eq!(prev, EMPTY_WORD); @@ -77,7 +82,12 @@ fn rocksdb_persistence_after_insertion() { let mut smt = LargeSmt::::with_entries(initial_storage, entries).unwrap(); let key = Word::new([ONE, ONE, ONE, ONE]); - let new_value = Word::new([Felt::new(2), Felt::new(2), Felt::new(2), Felt::new(2)]); + let new_value = Word::new([ + Felt::new_unchecked(2), + Felt::new_unchecked(2), + Felt::new_unchecked(2), + Felt::new_unchecked(2), + ]); smt.insert(key, new_value).unwrap(); let root = smt.root(); @@ -111,14 +121,24 @@ fn rocksdb_persistence_after_insert_batch_with_deletions() { // Add new entries for i in 20_000..25_000 { - let key = Word::new([ONE, ONE, Felt::new(i as u64), Felt::new(i as u64 % 1000)]); - let value = Word::new([ONE, ONE, ONE, Felt::new(i as u64)]); + let key = Word::new([ + ONE, + ONE, + Felt::new_unchecked(i as u64), + Felt::new_unchecked(i as u64 % 1000), + ]); + let value = Word::new([ONE, ONE, ONE, Felt::new_unchecked(i as u64)]); batch_entries.push((key, value)); } // Delete some existing entries for i in 0..1000 { - let key = Word::new([ONE, ONE, Felt::new(i as u64), Felt::new(i as u64 % 1000)]); + let key = Word::new([ + ONE, + ONE, + Felt::new_unchecked(i as u64), + Felt::new_unchecked(i as u64 % 1000), + ]); batch_entries.push((key, EMPTY_WORD)); } @@ -189,7 +209,7 @@ fn rocksdb_load_with_root_mismatch_returns_error() { assert_eq!(expected, wrong_root); assert_eq!(actual, actual_root); }, - other => panic!("Expected RootMismatch error, got {:?}", other), + other => panic!("Expected RootMismatch error, got {other:?}"), } } @@ -230,7 +250,7 @@ fn rocksdb_new_fails_on_non_empty_storage() { assert!(result.is_err(), "new() should fail on non-empty storage"); match result.unwrap_err() { LargeSmtError::StorageNotEmpty => {}, - other => panic!("Expected StorageNotEmpty error, got {:?}", other), + other => panic!("Expected StorageNotEmpty error, got {other:?}"), } } diff --git a/miden-field/Cargo.toml b/miden-field/Cargo.toml index 4ef6f709a9..10006be345 100644 --- a/miden-field/Cargo.toml +++ b/miden-field/Cargo.toml @@ -17,29 +17,33 @@ crate-type = ["rlib"] # dependendies for both off-chain and on-chain targets [dependencies] -thiserror = { default-features = false, version = "2.0" } +thiserror = { workspace = true } # dependendies for the off-chain target only [target.'cfg(not(all(target_family = "wasm", miden)))'.dependencies] -miden-serde-utils = { workspace = true } -num-bigint = { default-features = false, version = "0.4" } -p3-challenger = { default-features = false, version = "0.5" } -p3-field = { default-features = false, version = "0.5" } -p3-goldilocks = { default-features = false, version = "0.5" } -paste = { version = "1.0.15" } -proptest = { default-features = false, features = ["alloc", "std"], optional = true, version = "1.7" } -rand = { default-features = false, version = "0.10" } -serde = { default-features = false, features = ["derive"], version = "1.0" } -subtle = { default-features = false, version = "2.6" } +miden-serde-utils = { workspace = true } +num-bigint = { default-features = false, version = "0.4" } +p3-challenger.workspace = true +p3-field.workspace = true +p3-goldilocks.workspace = true +p3-util.workspace = true +paste = { version = "1.0.15" } +proptest = { default-features = false, features = ["alloc", "std"], optional = true, version = "1.7" } +rand = { default-features = false, version = "0.10" } +serde = { default-features = false, features = ["derive"], version = "1.0" } +subtle = { default-features = false, version = "2.6" } [features] default = [] testing = ["dep:proptest"] [dev-dependencies] -proptest = { default-features = false, features = ["alloc", "std"], version = "1.7" } +proptest = { features = ["alloc", "std"], workspace = true } rand = { default-features = false, version = "0.10" } -rstest = { version = "0.26" } +rstest = { workspace = true } [lints] workspace = true + +[package.metadata.cargo-shear] +ignored = ["paste"] diff --git a/miden-field/src/lib.rs b/miden-field/src/lib.rs index 3d281d79b3..480ef627e5 100644 --- a/miden-field/src/lib.rs +++ b/miden-field/src/lib.rs @@ -12,7 +12,7 @@ extern crate alloc; #[cfg(all(target_family = "wasm", miden))] mod wasm_miden; #[cfg(all(target_family = "wasm", miden))] -pub use wasm_miden::Felt; +pub use wasm_miden::{Felt, FeltFromIntError}; #[cfg(not(all(target_family = "wasm", miden)))] mod native; @@ -36,4 +36,4 @@ pub use p3_field::{ }, integers::QuotientMap, }; -pub use word::{LexicographicWord, Word, WordError}; +pub use word::{Word, WordError}; diff --git a/miden-field/src/native/mod.rs b/miden-field/src/native/mod.rs index 6191f01d00..96d6368878 100644 --- a/miden-field/src/native/mod.rs +++ b/miden-field/src/native/mod.rs @@ -1,11 +1,12 @@ //! Off-chain implementation of [`crate::Felt`]. -use alloc::format; +use alloc::{format, vec, vec::Vec}; use core::{ array, fmt, hash::{Hash, Hasher}, iter::{Product, Sum}, - ops::{Add, AddAssign, Deref, DerefMut, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}, + mem::{align_of, size_of}, + ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}, }; use miden_serde_utils::{ @@ -22,6 +23,7 @@ use p3_field::{ quotient_map_large_iint, quotient_map_large_uint, quotient_map_small_int, }; use p3_goldilocks::Goldilocks; +use p3_util::flatten_to_base; use rand::{ Rng, distr::{Distribution, StandardUniform}, @@ -49,12 +51,21 @@ impl Felt { /// The number of bytes which this field element occupies in memory. pub const NUM_BYTES: usize = Goldilocks::NUM_BYTES; - /// Creates a new field element from any `u64`. + /// Constructs a new field element from the provided `value`. + /// + /// # Errors + /// + /// - [`FeltFromIntError`] if the provided `value` is not a valid input. + pub fn new(value: u64) -> Result { + Felt::from_canonical_checked(value).ok_or(FeltFromIntError(value)) + } + + /// Creates a new field element from any `u64` without performing reduction. /// /// Any `u64` value is accepted. No reduction is performed since Goldilocks uses a /// non-canonical internal representation. #[inline] - pub const fn new(value: u64) -> Self { + pub const fn new_unchecked(value: u64) -> Self { Self(Goldilocks::new(value)) } @@ -108,7 +119,7 @@ impl Felt { /// Plonky3's Goldilocks implementation. #[inline] pub fn as_canonical_u64_ct(&self) -> u64 { - let raw = raw_felt_u64(self); + let raw = raw_felt_u64(*self); // Mirrors Goldilocks::as_canonical_u64: conditional subtraction of ORDER. // A single subtraction is sufficient for any u64 value since 2*ORDER > u64::MAX. let reduced = raw.wrapping_sub(Self::ORDER); @@ -118,14 +129,36 @@ impl Felt { } #[inline] -fn raw_felt_u64(value: &Felt) -> u64 { +fn raw_felt_u64(value: Felt) -> u64 { const _: () = { - assert!(core::mem::size_of::() == core::mem::size_of::()); - assert!(core::mem::align_of::() == core::mem::align_of::()); + assert!(size_of::() == size_of::()); + assert!(align_of::() == align_of::()); assert!(2u128 * (Felt::ORDER as u128) > u64::MAX as u128); }; // SAFETY: Felt is repr(transparent) over Goldilocks, which is repr(transparent) over u64. - unsafe { core::mem::transmute_copy(value) } + unsafe { core::mem::transmute_copy(&value) } +} + +/// Reinterprets a `Felt` slice as `Goldilocks`. +/// +/// # Safety +/// +/// `Felt` is `#[repr(transparent)]` over `Goldilocks`, so the element layout matches. +#[inline] +fn felts_as_goldilocks_slice(s: &[Felt]) -> &[Goldilocks] { + // SAFETY: `Felt` is `#[repr(transparent)]` over `Goldilocks`, so the element layout matches. + unsafe { core::slice::from_raw_parts(s.as_ptr().cast::(), s.len()) } +} + +/// Reinterprets a `Felt` array as `Goldilocks`. +/// +/// # Safety +/// +/// `Felt` is `#[repr(transparent)]` over `Goldilocks`, so `[Felt; N]` matches `[Goldilocks; N]`. +#[inline] +fn felts_as_goldilocks_array(a: &[Felt; N]) -> &[Goldilocks; N] { + // SAFETY: same layout as `felts_as_goldilocks_slice`, for a fixed `N`. + unsafe { &*(a as *const [Felt; N] as *const [Goldilocks; N]) } } impl fmt::Display for Felt { @@ -153,6 +186,8 @@ impl Hash for Felt { // ================================================================================================ impl Field for Felt { + // TODO: This should only be the case for WASM targets. + // Native targets should be able to leverage AVX2 / NEON optimizations from Plonky3. type Packing = Self; const GENERATOR: Self = Self(Goldilocks::GENERATOR); @@ -190,7 +225,7 @@ impl PrimeCharacteristicRing for Felt { #[inline] fn from_bool(value: bool) -> Self { - Self::new(value.into()) + Self::new_unchecked(value.into()) } #[inline] @@ -212,6 +247,29 @@ impl PrimeCharacteristicRing for Felt { fn exp_u64(&self, power: u64) -> Self { self.0.exp_u64(power).into() } + + #[inline] + fn sum_array(input: &[Self]) -> Self { + assert_eq!(N, input.len()); + let g = felts_as_goldilocks_slice(input); + Self(Goldilocks::sum_array::(g)) + } + + #[inline] + fn dot_product(lhs: &[Self; N], rhs: &[Self; N]) -> Self { + let lhs_g = felts_as_goldilocks_array(lhs); + let rhs_g = felts_as_goldilocks_array(rhs); + Self(Goldilocks::dot_product(lhs_g, rhs_g)) + } + + #[inline] + fn zero_vec(len: usize) -> Vec { + // SAFETY: + // Due to `#[repr(transparent)]`, Felt, Goldilocks and u64 have the same size, + // alignment and memory layout making `flatten_to_base` safe. + // This will create a vector of Felt elements with value set to 0. + unsafe { flatten_to_base(vec![0u64; len]) } + } } quotient_map_small_int!(Felt, u64, [u8, u16, u32]); @@ -403,7 +461,7 @@ impl TryFrom for Felt { type Error = FeltFromIntError; fn try_from(int: u64) -> Result { - Felt::from_canonical_checked(int).ok_or(FeltFromIntError(int)) + Felt::new(int) } } @@ -418,22 +476,6 @@ impl FeltFromIntError { } } -impl Deref for Felt { - type Target = Goldilocks; - - #[inline] - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for Felt { - #[inline] - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - impl From for Felt { #[inline] fn from(value: Goldilocks) -> Self { @@ -594,7 +636,7 @@ impl Serializable for Felt { } fn get_size_hint(&self) -> usize { - core::mem::size_of::() + size_of::() } } @@ -621,11 +663,11 @@ mod arbitrary { type Strategy = BoxedStrategy; fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - let canonical = (0u64..Felt::ORDER).prop_map(Felt::new).boxed(); + let canonical = (0u64..Felt::ORDER).prop_map(Felt::new_unchecked).boxed(); // Goldilocks uses representation where values above the field order are valid and // represent wrapped field elements. Generate such values 1/5 of the time to exercise // this behavior. - let non_canonical = (Felt::ORDER..=u64::MAX).prop_map(Felt::new).boxed(); + let non_canonical = (Felt::ORDER..=u64::MAX).prop_map(Felt::new_unchecked).boxed(); prop_oneof![4 => canonical, 1 => non_canonical].no_shrink().boxed() } } diff --git a/miden-field/src/native/tests.rs b/miden-field/src/native/tests.rs index 3cbd4a0c76..adfefa4525 100644 --- a/miden-field/src/native/tests.rs +++ b/miden-field/src/native/tests.rs @@ -49,14 +49,14 @@ proptest! { /// `Felt::new` matches `Goldilocks::new` for the same input. #[test] fn felt_new_matches_goldilocks_new(x in any::()) { - prop_assert_eq!(Felt::new(x), Goldilocks::new(x)); + prop_assert_eq!(Felt::new_unchecked(x), Goldilocks::new(x)); } /// Core arithmetic operations match `Goldilocks`. #[test] fn felt_arithmetic_matches_goldilocks(a in any::(), b in any::()) { - let fa = Felt::new(a); - let fb = Felt::new(b); + let fa = Felt::new_unchecked(a); + let fb = Felt::new_unchecked(b); let ga = Goldilocks::new(a); let gb = Goldilocks::new(b); @@ -88,7 +88,7 @@ proptest! { /// `Field` and `PrimeCharacteristicRing` operations match `Goldilocks`. #[test] fn felt_field_methods_match_goldilocks(a in any::(), exp in any::(), shift in any::()) { - let fa = Felt::new(a); + let fa = Felt::new_unchecked(a); let ga = Goldilocks::new(a); prop_assert_eq!( @@ -123,8 +123,8 @@ proptest! { /// Formatting, ordering, and hashing match `Goldilocks`. #[test] fn felt_misc_traits_match_goldilocks(a in any::(), b in any::()) { - let fa = Felt::new(a); - let fb = Felt::new(b); + let fa = Felt::new_unchecked(a); + let fb = Felt::new_unchecked(b); let ga = Goldilocks::new(a); let gb = Goldilocks::new(b); @@ -176,7 +176,7 @@ proptest! { /// Iterated operations (`Sum`/`Product`) match `Goldilocks`. #[test] fn felt_iterators_match_goldilocks(xs in prop::collection::vec(any::(), 0..64)) { - let felts: Vec = xs.iter().copied().map(Felt::new).collect(); + let felts: Vec = xs.iter().copied().map(Felt::new_unchecked).collect(); let golds: Vec = xs.iter().copied().map(Goldilocks::new).collect(); let fs = felts.iter().copied().sum::(); @@ -225,11 +225,11 @@ fn felt_constants_match_goldilocks() { assert_eq!(::order(), ::order()); assert_eq!( - ::as_canonical_biguint(&Felt::new(u64::MAX)), + ::as_canonical_biguint(&Felt::new_unchecked(u64::MAX)), ::as_canonical_biguint(&Goldilocks::new(u64::MAX)), ); assert_eq!( - Felt::new(u64::MAX).as_canonical_u64(), + Felt::new_unchecked(u64::MAX).as_canonical_u64(), Goldilocks::new(u64::MAX).as_canonical_u64() ); } @@ -287,9 +287,9 @@ fn felt_injective_monomial_matches_goldilocks() { let inputs = [ Felt::ZERO, Felt::ONE, - Felt::new(Felt::ORDER), - Felt::new(u64::MAX), - Felt::new(100), + Felt::new_unchecked(Felt::ORDER), + Felt::new_unchecked(u64::MAX), + Felt::new_unchecked(100), ]; for f in inputs { diff --git a/miden-field/src/wasm_miden.rs b/miden-field/src/wasm_miden.rs index 402c7365d2..e3612c6c59 100644 --- a/miden-field/src/wasm_miden.rs +++ b/miden-field/src/wasm_miden.rs @@ -77,6 +77,9 @@ impl Felt { /// The field modulus, `2^64 - 2^32 + 1`. pub const ORDER_U64: u64 = 0xffff_ffff_0000_0001; + /// Alias for [`Self::ORDER_U64`], matching the native API surface. + pub const ORDER: u64 = Self::ORDER_U64; + /// Field element representing zero. pub const ZERO: Self = Self { inner: f32::from_bits(0) }; @@ -86,15 +89,27 @@ impl Felt { /// Field element representing two. pub const TWO: Self = Self { inner: f32::from_bits(2) }; - /// Creates a new field element from any `u64`. + /// Constructs a new field element from the provided `value`. + /// + /// # Errors + /// + /// - [`FeltFromIntError`] if the provided `value` is not a valid input. + pub fn new(value: u64) -> Result { + Felt::from_canonical_checked(value).ok_or(FeltFromIntError(value)) + } + + /// Creates a new field element from any `u64` without performing reduction. + /// + /// Any `u64` value is accepted. No validation is performed; the value is passed directly + /// to the VM intrinsic. #[inline(always)] - pub fn new(value: u64) -> Self { + pub fn new_unchecked(value: u64) -> Self { unsafe { extern_from_u64_unchecked(value) } } #[inline(always)] pub fn from_canonical_checked(int: u64) -> Option { - (int < Self::ORDER_U64).then(|| Self::new(int)) + (int < Self::ORDER_U64).then(|| Self::new_unchecked(int)) } #[inline(always)] @@ -205,6 +220,14 @@ impl From for Felt { } } +impl TryFrom for Felt { + type Error = FeltFromIntError; + + fn try_from(int: u64) -> Result { + Felt::new(int) + } +} + impl core::ops::Add for Felt { type Output = Self; @@ -343,3 +366,17 @@ impl core::hash::Hash for Felt { core::hash::Hash::hash(&self.as_canonical_u64(), state); } } + +// ERRORS +// ================================================================================================ + +#[derive(Debug, thiserror::Error)] +#[error("integer {0} is equal to or exceeds the felt modulus {modulus}", modulus = Felt::ORDER)] +pub struct FeltFromIntError(u64); + +impl FeltFromIntError { + /// Returns the integer for which the conversion failed. + pub fn as_u64(&self) -> u64 { + self.0 + } +} diff --git a/miden-field/src/word/lexicographic.rs b/miden-field/src/word/lexicographic.rs deleted file mode 100644 index 65c2f2432d..0000000000 --- a/miden-field/src/word/lexicographic.rs +++ /dev/null @@ -1,137 +0,0 @@ -use core::cmp::Ordering; - -#[cfg(not(all(target_family = "wasm", miden)))] -use super::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; -use crate::{Felt, Word, word::WORD_SIZE_FELTS}; - -// LEXICOGRAPHIC WORD -// ================================================================================================ - -/// A [`Word`] wrapper with lexicographic ordering. -/// -/// This is a wrapper around any [`Word`] convertible type that overrides the equality and ordering -/// implementations with a lexigographic one based on the wrapped type's [`Word`] representation. -#[derive(Debug, Clone, Copy)] -pub struct LexicographicWord = Word>(T); - -impl> LexicographicWord { - /// Wraps the provided value into a new [`LexicographicWord`]. - pub fn new(inner: T) -> Self { - Self(inner) - } - - /// Returns a reference to the inner value. - pub fn inner(&self) -> &T { - &self.0 - } - - /// Consumes self and returns the inner value. - pub fn into_inner(self) -> T { - self.0 - } -} - -impl From<[Felt; WORD_SIZE_FELTS]> for LexicographicWord { - fn from(value: [Felt; WORD_SIZE_FELTS]) -> Self { - Self(value.into()) - } -} - -impl From for LexicographicWord { - fn from(word: Word) -> Self { - Self(word) - } -} - -impl> From> for Word { - fn from(key: LexicographicWord) -> Self { - key.0.into() - } -} - -impl + Copy> PartialEq for LexicographicWord { - fn eq(&self, other: &Self) -> bool { - let self_word: Word = self.0.into(); - let other_word: Word = other.0.into(); - self_word == other_word - } -} - -impl + Copy> Eq for LexicographicWord {} - -impl + Copy> PartialOrd for LexicographicWord { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl + Copy> Ord for LexicographicWord { - fn cmp(&self, other: &Self) -> Ordering { - let self_word: Word = self.0.into(); - let other_word: Word = other.0.into(); - - self_word.cmp(&other_word) - } -} - -// SERIALIZATION -// ================================================================================================ - -#[cfg(not(all(target_family = "wasm", miden)))] -impl + Copy> Serializable for LexicographicWord { - fn write_into(&self, target: &mut W) { - self.0.into().write_into(target); - } - - fn get_size_hint(&self) -> usize { - self.0.into().get_size_hint() - } -} - -#[cfg(not(all(target_family = "wasm", miden)))] -impl + From> Deserializable for LexicographicWord { - fn read_from(source: &mut R) -> Result { - let word = Word::read_from(source)?; - - Ok(Self::new(T::from(word))) - } -} - -// TESTS -// ================================================================================================ - -#[cfg(test)] -mod tests { - - use super::*; - - #[derive(Debug, Clone, Copy)] - struct NoteId(Word); - - impl From for NoteId { - fn from(value: Word) -> Self { - Self(value) - } - } - - impl From for Word { - fn from(value: NoteId) -> Self { - value.0 - } - } - - #[test] - fn lexicographic_serialization() { - let word = Word::from([1u64, 2, 3, 4].map(Felt::new)); - let key = LexicographicWord::new(word); - let bytes = key.to_bytes(); - let deserialized_key = LexicographicWord::::read_from_bytes(&bytes).unwrap(); - assert_eq!(key, deserialized_key); - - let note_id = NoteId::from(word); - let key = LexicographicWord::new(note_id); - let bytes = key.to_bytes(); - let deserialized_key = LexicographicWord::::read_from_bytes(&bytes).unwrap(); - assert_eq!(key, deserialized_key); - } -} diff --git a/miden-field/src/word/mod.rs b/miden-field/src/word/mod.rs index a13224ec30..46361e3626 100644 --- a/miden-field/src/word/mod.rs +++ b/miden-field/src/word/mod.rs @@ -6,6 +6,7 @@ use core::fmt::Display; use core::{ cmp::Ordering, hash::{Hash, Hasher}, + mem::size_of, ops::{Deref, DerefMut, Index, IndexMut, Range}, slice, }; @@ -14,20 +15,13 @@ use core::{ use miden_serde_utils::{ ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable, }; -use thiserror::Error; - -pub const WORD_SIZE_FELTS: usize = 4; -pub const WORD_SIZE_BYTES: usize = 32; - #[cfg(not(all(target_family = "wasm", miden)))] use p3_field::integers::QuotientMap; +use thiserror::Error; use super::Felt; use crate::utils::bytes_to_hex_string; -mod lexicographic; -pub use lexicographic::LexicographicWord; - #[cfg(test)] mod tests; @@ -69,13 +63,13 @@ pub struct Word { // Compile-time assertions to ensure `Word` has the same layout as `[Felt; 4]`. This is relied upon // in `as_elements_array`/`as_elements_array_mut`. const _: () = { - assert!(WORD_SIZE_FELTS == 4, "WORD_SIZE_FELTS is assumed to be 4"); - assert!(WORD_SIZE_BYTES == 32, "WORD_SIZE_BYTES is assumed to be 32"); - assert!(core::mem::size_of::() == WORD_SIZE_FELTS * core::mem::size_of::()); + assert!(Word::NUM_ELEMENTS == 4, "Word::NUM_ELEMENTS is assumed to be 4"); + assert!(Word::SERIALIZED_SIZE == 32, "Word::SERIALIZED_SIZE is assumed to be 32"); + assert!(size_of::() == Word::NUM_ELEMENTS * size_of::()); assert!(core::mem::offset_of!(Word, a) == 0); - assert!(core::mem::offset_of!(Word, b) == core::mem::size_of::()); - assert!(core::mem::offset_of!(Word, c) == 2 * core::mem::size_of::()); - assert!(core::mem::offset_of!(Word, d) == 3 * core::mem::size_of::()); + assert!(core::mem::offset_of!(Word, b) == size_of::()); + assert!(core::mem::offset_of!(Word, c) == 2 * size_of::()); + assert!(core::mem::offset_of!(Word, d) == 3 * size_of::()); }; impl core::fmt::Debug for Word { @@ -85,17 +79,20 @@ impl core::fmt::Debug for Word { } impl Word { + /// The number of field elements in the word. + pub const NUM_ELEMENTS: usize = 4; + /// The serialized size of the word in bytes. - pub const SERIALIZED_SIZE: usize = WORD_SIZE_BYTES; + pub const SERIALIZED_SIZE: usize = 32; /// Creates a new [`Word`] from the given field elements. - pub const fn new(value: [Felt; WORD_SIZE_FELTS]) -> Self { + pub const fn new(value: [Felt; Self::NUM_ELEMENTS]) -> Self { let [a, b, c, d] = value; Self { a, b, c, d } } /// Returns the elements of this word as an array. - pub const fn into_elements(self) -> [Felt; WORD_SIZE_FELTS] { + pub const fn into_elements(self) -> [Felt; Self::NUM_ELEMENTS] { [self.a, self.b, self.c, self.d] } @@ -104,8 +101,8 @@ impl Word { /// # Safety /// This assumes the four fields of [`Word`] are laid out contiguously with no padding, in /// the same order as `[Felt; 4]`. - fn as_elements_array(&self) -> &[Felt; WORD_SIZE_FELTS] { - unsafe { &*(&self.a as *const Felt as *const [Felt; WORD_SIZE_FELTS]) } + fn as_elements_array(&self) -> &[Felt; Self::NUM_ELEMENTS] { + unsafe { &*(&self.a as *const Felt as *const [Felt; Self::NUM_ELEMENTS]) } } /// Returns the elements of this word as a mutable array reference. @@ -113,8 +110,8 @@ impl Word { /// # Safety /// This assumes the four fields of [`Word`] are laid out contiguously with no padding, in /// the same order as `[Felt; 4]`. - fn as_elements_array_mut(&mut self) -> &mut [Felt; WORD_SIZE_FELTS] { - unsafe { &mut *(&mut self.a as *mut Felt as *mut [Felt; WORD_SIZE_FELTS]) } + fn as_elements_array_mut(&mut self) -> &mut [Felt; Self::NUM_ELEMENTS] { + unsafe { &mut *(&mut self.a as *mut Felt as *mut [Felt; Self::NUM_ELEMENTS]) } } /// Parses a hex string into a new [`Word`]. @@ -130,7 +127,15 @@ impl Word { /// ``` /// use miden_field::{Felt, Word, word}; /// let word = word!("0x1000000000000000200000000000000030000000000000004000000000000000"); - /// assert_eq!(word, Word::new([Felt::new(16), Felt::new(32), Felt::new(48), Felt::new(64)])); + /// assert_eq!( + /// word, + /// Word::new([ + /// Felt::new_unchecked(16), + /// Felt::new_unchecked(32), + /// Felt::new_unchecked(48), + /// Felt::new_unchecked(64) + /// ]) + /// ); /// ``` #[cfg(not(all(target_family = "wasm", miden)))] pub const fn parse(hex: &str) -> Result { @@ -187,16 +192,16 @@ impl Word { } Ok(Self::new([ - Felt::new(felts[0]), - Felt::new(felts[1]), - Felt::new(felts[2]), - Felt::new(felts[3]), + Felt::new_unchecked(felts[0]), + Felt::new_unchecked(felts[1]), + Felt::new_unchecked(felts[2]), + Felt::new_unchecked(felts[3]), ])) } /// Returns a new [Word] consisting of four ZERO elements. pub const fn empty() -> Self { - Self::new([Felt::ZERO; WORD_SIZE_FELTS]) + Self::new([Felt::ZERO; Self::NUM_ELEMENTS]) } /// Returns true if the word consists of four ZERO elements. @@ -214,8 +219,8 @@ impl Word { } /// Returns the word as a byte array. - pub fn as_bytes(&self) -> [u8; WORD_SIZE_BYTES] { - let mut result = [0; WORD_SIZE_BYTES]; + pub fn as_bytes(&self) -> [u8; Self::SERIALIZED_SIZE] { + let mut result = [0; Self::SERIALIZED_SIZE]; let elements = self.as_elements_array(); result[..8].copy_from_slice(&elements[0].as_canonical_u64().to_le_bytes()); @@ -236,7 +241,7 @@ impl Word { /// Returns all elements of multiple words as a slice. pub fn words_as_elements(words: &[Self]) -> &[Felt] { - let len = words.len() * WORD_SIZE_FELTS; + let len = words.len() * Self::NUM_ELEMENTS; unsafe { slice::from_raw_parts(words.as_ptr() as *const Felt, len) } } @@ -268,7 +273,7 @@ impl Hash for Word { } impl Deref for Word { - type Target = [Felt; WORD_SIZE_FELTS]; + type Target = [Felt; Word::NUM_ELEMENTS]; fn deref(&self) -> &Self::Target { self.as_elements_array() @@ -322,8 +327,8 @@ impl Ord for Word { // though the field order is p = 2^64 - 2^32 + 1. This method canonicalizes to [0, p). // // We must iterate over and compare each element individually. A simple bytestring - // comparison would be inappropriate because the `Word`s are represented in - // "lexicographical" order. + // comparison would be inappropriate because `Word`s internal representation is not + // naturally lexicographically comparable. for (felt0, felt1) in self .iter() .rev() @@ -373,7 +378,7 @@ pub enum WordError { TypeConversion(&'static str), } -impl TryFrom<&Word> for [bool; WORD_SIZE_FELTS] { +impl TryFrom<&Word> for [bool; Word::NUM_ELEMENTS] { type Error = WordError; fn try_from(value: &Word) -> Result { @@ -381,7 +386,7 @@ impl TryFrom<&Word> for [bool; WORD_SIZE_FELTS] { } } -impl TryFrom for [bool; WORD_SIZE_FELTS] { +impl TryFrom for [bool; Word::NUM_ELEMENTS] { type Error = WordError; fn try_from(value: Word) -> Result { @@ -399,7 +404,7 @@ impl TryFrom for [bool; WORD_SIZE_FELTS] { } } -impl TryFrom<&Word> for [u8; WORD_SIZE_FELTS] { +impl TryFrom<&Word> for [u8; Word::NUM_ELEMENTS] { type Error = WordError; fn try_from(value: &Word) -> Result { @@ -407,7 +412,7 @@ impl TryFrom<&Word> for [u8; WORD_SIZE_FELTS] { } } -impl TryFrom for [u8; WORD_SIZE_FELTS] { +impl TryFrom for [u8; Word::NUM_ELEMENTS] { type Error = WordError; fn try_from(value: Word) -> Result { @@ -421,7 +426,7 @@ impl TryFrom for [u8; WORD_SIZE_FELTS] { } } -impl TryFrom<&Word> for [u16; WORD_SIZE_FELTS] { +impl TryFrom<&Word> for [u16; Word::NUM_ELEMENTS] { type Error = WordError; fn try_from(value: &Word) -> Result { @@ -429,7 +434,7 @@ impl TryFrom<&Word> for [u16; WORD_SIZE_FELTS] { } } -impl TryFrom for [u16; WORD_SIZE_FELTS] { +impl TryFrom for [u16; Word::NUM_ELEMENTS] { type Error = WordError; fn try_from(value: Word) -> Result { @@ -443,7 +448,7 @@ impl TryFrom for [u16; WORD_SIZE_FELTS] { } } -impl TryFrom<&Word> for [u32; WORD_SIZE_FELTS] { +impl TryFrom<&Word> for [u32; Word::NUM_ELEMENTS] { type Error = WordError; fn try_from(value: &Word) -> Result { @@ -451,7 +456,7 @@ impl TryFrom<&Word> for [u32; WORD_SIZE_FELTS] { } } -impl TryFrom for [u32; WORD_SIZE_FELTS] { +impl TryFrom for [u32; Word::NUM_ELEMENTS] { type Error = WordError; fn try_from(value: Word) -> Result { @@ -465,37 +470,37 @@ impl TryFrom for [u32; WORD_SIZE_FELTS] { } } -impl From<&Word> for [u64; WORD_SIZE_FELTS] { +impl From<&Word> for [u64; Word::NUM_ELEMENTS] { fn from(value: &Word) -> Self { (*value).into() } } -impl From for [u64; WORD_SIZE_FELTS] { +impl From for [u64; Word::NUM_ELEMENTS] { fn from(value: Word) -> Self { value.into_elements().map(|felt| felt.as_canonical_u64()) } } -impl From<&Word> for [Felt; WORD_SIZE_FELTS] { +impl From<&Word> for [Felt; Word::NUM_ELEMENTS] { fn from(value: &Word) -> Self { (*value).into() } } -impl From for [Felt; WORD_SIZE_FELTS] { +impl From for [Felt; Word::NUM_ELEMENTS] { fn from(value: Word) -> Self { value.into_elements() } } -impl From<&Word> for [u8; WORD_SIZE_BYTES] { +impl From<&Word> for [u8; Word::SERIALIZED_SIZE] { fn from(value: &Word) -> Self { (*value).into() } } -impl From for [u8; WORD_SIZE_BYTES] { +impl From for [u8; Word::SERIALIZED_SIZE] { fn from(value: Word) -> Self { value.as_bytes() } @@ -520,26 +525,26 @@ impl From for String { // CONVERSIONS: TO WORD // ================================================================================================ -impl From<&[bool; WORD_SIZE_FELTS]> for Word { - fn from(value: &[bool; WORD_SIZE_FELTS]) -> Self { +impl From<&[bool; Word::NUM_ELEMENTS]> for Word { + fn from(value: &[bool; Word::NUM_ELEMENTS]) -> Self { (*value).into() } } -impl From<[bool; WORD_SIZE_FELTS]> for Word { - fn from(value: [bool; WORD_SIZE_FELTS]) -> Self { +impl From<[bool; Word::NUM_ELEMENTS]> for Word { + fn from(value: [bool; Word::NUM_ELEMENTS]) -> Self { [value[0] as u32, value[1] as u32, value[2] as u32, value[3] as u32].into() } } -impl From<&[u8; WORD_SIZE_FELTS]> for Word { - fn from(value: &[u8; WORD_SIZE_FELTS]) -> Self { +impl From<&[u8; Word::NUM_ELEMENTS]> for Word { + fn from(value: &[u8; Word::NUM_ELEMENTS]) -> Self { (*value).into() } } -impl From<[u8; WORD_SIZE_FELTS]> for Word { - fn from(value: [u8; WORD_SIZE_FELTS]) -> Self { +impl From<[u8; Word::NUM_ELEMENTS]> for Word { + fn from(value: [u8; Word::NUM_ELEMENTS]) -> Self { Self::new([ Felt::from_u8(value[0]), Felt::from_u8(value[1]), @@ -549,14 +554,14 @@ impl From<[u8; WORD_SIZE_FELTS]> for Word { } } -impl From<&[u16; WORD_SIZE_FELTS]> for Word { - fn from(value: &[u16; WORD_SIZE_FELTS]) -> Self { +impl From<&[u16; Word::NUM_ELEMENTS]> for Word { + fn from(value: &[u16; Word::NUM_ELEMENTS]) -> Self { (*value).into() } } -impl From<[u16; WORD_SIZE_FELTS]> for Word { - fn from(value: [u16; WORD_SIZE_FELTS]) -> Self { +impl From<[u16; Word::NUM_ELEMENTS]> for Word { + fn from(value: [u16; Word::NUM_ELEMENTS]) -> Self { Self::new([ Felt::from_u16(value[0]), Felt::from_u16(value[1]), @@ -566,14 +571,14 @@ impl From<[u16; WORD_SIZE_FELTS]> for Word { } } -impl From<&[u32; WORD_SIZE_FELTS]> for Word { - fn from(value: &[u32; WORD_SIZE_FELTS]) -> Self { +impl From<&[u32; Word::NUM_ELEMENTS]> for Word { + fn from(value: &[u32; Word::NUM_ELEMENTS]) -> Self { (*value).into() } } -impl From<[u32; WORD_SIZE_FELTS]> for Word { - fn from(value: [u32; WORD_SIZE_FELTS]) -> Self { +impl From<[u32; Word::NUM_ELEMENTS]> for Word { + fn from(value: [u32; Word::NUM_ELEMENTS]) -> Self { Self::new([ Felt::from_u32(value[0]), Felt::from_u32(value[1]), @@ -583,18 +588,18 @@ impl From<[u32; WORD_SIZE_FELTS]> for Word { } } -impl TryFrom<&[u64; WORD_SIZE_FELTS]> for Word { +impl TryFrom<&[u64; Word::NUM_ELEMENTS]> for Word { type Error = WordError; - fn try_from(value: &[u64; WORD_SIZE_FELTS]) -> Result { + fn try_from(value: &[u64; Word::NUM_ELEMENTS]) -> Result { (*value).try_into() } } -impl TryFrom<[u64; WORD_SIZE_FELTS]> for Word { +impl TryFrom<[u64; Word::NUM_ELEMENTS]> for Word { type Error = WordError; - fn try_from(value: [u64; WORD_SIZE_FELTS]) -> Result { + fn try_from(value: [u64; Word::NUM_ELEMENTS]) -> Result { let err = || WordError::InvalidFieldElement("value >= field modulus".into()); Ok(Self::new([ Felt::from_canonical_checked(value[0]).ok_or_else(err)?, @@ -605,30 +610,30 @@ impl TryFrom<[u64; WORD_SIZE_FELTS]> for Word { } } -impl From<&[Felt; WORD_SIZE_FELTS]> for Word { - fn from(value: &[Felt; WORD_SIZE_FELTS]) -> Self { +impl From<&[Felt; Word::NUM_ELEMENTS]> for Word { + fn from(value: &[Felt; Word::NUM_ELEMENTS]) -> Self { Self::new(*value) } } -impl From<[Felt; WORD_SIZE_FELTS]> for Word { - fn from(value: [Felt; WORD_SIZE_FELTS]) -> Self { +impl From<[Felt; Word::NUM_ELEMENTS]> for Word { + fn from(value: [Felt; Word::NUM_ELEMENTS]) -> Self { Self::new(value) } } -impl TryFrom<&[u8; WORD_SIZE_BYTES]> for Word { +impl TryFrom<&[u8; Word::SERIALIZED_SIZE]> for Word { type Error = WordError; - fn try_from(value: &[u8; WORD_SIZE_BYTES]) -> Result { + fn try_from(value: &[u8; Word::SERIALIZED_SIZE]) -> Result { (*value).try_into() } } -impl TryFrom<[u8; WORD_SIZE_BYTES]> for Word { +impl TryFrom<[u8; Word::SERIALIZED_SIZE]> for Word { type Error = WordError; - fn try_from(value: [u8; WORD_SIZE_BYTES]) -> Result { + fn try_from(value: [u8; Word::SERIALIZED_SIZE]) -> Result { // Note: the input length is known, the conversion from slice to array must succeed so the // `unwrap`s below are safe let a = u64::from_le_bytes(value[0..8].try_into().unwrap()); @@ -650,9 +655,9 @@ impl TryFrom<&[u8]> for Word { type Error = WordError; fn try_from(value: &[u8]) -> Result { - let value: [u8; WORD_SIZE_BYTES] = value - .try_into() - .map_err(|_| WordError::InvalidInputLength("bytes", WORD_SIZE_BYTES, value.len()))?; + let value: [u8; Word::SERIALIZED_SIZE] = value.try_into().map_err(|_| { + WordError::InvalidInputLength("bytes", Word::SERIALIZED_SIZE, value.len()) + })?; value.try_into() } } @@ -661,9 +666,9 @@ impl TryFrom<&[Felt]> for Word { type Error = WordError; fn try_from(value: &[Felt]) -> Result { - let value: [Felt; WORD_SIZE_FELTS] = value - .try_into() - .map_err(|_| WordError::InvalidInputLength("elements", WORD_SIZE_FELTS, value.len()))?; + let value: [Felt; Word::NUM_ELEMENTS] = value.try_into().map_err(|_| { + WordError::InvalidInputLength("elements", Word::NUM_ELEMENTS, value.len()) + })?; Ok(value.into()) } } @@ -674,7 +679,7 @@ impl TryFrom<&str> for Word { /// Expects the string to start with `0x`. fn try_from(value: &str) -> Result { - crate::utils::hex_to_bytes::(value) + crate::utils::hex_to_bytes::<{ Word::SERIALIZED_SIZE }>(value) .map_err(WordError::HexParse) .and_then(Word::try_from) } @@ -717,7 +722,7 @@ impl Serializable for Word { #[cfg(not(all(target_family = "wasm", miden)))] impl Deserializable for Word { fn read_from(source: &mut R) -> Result { - let mut inner: [Felt; WORD_SIZE_FELTS] = [Felt::ZERO; WORD_SIZE_FELTS]; + let mut inner: [Felt; Word::NUM_ELEMENTS] = [Felt::ZERO; Word::NUM_ELEMENTS]; for inner in inner.iter_mut() { let e = source.read_u64()?; if e >= Felt::ORDER { @@ -725,7 +730,7 @@ impl Deserializable for Word { "value not in the appropriate range", ))); } - *inner = Felt::new(e); + *inner = Felt::new_unchecked(e); } Ok(Self::new(inner)) diff --git a/miden-field/src/word/tests.rs b/miden-field/src/word/tests.rs index 62488c089d..52d6d9fa35 100644 --- a/miden-field/src/word/tests.rs +++ b/miden-field/src/word/tests.rs @@ -4,14 +4,14 @@ use core::cmp::Ordering; use miden_serde_utils::SliceReader; use proptest::prelude::*; -use super::{Deserializable, Felt, Serializable, WORD_SIZE_BYTES, WORD_SIZE_FELTS, Word}; +use super::{Deserializable, Felt, Serializable, Word}; use crate::word; // TESTS // ================================================================================================ /// Returns a strategy which generates a `[u64; 4]` where all values are canonical field elements. -fn any_word_elements_u64_canonical() -> BoxedStrategy<[u64; WORD_SIZE_FELTS]> { +fn any_word_elements_u64_canonical() -> BoxedStrategy<[u64; Word::NUM_ELEMENTS]> { prop::array::uniform4(0u64..Felt::ORDER).no_shrink().boxed() } @@ -29,7 +29,7 @@ proptest! { fn word_serialization_roundtrip(word in any::()) { let mut bytes = Vec::new(); word.write_into(&mut bytes); - prop_assert_eq!(WORD_SIZE_BYTES, bytes.len()); + prop_assert_eq!(Word::SERIALIZED_SIZE, bytes.len()); prop_assert_eq!(bytes.len(), word.get_size_hint()); let mut reader = SliceReader::new(&bytes); @@ -46,72 +46,72 @@ proptest! { } #[test] - fn word_bool_conversion_roundtrip(v in any::<[bool; WORD_SIZE_FELTS]>()) { + fn word_bool_conversion_roundtrip(v in any::<[bool; Word::NUM_ELEMENTS]>()) { let word: Word = v.into(); - prop_assert_eq!(v, <[bool; WORD_SIZE_FELTS]>::try_from(word).unwrap()); + prop_assert_eq!(v, <[bool; Word::NUM_ELEMENTS]>::try_from(word).unwrap()); let word: Word = (&v).into(); - prop_assert_eq!(v, <[bool; WORD_SIZE_FELTS]>::try_from(&word).unwrap()); + prop_assert_eq!(v, <[bool; Word::NUM_ELEMENTS]>::try_from(&word).unwrap()); } #[test] - fn word_u8_conversion_roundtrip(v in any::<[u8; WORD_SIZE_FELTS]>()) { + fn word_u8_conversion_roundtrip(v in any::<[u8; Word::NUM_ELEMENTS]>()) { let word: Word = v.into(); - prop_assert_eq!(v, <[u8; WORD_SIZE_FELTS]>::try_from(word).unwrap()); + prop_assert_eq!(v, <[u8; Word::NUM_ELEMENTS]>::try_from(word).unwrap()); let word: Word = (&v).into(); - prop_assert_eq!(v, <[u8; WORD_SIZE_FELTS]>::try_from(&word).unwrap()); + prop_assert_eq!(v, <[u8; Word::NUM_ELEMENTS]>::try_from(&word).unwrap()); } #[test] - fn word_u16_conversion_roundtrip(v in any::<[u16; WORD_SIZE_FELTS]>()) { + fn word_u16_conversion_roundtrip(v in any::<[u16; Word::NUM_ELEMENTS]>()) { let word: Word = v.into(); - prop_assert_eq!(v, <[u16; WORD_SIZE_FELTS]>::try_from(word).unwrap()); + prop_assert_eq!(v, <[u16; Word::NUM_ELEMENTS]>::try_from(word).unwrap()); let word: Word = (&v).into(); - prop_assert_eq!(v, <[u16; WORD_SIZE_FELTS]>::try_from(&word).unwrap()); + prop_assert_eq!(v, <[u16; Word::NUM_ELEMENTS]>::try_from(&word).unwrap()); } #[test] - fn word_u32_conversion_roundtrip(v in any::<[u32; WORD_SIZE_FELTS]>()) { + fn word_u32_conversion_roundtrip(v in any::<[u32; Word::NUM_ELEMENTS]>()) { let word: Word = v.into(); - prop_assert_eq!(v, <[u32; WORD_SIZE_FELTS]>::try_from(word).unwrap()); + prop_assert_eq!(v, <[u32; Word::NUM_ELEMENTS]>::try_from(word).unwrap()); let word: Word = (&v).into(); - prop_assert_eq!(v, <[u32; WORD_SIZE_FELTS]>::try_from(&word).unwrap()); + prop_assert_eq!(v, <[u32; Word::NUM_ELEMENTS]>::try_from(&word).unwrap()); } #[test] fn word_u64_conversion_roundtrip(v in any_word_elements_u64_canonical()) { let word: Word = v.try_into().unwrap(); - let round_trip: [u64; WORD_SIZE_FELTS] = word.into(); + let round_trip: [u64; Word::NUM_ELEMENTS] = word.into(); prop_assert_eq!(v, round_trip); let word: Word = (&v).try_into().unwrap(); - let round_trip: [u64; WORD_SIZE_FELTS] = (&word).into(); + let round_trip: [u64; Word::NUM_ELEMENTS] = (&word).into(); prop_assert_eq!(v, round_trip); } #[test] fn word_felt_conversion_roundtrip(elements in prop::array::uniform4(any::())) { - let elements = elements.map(Felt::new); + let elements = elements.map(Felt::new_unchecked); let word: Word = elements.into(); - let round_trip: [Felt; WORD_SIZE_FELTS] = word.into(); + let round_trip: [Felt; Word::NUM_ELEMENTS] = word.into(); prop_assert_eq!(elements, round_trip); let word: Word = (&elements).into(); - let round_trip: [Felt; WORD_SIZE_FELTS] = (&word).into(); + let round_trip: [Felt; Word::NUM_ELEMENTS] = (&word).into(); prop_assert_eq!(elements, round_trip); } #[test] fn word_bytes_conversion_roundtrip(word in any::()) { - let bytes: [u8; WORD_SIZE_BYTES] = word.into(); + let bytes: [u8; Word::SERIALIZED_SIZE] = word.into(); let round_trip: Word = bytes.try_into().unwrap(); prop_assert_eq!(word, round_trip); - let bytes: [u8; WORD_SIZE_BYTES] = (&word).into(); + let bytes: [u8; Word::SERIALIZED_SIZE] = (&word).into(); let round_trip: Word = (&bytes).try_into().unwrap(); prop_assert_eq!(word, round_trip); } @@ -135,10 +135,23 @@ proptest! { #[test] fn word_elements_array_layout_roundtrip() { - let mut word = Word::new([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]); + let mut word = Word::new([ + Felt::new_unchecked(1), + Felt::new_unchecked(2), + Felt::new_unchecked(3), + Felt::new_unchecked(4), + ]); let elements = word.as_elements_array(); - assert_eq!(elements, &[Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]); + assert_eq!( + elements, + &[ + Felt::new_unchecked(1), + Felt::new_unchecked(2), + Felt::new_unchecked(3), + Felt::new_unchecked(4) + ] + ); let base = core::ptr::addr_of!(word.a); assert_eq!(elements.as_ptr(), base); @@ -147,28 +160,28 @@ fn word_elements_array_layout_roundtrip() { assert_eq!(core::ptr::addr_of!(word.d), unsafe { base.add(3) }); let elements_mut = word.as_elements_array_mut(); - elements_mut[2] = Felt::new(42); - assert_eq!(word.c, Felt::new(42)); + elements_mut[2] = Felt::new_unchecked(42); + assert_eq!(word.c, Felt::new_unchecked(42)); } proptest! { #[test] fn word_index_matches_into_elements(word in any::()) { let elements = word.into_elements(); - for idx in 0..WORD_SIZE_FELTS { + for idx in 0..Word::NUM_ELEMENTS { prop_assert_eq!(word[idx], elements[idx]); } } #[test] - fn word_index_mut_updates_all_elements(word in any::(), values in any::<[u64; WORD_SIZE_FELTS]>()) { + fn word_index_mut_updates_all_elements(word in any::(), values in any::<[u64; Word::NUM_ELEMENTS]>()) { let mut word = word; let mut expected = word.into_elements(); - for idx in 0..WORD_SIZE_FELTS { + for idx in 0..Word::NUM_ELEMENTS { let value = values[idx]; - expected[idx] = Felt::new(value); - word[idx] = Felt::new(value); + expected[idx] = Felt::new_unchecked(value); + word[idx] = Felt::new_unchecked(value); } prop_assert_eq!(word.into_elements(), expected); } @@ -176,7 +189,7 @@ proptest! { #[test] fn word_index_mut_range_updates_slice(word in any::(), v0 in any::(), v1 in any::()) { let mut word = word; - let expected = [Felt::new(v0), Felt::new(v1)]; + let expected = [Felt::new_unchecked(v0), Felt::new_unchecked(v1)]; word[1..3].copy_from_slice(&expected); prop_assert_eq!(word[1], expected[0]); @@ -218,17 +231,17 @@ fn word_macro(#[case] input: &str) { // Right pad to 64 hex digits (66 including prefix). This is required by the // Word::try_from(String) implementation. let padded_input = format!("{input:<66}").replace(" ", "0"); - let expected = crate::Word::try_from(padded_input.as_str()).unwrap(); + let expected = Word::try_from(padded_input.as_str()).unwrap(); assert_eq!(uut, expected); } #[rstest::rstest] -#[case::first_nibble("0x1000000000000000000000000000000000000000000000000000000000000000", crate::Word::new([Felt::new(16), Felt::new(0), Felt::new(0), Felt::new(0)]))] -#[case::second_nibble("0x0100000000000000000000000000000000000000000000000000000000000000", crate::Word::new([Felt::new(1), Felt::new(0), Felt::new(0), Felt::new(0)]))] -#[case::all_first_nibbles("0x1000000000000000100000000000000010000000000000001000000000000000", crate::Word::new([Felt::new(16), Felt::new(16), Felt::new(16), Felt::new(16)]))] -#[case::all_first_nibbles_asc("0x1000000000000000200000000000000030000000000000004000000000000000", crate::Word::new([Felt::new(16), Felt::new(32), Felt::new(48), Felt::new(64)]))] -fn word_macro_endianness(#[case] input: &str, #[case] expected: crate::Word) { +#[case::first_nibble("0x1000000000000000000000000000000000000000000000000000000000000000", Word::new([Felt::new_unchecked(16), Felt::new_unchecked(0), Felt::new_unchecked(0), Felt::new_unchecked(0)]))] +#[case::second_nibble("0x0100000000000000000000000000000000000000000000000000000000000000", Word::new([Felt::new_unchecked(1), Felt::new_unchecked(0), Felt::new_unchecked(0), Felt::new_unchecked(0)]))] +#[case::all_first_nibbles("0x1000000000000000100000000000000010000000000000001000000000000000", Word::new([Felt::new_unchecked(16), Felt::new_unchecked(16), Felt::new_unchecked(16), Felt::new_unchecked(16)]))] +#[case::all_first_nibbles_asc("0x1000000000000000200000000000000030000000000000004000000000000000", Word::new([Felt::new_unchecked(16), Felt::new_unchecked(32), Felt::new_unchecked(48), Felt::new_unchecked(64)]))] +fn word_macro_endianness(#[case] input: &str, #[case] expected: Word) { let uut = word!(input); assert_eq!(uut, expected); } @@ -248,7 +261,7 @@ proptest! { map.insert(word, 1); // Round-trip via bytes to create an equivalent key. - let bytes: [u8; WORD_SIZE_BYTES] = word.into(); + let bytes: [u8; Word::SERIALIZED_SIZE] = word.into(); let key2: Word = bytes.try_into().unwrap(); prop_assert_eq!(word, key2); diff --git a/miden-serde-utils/Cargo.toml b/miden-serde-utils/Cargo.toml index 677ef635df..005cbcea93 100644 --- a/miden-serde-utils/Cargo.toml +++ b/miden-serde-utils/Cargo.toml @@ -1,7 +1,7 @@ [package] authors.workspace = true categories.workspace = true -description = "Serialization/deserialization utilities for Miden" +description = "Serialization/deserialization utilities for the Miden project" documentation = "https://docs.rs/miden-serde-utils" edition.workspace = true keywords.workspace = true @@ -17,8 +17,8 @@ default = ["std"] std = [] [dependencies] -p3-field = { default-features = false, version = "0.5.0" } -p3-goldilocks = { default-features = false, version = "0.5.0" } +p3-field.workspace = true +p3-goldilocks.workspace = true [lints] workspace = true diff --git a/miden-serde-utils/README.md b/miden-serde-utils/README.md index 3e0a72c85a..21d69ebd31 100644 --- a/miden-serde-utils/README.md +++ b/miden-serde-utils/README.md @@ -1,18 +1,19 @@ # Miden Serialization Utilities -This crate provides serialization and deserialization utilities for Miden projects. +This crate provides serialization and deserialization utilities for the Miden projects. ## Features -- `ByteReader` trait for reading primitive values from byte sources -- `ByteWriter` trait for writing primitive values to byte sinks -- `Serializable` and `Deserializable` traits for custom types -- Support for both `std` and `no_std` environments +- `Serializable` and `Deserializable` traits for custom types. +- `ByteWriter` trait for writing primitive values to byte sinks. +- `ByteReader` trait for reading primitive values from byte sources. +- `SliceReader` struct - a reader implementation for reading `Deserializable` from a slice of bytes. +- `BudgetedReader` struct - a reader implementation that enforces a byte budget during deserialization. +- Support for both `std` and `no_std` environments. ## Crate Features -- `std` - enabled by default; enables standard library support -- `winter-compat` - provides `Serializable` and `Deserializable` implementations for types from the `winter-math` and `winter-utils` crates (specifically for `Felt` field elements). This feature exists to work around Rust's orphan rule, which prevents implementing external traits on external types. By implementing these traits in this intermediate crate, both Miden and Winter ecosystem crates can use a common serialization interface +- `std` - enabled by default; enables standard library support. ## License diff --git a/miden-serde-utils/fuzz/Cargo.lock b/miden-serde-utils/fuzz/Cargo.lock index a6e59a05eb..6104a30fa2 100644 --- a/miden-serde-utils/fuzz/Cargo.lock +++ b/miden-serde-utils/fuzz/Cargo.lock @@ -102,10 +102,10 @@ dependencies = [ [[package]] name = "miden-serde-utils" -version = "0.21.0" +version = "0.24.0" dependencies = [ "p3-field", - "p3-miden-goldilocks", + "p3-goldilocks", ] [[package]] @@ -114,7 +114,7 @@ version = "0.0.0" dependencies = [ "libfuzzer-sys", "miden-serde-utils", - "p3-miden-goldilocks", + "p3-goldilocks", ] [[package]] @@ -147,9 +147,9 @@ dependencies = [ [[package]] name = "p3-challenger" -version = "0.4.2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20e42ba74a49c08c6e99f74cd9b343bfa31aa5721fea55079b18e3fd65f1dcbc" +checksum = "4a0b490c745a7d2adeeafff06411814c8078c432740162332b3cd71be0158a76" dependencies = [ "p3-field", "p3-maybe-rayon", @@ -161,9 +161,9 @@ dependencies = [ [[package]] name = "p3-dft" -version = "0.4.2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e63fa5eb1bd12a240089e72ae3fe10350944d9c166d00a3bfd2a1794db65cf5c" +checksum = "55301e91544440254977108b85c32c09d7ea05f2f0dd61092a2825339906a4a7" dependencies = [ "itertools", "p3-field", @@ -176,9 +176,9 @@ dependencies = [ [[package]] name = "p3-field" -version = "0.4.2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ebfdb6ef992ae64e9e8f449ac46516ffa584f11afbdf9ee244288c2a633cdf4" +checksum = "85affca7fc983889f260655c4cf74163eebb94605f702e4b6809ead707cba54f" dependencies = [ "itertools", "num-bigint", @@ -190,11 +190,31 @@ dependencies = [ "tracing", ] +[[package]] +name = "p3-goldilocks" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca1081f5c47b940f2d75a11c04f62ea1cc58a5d480dd465fef3861c045c63cd" +dependencies = [ + "num-bigint", + "p3-challenger", + "p3-dft", + "p3-field", + "p3-mds", + "p3-poseidon1", + "p3-poseidon2", + "p3-symmetric", + "p3-util", + "paste", + "rand", + "serde", +] + [[package]] name = "p3-matrix" -version = "0.4.2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5542f96504dae8100c91398fb1e3f5ec669eb9c73d9e0b018a93b5fe32bad230" +checksum = "53428126b009071563d1d07305a9de8be0d21de00b57d2475289ee32ffca6577" dependencies = [ "itertools", "p3-field", @@ -203,20 +223,19 @@ dependencies = [ "rand", "serde", "tracing", - "transpose", ] [[package]] name = "p3-maybe-rayon" -version = "0.4.2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e5669ca75645f99cd001e9d0289a4eeff2bc2cd9dc3c6c3aaf22643966e83df" +checksum = "082bf467011c06c768c579ec6eb9accb5e1e62108891634cc770396e917f978a" [[package]] name = "p3-mds" -version = "0.4.2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038763af23df9da653065867fd85b38626079031576c86fd537097e5be6a0da0" +checksum = "35209e6214102ea6ec6b8cb1b9c15a9b8e597a39f9173597c957f123bced81b3" dependencies = [ "p3-dft", "p3-field", @@ -226,53 +245,45 @@ dependencies = [ ] [[package]] -name = "p3-miden-goldilocks" -version = "0.4.0" +name = "p3-monty-31" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a883bc797df1c43b06f2d467340625ee736f748928d1ceb402d27eb05e694394" +checksum = "ffa8c99ec50c035020bbf5457c6a729ba6a975719c1a8dd3f16421081e4f650c" dependencies = [ + "itertools", "num-bigint", - "p3-challenger", "p3-dft", "p3-field", + "p3-matrix", + "p3-maybe-rayon", "p3-mds", + "p3-poseidon1", "p3-poseidon2", "p3-symmetric", "p3-util", "paste", "rand", "serde", + "spin", + "tracing", ] [[package]] -name = "p3-monty-31" -version = "0.4.2" +name = "p3-poseidon1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a981d60da3d8cbf8561014e2c186068578405fd69098fa75b43d4afb364a47" +checksum = "6a018b618e3fa0aec8be933b1d8e404edd23f46991f6bf3f5c2f3f95e9413fe9" dependencies = [ - "itertools", - "num-bigint", - "p3-dft", "p3-field", - "p3-matrix", - "p3-maybe-rayon", - "p3-mds", - "p3-poseidon2", "p3-symmetric", - "p3-util", - "paste", "rand", - "serde", - "spin", - "tracing", - "transpose", ] [[package]] name = "p3-poseidon2" -version = "0.4.2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "903b73e4f9a7781a18561c74dc169cf03333497b57a8dd02aaeb130c0f386599" +checksum = "256a668a9ba916f8767552f13d0ba50d18968bc74a623bfdafa41e2970c944d0" dependencies = [ "p3-field", "p3-mds", @@ -283,22 +294,24 @@ dependencies = [ [[package]] name = "p3-symmetric" -version = "0.4.2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd788f04e86dd5c35dd87cad29eefdb6371d2fd5f7664451382eeacae3c3ed0" +checksum = "6c60a71a1507c13611b0f2b0b6e83669fd5b76f8e3115bcbced5ccfdf3ca7807" dependencies = [ "itertools", "p3-field", + "p3-util", "serde", ] [[package]] name = "p3-util" -version = "0.4.2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "663b16021930bc600ecada915c6c3965730a3b9d6a6c23434ccf70bfc29d6881" +checksum = "f8b766b9e9254bf3fa98d76e42cf8a5b30628c182dfd5272d270076ee12f0fc0" dependencies = [ "serde", + "transpose", ] [[package]] @@ -339,18 +352,18 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" -version = "0.9.2" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "rand_core", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" [[package]] name = "scopeguard" diff --git a/miden-serde-utils/fuzz/Cargo.toml b/miden-serde-utils/fuzz/Cargo.toml index 406ba583fc..1bc6d7d522 100644 --- a/miden-serde-utils/fuzz/Cargo.toml +++ b/miden-serde-utils/fuzz/Cargo.toml @@ -11,7 +11,7 @@ cargo-fuzz = true [dependencies] libfuzzer-sys = "0.4" -p3-goldilocks = { default-features = false, version = "0.4.2" } +p3-goldilocks = { default-features = false, version = "0.5" } [dependencies.miden-serde-utils] path = ".." diff --git a/miden-serde-utils/src/byte_reader.rs b/miden-serde-utils/src/byte_reader.rs index 4dfd538b09..747c011e51 100644 --- a/miden-serde-utils/src/byte_reader.rs +++ b/miden-serde-utils/src/byte_reader.rs @@ -308,7 +308,7 @@ pub struct ReadAdapter<'a> { // // By default we attempt to satisfy reads from `reader` directly, but that is not always // possible. - buf: alloc::vec::Vec, + buf: Vec, // The position in `buf` at which we should start reading the next byte, when `buf` is // non-empty. pos: usize, @@ -913,6 +913,7 @@ impl ByteReader for BudgetedReader { #[cfg(all(test, feature = "std"))] mod tests { + use core::mem::size_of; use std::io::Cursor; use super::*; @@ -964,7 +965,7 @@ mod tests { const VALUE: usize = 2048; // Write VALUE to storage - let mut cursor = Cursor::new([0; core::mem::size_of::()]); + let mut cursor = Cursor::new([0; size_of::()]); cursor.write_usize(VALUE); // Read VALUE from storage @@ -1047,7 +1048,7 @@ mod tests { // reading STR_BYTES, so the total size of our adapter's buffer should be // 496 + STR_BYTES.len() + size_of::(); assert_eq!(reader.read_slice(STR_BYTES.len()).unwrap(), STR_BYTES); - assert_eq!(reader.buf.len(), 496 + STR_BYTES.len() + core::mem::size_of::()); + assert_eq!(reader.buf.len(), 496 + STR_BYTES.len() + size_of::()); // We haven't read the u32 yet assert_eq!(reader.pos, 509); assert_eq!(reader.read_u32().unwrap(), 0xbeef); @@ -1361,7 +1362,7 @@ mod tests { // Serialized: 1 byte for u8 + 8 bytes for u64 = 9 bytes // In-memory: 8 bytes for u8 (with 7 bytes padding) + 8 bytes for u64 = 16 bytes assert_eq!(<(u8, u64)>::min_serialized_size(), 9); - assert_eq!(core::mem::size_of::<(u8, u64)>(), 16); + assert_eq!(size_of::<(u8, u64)>(), 16); // Verify budget calculation uses 9, not 16 let mut data = Vec::new(); diff --git a/miden-serde-utils/src/lib.rs b/miden-serde-utils/src/lib.rs index bcd484092e..47899d24c6 100644 --- a/miden-serde-utils/src/lib.rs +++ b/miden-serde-utils/src/lib.rs @@ -11,8 +11,10 @@ use alloc::{ collections::{BTreeMap, BTreeSet}, format, string::String, + sync::Arc, vec::Vec, }; +use core::mem::size_of; // ERROR // ================================================================================================ @@ -35,8 +37,8 @@ impl core::fmt::Display for DeserializationError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::UnexpectedEOF => write!(f, "unexpected end of file"), - Self::InvalidValue(msg) => write!(f, "invalid value: {}", msg), - Self::UnknownError(msg) => write!(f, "unknown error: {}", msg), + Self::InvalidValue(msg) => write!(f, "invalid value: {msg}"), + Self::UnknownError(msg) => write!(f, "unknown error: {msg}"), } } } @@ -221,7 +223,7 @@ impl Serializable for u8 { } fn get_size_hint(&self) -> usize { - core::mem::size_of::() + size_of::() } } @@ -231,7 +233,7 @@ impl Serializable for u16 { } fn get_size_hint(&self) -> usize { - core::mem::size_of::() + size_of::() } } @@ -241,7 +243,7 @@ impl Serializable for u32 { } fn get_size_hint(&self) -> usize { - core::mem::size_of::() + size_of::() } } @@ -251,7 +253,7 @@ impl Serializable for u64 { } fn get_size_hint(&self) -> usize { - core::mem::size_of::() + size_of::() } } @@ -261,7 +263,7 @@ impl Serializable for u128 { } fn get_size_hint(&self) -> usize { - core::mem::size_of::() + size_of::() } } @@ -287,7 +289,7 @@ impl Serializable for Option { } fn get_size_hint(&self) -> usize { - core::mem::size_of::() + self.as_ref().map(|value| value.get_size_hint()).unwrap_or(0) + size_of::() + self.as_ref().map(Serializable::get_size_hint).unwrap_or(0) } } @@ -380,12 +382,21 @@ impl Serializable for str { impl Serializable for String { fn write_into(&self, target: &mut W) { - target.write_usize(self.len()); - target.write_many(self.as_bytes()); + self.as_str().write_into(target); } fn get_size_hint(&self) -> usize { - self.len().get_size_hint() + self.len() + self.as_str().get_size_hint() + } +} + +impl Serializable for Arc { + fn write_into(&self, target: &mut W) { + self.as_ref().write_into(target); + } + + fn get_size_hint(&self) -> usize { + self.as_ref().get_size_hint() } } @@ -419,7 +430,7 @@ pub trait Deserializable: Sized { /// Override this method for types where the serialized representation is smaller than /// the in-memory representation to allow more elements to be deserialized. fn min_serialized_size() -> usize { - core::mem::size_of::() + size_of::() } // PROVIDED METHODS @@ -711,6 +722,16 @@ impl Deserializable for String { } } +impl Deserializable for Arc { + fn read_from(source: &mut R) -> Result { + String::read_from(source).map(Arc::from) + } + + fn min_serialized_size() -> usize { + 1 // minimum vint length prefix + } +} + // GOLDILOCKS FIELD ELEMENT IMPLEMENTATIONS // ================================================================================================ @@ -721,7 +742,7 @@ impl Serializable for p3_goldilocks::Goldilocks { } fn get_size_hint(&self) -> usize { - core::mem::size_of::() + size_of::() } } @@ -732,9 +753,81 @@ impl Deserializable for p3_goldilocks::Goldilocks { let value = source.read_u64()?; Self::from_canonical_checked(value).ok_or_else(|| { DeserializationError::InvalidValue(format!( - "value {} is not a valid Goldilocks field element", - value + "value {value} is not a valid Goldilocks field element" )) }) } } + +#[cfg(test)] +mod tests { + use alloc::sync::Arc; + + use super::*; + + #[test] + fn arc_str_roundtrip() { + let original: Arc = Arc::from("hello world"); + let bytes = original.to_bytes(); + let deserialized = Arc::::read_from_bytes(&bytes).unwrap(); + assert_eq!(original, deserialized); + } + + #[test] + fn string_roundtrip() { + let original = String::from("hello world"); + let bytes = original.to_bytes(); + let deserialized = String::read_from_bytes(&bytes).unwrap(); + assert_eq!(original, deserialized); + } + + #[test] + fn empty_string_roundtrip() { + let arc: Arc = Arc::from(""); + let bytes = arc.to_bytes(); + let deserialized = Arc::::read_from_bytes(&bytes).unwrap(); + assert_eq!(deserialized, Arc::from("")); + + let string = String::from(""); + let bytes = string.to_bytes(); + let deserialized = String::read_from_bytes(&bytes).unwrap(); + assert_eq!(deserialized, ""); + } + + #[test] + fn multibyte_utf8_roundtrip() { + let text = "héllo 🌍"; + + let arc: Arc = Arc::from(text); + let bytes = arc.to_bytes(); + let deserialized = Arc::::read_from_bytes(&bytes).unwrap(); + assert_eq!(&*deserialized, text); + + let string = String::from(text); + let bytes = string.to_bytes(); + let deserialized = String::read_from_bytes(&bytes).unwrap(); + assert_eq!(deserialized, text); + + // Cross-compat: Arc bytes can be read as String and vice versa + let arc_bytes = Arc::::from(text).to_bytes(); + let string_bytes = String::from(text).to_bytes(); + assert_eq!(arc_bytes, string_bytes); + assert_eq!(String::read_from_bytes(&arc_bytes).unwrap(), text); + assert_eq!(&*Arc::::read_from_bytes(&string_bytes).unwrap(), text); + } + + #[test] + fn arc_str_string_cross_compat() { + // Arc -> bytes -> String + let arc: Arc = Arc::from("cross type"); + let bytes = arc.to_bytes(); + let as_string = String::read_from_bytes(&bytes).unwrap(); + assert_eq!(as_string, "cross type"); + + // String -> bytes -> Arc + let string = String::from("other direction"); + let bytes = string.to_bytes(); + let as_arc = Arc::::read_from_bytes(&bytes).unwrap(); + assert_eq!(&*as_arc, "other direction"); + } +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 81d504e3f6..913aac09cf 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] -channel = "1.90" +channel = "1.94" components = ["clippy", "rust-src", "rustfmt"] profile = "minimal" targets = ["wasm32-unknown-unknown", "wasm32-wasip2"] diff --git a/scripts/check-features.sh b/scripts/check-features.sh index 10c17940c4..e916a995f6 100755 --- a/scripts/check-features.sh +++ b/scripts/check-features.sh @@ -2,17 +2,15 @@ set -euo pipefail -# Script to check all feature combinations compile without warnings -# This script ensures that warnings are treated as errors for CI +# Script to check all feature combinations compile without warnings. +# This script ensures that warnings are treated as errors for CI. echo "Checking all feature combinations with cargo-hack..." -# Set environment variables to treat warnings as errors +# Set environment variables to treat warnings as errors. export RUSTFLAGS="-D warnings" +export MIDEN_BUILD_LIB_DOCS=1 -# Run cargo-hack with comprehensive feature checking -# Note: We exclude 'default' to test non-default feature combinations -# and use --each-feature to test each feature individually cargo hack check \ --workspace \ --each-feature \ @@ -20,4 +18,10 @@ cargo hack check \ --all-targets echo "" +echo "Checking targeted multi-feature combinations..." + +# `cargo hack --each-feature` does not cover combinations like +# `miden-lifted-stark/testing,parallel`. +cargo check -p miden-lifted-stark --all-targets --features testing,parallel + echo "All feature combinations compiled successfully!" diff --git a/stark/CHANGELOG.md b/stark/CHANGELOG.md new file mode 100644 index 0000000000..3ec81fb93c --- /dev/null +++ b/stark/CHANGELOG.md @@ -0,0 +1,48 @@ +## Unreleased + +- Consolidated `p3-miden-lmcs`, `p3-miden-lifted-fri`, `p3-miden-dev-utils`, and `p3-miden-lifted-examples` into `miden-lifted-stark`; extracted profiling binary into `miden-bench` ([#66](https://github.com/0xMiden/p3-miden/pull/66)). +- Dropped BabyBear support; simplified tests, benchmarks, and dev-utils to Goldilocks-only ([#52](https://github.com/0xMiden/p3-miden/pull/52)). +- Added crate-local `testing` modules to `p3-miden-lmcs` and `p3-miden-lifted-fri` behind a `testing` feature flag ([#52](https://github.com/0xMiden/p3-miden/pull/52)). +- Moved `p3-miden-lifted-examples` from `[[example]]` to `[[bin]]` entries ([#52](https://github.com/0xMiden/p3-miden/pull/52)). +- [BREAKING] Restructured LMCS: removed `mmcs/` module and `serde` dependency, added `TreeIndices`, `MerkleWitness`, `NodeId`, `RowList` proof types ([#52](https://github.com/0xMiden/p3-miden/pull/52)). +- [BREAKING] LMCS tree now indexed by domain order; `Lmcs::build_tree`/`build_aligned_tree` require `BitReversibleMatrix` inputs and store `M::BitRev` ([#52](https://github.com/0xMiden/p3-miden/pull/52)). +- Removed `reverse_bits_len` from PCS query sampling, DEEP verifier, and FRI verifier ([#52](https://github.com/0xMiden/p3-miden/pull/52)). +- perf: faster constraint evaluation for wide matrices ([#57](https://github.com/0xMiden/p3-miden/pull/57)). +- Added info-level tracing spans to prover path: per-trace LDE, quotient iDFT/scaling/DFT; promoted `eval_instance`, `compress tree layers`, and `build aux traces` from debug to info ([#61](https://github.com/0xMiden/p3-miden/pull/61)). +- feat: add support for Blake3-192 ([#59](https://github.com/0xMiden/p3-miden/pull/59)) + +## 0.5.0 (2026-03-10) + +- Fixed periodic column evaluation on LDE/quotient domains. +- [BREAKING] Removed forced conversion of periodic values from F to EF. +- Added Lifted STARK implementation ([#17](https://github.com/0xMiden/p3-miden/pull/17)). +- Fixed length issue in boundary data length check ([#21](https://github.com/0xMiden/p3-miden/pull/21)). +- [BREAKING] Decoupled aux trace building from `LiftedAir` into standalone `AuxBuilder` trait and made auxiliary trace mandatory ([#35](https://github.com/0xMiden/p3-miden/pull/35)). +- [BREAKING] Incremented Plonky3 dependencies to v0.5.0 ([#34](https://github.com/0xMiden/p3-miden/pull/34)). + +## 0.4.2 (2026-01-14) + +- Added `p3-miden-lifted-fri` crate with Lifted FRI PCS (DEEP quotient + FRI), added `p3-miden-symmetric` crate with `StatefulHasher` trait for incremental hashing (#10). +- [BREAKING] Removed `p3-miden-goldilocks` crate, now uses upstream `p3-goldilocks` (#3). +- Updated `Pcs` trait implementation for Plonky3 v0.4.2 compatibility (#3). +- Updated Plonky3 dependencies to v0.4.2 (#3). +- Handle aux boundary values constraints in prover and verifier (#7). +- Fixed panics in verifier (#19). + +## 0.4.0 (2025-12-23) + +- Initial release on crates.io containing Miden-specific Plonky3 crates. +- [BREAKING] Consolidated crates and removed duplicate symbolic modules to use base Plonky3 (#1). +- Added workspace release automation with dry-run and publish workflows. +- Migrated Plonky3 dependencies from git to crates.io v0.4.1 (#1). +- Added README documenting the five Miden-specific Plonky3 crates. +- Added dual MIT/Apache-2.0 license. +- Added CI workflows and Makefile for build automation. +- Fixed debug constraint checking to be gated behind `cfg(debug_assertions)`. + +### Crates included + +- `p3-miden-air`: Miden-specific AIR abstractions. +- `p3-miden-fri`: Miden FRI implementation with hiding commitments. +- `p3-miden-prover`: Miden prover with constraint checking. +- `p3-miden-uni-stark`: Miden uni-STARK implementation. diff --git a/stark/LICENSE-APACHE b/stark/LICENSE-APACHE new file mode 100644 index 0000000000..5d225f9e1a --- /dev/null +++ b/stark/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2025 Miden + +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. diff --git a/stark/LICENSE-MIT b/stark/LICENSE-MIT new file mode 100644 index 0000000000..d7b28795b1 --- /dev/null +++ b/stark/LICENSE-MIT @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2025 Miden + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/stark/README.md b/stark/README.md new file mode 100644 index 0000000000..029c1de21d --- /dev/null +++ b/stark/README.md @@ -0,0 +1,65 @@ +# Miden Plonky3 + +Miden-specific [Plonky3](https://github.com/Plonky3/Plonky3) crates. + +The current focus of this workspace is a *lifted STARK* prover/verifier stack: +multi-trace proofs where traces of different heights are presented to the PCS +and verifier as a single uniform-height object via virtual lifting. + +## Lifted STARK Stack + +```text +miden-lifted-stark (prover, verifier, PCS, LMCS, shared types) +├── miden-lifted-air (AIR traits + symbolic analysis) +├── miden-stark-transcript (Fiat-Shamir channels) +├── miden-stateful-hasher (stateful hashers for LMCS) +└── miden-bench (profiling binary) +``` + +## Workspace Crates + +| Crate | Purpose | +|------|---------| +| `miden-lifted-stark` | Lifted STARK prover, verifier, PCS, LMCS, and shared types | +| `miden-lifted-air` | Lifted AIR traits and symbolic constraint analysis | +| `miden-stark-transcript` | Transcript channels (`ProverTranscript`, `VerifierTranscript`) | +| `miden-stateful-hasher` | Stateful hashers used by LMCS | +| `miden-bench` | Profiling binary for lifted and batch STARK runs | + +## Docs + +- `docs/faq.md` (architecture Q&A) +- `docs/lifting.md` (math background for lifting) +- `SECURITY.md` (audit/review guide; transcript and composition notes) + +## Where To Start (Code) + +- Protocol flow: `miden-lifted-stark/src/prover/mod.rs` and `miden-lifted-stark/src/verifier/mod.rs` +- PCS layer: `miden-lifted-stark/src/pcs/prover.rs` and `miden-lifted-stark/src/pcs/verifier.rs` +- Commitment layer: `miden-lifted-stark/src/lmcs/mod.rs` and `miden-lifted-stark/src/lmcs/lifted_tree.rs` +- Math background: `docs/lifting.md` + +## Build / Test + +```bash +make check +make test +make test-parallel +make lint +make doc +``` + +## Run An Example + +```bash +cargo run -p miden-bench --features parallel --release -- keccak:15 +``` + +## Security Disclaimer + +This code is research/prototype quality and has not been independently audited. +Do not treat any default parameters as production-ready. + +## License + +Any contribution intentionally submitted for inclusion in this repository, as defined in the Apache-2.0 license, shall be dual licensed under the [MIT](LICENSE-MIT) and [Apache 2.0](LICENSE-APACHE) licenses, without any additional terms or conditions. diff --git a/stark/SECURITY.md b/stark/SECURITY.md new file mode 100644 index 0000000000..da57e3a1fa --- /dev/null +++ b/stark/SECURITY.md @@ -0,0 +1,274 @@ +# Security Review Guide + +This document is a practical review guide for the *lifted STARK* stack in this +workspace. + +It is written for auditors and maintainers who want to understand the trust +boundaries, transcript/canonicality rules, and "what can go wrong" invariants. + +This code has not been independently audited. + +## High-Risk Items (Read These First) + +- Transcript "observed vs unobserved" split: `miden-stark-transcript/src/prover.rs` and `miden-stark-transcript/src/verifier.rs` +- LMCS batch opening verification and sibling order: `miden-lifted-stark/src/lmcs/mod.rs` +- DEEP reduction and domain-point reconstruction: `miden-lifted-stark/src/pcs/deep/verifier.rs` +- FRI round loop (index shifting, `s_inv` computation, final poly check): `miden-lifted-stark/src/pcs/fri/verifier.rs` +- STARK boundary canonicality and OOD identity check: `miden-lifted-stark/src/verifier/mod.rs` + +## Protocol Hierarchy (This Workspace) + +```text +LMCS -> DEEP + FRI -> PCS (lifted-fri) -> Lifted STARK (lifted-stark) +``` + +- **LMCS** (`miden-lifted-stark/src/lmcs`): Merkle commitments + batch openings for multiple + matrices, presented as a uniform-height view via virtual upsampling. +- **DEEP** (`miden-lifted-stark/src/pcs/deep`): batches OOD evaluation claims into a + single quotient polynomial. +- **FRI** (`miden-lifted-stark/src/pcs/fri`): low-degree testing of that quotient. +- **PCS** (`miden-lifted-stark/src/pcs`): wires DEEP + FRI together and drives query + sampling/opening. +- **Lifted STARK** (`miden-lifted-stark/src/{prover,verifier}`): commits traces/aux/Q, + samples STARK challenges, evaluates constraints OOD, and checks the quotient + identity. + +## Threat Model and Trust Boundaries + +Assume: + +- The attacker controls all proof/transcript bytes. +- Hash/compression primitives are collision-resistant. +- Fiat-Shamir is modeled as a random oracle (or an appropriate heuristic). + +Caller-provided "statement data" is *not* consistently observed into the +challenger by these libraries. In particular, **public inputs** are passed +out-of-band to prover/verifier APIs. If an input can vary per statement, the +application must bind it into Fiat-Shamir on *both* prover and verifier. + +## Normative Requirements (MUST) + +These are requirements on *applications* composing these crates. + +- You MUST bind all per-statement out-of-band inputs (notably `public_values`, + AIR identity/version tags, commitment roots, widths/heights metadata, and any + statement metadata) into the Fiat-Shamir challenger state, identically on both + prover and verifier. +- You MUST enforce transcript boundaries / canonicality at the protocol boundary. + The lifted STARK verifier rejects trailing data; if you compose the PCS + separately, use `verify_strict` or check `channel.is_empty()` at + the outer layer. +- You MUST cap proof sizes / transcript lengths when deserializing from bytes. + These libraries operate on already-deserialized streams and do not enforce + global size limits. +- You MUST ensure evaluation points used by DEEP/PCS lie outside the trace + subgroup `H` and outside the LDE coset `gK`. +- You MUST only use LMCS lifting with AIRs that are compatible with the lifted + view (see `docs/lifting.md`). + +Concrete examples of statement data that the application must treat explicitly: + +- `public_values` +- AIR identity / version tags (if multiple AIRs exist) +- configuration choices not already committed inside the transcript + +## Transcript Model (Observed vs Hinted) + +`miden-stark-transcript` stores two streams (fields and commitments) and provides +two kinds of writes/reads: + +- **Observed** (`send_*` / `receive_*`): data is appended/consumed and fed into + the challenger state. +- **Hints** (`hint_*` / `receive_hint_*`): data is appended/consumed but is *not* + observed into the challenger. LMCS openings are hints. + +This split is security-critical: + +- Anything that must affect challenge sampling must be **observed**. +- Hints are only safe for data whose integrity is checked cryptographically + against an already-observed commitment. + +Important detail: PoW witnesses produced/consumed by `grind` are also **unobserved**. +They are stored in the transcript field stream but are not absorbed into the +challenger state. + +## Canonicality / Proof Malleability + +The lifted STARK verifier (`miden-lifted-stark`) rejects trailing +transcript data. + +The PCS verifier (`miden-lifted-stark/src/pcs`) provides both: + +- `verify` (does not require transcript exhaustion; intended for composition), +- `verify_strict` (rejects trailing data), and +- `verify_aligned` (handles LMCS alignment; does not check transcript exhaustion). + +If you use `verify` or `verify_aligned` directly, you must define transcript boundaries +at the outer protocol layer. + +## Composition Rules + +- LMCS openings are hints: they must only be used when verified against an + already-observed commitment root. +- If you attach extra data before/after a proof in the same transcript, you must + define and enforce explicit boundaries. + +## What To Review First (Suggested Order) + +1. `miden-lifted-stark/src/verifier/mod.rs` (`verify_multi`) +2. `miden-lifted-stark/src/pcs/verifier.rs` (`verify`) +3. `miden-lifted-stark/src/lmcs/mod.rs` (`Lmcs::open_batch`) +4. `miden-lifted-stark/src/pcs/deep/verifier.rs` (DEEP reduction + quotient eval) +5. `miden-lifted-stark/src/pcs/fri/verifier.rs` (FRI round loop) + +## Security-Critical Invariants (Checklist) + +### Transcript Order + +- [ ] Commitments are observed before sampling challenges that depend on them. +- [ ] For each "grind then sample" boundary, the prover observes the same data + the verifier replays before checking the PoW witness. +- [ ] Hints are never used as a source of entropy. + +### LMCS (`miden-lifted-stark/src/lmcs`) + +- [ ] Leaf hashing absorption order is fixed and matches verifier recomputation. +- [ ] Batch proof sibling consumption is canonical (left-to-right, bottom-to-top). +- [ ] Duplicate indices are handled safely (coalesced is fine; callers must not + rely on duplicates being preserved). +- [ ] `widths`/`log_max_height` are treated as statement data; if they mismatch + the committed tree, verification must fail by root mismatch or parse error + (never by accepting an incorrect opening). + +### DEEP (`miden-lifted-stark/src/pcs/deep`) + +- [ ] OOD evaluations are observed *before* sampling `alpha`/`beta`. +- [ ] Column batching uses the same Horner convention everywhere + (first column gets the highest power). +- [ ] The verifier reconstructs the queried domain point from the tree index in + the same way the prover committed (bit-reversal + coset shift). +- [ ] Evaluation points are distinct and lie outside the LDE domain (division + by zero must be rejected). + +### FRI (`miden-lifted-stark/src/pcs/fri`) + +- [ ] For each round: commitment observed -> PoW verified -> folding challenge + sampled. +- [ ] Query indices are shifted consistently across rounds. +- [ ] The `s_inv` computation matches the prover's bit-reversed coset structure. +- [ ] Final polynomial coefficients are read in the intended order and evaluated + at the intended points. + +### Lifted STARK (`miden-lifted-stark/src/{prover,verifier}`) + +- [ ] Log trace heights are observed into the challenger; heights are powers of two + and in ascending order. +- [ ] The verifier's OOD evaluation point projection `y_j = z^{r_j}` matches + the prover's lifted commitment domains. +- [ ] Quotient chunk reconstruction (`reconstruct_quotient`) matches the prover's + quotient decomposition. +- [ ] Transcript exhaustion is enforced at the STARK boundary. + +## Soundness Sketches (High Level, Non-Formal) + +These sketches explain *why* the composition is intended to work. Formal +security bounds should come from a dedicated soundness calculator. + +### LMCS Binding + +If the hash and compression functions are collision-resistant, then (except with +negligible probability) a Merkle root binds the prover to exactly one set of leaf +preimages. LMCS openings are hints, but the verifier recomputes hashes and checks +the root. + +LMCS additionally defines a *lifted view*: shorter matrices are indistinguishable +from explicit repetition at the max height. This is a feature, not a bug: the +outer protocol must ensure that this lifted view is the one it intends to prove. + +### DEEP Batching + +DEEP reduces many claimed evaluations to one quotient polynomial by taking random +linear combinations (via `alpha` across columns and `beta` across points). + +Intuition: + +- If all claims are correct, the constructed quotient is a low-degree polynomial. +- If any claim is incorrect, the rational function has a "pole-like" obstruction + (a non-canceling term) that makes it extremely unlikely to agree with a + low-degree polynomial on the whole domain. + +The two challenges matter: `alpha` prevents the prover from "hiding" a bad column +inside a cancellation across columns, and `beta` prevents cancellation across +multiple evaluation points. + +### FRI Low-Degree Testing + +FRI is a standard proximity test: it checks that the committed evaluation vector +is close to a Reed-Solomon codeword of bounded degree. Soundness depends on +domain blowup, folding strategy, and the number of queries. + +This implementation is a conventional "commit, then query" FRI with per-round +Fiat-Shamir challenges and a final polynomial sent explicitly. + +### Lifting + +Lifting is the map `f(X) -> f(X^r)`. + +- LMCS upsampling in bit-reversed order corresponds to evaluating the lifted + polynomial on the max domain. +- Openings at a global point `z` implicitly provide openings at projected + points `y_j = z^{r_j}` for each smaller trace. + +The lifted STARK verifier evaluates each AIR instance at its projected point. +If the AIR is *liftable* (roughly: it does not depend on wrap-around "next row" +semantics unless explicitly constrained), then proving the lifted identity is as +sound as proving the non-lifted identity. + +For more detail on liftable AIR conditions and periodicity constraints, see +`docs/lifting.md`. + +## Parameter Guidance (Non-Normative) + +This workspace contains benchmark configurations (e.g. `log_blowup = 1`, +`num_queries = 100`) that are aimed at performance exploration, not +production-grade security. + +Soundness is primarily controlled by: + +- `miden-lifted-stark/src/pcs/fri/mod.rs::FriParams`: + - `log_blowup` + - `fold` (arity) + - `log_final_degree` +- `miden-lifted-stark/src/pcs/params.rs::PcsParams`: + - `num_queries` +- grinding parameters: + - `DeepParams::deep_pow_bits` + - `FriParams::folding_pow_bits` + - `PcsParams::query_pow_bits` + +Notes: + +- Grinding/PoW does not replace algebraic soundness; it is an anti-grinding + mechanism to make "searching for favorable challenges" expensive. +- Hash security (Merkle and Fiat-Shamir sponge) must meet your target security + level independently of PCS soundness. + +## DoS / Size-Bound Considerations + +Verification allocates based on a combination of: + +- statement data (matrix widths, number of commitment groups, `log_lde_height`, + `num_queries`), and +- transcript data (LMCS hints, FRI commitments, final polynomial coefficients). + +These libraries operate on already-deserialized transcript streams; they do not +enforce global size limits. Applications that deserialize proof bytes must cap +proof sizes and/or cap transcript lengths before constructing verifier channels. + +## Tests and Reference Points + +- LMCS lifting equivalence: `miden-lifted-stark/src/lmcs/lifted_tree.rs` (tests) +- LMCS batch openings: `miden-lifted-stark/src/lmcs/tests.rs` +- PCS end-to-end: `miden-lifted-stark/src/pcs/tests.rs` +- DEEP tests: `miden-lifted-stark/src/pcs/deep/tests.rs` +- FRI tests: `miden-lifted-stark/src/pcs/fri/tests.rs` diff --git a/stark/miden-lifted-air/Cargo.toml b/stark/miden-lifted-air/Cargo.toml new file mode 100644 index 0000000000..3401252493 --- /dev/null +++ b/stark/miden-lifted-air/Cargo.toml @@ -0,0 +1,25 @@ +[package] +description = "AIR traits for the Miden lifted STARK protocol, with symbolic constraint analysis." +edition = "2024" +homepage = "https://github.com/0xMiden/crypto/tree/main/crates" +license = "MIT OR Apache-2.0" +name = "miden-lifted-air" +readme = "../README.md" +repository = "https://github.com/0xMiden/crypto" +rust-version.workspace = true +version.workspace = true + +[lib] +doctest = false +test = false + +[dependencies] +p3-air.workspace = true +p3-field.workspace = true +p3-matrix.workspace = true +p3-util.workspace = true + +thiserror.workspace = true + +[lints] +workspace = true diff --git a/stark/miden-lifted-air/src/air.rs b/stark/miden-lifted-air/src/air.rs new file mode 100644 index 0000000000..d7e349d4c3 --- /dev/null +++ b/stark/miden-lifted-air/src/air.rs @@ -0,0 +1,336 @@ +//! The `LiftedAir` super-trait for AIR definitions in the lifted STARK system. +//! +//! # Panic safety of `eval()` +//! +//! [`LiftedAir::eval`] is generic over `AB: LiftedAirBuilder`, so it cannot branch +//! on the concrete builder type. All builders expose data through the same trait +//! methods — [`main()`](crate::AirBuilder::main), +//! [`permutation()`](crate::PermutationAirBuilder::permutation), +//! [`public_values()`](crate::AirBuilder::public_values), +//! [`permutation_randomness()`](crate::PermutationAirBuilder::permutation_randomness), +//! [`permutation_values()`](crate::PermutationAirBuilder::permutation_values), and +//! [`periodic_values()`](crate::PeriodicAirBuilder::periodic_values) — which return +//! matrices or slices. +//! +//! If the symbolic evaluation in [`LiftedAir::log_quotient_degree`] succeeds (i.e. +//! does not panic), it proves that the AIR's `eval()` only accesses indices within +//! the declared dimensions. Any concrete builder constructed with matching dimensions +//! is therefore safe from out-of-bounds panics. +//! +//! Use [`LiftedAir::is_valid_builder`] to verify that a concrete builder's +//! dimensions match the AIR before calling `eval()`. + +use alloc::vec::Vec; + +use p3_air::{BaseAir, WindowAccess}; +use p3_field::{ExtensionField, Field}; +use p3_matrix::dense::RowMajorMatrix; +use p3_util::log2_ceil_usize; +use thiserror::Error; + +use crate::{ + LiftedAirBuilder, + auxiliary::{ReducedAuxValues, ReductionError, VarLenPublicInputs}, + symbolic::{AirLayout, SymbolicAirBuilder, SymbolicExpression, SymbolicExpressionExt}, +}; + +/// Super-trait for AIR definitions used by the lifted STARK prover/verifier. +/// +/// Inherits from upstream traits for width and public values. +/// Adds Miden-specific auxiliary trace support and periodic column data. +/// Every `LiftedAir` must provide an auxiliary trace (even if it is a minimal +/// 1-column dummy). +/// +/// # Type Parameters +/// - `F`: Base field +/// - `EF`: Extension field (for aux trace challenges and aux values) +pub trait LiftedAir: Sync + BaseAir { + /// Return the periodic table data: a list of columns, each a `Vec` of evaluations. + /// + /// Each inner `Vec` represents one periodic column. Its length is the period of + /// that column, and the entries are the evaluations over a subgroup of that order. + /// + /// Default: no periodic columns. + fn periodic_columns(&self) -> Vec> { + Vec::new() + } + + /// Return a matrix with all periodic columns extended to a common height. + /// + /// Columns with smaller periods are repeated cyclically to fill the extended domain. + /// Returns `None` if there are no periodic columns. + fn periodic_columns_matrix(&self) -> Option> { + let cols = self.periodic_columns(); + if cols.is_empty() { + return None; + } + + let max_period = cols.iter().map(Vec::len).max()?; + let num_cols = cols.len(); + + let mut values = Vec::with_capacity(max_period * num_cols); + for row in 0..max_period { + for col in &cols { + let period = col.len(); + values.push(col[row % period]); + } + } + + Some(RowMajorMatrix::new(values, num_cols)) + } + + /// Number of extension-field challenges required for the auxiliary trace. + fn num_randomness(&self) -> usize; + + /// Number of extension-field columns in the auxiliary trace. + fn aux_width(&self) -> usize; + + /// Number of extension-field aux values committed to the Fiat-Shamir transcript. + /// + /// These are the values returned by + /// [`AuxBuilder::build_aux_trace`](crate::AuxBuilder::build_aux_trace) alongside the aux + /// trace matrix. Their count may differ from [`aux_width`](Self::aux_width) (the number of + /// aux trace columns). + /// + /// These values are exposed to AIR constraints as *permutation values* via + /// [`PermutationAirBuilder::permutation_values`](crate::PermutationAirBuilder::permutation_values). + fn num_aux_values(&self) -> usize; + + /// Number of variable-length public inputs this AIR expects. + /// + /// Each input is a slice of base-field elements that + /// [`reduced_aux_values`](Self::reduced_aux_values) reduces to a single value. + /// The prover validates that witnesses provide exactly this many slices. + /// + /// Implementors of [`reduced_aux_values`](Self::reduced_aux_values) should verify + /// that `var_len_public_inputs` contains exactly this many slices, returning + /// [`ReductionError`] otherwise. + fn num_var_len_public_inputs(&self) -> usize; + + /// Reduce this AIR's aux values to a [`ReducedAuxValues`] contribution. + /// + /// Called by the verifier (with concrete field values, not symbolic expressions) + /// to compute each AIR's contribution to the global cross-AIR bus identity check. + /// The verifier accumulates contributions across all AIRs and checks that the + /// combined result is identity (prod=1, sum=0). + /// + /// # Arguments + /// - `aux_values`: prover-supplied aux values (from the proof) + /// - `challenges`: extension-field challenges (same as used for aux trace building) + /// - `public_values`: this AIR's public values (base field) + /// - `var_len_public_inputs`: reducible inputs for the cross-AIR identity check + /// + /// # Errors + /// + /// The verifier validates instance dimensions (public values length, + /// var-len public inputs count) before calling this method, so + /// implementations can assume correct input counts. However, the + /// *length of each individual var-len slice* is not validated upfront — + /// implementations that index into these slices must check lengths + /// themselves or use the `Result` return type to report errors. + /// + /// Default: returns identity (correct for AIRs without buses). + fn reduced_aux_values( + &self, + _aux_values: &[EF], + _challenges: &[EF], + _public_values: &[F], + _var_len_public_inputs: VarLenPublicInputs<'_, F>, + ) -> Result, ReductionError> + where + EF: ExtensionField, + { + Ok(ReducedAuxValues::identity()) + } + + /// Return the [`AirLayout`] describing this AIR's dimensions. + /// + /// This is the single source of truth for building symbolic or layout builders. + /// `preprocessed_width` is always 0 because lifted AIRs forbid preprocessed traces. + fn air_layout(&self) -> AirLayout { + AirLayout { + preprocessed_width: 0, + main_width: self.width(), + num_public_values: self.num_public_values(), + permutation_width: self.aux_width(), + num_permutation_challenges: self.num_randomness(), + num_permutation_values: self.num_aux_values(), + num_periodic_columns: self.periodic_columns().len(), + } + } + + /// Validate that this AIR satisfies the [`LiftedAir`] contract. + /// + /// The lifted STARK protocol relies on several structural properties of the AIR + /// that can be checked statically (i.e. without a witness). This method verifies + /// the subset that is machine-checkable; the full list of trust assumptions is + /// documented in the module docs of `miden-lifted-stark`. Both the prover and + /// verifier call this before proceeding, so a malformed AIR is caught early. + /// + /// # Checked properties + /// + /// - **No preprocessed trace** — the lifted STARK protocol does not support preprocessed + /// (fixed) columns; their presence is an error. + /// - **Positive auxiliary width** — every lifted AIR must declare at least one auxiliary column + /// (`aux_width() > 0`). + /// - **Well-formed periodic columns** — each periodic column must be non-empty and have a + /// power-of-two length. + fn validate(&self) -> Result<(), AirStructureError> { + if self.preprocessed_trace().is_some() { + return Err(AirStructureError::PreprocessedTrace); + } + if self.aux_width() == 0 { + return Err(AirStructureError::ZeroAuxWidth); + } + for (i, col) in self.periodic_columns().iter().enumerate() { + if col.is_empty() || !col.len().is_power_of_two() { + return Err(AirStructureError::InvalidPeriodicColumn { + index: i, + length: col.len(), + }); + } + } + Ok(()) + } + + /// Evaluate all AIR constraints using the provided builder. + fn eval>(&self, builder: &mut AB); + + /// Log₂ of the number of quotient chunks, inferred from symbolic constraint analysis. + /// + /// Evaluates the AIR on a [`SymbolicAirBuilder`](crate::symbolic::SymbolicAirBuilder) to + /// determine the maximum constraint degree M, then returns `log2_ceil(M - 1)` (padded so M + /// ≥ 2). + /// + /// Uses `SymbolicAirBuilder` (i.e. `EF = F`) which is sufficient for degree + /// computation since extension-field operations have the same degree structure. + /// + /// # Why `M − 1` chunks? + /// + /// Let N be the trace height (so trace columns are polynomials of degree < N). + /// Symbolic evaluation assigns each constraint a *degree multiple* M, meaning the + /// resulting numerator polynomial C(X) has degree bounded by roughly M·(N − 1). + /// + /// In a STARK, the constraint numerator is divisible by the trace vanishing + /// polynomial `Z_H(X) = Xᴺ − 1`, so the quotient polynomial + /// `Q(X) = C(X) / Z_H(X)` has + /// + /// `deg(Q) ≤ deg(C) − N ≤ M·(N − 1) − N < (M − 1)·N`. + /// + /// We commit to Q(X) by splitting it into D chunks of degree < N. The bound above + /// shows that D = M − 1 chunks suffice; we then round D up to a power of two and + /// return `log2(D)`. + /// + /// We clamp M ≥ 2 so that D ≥ 1. If M = 1 then `deg(C) < N`, and divisibility by + /// `Z_H` would force C(X) to be the zero polynomial (i.e. the constraint carries no + /// information about the trace). + fn log_quotient_degree(&self) -> usize + where + Self: Sized, + { + let mut builder = SymbolicAirBuilder::::new(self.air_layout()); + self.eval(&mut builder); + + let base_degree_multiple = + |constraint: &SymbolicExpression| constraint.degree_multiple(); + let ext_degree_multiple = + |constraint: &SymbolicExpressionExt| constraint.degree_multiple(); + + let base_degree = + builder.base_constraints().iter().map(base_degree_multiple).max().unwrap_or(0); + let ext_degree = builder + .extension_constraints() + .iter() + .map(ext_degree_multiple) + .max() + .unwrap_or(0); + let constraint_degree = base_degree.max(ext_degree).max(2); + + log2_ceil_usize(constraint_degree - 1) + } + + /// Number of quotient chunks: `2^log_quotient_degree()`. + fn constraint_degree(&self) -> usize + where + Self: Sized, + { + 1 << self.log_quotient_degree() + } + + /// Check that a builder's dimensions match this AIR. + /// + /// Verifies every data-carrying accessor on [`LiftedAirBuilder`]: main trace, + /// preprocessed trace, aux trace, public values, randomness, aux values, and + /// periodic values. + /// + /// This guards the invariant that makes [`eval`](Self::eval) panic-free: if + /// the symbolic evaluation in [`log_quotient_degree`](Self::log_quotient_degree) + /// succeeds and this check passes, then `eval()` cannot panic from + /// out-of-bounds access on the builder's accessors. + fn is_valid_builder>( + &self, + builder: &AB, + ) -> Result<(), AirStructureError> { + let check = + |part: TracePart, expected: usize, actual: usize| -> Result<(), AirStructureError> { + if actual != expected { + return Err(AirStructureError::BuilderMismatch { part, expected, actual }); + } + Ok(()) + }; + + let main = builder.main(); + // Check current and next slices of the main trace. + check(TracePart::Main, self.width(), main.current_slice().len())?; + check(TracePart::Main, self.width(), main.next_slice().len())?; + + // Check current and next slices of the aux trace. + let perm = builder.permutation(); + check(TracePart::Aux, self.aux_width(), perm.current_slice().len())?; + check(TracePart::Aux, self.aux_width(), perm.next_slice().len())?; + + check(TracePart::PublicValues, self.num_public_values(), builder.public_values().len())?; + check( + TracePart::Randomness, + self.num_randomness(), + builder.permutation_randomness().len(), + )?; + check(TracePart::AuxValues, self.num_aux_values(), builder.permutation_values().len())?; + check( + TracePart::PeriodicValues, + self.periodic_columns().len(), + builder.periodic_values().len(), + )?; + + Ok(()) + } +} + +/// Which part of the trace a builder mismatch refers to. +#[derive(Copy, Clone, Debug)] +pub enum TracePart { + Main, + Aux, + PublicValues, + Randomness, + AuxValues, + PeriodicValues, +} + +/// Errors intrinsic to a single AIR definition, independent of any instance +/// data. Returned by [`LiftedAir::validate`] and [`LiftedAir::is_valid_builder`]. +#[derive(Debug, Error)] +pub enum AirStructureError { + #[error("periodic column {index}: length must be positive power of two, got {length}")] + InvalidPeriodicColumn { index: usize, length: usize }, + #[error("preprocessed traces are not supported")] + PreprocessedTrace, + #[error("{part:?} dimension mismatch: expected {expected}, got {actual}")] + BuilderMismatch { + part: TracePart, + expected: usize, + actual: usize, + }, + #[error("aux width must be positive")] + ZeroAuxWidth, +} diff --git a/stark/miden-lifted-air/src/auxiliary/builder.rs b/stark/miden-lifted-air/src/auxiliary/builder.rs new file mode 100644 index 0000000000..0846b88b38 --- /dev/null +++ b/stark/miden-lifted-air/src/auxiliary/builder.rs @@ -0,0 +1,36 @@ +//! The `AuxBuilder` trait for constructing auxiliary traces. +//! +//! This trait decouples auxiliary trace *building* from the AIR definition, +//! allowing the prover to supply a separate builder per instance. + +use alloc::vec::Vec; + +use p3_field::{ExtensionField, Field}; +use p3_matrix::dense::RowMajorMatrix; + +/// Builder for constructing the auxiliary trace from a main trace and challenges. +/// +/// Decoupled from [`LiftedAir`](crate::LiftedAir) so that prover-side trace +/// construction is not part of the AIR trait. Each prover instance can supply +/// its own `AuxBuilder`. +pub trait AuxBuilder> { + /// Build the auxiliary trace and return aux values. + /// + /// # Arguments + /// - `main`: The main trace matrix + /// - `challenges`: Extension-field challenges for aux trace construction + /// + /// # Returns + /// `(aux_trace, aux_values)` where: + /// - `aux_trace`: The auxiliary trace matrix (EF columns) + /// - `aux_values`: Extension-field scalars committed to the Fiat-Shamir transcript. Their + /// meaning is AIR-defined — typically the aux trace's last row, but the protocol does not + /// require this. The AIR's [`eval`](crate::LiftedAir::eval) should constrain how they relate + /// to the committed trace, and [`reduced_aux_values`](crate::LiftedAir::reduced_aux_values) + /// uses them for cross-AIR bus identity checking. + fn build_aux_trace( + &self, + main: &RowMajorMatrix, + challenges: &[EF], + ) -> (RowMajorMatrix, Vec); +} diff --git a/stark/miden-lifted-air/src/auxiliary/mod.rs b/stark/miden-lifted-air/src/auxiliary/mod.rs new file mode 100644 index 0000000000..89116a68e8 --- /dev/null +++ b/stark/miden-lifted-air/src/auxiliary/mod.rs @@ -0,0 +1,54 @@ +//! Auxiliary trace types: builder and cross-AIR identity checking. +//! +//! # Protocol Overview +//! +//! The auxiliary trace enables cross-AIR buses (multiset / logup) in the lifted STARK. +//! +//! ## Prover +//! +//! 1. [`AuxBuilder::build_aux_trace`] constructs the aux trace and returns aux values +//! (extension-field elements whose meaning is AIR-defined). +//! 2. The aux trace is committed (Merkle commitment). +//! 3. Aux values are sent via the Fiat-Shamir transcript. +//! +//! ## AIR constraints ([`eval`](crate::LiftedAir::eval)) +//! +//! 4. The AIR defines how aux values relate to the committed aux trace. A common pattern is to +//! constrain them to equal the aux trace's last row, but the protocol does not impose this — the +//! AIR is free to define whatever relationship it needs. +//! 5. Transition constraints enforce the aux trace's internal logic (e.g. running product +//! accumulation). +//! +//! ## Verifier +//! +//! 6. The verifier receives aux values from the transcript. +//! 7. Constraint evaluation (steps 4–5) is checked at a random point. +//! 8. [`reduced_aux_values`](crate::LiftedAir::reduced_aux_values) computes each AIR's bus +//! contribution from the aux values, challenges, and public inputs. +//! 9. Global check: all contributions combine to identity (prod=1, sum=0). + +mod builder; +mod values; + +pub use builder::AuxBuilder; +pub use values::ReducedAuxValues; + +/// Variable-length public inputs for an AIR instance. +/// +/// A list of *reducible inputs*: each `&[F]` is a slice of base-field elements +/// that [`LiftedAir::reduced_aux_values`](crate::LiftedAir::reduced_aux_values) +/// reduces to a single extension-field value. The AIR defines how to group and +/// interpret them (e.g. which inputs belong to which bus). +/// +/// The number of slices must equal +/// [`LiftedAir::num_var_len_public_inputs`](crate::LiftedAir::num_var_len_public_inputs). +/// +/// **Commitment:** callers **must** bind these inputs to the Fiat-Shamir +/// challenger state, just like the AIR's public values. +pub type VarLenPublicInputs<'a, F> = &'a [&'a [F]]; + +/// Boxed error returned by +/// [`LiftedAir::reduced_aux_values`](crate::LiftedAir::reduced_aux_values). +/// +/// Each AIR defines its own concrete error type and boxes it into this alias. +pub type ReductionError = alloc::boxed::Box; diff --git a/stark/miden-lifted-air/src/auxiliary/values.rs b/stark/miden-lifted-air/src/auxiliary/values.rs new file mode 100644 index 0000000000..98a3375212 --- /dev/null +++ b/stark/miden-lifted-air/src/auxiliary/values.rs @@ -0,0 +1,48 @@ +//! Types for auxiliary trace value reduction and cross-AIR identity checking. +//! +//! Each AIR's aux trace has associated aux values (extension field scalars), +//! sent via the transcript by the prover. The verifier calls +//! [`LiftedAir::reduced_aux_values`](crate::LiftedAir::reduced_aux_values) +//! on each AIR to compute a [`ReducedAuxValues`] contribution, then checks that +//! the global combination is identity (prod=1, sum=0). + +use p3_field::{Field, PrimeCharacteristicRing}; + +/// Accumulated contribution from reducing aux values across one or more AIRs. +/// +/// The global identity check requires: +/// - `prod == 1` (multiset buses: all ratios multiply to 1) +/// - `sum == 0` (logup buses: all differences sum to 0) +#[derive(Clone, Debug)] +pub struct ReducedAuxValues { + /// Accumulated product for multiset buses. + pub prod: EF, + /// Accumulated sum for logup buses. + pub sum: EF, +} + +impl ReducedAuxValues { + /// The identity contribution (no buses): prod=1, sum=0. + pub fn identity() -> Self { + Self { prod: EF::ONE, sum: EF::ZERO } + } + + /// Combine another contribution into this one. + pub fn combine_in_place(&mut self, other: &Self) { + self.prod *= other.prod.clone(); + self.sum += other.sum.clone(); + } + + /// Combine two contributions, returning a new one. + pub fn combine(mut self, other: &Self) -> Self { + self.combine_in_place(other); + self + } +} + +impl ReducedAuxValues { + /// Check whether this contribution is the identity (all buses satisfied). + pub fn is_identity(&self) -> bool { + self.prod == EF::ONE && self.sum == EF::ZERO + } +} diff --git a/stark/miden-lifted-air/src/builder.rs b/stark/miden-lifted-air/src/builder.rs new file mode 100644 index 0000000000..3179763275 --- /dev/null +++ b/stark/miden-lifted-air/src/builder.rs @@ -0,0 +1,18 @@ +//! The `LiftedAirBuilder` super-trait for constraint evaluation builders. + +use crate::{AirBuilder, ExtensionBuilder, PeriodicAirBuilder, PermutationAirBuilder}; + +/// Super-trait bundling all builder capabilities needed by the lifted STARK system. +/// +/// Every type that already satisfies the four upstream builder traits automatically +/// implements this trait via the blanket impl below. No additional methods or +/// associated types are required . +pub trait LiftedAirBuilder: + AirBuilder + ExtensionBuilder + PermutationAirBuilder + PeriodicAirBuilder +{ +} + +impl LiftedAirBuilder for T where + T: AirBuilder + ExtensionBuilder + PermutationAirBuilder + PeriodicAirBuilder +{ +} diff --git a/stark/miden-lifted-air/src/empty_window.rs b/stark/miden-lifted-air/src/empty_window.rs new file mode 100644 index 0000000000..1eaad1135c --- /dev/null +++ b/stark/miden-lifted-air/src/empty_window.rs @@ -0,0 +1,38 @@ +//! A zero-width window that prevents access to preprocessed columns. +//! +//! Lifted AIRs have no preprocessed trace. [`EmptyWindow`] encodes this invariant: +//! AIR validation prevents preprocessed access, and the window methods are +//! unreachable as a defence-in-depth measure. + +use core::marker::PhantomData; + +use p3_air::WindowAccess; + +/// A window type for traces that must never be accessed. +/// +/// Satisfies the `WindowAccess + Clone` bound required by +/// [`AirBuilder::PreprocessedWindow`](p3_air::AirBuilder::PreprocessedWindow). +/// Lifted AIRs have no preprocessed trace, so these methods should never be +/// called; AIR validation prevents this at a higher level. +#[derive(Debug, Clone, Copy)] +pub struct EmptyWindow(PhantomData); + +impl EmptyWindow { + /// Static reference to an empty window. + /// + /// Safe because `EmptyWindow` is a ZST — no actual `T` is stored, + /// so the `'static` lifetime is always valid. + pub fn empty_ref() -> &'static Self { + &EmptyWindow(PhantomData) + } +} + +impl WindowAccess for EmptyWindow { + fn current_slice(&self) -> &[T] { + unreachable!("preprocessed trace does not exist in lifted AIRs") + } + + fn next_slice(&self) -> &[T] { + unreachable!("preprocessed trace does not exist in lifted AIRs") + } +} diff --git a/stark/miden-lifted-air/src/lib.rs b/stark/miden-lifted-air/src/lib.rs new file mode 100644 index 0000000000..df03316935 --- /dev/null +++ b/stark/miden-lifted-air/src/lib.rs @@ -0,0 +1,41 @@ +//! AIR traits for the Miden lifted STARK protocol. +//! +//! This crate provides: +//! - [`LiftedAir`]: Super-trait for AIR definitions (inherits upstream + adds aux trace support and +//! periodic column data) +//! - [`LiftedAirBuilder`]: Super-trait for constraint builders +//! - [`auxiliary`]: Auxiliary trace types (builder, cross-AIR identity checking). + +#![no_std] + +extern crate alloc; + +mod air; +pub mod auxiliary; +mod builder; +mod util; + +pub use air::{AirStructureError, LiftedAir, TracePart}; +pub use auxiliary::{AuxBuilder, ReducedAuxValues, ReductionError, VarLenPublicInputs}; +pub use builder::LiftedAirBuilder; +pub use util::log2_strict_u8; + +mod empty_window; + +pub use empty_window::EmptyWindow; +// Re-export upstream p3-air types so downstream crates never need to depend on p3-air +// directly. +pub use p3_air::{ + Air, AirBuilder, AirBuilderWithContext, BaseAir, ExtensionBuilder, FilteredAirBuilder, + PeriodicAirBuilder, PermutationAirBuilder, RowWindow, WindowAccess, +}; + +/// Symbolic constraint analysis types from upstream p3-air. +pub mod symbolic { + pub use p3_air::symbolic::*; +} + +/// AIR constraint utility functions from upstream p3-air. +pub mod utils { + pub use p3_air::utils::*; +} diff --git a/stark/miden-lifted-air/src/util.rs b/stark/miden-lifted-air/src/util.rs new file mode 100644 index 0000000000..8202692694 --- /dev/null +++ b/stark/miden-lifted-air/src/util.rs @@ -0,0 +1,12 @@ +//! Small utility helpers shared across lifted-STARK crates. + +use p3_util::log2_strict_usize; + +/// Strict log₂ returning `u8`. +/// +/// Panics if `n` is not a power of two, or if the result exceeds `u8::MAX` +/// (i.e., `n >= 2^256` — impossible on any real platform). +#[inline] +pub fn log2_strict_u8(n: usize) -> u8 { + log2_strict_usize(n) as u8 +} diff --git a/stark/miden-lifted-stark/Cargo.toml b/stark/miden-lifted-stark/Cargo.toml new file mode 100644 index 0000000000..6d2d8a6485 --- /dev/null +++ b/stark/miden-lifted-stark/Cargo.toml @@ -0,0 +1,101 @@ +[package] +description = "Lifted STARK prover and verifier (LMCS-based)." +edition = "2024" +homepage = "https://github.com/0xMiden/crypto/tree/main/crates" +license = "MIT OR Apache-2.0" +name = "miden-lifted-stark" +readme = "../README.md" +repository = "https://github.com/0xMiden/crypto" +rust-version.workspace = true +version.workspace = true + +[lib] +doctest = false + +[dependencies] +# Internal +miden-lifted-air.workspace = true +miden-stark-transcript.workspace = true +miden-stateful-hasher.workspace = true + +# Plonky3 (always needed) +p3-challenger.workspace = true +p3-dft.workspace = true +p3-field.workspace = true +p3-goldilocks.workspace = true +p3-matrix.workspace = true +p3-maybe-rayon.workspace = true +p3-symmetric.workspace = true +p3-util.workspace = true + +# Optional (for testing/examples features) +p3-blake3 = { optional = true, workspace = true } +p3-blake3-air = { optional = true, workspace = true } +p3-keccak = { optional = true, workspace = true } +p3-keccak-air = { optional = true, workspace = true } +p3-poseidon2-air = { optional = true, workspace = true } + +# Third-party +rand.workspace = true +serde.workspace = true +thiserror.workspace = true +tracing.workspace = true + +[dev-dependencies] +# Plonky3 +p3-blake3.workspace = true +p3-challenger.workspace = true +p3-commit.workspace = true +p3-fri.workspace = true +p3-goldilocks.workspace = true +p3-interpolation.workspace = true +p3-keccak.workspace = true +p3-merkle-tree.workspace = true +p3-symmetric.workspace = true + +# Third-party +criterion.workspace = true +tracing-subscriber.workspace = true + +[features] +default = [] +parallel = ["p3-maybe-rayon/parallel"] +testing = ["dep:p3-blake3", "dep:p3-blake3-air", "dep:p3-keccak", "dep:p3-keccak-air", "dep:p3-poseidon2-air"] + +[[bench]] +harness = false +name = "quotient_commit" +required-features = ["testing"] + +[[bench]] +harness = false +name = "merkle_commit" +required-features = ["testing"] + +[[bench]] +harness = false +name = "fri_fold" +required-features = ["testing"] + +[[bench]] +harness = false +name = "deep_quotient" +required-features = ["testing"] + +[[bench]] +harness = false +name = "pcs" +required-features = ["testing"] + +[[bench]] +harness = false +name = "pcs_trace" +required-features = ["testing"] + +[[bench]] +harness = false +name = "plonky3" +required-features = ["testing"] + +[lints] +workspace = true diff --git a/stark/miden-lifted-stark/README.md b/stark/miden-lifted-stark/README.md new file mode 100644 index 0000000000..4035b48e8b --- /dev/null +++ b/stark/miden-lifted-stark/README.md @@ -0,0 +1,256 @@ +# Lifted STARK Protocol + +Multi-trace STARK prover and verifier using LMCS commitments, DEEP quotient +batching, and lifted FRI for low-degree testing. + +This README is protocol-level documentation (intended for maintainers and +reviewers). Per-module API details live in `src/prover/README.md` and +`src/verifier/README.md`. + +## Overview + +This crate contains the full lifted STARK implementation: shared types, +prover, and verifier. + +``` +miden-lifted-stark ← this crate +├── src/prover/ ← Proving: trace commitment, constraint evaluation, quotient construction +├── src/verifier/ ← Verification: OOD check, quotient reconstruction, transcript canonicality +├── src/pcs/ ← PCS (DEEP + FRI) +├── src/lmcs/ ← Merkle commitments with lifting +└── miden-lifted-air ← AIR traits (aux columns, periodic columns) +``` + +The system supports **multiple traces of different power-of-two heights**. +Shorter traces are virtually lifted to the maximum height via LMCS upsampling, +so the PCS and verifier operate on a single uniform view. + +## Notation + +- `N = 2^n`: maximum trace height across all traces in a proof. +- `n_j`: height of trace `j`. +- `r_j = N / n_j`: lift ratio (a power of two). +- `H`: two-adic subgroup of size `N` with generator `omega_H`. +- `g`: multiplicative coset shift (`F::GENERATOR` by convention). +- `D`: constraint degree blowup (here fixed at `D = 4`). +- `gJ`: quotient-domain coset (size `N * D`). +- `gK`: PCS/LDE coset (size `N * B`, where `B` is the FRI blowup). +- `z`: global out-of-domain point sampled once. +- `z_next = z * omega_H`: "next row" point for max height. + +When referring to LMCS, a *tree index* means a bit-reversed leaf index. + +## Liftable AIR Assumption + +LMCS makes shorter traces indistinguishable from explicit repetition at height +`N`. This is safe only if the AIR constraints are compatible with that lifted +view. + +Informally, an AIR is "liftable" if transition constraints do not rely on the +wrap-around row (last -> first) unless that behavior is explicitly constrained. +See `docs/lifting.md` for a deeper discussion and sufficient conditions. + +## Protocol Summary + +### Prover (`prove_multi`) + +1. **Commit main traces** — LDE each trace on its lifted coset, bit-reverse + rows, build LMCS tree. Send root. +2. **Sample randomness** — Squeeze auxiliary randomness from the Fiat-Shamir + channel. Build and commit auxiliary traces. +3. **Sample challenges** — `alpha` (constraint folding) and `beta` + (cross-trace accumulation). +4. **Evaluate constraints** — For each trace in ascending height order, + evaluate AIR constraints on the quotient domain using SIMD-packed + arithmetic. Produces a numerator N_j per trace (no vanishing division). +5. **Accumulate numerators** — Fold across traces: + `acc = cyclic_extend(acc) * beta + N_j`. +6. **Divide by vanishing polynomial** — One pass on the full quotient domain, + exploiting Z_H periodicity for batch inverse. +7. **Commit quotient** — Decompose Q into D chunks via fused iDFT + coefficient + scaling + flatten + DFT pipeline. Commit via LMCS. +8. **Sample OOD point z** — Rejection-sampled to lie outside H and the LDE + coset. +9. **Open via PCS** — Delegate to the internal `pcs` modules. + +### Verifier (`verify_multi`) + +1. **Receive commitments** — Main, auxiliary, and quotient roots from transcript. +2. **Re-derive challenges** — Same `alpha`, `beta`, `z` via Fiat-Shamir. +3. **Verify PCS openings** — At `[z, z_next]` where `z_next = z * omega_H`. +4. **Reconstruct Q(z)** — Barycentric interpolation over the D quotient + chunks. +5. **Evaluate constraints at OOD** — For each AIR at the lifted OOD point + `y_j = z^{r_j}`: compute selectors, evaluate periodic polynomials, + fold constraints with alpha, accumulate with beta. +6. **Check identity** — `accumulated == Q(z) * Z_H(z)`. +7. **Ensure transcript is fully consumed** — Canonicality enforcement. + +## Math Sketch + +### Multi-Trace Lifting + +Each trace j has height `n_j = n_max / r_j` where `r_j` is a power-of-two +lift ratio. The committed polynomial is `p_j(X^{r_j})`, so opening the LMCS +commitment at `z` yields `p_j(z^{r_j})`. The coset shift for trace j +is `g^{r_j}` where g is the multiplicative generator. + +### Constraint Folding + +For a single trace, constraints `C_0, C_1, ...` are folded via Horner +accumulation: + +``` +folded = (...((C_0 * alpha + C_1) * alpha + C_2)...) * alpha + C_k +``` + +This avoids precomputing alpha powers and does not require knowing the +constraint count ahead of time. + +### Cross-Trace Accumulation + +Numerators from traces of increasing height are combined: + +``` +acc = cyclic_extend(acc) * beta + N_j +``` + +where `cyclic_extend` repeats the accumulator via modular indexing +(`i & (len - 1)`) to match the next trace's quotient domain size. +This works because: + +``` +Z_H(x) = Z_{H^r}(x) * Phi_r(x) +``` + +so cyclic extension of a polynomial divisible by `Z_{H^r}` preserves +divisibility by `Z_H`. + +### Vanishing Division + +After accumulation, the combined numerator is divided by `Z_H(x) = x^N - 1` +once on the full quotient domain. + +On the quotient coset `gJ` (where `|J| = N * D`), the values `x^N` range over a +size-`D` subgroup, so `Z_H(x)` takes only `D` distinct values. The prover can +batch-invert those `D` values once and index them by `i mod D`. + +### Quotient Decomposition + +The quotient polynomial Q of degree `N * D - 1` is decomposed into D chunks +`q_0, ..., q_{D-1}` of degree `N - 1`: + +``` +Q(X) = q_0(X^D) + X * q_1(X^D) + ... + X^{D-1} * q_{D-1}(X^D) +``` + +The prover commits evaluations of each `q_t` over the LDE domain. The +verifier reconstructs `Q(z)` from `q_t(z)` via barycentric +interpolation: + +``` +Q(z) = (sum_t w_t * q_t(z)) / (sum_t w_t) + where w_t = omega_S^t / (u - omega_S^t), u = (z/g)^N +``` + +### Virtual OOD Point + +For a trace with lift ratio `r_j`, the effective OOD evaluation point is +`y_j = z^{r_j}`. The verifier evaluates selectors and periodic polynomials +at `y_j`, and the opened trace values already correspond to `p_j(y_j)`. + +## Optimizations + +- **SIMD constraint evaluation** — Constraints are evaluated on `PackedVal::WIDTH` + points simultaneously. Main trace stays in base field; only auxiliary columns + use extension field arithmetic. +- **Horner folding** — Constraint accumulation via `acc = acc * alpha + C_i` + avoids precomputing and storing alpha powers. +- **Fused quotient pipeline** — iDFT, coefficient scaling by `(omega^t)^{-k}`, + flatten to base field, zero-pad, forward DFT — all in one pass, no redundant + coset operations. +- **Periodic vanishing exploit** — On the quotient coset `gJ`, `Z_H(x)` takes + only `D` distinct values; batch inverse computes those once. +- **Zero-copy quotient domain** — `split_rows().bit_reverse_rows()` gives a + natural-order view of committed LDE data without copying. +- **Efficient periodic columns** — Only `max_period * blowup` LDE values + stored per periodic table; accessed via modular indexing. +- **Cyclic extension** — Cross-trace accumulation uses bitwise AND for + modular indexing (power-of-two sizes). +- **Parallel execution** — Rayon parallelism throughout constraint evaluation + and vanishing division (gated by `parallel` feature). + +## Entry Points + +| Item | Purpose | +|------|---------| +| `prover::prove_single` | Prove a single-AIR STARK | +| `prover::prove_multi` | Prove a multi-trace STARK | +| `AirWitness` | Prover witness (trace + public values) | +| `verifier::verify_single` | Verify a single-AIR proof | +| `verifier::verify_multi` | Verify a multi-trace proof | +| `AirInstance` | Verifier instance (public values + variable-length inputs) | +| `Transcript` | Structured transcript view (alias for `proof::StarkTranscript`) | +| `StarkConfig` | PCS params + LMCS + DFT configuration | +| `coset::LiftedCoset` | Domain operations: selectors, vanishing, coset shifts | + +## Modules + +| Path | Purpose | +|------|---------| +| `src/config.rs` | `StarkConfig` — wraps `PcsParams`, LMCS, and DFT | +| `src/coset.rs` | `LiftedCoset` — domain queries, selector computation, vanishing | +| `src/selectors.rs` | `Selectors` — generic container for row selectors | +| `src/prover/mod.rs` | `prove_single`, `prove_multi` — orchestration and protocol flow | +| `src/prover/commit.rs` | `Committed` — LDE, bit-reverse, LMCS tree construction | +| `src/prover/constraints/` | Constraint evaluation (SIMD) and layout discovery | +| `src/prover/periodic.rs` | `PeriodicLde` — precomputed periodic column LDEs | +| `src/prover/quotient.rs` | Quotient construction, cyclic extension, vanishing division | +| `src/verifier/mod.rs` | `verify_single`, `verify_multi` — orchestration and identity check | +| `src/verifier/constraints.rs` | `ConstraintFolder` — OOD constraint evaluation, quotient reconstruction | +| `src/verifier/periodic.rs` | `PeriodicPolys` — polynomial coefficients for OOD evaluation | +| `src/proof.rs` | `StarkProof`, `StarkTranscript` — proof artifact and structured transcript view | +| `src/instance.rs` | `AirInstance`, `AirWitness`, `InstanceShapes` — protocol-level instance types | + +## Conventions & Assumptions + +- **AIR ordering** — The proof defines an ordering of AIR instances + (queryable via `InstanceShapes::air_order`). The caller must bind AIR + configurations and `air_order` into the Fiat-Shamir challenger. See the + prover module-level docs. +- **Power-of-two heights** — All trace heights are powers of two. +- **Bit-reversed storage** — All evaluation matrices are in bit-reversed order. +- **Constraint degree** — Fixed at `D = 4` (`LOG_CONSTRAINT_DEGREE = 2`). + Both prover and verifier must agree on this constant. +- **Transcript ordering** — The Fiat-Shamir transcript follows a strict + observe/squeeze protocol. Prover and verifier must process commitments and + challenges in identical order. This is security-critical. +- **Extension field discipline** — Main trace and preprocessed data stay in + the base field. Only auxiliary columns, challenges, alpha powers, and the + accumulator use the extension field. +- **Periodic columns** — Column periods must be powers of two and divide the + trace height. Columns are grouped by period for batch interpolation. + +## Tests + +The end-to-end test suite lives in `tests/`: + +- **`tiny_air.rs`** — `TinyAir` exercising single-trace, multi-trace + (same and different heights), periodic columns, and malformed transcript + rejection. +- **`aux_shape.rs`** — Validates that mismatched auxiliary trace dimensions + are caught. + +Run with: +```bash +cargo test -p miden-lifted-stark +``` + +## Security + +Audits should start with `SECURITY.md` at the workspace root for transcript +ordering, lifting correctness, constraint identity, and critical paths. + +## License + +Dual-licensed under MIT and Apache-2.0 at the workspace root. diff --git a/stark/miden-lifted-stark/benches/deep_quotient.rs b/stark/miden-lifted-stark/benches/deep_quotient.rs new file mode 100644 index 0000000000..3f248c86e5 --- /dev/null +++ b/stark/miden-lifted-stark/benches/deep_quotient.rs @@ -0,0 +1,97 @@ +//! DEEP quotient benchmarks. +//! +//! Benchmarks the barycentric evaluation used in DEEP quotient construction. +//! Runs benchmarks for Goldilocks with Poseidon2. +//! +//! Run with: +//! ```bash +//! RUSTFLAGS="-Ctarget-cpu=native" cargo bench --bench deep_quotient --features testing +//! +//! # With parallelism +//! RUSTFLAGS="-Ctarget-cpu=native" cargo bench --bench deep_quotient --features testing,parallel +//! +//! # Filter by field +//! cargo bench --bench deep_quotient --features testing -- goldilocks +//! ``` + +use std::hint::black_box; + +use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; +use miden_lifted_stark::{ + Lmcs, LmcsTree, + testing::{ + LOG_HEIGHTS, PARALLEL_STR, PointQuotients, RELATIVE_SPECS, bit_reversed_coset_points, + configs::goldilocks_poseidon2::{Felt, QuadFelt, test_lmcs}, + generate_matrices_from_specs, total_elements, + }, +}; +use p3_field::FieldArray; +use p3_matrix::dense::RowMajorMatrix; +use rand::{RngExt, SeedableRng, distr::StandardUniform, rngs::SmallRng}; + +/// Log blowup factor for LDE. +const LOG_BLOWUP: u8 = 3; + +// ============================================================================= +// Benchmark implementation +// ============================================================================= + +fn bench_deep_quotient(c: &mut Criterion) { + let lmcs = test_lmcs(); + + for &log_lde_height in LOG_HEIGHTS { + let n_leaves = 1usize << log_lde_height; + let group_name = format!("DEEP_Quotient/{n_leaves}/goldilocks/poseidon2/{PARALLEL_STR}"); + let mut group = c.benchmark_group(&group_name); + + // Generate matrices using canonical specs + let matrix_groups: Vec>> = + generate_matrices_from_specs(RELATIVE_SPECS, log_lde_height); + group.throughput(Throughput::Elements(total_elements(&matrix_groups))); + + let trees: Vec<_> = + matrix_groups.iter().map(|matrices| lmcs.build_tree(matrices.clone())).collect(); + + // Precompute coset points (LDE domain matches max matrix height) + let coset_points = bit_reversed_coset_points::(log_lde_height); + + // Get matrix references from trees (stored as BitReversedMatrixView after build_tree) + let matrices_refs: Vec> = + trees.iter().map(|tree| tree.leaves().iter().collect()).collect(); + + // Benchmark: batch_eval_lifted with 1 point + group.bench_function(BenchmarkId::from_parameter("batch_eval/N1"), |b| { + let mut rng = SmallRng::seed_from_u64(789); + b.iter(|| { + let z: QuadFelt = rng.sample(StandardUniform); + let quotient = + PointQuotients::::new(FieldArray([z]), &coset_points); + black_box(quotient.batch_eval_lifted(&matrices_refs, &coset_points, LOG_BLOWUP)) + }); + }); + + // Benchmark: batch_eval_lifted with 2 points + group.bench_function(BenchmarkId::from_parameter("batch_eval/N2"), |b| { + let mut rng = SmallRng::seed_from_u64(789); + b.iter(|| { + let z1: QuadFelt = rng.sample(StandardUniform); + let z2: QuadFelt = rng.sample(StandardUniform); + let quotient = + PointQuotients::::new(FieldArray([z1, z2]), &coset_points); + black_box(quotient.batch_eval_lifted(&matrices_refs, &coset_points, LOG_BLOWUP)) + }); + }); + + group.finish(); + } +} + +criterion_group! { + name = benches; + config = Criterion::default() + .sample_size(10) + .measurement_time(std::time::Duration::from_secs(12)) + .warm_up_time(std::time::Duration::from_secs(3)); + targets = bench_deep_quotient +} +criterion_main!(benches); diff --git a/stark/miden-lifted-stark/benches/fri_fold.rs b/stark/miden-lifted-stark/benches/fri_fold.rs new file mode 100644 index 0000000000..69df1f840b --- /dev/null +++ b/stark/miden-lifted-stark/benches/fri_fold.rs @@ -0,0 +1,91 @@ +//! FRI folding benchmarks for lifted implementation. +//! +//! Benchmarks FRI fold operations at different arities (2, 4, 8). +//! Runs benchmarks for Goldilocks (field-only, no hashing). +//! +//! Run with: +//! ```bash +//! RUSTFLAGS="-Ctarget-cpu=native" cargo bench --bench fri_fold --features testing +//! +//! # With parallelism +//! RUSTFLAGS="-Ctarget-cpu=native" cargo bench --bench fri_fold --features testing,parallel +//! +//! # Filter by field +//! cargo bench --bench fri_fold --features testing -- goldilocks +//! ``` + +use std::hint::black_box; + +use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; +use miden_lifted_stark::testing::{ + FRI_FOLD_ARITY_2, FRI_FOLD_ARITY_4, FRI_FOLD_ARITY_8, FriFold, LOG_HEIGHTS, PARALLEL_STR, + TEST_SEED, + configs::goldilocks_poseidon2::{Felt, QuadFelt}, +}; +use p3_matrix::{Matrix, dense::RowMajorMatrix}; +use rand::{RngExt, SeedableRng, distr::StandardUniform, rngs::SmallRng}; + +/// Target number of rows after all folding rounds. +const TARGET: usize = 8; + +fn bench_lifted_fold( + group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, + fold: FriFold, + n_elems: usize, +) { + let rng = &mut SmallRng::seed_from_u64(TEST_SEED); + let arity = fold.arity(); + + let n_rows = n_elems / arity; + let s_invs: Vec = rng.sample_iter(StandardUniform).take(n_rows).collect(); + + let values: Vec = rng.sample_iter(StandardUniform).take(n_elems).collect(); + let input = RowMajorMatrix::new(values, arity); + + group.bench_with_input( + BenchmarkId::from_parameter(format!("arity{arity}")), + &n_elems, + |b, &_n| { + b.iter(|| { + let mut current = input.clone(); + + while current.height() > TARGET { + let rows = current.height(); + let beta: QuadFelt = rng.sample(StandardUniform); + let evals = fold.fold_matrix( + black_box(current.as_view()), + black_box(&s_invs[..rows]), + black_box(beta), + ); + current = RowMajorMatrix::new(evals, arity); + } + black_box(current) + }); + }, + ); +} + +fn bench_fri_fold(c: &mut Criterion) { + for &log_height in LOG_HEIGHTS { + let n_elems = 1usize << log_height; + let group_name = format!("FRI_Fold/{n_elems}/goldilocks/{PARALLEL_STR}"); + let mut group = c.benchmark_group(&group_name); + group.throughput(Throughput::Elements(n_elems as u64)); + + bench_lifted_fold(&mut group, FRI_FOLD_ARITY_2, n_elems); + bench_lifted_fold(&mut group, FRI_FOLD_ARITY_4, n_elems); + bench_lifted_fold(&mut group, FRI_FOLD_ARITY_8, n_elems); + + group.finish(); + } +} + +criterion_group! { + name = benches; + config = Criterion::default() + .sample_size(10) + .measurement_time(std::time::Duration::from_secs(12)) + .warm_up_time(std::time::Duration::from_secs(3)); + targets = bench_fri_fold +} +criterion_main!(benches); diff --git a/stark/miden-lifted-stark/benches/merkle_commit.rs b/stark/miden-lifted-stark/benches/merkle_commit.rs new file mode 100644 index 0000000000..00b2c91ea7 --- /dev/null +++ b/stark/miden-lifted-stark/benches/merkle_commit.rs @@ -0,0 +1,115 @@ +//! Merkle tree commit benchmarks for LMCS. +//! +//! Benchmarks LMCS commit operations including ExtensionMmcs for FRI. +//! Runs benchmarks for Goldilocks with Poseidon2. +//! +//! Run with: +//! ```bash +//! RUSTFLAGS="-Ctarget-cpu=native" cargo bench --bench merkle_commit --features testing +//! +//! # With parallelism +//! RUSTFLAGS="-Ctarget-cpu=native" cargo bench --bench merkle_commit --features testing,parallel +//! +//! # Filter by field +//! cargo bench --bench merkle_commit --features testing -- goldilocks +//! ``` + +use std::hint::black_box; + +use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; +use miden_lifted_stark::{ + Lmcs, LmcsTree, + testing::{ + LOG_HEIGHTS, PARALLEL_STR, RELATIVE_SPECS, + configs::goldilocks_poseidon2::{Felt, QuadFelt, test_lmcs}, + generate_matrices_from_specs, total_elements, + }, +}; +use p3_matrix::{bitrev::BitReversalPerm, dense::RowMajorMatrix, extension::FlatMatrixView}; +use rand::{SeedableRng, rngs::SmallRng}; + +// ============================================================================= +// Benchmark implementation +// ============================================================================= + +fn bench_merkle_commit(c: &mut Criterion) { + let lmcs = test_lmcs(); + + for &log_max_height in LOG_HEIGHTS { + let n_leaves = 1usize << log_max_height; + let group_name = format!("MerkleCommit/{n_leaves}/goldilocks/poseidon2/{PARALLEL_STR}"); + let mut group = c.benchmark_group(&group_name); + group.throughput(Throughput::Elements(total_elements( + &generate_matrices_from_specs::(RELATIVE_SPECS, log_max_height), + ))); + + // Generate matrices using canonical specs + let matrix_groups: Vec>> = + generate_matrices_from_specs(RELATIVE_SPECS, log_max_height); + + // LMCS commit + { + group.bench_with_input( + BenchmarkId::from_parameter("lmcs"), + &matrix_groups, + |b, groups| { + b.iter(|| { + for matrices in groups { + let tree = lmcs.build_tree(matrices.clone()); + black_box(tree.root()); + } + }); + }, + ); + } + + // Extension field matrix with width-2 (simulates FRI arity-2 commit) + // Uses FlatMatrixView to convert EF matrix to base field view + { + let rng = &mut SmallRng::seed_from_u64(miden_lifted_stark::testing::TEST_SEED); + let ext_matrix = RowMajorMatrix::::rand(rng, n_leaves, 2); + + group.bench_with_input( + BenchmarkId::from_parameter("ext/arity2"), + &ext_matrix, + |b, matrix| { + b.iter(|| { + let flat = FlatMatrixView::new(matrix.clone()); + let tree = lmcs.build_tree(vec![BitReversalPerm::new_view(flat)]); + black_box(tree.root()) + }); + }, + ); + } + + // Extension field matrix with width-4 (simulates FRI arity-4 commit) + { + let rng = &mut SmallRng::seed_from_u64(miden_lifted_stark::testing::TEST_SEED); + let ext_matrix = RowMajorMatrix::::rand(rng, n_leaves, 4); + + group.bench_with_input( + BenchmarkId::from_parameter("ext/arity4"), + &ext_matrix, + |b, matrix| { + b.iter(|| { + let flat = FlatMatrixView::new(matrix.clone()); + let tree = lmcs.build_tree(vec![BitReversalPerm::new_view(flat)]); + black_box(tree.root()) + }); + }, + ); + } + + group.finish(); + } +} + +criterion_group! { + name = benches; + config = Criterion::default() + .sample_size(10) + .measurement_time(std::time::Duration::from_secs(12)) + .warm_up_time(std::time::Duration::from_secs(3)); + targets = bench_merkle_commit +} +criterion_main!(benches); diff --git a/stark/miden-lifted-stark/benches/pcs.rs b/stark/miden-lifted-stark/benches/pcs.rs new file mode 100644 index 0000000000..6dfe59391a --- /dev/null +++ b/stark/miden-lifted-stark/benches/pcs.rs @@ -0,0 +1,90 @@ +//! Lifted PCS open benchmarks at different folding arities. +//! +//! ```bash +//! RUSTFLAGS="-Ctarget-cpu=native" cargo bench --bench pcs --features testing +//! ``` + +use std::hint::black_box; + +use criterion::{Criterion, Throughput, criterion_group, criterion_main}; +use miden_lifted_stark::{ + Lmcs, LmcsTree, log2_strict_u8, + testing::{ + BENCH_PCS_PARAMS, LOG_HEIGHTS, PARALLEL_STR, RELATIVE_SPECS, + configs::goldilocks_poseidon2::{Felt, QuadFelt, test_challenger, test_lmcs}, + generate_matrices_from_specs, open_with_channel, total_elements, + }, +}; +use miden_stark_transcript::ProverTranscript; +use p3_challenger::{CanObserve, FieldChallenger}; +use p3_dft::{Radix2DitParallel, TwoAdicSubgroupDft}; +use p3_field::Field; +use p3_matrix::{Matrix, dense::RowMajorMatrix}; + +fn bench_pcs(c: &mut Criterion) { + let dft = Radix2DitParallel::::default(); + let shift = Felt::GENERATOR; + let lmcs = test_lmcs(); + + for &log_lde_height in LOG_HEIGHTS { + let max_lde_size = 1usize << log_lde_height; + let group_name = format!("PCS_Open/{max_lde_size}/goldilocks/poseidon2/{PARALLEL_STR}"); + let mut group = c.benchmark_group(&group_name); + + let matrix_groups: Vec>> = + generate_matrices_from_specs(RELATIVE_SPECS, log_lde_height); + group.throughput(Throughput::Elements(total_elements(&matrix_groups))); + + // Compute LDE matrices and flatten into a single group (sorted by height) + let mut all_lde_matrices: Vec<_> = matrix_groups + .iter() + .flat_map(|matrices| { + matrices.iter().map(|m| { + dft.coset_lde_batch(m.clone(), BENCH_PCS_PARAMS.log_blowup() as usize, shift) + }) + }) + .collect(); + all_lde_matrices.sort_by_key(Matrix::height); + + let tree = lmcs.build_aligned_tree(all_lde_matrices); + let commitment = tree.root(); + let log_lde_height = log2_strict_u8(tree.height()); + + let base_challenger = test_challenger(); + + { + group.bench_function("open", |b| { + b.iter(|| { + let mut challenger = base_challenger.clone(); + challenger.observe(commitment); + let z1: QuadFelt = challenger.sample_algebra_element(); + let z2: QuadFelt = challenger.sample_algebra_element(); + let mut channel = ProverTranscript::new(challenger); + + let trace_trees: &[&_] = &[&tree]; + open_with_channel::( + &BENCH_PCS_PARAMS, + &lmcs, + log_lde_height, + [z1, z2], + trace_trees, + &mut channel, + ); + black_box(channel.finalize()) + }); + }); + } + + group.finish(); + } +} + +criterion_group! { + name = benches; + config = Criterion::default() + .sample_size(10) + .measurement_time(std::time::Duration::from_secs(30)) + .warm_up_time(std::time::Duration::from_secs(3)); + targets = bench_pcs +} +criterion_main!(benches); diff --git a/stark/miden-lifted-stark/benches/pcs_trace.rs b/stark/miden-lifted-stark/benches/pcs_trace.rs new file mode 100644 index 0000000000..b7dcad3b55 --- /dev/null +++ b/stark/miden-lifted-stark/benches/pcs_trace.rs @@ -0,0 +1,102 @@ +//! Traced PCS run for profiling with `tracing-subscriber`. +//! +//! Runs the lifted PCS open (Goldilocks + Poseidon2, arity-4) at log heights 16, 18, 20 +//! with a tracing subscriber that prints hierarchical span timings. +//! +//! Run with: +//! ```bash +//! RUSTFLAGS="-Ctarget-cpu=native" cargo bench -p miden-lifted-stark --bench pcs_trace --features testing,parallel +//! ``` + +use std::time::Instant; + +use miden_lifted_stark::{ + Lmcs, LmcsTree, PcsParams, log2_strict_u8, + testing::{ + LOG_HEIGHTS, RELATIVE_SPECS, + configs::goldilocks_poseidon2::{Felt, QuadFelt, test_challenger, test_lmcs}, + generate_matrices_from_specs, open_with_channel, + }, +}; +use miden_stark_transcript::ProverTranscript; +use p3_challenger::{CanObserve, FieldChallenger}; +use p3_dft::{Radix2DitParallel, TwoAdicSubgroupDft}; +use p3_field::Field; +use p3_matrix::{Matrix, bitrev::BitReversibleMatrix, dense::RowMajorMatrix}; +use tracing_subscriber::EnvFilter; + +fn main() { + // Initialize tracing subscriber. + // Use RUST_LOG to control verbosity, e.g. RUST_LOG=debug for debug_span! events. + tracing_subscriber::fmt() + .with_env_filter( + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("debug")), + ) + .with_span_events(tracing_subscriber::fmt::format::FmtSpan::CLOSE) + .init(); + + let dft = Radix2DitParallel::::default(); + let shift = Felt::GENERATOR; + + let params = PcsParams::new( + 2, // log_blowup + 2, // log_folding_arity (arity 4) + 8, // log_final_degree + 0, // folding_pow_bits + 0, // deep_pow_bits + 30, // num_queries + 0, // query_pow_bits + ) + .expect("valid PCS params"); + + for &log_lde_height in LOG_HEIGHTS { + let size = 1usize << log_lde_height; + eprintln!("\n{}", "=".repeat(60)); + eprintln!("=== Goldilocks lifted/arity4 log_height={log_lde_height} (n={size}) ==="); + eprintln!("{}\n", "=".repeat(60)); + + let matrix_groups: Vec>> = + generate_matrices_from_specs(RELATIVE_SPECS, log_lde_height); + + let lmcs = test_lmcs(); + + // Compute LDE matrices and build LMCS tree + let mut all_lde_matrices: Vec<_> = matrix_groups + .iter() + .flat_map(|matrices| { + matrices.iter().map(|m| { + dft.coset_lde_batch(m.clone(), 2, shift) + .bit_reverse_rows() + .to_row_major_matrix() + .bit_reverse_rows() + }) + }) + .collect::>(); + all_lde_matrices.sort_by_key(Matrix::height); + + let tree = lmcs.build_aligned_tree(all_lde_matrices); + let commitment = tree.root(); + let log_lde_height = log2_strict_u8(tree.height()); + + let mut challenger = test_challenger(); + challenger.observe(commitment); + let z1: QuadFelt = challenger.sample_algebra_element(); + let z2: QuadFelt = challenger.sample_algebra_element(); + let mut channel = ProverTranscript::new(challenger); + + let trace_trees: &[&_] = &[&tree]; + + let start = Instant::now(); + open_with_channel::( + ¶ms, + &lmcs, + log_lde_height, + [z1, z2], + trace_trees, + &mut channel, + ); + let elapsed = start.elapsed(); + + eprintln!(">>> Total open_with_channel: {elapsed:.3?}\n"); + } +} diff --git a/stark/miden-lifted-stark/benches/plonky3.rs b/stark/miden-lifted-stark/benches/plonky3.rs new file mode 100644 index 0000000000..058802104f --- /dev/null +++ b/stark/miden-lifted-stark/benches/plonky3.rs @@ -0,0 +1,389 @@ +//! Plonky3 comparison benchmarks. +//! +//! All benchmarks that compare our lifted implementation against upstream Plonky3 +//! abstractions (`TwoAdicFriPcs`, `MerkleTreeMmcs`) live here. +//! +//! ```bash +//! RUSTFLAGS="-Ctarget-cpu=native" cargo bench --bench plonky3 --features testing +//! +//! # Filter by benchmark group +//! cargo bench --bench plonky3 --features testing -- LMCS_vs_MMCS +//! cargo bench --bench plonky3 --features testing -- PCS_Open +//! cargo bench --bench plonky3 --features testing -- quotient_commit +//! ``` + +use std::hint::black_box; + +use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; +use miden_lifted_stark::{ + LiftedCoset, Lmcs, LmcsTree, log2_strict_u8, + testing::{ + BENCH_PCS_PARAMS, LOG_HEIGHTS, PARALLEL_STR, QC_CONSTRAINT_DEGREE, QC_PCS_PARAMS, + RELATIVE_SPECS, commit_quotient, + configs::{ + goldilocks_blake3_192 as gl_blake3_192, goldilocks_keccak as gl_keccak, + goldilocks_poseidon2 as gl, + }, + generate_matrices_from_specs, open_with_channel, total_elements, + }, +}; +use miden_stark_transcript::ProverTranscript; +use p3_blake3::Blake3; +use p3_challenger::{CanObserve, FieldChallenger}; +use p3_commit::{ExtensionMmcs, Mmcs, Pcs}; +use p3_dft::{Radix2DitParallel, TwoAdicSubgroupDft}; +use p3_field::{Field, coset::TwoAdicMultiplicativeCoset}; +use p3_fri::{FriParameters, TwoAdicFriPcs}; +use p3_keccak::KeccakF; +use p3_matrix::{Matrix, dense::RowMajorMatrix}; +use p3_merkle_tree::MerkleTreeMmcs; +use p3_symmetric::{PaddingFreeSponge, SerializingHasher}; +use rand::{RngExt, SeedableRng, rngs::SmallRng}; + +// ============================================================================= +// Workspace MMCS / PCS types (Goldilocks + Poseidon2) +// ============================================================================= + +type Poseidon2MmcsSponge = PaddingFreeSponge; +type Poseidon2ValMmcs = MerkleTreeMmcs< + gl::PackedFelt, + gl::PackedFelt, + Poseidon2MmcsSponge, + gl::Compress, + 2, + { gl::DIGEST }, +>; +type Poseidon2ChallengeMmcs = ExtensionMmcs; +type WorkspacePcs = + TwoAdicFriPcs, Poseidon2ValMmcs, Poseidon2ChallengeMmcs>; + +fn gl_poseidon2_mmcs() -> Poseidon2ValMmcs { + let perm = gl::create_perm(); + Poseidon2ValMmcs::new(Poseidon2MmcsSponge::new(perm.clone()), gl::Compress::new(perm), 0) +} + +fn workspace_pcs( + log_blowup: usize, + log_final_poly_len: usize, + max_log_arity: usize, + num_queries: usize, +) -> WorkspacePcs { + let (perm, _, compress) = gl::test_components(); + let mmcs_sponge = Poseidon2MmcsSponge::new(perm); + let mmcs = Poseidon2ValMmcs::new(mmcs_sponge, compress, 0); + let challenge_mmcs = Poseidon2ChallengeMmcs::new(mmcs.clone()); + let fri_params = FriParameters { + log_blowup, + log_final_poly_len, + max_log_arity, + num_queries, + commit_proof_of_work_bits: 0, + query_proof_of_work_bits: 0, + mmcs: challenge_mmcs, + }; + WorkspacePcs::new(Radix2DitParallel::default(), mmcs, fri_params) +} + +// ============================================================================= +// Workspace MMCS types (Keccak, Blake3-192) +// ============================================================================= + +type KeccakMmcs = MerkleTreeMmcs< + gl_keccak::Felt, + u64, + SerializingHasher, + gl_keccak::Compress, + 2, + { gl_keccak::DIGEST }, +>; + +fn gl_keccak_mmcs() -> KeccakMmcs { + let inner = gl_keccak::KeccakMmcsSponge::new(KeccakF); + KeccakMmcs::new(SerializingHasher::new(inner), gl_keccak::Compress::new(inner), 0) +} + +type Blake3_192Mmcs = MerkleTreeMmcs< + gl_blake3_192::Felt, + u8, + SerializingHasher, + gl_blake3_192::Compress, + 2, + { gl_blake3_192::DIGEST }, +>; + +fn gl_blake3_192_mmcs() -> Blake3_192Mmcs { + let inner = gl_blake3_192::Blake3_192::new(Blake3); + Blake3_192Mmcs::new(SerializingHasher::new(inner), gl_blake3_192::Compress::new(inner), 0) +} + +// ============================================================================= +// LMCS vs MMCS commit +// ============================================================================= + +fn bench_hash, M: Mmcs>( + c: &mut Criterion, + lmcs: &L, + mmcs: &M, + hash_name: &str, +) { + for &log_max_height in LOG_HEIGHTS { + let n_leaves = 1usize << log_max_height; + let group_name = format!("LMCS_vs_MMCS/{n_leaves}/goldilocks/{hash_name}/{PARALLEL_STR}"); + let mut group = c.benchmark_group(&group_name); + group.throughput(Throughput::Elements(total_elements(&generate_matrices_from_specs::< + gl::Felt, + >( + RELATIVE_SPECS, log_max_height + )))); + + let matrix_groups: Vec>> = + generate_matrices_from_specs(RELATIVE_SPECS, log_max_height); + + group.bench_with_input(BenchmarkId::from_parameter("lmcs"), &matrix_groups, |b, groups| { + b.iter(|| { + for matrices in groups { + let tree = lmcs.build_tree(matrices.clone()); + black_box(tree.root()); + } + }); + }); + + group.bench_with_input(BenchmarkId::from_parameter("mmcs"), &matrix_groups, |b, groups| { + b.iter(|| { + for matrices in groups { + black_box(mmcs.commit(matrices.clone())); + } + }); + }); + + group.finish(); + } +} + +fn bench_lmcs_vs_mmcs(c: &mut Criterion) { + bench_hash(c, &gl::test_lmcs(), &gl_poseidon2_mmcs(), "poseidon2"); + bench_hash(c, &gl_keccak::test_lmcs(), &gl_keccak_mmcs(), "keccak"); + bench_hash(c, &gl_blake3_192::test_lmcs(), &gl_blake3_192_mmcs(), "blake3-192"); +} + +// ============================================================================= +// PCS open comparison +// ============================================================================= + +fn bench_pcs_open(c: &mut Criterion) { + let dft = Radix2DitParallel::::default(); + let shift = gl::Felt::GENERATOR; + + for &log_lde_height in LOG_HEIGHTS { + let max_lde_size = 1usize << log_lde_height; + let group_name = format!("PCS_Open/{max_lde_size}/goldilocks/poseidon2/{PARALLEL_STR}"); + let mut group = c.benchmark_group(&group_name); + + let matrix_groups: Vec>> = + generate_matrices_from_specs(RELATIVE_SPECS, log_lde_height); + group.throughput(Throughput::Elements(total_elements(&matrix_groups))); + + // --- Workspace TwoAdicFriPcs --- + { + let ws_pcs = workspace_pcs( + BENCH_PCS_PARAMS.log_blowup() as usize, + BENCH_PCS_PARAMS.log_final_degree() as usize, + BENCH_PCS_PARAMS.log_folding_arity() as usize, + BENCH_PCS_PARAMS.num_queries(), + ); + + let commits_and_data: Vec<_> = matrix_groups + .iter() + .map(|matrices| { + let domains_and_evals = matrices.iter().map(|m| { + let domain = + >::natural_domain_for_degree( + &ws_pcs, + m.height(), + ); + (domain, m.clone()) + }); + >::commit(&ws_pcs, domains_and_evals) + }) + .collect(); + + let base_challenger = gl::test_challenger(); + + group.bench_function(BenchmarkId::from_parameter("workspace"), |b| { + b.iter(|| { + let mut challenger = base_challenger.clone(); + for (commitment, _) in &commits_and_data { + challenger.observe(commitment.clone()); + } + let z1: gl::QuadFelt = challenger.sample_algebra_element(); + let z2: gl::QuadFelt = challenger.sample_algebra_element(); + + let data_and_points: Vec<_> = commits_and_data + .iter() + .enumerate() + .map(|(i, (_, prover_data))| { + let num_matrices = matrix_groups[i].len(); + let points = if i < 2 { + vec![vec![z1, z2]; num_matrices] + } else { + vec![vec![z1]; num_matrices] + }; + (prover_data, points) + }) + .collect(); + + let (_openings, proof) = + >::open( + &ws_pcs, + black_box(data_and_points), + &mut challenger, + ); + black_box(proof) + }); + }); + } + + // --- Lifted PCS (arity 2 and 4) --- + { + let lmcs = gl::test_lmcs(); + + let mut all_lde_matrices: Vec<_> = matrix_groups + .iter() + .flat_map(|matrices| { + matrices.iter().map(|m| { + dft.coset_lde_batch( + m.clone(), + BENCH_PCS_PARAMS.log_blowup() as usize, + shift, + ) + }) + }) + .collect(); + all_lde_matrices.sort_by_key(Matrix::height); + + let tree = lmcs.build_aligned_tree(all_lde_matrices); + let commitment = tree.root(); + let log_lde_height = log2_strict_u8(tree.height()); + + let base_challenger = gl::test_challenger(); + + { + group.bench_function(BenchmarkId::from_parameter("lifted"), |b| { + b.iter(|| { + let mut challenger = base_challenger.clone(); + challenger.observe(commitment); + let z1: gl::QuadFelt = challenger.sample_algebra_element(); + let z2: gl::QuadFelt = challenger.sample_algebra_element(); + let mut channel = ProverTranscript::new(challenger); + + let trace_trees: &[&_] = &[&tree]; + open_with_channel::( + &BENCH_PCS_PARAMS, + &lmcs, + log_lde_height, + [z1, z2], + trace_trees, + &mut channel, + ); + black_box(channel.finalize()) + }); + }); + } + } + + group.finish(); + } +} + +// ============================================================================= +// Quotient commit comparison +// ============================================================================= + +type Dft = Radix2DitParallel; +type LiftedLmcs = gl::Lmcs; +type LiftedConfig = + miden_lifted_stark::GenericStarkConfig; + +fn lifted_config() -> LiftedConfig { + LiftedConfig::new(QC_PCS_PARAMS, gl::test_lmcs(), Dft::default(), gl::test_challenger()) +} + +fn random_quotient_evals(n: usize, d: usize, seed: u64) -> Vec { + let mut rng = SmallRng::seed_from_u64(seed); + (0..n * d).map(|_| rng.random()).collect() +} + +fn bench_quotient_commit(c: &mut Criterion) { + let mut group = c.benchmark_group("quotient_commit"); + let log_d = log2_strict_u8(QC_CONSTRAINT_DEGREE); + + for log_n in [16u8, 17u8] { + let n = 1usize << log_n; + let b = 1usize << QC_PCS_PARAMS.log_blowup(); + let label = format!("N=2^{log_n}"); + + // --- Lifted --- + { + let config = lifted_config(); + let coset = LiftedCoset::unlifted(log_n, QC_PCS_PARAMS.log_blowup()); + + group.bench_function(BenchmarkId::new("lifted", &label), |bench| { + bench.iter(|| { + let mut q_evals = random_quotient_evals(n, QC_CONSTRAINT_DEGREE, 42); + q_evals.reserve(n * b - n * QC_CONSTRAINT_DEGREE); + let committed = commit_quotient(&config, q_evals, &coset); + black_box(committed) + }); + }); + } + + // --- Plonky3 PCS --- + { + let pcs = workspace_pcs(QC_PCS_PARAMS.log_blowup() as usize, 0, 1, 1); + let quotient_domain = + TwoAdicMultiplicativeCoset::new(gl::Felt::GENERATOR, (log_n + log_d) as usize) + .unwrap(); + + group.bench_function(BenchmarkId::new("plonky3_pcs", &label), |bench| { + bench.iter(|| { + let q_evals = random_quotient_evals(n, QC_CONSTRAINT_DEGREE, 42); + let q_flat = RowMajorMatrix::new_col(q_evals).flatten_to_base(); + let (commitment, data) = + >::commit_quotient( + &pcs, + quotient_domain, + q_flat, + QC_CONSTRAINT_DEGREE, + ); + black_box((commitment, data)) + }); + }); + } + } + + group.finish(); +} + +// ============================================================================= +// Criterion groups +// ============================================================================= + +criterion_group! { + name = merkle_commit; + config = Criterion::default() + .sample_size(10) + .measurement_time(std::time::Duration::from_secs(12)) + .warm_up_time(std::time::Duration::from_secs(3)); + targets = bench_lmcs_vs_mmcs +} + +criterion_group! { + name = pcs_open; + config = Criterion::default() + .sample_size(10) + .measurement_time(std::time::Duration::from_secs(30)) + .warm_up_time(std::time::Duration::from_secs(3)); + targets = bench_pcs_open, bench_quotient_commit +} + +criterion_main!(merkle_commit, pcs_open); diff --git a/stark/miden-lifted-stark/benches/quotient_commit.rs b/stark/miden-lifted-stark/benches/quotient_commit.rs new file mode 100644 index 0000000000..8b113a3509 --- /dev/null +++ b/stark/miden-lifted-stark/benches/quotient_commit.rs @@ -0,0 +1,64 @@ +//! Lifted `commit_quotient` benchmark. +//! +//! Measures the decomposition + LDE + Merkle commit pipeline for quotient +//! polynomials at different trace sizes. +//! +//! ```bash +//! RUSTFLAGS="-Ctarget-cpu=native" cargo bench --bench quotient_commit --features testing +//! ``` + +use std::hint::black_box; + +use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; +use miden_lifted_stark::{ + GenericStarkConfig, LiftedCoset, + testing::{ + QC_CONSTRAINT_DEGREE, QC_PCS_PARAMS, commit_quotient, configs::goldilocks_poseidon2 as gl, + }, +}; +use p3_dft::Radix2DitParallel; +use rand::{RngExt, SeedableRng, rngs::SmallRng}; + +fn random_quotient_evals(n: usize, d: usize, seed: u64) -> Vec { + let mut rng = SmallRng::seed_from_u64(seed); + (0..n * d).map(|_| rng.random()).collect() +} + +fn bench_quotient_commit(c: &mut Criterion) { + let config = GenericStarkConfig::new( + QC_PCS_PARAMS, + gl::test_lmcs(), + Radix2DitParallel::default(), + gl::test_challenger(), + ); + let mut group = c.benchmark_group("quotient_commit"); + + for log_n in [16u8, 17u8] { + let n = 1usize << log_n; + let b = 1usize << QC_PCS_PARAMS.log_blowup(); + let label = format!("N=2^{log_n}"); + + let coset = LiftedCoset::unlifted(log_n, QC_PCS_PARAMS.log_blowup()); + + group.bench_function(BenchmarkId::new("lifted", &label), |bench| { + bench.iter(|| { + let mut q_evals = random_quotient_evals(n, QC_CONSTRAINT_DEGREE, 42); + q_evals.reserve(n * b - n * QC_CONSTRAINT_DEGREE); + let committed = commit_quotient(&config, q_evals, &coset); + black_box(committed) + }); + }); + } + + group.finish(); +} + +criterion_group! { + name = benches; + config = Criterion::default() + .sample_size(10) + .measurement_time(std::time::Duration::from_secs(30)) + .warm_up_time(std::time::Duration::from_secs(3)); + targets = bench_quotient_commit +} +criterion_main!(benches); diff --git a/stark/miden-lifted-stark/src/config.rs b/stark/miden-lifted-stark/src/config.rs new file mode 100644 index 0000000000..3f015ea256 --- /dev/null +++ b/stark/miden-lifted-stark/src/config.rs @@ -0,0 +1,108 @@ +//! STARK configuration trait and generic implementation. +//! +//! [`StarkConfig`] bundles the LMCS, DFT, and challenger as associated types. +//! The base field `F` and extension field `EF` are generic parameters so that +//! functions using `SC: StarkConfig` can refer to `F` and `EF` directly +//! instead of going through `SC::F` / `SC::EF`. +//! +//! [`GenericStarkConfig`] provides a ready-made implementation for tests and +//! examples. Production integrations can implement `StarkConfig` on their own +//! concrete struct. + +use core::marker::PhantomData; + +use miden_stark_transcript::TranscriptChallenger; +use p3_dft::TwoAdicSubgroupDft; +use p3_field::{ExtensionField, TwoAdicField}; + +use crate::{lmcs::Lmcs, pcs::params::PcsParams}; + +/// Lifted STARK configuration. +/// +/// `F` and `EF` are generic parameters rather than associated types so that +/// functions bounded by `SC: StarkConfig` can refer to them directly. +/// Bounds on `F` and `EF` are declared once here and inherited by every user. +pub trait StarkConfig>: Clone { + /// LMCS (Merkle commitment scheme). + type Lmcs: Lmcs; + /// DFT for LDE computation. + type Dft: TwoAdicSubgroupDft; + /// Fiat-Shamir challenger. + type Challenger: TranscriptChallenger::Commitment>; + + /// PCS parameters (DEEP + FRI settings). + fn pcs(&self) -> &PcsParams; + /// LMCS instance for commitments. + fn lmcs(&self) -> &Self::Lmcs; + /// DFT implementation for LDE computation. + fn dft(&self) -> &Self::Dft; + /// Create a fresh challenger for a new proof/verification. + fn challenger(&self) -> Self::Challenger; +} + +/// Generic [`StarkConfig`] implementation. +/// +/// Stores the PCS parameters, LMCS, DFT, and a challenger prototype +/// (cloned for each proof/verification). Use this for tests and examples; +/// production code can implement `StarkConfig` on a custom struct. +pub struct GenericStarkConfig { + pub pcs: PcsParams, + pub lmcs: L, + pub dft: Dft, + pub challenger: Ch, + _phantom: PhantomData (F, EF)>, +} + +impl GenericStarkConfig { + pub fn new(pcs: PcsParams, lmcs: L, dft: Dft, challenger: Ch) -> Self { + Self { + pcs, + lmcs, + dft, + challenger, + _phantom: PhantomData, + } + } +} + +// Manual Clone: avoids requiring F: Clone, EF: Clone. +impl Clone for GenericStarkConfig { + fn clone(&self) -> Self { + Self { + pcs: self.pcs, + lmcs: self.lmcs.clone(), + dft: self.dft.clone(), + challenger: self.challenger.clone(), + _phantom: PhantomData, + } + } +} + +impl StarkConfig for GenericStarkConfig +where + F: TwoAdicField, + EF: ExtensionField, + L: Lmcs, + Dft: TwoAdicSubgroupDft + Clone, + Ch: TranscriptChallenger, +{ + type Lmcs = L; + type Dft = Dft; + type Challenger = Ch; + + fn pcs(&self) -> &PcsParams { + &self.pcs + } + + fn lmcs(&self) -> &L { + &self.lmcs + } + + fn dft(&self) -> &Dft { + &self.dft + } + + fn challenger(&self) -> Ch { + self.challenger.clone() + } +} diff --git a/stark/miden-lifted-stark/src/coset.rs b/stark/miden-lifted-stark/src/coset.rs new file mode 100644 index 0000000000..2d7fb1ddb7 --- /dev/null +++ b/stark/miden-lifted-stark/src/coset.rs @@ -0,0 +1,419 @@ +//! Lifted coset domain abstraction with selector and vanishing computation. +//! +//! This module provides [`LiftedCoset`](crate::coset::LiftedCoset), the central abstraction for +//! domain operations in lifted STARKs where traces of different heights share a common evaluation +//! domain. + +use alloc::vec::Vec; + +use miden_stark_transcript::Channel; +use p3_field::{ExtensionField, TwoAdicField, batch_multiplicative_inverse}; +use p3_maybe_rayon::prelude::*; + +use crate::{pcs::params::MAX_LOG_DOMAIN_SIZE, selectors::Selectors}; + +// ============================================================================ +// LiftedCoset +// ============================================================================ + +/// Lifted coset for polynomial evaluation. +/// +/// Represents a coset (gK)ʳ where: +/// - K is the evaluation domain of size 2^log_lde_height +/// - r = 2^log_lift_ratio is the lift factor (row repetition) +/// - The shift is gʳ where g = F::GENERATOR +/// +/// Key relationships: +/// - log_blowup = log_lde_height - log_trace_height +/// - log_lift_ratio = log_max_lde_height - log_lde_height +/// - lde_shift = gʳ = F::GENERATOR.exp_power_of_2(log_lift_ratio) +/// +/// # Invariants +/// +/// - `log_lde_height = log_trace_height + log_blowup` +/// - `log_lde_height <= log_max_lde_height` +/// - All heights are powers of two (stored as log values) +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct LiftedCoset { + /// Log₂ of the original trace height. + pub log_trace_height: u8, + /// Log₂ of this matrix's LDE height. + pub log_lde_height: u8, + /// Log₂ of the maximum LDE height in the commitment. + pub log_max_lde_height: u8, +} + +impl LiftedCoset { + /// Create a new `LiftedCoset`. + /// + /// Both `log_lde_height` and `log_max_lde_height` are derived by adding + /// `log_blowup` to the respective trace heights. + /// + /// # Panics + /// + /// Panics if `log_trace_height > log_max_trace_height` or if + /// `log_max_trace_height + log_blowup > MAX_LOG_DOMAIN_SIZE`. + #[inline] + pub fn new(log_trace_height: u8, log_blowup: u8, log_max_trace_height: u8) -> Self { + assert!( + log_trace_height <= log_max_trace_height, + "trace height cannot exceed max trace height" + ); + let log_lde_height = log_trace_height as u16 + log_blowup as u16; + let log_max_lde_height = log_max_trace_height as u16 + log_blowup as u16; + assert!( + log_max_lde_height <= MAX_LOG_DOMAIN_SIZE as u16, + "LDE height 2^{log_max_lde_height} exceeds maximum 2^{MAX_LOG_DOMAIN_SIZE}", + ); + Self { + log_trace_height, + log_lde_height: log_lde_height as u8, + log_max_lde_height: log_max_lde_height as u8, + } + } + + /// Create a `LiftedCoset` at max height (no lifting). + /// + /// Convenience for the common single-trace case where the LDE height + /// equals the max LDE height. + #[inline] + pub fn unlifted(log_trace_height: u8, log_blowup: u8) -> Self { + let log_lde_height = log_trace_height as u16 + log_blowup as u16; + assert!( + log_lde_height <= MAX_LOG_DOMAIN_SIZE as u16, + "LDE height 2^{log_lde_height} exceeds maximum 2^{MAX_LOG_DOMAIN_SIZE}", + ); + Self { + log_trace_height, + log_lde_height: log_lde_height as u8, + log_max_lde_height: log_lde_height as u8, + } + } + + // ============ Existing methods ============ + + /// Log₂ of the blowup factor for this matrix. + /// + /// Returns `log_lde_height - log_trace_height`. + #[inline] + pub fn log_blowup(&self) -> usize { + (self.log_lde_height - self.log_trace_height) as usize + } + + /// Log₂ of the lift ratio for this matrix. + /// + /// The lift ratio is how many times this matrix's rows are virtually repeated + /// to match the max LDE height: `max_lde_height / lde_height`. + /// + /// Returns `log_max_lde_height - log_lde_height`. + #[inline] + pub fn log_lift_ratio(&self) -> usize { + (self.log_max_lde_height - self.log_lde_height) as usize + } + + /// Whether this matrix is lifted (its LDE height is less than the max). + #[inline] + pub fn is_lifted(&self) -> bool { + self.log_lde_height < self.log_max_lde_height + } + + /// Compute the coset shift for this matrix's LDE domain. + /// + /// For a matrix with lift ratio `r = 2^log_lift_ratio`, the coset shift is gʳ + /// where g is the field generator. + /// + /// Why gʳ: lifting embeds a smaller-domain polynomial into the max domain by + /// composition `p_lift(X) = p(Xʳ)`. Evaluating `p_lift` on the max coset `g·K_max` + /// corresponds to evaluating `p` on the nested coset `gʳ·K`, because + /// `(g·ω)ʳ = gʳ·ωʳ` and ωʳ ranges over K when ω ranges over `K_max`. + #[inline] + pub fn lde_shift(&self) -> F { + F::GENERATOR.exp_power_of_2(self.log_lift_ratio()) + } + + /// The trace height (number of constraint rows). + #[inline] + pub fn trace_height(&self) -> usize { + 1 << self.log_trace_height as usize + } + + /// The LDE height for this matrix. + #[inline] + pub fn lde_height(&self) -> usize { + 1 << self.log_lde_height as usize + } + + /// The maximum LDE height across all matrices. + #[inline] + pub fn max_lde_height(&self) -> usize { + 1 << self.log_max_lde_height as usize + } + + /// The blowup factor for this matrix. + #[inline] + pub fn blowup(&self) -> usize { + 1 << self.log_blowup() + } + + // ============ Domain derivation ============ + + /// Derive the quotient domain coset from this LDE coset. + /// + /// For constraint evaluation, we need a coset of size `trace_height * constraint_degree`. + /// This transforms (gK)ʳ into (gJ)ʳ while preserving the lift ratio. + /// + /// # Panics + /// Panics if log_constraint_degree > log_blowup. + /// + /// The quotient domain is a strict subset of the committed LDE domain. + /// + /// If the constraint degree is `D`, the resulting quotient polynomial has degree + /// `< N * (D - 1)`, so `N * D` evaluation points suffice for commitment and for the + /// verifier's reconstruction. The PCS uses a larger blowup `B`, so the committed + /// LDE domain `gK` has `N * B` points, but constraint evaluation only needs the + /// sub-coset `gJ` of size `N * D` (with `D <= B`). + pub fn quotient_domain(&self, log_constraint_degree: u8) -> Self { + let log_blowup = self.log_lde_height - self.log_trace_height; + assert!(log_constraint_degree <= log_blowup, "constraint degree cannot exceed blowup"); + let log_max_trace_height = self.log_max_lde_height - log_blowup; + Self { + log_trace_height: self.log_trace_height, + log_lde_height: self.log_trace_height + log_constraint_degree, + log_max_lde_height: log_max_trace_height + log_constraint_degree, + } + } + + // ============ Selector computation ============ + + /// Compute selectors for evaluation over this coset in natural order. + /// + /// Returns is_first_row, is_last_row, is_transition for each point in the coset + /// (gK)ʳ. The trace domain H has size `2^log_trace_height`. + /// + /// Selectors use unnormalized Lagrange basis polynomials. The is_first_row selector + /// is `L₀(x) = Z_H(x) / (x − 1)`, which equals 0 on all of H except the first row. + /// When multiplied by a constraint C(x), it enforces C only at the first row: + /// `L₀(x)·C(x)` vanishes on H iff `C(1) = 0`. Similarly, + /// `is_last_row = Z_H(x) / (x − ω⁻¹)`. + /// + /// The is_transition selector is `(x − ω⁻¹)`, which is nonzero everywhere except the + /// last row, enforcing transition constraints on all consecutive row pairs. + /// These are "unnormalized" because we omit the constant factor 1/N that would make + /// them evaluate to exactly 1 at their target row. This is fine because both prover + /// and verifier evaluate the same unnormalized form: multiplying all boundary constraints + /// by a common nonzero constant does not affect whether the quotient is a polynomial. + pub fn selectors(&self) -> Selectors> { + let shift: F = self.lde_shift(); + let coset_size = self.lde_height(); + let log_blowup = self.log_blowup(); + + // Z_H(x) = xⁿ − 1 evaluated at coset points. + // Periodic with 2^log_blowup distinct values; expand to full coset size for zip. + let s_pow_n = shift.exp_power_of_2(self.log_trace_height as usize); + let z_h_periodic: Vec = F::two_adic_generator(log_blowup) + .shifted_powers(s_pow_n) + .take(1 << log_blowup) + .map(|x| x - F::ONE) + .collect(); + let period = z_h_periodic.len(); + + // Coset points in natural order: shift·ω_Jⁱ + let omega_j = F::two_adic_generator(self.log_lde_height as usize); + let xs: Vec = omega_j.shifted_powers(shift).collect_n(coset_size); + + let omega_h_inv = F::two_adic_generator(self.log_trace_height as usize).inverse(); + + // Unnormalized Lagrange selector: selᵢ = Z_H(xᵢ) / (xᵢ − basis_point) + // Uses modular indexing into z_h_periodic to avoid a full-size allocation. + let single_point_selector = |basis_point: F| -> Vec { + let denoms: Vec = xs.par_iter().map(|&x| x - basis_point).collect(); + let invs = batch_multiplicative_inverse(&denoms); + (0..coset_size) + .into_par_iter() + .map(|i| z_h_periodic[i % period] * invs[i]) + .collect() + }; + + Selectors { + is_first_row: single_point_selector(F::ONE), + is_last_row: single_point_selector(omega_h_inv), + is_transition: xs.into_par_iter().map(|x| x - omega_h_inv).collect(), + } + } + + /// Lifted selectors at the OOD point (verifier). + /// + /// For a selector `s(x)` defined over the original trace domain of size `n_j`, + /// lifting evaluates `s(z_lift)` where `z_lift = z^r` and + /// `r = 2^log_lift_ratio = max_n / n_j`. This maps the OOD point `z` + /// (sampled in the max-trace domain) into the per-instance trace domain. + /// + /// # Formulas (unnormalized) + /// - `is_first_row = Z_H(z_lift) / (z_lift − 1)` + /// - `is_last_row = Z_H(z_lift) / (z_lift − ω_{n_j}⁻¹)` + /// - `is_transition = z_lift − ω_{n_j}⁻¹` + /// + /// where `Z_H(z_lift) = z_lift^{n_j} − 1 = z^{max_n} − 1`. + pub fn selectors_at(&self, z: EF) -> Selectors + where + F: TwoAdicField, + EF: ExtensionField, + { + let z_lift = z.exp_power_of_2(self.log_lift_ratio()); + let vanishing = self.vanishing_at::(z_lift); + let omega_inv = F::two_adic_generator(self.log_trace_height as usize).inverse(); + + Selectors { + is_first_row: vanishing / (z_lift - F::ONE), + is_last_row: vanishing / (z_lift - omega_inv), + is_transition: z_lift - omega_inv, + } + } + + // ============ Vanishing polynomial ============ + + /// Vanishing polynomial at an out-of-domain point. + /// + /// Returns `Z_H(z) = zⁿ − 1` using `exp_power_of_2` (log-many squarings). + pub fn vanishing_at(&self, z: EF) -> EF + where + F: TwoAdicField, + EF: ExtensionField, + { + z.exp_power_of_2(self.log_trace_height as usize) - EF::ONE + } + + // ============ Domain membership ============ + + /// Check if a point is in the trace domain H. + /// + /// Returns true if `z^N == 1` where N is the trace height. + /// Points in H cause division by zero in vanishing polynomial inversion. + #[inline] + pub fn is_in_trace_domain(&self, z: EF) -> bool + where + F: TwoAdicField, + EF: ExtensionField, + { + z.exp_power_of_2(self.log_trace_height as usize) == EF::ONE + } + + /// Check if a point is in the LDE coset gK. + /// + /// Returns true if `(z/g)^|K| == 1` where g is the generator shift + /// and K is the LDE domain. Points in gK cause division by zero in DEEP quotients. + #[inline] + pub fn is_in_lde_coset(&self, z: EF) -> bool + where + F: TwoAdicField, + EF: ExtensionField, + { + let shift: F = self.lde_shift(); + let z_over_shift = z * shift.inverse(); + z_over_shift.exp_power_of_2(self.log_lde_height as usize) == EF::ONE + } + + // ============ OOD point sampling ============ + + /// Sample an OOD evaluation point from the channel that lies outside both the + /// trace-domain subgroup `H` and the LDE evaluation coset `gK`. + /// + /// Repeatedly draws `sample_algebra_element` candidates until one satisfies + /// both exclusion tests. This terminates with overwhelming probability because + /// `|H ∪ gK|` is negligible relative to the extension field size. + pub fn sample_ood_point(&self, channel: &mut impl Channel) -> EF + where + F: TwoAdicField, + EF: ExtensionField, + { + loop { + let candidate: EF = channel.sample_algebra_element(); + if !self.is_in_trace_domain::(candidate) + && !self.is_in_lde_coset::(candidate) + { + break candidate; + } + } + } +} + +// ============================================================================ +// Tests +// ============================================================================ + +#[cfg(test)] +mod tests { + use p3_field::{Field, PrimeCharacteristicRing}; + + use super::*; + use crate::testing::configs::goldilocks_poseidon2::Felt; + + #[test] + fn domain_info_basic() { + // Trace height 2^10, blowup 2^3, max trace 2^12 + let info = LiftedCoset::new(10, 3, 12); + + assert_eq!(info.log_trace_height, 10); + assert_eq!(info.log_lde_height, 13); + assert_eq!(info.log_max_lde_height, 15); + + assert_eq!(info.log_blowup(), 3); + assert_eq!(info.log_lift_ratio(), 2); + assert!(info.is_lifted()); + + assert_eq!(info.trace_height(), 1024); + assert_eq!(info.lde_height(), 8192); + assert_eq!(info.max_lde_height(), 32768); + assert_eq!(info.blowup(), 8); + } + + #[test] + fn domain_info_no_lift() { + // Matrix at max height (no lifting needed) + let info = LiftedCoset::unlifted(10, 3); + + assert_eq!(info.log_lift_ratio(), 0); + assert!(!info.is_lifted()); + } + + #[test] + fn domain_info_lde_shift() { + // Trace height 2^10, blowup 2^3, max trace 2^12 + let info = LiftedCoset::new(10, 3, 12); + let shift: Felt = info.lde_shift(); + + // shift = g^(2^2) = g^4 + let expected = Felt::GENERATOR.exp_power_of_2(2); + assert_eq!(shift, expected); + } + + #[test] + fn domain_info_no_lift_shift() { + // When not lifted, shift should be g^1 = g + let info = LiftedCoset::unlifted(10, 3); + let shift: Felt = info.lde_shift(); + + // shift = g^(2^0) = g + assert_eq!(shift, Felt::GENERATOR); + } + + #[test] + fn quotient_domain_preserves_lift_ratio_and_updates_blowup() { + // Trace height 2^10, blowup 2^3 (B=8), max trace 2^12. + let lde = LiftedCoset::new(10, 3, 12); + + // Constraint degree D = 4 (log D = 2), so quotient domain size is N*D. + let q = lde.quotient_domain(2); + + // Trace height is unchanged; the evaluation domain becomes N*D. + assert_eq!(q.log_trace_height, 10); + assert_eq!(q.log_blowup(), 2); + assert_eq!(q.log_lde_height, 12); + + // Max evaluation domain becomes N_max*D. + assert_eq!(q.log_max_lde_height, 14); + + // Lift ratio is preserved. + assert_eq!(q.log_lift_ratio(), lde.log_lift_ratio()); + } +} diff --git a/stark/miden-lifted-stark/src/debug.rs b/stark/miden-lifted-stark/src/debug.rs new file mode 100644 index 0000000000..42aa358c49 --- /dev/null +++ b/stark/miden-lifted-stark/src/debug.rs @@ -0,0 +1,349 @@ +//! Debug constraint checker for lifted AIRs. +//! +//! Evaluates constraints row-by-row on concrete trace values and panics if any constraint +//! is nonzero. This avoids the full STARK pipeline (DFT, commitment, FRI) and provides +//! immediate feedback on constraint violations during development. +//! +//! # Usage +//! +//! ```ignore +//! use miden_lifted_stark::AirWitness; +//! +//! // Single instance +//! let witness = AirWitness::new(&trace, &public_values, &[]); +//! check_constraints(&air, &witness, &aux_builder, &challenges); +//! +//! // Multiple instances +//! check_constraints_multi( +//! &[(&air_a, witness_a, &builder_a), (&air_b, witness_b, &builder_b)], +//! &challenges, +//! ); +//! ``` + +extern crate alloc; + +use alloc::vec::Vec; + +use miden_lifted_air::{ + AirBuilder, AuxBuilder, EmptyWindow, ExtensionBuilder, LiftedAir, PeriodicAirBuilder, + PermutationAirBuilder, RowWindow, +}; +use p3_field::{ExtensionField, Field}; +use p3_matrix::{Matrix, dense::RowMajorMatrix}; + +use crate::instance::AirWitness; + +// ============================================================================ +// Public API +// ============================================================================ + +/// Evaluate every AIR constraint against a concrete trace and panic on failure. +/// +/// Convenience wrapper around [`check_constraints_multi`] for a single instance. +/// +/// # Panics +/// +/// - If the AIR fails validation +/// - If trace dimensions don't match the AIR +/// - If challenges are insufficient +/// - If any constraint evaluates to nonzero on any row +pub fn check_constraints( + air: &A, + witness: AirWitness<'_, F>, + aux_builder: &B, + challenges: &[EF], +) where + F: Field, + EF: ExtensionField, + A: LiftedAir, + B: AuxBuilder, +{ + check_constraints_multi(&[(air, witness, aux_builder)], challenges); +} + +/// Evaluate constraints for multiple AIR instances and panic on failure. +/// +/// Each instance is a tuple of `(air, witness, aux_builder)`. +/// +/// Builds the auxiliary trace for each instance and checks constraints row by row. +/// Uses shared challenges across all instances (caller samples from RNG in test code). +/// +/// # Panics +/// +/// - If any AIR fails validation +/// - If trace dimensions don't match their AIR +/// - If challenges are insufficient +/// - If any constraint evaluates to nonzero on any row +pub fn check_constraints_multi( + instances: &[(&A, AirWitness<'_, F>, &B)], + challenges: &[EF], +) where + F: Field, + EF: ExtensionField, + A: LiftedAir, + B: AuxBuilder, +{ + assert!(!instances.is_empty(), "no instances provided"); + + // Sort by (trace_height, caller_index) to match InstanceShapes::from_trace_heights. + let mut perm: Vec = (0..instances.len()).collect(); + perm.sort_by_key(|&i| (instances[i].1.trace.height(), i)); + + for (i, &orig_idx) in perm.iter().enumerate() { + let &(air, ref witness, aux_builder) = &instances[orig_idx]; + + air.validate() + .unwrap_or_else(|e| panic!("AIR validation failed for instance {i}: {e}")); + + let main = witness.trace; + let height = main.height(); + + // Main trace dimensions. + assert!( + height.is_power_of_two(), + "instance {i}: trace height {height} is not a power of two" + ); + assert_eq!( + main.width, + air.width(), + "instance {i}: main trace width mismatch: expected {}, got {}", + air.width(), + main.width + ); + assert_eq!( + witness.public_values.len(), + air.num_public_values(), + "instance {i}: public values length mismatch: expected {}, got {}", + air.num_public_values(), + witness.public_values.len() + ); + assert_eq!( + witness.var_len_public_inputs.len(), + air.num_var_len_public_inputs(), + "instance {i}: var-len public inputs count mismatch: expected {}, got {}", + air.num_var_len_public_inputs(), + witness.var_len_public_inputs.len() + ); + assert!( + challenges.len() >= air.num_randomness(), + "instance {i}: not enough challenges: need {}, got {}", + air.num_randomness(), + challenges.len() + ); + + // Build auxiliary trace. + let (aux_trace, aux_values) = + aux_builder.build_aux_trace(main, &challenges[..air.num_randomness()]); + + // Auxiliary trace dimensions. + assert_eq!( + aux_trace.height(), + height, + "instance {i}: aux trace height mismatch: expected {height}, got {}", + aux_trace.height() + ); + assert_eq!( + aux_trace.width, + air.aux_width(), + "instance {i}: aux trace width mismatch: expected {}, got {}", + air.aux_width(), + aux_trace.width + ); + assert_eq!( + aux_values.len(), + air.num_aux_values(), + "instance {i}: aux values count mismatch: expected {}, got {}", + air.num_aux_values(), + aux_values.len() + ); + + check_single_trace( + air, + main, + &aux_trace, + &aux_values, + witness.public_values, + challenges, + i, + ); + } +} + +/// Check constraints for one instance's traces row by row. +fn check_single_trace( + air: &A, + main: &RowMajorMatrix, + aux_trace: &RowMajorMatrix, + aux_values: &[EF], + public_values: &[F], + challenges: &[EF], + instance_index: usize, +) where + F: Field, + EF: ExtensionField, + A: LiftedAir, +{ + let height = main.height(); + let periodic_matrix = air.periodic_columns_matrix(); + for row in 0..height { + let next_row = (row + 1) % height; + + // Main trace rows. + let main_current = main.row_slice(row).unwrap(); + let main_next = main.row_slice(next_row).unwrap(); + + // Aux trace rows. + let aux_current = aux_trace.row_slice(row).unwrap(); + let aux_next = aux_trace.row_slice(next_row).unwrap(); + + // Periodic values for this row via modulo indexing into the periodic table. + let periodic_row = periodic_matrix.as_ref().map(|m| m.row_slice(row % m.height()).unwrap()); + let periodic_values: &[F] = periodic_row.as_deref().unwrap_or(&[]); + + let mut builder = DebugConstraintBuilder { + main: RowWindow::from_two_rows(&main_current, &main_next), + permutation: RowWindow::from_two_rows(&aux_current, &aux_next), + randomness: &challenges[..air.num_randomness()], + public_values, + periodic_values, + permutation_values: aux_values, + is_first_row: F::from_bool(row == 0), + is_last_row: F::from_bool(row == height - 1), + is_transition: F::from_bool(row != height - 1), + instance_index, + row_index: row, + }; + + debug_assert!(air.is_valid_builder(&builder).is_ok()); + + air.eval(&mut builder); + } +} + +// ============================================================================ +// DebugConstraintBuilder +// ============================================================================ + +/// Lightweight constraint builder that checks constraints against concrete trace values. +/// +/// Evaluates constraints row-by-row and panics immediately on the first nonzero constraint. +/// Uses base field `F` for the main trace and extension field `EF` for the auxiliary +/// (permutation) trace, matching the actual field layout of lifted STARK traces. +struct DebugConstraintBuilder<'a, F: Field, EF: ExtensionField> { + main: RowWindow<'a, F>, + permutation: RowWindow<'a, EF>, + randomness: &'a [EF], + public_values: &'a [F], + periodic_values: &'a [F], + permutation_values: &'a [EF], + is_first_row: F, + is_last_row: F, + is_transition: F, + instance_index: usize, + row_index: usize, +} + +impl<'a, F, EF> AirBuilder for DebugConstraintBuilder<'a, F, EF> +where + F: Field, + EF: ExtensionField, +{ + type F = F; + type Expr = F; + type Var = F; + type PreprocessedWindow = EmptyWindow; + type MainWindow = RowWindow<'a, F>; + type PublicVar = F; + + fn main(&self) -> Self::MainWindow { + self.main + } + + fn preprocessed(&self) -> &Self::PreprocessedWindow { + EmptyWindow::empty_ref() + } + + fn is_first_row(&self) -> Self::Expr { + self.is_first_row + } + + fn is_last_row(&self) -> Self::Expr { + self.is_last_row + } + + fn is_transition_window(&self, size: usize) -> Self::Expr { + assert!(size <= 2, "only two-row windows are supported, got {size}"); + self.is_transition + } + + fn assert_zero>(&mut self, x: I) { + assert_eq!( + x.into(), + F::ZERO, + "constraint not satisfied at instance {}, row {}", + self.instance_index, + self.row_index + ); + } + + fn public_values(&self) -> &[Self::PublicVar] { + self.public_values + } +} + +impl ExtensionBuilder for DebugConstraintBuilder<'_, F, EF> +where + F: Field, + EF: ExtensionField, +{ + type EF = EF; + type ExprEF = EF; + type VarEF = EF; + + fn assert_zero_ext(&mut self, x: I) + where + I: Into, + { + assert_eq!( + x.into(), + EF::ZERO, + "ext constraint not satisfied at instance {}, row {}", + self.instance_index, + self.row_index + ); + } +} + +impl<'a, F, EF> PermutationAirBuilder for DebugConstraintBuilder<'a, F, EF> +where + F: Field, + EF: ExtensionField, +{ + type MP = RowWindow<'a, EF>; + type RandomVar = EF; + type PermutationVar = EF; + + fn permutation(&self) -> Self::MP { + self.permutation + } + + fn permutation_randomness(&self) -> &[Self::RandomVar] { + self.randomness + } + + fn permutation_values(&self) -> &[Self::PermutationVar] { + self.permutation_values + } +} + +impl PeriodicAirBuilder for DebugConstraintBuilder<'_, F, EF> +where + F: Field, + EF: ExtensionField, +{ + type PeriodicVar = F; + + fn periodic_values(&self) -> &[Self::PeriodicVar] { + self.periodic_values + } +} diff --git a/stark/miden-lifted-stark/src/instance.rs b/stark/miden-lifted-stark/src/instance.rs new file mode 100644 index 0000000000..fb64d46671 --- /dev/null +++ b/stark/miden-lifted-stark/src/instance.rs @@ -0,0 +1,342 @@ +//! Protocol-level instance types for the lifted STARK prover and verifier. +//! +//! - [`AirInstance`]: Verifier instance — public values + variable-length inputs +//! - [`AirWitness`]: Prover witness — trace + public values +//! - [`InstanceShapes`]: Per-instance trace heights carried on [`StarkProof`](crate::StarkProof) + +extern crate alloc; + +use alloc::{vec, vec::Vec}; + +use miden_lifted_air::{AirStructureError, LiftedAir, VarLenPublicInputs, log2_strict_u8}; +use p3_challenger::CanObserve; +use p3_field::{Field, PrimeCharacteristicRing, TwoAdicField}; +use p3_matrix::{Matrix, dense::RowMajorMatrix}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +// ============================================================================ +// Instance data +// ============================================================================ + +/// Verifier instance: public values and variable-length inputs. +/// +/// Both the prover and verifier carry `var_len_public_inputs`. The verifier uses +/// them in [`LiftedAir::reduced_aux_values`] for the cross-AIR identity check. +/// +/// Log trace heights are not part of the instance — they are carried on the +/// [`StarkProof`](crate::StarkProof) as [`InstanceShapes`] and absorbed into +/// the Fiat-Shamir state. +#[derive(Clone, Copy, Debug)] +pub struct AirInstance<'a, F> { + /// Public values for this AIR. + pub public_values: &'a [F], + /// Reducible inputs for the cross-AIR identity check. Empty slice if no buses. + pub var_len_public_inputs: VarLenPublicInputs<'a, F>, +} + +/// Prover witness: trace matrix, public values, and variable-length public inputs. +/// +/// Validates on construction that the trace height is a power of two. +/// +/// **Commitment:** callers **must** bind both `public_values` and +/// `var_len_public_inputs` to the Fiat-Shamir challenger state before proving. +#[derive(Clone, Copy, Debug)] +pub struct AirWitness<'a, F> { + /// Main trace matrix. + pub trace: &'a RowMajorMatrix, + /// Public values for this AIR. + pub public_values: &'a [F], + /// Variable-length public inputs (reducible inputs for bus identity checks). + pub var_len_public_inputs: VarLenPublicInputs<'a, F>, +} + +impl<'a, F> AirWitness<'a, F> { + /// Create a new prover witness with validation. + /// + /// # Panics + /// + /// - If `trace.height()` is not a power of two + pub fn new( + trace: &'a RowMajorMatrix, + public_values: &'a [F], + var_len_public_inputs: VarLenPublicInputs<'a, F>, + ) -> Self + where + F: Field, + { + assert!( + trace.height().is_power_of_two(), + "trace height must be power of two, got {}", + trace.height() + ); + Self { + trace, + public_values, + var_len_public_inputs, + } + } + + /// Convert to a verifier instance (drops the trace). + pub fn to_instance(&self) -> AirInstance<'a, F> { + AirInstance { + public_values: self.public_values, + var_len_public_inputs: self.var_len_public_inputs, + } + } +} + +// ============================================================================ +// Shape metadata +// ============================================================================ + +/// Per-instance shape metadata carried on [`StarkProof`](crate::StarkProof). +/// +/// Stores log₂ trace heights (absorbed into the Fiat-Shamir challenger) +/// and the AIR ordering (not absorbed — see [`air_order`](Self::air_order)). +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct InstanceShapes { + // `pub(crate)` so in-crate tests can construct malformed shapes to + // exercise the verifier-path validation in `validate_inputs`. External + // callers go through `InstanceShapes::from_trace_heights`. + pub(crate) log_trace_heights: Vec, + /// The AIR ordering: `air_order[j]` is the caller's original index of + /// the instance at position `j` in the proof's ordering. + pub(crate) air_order: Vec, +} + +impl InstanceShapes { + /// Construct from raw trace heights (must be powers of two). + /// + /// Determines the proof's AIR ordering by sorting instances by + /// `(log_trace_height, caller_index)`. The resulting + /// [`air_order`](Self::air_order) maps each position in the proof's + /// ordering back to the caller's original index. + pub fn from_trace_heights(trace_heights: Vec) -> Result { + let log_heights: Vec = trace_heights + .iter() + .map(|&h| { + if !h.is_power_of_two() { + return Err(InstanceValidationError::InvalidTraceHeight { height: h }); + } + Ok(log2_strict_u8(h)) + }) + .collect::>()?; + + // Sort by (log_height, caller_index) for a canonical ordering. + let mut perm: Vec = (0..log_heights.len()).collect(); + perm.sort_by_key(|&i| (log_heights[i], i)); + + let sorted_log_heights: Vec = perm.iter().map(|&i| log_heights[i]).collect(); + let air_order: Vec = perm.iter().map(|&i| i as u32).collect(); + + Ok(Self { + log_trace_heights: sorted_log_heights, + air_order, + }) + } + + /// Log₂ of the trace height for each instance, in the proof's AIR + /// ordering. + pub fn log_trace_heights(&self) -> &[u8] { + &self.log_trace_heights + } + + /// The AIR ordering used by the proof: `air_order()[j]` is the caller's + /// original index of the instance at position `j` in the proof's + /// ordering. Not absorbed into the Fiat-Shamir transcript. + pub fn air_order(&self) -> &[u32] { + &self.air_order + } + + pub(crate) fn len(&self) -> usize { + self.log_trace_heights.len() + } + + /// Reorder `data` from the caller's natural order to the proof's AIR + /// ordering. Returns a `Vec` where position `j` holds + /// `data[air_order[j]]`. + /// + /// Validates that `air_order` is a valid permutation before applying it. + /// Returns an error if lengths mismatch or if `air_order` is malformed. + pub(crate) fn reorder(&self, mut data: Vec) -> Result, InstanceValidationError> { + let n = data.len(); + validate_air_order(&self.air_order, n)?; + let mut placed = vec![false; n]; + for start in 0..n { + if placed[start] { + continue; + } + let mut j = start; + loop { + let src = self.air_order[j] as usize; + placed[j] = true; + if src == start { + break; + } + data.swap(j, src); + j = src; + } + } + Ok(data) + } + + pub fn size_in_bytes(&self) -> usize { + size_of_val(self.log_trace_heights.as_slice()) + size_of_val(self.air_order.as_slice()) + } + + /// Absorb the log trace heights into a Fiat-Shamir challenger as one + /// base field element per `log_h`. The `air_order` values are **not** + /// absorbed. + pub(crate) fn observe_heights(&self, challenger: &mut C) + where + F: Field + PrimeCharacteristicRing, + C: CanObserve, + { + for &h in &self.log_trace_heights { + challenger.observe(F::from_u8(h)); + } + } +} + +// ============================================================================ +// Validation +// ============================================================================ + +/// Errors from validating instance- and protocol-level inputs. +#[derive(Debug, Error)] +pub enum InstanceValidationError { + #[error(transparent)] + AirStructure(#[from] AirStructureError), + #[error("no instances provided")] + Empty, + #[error("trace height {height} is not a power of two")] + InvalidTraceHeight { height: usize }, + #[error("trace width mismatch: expected {expected}, got {actual}")] + WidthMismatch { expected: usize, actual: usize }, + #[error("public values length mismatch: expected {expected}, got {actual}")] + PublicValuesMismatch { expected: usize, actual: usize }, + #[error("var-len public inputs count mismatch: expected {expected}, got {actual}")] + VarLenPublicInputsMismatch { expected: usize, actual: usize }, + #[error("trace height {trace_height} is less than max periodic column length {max_period}")] + TraceHeightBelowPeriod { trace_height: usize, max_period: usize }, + #[error( + "instance count {instances} does not match log trace heights count {log_trace_heights}" + )] + HeightCountMismatch { + instances: usize, + log_trace_heights: usize, + }, + #[error("LDE domain log-size {log_h} + {log_blowup} exceeds field two-adicity {two_adicity}")] + LdeDomainExceedsTwoAdicity { + log_h: u8, + log_blowup: u8, + two_adicity: usize, + }, + #[error("air_order length {air_order} does not match instance count {instances}")] + AirOrderLengthMismatch { instances: usize, air_order: usize }, + #[error("invalid air_order permutation for {count} instances")] + InvalidAirOrder { count: usize }, + #[error("log trace heights are not in ascending order")] + HeightsNotAscending, +} + +/// Cross-check instances against their shapes and return the log of the +/// maximum trace height. +/// +/// Instances and shapes must already be in the proof's AIR ordering. +/// +/// Checks: +/// - shape count matches instance count +/// - each `log_h + log_blowup` fits in both `F::TWO_ADICITY` and `usize::BITS - 1` (guards +/// downstream `two_adic_generator` and `1usize << log_lde_height` against wire-format shapes; the +/// `usize` bound only bites on 32-bit targets) +/// - each AIR is structurally valid ([`LiftedAir::validate`]) +/// - each instance's public values / var-len inputs match its AIR +/// - max height ≥ 2 (needed for the 2-row transition window) +/// - each trace height covers the AIR's longest periodic column +pub(crate) fn validate_inputs( + instances: &[(&A, AirInstance<'_, F>)], + shapes: &InstanceShapes, + log_blowup: u8, +) -> Result +where + F: TwoAdicField, + A: LiftedAir, +{ + if instances.len() != shapes.len() { + return Err(InstanceValidationError::HeightCountMismatch { + instances: instances.len(), + log_trace_heights: shapes.len(), + }); + } + // Upper bound on `log_h + log_blowup`: the two-adic generator must exist, + // and `1usize << log_lde_height` must not overflow on this target. + let max_log_lde_height = F::TWO_ADICITY.min((usize::BITS - 1) as usize); + let mut log_prev: u8 = 0; + for ((air, inst), &log_h) in instances.iter().zip(shapes.log_trace_heights()) { + if log_h as usize + log_blowup as usize > max_log_lde_height { + return Err(InstanceValidationError::LdeDomainExceedsTwoAdicity { + log_h, + log_blowup, + two_adicity: F::TWO_ADICITY, + }); + } + air.validate()?; + let expected_pv = air.num_public_values(); + if inst.public_values.len() != expected_pv { + return Err(InstanceValidationError::PublicValuesMismatch { + expected: expected_pv, + actual: inst.public_values.len(), + }); + } + let expected_vl = air.num_var_len_public_inputs(); + if inst.var_len_public_inputs.len() != expected_vl { + return Err(InstanceValidationError::VarLenPublicInputsMismatch { + expected: expected_vl, + actual: inst.var_len_public_inputs.len(), + }); + } + if log_h < log_prev { + return Err(InstanceValidationError::HeightsNotAscending); + } + let trace_height = 1usize << log_h as usize; + let max_period = air.periodic_columns().iter().map(Vec::len).max().unwrap_or(0); + if trace_height < max_period { + return Err(InstanceValidationError::TraceHeightBelowPeriod { + trace_height, + max_period, + }); + } + log_prev = log_h; + } + // `log_prev == 0` catches both "no instances" and "all traces are + // height 1" — both break the 2-row transition window. + if log_prev == 0 { + return Err(InstanceValidationError::Empty); + } + Ok(log_prev) +} + +/// Validate that `air_order` is a valid permutation of `0..n`. +/// +/// Called on the verifier side where `air_order` comes from an untrusted proof. +pub(crate) fn validate_air_order( + air_order: &[u32], + n: usize, +) -> Result<(), InstanceValidationError> { + if air_order.len() != n { + return Err(InstanceValidationError::AirOrderLengthMismatch { + instances: n, + air_order: air_order.len(), + }); + } + let mut seen = vec![false; n]; + for &idx in air_order { + let Some(slot @ false) = seen.get_mut(idx as usize) else { + return Err(InstanceValidationError::InvalidAirOrder { count: n }); + }; + *slot = true; + } + Ok(()) +} diff --git a/stark/miden-lifted-stark/src/lib.rs b/stark/miden-lifted-stark/src/lib.rs new file mode 100644 index 0000000000..3e872c41f5 --- /dev/null +++ b/stark/miden-lifted-stark/src/lib.rs @@ -0,0 +1,209 @@ +//! Lifted STARK prover and verifier (LMCS-based). +//! +//! This crate implements the lifted STARK protocol combining LMCS (Lifted Matrix +//! Commitment Scheme), DEEP quotient construction, and FRI for low-degree testing. +//! +//! # AIR Trust Model +//! +//! The lifted STARK has three trust domains: +//! +//! 1. **AIR = trusted** — [`air::LiftedAir`] implementations are correct application code. It is +//! the AIR implementer's responsibility to satisfy the contract below. +//! [`air::LiftedAir::validate`] checks the statically-verifiable subset. +//! +//! 2. **Instance = validated** — The prover validates that its witness matches the AIR spec. The +//! verifier validates instance metadata. Both return structured errors. +//! +//! 3. **Proof = untrusted** — Transcript data is verified cryptographically (PCS errors, constraint +//! mismatch, etc.). +//! +//! ## Validated properties +//! +//! These are checked by [`air::LiftedAir::validate`] and by the internal +//! instance validator in [`instance`], and enforced by both prover and +//! verifier before proceeding: +//! +//! - **No preprocessed trace** — the lifted protocol does not support them. +//! - **Positive aux width** — every AIR must have an auxiliary trace. +//! - **Periodic columns** — each has positive, power-of-two length ≤ trace height. +//! - **Constraint degree** — `log_quotient_degree() ≤ log_blowup`. +//! - **Instance dimensions** — trace width, public values length, var-len public inputs count, and +//! trace height (power of two) all match the AIR specification. +//! +//! ## Unchecked trust assumptions +//! +//! These cannot be verified statically and are the AIR implementer's responsibility: +//! +//! 1. **Window size** — Only transition window size 2. +//! 2. **Deterministic constraints** — `eval()` emits the same number and types of constraints +//! regardless of builder implementation. +//! 3. **Consistent aux builder** — `AuxBuilder::build_aux_trace` returns width = `aux_width()`, +//! height = main trace height, and exactly `num_aux_values()` values. (The prover asserts these +//! at runtime as a defense-in-depth sanity check.) +//! 4. **Sound `reduced_aux_values`** — Returns correct bus contributions for valid inputs. + +#![no_std] + +extern crate alloc; + +// ============================================================================ +// Private implementation modules +// ============================================================================ + +mod config; +mod coset; +pub mod debug; +pub mod instance; +pub mod lmcs; +mod pcs; +pub mod proof; +pub mod prover; +mod selectors; +pub mod verifier; + +pub use config::{GenericStarkConfig, StarkConfig}; +pub use coset::LiftedCoset; +pub use debug::{check_constraints, check_constraints_multi}; +pub use instance::{AirInstance, AirWitness, InstanceShapes, InstanceValidationError}; +pub use lmcs::{ + Lmcs, LmcsError, LmcsTree, OpenedRows, + bitrev::{BitReversibleMatrix, materialize_bitrev}, + config::LmcsConfig, + hiding_config::HidingLmcsConfig, + lifted_tree::LiftedMerkleTree, + merkle_witness::MerkleWitness, + node_id::NodeId, + proof::{ + BatchProof as LmcsBatchProof, BatchProofView as LmcsBatchProofView, + LeafOpening as LmcsLeafOpening, Proof as LmcsProof, + }, + row_list::RowList, + tree_indices::{MissingSiblingsIter, TreeIndices}, + utils::log2_strict_u8, +}; +pub use pcs::{ + deep::{ + proof::{DeepTranscript, OpenedValues as PcsOpenedValues}, + verifier::DeepError, + }, + fri::{ + proof::{FriRoundTranscript, FriTranscript}, + verifier::FriError, + }, + params::{PcsParams, PcsParamsError}, + proof::PcsTranscript, + verifier::PcsError, +}; +pub use proof::{StarkDigest, StarkOutput, StarkProof, StarkTranscript}; +pub use prover::{ProverError, prove_multi, prove_single}; +pub use verifier::{VerifierError, verify_multi, verify_single}; + +/// Backward-compatible PCS namespace. +/// +/// Older consumers accessed DEEP/FRI/PCS types through `miden_lifted_stark::fri`. +/// The current implementation organizes them under an internal `pcs` module, so this +/// public facade preserves the earlier module path. +pub mod fri { + pub use crate::{ + DeepError, DeepTranscript, FriError, FriRoundTranscript, FriTranscript, PcsError, + PcsOpenedValues, PcsParams, PcsParamsError, PcsTranscript, + }; + + pub mod deep { + pub use crate::{DeepError, DeepTranscript, PcsOpenedValues}; + + pub mod proof { + pub use crate::{DeepTranscript, PcsOpenedValues}; + } + + pub mod verifier { + pub use crate::DeepError; + } + } + + pub mod params { + pub use crate::{PcsParams, PcsParamsError}; + } + + pub mod proof { + pub use crate::PcsTranscript; + } + + pub mod round_proof { + pub use crate::{FriRoundTranscript, FriTranscript}; + } + + pub mod verifier { + pub use crate::{FriError, PcsError}; + } +} + +// ============================================================================ +// Namespaced re-exports from upstream crates +// ============================================================================ + +/// AIR traits, instance/witness types, and upstream `p3-air` re-exports. +/// +/// This module re-exports items from [`miden_lifted_air`], which in turn +/// re-exports `p3-air` types. Consumers should never need to depend on `p3-air` +/// directly. +pub mod air { + pub use miden_lifted_air::{ + // Upstream p3-air re-exports + Air, + AirBuilder, + AirBuilderWithContext, + // Lifted AIR types + AirStructureError, + AuxBuilder, + BaseAir, + EmptyWindow, + ExtensionBuilder, + FilteredAirBuilder, + LiftedAir, + LiftedAirBuilder, + PeriodicAirBuilder, + PermutationAirBuilder, + ReducedAuxValues, + ReductionError, + RowWindow, + TracePart, + VarLenPublicInputs, + WindowAccess, + log2_strict_u8, + }; + + /// Symbolic constraint analysis types from upstream p3-air. + pub mod symbolic { + pub use miden_lifted_air::symbolic::*; + } + + /// Auxiliary trace types (builder, cross-AIR identity checking). + pub mod auxiliary { + pub use miden_lifted_air::auxiliary::*; + } + + /// AIR constraint utility functions from upstream p3-air. + pub mod utils { + pub use miden_lifted_air::utils::*; + } +} + +/// Fiat-Shamir transcript channels and data types. +pub mod transcript { + pub use miden_stark_transcript::{TranscriptChallenger, TranscriptData, TranscriptError}; +} + +/// Stateful hasher primitives for LMCS construction. +pub mod hasher { + pub use miden_stateful_hasher::{ + Alignable, ChainingHasher, SerializingStatefulSponge, StatefulHasher, StatefulSponge, + }; +} + +/// Testing infrastructure: configurations, fixtures, and example AIRs. +/// +/// Available when the `testing` feature is enabled or during `cargo test`. +/// Integration tests should use `cargo test --features testing`. +#[cfg(any(test, feature = "testing"))] +pub mod testing; diff --git a/stark/miden-lifted-stark/src/lmcs/bitrev.rs b/stark/miden-lifted-stark/src/lmcs/bitrev.rs new file mode 100644 index 0000000000..d2f9216392 --- /dev/null +++ b/stark/miden-lifted-stark/src/lmcs/bitrev.rs @@ -0,0 +1,103 @@ +//! Local copy of `BitReversibleMatrix` with additional impls for [`FlatMatrixView`]. +//! +//! # Temporary stopgap +//! +//! The upstream `BitReversibleMatrix` trait in `p3-matrix` is only implemented for +//! [`DenseMatrix`], not for [`FlatMatrixView`]. This module provides an identical +//! trait with impls for all matrix types used by the LMCS and FRI. +//! +//! Once an upstream impl is available, this module can be removed and all uses +//! replaced with `p3_matrix::bitrev::BitReversibleMatrix`. + +use p3_field::{ExtensionField, Field}; +use p3_matrix::{ + Matrix, + bitrev::{BitReversalPerm, BitReversedMatrixView}, + dense::{DenseMatrix, DenseStorage, RowMajorMatrix}, + extension::FlatMatrixView, +}; + +/// Materialize a matrix into domain-ordered `BitReversedMatrixView>`. +/// +/// Temporary adapter for types that implement the upstream +/// [`p3_matrix::bitrev::BitReversibleMatrix`] but not this crate's local copy. +/// The returned type implements both traits and can be passed directly to +/// [`Lmcs::build_tree`](crate::lmcs::Lmcs::build_tree) / +/// [`Lmcs::build_aligned_tree`](crate::lmcs::Lmcs::build_aligned_tree). +/// +/// Remove alongside this module when upstream impls cover all DFT output types. +pub fn materialize_bitrev( + evals: impl p3_matrix::bitrev::BitReversibleMatrix, +) -> BitReversedMatrixView> { + BitReversalPerm::new_view(evals.bit_reverse_rows().to_row_major_matrix()) +} + +/// A matrix that supports bit-reversed row reordering. +/// +/// Local copy of `p3_matrix::bitrev::BitReversibleMatrix` extended with impls for +/// [`FlatMatrixView`]. +pub trait BitReversibleMatrix: Matrix { + /// The type returned when this matrix is viewed in bit-reversed order. + type BitRev: BitReversibleMatrix; + + /// Return a version of the matrix with its row order reversed by bit index. + fn bit_reverse_rows(self) -> Self::BitRev; +} + +// ============================================================================ +// DenseMatrix impls (mirrors upstream) +// ============================================================================ + +impl BitReversibleMatrix for DenseMatrix +where + T: Clone + Send + Sync, + S: DenseStorage, +{ + type BitRev = BitReversedMatrixView; + + fn bit_reverse_rows(self) -> Self::BitRev { + BitReversalPerm::new_view(self) + } +} + +impl BitReversibleMatrix for BitReversedMatrixView> +where + T: Clone + Send + Sync, + S: DenseStorage, +{ + type BitRev = DenseMatrix; + + fn bit_reverse_rows(self) -> Self::BitRev { + self.inner + } +} + +// ============================================================================ +// FlatMatrixView impls (not available upstream) +// ============================================================================ + +impl BitReversibleMatrix for FlatMatrixView +where + F: Field, + EF: ExtensionField, + Inner: Matrix, +{ + type BitRev = BitReversedMatrixView; + + fn bit_reverse_rows(self) -> Self::BitRev { + BitReversalPerm::new_view(self) + } +} + +impl BitReversibleMatrix for BitReversedMatrixView> +where + F: Field, + EF: ExtensionField, + Inner: Matrix, +{ + type BitRev = FlatMatrixView; + + fn bit_reverse_rows(self) -> Self::BitRev { + self.inner + } +} diff --git a/stark/miden-lifted-stark/src/lmcs/config.rs b/stark/miden-lifted-stark/src/lmcs/config.rs new file mode 100644 index 0000000000..f636428d4d --- /dev/null +++ b/stark/miden-lifted-stark/src/lmcs/config.rs @@ -0,0 +1,483 @@ +//! LMCS configuration types. + +use alloc::{collections::BTreeMap, vec::Vec}; +use core::marker::PhantomData; + +use miden_stark_transcript::VerifierChannel; +use miden_stateful_hasher::{Alignable, StatefulHasher}; +use p3_field::PackedValue; +use p3_matrix::Matrix; +use p3_symmetric::{Hash, PseudoCompressionFunction}; + +use crate::lmcs::{ + Lmcs, LmcsError, OpenedRows, + bitrev::BitReversibleMatrix, + lifted_tree::LiftedMerkleTree, + merkle_witness::MerkleWitness, + proof::{BatchProof, LeafOpening}, + row_list::RowList, + tree_indices::TreeIndices, +}; + +/// LMCS configuration holding cryptographic primitives (sponge + compression). +/// +/// This implementation defines the transcript hint layout used by +/// [`LmcsTree::prove_batch`](crate::lmcs::LmcsTree::prove_batch) and consumed by +/// `open_batch` and [`Lmcs::read_batch_proof`]: +/// - For each *distinct* query index (in caller order, skipping duplicates): one row per matrix (in +/// leaf order), then `SALT_ELEMS` field elements of salt. +/// - After all indices: missing sibling hashes, level-by-level, left-to-right, bottom-to-top. +/// +/// Hints are not observed into the Fiat-Shamir challenger. +/// +/// `open_batch` expects `widths` and `log_max_height` to match the committed tree, +/// rejects empty `indices`, and ignores extra hint data. Widths must match the +/// committed row lengths (including any alignment padding if `build_aligned_tree` +/// was used). Duplicate indices are coalesced in the returned openings. +/// [`read_batch_proof`](crate::lmcs::Lmcs::read_batch_proof) parses +/// the same hint stream, hashes leaves, and reconstructs per-index authentication paths +/// without verifying against a commitment. Empty indices yield an empty map, and +/// out-of-range indices return `InvalidProof`. +/// +/// Padding note: +/// - LMCS does not enforce that aligned padding values are zero. Verifiers cannot distinguish zero +/// padding from arbitrary values unless they check those columns in the opened rows or constrain +/// them elsewhere. +/// +/// For hiding commitments with salt, use +/// [`HidingLmcsConfig`](crate::lmcs::hiding_config::HidingLmcsConfig) instead. +#[derive(Clone, Debug)] +pub struct LmcsConfig< + PF, + PD, + H, + C, + const WIDTH: usize, + const DIGEST: usize, + const SALT_ELEMS: usize = 0, +> { + /// Stateful sponge for hashing matrix rows into leaf hashes. + pub sponge: H, + /// 2-to-1 compression function for building internal tree nodes. + pub compress: C, + pub(crate) _phantom: PhantomData<(PF, PD)>, +} + +impl + LmcsConfig +{ + /// Create a new LMCS configuration. + #[inline] + pub const fn new(sponge: H, compress: C) -> Self { + Self { sponge, compress, _phantom: PhantomData } + } +} + +impl Lmcs + for LmcsConfig +where + PF: PackedValue + Default, + PD: PackedValue + Default, + H: StatefulHasher + + StatefulHasher + + Alignable + + Sync, + C: PseudoCompressionFunction<[PD::Value; DIGEST], 2> + + PseudoCompressionFunction<[PD; DIGEST], 2> + + Sync, +{ + type F = PF::Value; + type Commitment = Hash; + type Tree> = LiftedMerkleTree; + type BatchProof = BatchProof; + + /// Build a tree from domain-ordered matrices with no transcript padding (alignment = 1). + /// + /// Extracts the inner bit-reversed matrices and stores them. + /// + /// Preconditions: + /// - `leaves` is non-empty. + /// - Matrix heights are powers of two and sorted by height (shortest to tallest). + /// + /// Panics if `leaves` is empty. Incorrect height order commits to a different + /// lifted matrix than intended. + fn build_tree>(&self, leaves: Vec) -> Self::Tree { + const { assert!(SALT_ELEMS == 0) } + LiftedMerkleTree::build_with_alignment::( + &self.sponge, + &self.compress, + leaves, + None, + 1, + ) + } + + /// Build a tree from domain-ordered matrices using the hasher alignment for transcript + /// padding. + /// + /// Extracts the inner bit-reversed matrices and stores them. + /// + /// Preconditions: + /// - `leaves` is non-empty. + /// - Matrix heights are powers of two and sorted by height (shortest to tallest). + /// + /// Panics if `leaves` is empty. Incorrect height order commits to a different + /// lifted matrix than intended. + fn build_aligned_tree>( + &self, + leaves: Vec, + ) -> Self::Tree { + const { assert!(SALT_ELEMS == 0) } + LiftedMerkleTree::build_with_alignment::( + &self.sponge, + &self.compress, + leaves, + None, + >::ALIGNMENT, + ) + } + + fn hash<'a, I>(&self, rows: I) -> Self::Commitment + where + I: IntoIterator, + Self::F: 'a, + { + let mut state = [PD::Value::default(); WIDTH]; + for row in rows { + self.sponge.absorb_into(&mut state, row.iter().cloned()); + } + let digest: [PD::Value; DIGEST] = self.sponge.squeeze(&state); + Hash::from(digest) + } + + fn compress(&self, left: Self::Commitment, right: Self::Commitment) -> Self::Commitment { + let left_digest = *left.as_ref(); + let right_digest = *right.as_ref(); + Hash::from(self.compress.compress([left_digest, right_digest])) + } + + /// Verify a batch opening from transcript hints. + /// + /// Security notes: + /// - `widths` and `log_max_height` must describe the committed tree; they are not checked. + /// - `widths` must match the committed row lengths (including any alignment padding if + /// `build_aligned_tree` was used); LMCS does not enforce that padded values are zero. + /// Verifiers cannot distinguish zero padding from arbitrary values unless they check the + /// opened rows or constrain them elsewhere. + /// - Empty `indices` returns `InvalidProof`. + /// - Duplicate indices are coalesced in the returned map (unique keys only). + /// - Out-of-range indices (>= 2^log_max_height) return `InvalidProof`. + /// - Missing siblings or malformed hints return `InvalidProof`. + /// - Extra hints are ignored and left unread. + /// - Returns `RootMismatch` only after a well-formed proof yields a different root. + /// + /// Leaf openings are read in **sorted tree index order** (ascending, deduplicated). + fn open_batch( + &self, + commitment: &Self::Commitment, + widths: &[usize], + indices: &TreeIndices, + channel: &mut Ch, + ) -> Result, LmcsError> + where + Ch: VerifierChannel, + { + if indices.is_empty() { + return Err(LmcsError::InvalidProof); + } + + // 1. Read openings and hash each into a leaf hash. + let mut opened_rows: BTreeMap> = BTreeMap::new(); + let mut leaf_hashes: Vec<(usize, Self::Commitment)> = Vec::with_capacity(indices.len()); + + for &index in indices.iter() { + let opening = + LeafOpening::<_, SALT_ELEMS>::read_from_channel(widths.to_vec(), channel)?; + leaf_hashes.push((index, opening.leaf_hash(self))); + opened_rows.insert(index, opening.rows); + } + + // 2. Recompute root by streaming siblings directly from the channel. + let tree = MerkleWitness::build( + leaf_hashes, + indices.depth() as usize, + |_| -> Result<_, LmcsError> { Ok(*channel.receive_hint_commitment()?) }, + |l, r| self.compress(l, r), + )?; + let computed_commitment = tree.root().ok_or(LmcsError::InvalidProof)?; + + if *computed_commitment != *commitment { + return Err(LmcsError::RootMismatch); + } + + Ok(opened_rows) + } + + /// Parse batch hints into per-index opening proofs. + /// + /// Reads openings, hashes leaves, builds a pruned tree, and extracts + /// authentication paths. Salt is stored as `Vec` in the output. + /// + /// Notes: + /// - `widths` must match the committed row lengths (including any alignment padding if + /// `build_aligned_tree` was used). + fn read_batch_proof( + &self, + widths: &[usize], + indices: &TreeIndices, + channel: &mut Ch, + ) -> Result + where + Ch: VerifierChannel, + { + let mut openings = BTreeMap::new(); + let mut leaf_hashes: Vec<(usize, Self::Commitment)> = Vec::with_capacity(indices.len()); + + for &index in indices.iter() { + let opening = + LeafOpening::<_, SALT_ELEMS>::read_from_channel(widths.to_vec(), channel)?; + leaf_hashes.push((index, opening.leaf_hash(self))); + openings.insert(index, opening); + } + + // 2. Build PrunedTree from leaf hashes + channel siblings. + let witness = MerkleWitness::build( + leaf_hashes, + indices.depth() as usize, + |_| -> Result<_, LmcsError> { Ok(*channel.receive_hint_commitment()?) }, + |l, r| self.compress(l, r), + )?; + + Ok(BatchProof { openings, witness }) + } + + fn alignment(&self) -> usize { + >::ALIGNMENT + } +} +// ============================================================================ +// Tests +// ============================================================================ + +#[cfg(test)] +mod tests { + use alloc::vec; + + use miden_stark_transcript::{ProverTranscript, TranscriptData, VerifierTranscript}; + use p3_field::PrimeCharacteristicRing; + use p3_matrix::dense::RowMajorMatrix; + + use super::*; + use crate::{ + lmcs::{LmcsTree, utils::log2_strict_u8}, + testing::configs::goldilocks_poseidon2 as gl, + }; + + fn small_matrix(height: usize, width: usize, seed: u64) -> RowMajorMatrix { + let values = (0..height * width).map(|i| gl::Felt::from_u64(seed + i as u64)).collect(); + RowMajorMatrix::new(values, width) + } + + #[test] + fn open_batch_cases() { + let lmcs = gl::test_lmcs(); + let matrices = vec![small_matrix(4, 2, 0), small_matrix(4, 3, 100)]; + let tree = lmcs.build_tree(matrices); + let widths = tree.aligned_widths(); + let log_max_height = log2_strict_u8(tree.height()); + let commitment = tree.root(); + + let ti = |indices: &[usize], depth: u8| { + TreeIndices::new(indices.iter().copied(), depth).unwrap() + }; + + let make_transcript = |indices: &TreeIndices| { + let mut prover_channel = gl::prover_channel(); + tree.prove_batch(indices, &mut prover_channel); + prover_channel.finalize() + }; + + let assert_open = |indices: &[usize]| { + let tree_indices = ti(indices, log_max_height); + let (prover_digest, transcript) = make_transcript(&tree_indices); + let mut verifier_channel = gl::verifier_channel(&transcript); + let opened = lmcs + .open_batch(&commitment, &widths, &tree_indices, &mut verifier_channel) + .unwrap(); + for &idx in indices { + assert_eq!(opened[&idx], tree.aligned_rows(idx)); + } + let verifier_digest = + verifier_channel.finalize().expect("transcript should finalize cleanly"); + assert_eq!(prover_digest, verifier_digest); + }; + + assert_open(&[0]); + assert_open(&[0, 1]); + assert_open(&[0, 2]); + assert_open(&[0, 1, 2, 3]); + assert_open(&[2, 2]); + + let tiny_tree = lmcs.build_tree(vec![small_matrix(1, 1, 7)]); + let widths_tiny = tiny_tree.aligned_widths(); + let log_tiny = log2_strict_u8(tiny_tree.height()); + let tiny_indices = ti(&[0], log_tiny); + let mut prover_channel = gl::prover_channel(); + tiny_tree.prove_batch(&tiny_indices, &mut prover_channel); + let (prover_digest, transcript) = prover_channel.finalize(); + let mut verifier_channel = gl::verifier_channel(&transcript); + let opened = lmcs + .open_batch(&tiny_tree.root(), &widths_tiny, &tiny_indices, &mut verifier_channel) + .unwrap(); + assert_eq!(opened[&0], tiny_tree.aligned_rows(0)); + let verifier_digest = + verifier_channel.finalize().expect("transcript should finalize cleanly"); + assert_eq!(prover_digest, verifier_digest); + + // oob index + assert_eq!(TreeIndices::new([tree.height()], log_max_height), Err(LmcsError::InvalidProof)); + + // wrong tree + let tree_indices_0 = ti(&[0], log_max_height); + let (_, transcript) = make_transcript(&tree_indices_0); + let mut verifier_channel = gl::verifier_channel(&transcript); + let wrong_tree = lmcs.build_tree(vec![small_matrix(4, 2, 999)]); + assert_eq!( + lmcs.open_batch(&wrong_tree.root(), &widths, &tree_indices_0, &mut verifier_channel,), + Err(LmcsError::RootMismatch) + ); + + // missing item from transcript + let (_, transcript) = make_transcript(&tree_indices_0); + let (fields, mut commitments) = transcript.into_parts(); + commitments.pop(); + let truncated = TranscriptData::new(fields, commitments); + let mut verifier_channel = gl::verifier_channel(&truncated); + assert_eq!( + lmcs.open_batch(&commitment, &widths, &tree_indices_0, &mut verifier_channel,), + Err(LmcsError::TranscriptError( + miden_stark_transcript::TranscriptError::NoMoreCommitments + )) + ); + + // empty indices + let empty_indices = ti(&[], log_max_height); + let (_, transcript) = gl::prover_channel().finalize(); + let mut verifier_channel = gl::verifier_channel(&transcript); + assert_eq!( + lmcs.open_batch(&commitment, &widths, &empty_indices, &mut verifier_channel), + Err(LmcsError::InvalidProof) + ); + } + + /// Reproduces the "root mismatch" bug when using Goldilocks + Blake3 (byte-based hash). + /// + /// The lifted STARK only tests with field-based Poseidon2, never with byte-based hashes. + /// This test isolates the LMCS layer to confirm that ChainingHasher + + /// CompressionFunctionFromHasher work correctly for commit-then-open. + #[test] + fn goldilocks_blake3_roundtrip() { + use alloc::{vec, vec::Vec}; + + use miden_stark_transcript::{ProverTranscript, VerifierTranscript}; + use miden_stateful_hasher::ChainingHasher; + use p3_blake3::Blake3; + use p3_challenger::{HashChallenger, SerializingChallenger64}; + use p3_symmetric::CompressionFunctionFromHasher; + + use crate::testing::configs::goldilocks_poseidon2::Felt; + + type Sponge = ChainingHasher; + type Compress = CompressionFunctionFromHasher; + const WIDTH: usize = 32; + const DIGEST: usize = 32; + type Blake3Lmcs = LmcsConfig; + type Challenger = SerializingChallenger64>; + + fn challenger() -> Challenger { + SerializingChallenger64::from_hasher(vec![], Blake3) + } + + let sponge = ChainingHasher::new(Blake3); + let compress = CompressionFunctionFromHasher::new(Blake3); + let lmcs: Blake3Lmcs = LmcsConfig::new(sponge, compress); + + // Single 4x2 matrix of constant values. + let values: Vec = (0..4 * 2).map(|i| Felt::from_u64(i as u64)).collect(); + let matrix = RowMajorMatrix::new(values, 2); + + let tree = lmcs.build_tree(vec![matrix]); + let widths = tree.aligned_widths(); + let log_max_height = log2_strict_u8(tree.height()); + let commitment = tree.root(); + + // Prove then verify a single index. + let indices = TreeIndices::new([0usize], log_max_height).unwrap(); + let mut prover_channel = ProverTranscript::new(challenger()); + tree.prove_batch(&indices, &mut prover_channel); + let (prover_digest, transcript) = prover_channel.finalize(); + + let mut verifier_channel = VerifierTranscript::from_data(challenger(), &transcript); + let opened = lmcs + .open_batch(&commitment, &widths, &indices, &mut verifier_channel) + .expect("Goldilocks+Blake3 LMCS roundtrip should verify"); + + assert_eq!(opened[&0], tree.aligned_rows(0)); + let verifier_digest = + verifier_channel.finalize().expect("transcript should finalize cleanly"); + assert_eq!(prover_digest, verifier_digest); + } + + /// Same as [`goldilocks_blake3_roundtrip`] but with a 24-byte BLAKE3 digest (BLAKE3-192). + #[test] + fn goldilocks_blake3_192_roundtrip() { + use alloc::{vec, vec::Vec}; + + use miden_stateful_hasher::{ChainingHasher, TruncatingHasher}; + use p3_blake3::Blake3; + use p3_challenger::{HashChallenger, SerializingChallenger64}; + use p3_symmetric::CompressionFunctionFromHasher; + + use crate::testing::configs::goldilocks_poseidon2::Felt; + + pub type Blake3_192 = TruncatingHasher; + + type Sponge = ChainingHasher; + type Compress = CompressionFunctionFromHasher; + const WIDTH: usize = 24; + const DIGEST: usize = 24; + type Blake3Lmcs = LmcsConfig; + type Challenger = SerializingChallenger64>; + + fn challenger() -> Challenger { + SerializingChallenger64::new(HashChallenger::new(Vec::new(), Blake3_192::new(Blake3))) + } + + let sponge = ChainingHasher::new(Blake3_192::new(Blake3)); + let compress = CompressionFunctionFromHasher::new(Blake3_192::new(Blake3)); + let lmcs: Blake3Lmcs = LmcsConfig::new(sponge, compress); + + let values: Vec = (0..4 * 2).map(|i| Felt::from_u64(i as u64)).collect(); + let matrix = RowMajorMatrix::new(values, 2); + + let tree = lmcs.build_tree(vec![matrix]); + let widths = tree.widths(); + let log_max_height = log2_strict_u8(tree.height()); + let commitment = tree.root(); + + let mut prover_channel = ProverTranscript::new(challenger()); + let indices = TreeIndices::new([0usize], log_max_height).unwrap(); + tree.prove_batch(&indices, &mut prover_channel); + let (prover_digest, transcript) = prover_channel.finalize(); + + let mut verifier_channel = VerifierTranscript::from_data(challenger(), &transcript); + let opened = lmcs + .open_batch(&commitment, &widths, &indices, &mut verifier_channel) + .expect("Goldilocks+Blake3-192 LMCS roundtrip should verify"); + + assert_eq!(opened[&0], tree.rows(0)); + let verifier_digest = + verifier_channel.finalize().expect("transcript should finalize cleanly"); + assert_eq!(prover_digest, verifier_digest); + } +} diff --git a/stark/miden-lifted-stark/src/lmcs/hiding_config.rs b/stark/miden-lifted-stark/src/lmcs/hiding_config.rs new file mode 100644 index 0000000000..26ed69dc78 --- /dev/null +++ b/stark/miden-lifted-stark/src/lmcs/hiding_config.rs @@ -0,0 +1,189 @@ +//! Hiding LMCS configuration types. + +use alloc::vec::Vec; +use core::cell::RefCell; + +use miden_stark_transcript::VerifierChannel; +use miden_stateful_hasher::{Alignable, StatefulHasher}; +use p3_field::PackedValue; +use p3_matrix::{Matrix, dense::RowMajorMatrix}; +use p3_symmetric::{Hash, PseudoCompressionFunction}; +use rand::{ + Rng, + distr::{Distribution, StandardUniform}, +}; + +use crate::lmcs::{ + Lmcs, LmcsError, OpenedRows, bitrev::BitReversibleMatrix, config::LmcsConfig, + lifted_tree::LiftedMerkleTree, proof::BatchProof, tree_indices::TreeIndices, +}; + +/// Configuration for hiding LMCS with random salt. +/// +/// This type wraps a [`LmcsConfig`] and adds an RNG for generating salt +/// during tree construction. The RNG is stored in a `RefCell` to allow +/// salt generation without `&mut self` (required by `Mmcs::commit`). +/// +/// `open_batch` delegates to the inner `LmcsConfig`, so hint layout and proof shape +/// match the non-hiding implementation except for +/// the presence of salt. The RNG is only used during tree construction. +/// +/// # Type Parameters +/// +/// - `PF`: Packed field element type for SIMD operations. +/// - `PD`: Packed hash word element type. +/// - `H`: Stateful hasher/sponge type. +/// - `C`: 2-to-1 compression function type. +/// - `R`: Random number generator type. +/// - `WIDTH`: State width for the hasher. +/// - `DIGEST`: Number of elements in a hash. +/// - `SALT`: Number of salt elements per leaf (must be > 0). +/// +/// # Security notes +/// - `SALT` should be sized so `SALT * sizeof(PF::Value)` meets the target security parameter. +/// - `R` should be an appropriately seeded CSPRNG; weaker RNGs can undermine hiding. +/// - Cloning this config clones RNG state. Re-seed if you need independent salts per config. +/// +/// # Example +/// +/// ```ignore +/// use p3_miden_lmcs::{HidingLmcsConfig, Lmcs, LmcsTree}; +/// use rand::rngs::StdRng; +/// use rand::SeedableRng; +/// +/// let rng = StdRng::seed_from_u64(42); +/// let config = +/// HidingLmcsConfig::::new(sponge, compress, rng); +/// +/// let tree = config.build_aligned_tree(matrices); +/// let root = tree.root(); +/// ``` +#[derive(Clone, Debug)] +pub struct HidingLmcsConfig< + PF, + PD, + H, + C, + R, + const WIDTH: usize, + const DIGEST: usize, + const SALT: usize, +> { + /// Inner non-hiding config with sponge and compression. + pub inner: LmcsConfig, + /// RNG for salt generation. Uses `RefCell` for interior mutability. + rng: RefCell, +} + +impl + HidingLmcsConfig +{ + /// Create a new hiding LMCS configuration. + /// + /// # Compile-time Error + /// + /// Fails to compile if `SALT == 0`. Use [`LmcsConfig`] for non-hiding commitments. + #[inline] + pub fn new(sponge: H, compress: C, rng: R) -> Self { + const { assert!(SALT > 0) } + Self { + inner: LmcsConfig::new(sponge, compress), + rng: RefCell::new(rng), + } + } +} + +impl Lmcs + for HidingLmcsConfig +where + PF: PackedValue + Default, + PD: PackedValue + Default, + R: Rng + Clone, + StandardUniform: Distribution, + H: StatefulHasher + + StatefulHasher + + Alignable + + Sync, + C: PseudoCompressionFunction<[PD::Value; DIGEST], 2> + + PseudoCompressionFunction<[PD; DIGEST], 2> + + Sync, +{ + type F = PF::Value; + type Commitment = Hash; + type Tree> = LiftedMerkleTree; + type BatchProof = BatchProof; + + /// Build a tree with per-leaf salt sampled from the RNG. + /// + /// Preconditions match `LmcsConfig::build_tree`; panics if `leaves` is empty. + fn build_tree>(&self, leaves: Vec) -> Self::Tree { + let tree_height = leaves.last().map(Matrix::height).unwrap_or(0); + let salt = RowMajorMatrix::rand(&mut *self.rng.borrow_mut(), tree_height, SALT); + LiftedMerkleTree::build_with_alignment::( + &self.inner.sponge, + &self.inner.compress, + leaves, + Some(salt), + 1, + ) + } + + /// Build a tree with per-leaf salt sampled from the RNG and hasher alignment padding. + /// + /// Preconditions match `LmcsConfig::build_tree`; panics if `leaves` is empty. + fn build_aligned_tree>( + &self, + leaves: Vec, + ) -> Self::Tree { + let tree_height = leaves.last().map(Matrix::height).unwrap_or(0); + let salt = RowMajorMatrix::rand(&mut *self.rng.borrow_mut(), tree_height, SALT); + LiftedMerkleTree::build_with_alignment::( + &self.inner.sponge, + &self.inner.compress, + leaves, + Some(salt), + >::ALIGNMENT, + ) + } + + fn hash<'a, I>(&self, rows: I) -> Self::Commitment + where + I: IntoIterator, + Self::F: 'a, + { + self.inner.hash(rows) + } + + fn compress(&self, left: Self::Commitment, right: Self::Commitment) -> Self::Commitment { + self.inner.compress(left, right) + } + + fn open_batch( + &self, + commitment: &Self::Commitment, + widths: &[usize], + indices: &TreeIndices, + channel: &mut Ch, + ) -> Result, LmcsError> + where + Ch: VerifierChannel, + { + self.inner.open_batch(commitment, widths, indices, channel) + } + + fn read_batch_proof( + &self, + widths: &[usize], + indices: &TreeIndices, + channel: &mut Ch, + ) -> Result + where + Ch: VerifierChannel, + { + self.inner.read_batch_proof(widths, indices, channel) + } + + fn alignment(&self) -> usize { + self.inner.alignment() + } +} diff --git a/stark/miden-lifted-stark/src/lmcs/lifted_tree.rs b/stark/miden-lifted-stark/src/lmcs/lifted_tree.rs new file mode 100644 index 0000000000..9a2a801f80 --- /dev/null +++ b/stark/miden-lifted-stark/src/lmcs/lifted_tree.rs @@ -0,0 +1,654 @@ +use alloc::{vec, vec::Vec}; +use core::{array, mem}; + +use miden_stark_transcript::ProverChannel; +use miden_stateful_hasher::StatefulHasher; +use p3_field::PackedValue; +use p3_matrix::{Matrix, dense::RowMajorMatrix}; +use p3_maybe_rayon::prelude::*; +use p3_symmetric::{Hash, PseudoCompressionFunction}; +use p3_util::{log2_strict_usize, reverse_bits_len}; +use tracing::info_span; + +use crate::lmcs::{ + LmcsTree, bitrev::BitReversibleMatrix, proof::LeafOpening, row_list::RowList, + tree_indices::TreeIndices, utils::PackedValueExt, +}; + +/// A uniform binary Merkle tree whose leaves are constructed from matrices with power-of-two +/// heights. +/// +/// # Type Parameters +/// +/// * `F` – scalar field element type used in both matrices and hash words. +/// * `D` – digest element type. +/// * `M` – matrix type. Must implement [`Matrix`]. +/// * `DIGEST_ELEMS` – number of elements in one digest. +/// * `SALT_ELEMS` – number of salt elements per leaf (0 = non-hiding, >0 = hiding). +/// +/// Unlike the standard `MerkleTree`, this uniform variant requires: +/// - **All matrix heights must be powers of two** +/// - **Matrices must be sorted by height** (shortest to tallest) +/// - Uses incremental hashing via [`StatefulHasher`] instead of one-shot hashing +/// +/// The per-leaf row composition uses nearest-neighbor upsampling: each matrix Mᵢ is virtually +/// extended to height N (width unchanged) by repeating each row rᵢ = N/nᵢ times +/// contiguously. For physical row index `j`, the sponge absorbs the `j`-th row from each +/// lifted matrix in sequence. The sponge applies its own padding semantics during absorption; +/// LMCS alignment only affects transcript hints. +/// +/// Leaf digests are squeezed directly into domain order (digest `i` comes from +/// state `bitrev(i)`) so the Merkle tree is indexed by **domain order** (natural index). +/// External callers address leaves by domain index; the internal row access maps +/// `domain_index → bitrev(domain_index)` to reach the same physical row that was hashed. +/// +/// Note: alignment padding is a convention for transcript openings and does not affect the +/// commitment. It is independent of the sponge's absorption alignment. LMCS does not enforce +/// that padded columns are zero; verifiers cannot distinguish zero padding from arbitrary values +/// unless they check those columns or constrain them elsewhere. +/// +/// Equivalent single-matrix view: this commitment is equivalent to first forming a single +/// height-`N` matrix by (a) lifting every input matrix to height `N`, (b) padding each lifted +/// matrix horizontally with zero columns to reflect the sponge's absorption alignment (if any), +/// and (c) concatenating the results side-by-side. The leaf hash at index `j` is then the +/// sponge of that single concatenated matrix's row `j`. This is a conceptual view: LMCS does +/// not enforce that those padded columns are zero. +/// +/// Since [`StatefulHasher`] operates on a single field type, this tree uses the same type `F` +/// for both matrix elements and hash words, unlike `MerkleTree` which can hash `F → W`. +/// +/// Use [`root`](Self::root) to fetch the final commitment once the tree is built. +/// +/// ## Transcript Hints +/// +/// `prove_batch` streams transcript hints in the format expected by +/// [`Lmcs::open_batch`](crate::lmcs::Lmcs::open_batch): +/// - For each unique query index **in sorted tree index order** (ascending, deduplicated): one row +/// per matrix (in leaf order), then `SALT_ELEMS` field elements of salt. +/// - Each row is padded with explicit zeros to the LMCS alignment. This allows verifiers to absorb +/// fixed-size chunks without special-casing the final partial chunk; padding is not enforced to +/// be zero. +/// - After all indices: missing sibling hashes, level-by-level, left-to-right, bottom-to-top. +/// +/// Hints are not observed into the Fiat-Shamir challenger. +/// +/// This generally shouldn't be used directly. If you're using a Merkle tree as an MMCS, +/// see the MMCS wrapper types. +#[derive(Debug)] +pub struct LiftedMerkleTree { + /// All leaf matrices in insertion order. + /// + /// Matrices must be sorted by height (shortest to tallest) and all heights must be + /// powers of two. Each matrix's rows are absorbed into sponge states that are + /// maintained and upsampled across matrices of increasing height. + /// + /// This vector is retained for inspection or re-opening of the tree; it is not used + /// after construction time. + pub(crate) leaves: Vec, + + /// All hash layers (digest arrays) in top-down order: index 0 is the root + /// (one hash) and the last layer contains the leaf hashes. + /// + /// This matches the top-down depth convention of [`NodeId`](crate::lmcs::node_id::NodeId): + /// `digest_layers[d]` has `2^d` entries, so `digest_layers[node.depth()][node.position()]` + /// gives direct access. + pub(crate) digest_layers: Vec>, + + /// Salt matrix for hiding commitment. Each row contains `SALT_ELEMS` random field elements. + /// `None` when `SALT_ELEMS = 0` (non-hiding mode). + pub(crate) salt: Option>, + /// Column alignment used for transcript proofs. + pub(crate) alignment: usize, +} + +impl + LmcsTree, M> for LiftedMerkleTree +where + F: Copy + Default + PartialEq + Send + Sync, + D: Copy + Default + PartialEq + Send + Sync, + M: Matrix, +{ + fn root(&self) -> Hash { + Hash::from(self.digest_layers[0][0]) + } + + fn height(&self) -> usize { + self.leaves.last().unwrap().height() + } + + fn leaves(&self) -> &[M] { + &self.leaves + } + + /// Return the upsampled rows for `index` with original matrix widths (no padding). + /// + /// Panics if `index` is out of range for the tree height. + fn rows(&self, index: usize) -> RowList { + self.collect_rows(index, self.widths()) + } + + /// Return the upsampled rows for `index`, padded to the tree's alignment. + /// + /// Padding uses `Default::default()` and is not enforced by verification; callers + /// that require zero padding must check these columns explicitly. + /// + /// Panics if `index` is out of range for the tree height. + fn aligned_rows(&self, index: usize) -> RowList { + self.collect_rows(index, self.aligned_widths()) + } + + fn alignment(&self) -> usize { + self.alignment + } + + fn widths(&self) -> Vec { + self.leaves.iter().map(Matrix::width).collect() + } + + /// Prove a batch opening and stream it into a transcript channel. + /// + /// Panics if any index is out of range. Rows are padded to `alignment` and those + /// padding values are not validated by verification; callers that require zero + /// padding must check the opened rows explicitly. + /// + /// Leaf openings are written in **sorted tree index order** (ascending, deduplicated). + fn prove_batch(&self, indices: &TreeIndices, channel: &mut Ch) + where + Ch: ProverChannel>, + { + // Stream leaf openings in sorted tree index order. + for &index in indices.iter() { + let opening = LeafOpening { + rows: self.aligned_rows(index), + salt: self.salt(index), + }; + opening.write_to_channel(channel); + } + + // Emit missing sibling hashes left-to-right, bottom-to-top. + for sibling in indices.missing_siblings() { + let hash = self.digest_layers[sibling.depth()][sibling.position()]; + channel.hint_commitment(Hash::from(hash)); + } + } +} + +impl + LiftedMerkleTree +where + F: Copy + Default + PartialEq + Send + Sync, + D: Copy + Default + PartialEq + Send + Sync, + M: Matrix, +{ + /// Build a tree from domain-ordered matrices with optional salt and explicit alignment. + /// + /// Matrices are bit-reversed internally before storage and hashing. + /// + /// Preconditions: + /// - `leaves` is non-empty and heights are powers of two. + /// - Matrices are sorted by height (shortest to tallest). + /// + /// `alignment` controls transcript padding only; it does not affect the commitment. + /// LMCS does not enforce that padded columns are zero. + /// + /// Panics if `leaves` is empty. + pub fn build_with_alignment( + h: &H, + c: &C, + leaves: Vec, + salt: Option>, + alignment: usize, + ) -> Self + where + DomainM: BitReversibleMatrix, + PF: PackedValue, + PD: PackedValue, + H: StatefulHasher + + StatefulHasher + + Sync, + C: PseudoCompressionFunction<[D; DIGEST_ELEMS], 2> + + PseudoCompressionFunction<[PD; DIGEST_ELEMS], 2> + + Sync, + { + const { assert!(PF::WIDTH == PD::WIDTH) } + assert!(!leaves.is_empty(), "cannot commit empty batch"); + debug_assert!(alignment > 0, "alignment must be non-zero"); + + let leaves: Vec = + leaves.into_iter().map(BitReversibleMatrix::bit_reverse_rows).collect(); + + // Build leaf hashes: absorb all matrix rows into sponge states, then squeeze. + let leaf_digests: Vec<[PD::Value; DIGEST_ELEMS]> = + info_span!("hash leaves").in_scope(|| { + let mut leaf_states: Vec<[PD::Value; WIDTH]> = + build_leaf_states_upsampled::(&leaves, h); + + // Absorb salt into states using SIMD-parallelized path (no-op when salt is None) + if let Some(ref salt_matrix) = salt { + debug_assert_eq!(salt_matrix.height(), leaf_states.len()); + debug_assert_eq!(salt_matrix.width(), SALT_ELEMS); + info_span!("absorb salt", height = salt_matrix.height(), width = SALT_ELEMS) + .in_scope(|| { + absorb_matrix::( + &mut leaf_states, + salt_matrix, + h, + ); + }); + } + + // Squeeze leaf hashes and bit-reverse in one pass: digest[i] = + // squeeze(state[bitrev(i)]). This places digests in domain order so + // the Merkle tree is indexed naturally. + let n = leaf_states.len(); + let log_n = log2_strict_usize(n); + (0..n) + .into_par_iter() + .map(|i| { + let src = reverse_bits_len(i, log_n); + h.squeeze(&leaf_states[src]) + }) + .collect() + }); + + // Build digest layers by repeatedly compressing until we reach the root, + // then reverse so index 0 = root, matching the top-down NodeId convention. + let digest_layers = info_span!("compress tree layers").in_scope(|| { + let mut digest_layers = vec![leaf_digests]; + loop { + let prev_layer = digest_layers.last().unwrap(); + if prev_layer.len() == 1 { + break; + } + + let next_layer = compress_uniform::(prev_layer, c); + digest_layers.push(next_layer); + } + digest_layers.reverse(); + digest_layers + }); + + Self { + leaves, + digest_layers, + salt, + alignment: alignment.max(1), + } + } + + /// Column alignment used when streaming openings. + pub fn alignment(&self) -> usize { + self.alignment + } + + /// Extract the salt for the given domain index. + /// + /// Maps `domain_index` to the physical salt row via `bitrev(domain_index)`, matching + /// the row that was absorbed during tree construction. + /// + /// # Panics + /// + /// Panics if `domain_index` is out of range, or if `SALT_ELEMS > 0` but the tree was + /// constructed without salt. + pub fn salt(&self, domain_index: usize) -> [F; SALT_ELEMS] { + match &self.salt { + Some(salt_matrix) => { + let physical_index = + reverse_bits_len(domain_index, log2_strict_usize(salt_matrix.height())); + let row = salt_matrix.row_slice(physical_index).expect("index must be valid"); + // Tree construction guarantees salt width == SALT_ELEMS + array::from_fn(|i| row[i]) + }, + None => { + // For SALT_ELEMS == 0, this returns an empty array. + // For SALT_ELEMS > 0, this should never be reached if using safe constructors. + debug_assert!(SALT_ELEMS == 0, "tree constructed without salt but SALT_ELEMS > 0"); + [F::default(); SALT_ELEMS] + }, + } + } + + /// Collect upsampled rows for `domain_index` into a flat `RowList` with the given widths. + /// + /// Maps the domain index to a bit-reversed row index: `bitrev(domain_index) >> k`. + /// This returns the same values that were hashed into Merkle leaf `domain_index` + /// (the tree is indexed by domain order after leaf digest permutation). + /// + /// Uses `Matrix::row()` to extend directly into a single pre-allocated buffer + /// without per-row allocations. + fn collect_rows(&self, domain_index: usize, widths: Vec) -> RowList { + let max_height = self.leaves.last().unwrap().height(); + let log_max_height = log2_strict_usize(max_height); + let bit_reversed = reverse_bits_len(domain_index, log_max_height); + let mut elems = Vec::with_capacity(widths.iter().sum()); + for (m, &padded_len) in self.leaves.iter().zip(&widths) { + // Map domain index to bit-reversed row: bitrev(domain_index) >> log₂(max_height/h). + let log_scaling = log2_strict_usize(max_height / m.height()); + elems.extend( + m.row(bit_reversed >> log_scaling) + .expect("row_index must be valid after upsampling"), + ); + elems.resize(elems.len() + padded_len - m.width(), F::default()); + } + RowList::new(elems, widths) + } +} + +/// Build leaf states using the upsampled view (nearest-neighbor upsampling). +/// +/// Returns the sponge states after absorbing all matrix rows but **before squeezing**. +/// Callers must squeeze the states to obtain final leaf hashes. +/// +/// Conceptually, each matrix is virtually extended to height `H` by repeating each row +/// `L = H / h` times (width unchanged), and the leaf `r` absorbs the `r`-th row from each +/// extended matrix in order. Each absorbed row is virtually padded with zeros to a multiple of the +/// hasher's padding width for absorption; see [`LiftedMerkleTree`](crate::lmcs::LiftedMerkleTree) +/// docs for the equivalent single-matrix view. +/// +/// Padding is implicit and not checked; callers that require zero padding must enforce +/// it elsewhere. +/// +/// # Preconditions +/// - `matrices` is non-empty and sorted by non-decreasing power-of-two heights. +/// - `P::WIDTH` is a power of two. +/// +/// Panics in debug builds if preconditions are violated. +fn build_leaf_states_upsampled( + matrices: &[M], + sponge: &H, +) -> Vec<[PD::Value; WIDTH]> +where + PF: PackedValue, + PD: PackedValue, + M: Matrix, + H: StatefulHasher + + StatefulHasher + + Sync, +{ + const { assert!(PF::WIDTH.is_power_of_two()) }; + const { assert!(PD::WIDTH.is_power_of_two()) }; + let final_height = validate_heights(matrices.iter().map(|d| d.dimensions().height)); + + // Memory buffers: + // - states: Per-leaf scalar states (one per final row), maintained across matrices. + // - scratch_states: Temporary buffer used when duplicating states during upsampling. + let default_state = [PD::Value::default(); WIDTH]; + let mut states = vec![default_state; final_height]; + let mut scratch_states = vec![default_state; final_height]; + + let mut active_height = matrices.first().unwrap().height(); + + for matrix in matrices { + let height = matrix.height(); + + // Upsample states when height increases (applies to both scalar and packed paths). + // Duplicate each existing state to fill the expanded height. + // E.g., [s0, s1] with scaling_factor=2 → [s0, s0, s1, s1] + if height > active_height { + let scaling_factor = height / active_height; + + // Copy `states` into `scratch_states`, repeating each entry `scaling_factor` times + // so we keep the accumulated sponge states aligned with the taller matrix. + scratch_states[..height] + .par_chunks_mut(scaling_factor) + .zip(states[..active_height].par_iter()) + .for_each(|(chunk, state)| chunk.fill(*state)); + + // Copy upsampled states back to canonical buffer + mem::swap(&mut scratch_states, &mut states); + } + + // Absorb the rows of the matrix into the extended state vector + info_span!("absorb matrix", height, width = matrix.width()).in_scope(|| { + absorb_matrix::(&mut states[..height], matrix, sponge) + }); + + active_height = height; + } + + states +} + +/// Incorporate one matrix's row-wise contribution into the running per-leaf states. +/// +/// Semantics: given `states` of length `h = matrix.height()`, for each row index `r ∈ [0, h)` +/// update `states[r]` by absorbing the matrix row `r` into that state. In the overall tree +/// construction, callers ensure that `states` is the correct lifted view for the current matrix +/// (either the "nearest-neighbor" duplication or the "modulo" duplication across the final +/// height). This helper performs exactly one absorption round for that matrix and returns with the +/// states mutated; it does not change the lifting shape or squeeze hashes. +fn absorb_matrix( + states: &mut [[PD::Value; WIDTH]], + matrix: &M, + sponge: &H, +) where + PF: PackedValue, + PD: PackedValue, + M: Matrix, + H: StatefulHasher + + StatefulHasher + + Sync, +{ + let height = matrix.height(); + assert_eq!(height, states.len()); + + if height < PF::WIDTH || PF::WIDTH == 1 { + // Scalar path: walk every final leaf state and absorb the wrapped row for this matrix. + states.par_iter_mut().zip(matrix.par_rows()).for_each(|(state, row)| { + sponge.absorb_into(state, row); + }); + } else { + // SIMD path: gather → absorb wrapped packed row → scatter per chunk. + states + .par_chunks_mut(PF::WIDTH) + .enumerate() + .for_each(|(packed_idx, states_chunk)| { + let mut packed_state: [PD; WIDTH] = PD::pack_columns(states_chunk); + let row_idx = packed_idx * PF::WIDTH; + let row = matrix.vertically_packed_row::(row_idx); + sponge.absorb_into(&mut packed_state, row); + PD::unpack_into(&packed_state, states_chunk); + }); + } +} + +/// Compress a layer of hashes in a uniform Merkle tree. +/// +/// Takes a layer of hashes and compresses pairs into a new layer with half as many elements. +/// The layer length must be a power of two. +/// +/// When the result would be smaller than the packing width, uses a pure scalar path. +/// Otherwise uses SIMD parallelization. Since both the result length and packing width are +/// powers of two, the result is always a multiple of the packing width in the SIMD path, +/// requiring no scalar fallback for remainders. +fn compress_uniform< + P: PackedValue, + C: PseudoCompressionFunction<[P::Value; DIGEST_ELEMS], 2> + + PseudoCompressionFunction<[P; DIGEST_ELEMS], 2> + + Sync, + const DIGEST_ELEMS: usize, +>( + prev_layer: &[[P::Value; DIGEST_ELEMS]], + c: &C, +) -> Vec<[P::Value; DIGEST_ELEMS]> { + assert!(prev_layer.len().is_power_of_two(), "previous layer length must be a power of 2"); + + let next_len = prev_layer.len() / 2; + let default_digest = [P::Value::default(); DIGEST_ELEMS]; + let mut next_digests = vec![default_digest; next_len]; + + // Use scalar path when output is too small for packing + if next_len < P::WIDTH || P::WIDTH == 1 { + next_digests.par_iter_mut().zip(prev_layer.par_chunks_exact(2)).for_each( + |(next_digest, prev_layer_pair)| { + *next_digest = c.compress([prev_layer_pair[0], prev_layer_pair[1]]); + }, + ); + } else { + // Packed path: since next_len and P::WIDTH are both powers of 2, + // next_len is a multiple of P::WIDTH, so no remainder handling needed. + next_digests.par_chunks_exact_mut(P::WIDTH).enumerate().for_each( + |(packed_chunk_idx, digests_chunk)| { + let chunk_idx = packed_chunk_idx * P::WIDTH; + let left: [P; DIGEST_ELEMS] = + array::from_fn(|j| P::from_fn(|k| prev_layer[2 * (chunk_idx + k)][j])); + let right: [P; DIGEST_ELEMS] = + array::from_fn(|j| P::from_fn(|k| prev_layer[2 * (chunk_idx + k) + 1][j])); + let packed_digest = c.compress([left, right]); + P::unpack_into(&packed_digest, digests_chunk); + }, + ); + } + next_digests +} + +/// Validate a sequence of matrix heights for LMCS. +/// +/// Requirements enforced: +/// - Non-empty sequence (at least one matrix). +/// - Every height is a power of two and non-zero. +/// - Heights are in non-decreasing order (sorted by height), so the last height is the maximum `H` +/// used by lifting. +/// +/// # Panics +/// Panics if any requirement is violated. +fn validate_heights(heights: impl IntoIterator) -> usize { + let mut active_height = 0; + + for (matrix, height) in heights.into_iter().enumerate() { + assert_ne!(height, 0, "zero height at matrix {matrix}"); + assert!(height.is_power_of_two(), "non-power-of-two height at matrix {matrix}"); + assert!(height >= active_height, "matrices must be sorted by height"); + active_height = height; + } + + assert_ne!(active_height, 0, "empty batch"); + active_height +} + +#[cfg(test)] +mod tests { + use alloc::vec; + + use p3_field::{Field, PackedValue, PrimeCharacteristicRing}; + use rand::{SeedableRng, rngs::SmallRng}; + + use super::*; + use crate::{ + lmcs::{tests::build_leaves_single, utils::aligned_len}, + testing::configs::goldilocks_poseidon2::{ + self as gl, DIGEST, Felt, PackedFelt, RATE, Sponge, + }, + }; + + /// Common matrix group scenarios for testing lifting with varying heights. + fn matrix_scenarios(rate: usize) -> Vec> { + let pack_width = P::WIDTH.max(2); + vec![ + // Single matrices + vec![(1, 1)], + vec![(1, rate - 1)], + // Multiple heights (must be ascending) + vec![(2, 3), (4, 5), (8, rate)], + vec![(1, 5), (1, 3), (2, 7), (4, 1), (8, rate + 1)], + // Packing boundary tests + vec![(pack_width / 2, rate - 1), (pack_width, rate), (pack_width * 2, rate + 3)], + vec![(pack_width, rate + 5), (pack_width * 2, 25)], + vec![ + (1, rate * 2), + (pack_width / 2, rate * 2 - 1), + (pack_width, rate * 2), + (pack_width * 2, rate * 3 - 2), + ], + // Same-height matrices + vec![(4, rate - 1), (4, rate), (8, rate + 3), (8, rate * 2)], + // Single tall matrix + vec![(pack_width * 2, rate - 1)], + ] + } + + /// Concatenate matrices horizontally, padding each to a multiple of `R`. + /// All matrices are lifted to the maximum height first. + fn concatenate_matrices( + matrices: &[RowMajorMatrix], + ) -> RowMajorMatrix { + let max_height = matrices.last().unwrap().height(); + let width: usize = matrices.iter().map(|m| aligned_len(m.width(), R)).sum(); + + let concatenated_data: Vec<_> = (0..max_height) + .flat_map(|idx| { + matrices.iter().flat_map(move |m| { + let mut row = m.row_slice(idx).unwrap().to_vec(); + let padded_width = aligned_len(row.len(), R); + row.resize(padded_width, F::ZERO); + row + }) + }) + .collect(); + RowMajorMatrix::new(concatenated_data, width) + } + + /// Upsample matrix to exactly `target_height` rows via nearest-neighbor repetition. + fn upsample_matrix( + matrix: &impl Matrix, + target_height: usize, + ) -> RowMajorMatrix { + let height = matrix.height(); + assert!(target_height >= height); + assert!(height.is_power_of_two() && target_height.is_power_of_two()); + + let repeat_factor = target_height / height; + let width = matrix.width(); + + let mut values = Vec::with_capacity(target_height * width); + for row in matrix.rows() { + let row_vec: Vec = row.collect(); + for _ in 0..repeat_factor { + values.extend(row_vec.iter().cloned()); + } + } + + RowMajorMatrix::new(values, width) + } + + fn build_leaves_upsampled( + matrices: &[RowMajorMatrix], + sponge: &Sponge, + ) -> Vec<[Felt; DIGEST]> { + let mut states = + build_leaf_states_upsampled::(matrices, sponge); + states.iter_mut().map(|s| sponge.squeeze(s)).collect() + } + + /// Test that upsampled lifting produces correct results: + /// 1. Incremental lifting equals explicit lifting + /// 2. Explicit lifting equals single-matrix concatenation baseline + #[test] + fn upsampled_equivalence() { + let (_, sponge, _compressor) = gl::test_components(); + let mut rng = SmallRng::seed_from_u64(42); + + for scenario in matrix_scenarios::(RATE) { + let matrices: Vec> = scenario + .into_iter() + .map(|(h, w)| RowMajorMatrix::rand(&mut rng, h, w)) + .collect(); + + let max_height = matrices.last().unwrap().height(); + + // Upsampled path equivalence vs explicit upsampled lifting and single-concat baseline + let leaves = build_leaves_upsampled(&matrices, &sponge); + + let matrices_upsampled: Vec<_> = matrices + .iter() + .map(|m: &RowMajorMatrix| upsample_matrix(m, max_height)) + .collect(); + let leaves_lifted = build_leaves_upsampled(&matrices_upsampled, &sponge); + assert_eq!(leaves, leaves_lifted); + + let matrix_single = concatenate_matrices::<_, RATE>(&matrices_upsampled); + let leaves_single = build_leaves_single(&matrix_single, &sponge); + assert_eq!(leaves, leaves_single); + } + } +} diff --git a/stark/miden-lifted-stark/src/lmcs/merkle_witness.rs b/stark/miden-lifted-stark/src/lmcs/merkle_witness.rs new file mode 100644 index 0000000000..666acf421c --- /dev/null +++ b/stark/miden-lifted-stark/src/lmcs/merkle_witness.rs @@ -0,0 +1,162 @@ +//! Merkle witness for batch proof verification. +//! +//! [`MerkleWitness`] reconstructs the minimal subset of a Merkle tree from +//! opened leaves and provided sibling hashes. It supports [`root`](MerkleWitness::root) +//! verification and [`path`](MerkleWitness::path) extraction. + +use alloc::vec::Vec; + +use crate::lmcs::node_id::NodeId; + +/// The minimal subset of a Merkle tree reconstructed from opened leaves and sibling hashes. +/// +/// Contains every node on the authentication paths from the opened leaves to the +/// root — the opened leaves themselves, their siblings, and all ancestors up to +/// and including the root. Nodes not on any authentication path are absent. +/// +/// Nodes are stored as `(NodeId, value)` pairs sorted by heap index, which gives +/// natural top-down, left-to-right ordering. +pub struct MerkleWitness { + /// All nodes sorted by NodeId (heap index order). + nodes: Vec<(NodeId, T)>, + /// Tree depth (leaves are at this depth, root at 0). + tree_depth: usize, +} + +impl MerkleWitness { + /// Look up a node by its ID. + fn get(&self, id: NodeId) -> Option<&T> { + self.nodes.binary_search_by_key(&id, |(k, _)| *k).ok().map(|i| &self.nodes[i].1) + } + + /// Build a witness from sorted leaf hashes. + /// + /// Leaves must be sorted by position in ascending order with no duplicates. + /// + /// `fetch_sibling` is called with the [`NodeId`] of each missing sibling, + /// level-by-level, left-to-right, bottom-to-top, matching transcript order. + pub fn build( + leaves: impl IntoIterator, + tree_depth: usize, + mut fetch_sibling: impl FnMut(NodeId) -> Result, + compress: impl Fn(T, T) -> T, + ) -> Result { + let mut current: Vec<(NodeId, T)> = leaves + .into_iter() + .map(|(pos, val)| (NodeId::new(tree_depth, pos), val)) + .collect(); + debug_assert!(current.windows(2).all(|w| w[0].0 < w[1].0), "leaves must be sorted"); + + let mut nodes: Vec<(NodeId, T)> = Vec::new(); + // Each level has at most ceil(n/2) parents; pre-allocate to avoid + // reallocation on the first level (subsequent levels reuse via swap). + let mut next: Vec<(NodeId, T)> = Vec::with_capacity(current.len().div_ceil(2)); + + for _ in 0..tree_depth { + let mut iter = current.drain(..).peekable(); + while let Some((node, hash)) = iter.next() { + let sibling = node.sibling(); + + // Get sibling hash: from the set if present, otherwise fetched. + let sib_hash = iter + .next_if(|(id, _)| *id == sibling) + .map(|(_, h)| h) + .map_or_else(|| fetch_sibling(sibling), Ok)?; + + // Order children left-to-right, compress, and promote. + let (left, right) = if node < sibling { + ((node, hash), (sibling, sib_hash)) + } else { + ((sibling, sib_hash), (node, hash)) + }; + + let parent_hash = compress(left.1.clone(), right.1.clone()); + next.push((node.parent(), parent_hash)); + + nodes.extend([left, right]); + } + drop(iter); + core::mem::swap(&mut current, &mut next); + } + + // Root level (depth 0). + nodes.extend(current); + + // Sort by heap index for binary search lookups. + nodes.sort_by_key(|(id, _)| *id); + + Ok(Self { nodes, tree_depth }) + } + + /// The root hash, or `None` if the tree is empty. + pub fn root(&self) -> Option<&T> { + self.get(NodeId::new(0, 0)) + } + + /// Authentication path for a leaf index (sibling hashes, bottom-to-top). + pub fn path(&self, index: usize) -> Option> { + let mut path = Vec::with_capacity(self.tree_depth); + let mut id = NodeId::new(self.tree_depth, index); + for _ in 0..self.tree_depth { + path.push(self.get(id.sibling())?.clone()); + id = id.parent(); + } + Some(path) + } +} + +#[cfg(test)] +mod tests { + use alloc::vec; + + use super::*; + + #[test] + fn both_children_known() { + let tree = MerkleWitness::build( + [(0, 10u64), (1, 20)], + 1, + |_| -> Result { panic!("should not be called") }, + |l, r| l + r, + ) + .unwrap(); + assert_eq!(tree.root(), Some(&30)); + } + + #[test] + fn fetches_missing_sibling() { + let tree = MerkleWitness::build( + [(0, 10u64)], + 1, + |sib| { + assert_eq!(sib, NodeId::new(1, 1)); + Ok::<_, ()>(20) + }, + |l, r| l + r, + ) + .unwrap(); + assert_eq!(tree.root(), Some(&30)); + } + + #[test] + fn path_extraction() { + // tree_depth=2: 4 leaves, only positions 0 and 3 known. + let tree = MerkleWitness::build( + [(0, 1u64), (3, 4)], + 2, + |sib| match sib { + s if s == NodeId::new(2, 1) => Ok::<_, ()>(2u64), + s if s == NodeId::new(2, 2) => Ok(3u64), + _ => panic!("unexpected sibling request: {sib:?}"), + }, + |l, r| l + r, + ) + .unwrap(); + + assert_eq!(tree.root(), Some(&10)); // (1+2) + (3+4) = 10 + + // path: sibling hashes from leaf to root + assert_eq!(tree.path(0).unwrap(), vec![2, 7]); + assert_eq!(tree.path(3).unwrap(), vec![3, 3]); + } +} diff --git a/stark/miden-lifted-stark/src/lmcs/mod.rs b/stark/miden-lifted-stark/src/lmcs/mod.rs new file mode 100644 index 0000000000..422ea7c9c2 --- /dev/null +++ b/stark/miden-lifted-stark/src/lmcs/mod.rs @@ -0,0 +1,258 @@ +//! Lifted Matrix Commitment Scheme (LMCS) for matrices with power-of-two heights. +//! +//! This module provides a Merkle tree commitment scheme for matrices that store +//! polynomial evaluations over multiplicative cosets. The tree is indexed by +//! **domain order** (natural index): callers address leaves by domain index, +//! and bit-reversal concerns are encapsulated inside the LMCS. +//! +//! # Main Types +//! +//! - [`config::LmcsConfig`]: Configuration holding cryptographic primitives (sponge + compression) +//! with packed types for SIMD parallelization. +//! - [`Lmcs`]: Trait for LMCS configurations, providing type-erased access to commitment +//! operations. +//! - [`LmcsTree`]: Trait for built LMCS trees, providing opening operations. +//! - [`lifted_tree::LiftedMerkleTree`]: The underlying Merkle tree data structure. +//! - [`proof::Proof`]: Single-opening proof with rows, optional salt, and authentication path. +//! - [`proof::BatchProof`]: Batch opening data with Merkle witness for path extraction. +//! +//! # Mathematical Foundation +//! +//! Consider a polynomial `f(X)` of degree less than `d`, and let `g` be the coset generator and +//! `K` a subgroup of order `n ≥ d` with primitive root `ω`. The coset evaluations +//! `{f(g·ω^j) : j ∈ [0, n)}` can be stored in two orderings: +//! +//! - **Canonical order**: `[f(g·ω^0), f(g·ω^1), ..., f(g·ω^{n-1})]` +//! - **Bit-reversed order**: `[f(g·ω^{bitrev(0)}), f(g·ω^{bitrev(1)}), ..., f(g·ω^{bitrev(n-1)})]` +//! +//! where `bitrev(i)` is the bit-reversal of index `i` within `log2(n)` bits. +//! +//! # Lifting by Upsampling +//! +//! When we have matrices with different heights n₀ ≤ n₁ ≤ … ≤ nₜ₋₁ (each a power of two), +//! we "lift" smaller matrices to the maximum height N = nₜ₋₁ using **nearest-neighbor +//! upsampling**: each row is repeated contiguously `r = N/n` times. +//! +//! For a matrix of height `n` lifted to `N`, the index map is: `i ↦ floor(i / r) = i >> log2(r)` +//! +//! **Example** (`n=4`, `N=8`): +//! - Original rows: `[row0, row1, row2, row3]` +//! - Upsampled: `[row0, row0, row1, row1, row2, row2, row3, row3]` (blocks of 2) +//! +//! # Why Upsampling Works +//! +//! Given evaluations of `f(X)` over a coset, upsampling to height `N = n · r` (where `r = 2^k`) +//! produces evaluations of the lifted polynomial `f'(X) = f(Xʳ)` over the larger coset. +//! +//! The internal hashing uses matrices whose rows are in bit-reversed order (as produced by +//! `BitReversedMatrixView`). For such data, upsampling by nearest-neighbor repetition +//! (`i >> k`) produces the correct lifted evaluations. The LMCS then bit-reverses the +//! leaf digest array so the Merkle tree is indexed by domain order. +//! +//! # Opening Semantics +//! +//! When opening at domain index `d`, the LMCS maps `d` to bit-reversed row index +//! `bitrev(d) >> k` for each matrix. This returns the same values that were hashed +//! into Merkle leaf `d`. +//! +//! # Equivalence to Cyclic Lifting +//! +//! Upsampling bit-reversed data is equivalent to cyclically repeating canonically-ordered data: +//! +//! ```text +//! Upsample(BitReverse(data)) = BitReverse(Cyclic(data)) +//! ``` +//! +//! where cyclic repetition tiles the original `n` rows periodically: `[row0, row1, ..., row_{n-1}, +//! row0, ...]`. +//! +//! This equivalence follows from the bit-reversal identity: for `r = N/n = 2^k`, +//! `bitrev_N(i) mod n = bitrev_n(i >> k)`. + +pub mod bitrev; +pub mod config; +pub mod hiding_config; +pub mod lifted_tree; +pub mod merkle_witness; +pub mod node_id; +pub mod proof; +pub mod row_list; +pub mod tree_indices; +pub mod utils; + +#[cfg(test)] +mod tests; + +use alloc::{collections::BTreeMap, vec::Vec}; + +use bitrev::BitReversibleMatrix; +use miden_stark_transcript::{ProverChannel, TranscriptError, VerifierChannel}; +use p3_matrix::Matrix; +use proof::BatchProofView; +use row_list::RowList; +use thiserror::Error; +use tree_indices::TreeIndices; + +// ============================================================================ +// Type Aliases +// ============================================================================ + +/// Opened rows keyed by leaf index, returned by [`Lmcs::open_batch`]. +pub type OpenedRows = BTreeMap>; + +// ============================================================================ +// Traits +// ============================================================================ + +/// Trait for LMCS configurations. +pub trait Lmcs: Clone { + /// Scalar field element type for matrix data. + /// + /// `Send + Sync` bounds required by [`Matrix`]. + type F: Clone + Send + Sync; + /// Commitment type (root hash). + type Commitment: Clone + Eq; + /// Tree type (prover data), parameterized by stored matrix type. + type Tree>: LmcsTree; + /// Batch witness type returned by [`read_batch_proof`](Self::read_batch_proof). + type BatchProof: BatchProofView; + + /// Build a tree from domain-ordered matrices with no transcript padding (alignment = 1). + /// + /// The LMCS extracts the inner bit-reversed matrices via + /// [`BitReversibleMatrix::bit_reverse_rows`] and stores them. The tree is indexed + /// by domain order; [`LmcsTree::leaves`] returns the stored bit-reversed matrices. + /// + /// This affects only transcript hint formatting; the commitment root is unchanged. + fn build_tree>(&self, leaves: Vec) -> Self::Tree; + + /// Build a tree from domain-ordered matrices using the hasher alignment for transcript + /// padding. + /// + /// Rows are padded to the hasher's alignment when streaming hints. + /// When the alignment is 1, this is identical to [`Self::build_tree`]. + fn build_aligned_tree>( + &self, + leaves: Vec, + ) -> Self::Tree; + + /// Hash a sequence of field slices into a leaf hash. + /// + /// Inputs are absorbed in order. For salted leaves, append the salt slice to the + /// iterator (or call this with a chained iterator). + fn hash<'a, I>(&self, rows: I) -> Self::Commitment + where + I: IntoIterator, + Self::F: 'a; + + /// Compress two hashes into their parent (2-to-1 compression). + fn compress(&self, left: Self::Commitment, right: Self::Commitment) -> Self::Commitment; + + /// Open a batch proof by reading hint data from a transcript channel. + /// + /// The hint format is implementation-defined; callers must use the matching + /// `LmcsTree::prove_batch` implementation to produce compatible hints. + /// `widths` must match the committed tree (including any alignment padding + /// if `build_aligned_tree` was used). + /// + /// # Preconditions + /// - `indices` must be non-empty and have depth matching `log₂(tree height)`. + /// + /// # Postconditions + /// On success, the returned map contains exactly one entry per unique index. + /// Each entry's `RowList` has one row per width in `widths`, with that + /// row's length matching the corresponding width. + fn open_batch( + &self, + commitment: &Self::Commitment, + widths: &[usize], + indices: &TreeIndices, + channel: &mut Ch, + ) -> Result, LmcsError> + where + Ch: VerifierChannel; + + /// Parse a batch opening from transcript hints without verification. + /// + /// Reads leaf openings and sibling hashes from the channel, hashes leaves, + /// and reconstructs the Merkle witness. Does not verify against a commitment; + /// validation happens in [`open_batch`](Lmcs::open_batch). + /// + /// Use [`merkle_witness::MerkleWitness::path`] on the returned witness to extract + /// authentication paths. + fn read_batch_proof( + &self, + widths: &[usize], + indices: &TreeIndices, + channel: &mut Ch, + ) -> Result + where + Ch: VerifierChannel; + + /// Get the alignment used by `build_aligned_tree`. + /// + /// This is the hasher's rate, used to pad rows when streaming hints. + fn alignment(&self) -> usize; +} + +/// Trait for built LMCS trees. +/// +/// Provides methods for accessing tree data and generating proofs. +pub trait LmcsTree { + /// Get the tree root (commitment). + fn root(&self) -> Commitment; + + /// Get the height of the largest matrix (i.e. the number of leaves of the Merkle tree). + fn height(&self) -> usize; + + /// Get references to the committed matrices. + /// + /// Matrix widths are not padded; use [`Self::aligned_widths`] for aligned widths. + fn leaves(&self) -> &[M]; + + /// Get the opened rows for a given leaf index (original matrix widths, no padding). + fn rows(&self, index: usize) -> RowList; + + /// Get the opened rows for a given leaf index, padded to the tree's alignment. + /// + /// Padding uses `Default::default()` and is not enforced by verification. + fn aligned_rows(&self, index: usize) -> RowList; + + /// Column alignment used when streaming openings. + fn alignment(&self) -> usize; + + /// Get widths for each committed matrix (original, no padding). + fn widths(&self) -> Vec; + + /// Get aligned widths for each committed matrix (padded to alignment). + fn aligned_widths(&self) -> Vec { + let alignment = self.alignment(); + self.widths().into_iter().map(|w| utils::aligned_len(w, alignment)).collect() + } + + /// Prove a batch opening and stream it into a transcript channel. + /// + /// The hint format is implementation-defined and must be consumed by the + /// corresponding `Lmcs::open_batch` implementation. Rows are padded to the + /// tree's alignment before being written to the channel. + /// + /// Leaf openings are written in **sorted tree index order** (ascending, deduplicated). + fn prove_batch(&self, indices: &TreeIndices, channel: &mut Ch) + where + Ch: ProverChannel; +} + +// ============================================================================ +// Error Types +// ============================================================================ + +/// Errors that can occur during LMCS operations. +#[derive(Debug, Clone, PartialEq, Eq, Error)] +pub enum LmcsError { + #[error("invalid proof")] + InvalidProof, + #[error("root mismatch")] + RootMismatch, + #[error("transcript error: {0}")] + TranscriptError(#[from] TranscriptError), +} diff --git a/stark/miden-lifted-stark/src/lmcs/node_id.rs b/stark/miden-lifted-stark/src/lmcs/node_id.rs new file mode 100644 index 0000000000..35331d31d1 --- /dev/null +++ b/stark/miden-lifted-stark/src/lmcs/node_id.rs @@ -0,0 +1,50 @@ +//! Heap-indexed node address in a binary Merkle tree. + +/// A node address in a binary Merkle tree using heap indexing. +/// +/// Depth 0 = root. At depth `d`, positions range over `0..2^d`. +/// The heap index `(1 << depth) + position` yields a single `usize` +/// whose natural ordering is top-down, left-to-right. Standard tree +/// operations reduce to bit manipulation: +/// +/// - `parent()` = `id >> 1` +/// - `sibling()` = `id ^ 1` +/// - `depth()` = `ilog2(id)` +/// - `position()` = `id − 2^depth` +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct NodeId(usize); + +impl NodeId { + /// Create a node ID from a (depth, position) pair. + /// + /// Callers must ensure `position < 2^depth` (except for the root at depth 0, + /// where position must be 0). + #[inline] + pub const fn new(depth: usize, position: usize) -> Self { + Self((1 << depth) + position) + } + + /// The depth in the tree (0 = root). + #[inline] + pub const fn depth(&self) -> usize { + self.0.ilog2() as usize + } + + /// The position within the depth level. + #[inline] + pub const fn position(&self) -> usize { + self.0 - (1 << self.depth()) + } + + /// The sibling node (same depth, position XOR 1). + #[inline] + pub const fn sibling(&self) -> Self { + Self(self.0 ^ 1) + } + + /// The parent node (depth − 1, position >> 1). + #[inline] + pub const fn parent(&self) -> Self { + Self(self.0 >> 1) + } +} diff --git a/stark/miden-lifted-stark/src/lmcs/proof.rs b/stark/miden-lifted-stark/src/lmcs/proof.rs new file mode 100644 index 0000000000..d542b130e7 --- /dev/null +++ b/stark/miden-lifted-stark/src/lmcs/proof.rs @@ -0,0 +1,195 @@ +//! LMCS proof structures. +//! +//! - [`Proof`]: Single-opening proof with rows, optional salt, and authentication path. +//! - [`BatchProof`]: Batch opening data with per-index rows/salt and a [`MerkleWitness`]. +//! +//! Use [`Lmcs::read_batch_proof`] to parse transcript hints +//! into a [`BatchProof`] without verifying against a commitment. + +use alloc::{collections::BTreeMap, vec::Vec}; + +use miden_stark_transcript::{ProverChannel, TranscriptError, VerifierChannel}; + +use crate::lmcs::{Lmcs, merkle_witness::MerkleWitness, row_list::RowList}; + +/// Single-opening Merkle proof with rows and authentication path. +/// +/// Contains the opening (rows + salt) and siblings (bottom-to-top) for a single leaf. +/// +/// # Type Parameters +/// +/// - `F`: Field element type. +/// - `C`: Hash type (also used for commitments). +pub struct Proof { + /// The leaf opening (rows + salt) for this query. + pub opening: LeafOpening, + /// Sibling hashes from leaf level to root (bottom-to-top). + pub siblings: Vec, +} + +/// Batch opening data parsed from transcript hints without verification. +/// +/// Bundles opened leaf data (rows + salt) per index with the reconstructed +/// [`MerkleWitness`] for authentication path queries. +pub struct BatchProof { + /// Opened leaf data keyed by leaf index. + pub openings: BTreeMap>, + /// Reconstructed Merkle authentication structure. + pub witness: MerkleWitness, +} + +/// Accessor trait for batch proof data. +/// +/// Provides read access to individual openings, authentication paths, and query indices. +/// This allows consumers (e.g. the Miden VM recursive verifier) to work with batch proofs +/// through the opaque `Lmcs::BatchProof` associated type. +pub trait BatchProofView { + /// Get the opened rows for a given leaf index. + fn opening(&self, index: usize) -> Option<&RowList>; + + /// Get the salt for a given leaf index. + /// + /// Returns an empty slice for non-hiding configurations. + fn salt(&self, index: usize) -> Option<&[F]>; + + /// Get the authentication path (bottom-to-top sibling hashes) for a given leaf index. + fn path(&self, index: usize) -> Option>; + + /// Iterate over the unique query indices (in sorted order). + fn indices(&self) -> impl Iterator + '_; +} + +impl BatchProofView for BatchProof { + fn opening(&self, index: usize) -> Option<&RowList> { + self.openings.get(&index).map(|o| &o.rows) + } + + fn salt(&self, index: usize) -> Option<&[F]> { + self.openings.get(&index).map(|o| o.salt.as_slice()) + } + + fn path(&self, index: usize) -> Option> { + self.witness.path(index) + } + + fn indices(&self) -> impl Iterator + '_ { + self.openings.keys().copied() + } +} + +/// Opened rows and optional salt for a single leaf. +pub struct LeafOpening { + /// Opened rows for this query. + pub rows: RowList, + /// Salt for this leaf (zero-sized when the configuration is non-hiding). + pub salt: [F; SALT_ELEMS], +} + +impl LeafOpening { + /// Read a single leaf opening (rows + salt) from a verifier channel. + /// + /// Reads `sum(widths)` field elements as a flat row, then `SALT_ELEMS` salt elements. + /// When `SALT_ELEMS == 0`, the salt read is a no-op. + pub fn read_from_channel( + widths: Vec, + channel: &mut Ch, + ) -> Result + where + F: Copy, + Ch: VerifierChannel, + { + let total_width: usize = widths.iter().sum(); + let elems = channel.receive_hint_field_slice(total_width)?.to_vec(); + let rows = RowList::new(elems, widths); + let salt: [F; SALT_ELEMS] = channel.receive_hint_field_array()?; + Ok(Self { rows, salt }) + } + + /// Write this leaf opening (rows + salt) to a prover channel. + /// + /// Writes each row slice, then salt elements (when `SALT_ELEMS > 0`). + /// Symmetric with [`read_from_channel`](Self::read_from_channel). + pub fn write_to_channel(&self, channel: &mut Ch) + where + F: Copy, + Ch: ProverChannel, + { + for row in self.rows.iter_rows() { + channel.hint_field_slice(row); + } + channel.hint_field_slice(&self.salt); + } + + /// Compute the leaf hash from this opening's rows and salt. + /// + /// Absorbs row slices in order, then salt (when `SALT_ELEMS > 0`). + /// This is the canonical leaf hash used in Merkle tree construction. + pub fn leaf_hash(&self, lmcs: &L) -> L::Commitment + where + F: Copy, + L: Lmcs, + { + let rows_iter = self.rows.iter_rows(); + if SALT_ELEMS > 0 { + lmcs.hash(rows_iter.chain(core::iter::once(self.salt.as_slice()))) + } else { + lmcs.hash(rows_iter) + } + } +} + +#[cfg(test)] +mod tests { + use p3_matrix::dense::RowMajorMatrix; + use rand::{SeedableRng, rngs::SmallRng}; + + use super::*; + use crate::{ + lmcs::{ + LmcsTree, tests::roundtrip_open_batch, tree_indices::TreeIndices, utils::log2_strict_u8, + }, + testing::configs::goldilocks_poseidon2 as gl, + }; + + #[test] + fn batch_proof_consistent_with_open_batch() { + let lmcs = gl::test_lmcs(); + + let test = |seed: u64, shapes: &[(usize, usize)], indices: &[usize]| { + let mut rng = SmallRng::seed_from_u64(seed); + let matrices: Vec<_> = + shapes.iter().map(|&(h, w)| RowMajorMatrix::rand(&mut rng, h, w)).collect(); + let tree = lmcs.build_tree(matrices); + let widths = tree.aligned_widths(); + let log_max_height = log2_strict_u8(tree.height()); + + // Path A: open_batch (verification) + let (transcript, opened_rows) = + roundtrip_open_batch(&lmcs, &tree, indices).expect("open_batch should verify"); + + // Path B: read_batch_proof (parse-only) + let mut verifier_channel = gl::verifier_channel(&transcript); + let tree_indices = TreeIndices::new(indices.iter().copied(), log_max_height).unwrap(); + let witness = lmcs + .read_batch_proof(&widths, &tree_indices, &mut verifier_channel) + .expect("batch witness should parse"); + assert!(verifier_channel.is_empty(), "parse path should fully consume transcript"); + + // Same number of unique openings + assert_eq!(opened_rows.len(), witness.openings.len()); + + // Row data must match between the two paths + for (&idx, verified_rows) in &opened_rows { + let opening = witness.openings.get(&idx).expect("opening for index"); + assert_eq!( + *verified_rows, opening.rows, + "row mismatch between open_batch and batch witness at index {idx}" + ); + } + }; + + test(1, &[(8, 4)], &[0, 3, 7]); + test(42, &[(4, 3), (8, 5), (16, 7)], &[0, 5, 10, 15]); + test(99, &[(4, 2), (8, 6)], &[3, 1, 3, 0, 1]); // duplicates + } +} diff --git a/stark/miden-lifted-stark/src/lmcs/row_list.rs b/stark/miden-lifted-stark/src/lmcs/row_list.rs new file mode 100644 index 0000000000..93f72d8064 --- /dev/null +++ b/stark/miden-lifted-stark/src/lmcs/row_list.rs @@ -0,0 +1,136 @@ +//! Flat storage of variable-width rows. + +use alloc::vec::Vec; + +use crate::lmcs::utils::aligned_len; + +/// Flat storage of variable-width rows. +/// +/// In a STARK proof, each row typically holds one committed matrix's evaluations at a +/// leaf index queried by the verifier as part of the low-degree test (LDT). Matrices +/// have different widths because they encode different sets of constraint polynomials +/// (e.g., main trace vs auxiliary trace). +/// +/// Stores all elements contiguously in a single `Vec`, with a separate `Vec` +/// tracking the width of each row. This avoids N+1 heap allocations compared to +/// `Vec>` and enables efficient flat iteration. +/// +/// Invariant: `widths.iter().sum::() == elems.len()`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RowList { + elems: Vec, + widths: Vec, +} + +impl RowList { + /// Create a `RowList` from raw elements and widths. + /// + /// # Panics + /// + /// Panics if `widths.iter().sum() != elems.len()`. + pub fn new(elems: Vec, widths: Vec) -> Self { + let expected: usize = widths.iter().sum(); + assert_eq!( + elems.len(), + expected, + "RowList invariant violated: {} elems but widths sum to {}", + elems.len(), + expected, + ); + Self { elems, widths } + } + + /// Build a `RowList` from an iterator of row-like items. + /// + /// Accepts anything convertible to `&[T]`: owned `Vec`, `&Vec`, `&[T]`, `Cow`, etc. + pub fn from_rows>(rows: impl IntoIterator) -> Self + where + T: Clone, + { + let mut elems = Vec::new(); + let mut widths = Vec::new(); + for row in rows { + let row = row.as_ref(); + widths.push(row.len()); + elems.extend_from_slice(row); + } + Self { elems, widths } + } + + /// Contiguous element slice. + #[inline] + pub fn as_slice(&self) -> &[T] { + &self.elems + } + + /// Iterate over all elements by value. + #[inline] + pub fn iter_values(&self) -> impl Iterator + '_ + where + T: Copy, + { + self.elems.iter().copied() + } + + /// Number of rows. + #[inline] + pub fn num_rows(&self) -> usize { + self.widths.len() + } + + /// Iterate over rows as slices. + pub fn iter_rows(&self) -> impl Iterator { + let mut offset = 0; + self.widths.iter().map(move |&w| { + let row = &self.elems[offset..offset + w]; + offset += w; + row + }) + } + + /// Get a single row by index. + /// + /// # Panics + /// + /// Panics if `idx >= self.num_rows()`. + pub fn row(&self, idx: usize) -> &[T] { + let offset: usize = self.widths[..idx].iter().sum(); + &self.elems[offset..offset + self.widths[idx]] + } +} + +impl RowList { + /// Iterate over all elements with each row zero-padded to a multiple of `alignment`. + /// + /// Alignment matches the cryptographic sponge's absorption rate. Both prover and + /// verifier must hash identical padded data for the Merkle commitment to verify, + /// so OOD evaluations sent over the transcript use the same padding convention. + /// + /// Yields the original row elements followed by implicit zeros, without allocating + /// a padded copy. + pub fn iter_aligned(&self, alignment: usize) -> impl Iterator + '_ { + self.iter_rows().flat_map(move |row| { + let padding = aligned_len(row.len(), alignment) - row.len(); + row.iter().copied().chain(core::iter::repeat_n(T::default(), padding)) + }) + } +} + +impl RowList { + /// Build a `RowList` from an iterator of row-like items, padding each to `alignment`. + pub fn from_rows_aligned>( + rows: impl IntoIterator, + alignment: usize, + ) -> Self { + let mut elems = Vec::new(); + let mut widths = Vec::new(); + for row in rows { + let row = row.as_ref(); + let padded_len = aligned_len(row.len(), alignment); + widths.push(padded_len); + elems.extend_from_slice(row); + elems.resize(elems.len() + (padded_len - row.len()), T::default()); + } + Self { elems, widths } + } +} diff --git a/stark/miden-lifted-stark/src/lmcs/tests.rs b/stark/miden-lifted-stark/src/lmcs/tests.rs new file mode 100644 index 0000000000..b19d71014e --- /dev/null +++ b/stark/miden-lifted-stark/src/lmcs/tests.rs @@ -0,0 +1,289 @@ +//! Integration tests for LMCS. + +use alloc::vec; + +use gl::{ + Compress, DIGEST, Felt, PackedFelt, Sponge, TestCommitment, TestDigest, TestTranscriptData, + WIDTH, +}; +use hiding_config::HidingLmcsConfig; +use lifted_tree::LiftedMerkleTree; +use miden_stateful_hasher::{Alignable, StatefulHasher}; +use p3_field::PrimeCharacteristicRing; +use p3_matrix::dense::RowMajorMatrix; +use rand::{RngExt, SeedableRng, rngs::SmallRng}; +use utils::{aligned_len, log2_strict_u8}; + +use super::*; +// ============================================================================ +// Test Helpers and Re-exports +// ============================================================================ +use crate::testing::configs::goldilocks_poseidon2 as gl; + +type OpenedRows = BTreeMap>; + +/// Build leaf hashes for a single matrix (used for equivalence testing). +pub fn build_leaves_single(matrix: &RowMajorMatrix, sponge: &Sponge) -> Vec<[Felt; DIGEST]> { + matrix + .rows() + .map(|row| { + let mut state = [Felt::ZERO; WIDTH]; + sponge.absorb_into(&mut state, row); + sponge.squeeze(&state) + }) + .collect() +} + +fn verify_open_batch( + lmcs: &C, + commitment: &TestCommitment, + widths: &[usize], + indices: &TreeIndices, + transcript: &TestTranscriptData, + prover_digest: &TestDigest, +) -> Result +where + C: Lmcs, +{ + let mut verifier_channel = gl::verifier_channel(transcript); + let result = lmcs.open_batch(commitment, widths, indices, &mut verifier_channel); + if result.is_ok() { + let verifier_digest = + verifier_channel.finalize().expect("transcript should finalize cleanly"); + assert_eq!(verifier_digest, *prover_digest); + } + result +} + +pub fn roundtrip_open_batch( + lmcs: &C, + tree: &C::Tree, + indices: &[usize], +) -> Result<(TestTranscriptData, OpenedRows), LmcsError> +where + C: Lmcs, + M: Matrix, +{ + let widths = tree.aligned_widths(); + let log_max_height = log2_strict_u8(tree.height()); + let tree_indices = TreeIndices::new(indices.iter().copied(), log_max_height).unwrap(); + + let (prover_digest, transcript) = { + let mut prover_channel = gl::prover_channel(); + tree.prove_batch(&tree_indices, &mut prover_channel); + prover_channel.finalize() + }; + let opened_rows = + verify_open_batch(lmcs, &tree.root(), &widths, &tree_indices, &transcript, &prover_digest)?; + Ok((transcript, opened_rows)) +} + +// ============================================================================ +// Hiding LMCS Types and Helpers +// ============================================================================ + +const SALT: usize = 4; +type HidingTree = LiftedMerkleTree; +type HidingConfig = + HidingLmcsConfig; + +fn hiding_lmcs(rng: SmallRng) -> HidingConfig { + let (_, sponge, compress) = gl::test_components(); + HidingLmcsConfig::new(sponge, compress, rng) +} + +// ============================================================================ +// Integration Tests +// ============================================================================ + +#[test] +fn lmcs_roundtrip() { + let test = |seed: u64, matrices: &[(usize, usize)], num_queries: usize| { + let mut rng = SmallRng::seed_from_u64(seed); + let lmcs = gl::test_lmcs(); + let matrices: Vec<_> = + matrices.iter().map(|&(h, w)| RowMajorMatrix::rand(&mut rng, h, w)).collect(); + + let tree = lmcs.build_tree(matrices); + let widths = tree.aligned_widths(); + let max_height = tree.height(); + let indices: Vec = + (0..num_queries).map(|_| rng.random_range(0..max_height)).collect(); + let (_transcript, opened_rows) = + roundtrip_open_batch(&lmcs, &tree, &indices).expect("batch opening should verify"); + + for (&leaf_idx, rows_for_query) in &opened_rows { + assert_eq!(rows_for_query.num_rows(), widths.len()); + assert_eq!(*rows_for_query, tree.aligned_rows(leaf_idx)); + } + }; + + test(1, &[(8, 4)], 1); // single matrix + test(42, &[(4, 3), (8, 5), (16, 7)], 4); // multi-height + test(99, &[(32, 2)], 8); // tall matrix +} + +#[test] +fn lmcs_duplicate_indices_roundtrip() { + let mut rng = SmallRng::seed_from_u64(123); + let lmcs = gl::test_lmcs(); + let matrices = vec![RowMajorMatrix::rand(&mut rng, 4, 5), RowMajorMatrix::rand(&mut rng, 8, 3)]; + + let tree = lmcs.build_tree(matrices); + let widths = tree.aligned_widths(); + let log_max_height = log2_strict_u8(tree.height()); + let indices = [3usize, 1, 3, 0, 1]; + + let (transcript, opened_rows) = + roundtrip_open_batch(&lmcs, &tree, &indices).expect("batch opening should verify"); + + // BTreeMap coalesces duplicates: 5 indices → 3 unique keys + assert_eq!(opened_rows.len(), 3); + + for (&index, rows) in &opened_rows { + assert_eq!(*rows, tree.aligned_rows(index), "row mismatch for index {index}"); + } + + let tree_indices = TreeIndices::new(indices.iter().copied(), log_max_height).unwrap(); + let mut verifier_channel = gl::verifier_channel(&transcript); + let batch = lmcs + .read_batch_proof(&widths, &tree_indices, &mut verifier_channel) + .expect("batch witness should parse from transcript"); + + assert_eq!(batch.openings.len(), 3); + for &index in &[0usize, 1, 3] { + let opening = batch.openings.get(&index).expect("opening for index"); + assert_eq!( + opening.rows, + tree.aligned_rows(index), + "batch witness rows mismatch for index {index}" + ); + } +} + +#[test] +fn hiding_roundtrip() { + let test = |seed: u64, matrices: &[(usize, usize)], indices: &[usize]| { + let mut rng = SmallRng::seed_from_u64(seed); + let matrices: Vec<_> = + matrices.iter().map(|&(h, w)| RowMajorMatrix::rand(&mut rng, h, w)).collect(); + + let config = hiding_lmcs(rng); + let tree: HidingTree<_> = config.build_tree(matrices); + let (_transcript, opened_rows) = + roundtrip_open_batch(&config, &tree, indices).expect("batch opening should verify"); + + for (&leaf_idx, rows) in &opened_rows { + assert_eq!(*rows, tree.aligned_rows(leaf_idx)); + } + }; + + test(99, &[(4, 3), (8, 5)], &[1, 3, 5]); + + // Different salts should produce different commitments + let matrices1 = vec![RowMajorMatrix::rand(&mut SmallRng::seed_from_u64(100), 4, 3)]; + let matrices2 = matrices1.clone(); + + let config1 = hiding_lmcs(SmallRng::seed_from_u64(1)); + let config2 = hiding_lmcs(SmallRng::seed_from_u64(2)); + + let tree1: HidingTree<_> = config1.build_tree(matrices1); + let tree2: HidingTree<_> = config2.build_tree(matrices2); + + assert_ne!(tree1.root(), tree2.root()); +} + +#[test] +fn open_batch_handles_empty_or_oob() { + let mut rng = SmallRng::seed_from_u64(7); + let lmcs = gl::test_lmcs(); + let matrix = RowMajorMatrix::rand(&mut rng, 4, 3); + let tree = lmcs.build_tree(vec![matrix]); + let widths = tree.aligned_widths(); + let log_max_height = log2_strict_u8(tree.height()); + let commitment = tree.root(); + + let (prover_digest, transcript) = gl::prover_channel().finalize(); + + // Empty indices → open_batch returns InvalidProof. + let empty = TreeIndices::new([], log_max_height).unwrap(); + assert_eq!( + verify_open_batch(&lmcs, &commitment, &widths, &empty, &transcript, &prover_digest), + Err(LmcsError::InvalidProof) + ); + + // Out-of-range index → TreeIndices construction returns InvalidProof. + assert_eq!(TreeIndices::new([tree.height()], log_max_height), Err(LmcsError::InvalidProof)); +} + +#[test] +fn build_tree_alignment_modes() { + let mut rng = SmallRng::seed_from_u64(123); + let lmcs = gl::test_lmcs(); + let m1 = RowMajorMatrix::rand(&mut rng, 4, 3); + let m2 = RowMajorMatrix::rand(&mut rng, 8, 5); + + let tree_unaligned = lmcs.build_tree(vec![m1.clone(), m2.clone()]); + let tree_aligned = lmcs.build_aligned_tree(vec![m1, m2]); + let alignment = tree_aligned.alignment(); + let expected_alignment = >::ALIGNMENT; + + assert_eq!(tree_unaligned.alignment(), 1); + assert_eq!(alignment, expected_alignment); + assert_eq!(tree_unaligned.root(), tree_aligned.root()); + + let widths_aligned = tree_aligned.aligned_widths(); + assert_eq!(widths_aligned[0], aligned_len(3, expected_alignment)); + assert_eq!(widths_aligned[1], aligned_len(5, expected_alignment)); + + let widths_unaligned = tree_unaligned.widths(); + assert_eq!(widths_unaligned, vec![3, 5]); + if expected_alignment > 1 { + assert_ne!(widths_unaligned, widths_aligned); + } + + let rows_aligned = tree_aligned.aligned_rows(0); + let widths_a: Vec = rows_aligned.iter_rows().map(<[Felt]>::len).collect(); + assert_eq!(widths_a, widths_aligned); + + let rows_unaligned = tree_unaligned.rows(0); + let widths_u: Vec = rows_unaligned.iter_rows().map(<[Felt]>::len).collect(); + assert_eq!(widths_u, widths_unaligned); + + let indices = [0usize, 1usize]; + let (_transcript, opened_rows) = roundtrip_open_batch(&lmcs, &tree_aligned, &indices) + .expect("aligned opening should verify"); + for (&idx, rows) in &opened_rows { + assert_eq!(*rows, tree_aligned.aligned_rows(idx)); + } +} + +#[test] +fn batch_proof_handles_empty_or_oob() { + let mut rng = SmallRng::seed_from_u64(9); + let lmcs = gl::test_lmcs(); + let matrix = RowMajorMatrix::rand(&mut rng, 4, 3); + let tree = lmcs.build_tree(vec![matrix]); + let widths = tree.aligned_widths(); + let log_max_height = log2_strict_u8(tree.height()); + + let idx0 = TreeIndices::new([0], log_max_height).unwrap(); + let mut prover_channel = gl::prover_channel(); + tree.prove_batch(&idx0, &mut prover_channel); + let (_, transcript) = prover_channel.finalize(); + + // Empty indices → no openings parsed. + let empty = TreeIndices::new([], log_max_height).unwrap(); + let mut verifier_channel = gl::verifier_channel(&transcript); + let batch = lmcs.read_batch_proof(&widths, &empty, &mut verifier_channel).unwrap(); + assert!(batch.openings.is_empty()); + + // Zero-width openings with a valid index. + let mut verifier_channel = gl::verifier_channel(&transcript); + let batch = lmcs.read_batch_proof(&[], &idx0, &mut verifier_channel).unwrap(); + assert_eq!(batch.openings.len(), 1); + let opening = batch.openings.get(&0).expect("opening for index 0"); + assert_eq!(opening.rows.num_rows(), 0); + assert!(opening.salt.is_empty()); + assert_eq!(batch.witness.path(0).unwrap().len(), 2); +} diff --git a/stark/miden-lifted-stark/src/lmcs/tree_indices.rs b/stark/miden-lifted-stark/src/lmcs/tree_indices.rs new file mode 100644 index 0000000000..907a9f6b17 --- /dev/null +++ b/stark/miden-lifted-stark/src/lmcs/tree_indices.rs @@ -0,0 +1,298 @@ +//! Validated Merkle tree leaf indices and missing sibling iteration. +//! +//! [`TreeIndices`] bundles a sorted, deduplicated set of leaf indices with +//! the tree depth, enforcing the invariant that every index is in `0..2^depth`. +//! +//! [`MissingSiblingsIter`] walks the tree upward from a set of leaf positions +//! and yields the sibling nodes absent from the set — exactly the nodes whose +//! hashes must be provided to reconstruct the root. + +use alloc::vec::Vec; + +use crate::lmcs::{LmcsError, node_id::NodeId}; + +/// A validated set of Merkle tree leaf indices at a given depth. +/// +/// Invariants (enforced by [`new`](Self::new) and [`shrink_depth`](Self::shrink_depth)): +/// - `indices` is sorted ascending with no duplicates. +/// - Every index satisfies `index < 2^depth`. +/// +/// May be empty. Consumers that require non-empty input should check +/// [`is_empty`](Self::is_empty). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TreeIndices { + indices: Vec, + depth: u8, +} + +impl TreeIndices { + /// Create validated tree indices from an arbitrary iterator. + /// + /// Sorts, deduplicates, and validates that every index is in `0..2^depth`. + /// Returns `LmcsError::InvalidProof` if any index is out of range. + pub fn new(indices: impl IntoIterator, depth: u8) -> Result { + let mut indices: Vec = indices.into_iter().collect(); + indices.sort_unstable(); + indices.dedup(); + + let max = 1usize << depth as usize; + if indices.last().is_some_and(|&i| i >= max) { + return Err(LmcsError::InvalidProof); + } + + Ok(Self { indices, depth }) + } + + /// The tree depth (log₂ of the number of leaves). + pub fn depth(&self) -> u8 { + self.depth + } + + /// Number of unique indices. + pub fn len(&self) -> usize { + self.indices.len() + } + + /// Whether the index set is empty. + pub fn is_empty(&self) -> bool { + self.indices.is_empty() + } + + /// Iterate over the indices in ascending order. + pub fn iter(&self) -> core::slice::Iter<'_, usize> { + self.indices.iter() + } + + /// Iterator over sibling nodes absent from this leaf set, bottom-to-top. + pub fn missing_siblings(&self) -> MissingSiblingsIter { + MissingSiblingsIter::new(&self.indices, self.depth) + } + + /// Map domain indices to folded domain indices `shift` levels down, in place. + /// + /// In natural (domain) order, folding by `2^shift` maps each index to its + /// low `(depth - shift)` bits: `index & ((1 << (depth - shift)) - 1)`. + /// The depth is reduced and duplicates (from indices in the same coset) + /// are removed. + /// + /// Unlike the bit-reversed right-shift, masking can reorder indices, + /// so `sort_unstable()` is needed before `dedup()`. + /// + /// Shrinking a root-only set (depth 0) has no effect. + pub fn shrink_depth(&mut self, shift: u8) { + let new_depth = self.depth.saturating_sub(shift); + let mask = (1usize << new_depth as usize) - 1; + for idx in &mut self.indices { + *idx &= mask; + } + self.indices.sort_unstable(); + self.indices.dedup(); + self.depth = new_depth; + } +} + +// ============================================================================ +// MissingSiblingsIter +// ============================================================================ + +/// Iterator over sibling nodes absent from a queried leaf set, bottom-to-top. +/// +/// Given sorted, deduplicated leaf positions, walks the Merkle tree upward +/// and yields a [`NodeId`] for every sibling not in the set — exactly the +/// nodes whose hashes a verifier must receive to reconstruct the root. +/// +/// # Algorithm +/// +/// Each layer is scanned left-to-right. For every node, if its sibling is +/// the next entry it is "present" and both are consumed; otherwise the +/// sibling is "missing" and yielded. Either way, the node's parent is +/// promoted to the next layer (deduplicated, since sibling pairs share a +/// parent). When the current layer is exhausted, the accumulated parents +/// become the new current layer. Iteration ends when the parents reach +/// depth 0 (the root). +/// +/// # Buffer layout +/// +/// A single `Vec` holds both regions in non-overlapping slices: +/// +/// ```text +/// [ next-layer parents | ... gap ... | current-layer unprocessed ] +/// 0..next_len current.start..current.end +/// ``` +/// +/// The gap never closes because each pair of siblings produces at most one +/// parent, so `next_len` grows slower than `current.start` advances. +pub struct MissingSiblingsIter { + /// Shared buffer: `nodes[current]` are unprocessed nodes in the current + /// layer; `nodes[..next_len]` accumulates their parents for the next layer. + nodes: Vec, + /// Slice of `nodes` still to process in this layer. + current: core::ops::Range, + /// Number of parent nodes written into `nodes[..next_len]`. + /// Invariant: `next_len ≤ current.start` (regions never overlap). + next_len: usize, +} + +impl MissingSiblingsIter { + /// Create a new iterator from sorted, deduplicated leaf positions. + pub fn new(positions: &[usize], tree_depth: u8) -> Self { + let tree_depth = tree_depth as usize; + let len = if tree_depth > 0 { positions.len() } else { 0 }; + Self { + nodes: positions.iter().map(|&p| NodeId::new(tree_depth, p)).collect(), + current: 0..len, + next_len: 0, + } + } +} + +impl Iterator for MissingSiblingsIter { + type Item = NodeId; + + fn next(&mut self) -> Option { + loop { + // The two buffer regions must never overlap. + debug_assert!(self.next_len <= self.current.start); + + if let Some((node, rest)) = self.nodes[self.current.clone()].split_first() { + let sibling = node.sibling(); + let sibling_present = rest.first() == Some(&sibling); + + // Promote parent to the next layer, deduplicating consecutive siblings. + let parent = node.parent(); + if self.next_len == 0 || self.nodes[self.next_len - 1] != parent { + self.nodes[self.next_len] = parent; + self.next_len += 1; + } + + // Consume one node (missing sibling) or two (sibling pair). + self.current.start += if sibling_present { 2 } else { 1 }; + debug_assert!(self.next_len <= self.current.start); + + if !sibling_present { + return Some(sibling); + } + } else { + // Current layer exhausted — promote to the parent layer. + debug_assert!(self.current.is_empty()); + let next = self.nodes[..self.next_len].first()?; + if next.depth() == 0 { + debug_assert_eq!(self.next_len, 1, "tree must converge to a single root"); + return None; // Reached the root; no more siblings to yield. + } + self.current = 0..self.next_len; + self.next_len = 0; + } + } + } +} + +#[cfg(test)] +mod tests { + use alloc::vec; + + use super::*; + + #[test] + fn tree_indices_construction_and_validation() { + // Sorts, deduplicates, and validates. + let ti = TreeIndices::new([3, 1, 2, 1, 3], 3).unwrap(); + let vals: Vec = ti.iter().copied().collect(); + assert_eq!(vals, [1, 2, 3]); + assert_eq!(ti.depth(), 3); + + // Empty is valid. + assert!(TreeIndices::new([], 5).unwrap().is_empty()); + + // depth=0 → single leaf, only index 0 is valid. + assert!(TreeIndices::new([0], 0).is_ok()); + assert!(TreeIndices::new([1], 0).is_err()); + + // Boundary: depth=2 → valid range 0..4. + assert!(TreeIndices::new([3], 2).is_ok()); + assert!(TreeIndices::new([4], 2).is_err()); + assert!(TreeIndices::new([0, 4], 2).is_err()); + } + + #[test] + fn shrink_depth() { + // Domain indices [4,5,6,7] at depth 3: low-bit mask with new_depth=1 → mask=1. + // 4&1=0, 5&1=1, 6&1=0, 7&1=1 → sorted dedup → [0,1]. + let mut ti = TreeIndices::new([4, 5, 6, 7], 3).unwrap(); + ti.shrink_depth(2); + assert_eq!(ti.iter().copied().collect::>(), [0, 1]); + assert_eq!(ti.depth(), 1); + + // Domain indices [0,3] at depth 2: low-bit mask with new_depth=1 → mask=1. + // 0&1=0, 3&1=1 → [0,1]. + let mut ti = TreeIndices::new([0, 3], 2).unwrap(); + ti.shrink_depth(1); + assert_eq!(ti.iter().copied().collect::>(), [0, 1]); + assert_eq!(ti.depth(), 1); + + // Shift by 0 is a no-op. + let mut ti = TreeIndices::new([1, 3], 3).unwrap(); + ti.shrink_depth(0); + assert_eq!(ti.iter().copied().collect::>(), [1, 3]); + assert_eq!(ti.depth(), 3); + + // Domain indices [0,2,4,6] at depth 3: low-bit mask with new_depth=2 → mask=3. + // 0&3=0, 2&3=2, 4&3=0, 6&3=2 → sorted dedup → [0,2]. + let mut ti = TreeIndices::new([0, 2, 4, 6], 3).unwrap(); + ti.shrink_depth(1); + assert_eq!(ti.iter().copied().collect::>(), [0, 2]); + assert_eq!(ti.depth(), 2); + } + + fn missing_siblings(indices: impl IntoIterator, depth: u8) -> Vec { + TreeIndices::new(indices, depth).unwrap().missing_siblings().collect() + } + + #[test] + fn missing_siblings_edge_cases() { + // Empty input → nothing to do. + assert!(missing_siblings([], 3).is_empty()); + + // depth=0 → root is the only leaf, no siblings exist. + assert!(missing_siblings([0], 0).is_empty()); + + // All leaves present → every sibling is in the set, nothing missing. + assert!(missing_siblings([0, 1], 1).is_empty()); + assert!(missing_siblings([0, 1, 2, 3], 2).is_empty()); + } + + #[test] + fn single_leaf_needs_one_sibling_per_level() { + // A single leaf at depth d requires exactly d sibling hashes to reach the root, + // one at each level from the leaf up to depth 1. + for depth in 1u8..=5 { + let sibs = missing_siblings([0], depth); + assert_eq!(sibs.len(), depth as usize, "depth={depth}"); + for (i, sib) in sibs.iter().enumerate() { + assert_eq!(sib.depth(), depth as usize - i, "depth={depth}, level={i}"); + } + } + } + + #[test] + fn missing_siblings_various_patterns() { + // Single leaf at depth 2: sibling + uncle. + assert_eq!(missing_siblings([2], 2), vec![NodeId::new(2, 3), NodeId::new(1, 0)]); + + // Sibling pair: only the parent's sibling is missing. + assert_eq!(missing_siblings([2, 3], 2), vec![NodeId::new(1, 0)]); + + // One pair + one lone leaf covering both subtrees → only the lone leaf's sibling. + // Parents {0,1} form a complete pair, so nothing missing above. + assert_eq!(missing_siblings([0, 2, 3], 2), vec![NodeId::new(2, 1)]); + + // Multi-level propagation at depth 3: positions [2,3,4]. + // Leaf level: (2,3) pair + lone 4 → missing sibling 5. + // Parent level: parents {1,2} are not siblings → missing 0 and 3. + // Grandparent level: {0,1} form a pair → converges to root. + assert_eq!( + missing_siblings([2, 3, 4], 3), + vec![NodeId::new(3, 5), NodeId::new(2, 0), NodeId::new(2, 3)] + ); + } +} diff --git a/stark/miden-lifted-stark/src/lmcs/utils.rs b/stark/miden-lifted-stark/src/lmcs/utils.rs new file mode 100644 index 0000000000..8ef58fc142 --- /dev/null +++ b/stark/miden-lifted-stark/src/lmcs/utils.rs @@ -0,0 +1,53 @@ +//! Utility functions for LMCS operations. + +use alloc::vec::Vec; +use core::array; + +use p3_field::PackedValue; +use p3_util::log2_strict_usize; + +/// Strict log₂ returning `u8`. +/// +/// Panics if `n` is not a power of two. +#[inline] +pub fn log2_strict_u8(n: usize) -> u8 { + log2_strict_usize(n) as u8 +} + +/// Extension trait for `PackedValue` providing columnar pack/unpack operations. +/// +/// These methods perform transpose operations on packed data, useful for +/// SIMD-parallelized Merkle tree construction. +pub trait PackedValueExt: PackedValue { + /// Pack columns from `WIDTH` rows of scalar values. + /// + /// Given `WIDTH` rows of `N` scalar values, extract each column and pack it + /// into a single packed value. This performs a transpose operation. + #[inline] + #[must_use] + fn pack_columns(rows: &[[Self::Value; N]]) -> [Self; N] { + assert_eq!(rows.len(), Self::WIDTH); + array::from_fn(|col| Self::from_fn(|lane| rows[lane][col])) + } +} + +// Blanket implementation for all PackedValue types +impl PackedValueExt for T {} + +/// Compute the aligned length for `len` given an alignment. +#[inline] +pub const fn aligned_len(len: usize, alignment: usize) -> usize { + if alignment <= 1 { + len + } else { + len.next_multiple_of(alignment) + } +} + +/// Align each width in place, returning the same `Vec`. +pub fn aligned_widths(mut widths: Vec, alignment: usize) -> Vec { + for w in &mut widths { + *w = aligned_len(*w, alignment); + } + widths +} diff --git a/stark/miden-lifted-stark/src/pcs/deep/interpolate.rs b/stark/miden-lifted-stark/src/pcs/deep/interpolate.rs new file mode 100644 index 0000000000..38aa1005cb --- /dev/null +++ b/stark/miden-lifted-stark/src/pcs/deep/interpolate.rs @@ -0,0 +1,513 @@ +//! Barycentric interpolation for DEEP openings (with lifting). +//! +//! The DEEP technique needs values `f(z)` for many committed polynomials `f` at a +//! small number of out-of-domain (OOD) points `z`. Interpolating a degree-`< d` +//! polynomial from `d` samples naively is `O(d²)`; barycentric interpolation makes +//! it `O(d)` once we precompute the expensive inverses. +//! +//! # Notation +//! - Domain points: `xᵢ = g·ωⁱ` for `i = 0..d−1` (a coset `gH` of size `d`). +//! - OOD points: `zⱼ`, chosen so `zⱼ ≠ xᵢ` for all `i, j`. +//! - Vanishing on `gH`: `V_{gH}(X) = (X/g)ᵈ − 1`. +//! +//! # Barycentric form +//! For `deg(f) < d`: +//! +//! ```text +//! f(z) = s(z) · Σᵢ wᵢ(z) · f(xᵢ) +//! s(z) = V_{gH}(z) / d = ((z/g)ᵈ − 1) / d +//! wᵢ(z) = xᵢ / (z − xᵢ) +//! ``` +//! +//! # Point quotients +//! We precompute `qᵢ(zⱼ) = 1/(zⱼ − xᵢ)` for all domain points `xᵢ` and all +//! opening points `zⱼ` using batch inversion (Montgomery's trick). This single +//! table is reused for: +//! - barycentric weights: `wᵢ(zⱼ) = xᵢ · qᵢ(zⱼ)` +//! - DEEP quotients: `(f(zⱼ) − f(X)) / (zⱼ − X)` +//! +//! # Lifting and weight folding +//! In lifted STARKs, different matrices correspond to polynomials on different +//! (power-of-two) domain sizes. A polynomial on a smaller domain is embedded into +//! the max domain by composing with an r-th power map: `f_lift(X) = f(Xʳ)`. +//! The verifier always queries at `z`, so the prover reports `f(zʳ)` for that +//! matrix; equivalently, it is evaluating `f_lift(z)`. +//! +//! To avoid recomputing barycentric weights for every height, we exploit the +//! bit-reversed ordering used by commitments: on a two-adic coset, points come in +//! adjacent `(+x, −x)` pairs. When lifting by a factor of 2, the lifted polynomial +//! `f(X²)` takes the same value on each adjacent pair, so we can fold the barycentric +//! sum by *summing the corresponding weights*. Repeating this `k` times handles lift +//! factors `r = 2ᵏ`. +//! +//! **Why weight summing is correct.** In bit-reversed order, `x_{2i+1} = −x_{2i}`. +//! Adding the two barycentric weights gives +//! `w_{2i} + w_{2i+1} = x/(z−x) + (−x)/(z+x) = 2x²/(z²−x²) = 2·w'ᵢ(z²)`, +//! where `w'ᵢ` is the weight on the squared domain. The factor of 2 cancels with the +//! halved scaling `s'(z²) = 2·s(z)`, so the interpolation identity is preserved. + +use alloc::{collections::BTreeSet, vec::Vec}; +use core::marker::PhantomData; + +use p3_field::{ExtensionField, FieldArray, TwoAdicField, batch_multiplicative_inverse}; +use p3_matrix::Matrix; +use p3_maybe_rayon::prelude::*; +use p3_util::{linear_map::LinearMap, log2_strict_usize, reconstitute_from_base}; +use tracing::{debug_span, info_span}; + +use crate::lmcs::row_list::RowList; + +/// Precomputed `1/(zⱼ − xᵢ)` for N evaluation points. +/// +/// This enables batched `O(d)` barycentric evaluation and DEEP quotient construction +/// without repeating inversions. +pub struct PointQuotients, const N: usize> { + /// The evaluation points `[z₀, z₁, ..., z_{N-1}]`. + points: FieldArray, + /// `point_quotient[i][j] = 1/(zⱼ − xᵢ)` for domain point xᵢ and eval point zⱼ. + pub(super) point_quotient: Vec>, + _marker: PhantomData, +} + +impl, const N: usize> PointQuotients { + /// Create precomputation for N evaluation points via batched inversion. + /// + /// Preconditions: all evaluation points must be outside the LDE evaluation coset + /// `gK` represented by `coset_points` (i.e., `zⱼ ≠ xᵢ` for all i, j). Otherwise + /// division by zero occurs in the barycentric weights and DEEP quotient. + /// + /// In the common case where the trace domain `H` is a sub-coset of `gK`, avoiding + /// `gK` also avoids `H`. If a caller uses a different domain relationship, it must + /// additionally ensure points are outside the trace domain. + pub fn new(points: FieldArray, coset_points: &[F]) -> Self { + let _span = info_span!("PointQuotients::new", n = coset_points.len()).entered(); + let n_points = coset_points.len(); + + // Compute differences in parallel: for each domain point x, compute [z₀ - x, z₁ - x, ...] + let diffs: Vec> = + coset_points.par_iter().map(|&x| points.map(|z| z - x)).collect(); + + // Flatten FieldArray slice for batch inversion (zero-copy), then reconstitute. + let diffs_flat = FieldArray::as_raw_slice(&diffs).as_flattened(); + let invs_flat = batch_multiplicative_inverse(diffs_flat); + debug_assert_eq!(invs_flat.len(), N * n_points); + // SAFETY: `reconstitute_from_base` requires: + // - Same alignment: `FieldArray` is `#[repr(transparent)]` over `[EF; N]`, so it has + // the same alignment as `EF`. + // - Length is a multiple of N: `invs_flat` has length `N * n_points` (one inverse per OOD + // point per domain element). + let point_quotient: Vec> = unsafe { reconstitute_from_base(invs_flat) }; + + debug_assert_eq!(point_quotient.len(), n_points); + + Self { + points, + point_quotient, + _marker: PhantomData, + } + } + + /// Evaluate all matrix columns at `[z₀ʳ, z₁ʳ, …, z_{N−1}ʳ]`. + /// + /// Here `r = domain_size / matrix_height` is the lift factor for that matrix. + /// + /// Returns evaluations grouped by commitment: `groups[group_idx][matrix_idx][col_idx]` + /// where each element is a `FieldArray` containing evaluations at all N points. + /// This batches N evaluation points together, using `columnwise_dot_product_batched` + /// for better cache utilization than N separate calls. + /// + /// Implementation note: we compute barycentric weights for the maximum domain once, + /// then derive weights for smaller heights by folding (summing blocks). All heights + /// share the same precomputed point quotients `1/(zⱼ − xᵢ)`. + pub fn batch_eval_lifted>( + &self, + matrices_groups: &[Vec<&M>], + coset_points: &[F], + log_blowup: u8, + ) -> RowList> { + let _span = info_span!("batch_eval_lifted", n_groups = matrices_groups.len()).entered(); + let n = coset_points.len(); + let d = n >> log_blowup as usize; + let log_d = log2_strict_usize(d); + + let shift = coset_points[0]; // g in bit-reversed order + let shift_inverse = shift.inverse(); + + // Compute barycentric scaling factors for each point: + // sⱼ(zⱼ) = ((zⱼ/g)ᵈ − 1) / d + let barycentric_scalings = self.points.map(|point| { + let z_over_shift = point * shift_inverse; + let t = z_over_shift.exp_power_of_2(log_d) - EF::ONE; + t.div_2exp_u64(log_d as u64) + }); + + let used_degrees: BTreeSet = matrices_groups + .iter() + .flat_map(|g| g.iter().map(|m| m.height() >> log_blowup as usize)) + .collect(); + + // Compute barycentric weights for each point at each height: + // wᵢⱼ(zⱼ) = xᵢ / (zⱼ − xᵢ) = xᵢ · point_quotient[i][j] + // For smaller domains, sum chunks (weight folding). + let barycentric_weights: LinearMap>> = + debug_span!("barycentric_weights", d).in_scope(|| { + assert_eq!(*used_degrees.last().unwrap(), d); + // Initial weights at full domain size + let top_weights: Vec> = coset_points[..d] + .par_iter() + .zip(self.point_quotient[..d].par_iter()) + .map(|(&x, invs)| (*invs).map(|inv| inv * x)) + .collect(); + + let mut weights = Vec::with_capacity(used_degrees.len()); + weights.push(top_weights); + + // Descending order: progressively sum chunks to shrink weights + for &next_degree in used_degrees.iter().rev().skip(1) { + let prev_weights = weights.last().unwrap(); + let chunk_size = prev_weights.len() / next_degree; + let next_weights = prev_weights + .par_chunks_exact(chunk_size) + .map(|chunk| chunk.iter().copied().sum()) + .collect(); + weights.push(next_weights); + } + + weights.into_iter().map(|w| (w.len(), w)).collect() + }); + + // f(zⱼʳ) = sⱼ(zⱼ)·Σᵢ wᵢⱼ(zⱼ)·f(xᵢ) + // For each group, evaluate at all N points using columnwise_dot_product_batched + // Returns Vec<[EF; N]> where result[col][point] = eval of column col at point point + let all_evals: Vec>> = matrices_groups + .iter() + .flat_map(|group| { + group.iter().map(|m| { + let weights = &barycentric_weights[&(m.height() >> log_blowup as usize)]; + let _guard = + debug_span!("evaluate matrix", height = weights.len(), width = m.width()) + .entered(); + let mut results = m.columnwise_dot_product_batched(weights); + for batch_evals in results.iter_mut() { + *batch_evals *= barycentric_scalings; + } + results + }) + }) + .collect(); + + RowList::from_rows(&all_evals) + } +} + +#[cfg(test)] +mod tests { + use alloc::vec; + + use p3_dft::{NaiveDft, TwoAdicSubgroupDft}; + use p3_field::{Field, PrimeCharacteristicRing}; + use p3_interpolation::{interpolate_coset, interpolate_coset_with_precomputation}; + use p3_matrix::{bitrev::BitReversibleMatrix, dense::RowMajorMatrix}; + use p3_util::reverse_slice_index_bits; + use rand::{RngExt, SeedableRng, distr::StandardUniform, prelude::SmallRng}; + + use super::*; + use crate::{ + pcs::utils::bit_reversed_coset_points, + testing::configs::goldilocks_poseidon2::{Felt, QuadFelt}, + }; + + /// Verify `batch_eval_lifted` matches `interpolate_coset` for various lift factors. + /// + /// This test creates matrices of varying heights and verifies that lifting produces + /// the correct evaluation. To satisfy `batch_eval_lifted`'s requirement that at least + /// one matrix fills the domain, we include a full-height dummy matrix alongside + /// each smaller test matrix. + #[test] + fn batch_eval_matches_interpolate_coset() { + let rng = &mut SmallRng::seed_from_u64(42); + let log_blowup = 2; + let log_n = 8; // Full LDE domain size = 256 + let n = 1 << log_n; + let shift = Felt::GENERATOR; + + // Coset points in bit-reversed order for our barycentric evaluation + let coset_points_br = bit_reversed_coset_points::(log_n); + + // Random out-of-domain evaluation point + let z: QuadFelt = rng.sample(StandardUniform); + + // Test multiple polynomial degrees + for log_scaling in 0..=2 { + // Polynomial degree (trace height before LDE) + let poly_degree = (n >> log_blowup) >> log_scaling; + // LDE evaluation count = poly_degree * blowup + let lde_height = poly_degree << log_blowup; + let width = 3; + + // For lifted polynomials, the coset becomes (gK)ʳ = gʳ · Kʳ + // So the shift for the smaller coset is shiftʳ + let lifted_shift = shift.exp_power_of_2(log_scaling); + + // Generate random polynomial coefficients and pad to LDE size + let mut coeffs_values = RowMajorMatrix::::rand(rng, poly_degree, width).values; + coeffs_values.resize(lde_height * width, Felt::ZERO); + let padded_coeffs = RowMajorMatrix::new(coeffs_values, width); + + // Compute evaluations on the lifted coset via DFT (standard order) + let evals_std = NaiveDft.coset_dft_batch(padded_coeffs, lifted_shift); + + // Convert to bit-reversed order for our evaluation + let evals_br: RowMajorMatrix = + evals_std.clone().bit_reverse_rows().to_row_major_matrix(); + + // Our method computes f(zʳ) where r = n / lde_height = 2^log_scaling + let z_lifted = z.exp_power_of_2(log_scaling); + + // Create a full-height dummy matrix to satisfy batch_eval_lifted's domain requirement + // (at least one matrix must fill the domain) + let dummy_matrix = RowMajorMatrix::new(vec![Felt::ZERO; n], 1); + + // Our barycentric evaluation using PointQuotients<1> + // Include both the dummy (full height) and the test matrix (possibly smaller) + let quotient = + PointQuotients::::new(FieldArray([z]), &coset_points_br); + let result = quotient.batch_eval_lifted( + &[vec![&dummy_matrix, &evals_br]], + &coset_points_br, + log_blowup, + ); + // Skip the 1-column dummy, unwrap FieldArray → EF + let our_evals: Vec = + result.as_slice()[1..].iter().map(|arr| arr[0]).collect(); + + // Standard interpolation on the lifted coset + let expected_evals = interpolate_coset(&evals_std, lifted_shift, z_lifted); + + assert_eq!( + our_evals.len(), + expected_evals.len(), + "log_scaling={log_scaling}: length mismatch" + ); + for (col, (our, expected)) in our_evals.iter().zip(expected_evals.iter()).enumerate() { + assert_eq!( + our, expected, + "log_scaling={log_scaling}, col={col}: evaluation mismatch" + ); + } + } + } + + /// Verify `batch_eval_lifted` matches `interpolate_coset_with_precomputation`. + #[test] + fn batch_eval_matches_interpolate_with_precomputation() { + let rng = &mut SmallRng::seed_from_u64(123); + let log_blowup = 2; + let log_n = 8; + let n = 1 << log_n; + let shift = Felt::GENERATOR; + + // Coset points in both orderings + let coset_points_br = bit_reversed_coset_points::(log_n); + let mut coset_points_std = coset_points_br.clone(); + reverse_slice_index_bits(&mut coset_points_std); // Convert to standard order + + // Random out-of-domain evaluation point + let z: QuadFelt = rng.sample(StandardUniform); + + // Create quotient for bit-reversed coset using PointQuotients<1> + let quotient = PointQuotients::::new(FieldArray([z]), &coset_points_br); + + // Test polynomial with no lifting (log_scaling = 0, full LDE domain) + let poly_degree = n >> log_blowup; // = 64 + let lde_height = n; // = 256, full LDE + let width = 4; + + // Generate random polynomial coefficients and pad to LDE size + let mut coeffs_values = RowMajorMatrix::::rand(rng, poly_degree, width).values; + coeffs_values.resize(lde_height * width, Felt::ZERO); + let padded_coeffs = RowMajorMatrix::new(coeffs_values, width); + + // Compute evaluations on coset via DFT (standard order) + let evals_std = NaiveDft.coset_dft_batch(padded_coeffs, shift); + + // Convert to bit-reversed order + let evals_br = evals_std.clone().bit_reverse_rows(); + + // Our barycentric evaluation (no lifting since lde_height = n) + let result = quotient.batch_eval_lifted(&[vec![&evals_br]], &coset_points_br, log_blowup); + // Unwrap FieldArray → EF + let our_evals: Vec = result.as_slice().iter().map(|arr| arr[0]).collect(); + + // Convert our diff_invs from bit-reversed to standard order for precomputation + let mut diff_invs_std: Vec = + quotient.point_quotient[..lde_height].iter().map(|arr| arr[0]).collect(); + reverse_slice_index_bits(&mut diff_invs_std); + + // Interpolation with precomputation (both in standard order) + let expected_evals = interpolate_coset_with_precomputation( + &evals_std, + shift, + z, + &coset_points_std[..lde_height], + &diff_invs_std, + ); + + assert_eq!(our_evals.len(), expected_evals.len(), "length mismatch"); + for (col, (&our, &expected)) in our_evals.iter().zip(expected_evals.iter()).enumerate() { + assert_eq!(our, expected, "col={col}: evaluation mismatch"); + } + } + + /// Verify `PointQuotients<2>` produces consistent results with separate `PointQuotients<1>` + /// calls. + #[test] + fn point_quotients_matches_single_point() { + use alloc::vec::Vec; + + use p3_matrix::Matrix; + + let rng = &mut SmallRng::seed_from_u64(999); + let log_blowup = 2; + let log_n = 8; + let n = 1 << log_n; + let shift = Felt::GENERATOR; + + // Coset points in bit-reversed order + let coset_points_br = bit_reversed_coset_points::(log_n); + + // Two random out-of-domain evaluation points + let z1: QuadFelt = rng.sample(StandardUniform); + let z2: QuadFelt = rng.sample(StandardUniform); + + // Generate test matrices of varying heights + let poly_degree_1 = n >> log_blowup; // Full size + let poly_degree_2 = poly_degree_1 >> 1; // Half size + let width = 3; + + let lifted_shift_1 = shift; + let lifted_shift_2 = shift.square(); + + let mut coeffs1 = RowMajorMatrix::::rand(rng, poly_degree_1, width).values; + coeffs1.resize(n * width, Felt::ZERO); + let evals1_std = + NaiveDft.coset_dft_batch(RowMajorMatrix::new(coeffs1, width), lifted_shift_1); + let evals1_br: RowMajorMatrix = evals1_std.bit_reverse_rows().to_row_major_matrix(); + + let mut coeffs2 = RowMajorMatrix::::rand(rng, poly_degree_2, width).values; + coeffs2.resize((n >> 1) * width, Felt::ZERO); + let evals2_std = + NaiveDft.coset_dft_batch(RowMajorMatrix::new(coeffs2, width), lifted_shift_2); + let evals2_br: RowMajorMatrix = evals2_std.bit_reverse_rows().to_row_major_matrix(); + + let matrices_groups: Vec>> = vec![vec![&evals1_br, &evals2_br]]; + + // --- Single-point evaluation using PointQuotients<1> (baseline) --- + let sq1 = PointQuotients::::new(FieldArray([z1]), &coset_points_br); + let sq2 = PointQuotients::::new(FieldArray([z2]), &coset_points_br); + let single_evals1 = sq1.batch_eval_lifted(&matrices_groups, &coset_points_br, log_blowup); + let single_evals2 = sq2.batch_eval_lifted(&matrices_groups, &coset_points_br, log_blowup); + + // --- Multi-point evaluation --- + let mq = PointQuotients::::new(FieldArray([z1, z2]), &coset_points_br); + let multi_evals = mq.batch_eval_lifted(&matrices_groups, &coset_points_br, log_blowup); + + // Verify point_quotient matches + for (i, (sq1_q, sq2_q)) in + sq1.point_quotient.iter().zip(sq2.point_quotient.iter()).enumerate() + { + let mq_q = &mq.point_quotient[i]; + assert_eq!(sq1_q[0], mq_q[0], "point_quotient mismatch at {i} for z1"); + assert_eq!(sq2_q[0], mq_q[1], "point_quotient mismatch at {i} for z2"); + } + + // Verify batch_eval_lifted results match. + // Single-point evals have FieldArray; multi-point evals have FieldArray. + assert_eq!(multi_evals.num_rows(), single_evals1.num_rows()); + for (row_idx, ((multi_row, single_row1), single_row2)) in multi_evals + .iter_rows() + .zip(single_evals1.iter_rows()) + .zip(single_evals2.iter_rows()) + .enumerate() + { + assert_eq!( + multi_row.len(), + single_row1.len(), + "length mismatch for z1 at row {row_idx}" + ); + + for (col, (m, s)) in multi_row.iter().zip(single_row1.iter()).enumerate() { + assert_eq!(m[0], s[0], "mismatch at row {row_idx}, col {col} for z1"); + } + + for (col, (m, s)) in multi_row.iter().zip(single_row2.iter()).enumerate() { + assert_eq!(m[1], s[0], "mismatch at row {row_idx}, col {col} for z2"); + } + } + } + + /// Verify two-point `PointQuotients<2>` produces correct results for mixed-height + /// matrices, checked against `interpolate_coset`. + #[test] + fn two_point_quotients_match_interpolate_coset() { + let rng = &mut SmallRng::seed_from_u64(999); + let log_blowup = 2; + let log_n = 8; + let n = 1 << log_n; + let shift = Felt::GENERATOR; + + let coset_points_br = bit_reversed_coset_points::(log_n); + + let z1: QuadFelt = rng.sample(StandardUniform); + let z2: QuadFelt = rng.sample(StandardUniform); + + // Matrix 1: full height (no lifting) + let poly_degree_1 = n >> log_blowup; + let width = 3; + let lifted_shift_1 = shift; + + let mut coeffs1 = RowMajorMatrix::::rand(rng, poly_degree_1, width).values; + coeffs1.resize(n * width, Felt::ZERO); + let evals1_std = + NaiveDft.coset_dft_batch(RowMajorMatrix::new(coeffs1, width), lifted_shift_1); + let evals1_br: RowMajorMatrix = + evals1_std.clone().bit_reverse_rows().to_row_major_matrix(); + + // Matrix 2: half height (lift factor 2) + let poly_degree_2 = poly_degree_1 >> 1; + let lifted_shift_2 = shift.square(); + + let mut coeffs2 = RowMajorMatrix::::rand(rng, poly_degree_2, width).values; + coeffs2.resize((n >> 1) * width, Felt::ZERO); + let evals2_std = + NaiveDft.coset_dft_batch(RowMajorMatrix::new(coeffs2, width), lifted_shift_2); + let evals2_br: RowMajorMatrix = + evals2_std.clone().bit_reverse_rows().to_row_major_matrix(); + + let matrices_groups: Vec>> = vec![vec![&evals1_br, &evals2_br]]; + + // Evaluate at both points using PointQuotients<2> + let pq = PointQuotients::::new(FieldArray([z1, z2]), &coset_points_br); + let result = pq.batch_eval_lifted(&matrices_groups, &coset_points_br, log_blowup); + let rows: Vec<&[FieldArray]> = result.iter_rows().collect(); + assert_eq!(rows.len(), 2, "expected 2 matrix rows"); + + // Verify each point against reference + for (point_idx, (label, z)) in + [(0, "z1", z1), (1, "z2", z2)].into_iter().map(|(i, l, z)| (i, (l, z))) + { + // Matrix 1 (no lifting): evaluate at z directly + let expected1 = interpolate_coset(&evals1_std, lifted_shift_1, z); + for (col, (&our, &exp)) in rows[0].iter().zip(expected1.iter()).enumerate() { + assert_eq!(our[point_idx], exp, "{label}, mat1, col={col}: mismatch"); + } + + // Matrix 2 (lift factor 2): evaluate at z^2 + let z_lifted = z.square(); + let expected2 = interpolate_coset(&evals2_std, lifted_shift_2, z_lifted); + for (col, (&our, &exp)) in rows[1].iter().zip(expected2.iter()).enumerate() { + assert_eq!(our[point_idx], exp, "{label}, mat2, col={col}: mismatch"); + } + } + } +} diff --git a/stark/miden-lifted-stark/src/pcs/deep/mod.rs b/stark/miden-lifted-stark/src/pcs/deep/mod.rs new file mode 100644 index 0000000000..4eb8551bd1 --- /dev/null +++ b/stark/miden-lifted-stark/src/pcs/deep/mod.rs @@ -0,0 +1,117 @@ +//! # DEEP Quotient for Lifted FRI +//! +//! DEEP converts evaluation claims into a low-degree test. Given W committed polynomials +//! `{fᵢ}` and claimed evaluations `fᵢ(zⱼ) = vᵢⱼ`, the quotient +//! +//! ```text +//! Q(X) = Σⱼ βʲ · Σᵢ αᵂ⁻¹⁻ⁱ · (vᵢⱼ - fᵢ(X)) / (zⱼ - X) +//! ``` +//! +//! is low-degree iff all claims are correct. A false claim creates a pole, detectable by FRI. +//! +//! ## Design Choices +//! +//! **Uniform opening points.** All columns share the same opening points `{zⱼ}`. This enables +//! factoring out `f_reduced(X) = Σᵢ αᵂ⁻¹⁻ⁱ·fᵢ(X)`, so the verifier computes one inner +//! product per query rather than one per column per point. +//! +//! **Two challenges.** Separating α (columns) from β (points) improves soundness. With a +//! single challenge, a cheating prover must avoid collisions among k·m terms; with two, +//! only k+m terms matter. This costs one extra field element in the transcript. +//! +//! **Lifting.** Polynomials of degree d on domain D embed into a larger domain D* via +//! `f(X) ↦ f(Xʳ)` where r = |D*|/|D|. In bit-reversed order, this means each evaluation +//! repeats r times consecutively—implemented by virtual upsampling without data movement. +//! +//! **Verifier's view of lifting.** From the verifier's perspective, all polynomials +//! appear to be evaluated at the same point z on the same domain. The prover computes +//! `fᵢ(zʳ)` for degree-d polynomials, but this equals `fᵢ'(z)` where `fᵢ'(X) = fᵢ(Xʳ)` +//! is the lifted polynomial. This uniformity enables the `f_reduced` factorization. +//! +//! ## Preconditions (caller responsibility) +//! +//! The DEEP constructors assume all opening points are valid: distinct and outside the +//! trace subgroup `H` and the LDE evaluation coset `gK`. Invalid points can trigger +//! division by zero in the barycentric weights. In practice, the outer STARK protocol +//! is expected to enforce this before invoking DEEP. +//! +//! ## Random Linear Combination Convention +//! +//! The batching challenge `α` reduces W columns via Horner evaluation: +//! `f_reduced = horner(α, [f₀, f₁, ..., fᵂ₋₁])`, where columns are flattened +//! across groups and matrices in commitment order. The `horner` function assigns +//! the highest power to the first element: `f₀·αᵂ⁻¹ + f₁·αᵂ⁻² + ... + fᵂ₋₁·α⁰`. +//! +//! This convention is shared by: +//! - Prover OOD reduction (`horner` over aligned batched evals) +//! - Verifier OOD reduction (inline in `verifier::DeepOracle::new` via `horner_acc`) +//! - Verifier query-time row reduction (`verifier::DeepOracle::open_batch` via `horner_acc`) +//! - Prover LDE evaluation (`prover::DeepPoly::from_trees` via explicit dot-product with reversed +//! negated coefficients — see comments there) + +pub mod interpolate; +pub mod proof; +pub mod prover; +pub mod verifier; + +use alloc::vec::Vec; + +use miden_stark_transcript::{TranscriptError, VerifierChannel}; +use p3_field::{ExtensionField, TwoAdicField}; +use p3_matrix::dense::RowMajorMatrix; +use proof::OpenedValues; + +/// DEEP quotient parameters. +/// +/// Controls proof-of-work grinding for DEEP challenge sampling. +/// Column alignment is handled at the LMCS layer and by padding evaluations. +#[derive(Clone, Copy, Debug)] +pub struct DeepParams { + /// Grinding bits before DEEP challenge sampling. + pub(crate) deep_pow_bits: usize, +} + +/// Read OOD evaluation matrices from a verifier channel. +/// +/// The prover sends one flat slice per evaluation point containing all matrices' +/// column values concatenated. This function splits by widths and reshapes into +/// per-group, per-matrix `RowMajorMatrix` with `num_eval_points` rows each. +pub fn read_eval_matrices( + group_widths: &[&[usize]], + num_eval_points: usize, + channel: &mut Ch, +) -> Result, TranscriptError> +where + F: TwoAdicField, + EF: ExtensionField, + Ch: VerifierChannel, +{ + let all_widths: Vec = group_widths.iter().flat_map(|gw| gw.iter().copied()).collect(); + let total_width: usize = all_widths.iter().sum(); + + let mut values: Vec> = + all_widths.iter().map(|&w| Vec::with_capacity(w * num_eval_points)).collect(); + + for _ in 0..num_eval_points { + let flat = channel.receive_algebra_slice::(total_width)?; + let mut offset = 0; + for (m, &w) in all_widths.iter().enumerate() { + values[m].extend_from_slice(&flat[offset..offset + w]); + offset += w; + } + } + + let mut mat_iter = values + .into_iter() + .zip(&all_widths) + .map(|(vals, &w)| RowMajorMatrix::new(vals, w)); + let evals = group_widths + .iter() + .map(|gw| mat_iter.by_ref().take(gw.len()).collect()) + .collect(); + + Ok(evals) +} + +#[cfg(test)] +mod tests; diff --git a/stark/miden-lifted-stark/src/pcs/deep/proof.rs b/stark/miden-lifted-stark/src/pcs/deep/proof.rs new file mode 100644 index 0000000000..1f16bf7583 --- /dev/null +++ b/stark/miden-lifted-stark/src/pcs/deep/proof.rs @@ -0,0 +1,70 @@ +//! DEEP transcript data structures. + +use alloc::vec::Vec; + +use miden_stark_transcript::{Channel, TranscriptError, VerifierChannel}; +use p3_field::{ExtensionField, Field, TwoAdicField}; +use p3_matrix::dense::RowMajorMatrix; + +use crate::pcs::deep::{DeepParams, read_eval_matrices}; + +/// Opened evaluations grouped by commitment group and matrix. +/// +/// `opened[g][m]` is a `RowMajorMatrix` with one row per evaluation point, +/// where `g` is the commitment group index and `m` the matrix index within that group. +pub type OpenedValues = Vec>>; + +/// Structured transcript view for the DEEP interaction. +/// +/// This records the prover's PoW witness and the two challenges sampled +/// from the Fiat-Shamir transcript after observing evaluations. +/// +/// `evals[g][m]` is a `RowMajorMatrix` with `num_eval_points` rows for +/// commitment group `g`, matrix `m`. Widths include alignment padding (matching +/// the committed rows). +pub struct DeepTranscript> { + /// `evals[g][m]` is a `RowMajorMatrix` with `num_eval_points` rows. + pub evals: OpenedValues, + /// Proof-of-work witness sampled before DEEP challenges. + pub pow_witness: F, + /// Challenge `α` for batching columns into `f_reduced`. + pub challenge_columns: EF, + /// Challenge `β` for batching opening points. + pub challenge_points: EF, +} + +impl DeepTranscript +where + F: TwoAdicField, + EF: ExtensionField, +{ + /// Parse DEEP transcript data from a verifier channel. + /// + /// Reads OOD evaluations, verifies the PoW witness, and samples batching + /// challenges. Does not verify the DEEP quotient itself; that validation + /// happens in the DEEP verifier. Commitment widths must match the + /// committed rows (including any alignment padding). + pub fn from_verifier_channel( + params: &DeepParams, + commitments: &[(::Commitment, Vec)], + num_eval_points: usize, + channel: &mut Ch, + ) -> Result + where + Ch: VerifierChannel, + { + let group_widths: Vec<&[usize]> = commitments.iter().map(|(_, gw)| gw.as_slice()).collect(); + let evals = read_eval_matrices::(&group_widths, num_eval_points, channel)?; + + let pow_witness = channel.grind(params.deep_pow_bits)?; + let challenge_columns: EF = channel.sample_algebra_element(); + let challenge_points: EF = channel.sample_algebra_element(); + + Ok(Self { + evals, + pow_witness, + challenge_columns, + challenge_points, + }) + } +} diff --git a/stark/miden-lifted-stark/src/pcs/deep/prover.rs b/stark/miden-lifted-stark/src/pcs/deep/prover.rs new file mode 100644 index 0000000000..710cbd51d9 --- /dev/null +++ b/stark/miden-lifted-stark/src/pcs/deep/prover.rs @@ -0,0 +1,444 @@ +use alloc::vec::Vec; +use core::iter::zip; + +use miden_stark_transcript::ProverChannel; +use p3_field::{ + ExtensionField, Field, FieldArray, PackedFieldExtension, PackedValue, TwoAdicField, +}; +use p3_matrix::Matrix; +use p3_maybe_rayon::prelude::*; +use tracing::info_span; + +use crate::{ + lmcs::{ + Lmcs, LmcsTree, + row_list::RowList, + utils::{aligned_widths, log2_strict_u8}, + }, + pcs::{ + deep::{DeepParams, interpolate::PointQuotients}, + utils::{PackedFieldExtensionExt, bit_reversed_coset_points, horner}, + }, +}; + +/// The DEEP quotient `Q(X)` evaluated over the LDE domain. +/// +/// Combines all polynomial evaluation claims into a single low-degree polynomial. +/// +/// Intuition: DEEP turns many separate opening claims into a single polynomial identity. +/// +/// After reducing all opened columns with a random `α` into one polynomial `f_red(X)`, +/// DEEP defines +/// +/// ```text +/// Q(X) = Σⱼ βʲ · (f_red(zⱼ) − f_red(X)) / (zⱼ − X) +/// ``` +/// +/// If the claimed OOD values `f_red(zⱼ)` are correct, each numerator vanishes at +/// `X = zⱼ` and cancels the denominator, so `Q` is a (low-degree) polynomial. +/// If any claim is wrong, the corresponding term has a pole and `Q` is not even a +/// polynomial; committing to `Q` via FRI then detects the inconsistency. +pub struct DeepPoly { + /// The DEEP quotient polynomial evaluated over the domain. + /// `deep_evals[i]` is the evaluation at the i-th domain point (bit-reversed order). + pub(crate) deep_evals: Vec, +} + +impl DeepPoly { + /// Construct `Q(X)` by evaluating trace trees at the opening points. + /// + /// This computes the LDE coset points from the trace tree height, evaluates the committed + /// matrices at `eval_points`, and then calls [`Self::from_evals`]. + /// + /// Preconditions: `eval_points` must be distinct and lie outside the trace subgroup `H` + /// and LDE evaluation coset `gK`. The outer protocol is expected to enforce this. + pub fn from_trees( + params: DeepParams, + trace_trees: &[&L::Tree], + eval_points: [EF; N], + log_blowup: u8, + channel: &mut Ch, + ) -> Self + where + L: Lmcs, + L::F: TwoAdicField, + EF: ExtensionField, + M: Matrix, + Ch: ProverChannel, + { + let lde_height = trace_trees.first().expect("at least one trace tree required").height(); + assert!( + trace_trees.iter().all(|tree| tree.height() == lde_height), + "mixed trace tree heights are not supported" + ); + + let log_lde_height = log2_strict_u8(lde_height); + let coset_points = bit_reversed_coset_points::(log_lde_height); + + let matrices_groups: Vec> = + trace_trees.iter().map(|tree| tree.leaves().iter().collect()).collect(); + + let quotient = PointQuotients::new(FieldArray::from(eval_points), &coset_points); + let batched_evals = info_span!("evaluate at OOD points") + .in_scope(|| quotient.batch_eval_lifted(&matrices_groups, &coset_points, log_blowup)); + + let (deep_poly, _evals) = + Self::from_evals::(params, trace_trees, batched_evals, "ient, channel); + deep_poly + } + + /// Construct `Q(X)` from committed matrices and batched evaluations at N opening points. + /// + /// # Arguments + /// - `trace_trees`: Trace trees used to derive alignment and matrix groups. All trees must + /// share the same alignment; mixed alignments are not supported. + /// - `batched_evals`: One row per matrix, each row holding `FieldArray` per column. + /// Widths match the unpadded matrices; alignment padding is applied lazily during channel + /// writes and Horner reduction. + /// - `quotient`: Precomputed `1/(zⱼ − xᵢ)` for all opening points zⱼ and domain points xᵢ. + /// + /// Returns the constructed `DeepPoly` and the (unaligned) `batched_evals` for test inspection. + /// + /// Columns are reduced with a random `α` using Horner + /// (`f_red(X) = Σᵢ α^{W−1−i} · fᵢ(X)`). This lets the verifier stream the + /// reduction as it reads opened rows, and ensures a cheating prover cannot satisfy + /// some constraints while failing others with non-negligible probability. + /// + /// Implementation note: we fuse a sign change into the per-column coefficient stream + /// so reduction and quotient assembly can share a single traversal over the domain. + pub fn from_evals( + params: DeepParams, + trace_trees: &[&L::Tree], + batched_evals: RowList>, + quotient: &PointQuotients, + channel: &mut Ch, + ) -> (Self, RowList>) + where + L: Lmcs, + L::F: TwoAdicField, + EF: ExtensionField, + M: Matrix, + Ch: ProverChannel, + { + // The alignment of the trees defines the number of virtual zero-values columns were + // inserted while hashing the rows of the matrices. The prover pads the opened rows of each + // matrix with zeros, so that the length of the row is a multiple of the alignment. + // The alignment is tied to the underlying cryptographic permutation's rate. + let alignment = + trace_trees.first().expect("at least one tree must be provided").alignment(); + assert!( + trace_trees.iter().all(|tree| tree.alignment() == alignment), + "mixed trace tree alignments are not supported" + ); + + // Collect the LDE matrices from each committed tree, grouped by commitment. + // matrices_groups[group_idx][matrix_idx] is a reference to the LDE matrix + // whose rows are bit-reversed coset evaluations at height `lde_height`. + let matrices_groups: Vec> = + trace_trees.iter().map(|tree| tree.leaves().iter().collect()).collect(); + + // 1. Bind the prover's OOD evaluation claims into the Fiat-Shamir transcript. The DEEP + // challenges (alpha, beta) are derived after this, so a cheating prover cannot adapt its + // claims to the challenges. Each matrix row is zero-padded to the tree alignment, + // matching the virtual zero columns the LMCS inserts when hashing rows. All matrices are + // concatenated into a single flat slice per eval point. + for point_idx in 0..N { + let flat: Vec = + batched_evals.iter_aligned(alignment).map(|fa| fa[point_idx]).collect(); + channel.send_algebra_slice(&flat); + } + + // 2. Grind for proof-of-work witness + let _pow_witness = info_span!("DEEP grind", bits = params.deep_pow_bits) + .in_scope(|| channel.grind(params.deep_pow_bits)); + + // 3. Sample DEEP challenges + let challenge_columns: EF = channel.sample_algebra_element(); + let challenge_points: EF = channel.sample_algebra_element(); + + // Pre-compute f_reduced(zⱼ) for all N points using Horner. + // Reduces across all matrices' aligned columns in flat order. + let f_reduced_at_points: FieldArray = + horner(challenge_columns, batched_evals.iter_aligned(alignment)); + + let w = ::Packing::WIDTH; + let point_quotient = "ient.point_quotient; + let n = point_quotient.len(); + + let group_sizes: Vec = matrices_groups.iter().map(Vec::len).collect(); + let widths: Vec = + matrices_groups.iter().flat_map(|g| g.iter().map(|m| m.width())).collect(); + + // Align each matrix width so padding is explicit in the transcript. + let aligned_widths = aligned_widths(widths, alignment); + + // Compute explicit coefficients for -f_reduced(X) = -Σᵢ α^{W−1−i}·fᵢ(X). + // + // The verifier computes f_reduced via `horner(alpha, columns)`, which assigns + // the highest power to the first column: column 0 gets α^{W−1}, column W-1 + // gets α⁰. To match this with an explicit dot-product (needed for the LDE + // evaluation), we need coefficient[i] = −α^{W−1−i}. + // + // Construction: + // shifted_powers(NEG_ONE) produces [−1, −α, −α², …, −α^{W−1}] + // .rev() reverses to [−α^{W−1}, …, −α, −1] + // Split into per-matrix chunks in commitment order. + // + // The negation is folded into the coefficients so the DEEP quotient loop can + // compute f_reduced(zⱼ) + neg_f_reduced(X) = f_reduced(zⱼ) − f_reduced(X) + // without a separate negation pass. + let total_width: usize = aligned_widths.iter().sum(); + let mut neg_powers_iter = challenge_columns + .shifted_powers(EF::NEG_ONE) + .collect_n(total_width) + .into_iter() + .rev(); + let neg_column_coeffs: Vec> = aligned_widths + .iter() + .map(|&width| neg_powers_iter.by_ref().take(width).collect()) + .collect(); + + // Compute -f_reduced(X) = -Σᵢ α^{W−1−i}·fᵢ(X) over the LDE domain, then + // transform in-place into the DEEP quotient: + // Q(X) = Σⱼ βʲ·(f_reduced(zⱼ) − f_reduced(X))·1/(zⱼ − X) + // + // The column reduction and quotient assembly are fused: the `neg_f_reduced` + // vector is consumed in-place to produce `deep_evals`, avoiding a separate + // full-domain allocation and improving cache locality. + + let deep_evals = info_span!("DEEP reduce + assemble").in_scope(|| { + let mut neg_column_coeffs_iter = neg_column_coeffs.iter(); + let mut neg_f_reduced = zip(matrices_groups.iter(), &group_sizes) + .map(|(matrices_group, &size)| { + let group_coeffs: Vec<&Vec> = + neg_column_coeffs_iter.by_ref().take(size).collect(); + accumulate_matrices(matrices_group, &group_coeffs) + }) + .reduce(|mut acc, next| { + debug_assert_eq!(acc.len(), next.len()); + acc.par_chunks_mut(w).zip(next.par_chunks(w)).for_each( + |(acc_chunk, next_chunk)| { + EF::add_slices(acc_chunk, next_chunk); + }, + ); + acc + }) + .unwrap_or_else(|| EF::zero_vec(n)); + + // Pre-compute βʲ for all N points + let point_coeffs: [EF; N] = + core::array::from_fn(|j| challenge_points.exp_u64(j as u64)); + + // Transform neg_f_reduced in-place into deep_evals. + // Q(x) = Σⱼ βʲ·qⱼ(x)·(f_reduced(zⱼ) + neg_f_reduced(x)) + if w == 1 || n < w { + neg_f_reduced + .par_iter_mut() + .zip(point_quotient.par_iter()) + .for_each(|(neg, q)| { + let mut result = q[0] * (f_reduced_at_points[0] + *neg); + for j in 1..N { + result += point_coeffs[j] * q[j] * (f_reduced_at_points[j] + *neg); + } + *neg = result; + }); + } else { + let f_reduced_packed: [EF::ExtensionPacking; N] = + f_reduced_at_points.0.map(EF::ExtensionPacking::from); + let point_coeffs_packed: [EF::ExtensionPacking; N] = + point_coeffs.map(EF::ExtensionPacking::from); + + neg_f_reduced + .par_chunks_exact_mut(w) + .zip(point_quotient.par_chunks_exact(w)) + .for_each(|(neg_chunk, q_chunk)| { + let neg_p = EF::ExtensionPacking::from_ext_slice(neg_chunk); + + // Transpose quotients: q_chunk[lane][point] -> q_packed[point] packs all + // lanes + let q_packed: [EF::ExtensionPacking; N] = + EF::ExtensionPacking::pack_ext_columns(FieldArray::as_raw_slice( + q_chunk, + )); + + // First point (j=0) has coefficient β⁰ = 1, compute directly + let mut result_p = q_packed[0] * (f_reduced_packed[0] + neg_p); + + // Remaining points (j>0) multiply by βʲ + for j in 1..N { + result_p += point_coeffs_packed[j] + * q_packed[j] + * (f_reduced_packed[j] + neg_p); + } + result_p.to_ext_slice(neg_chunk); + }); + } + + neg_f_reduced // now contains deep_evals + }); + + (Self { deep_evals }, batched_evals) + } +} + +/// Accumulate `f_reduced(X) = Σᵢ α^{W−1−i}·fᵢ(X)` across matrices of varying heights. +/// +/// In bit-reversed order, the lifted polynomial `f(Xʳ)` repeats each evaluation r times +/// (because adjacent bit-reversed indices differ only in their low bits, mapping to points +/// that are r-th roots of the same value). This lets us upsample by repetition instead of +/// recomputing the lifted polynomial: when crossing a height boundary, repeat entries to +/// match the new height, then continue accumulating. Matrices must be sorted by ascending +/// height. +fn accumulate_matrices, M: Matrix, C: AsRef<[EF]>>( + matrices: &[&M], + coeffs: &[C], +) -> Vec { + debug_assert!( + matrices.windows(2).all(|w| w[0].height() <= w[1].height()), + "matrices must be sorted by ascending height" + ); + let n = matrices.last().unwrap().height(); + + let mut acc = EF::zero_vec(n); + let mut scratch = EF::zero_vec(n); + + let mut active_height = matrices.first().unwrap().height(); + + for (&matrix, coeffs) in zip(matrices, coeffs) { + let coeffs = coeffs.as_ref(); + let height = matrix.height(); + debug_assert!(height.is_power_of_two(), "matrix height must be a power of two"); + debug_assert!( + matrix.width() <= coeffs.len(), + "matrix width {} exceeds coeffs length {}", + matrix.width(), + coeffs.len() + ); + + // Upsample: [a, b] → [a, a, b, b] when height doubles + if height > active_height { + let scaling_factor = height / active_height; + scratch[..height] + .par_chunks_mut(scaling_factor) + .zip(acc[..active_height].par_iter()) + .for_each(|(chunk, &val)| chunk.fill(val)); + acc[..height].swap_with_slice(&mut scratch[..height]); + } + + // SIMD path using horizontal packing. + // Slice to matrix width to avoid packing alignment-padding coefficients. + let w = F::Packing::WIDTH; + let active_coeffs = &coeffs[..matrix.width()]; + let packed_coeffs: Vec = active_coeffs + .chunks(w) + .map(|chunk| { + if chunk.len() == w { + EF::ExtensionPacking::from_ext_slice(chunk) + } else { + // Pad with zeros for the last chunk + let mut padded = EF::zero_vec(w); + padded[..chunk.len()].copy_from_slice(chunk); + EF::ExtensionPacking::from_ext_slice(&padded) + } + }) + .collect(); + + matrix + .rowwise_packed_dot_product::(&packed_coeffs) + .zip(acc[..height].par_iter_mut()) + .for_each(|(dot_result, acc_val)| { + *acc_val += dot_result; + }); + + active_height = height; + } + + acc +} + +#[cfg(test)] +mod tests { + use alloc::vec; + + use p3_field::{PrimeCharacteristicRing, dot_product}; + + use super::*; + use crate::testing::configs::goldilocks_poseidon2::{Felt, QuadFelt}; + + /// `reduce_with_powers` (Horner) must match explicit negative coeffs + dot product. + #[test] + fn neg_coeffs_match_neg_horner() { + let c: QuadFelt = QuadFelt::from_u64(2); + let alignment = 3; + let widths = [2usize, 3]; + let aligned_widths = aligned_widths(widths.to_vec(), alignment); + let rows: Vec> = vec![ + vec![Felt::from_u64(1), Felt::from_u64(2)], + vec![Felt::from_u64(3), Felt::from_u64(4), Felt::from_u64(5)], + ]; + let padded = RowList::from_rows_aligned(&rows, alignment); + + let mut neg_powers_iter = c + .shifted_powers(QuadFelt::NEG_ONE) + .collect_n(aligned_widths.iter().sum()) + .into_iter() + .rev(); + let neg_coeffs: Vec> = aligned_widths + .iter() + .map(|&width| neg_powers_iter.by_ref().take(width).collect()) + .collect(); + + // Explicit coefficient sum: Σᵢ coeffs[i] · rows[i] + let explicit: QuadFelt = + dot_product(neg_coeffs.iter().flatten().copied(), padded.iter_values()); + + // Horner using reduce_with_powers (same as used in verifier) + let horner: QuadFelt = horner(c, padded.iter_values()); + + assert_eq!(explicit, QuadFelt::NEG_ONE * horner); + } + + /// Padding: negative coeffs match -Horner for various width/alignment combos. + #[test] + fn neg_coeffs_alignment() { + let c: QuadFelt = QuadFelt::from_u64(7); + let alignment = 4; + let widths = [3usize, 5, 2]; + let aligned_widths = aligned_widths(widths.to_vec(), alignment); + + let mut neg_powers_iter = c + .shifted_powers(QuadFelt::NEG_ONE) + .collect_n(aligned_widths.iter().sum()) + .into_iter() + .rev(); + let coeffs: Vec> = aligned_widths + .iter() + .map(|&width| neg_powers_iter.by_ref().take(width).collect()) + .collect(); + + // Verify lengths match aligned widths + assert_eq!(coeffs[0].len(), aligned_widths[0]); + assert_eq!(coeffs[1].len(), aligned_widths[1]); + assert_eq!(coeffs[2].len(), aligned_widths[2]); + + // Verify this matches Horner reduction with arbitrary test data + let rows: Vec> = vec![ + vec![Felt::from_u64(10), Felt::from_u64(20), Felt::from_u64(30)], + vec![ + Felt::from_u64(1), + Felt::from_u64(2), + Felt::from_u64(3), + Felt::from_u64(4), + Felt::from_u64(5), + ], + vec![Felt::from_u64(100), Felt::from_u64(200)], + ]; + let padded = RowList::from_rows_aligned(&rows, alignment); + + let explicit: QuadFelt = + dot_product(coeffs.iter().flatten().copied(), padded.iter_values()); + let horner: QuadFelt = horner(c, padded.iter_values()); + + assert_eq!(explicit, QuadFelt::NEG_ONE * horner); + } +} diff --git a/stark/miden-lifted-stark/src/pcs/deep/tests.rs b/stark/miden-lifted-stark/src/pcs/deep/tests.rs new file mode 100644 index 0000000000..c7250c1df2 --- /dev/null +++ b/stark/miden-lifted-stark/src/pcs/deep/tests.rs @@ -0,0 +1,107 @@ +//! End-to-end tests for DEEP quotient prover/verifier agreement. + +use alloc::vec; + +use proof::DeepTranscript; +use prover::DeepPoly; +use rand::{RngExt, SeedableRng, distr::StandardUniform, prelude::SmallRng}; +use verifier::DeepOracle; + +use super::*; +use crate::{ + lmcs::{Lmcs, LmcsTree, tree_indices::TreeIndices}, + testing::configs::goldilocks_poseidon2::{ + Felt, Lmcs as BaseLmcs, QuadFelt, prover_channel_with_commitment, test_lmcs, + verifier_channel_with_commitment, + }, +}; + +/// End-to-end: prover's `DeepPoly.open()` must match verifier's channel-based openings. +#[test] +fn deep_quotient_end_to_end() { + let rng = &mut SmallRng::seed_from_u64(42); + let lmcs = test_lmcs(); + + // Parameters + let log_blowup: u8 = 2; + let log_lde_height: u8 = 10; + let lde_height = 1 << log_lde_height as usize; + + let params = DeepParams { deep_pow_bits: 1 }; + // Two random opening points + let z1: QuadFelt = rng.sample(StandardUniform); + let z2: QuadFelt = rng.sample(StandardUniform); + + // Create matrices of varying heights (ascending order required) + // specs: (log_scaling, width) where height = n >> log_scaling + let specs: Vec<(usize, usize)> = vec![(2, 2), (1, 3), (0, 4)]; // heights: n/4, n/2, n + let matrices: Vec> = specs + .iter() + .map(|&(log_scaling, width)| { + let height = lde_height >> log_scaling; + RowMajorMatrix::::rand(rng, height, width) + }) + .collect(); + + // Step 1: Commit matrices via LMCS (aligned for trace commitments) + let tree = lmcs.build_aligned_tree(matrices); + let commitment = tree.root(); + let widths = tree.aligned_widths(); + + // Step 3: Prover constructs DeepPoly (handles observe, grind, sample internally) + let mut prover_channel = prover_channel_with_commitment(&commitment); + let trace_trees: &[&_] = &[&tree]; + let deep_poly = DeepPoly::from_trees::( + params, + trace_trees, + [z1, z2], + log_blowup, + &mut prover_channel, + ); + // Sample domain indices. The LMCS tree is indexed by domain order. + let tree_indices = + TreeIndices::new([0, 1, lde_height / 4, lde_height / 2, lde_height - 1], log_lde_height) + .expect("indices are in range"); + tree.prove_batch(&tree_indices, &mut prover_channel); + let (prover_digest, transcript) = prover_channel.finalize(); + + // Create commitments slice for multi-commitment API (single commitment in this case) + let commitments = vec![(commitment, widths)]; + + // Step 4: Verifier constructs DeepOracle with same transcript state + let mut verifier_channel = verifier_channel_with_commitment(&transcript, &commitment); + let (deep_oracle, _evals) = + DeepOracle::new(params, &[z1, z2], commitments, log_lde_height, &mut verifier_channel) + .expect("DeepOracle construction should succeed"); + + // Step 5: Verify at multiple query tree indices (proofs are read from transcript) + let verifier_evals = deep_oracle + .open_batch(&lmcs, &tree_indices, &mut verifier_channel) + .expect("Merkle verification should pass"); + + for &tree_idx in tree_indices.iter() { + // Prover's deep_evals are in bit-reversed order internally: + // deep_evals[bitrev(d)] = Q(g·ω^d). For domain index d, access bitrev(d). + let bitrev_idx = p3_util::reverse_bits_len(tree_idx, log_lde_height as usize); + let prover_eval = deep_poly.deep_evals[bitrev_idx]; + let verifier_eval = verifier_evals[&tree_idx]; + assert_eq!( + prover_eval, verifier_eval, + "Prover and verifier disagree at tree index {tree_idx}" + ); + } + + let verifier_digest = verifier_channel.finalize().expect("transcript should finalize cleanly"); + assert_eq!(prover_digest, verifier_digest); + + // Re-parse DeepTranscript (DEEP phase only) from a fresh channel. + let reparse_commitments = vec![(commitment, tree.aligned_widths())]; + let mut reparse_channel = verifier_channel_with_commitment(&transcript, &commitment); + DeepTranscript::::from_verifier_channel( + ¶ms, + &reparse_commitments, + 2, // num_eval_points + &mut reparse_channel, + ) + .expect("DeepTranscript re-parse should succeed"); +} diff --git a/stark/miden-lifted-stark/src/pcs/deep/verifier.rs b/stark/miden-lifted-stark/src/pcs/deep/verifier.rs new file mode 100644 index 0000000000..c0b265fc36 --- /dev/null +++ b/stark/miden-lifted-stark/src/pcs/deep/verifier.rs @@ -0,0 +1,211 @@ +use alloc::{collections::BTreeMap, vec::Vec}; +use core::{iter::zip, marker::PhantomData}; + +use miden_stark_transcript::{TranscriptError, VerifierChannel}; +use p3_field::{ExtensionField, TwoAdicField}; +use p3_matrix::Matrix; +use thiserror::Error; + +use crate::{ + lmcs::{Lmcs, LmcsError, tree_indices::TreeIndices}, + pcs::{ + deep::{DeepParams, proof::OpenedValues, read_eval_matrices}, + utils::horner_acc, + }, +}; + +/// Verifier's view of the DEEP quotient as a point-query oracle. +/// +/// The prover claims OOD evaluations for all committed columns at a small set of points +/// `zⱼ`. The verifier uses a random `α` to reduce (batch) all columns into a single +/// polynomial `f_red`, and a random `β` to combine multiple opening points into one +/// DEEP quotient polynomial: +/// +/// ```text +/// Q(X) = Σⱼ βʲ · (f_red(zⱼ) − f_red(X)) / (zⱼ − X) +/// ``` +/// +/// This oracle stores the commitments and the reduced OOD claims `(zⱼ, f_red(zⱼ))`. +/// At query time it: +/// - verifies Merkle openings for all committed matrices at the query index, +/// - reduces the opened row to `f_red(X)` using Horner with the same `α`, +/// - reconstructs `Q(X)` and returns it to the FRI verifier. +/// +/// Lifting is transparent at this layer: the prover commits to lifted codewords, so +/// every opened column is interpreted as a polynomial over the same max domain. +pub struct DeepOracle, L: Lmcs> { + /// Trace commitments with their widths (one per trace tree). + /// + /// Widths must match the committed rows (including any alignment padding if + /// `build_aligned_tree` was used). + commitments: Vec<(L::Commitment, Vec)>, + + /// Log₂ of the LDE domain height (tree has 2^log_lde_height leaves). + /// Verifier expects all commitments to be lifted to this same LDE height. + log_lde_height: u8, + + /// Reduced openings: pairs of `(zⱼ, f_reduced(zⱼ))` from the prover's claims. + reduced_openings: Vec<(EF, EF)>, + + /// Challenge `α` for batching columns into `f_reduced`. + challenge_columns: EF, + /// Challenge `β` for batching opening points. + challenge_points: EF, + + _marker: PhantomData, +} + +impl, L: Lmcs> DeepOracle { + /// Construct by reading evaluations, checking PoW, and sampling challenges. + /// + /// Commitment widths must match the committed rows (including any alignment padding). + /// All commitments are expected to be lifted to the same `log_lde_height`. + /// + /// Preconditions: `eval_points` must be distinct and lie outside the trace subgroup `H` + /// and LDE evaluation coset `gK`. The outer protocol is expected to enforce this. + /// + /// `log_lde_height` is the log₂ of the LDE evaluation domain height (i.e. the height of + /// the committed LDE matrices). When a trace degree is known, it is typically + /// `log_trace_height + params.fri.log_blowup` (plus any extension used by the caller). + /// + /// Returns the oracle and per-matrix evaluations: `evals[g][m]` is a + /// `RowMajorMatrix` with one row per evaluation point. + pub fn new( + params: DeepParams, + eval_points: &[EF], + commitments: Vec<(L::Commitment, Vec)>, + log_lde_height: u8, + channel: &mut Ch, + ) -> Result<(Self, OpenedValues), DeepError> + where + Ch: VerifierChannel, + { + let group_widths: Vec<&[usize]> = commitments.iter().map(|(_, gw)| gw.as_slice()).collect(); + let evals = read_eval_matrices::(&group_widths, eval_points.len(), channel)?; + + // 1. Check grinding witness + channel.grind(params.deep_pow_bits)?; + + // 2. Sample DEEP challenges + let challenge_columns: EF = channel.sample_algebra_element(); + let challenge_points: EF = channel.sample_algebra_element(); + + // Horner reduction: fold across all evals for each evaluation point + let reduced_openings: Vec<(EF, EF)> = eval_points + .iter() + .enumerate() + .map(|(p, &point)| { + let val = evals.iter().flat_map(|g| g.iter()).fold(EF::ZERO, |acc, mat| { + // mat has num_eval_points rows (one per z), p < num_eval_points. + horner_acc( + acc, + challenge_columns, + mat.row(p).expect("eval point index in range"), + ) + }); + (point, val) + }) + .collect(); + + let oracle = Self { + commitments, + log_lde_height, + reduced_openings, + challenge_columns, + challenge_points, + _marker: PhantomData, + }; + + Ok((oracle, evals)) + } + + /// Open the oracle at given tree indices by reading proofs from a verifier channel. + /// + /// `tree_indices` are domain indices (sorted, deduplicated). + /// Returns a map from domain index to DEEP evaluation at that point. + /// + /// The reduction to `f_red` must match the prover's exactly. + /// + /// In particular, the prover streams columns in a fixed commitment-group order + /// (e.g. main, aux, quotient). The verifier must iterate groups in the same order so + /// that `horner_acc` assigns the same `α` powers to the same columns; otherwise the + /// reconstructed `Q(X)` will not match the FRI-committed codeword. + pub fn open_batch( + &self, + lmcs: &L, + tree_indices: &TreeIndices, + channel: &mut Ch, + ) -> Result, DeepError> + where + Ch: VerifierChannel, + { + let mut reduced_rows: BTreeMap = + tree_indices.iter().map(|&idx| (idx, EF::ZERO)).collect(); + + for (group_idx, (commit, widths)) in self.commitments.iter().enumerate() { + let opened_rows = lmcs + .open_batch(commit, widths, tree_indices, channel) + .map_err(|source| DeepError::LmcsError { source, tree: group_idx })?; + + // Reduce opened rows via Horner: f_reduced(X) = Σᵢ αᵂ⁻¹⁻ⁱ · fᵢ(X). + // + // `horner_acc` continues the running accumulation across commitment groups: + // group 0's columns get the highest powers, group 1's continue from where + // group 0 left off. The coefficient ordering must match the prover's exactly; + // otherwise the reconstructed DEEP quotient diverges from the FRI-committed + // codeword, causing verification failure. + for (tree_idx, acc) in reduced_rows.iter_mut() { + let rows_for_query = opened_rows + .get(tree_idx) + .ok_or(DeepError::InvalidOpening { tree: group_idx, tree_index: *tree_idx })?; + *acc = horner_acc(*acc, self.challenge_columns, rows_for_query.iter_values()); + } + } + + let generator = F::two_adic_generator(self.log_lde_height as usize); + let shift = F::GENERATOR; + + // Reconstruct Q(x) at each queried domain point x from the opened row data. + // If the prover's OOD claims were correct, these values lie on the + // low-degree polynomial committed via FRI. + let evals: BTreeMap = reduced_rows + .into_iter() + .map(|(tree_idx, reduced_row)| { + // Recover domain point X = g·ω^{tree_idx} (tree index = domain index) + let row_point = shift * generator.exp_u64(tree_idx as u64); + + // DEEP quotient: Q(X) = Σⱼ βʲ · (f_reduced(zⱼ) - f_reduced(X)) / (zⱼ - X) + // Precondition: eval points lie outside the LDE domain. + let mut deep_eval = EF::ZERO; + for ((point, reduced_eval), coeff_point) in + zip(&self.reduced_openings, self.challenge_points.powers()) + { + let denom_inv = (*point - row_point) + .try_inverse() + .ok_or(DeepError::EvalPointOnDomain { tree_index: tree_idx })?; + deep_eval += coeff_point * (*reduced_eval - reduced_row) * denom_inv; + } + Ok((tree_idx, deep_eval)) + }) + .collect::>()?; + + Ok(evals) + } +} + +// ============================================================================ +// Error Types +// ============================================================================ + +/// Errors that can occur during DEEP oracle construction or verification. +#[derive(Debug, Error)] +pub enum DeepError { + #[error("LMCS verification failed for commitment group {tree}: {source}")] + LmcsError { source: LmcsError, tree: usize }, + #[error("invalid opening for tree index {tree_index} in commitment group {tree}")] + InvalidOpening { tree: usize, tree_index: usize }, + #[error("evaluation point coincides with domain point at tree index {tree_index}")] + EvalPointOnDomain { tree_index: usize }, + #[error("transcript error: {0}")] + TranscriptError(#[from] TranscriptError), +} diff --git a/stark/miden-lifted-stark/src/pcs/fri/fold/arity2.rs b/stark/miden-lifted-stark/src/pcs/fri/fold/arity2.rs new file mode 100644 index 0000000000..173de45a5f --- /dev/null +++ b/stark/miden-lifted-stark/src/pcs/fri/fold/arity2.rs @@ -0,0 +1,79 @@ +//! Arity-2 FRI folding using even-odd decomposition. +//! +//! Any polynomial `f(X)` can be uniquely decomposed into even and odd parts: +//! +//! ```text +//! f(X) = fₑ(X²) + X · fₒ(X²) +//! ``` +//! +//! where `fₑ` contains the even-degree coefficients and `fₒ` the odd-degree coefficients. +//! +//! ## Key Identity +//! +//! From evaluations at `s` and `−s`, we can recover `fₑ(s²)` and `fₒ(s²)`: +//! +//! ```text +//! f(s) = fₑ(s²) + s · fₒ(s²) +//! f(−s) = fₑ(s²) − s · fₒ(s²) +//! ``` +//! +//! Solving: +//! +//! ```text +//! fₑ(s²) = (f(s) + f(−s)) / 2 +//! fₒ(s²) = (f(s) − f(−s)) / (2s) +//! ``` +//! +//! ## FRI Folding +//! +//! Given a challenge `β`, FRI defines the folded polynomial: +//! +//! ```text +//! g(X) = fₑ(X) + β · fₒ(X) +//! ``` +//! +//! For a coset `{s, −s}`, we compute the folded value `g(s²)` by interpolating +//! `fₑ(s²)` and `fₒ(s²)` from the two evaluations (and when `deg f < 2`, `g(s²)=f(β)`). + +use p3_field::{Algebra, TwoAdicField}; + +/// Arity-2 FRI folding using even-odd decomposition. +/// +/// Folds pairs of evaluations using the even-odd decomposition: +/// `f(β) = (f(s) + f(-s))/2 + β/s · (f(s) - f(-s))/2` +/// +/// ## Inputs +/// +/// - `evals`: slice of 2 evaluations `[f(s), f(−s)]` in bit-reversed order. +/// - `s_inv`: the inverse of the coset generator `s`. +/// - `beta`: the FRI folding challenge `β`. +/// +/// ## Algorithm +/// +/// Using the even-odd decomposition `f(X) = fₑ(X²) + X · fₒ(X²)`: +/// +/// 1. Compute `fₑ(s²) = (f(s) + f(−s)) / 2` +/// 2. Compute `fₒ(s²) = (f(s) − f(−s)) / (2s)` +/// 3. Return `g(s²) = fₑ(s²) + β · fₒ(s²)` (equals `f(β)` when `deg f < 2`) +#[inline(always)] +pub fn fold_evals(evals: &[PEF], s_inv: PF, beta: PEF) -> PEF +where + F: TwoAdicField, + PF: Algebra + Algebra, + PEF: Algebra, +{ + debug_assert_eq!(evals.len(), 2, "evals must have 2 elements"); + // y₀ = f(s), y₁ = f(−s) + let [y0, y1] = [evals[0].clone(), evals[1].clone()]; + + // f(β) = fₑ(s²) + β · fₒ(s²) + // Even part: fₑ(s²) = (f(s) + f(−s)) / 2 + // Odd part: fₒ(s²) = (f(s) − f(−s)) / (2s) + // Combined: ((y0 + y1) + (y0 - y1) * beta * s_inv) / 2 + let sum = y0.clone() + y1.clone(); + let diff = y0 - y1; + let result = sum + diff * beta * s_inv; + + // Divide by 2 + result.div_2exp_u64(1) +} diff --git a/stark/miden-lifted-stark/src/pcs/fri/fold/arity4.rs b/stark/miden-lifted-stark/src/pcs/fri/fold/arity4.rs new file mode 100644 index 0000000000..d9f7643901 --- /dev/null +++ b/stark/miden-lifted-stark/src/pcs/fri/fold/arity4.rs @@ -0,0 +1,162 @@ +//! Arity-4 FRI folding using inverse FFT. +//! +//! Given evaluations of a polynomial `f` on a coset `s·⟨ω⟩` where `ω = i` is a primitive +//! 4th root of unity, we recover the folded value `g(s⁴)` for a challenge `β` +//! (and when `deg f < 4`, this equals `f(β)`). +//! +//! ## Setup +//! +//! Let `f(X) = c₀ + c₁X + c₂X² + c₃X³` with evaluations on the coset `s·⟨ω⟩`: +//! +//! ```text +//! y₀ = f(s), y₁ = f(ωs), y₂ = f(ω²s), y₃ = f(ω³s) +//! ``` +//! +//! We store these in **bit-reversed order**: `[y₀, y₂, y₁, y₃]`. +//! +//! We decompose `f` by residue class modulo 4: +//! `f(X) = Σⱼ X^j · fⱼ(X⁴)` for j ∈ {0,1,2,3}. +//! The folded polynomial is `g(X) = Σⱼ β^j · fⱼ(X)`. +//! +//! ## Algorithm +//! +//! 1. **Inverse FFT**: Recover coefficients of `f(sX)` from evaluations on `⟨ω⟩`. +//! 2. **Evaluate**: Compute `f(sX)` at `X = β/s`, yielding the folded value `g(s⁴)`. + +use core::array; + +use p3_field::{Algebra, TwoAdicField}; + +/// Evaluate the folded value `g(s⁴)` from evaluations on a coset +/// (equals `f(β)` when `deg f < 4`). +/// +/// ## Inputs +/// +/// - `evals`: slice of 4 evaluations `[f(s), f(ω²s), f(ωs), f(ω³s)]` in bit-reversed order, +/// equivalently `[f(s), f(−s), f(is), f(−is)]` since `ω = i`. +/// - `s_inv`: the inverse of the coset generator `s`. +/// - `beta`: the FRI folding challenge `β`. +/// +/// ## FRI Context +/// +/// In arity-4 FRI, the polynomial `f` is evaluated on cosets of the form `s·⟨ω⟩`. +/// The verifier needs to check that the folded value `g(s⁴)` matches the prover's claim. +/// This function recovers `g(s⁴)` from the four coset evaluations via interpolation. +#[inline(always)] +pub fn fold_evals(evals: &[PEF], s_inv: PF, beta: PEF) -> PEF +where + F: TwoAdicField, + PF: Algebra + Algebra, + PEF: Algebra, +{ + debug_assert_eq!(evals.len(), 4, "evals must have 4 elements"); + let evals = array::from_fn(|i| evals[i].clone()); + // Recover coefficients [c₀, c₁, c₂, c₃] of 4·f(sX) via inverse FFT. + let [c0, c1, c2, c3] = ifft4::(evals); + + // Folded value g(s⁴) = (1/4) · (c₀ + c₁·x + c₂·x² + c₃·x³) where x = β/s. + let x = beta * s_inv; + let terms = [ + c0, // c₀ + c1 * x.clone(), // c₁ · x + c2 * x.square(), // c₂ · x² + c3 * x.cube(), // c₃ · x³ + ]; + + // Divide by 4 + let four_inv: PF = F::ONE.halve().halve().into(); + PEF::sum_array::<4>(&terms) * four_inv +} + +/// Size-4 inverse FFT (unscaled), input in bit-reversed order. +/// +/// Returns coefficients `[c₀, c₁, c₂, c₃]` of `4·f(sX) = c₀ + c₁X + c₂X² + c₃X³`. +#[inline(always)] +fn ifft4(evals: [PEF; 4]) -> [PEF; 4] +where + F: TwoAdicField, + PF: Algebra + Algebra, + PEF: Algebra, +{ + // ω = i, primitive 4th root of unity + let w: PF = F::two_adic_generator(2).into(); + + // Input (bit-reversed): [y₀, y₂, y₁, y₃] + let [y0, y2, y1, y3] = evals; + + // Inverse DFT formula (without 1/N normalization): + // 4cⱼ = Σₖ yₖ · ω^(−jk) + // + // Expanded for each coefficient (i = imaginary unit): + // 4c₀ = y₀ + y₁ + y₂ + y₃ + // 4c₁ = y₀ − i·y₁ − y₂ + i·y₃ + // 4c₂ = y₀ − y₁ + y₂ − y₃ + // 4c₃ = y₀ + i·y₁ − y₂ − i·y₃ + + // ------------------------------------------------------------------------- + // Stage 0: length-2 butterflies on bit-reversed pairs + // ------------------------------------------------------------------------- + let s02 = y0.clone() + y2.clone(); // y₀ + y₂ (used in c₀, c₂) + let d02 = y0 - y2; // y₀ − y₂ (used in c₁, c₃) + let s13 = y1.clone() + y3.clone(); // y₁ + y₃ (used in c₀, c₂) + let d31 = y3 - y1; // y₃ − y₁ (note: negated so we can multiply by ω instead of ω⁻¹) + + // ------------------------------------------------------------------------- + // Stage 1: combine via length-4 butterflies + // + // Rewriting the target formulas using stage 0 results: + // 4c₀ = (y₀ + y₂) + (y₁ + y₃) = s02 + s13 + // 4c₂ = (y₀ + y₂) − (y₁ + y₃) = s02 − s13 + // 4c₁ = (y₀ − y₂) + i(y₃ − y₁) = d02 + i·d31 + // 4c₃ = (y₀ − y₂) − i(y₃ − y₁) = d02 − i·d31 + // ------------------------------------------------------------------------- + let d31_w = d31 * w; // i · (y₃ − y₁) + + [ + s02.clone() + s13.clone(), // 4c₀ + d02.clone() + d31_w.clone(), // 4c₁ + s02 - s13, // 4c₂ + d02 - d31_w, // 4c₃ + ] +} + +// ============================================================================ +// Tests +// ============================================================================ + +#[cfg(test)] +mod tests { + use p3_dft::{NaiveDft, TwoAdicSubgroupDft}; + use p3_field::PrimeCharacteristicRing; + use p3_matrix::dense::RowMajorMatrix; + use rand::{RngExt, SeedableRng, distr::StandardUniform, prelude::SmallRng}; + + use super::*; + use crate::testing::configs::goldilocks_poseidon2::{Felt, QuadFelt}; + + /// Test that ifft4 correctly recovers polynomial coefficients from DFT evaluations. + #[test] + fn test_ifft4() { + let mut rng = SmallRng::seed_from_u64(42); + + // Random polynomial coefficients + let coeffs: [QuadFelt; 4] = array::from_fn(|_| rng.sample(StandardUniform)); + + // Compute DFT using NaiveDft (standard order) + let coeffs_matrix = RowMajorMatrix::new(coeffs.to_vec(), 1); + let evals_matrix = NaiveDft.dft_batch(coeffs_matrix); + let evals_std = evals_matrix.values; + + // Convert to bit-reversed order for ifft4 + let evals_br: [QuadFelt; 4] = [evals_std[0], evals_std[2], evals_std[1], evals_std[3]]; + + // Run ifft4 (returns 4 * coefficients) + let recovered_scaled = ifft4::(evals_br); + + // Verify: recovered_scaled[i] == 4 * coeffs[i] + for (i, (recovered, &original)) in recovered_scaled.iter().zip(coeffs.iter()).enumerate() { + let expected = original.double().double(); // 4 * original + assert_eq!(*recovered, expected, "Coefficient mismatch at index {i}"); + } + } +} diff --git a/stark/miden-lifted-stark/src/pcs/fri/fold/arity8.rs b/stark/miden-lifted-stark/src/pcs/fri/fold/arity8.rs new file mode 100644 index 0000000000..fcf5e78b71 --- /dev/null +++ b/stark/miden-lifted-stark/src/pcs/fri/fold/arity8.rs @@ -0,0 +1,192 @@ +//! Arity-8 FRI folding using inverse FFT. +//! +//! Given evaluations of a polynomial `f` on a coset `s·⟨ω⟩` where `ω` is a primitive +//! 8th root of unity, we recover the folded value `g(s⁸)` for a challenge `β` +//! (and when `deg f < 8`, this equals `f(β)`). +//! +//! ## Algorithm +//! +//! 1. **Inverse FFT**: Recover coefficients of `f(sX)` from evaluations on `⟨ω⟩`. +//! 2. **Evaluate**: Compute `f(sX)` at `X = β/s`, yielding the folded value `g(s⁸)`. +//! +//! The inverse FFT uses a 3-stage Cooley-Tukey DIT butterfly structure. +//! +//! We decompose `f` by residue class modulo 8: +//! `f(X) = Σⱼ X^j · fⱼ(X⁸)` for j ∈ {0..7}. +//! The folded polynomial is `g(X) = Σⱼ β^j · fⱼ(X)`. + +use core::array; + +use p3_field::{Algebra, TwoAdicField}; + +/// Evaluate the folded value `g(s⁸)` from evaluations on a coset +/// (equals `f(β)` when `deg f < 8`). +/// +/// ## Inputs +/// +/// - `evals`: slice of 8 evaluations `[f(s), f(ω⁴s), f(ω²s), f(ω⁶s), f(ωs), f(ω⁵s), f(ω³s), +/// f(ω⁷s)]` in bit-reversed order, where `ω` is the primitive 8th root of unity. +/// - `s_inv`: the inverse of the coset generator `s`. +/// - `beta`: the FRI folding challenge `β`. +#[inline(always)] +pub fn fold_evals(evals: &[PEF], s_inv: PF, beta: PEF) -> PEF +where + F: TwoAdicField, + PF: Algebra + Algebra, + PEF: Algebra, +{ + debug_assert_eq!(evals.len(), 8, "evals must have 8 elements"); + let evals = array::from_fn(|i| evals[i].clone()); + // Recover coefficients [c₀, ..., c₇] of 8·f(sX) via inverse FFT. + let coeffs = ifft8::(evals); + + // Folded value g(s⁸) = (1/8) · Σᵢ cᵢ · xⁱ where x = β/s. + let x = beta * s_inv; + + // Compute powers of x efficiently + let x2 = x.square(); + let x3 = x2.clone() * x.clone(); + let x4 = x2.square(); + let x5 = x4.clone() * x.clone(); + let x6 = x4.clone() * x2.clone(); + let x7 = x4.clone() * x3.clone(); + + let terms = [ + coeffs[0].clone(), + coeffs[1].clone() * x, + coeffs[2].clone() * x2, + coeffs[3].clone() * x3, + coeffs[4].clone() * x4, + coeffs[5].clone() * x5, + coeffs[6].clone() * x6, + coeffs[7].clone() * x7, + ]; + + // Divide by 8 + let eight_inv: PF = F::ONE.halve().halve().halve().into(); + PEF::sum_array::<8>(&terms) * eight_inv +} + +/// Size-8 inverse FFT (unscaled), input in bit-reversed order. +/// +/// Returns coefficients `[c₀, c₁, ..., c₇]` of `8·f(sX)`. +/// +/// Uses DIT butterfly operations following the pattern from [`p3_dft::DitButterfly`]. +#[inline(always)] +fn ifft8(evals: [PEF; 8]) -> [PEF; 8] +where + F: TwoAdicField, + PF: Algebra + Algebra, + PEF: Algebra, +{ + // Compute powers of ω₈ needed for inverse twiddles + let w8 = F::two_adic_generator(3); + let w8_2 = F::two_adic_generator(2); + let w8_3 = w8_2 * w8; + let w8_5 = w8_3 * w8_2; + let w8_6 = w8_3.square(); + let w8_7 = w8_6 * w8; + + // Inverse twiddles: ω⁻ᵏ = ω^(n-k) + // Note: ω₄⁻¹ = ω₄³ = (ω₈²)³ = ω₈⁶ + let w4_inv: PF = w8_6.into(); + let w8_inv_1: PF = w8_7.into(); + let w8_inv_2: PF = w8_6.into(); + let w8_inv_3: PF = w8_5.into(); + + // Bit-reversed input: [y₀, y₄, y₂, y₆, y₁, y₅, y₃, y₇] + let [y0, y4, y2, y6, y1, y5, y3, y7] = evals; + + // ------------------------------------------------------------------------- + // Stage 0: 4 twiddle-free butterflies + // ------------------------------------------------------------------------- + let (a0, a1) = twiddle_free_butterfly(y0, y4); + let (a2, a3) = twiddle_free_butterfly(y2, y6); + let (a4, a5) = twiddle_free_butterfly(y1, y5); + let (a6, a7) = twiddle_free_butterfly(y3, y7); + + // ------------------------------------------------------------------------- + // Stage 1: length-4 butterflies with twiddle ω₄⁻¹ + // ------------------------------------------------------------------------- + let (b0, b2) = twiddle_free_butterfly(a0, a2); + let (b1, b3) = dit_butterfly(a1, a3, &w4_inv); + + let (b4, b6) = twiddle_free_butterfly(a4, a6); + let (b5, b7) = dit_butterfly(a5, a7, &w4_inv); + + // ------------------------------------------------------------------------- + // Stage 2: length-8 butterflies with twiddles ω₈⁻ᵏ + // ------------------------------------------------------------------------- + let (c0, c4) = twiddle_free_butterfly(b0, b4); + let (c1, c5) = dit_butterfly(b1, b5, &w8_inv_1); + let (c2, c6) = dit_butterfly(b2, b6, &w8_inv_2); + let (c3, c7) = dit_butterfly(b3, b7, &w8_inv_3); + + [c0, c1, c2, c3, c4, c5, c6, c7] +} + +// ============================================================================ +// Butterfly Helpers +// ============================================================================ + +/// DIT butterfly: `(x1 + twiddle * x2, x1 - twiddle * x2)` +/// +/// See [`p3_dft::DitButterfly`] for the standard implementation. +/// This version supports mixed-type operations where values are extension field +/// elements and twiddles are base field elements. +#[inline(always)] +fn dit_butterfly + Algebra>(x1: EF, x2: EF, twiddle: &F) -> (EF, EF) { + let x2_tw = x2 * twiddle.clone(); + (x1.clone() + x2_tw.clone(), x1 - x2_tw) +} + +/// Twiddle-free butterfly: `(x1 + x2, x1 - x2)` +/// +/// See [`p3_dft::TwiddleFreeButterfly`] for the standard implementation. +#[inline(always)] +fn twiddle_free_butterfly>(x1: F, x2: F) -> (F, F) { + (x1.clone() + x2.clone(), x1 - x2) +} + +// ============================================================================ +// Tests +// ============================================================================ + +#[cfg(test)] +mod tests { + use p3_dft::{NaiveDft, TwoAdicSubgroupDft}; + use p3_field::PrimeCharacteristicRing; + use p3_matrix::dense::RowMajorMatrix; + use p3_util::reverse_slice_index_bits; + use rand::{RngExt, SeedableRng, distr::StandardUniform, prelude::SmallRng}; + + use super::*; + use crate::testing::configs::goldilocks_poseidon2::{Felt, QuadFelt}; + + /// Test that ifft8 correctly recovers polynomial coefficients from DFT evaluations. + #[test] + fn test_ifft8() { + let mut rng = SmallRng::seed_from_u64(42); + + // Random polynomial coefficients + let coeffs: [QuadFelt; 8] = array::from_fn(|_| rng.sample(StandardUniform)); + + // Compute DFT using NaiveDft (standard order) + let coeffs_matrix = RowMajorMatrix::new(coeffs.to_vec(), 1); + let evals_matrix = NaiveDft.dft_batch(coeffs_matrix); + let mut evals = evals_matrix.values; + + // Convert to bit-reversed order for ifft8 + reverse_slice_index_bits(&mut evals); + let evals_br: [QuadFelt; 8] = evals.try_into().unwrap(); + + // Run ifft8 (returns 8 * coefficients) + let recovered_scaled = ifft8::(evals_br); + + // Verify: recovered_scaled[i] == 8 * coeffs[i] + for (i, (recovered, &original)) in recovered_scaled.iter().zip(coeffs.iter()).enumerate() { + let expected = original.double().double().double(); // 8 * original + assert_eq!(*recovered, expected, "Coefficient mismatch at index {i}"); + } + } +} diff --git a/stark/miden-lifted-stark/src/pcs/fri/fold/mod.rs b/stark/miden-lifted-stark/src/pcs/fri/fold/mod.rs new file mode 100644 index 0000000000..1dadb351f7 --- /dev/null +++ b/stark/miden-lifted-stark/src/pcs/fri/fold/mod.rs @@ -0,0 +1,400 @@ +//! FRI folding via polynomial interpolation. +//! +//! FRI (Fast Reed-Solomon IOP of Proximity) folds evaluations on a coset into a +//! lower-degree polynomial value parameterized by a challenge `β`. Each row fold +//! returns the folded value `g(s^r)` for its coset `s·⟨ω_r⟩` (and when `deg f < r`, +//! this equals `f(β)`). This module provides a struct-based abstraction for FRI +//! folding at different arities. +//! +//! ## Arity +//! +//! The **arity** determines how many evaluations are folded together in each round: +//! - **Arity 2**: Fold pairs `{f(s), f(-s)}` using even-odd decomposition +//! - **Arity 4**: Fold quadruples `{f(s), f(-s), f(is), f(-is)}` using inverse FFT +//! - **Arity 8**: Fold octuples using size-8 inverse FFT +//! +//! Higher arity reduces the number of FRI rounds but increases per-round work. + +mod arity2; +mod arity4; +mod arity8; + +use alloc::vec::Vec; + +use p3_field::{ExtensionField, PackedValue, TwoAdicField}; +use p3_matrix::{Matrix, dense::RowMajorMatrixView}; +use p3_maybe_rayon::prelude::*; + +use crate::pcs::utils::PackedFieldExtensionExt; + +/// FRI folding strategy. +/// +/// This struct encapsulates different folding arities (2, 4, 8). +#[derive(Clone, Copy, Debug)] +pub struct FriFold { + pub(crate) log_arity: u8, +} + +impl FriFold { + /// Create a new folder for a supported log-arity (currently only 1, 2, 3). + pub const fn new(log_arity: u8) -> Option { + if log_arity == 1 || log_arity == 2 || log_arity == 3 { + Some(Self { log_arity }) + } else { + None + } + } + + #[inline] + pub const fn arity(&self) -> usize { + 1 << self.log_arity as usize + } + + #[inline] + pub const fn log_arity(&self) -> u8 { + self.log_arity + } + + /// Fold evaluations from a slice of extension field elements. + /// + /// The slice must have exactly `arity()` elements. + /// Used by the verifier in scalar mode. + /// + /// Folding is the core FRI step: it turns `arity` evaluations of `f` on a coset + /// `s·⟨ω⟩` into a single evaluation of a new polynomial `g` on the folded domain. + /// + /// Conceptually, write `f(X)` as `Σⱼ Xʲ·fⱼ(X^arity)`. The fold interpolates the + /// `fⱼ` values from the row (an iFFT on the coset) and then takes a random linear + /// combination with challenge `β` to obtain `g(s^arity)`. The resulting `g` has + /// degree reduced by a factor of `arity`. If `deg(f) < arity`, folding recovers + /// `f(β)` exactly. + #[inline] + pub fn fold_evals>( + self, + evals: &[EF], + s_inv: F, + beta: EF, + ) -> EF { + match self.log_arity { + 1 => arity2::fold_evals::(evals, s_inv, beta), + 2 => arity4::fold_evals::(evals, s_inv, beta), + 3 => arity8::fold_evals::(evals, s_inv, beta), + _ => unreachable!("unsupported arity"), + } + } + + /// Packed (SIMD) version of `fold_evals`. + #[inline] + fn fold_evals_packed>( + self, + evals: &[EF::ExtensionPacking], + s_inv: F::Packing, + beta: EF, + ) -> EF::ExtensionPacking { + let beta_packed: EF::ExtensionPacking = beta.into(); + match self.log_arity { + 1 => { + arity2::fold_evals::(evals, s_inv, beta_packed) + }, + 2 => { + arity4::fold_evals::(evals, s_inv, beta_packed) + }, + 3 => { + arity8::fold_evals::(evals, s_inv, beta_packed) + }, + _ => unreachable!("unsupported arity"), + } + } + + /// Fold a matrix of coset evaluations using the challenge `beta`. + /// + /// Each row contains evaluations on a coset `s·⟨ω⟩`. Returns folded + /// evaluations, one per row, maintaining bit-reversed order. + /// + /// Automatically dispatches to scalar or packed implementation based on matrix size. + pub fn fold_matrix>( + self, + input: RowMajorMatrixView<'_, EF>, + s_invs: &[F], + beta: EF, + ) -> Vec { + let width = F::Packing::WIDTH; + if input.height() < width || width == 1 { + // Scalar path + let arity = self.arity(); + assert_eq!(input.width, arity); + input + .values + .par_chunks(arity) + .zip(s_invs.par_iter()) + .map(|(evals, &s_inv)| self.fold_evals(evals, s_inv, beta)) + .collect() + } else { + match self.log_arity { + 1 => self.fold_matrix_packed_impl::<2, F, EF>(input, s_invs, beta), + 2 => self.fold_matrix_packed_impl::<4, F, EF>(input, s_invs, beta), + 3 => self.fold_matrix_packed_impl::<8, F, EF>(input, s_invs, beta), + _ => unreachable!("unsupported arity"), + } + } + } + + fn fold_matrix_packed_impl( + self, + input: RowMajorMatrixView<'_, EF>, + s_invs: &[F], + beta: EF, + ) -> Vec + where + F: TwoAdicField, + EF: ExtensionField, + { + assert_eq!(input.width, ARITY); + assert_eq!(input.values.len() % ARITY, 0); + let evals: &[[EF; ARITY]] = unsafe { + // SAFETY: the slice length is checked above to be a multiple of `ARITY`, and + // `[EF; ARITY]` is laid out contiguously like `ARITY` adjacent `EF` values. + core::slice::from_raw_parts(input.values.as_ptr().cast(), input.values.len() / ARITY) + }; + let width = F::Packing::WIDTH; + assert_eq!(evals.len() % width, 0); + + let mut new_evals = EF::zero_vec(evals.len()); + + new_evals + .par_chunks_exact_mut(width) + .zip(evals.par_chunks_exact(width)) + .zip(s_invs.par_chunks_exact(width)) + .for_each(|((new_evals_chunk, evals_chunk), s_inv_chunk)| { + let evals_packed = + >::pack_ext_columns::< + ARITY, + >(evals_chunk); + let s_invs_packed = F::Packing::from_slice(s_inv_chunk); + let new_evals_packed = + self.fold_evals_packed::(&evals_packed, *s_invs_packed, beta); + >::to_ext_slice( + &new_evals_packed, + new_evals_chunk, + ); + }); + new_evals + } +} + +// ============================================================================ +// Shared Test Utilities +// ============================================================================ + +#[cfg(test)] +pub mod tests { + use alloc::vec::Vec; + + use p3_field::{ExtensionField, Field, PrimeCharacteristicRing, TwoAdicField}; + use p3_matrix::dense::RowMajorMatrix; + use p3_util::reverse_slice_index_bits; + use rand::{ + RngExt, SeedableRng, + distr::{Distribution, StandardUniform}, + prelude::SmallRng, + }; + + use super::*; + use crate::{ + pcs::utils::horner, + testing::{ + configs::goldilocks_poseidon2::{Felt, QuadFelt}, + params::{FRI_FOLD_ARITY_2, FRI_FOLD_ARITY_4, FRI_FOLD_ARITY_8}, + }, + }; + + // Type alias for tests using packed fields + type Pf = ::Packing; + + /// Test fold_evals against NaiveDft coset evaluations for a specific arity. + /// + /// Generates a random polynomial, computes evaluations on a coset using NaiveDft, + /// then verifies fold_evals correctly recovers f(β). + fn test_fold_evals_naive_dft(fold: FriFold) { + use p3_dft::{NaiveDft, TwoAdicSubgroupDft}; + + let mut rng = SmallRng::seed_from_u64(42); + let arity = fold.arity(); + + // Polynomial of degree arity-1 + let coeffs: Vec = (0..arity).map(|_| rng.sample(StandardUniform)).collect(); + + // Coset generator + let s: Felt = rng.sample(StandardUniform); + let s_inv = s.inverse(); + + // Compute evaluations using NaiveDft on coset s·⟨ω⟩ + let mut coeffs_padded = coeffs.clone(); + coeffs_padded.resize(arity, QuadFelt::ZERO); + let coeffs_matrix = RowMajorMatrix::new(coeffs_padded, 1); + let evals_matrix = NaiveDft.coset_dft_batch(coeffs_matrix, QuadFelt::from(s)); + let mut evals: Vec = evals_matrix.values; + reverse_slice_index_bits(&mut evals); + + // Fold with random beta + let beta: QuadFelt = rng.sample(StandardUniform); + let result = fold.fold_evals(&evals, s_inv, beta); + + // Expected: direct Horner evaluation at beta + let expected = horner(beta, coeffs.iter().rev().copied()); + assert_eq!(result, expected, "fold_evals mismatch for arity {arity}"); + } + + /// Test FRI folding correctness for a specific arity. + /// + /// Creates a random polynomial of degree `arity - 1`, evaluates it on a coset + /// of size `arity`, then verifies that `fold_evals` correctly recovers `f(β)`. + fn test_fold_correctness(fold: FriFold) + where + Base: TwoAdicField, + Ext: ExtensionField, + StandardUniform: Distribution + Distribution, + { + let rng = &mut SmallRng::seed_from_u64(1); + let beta: Ext = rng.sample(StandardUniform); + let arity = fold.arity(); + let log_arity = fold.log_arity() as usize; + + // Random polynomial of degree arity - 1 + let poly: Vec = (0..arity).map(|_| rng.sample(StandardUniform)).collect(); + + // Compute roots of unity in bit-reversed order for this arity + let mut roots: Vec = + Base::two_adic_generator(log_arity).powers().take(arity).collect(); + reverse_slice_index_bits(&mut roots); + + let s: Base = rng.sample(StandardUniform); + let s_inv = s.inverse(); + + // Evaluate polynomial at coset points: [f(s·root) for root in roots] + let evals: Vec = + roots.iter().map(|&root| horner(root * s, poly.iter().rev().copied())).collect(); + + // Expected: f(beta) + let expected = horner(beta, poly.iter().rev().copied()); + + // Test fold_evals + let result = fold.fold_evals(&evals, s_inv, beta); + assert_eq!(result, expected); + } + + /// Test that `fold_matrix` scalar and packed paths produce identical results. + /// + /// Creates a matrix large enough to trigger the packed path, then verifies + /// the result matches row-by-row scalar `fold_evals` computation. + fn test_fold_matrix_scalar_packed_equivalence(fold: FriFold) { + let rng = &mut SmallRng::seed_from_u64(42); + let arity = fold.arity(); + + // Create input matrix with height = multiple of packing width (triggers packed path) + let height = Pf::WIDTH * 4; + let values: Vec = + (0..height * arity).map(|_| rng.sample(StandardUniform)).collect(); + let input = RowMajorMatrix::new(values.clone(), arity); + + // Generate random coset generators and their inverses + let s_values: Vec = + (0..height).map(|_| rng.sample::(StandardUniform)).collect(); + let s_invs: Vec = s_values.iter().map(Field::inverse).collect(); + + let beta: QuadFelt = rng.sample(StandardUniform); + + // Scalar path: compute fold_evals for each row + let scalar_result: Vec = values + .chunks(arity) + .zip(s_invs.iter()) + .map(|(evals, &s_inv)| fold.fold_evals(evals, s_inv, beta)) + .collect(); + + // Packed path: call fold_matrix (uses packed impl for large matrices) + let packed_result = fold.fold_matrix(input.as_view(), &s_invs, beta); + + assert_eq!(scalar_result, packed_result, "Scalar vs packed mismatch for arity {arity}"); + } + + /// Test that folding preserves low-degree structure. + /// + /// After folding a degree-d polynomial, the result should have degree d/arity. + /// Verifies by checking that high coefficients are zero after IDFT. + fn test_folding_preserves_low_degree(fold: FriFold) { + let rng = &mut SmallRng::seed_from_u64(42); + let arity = fold.arity(); + let log_arity = fold.log_arity() as usize; + + let log_blowup = 2; + let log_poly_degree = 4; // degree 16 polynomial + let poly_degree = 1 << log_poly_degree; + let log_lde_size = log_poly_degree + log_blowup; + let lde_size = 1 << log_lde_size; + + // Generate random low-degree polynomial + let coeffs: Vec = (0..poly_degree).map(|_| rng.sample(StandardUniform)).collect(); + + // Compute LDE in bit-reversed order + let mut full_coeffs = coeffs; + full_coeffs.resize(lde_size, QuadFelt::ZERO); + let dft = p3_dft::Radix2DFTSmallBatch::::default(); + let mut evals = p3_dft::TwoAdicSubgroupDft::dft_algebra(&dft, full_coeffs); + reverse_slice_index_bits(&mut evals); + + // Compute s_invs + let log_num_cosets = log_lde_size - log_arity; + let num_cosets = 1 << log_num_cosets; + let g_inv = Felt::two_adic_generator(log_lde_size).inverse(); + let mut s_invs: Vec = g_inv.powers().take(num_cosets).collect(); + reverse_slice_index_bits(&mut s_invs); + + // Fold with random beta + let beta: QuadFelt = rng.sample(StandardUniform); + let matrix = RowMajorMatrix::new(evals, arity); + let folded = fold.fold_matrix(matrix.as_view(), &s_invs, beta); + + // IDFT the result to get coefficients + let mut folded_for_idft = folded; + reverse_slice_index_bits(&mut folded_for_idft); + let folded_coeffs = p3_dft::TwoAdicSubgroupDft::idft_algebra(&dft, folded_for_idft); + + // Check that all coefficients beyond degree/arity are zero + let expected_degree = poly_degree / arity; + for (i, coeff) in folded_coeffs.iter().enumerate().skip(expected_degree) { + assert_eq!( + *coeff, + QuadFelt::ZERO, + "Arity {arity}: High coefficient c[{i}] should be zero but was {coeff:?}" + ); + } + } + + #[test] + fn test_fold() { + test_fold_correctness::(FRI_FOLD_ARITY_2); + test_fold_correctness::(FRI_FOLD_ARITY_4); + test_fold_correctness::(FRI_FOLD_ARITY_8); + } + + #[test] + fn test_fold_evals_against_naive_dft() { + test_fold_evals_naive_dft(FRI_FOLD_ARITY_2); + test_fold_evals_naive_dft(FRI_FOLD_ARITY_4); + test_fold_evals_naive_dft(FRI_FOLD_ARITY_8); + } + + #[test] + fn test_fold_matrix() { + test_fold_matrix_scalar_packed_equivalence(FRI_FOLD_ARITY_2); + test_fold_matrix_scalar_packed_equivalence(FRI_FOLD_ARITY_4); + test_fold_matrix_scalar_packed_equivalence(FRI_FOLD_ARITY_8); + } + + #[test] + fn test_fold_low_degree() { + test_folding_preserves_low_degree(FRI_FOLD_ARITY_2); + test_folding_preserves_low_degree(FRI_FOLD_ARITY_4); + test_folding_preserves_low_degree(FRI_FOLD_ARITY_8); + } +} diff --git a/stark/miden-lifted-stark/src/pcs/fri/mod.rs b/stark/miden-lifted-stark/src/pcs/fri/mod.rs new file mode 100644 index 0000000000..b89373ac8f --- /dev/null +++ b/stark/miden-lifted-stark/src/pcs/fri/mod.rs @@ -0,0 +1,98 @@ +//! # FRI Protocol Implementation +//! +//! Fast Reed-Solomon Interactive Oracle Proof for low-degree testing. +//! Proves that a committed polynomial has degree below a target bound. +//! +//! ## Domain Convention +//! +//! This FRI implementation treats inputs as evaluations over the unshifted two-adic subgroup. +//! If the PCS evaluates over a coset `gK`, the shift is absorbed into the polynomial: +//! `Q'(X) = Q(g·X)`. The low-degree test is run on `Q'` using subgroup points. + +pub mod fold; +pub mod proof; +pub mod prover; +pub mod verifier; + +use fold::FriFold; + +/// FRI protocol parameters. +/// +/// Controls the trade-off between proof size, prover time, and verifier time. +/// +/// Higher `log_blowup` increases soundness per query (fewer queries needed) but introduces +/// larger Merkle trees — increasing both proof size (longer authentication paths) and prover time +/// (LDE over a larger domain). Higher arity reduces the number of FRI rounds (fewer Merkle +/// tree commitments) but increases per-query proof size (each opening reveals `arity` +/// siblings). `log_final_degree` reduces the number of rounds and therefore the number of +/// Merkle commitments; if too large, the final polynomial's coefficients dominate the proof +/// size. +#[derive(Clone, Copy, Debug)] +pub struct FriParams { + /// Log₂ of the blowup factor (LDE domain size / polynomial degree). + /// + /// Higher values increase soundness but also proof size and prover time. + /// Typical values: 2-4 (blowup factors of 4-16). + pub(crate) log_blowup: u8, + + /// The FRI folding strategy. + /// + /// Determines the folding arity (2, 4, or 8). + pub(crate) fold: FriFold, + + /// Log₂ of the final polynomial degree. + /// + /// Folding stops when degree reaches `2^log_final_degree`. + /// Final polynomial coefficients are sent in descending degree order + /// `[cₙ, ..., c₁, c₀]` for direct Horner evaluation by the verifier. + pub(crate) log_final_degree: u8, + + /// Grinding bits before each folding challenge. + pub(crate) folding_pow_bits: usize, +} + +impl FriParams { + /// Compute the number of folding rounds for a given initial evaluation domain size. + /// + /// Each round reduces the domain by `2^log_folding_factor`. We fold until the domain + /// size reaches `2^(log_final_degree + log_blowup)`, at which point the polynomial + /// degree is at most `2^log_final_degree`. + /// + /// Uses `div_ceil` to round up, ensuring we always reach the target degree even if + /// the domain size doesn't divide evenly by the folding factor. + #[inline] + pub fn num_rounds(&self, log_domain_size: u8) -> usize { + // Final domain size = final_degree × blowup = 2^(log_final_degree + log_blowup). + // Safety: PcsParams::new() validates this sum does not exceed MAX_LOG_DOMAIN_SIZE. + debug_assert!( + (self.log_final_degree as u16 + self.log_blowup as u16) + <= crate::pcs::params::MAX_LOG_DOMAIN_SIZE as u16, + "log_final_degree + log_blowup overflows; construct FriParams via PcsParams::new()", + ); + let log_max_final_size = self.log_final_degree + self.log_blowup; + // Number of times we need to divide by 2^log_folding_factor + log_domain_size + .saturating_sub(log_max_final_size) + .div_ceil(self.fold.log_arity()) as usize + } + + /// Compute the final polynomial degree after folding. + /// + /// After `num_rounds` folding rounds, the domain shrinks from `2^log_domain_size` + /// to `2^(log_domain_size - num_rounds × log_folding_factor)`. The polynomial + /// degree is then `domain_size / blowup`. + /// + /// Due to `div_ceil` in `num_rounds`, the actual final degree may be smaller than + /// `2^log_final_degree` when the folding doesn't divide evenly. + #[inline] + pub fn final_poly_degree(&self, log_domain_size: u8) -> usize { + let num_rounds = self.num_rounds(log_domain_size); + // log of final domain size after folding + let log_final_size = log_domain_size as usize - num_rounds * self.fold.log_arity() as usize; + // degree = domain_size / blowup = 2^(log_final_size - log_blowup) + 1 << log_final_size.saturating_sub(self.log_blowup as usize) + } +} + +#[cfg(test)] +mod tests; diff --git a/stark/miden-lifted-stark/src/pcs/fri/proof.rs b/stark/miden-lifted-stark/src/pcs/fri/proof.rs new file mode 100644 index 0000000000..64c1849d6b --- /dev/null +++ b/stark/miden-lifted-stark/src/pcs/fri/proof.rs @@ -0,0 +1,65 @@ +//! FRI transcript data structures. + +use alloc::vec::Vec; + +use miden_stark_transcript::{TranscriptError, VerifierChannel}; +use p3_field::{ExtensionField, TwoAdicField}; + +use crate::pcs::fri::FriParams; + +/// Structured transcript view for a single FRI folding round. +pub struct FriRoundTranscript { + /// Commitment to the folded evaluation matrix for this round. + pub commitment: Commitment, + /// Proof-of-work witness sampled before `beta`. + pub pow_witness: F, + /// Folding challenge `β` for this round. + pub beta: EF, +} + +/// Structured transcript view for the full FRI interaction. +pub struct FriTranscript { + /// Per-round commitments and challenges. + pub rounds: Vec>, + /// Coefficients of the final low-degree polynomial in descending degree order + /// `[cₙ, ..., c₁, c₀]`, ready for direct Horner evaluation. + pub final_poly: Vec, +} + +impl FriTranscript +where + F: TwoAdicField, + EF: ExtensionField, + Commitment: Clone, +{ + /// Parse a FRI transcript from a verifier channel. + /// + /// Reads commitments, verifies PoW witnesses, samples challenges, and + /// reads the final polynomial. Does not verify low-degree claims; + /// that validation happens in `FriOracle::test_low_degree`. + pub fn from_verifier_channel( + params: &FriParams, + log_domain_size: u8, + channel: &mut Ch, + ) -> Result + where + Ch: VerifierChannel, + { + let num_rounds = params.num_rounds(log_domain_size); + let mut rounds = Vec::with_capacity(num_rounds); + + for _ in 0..num_rounds { + let commitment = channel.receive_commitment()?.clone(); + + let pow_witness = channel.grind(params.folding_pow_bits)?; + + let beta: EF = channel.sample_algebra_element(); + rounds.push(FriRoundTranscript { commitment, pow_witness, beta }); + } + + let final_degree = params.final_poly_degree(log_domain_size); + let final_poly = channel.receive_algebra_slice(final_degree)?; + + Ok(Self { rounds, final_poly }) + } +} diff --git a/stark/miden-lifted-stark/src/pcs/fri/prover.rs b/stark/miden-lifted-stark/src/pcs/fri/prover.rs new file mode 100644 index 0000000000..76c5bc3b31 --- /dev/null +++ b/stark/miden-lifted-stark/src/pcs/fri/prover.rs @@ -0,0 +1,254 @@ +use alloc::vec::Vec; + +use miden_stark_transcript::ProverChannel; +use p3_dft::{Radix2DFTSmallBatch, TwoAdicSubgroupDft}; +use p3_field::{ExtensionField, TwoAdicField}; +use p3_matrix::{bitrev::BitReversalPerm, dense::RowMajorMatrix, extension::FlatMatrixView}; +use p3_maybe_rayon::prelude::*; +use p3_util::reverse_slice_index_bits; +use tracing::{debug_span, info_span}; + +use crate::{ + lmcs::{Lmcs, LmcsTree, tree_indices::TreeIndices, utils::log2_strict_u8}, + pcs::fri::FriParams, +}; + +/// Tree type for FRI folding rounds. +/// +/// Stores extension field evaluations flattened to base field via `FlatMatrixView`. +/// The LMCS internally bit-reverses leaf digests so the tree is indexed by domain order. +type FoldedTree = ::Tree>>; + +// ============================================================================ +// Prover Data Structure +// ============================================================================ + +/// Prover's state from the FRI commit phase. +/// +/// Contains the data needed to answer queries (LMCS trees). +/// Commitments, PoW witnesses, and the final polynomial (in descending degree +/// order) are written to the transcript channel during construction. +/// +/// Uses a single base-field LMCS. Extension field evaluations are flattened +/// to base field before commitment. +pub struct FriPolys +where + F: TwoAdicField, + EF: ExtensionField, + L: Lmcs, +{ + /// Trees for each folding round, used to open multiple query indices at once. + /// Stores flattened base-field matrices (EF elements flattened to F via FlatMatrixView). + folded_trees: Vec>, +} + +// ============================================================================ +// Commit Phase (Prover) +// ============================================================================ +// +// The FRI commit phase iteratively folds a polynomial until it reaches a +// target degree, committing to intermediate evaluations along the way. +// +// ## Algorithm +// +// Given polynomial f of degree d with evaluations on domain D of size n = d·blowup: +// +// 1. Reshape evaluations into matrix M with `arity` columns +// - Row i contains the coset {f(s·ωʲ) : j ∈ [0, arity)} where s = g^{bitrev(i)} and ω is a +// primitive `arity`-th root of unity +// +// 2. Commit to M via Merkle tree +// +// 3. Sample folding challenge β from verifier +// +// 4. Fold each row: for coset evaluations [y₀, ..., y_{arity-1}], compute f(β) +// - This reduces degree by factor of `arity` +// - New evaluations live on domain D' of size n/arity +// +// 5. Repeat until degree ≤ final_degree +// +// 6. Send final polynomial coefficients to verifier (descending degree order) +// +// ## Coset Structure in Bit-Reversed Order +// +// For domain D = g·H where H = ⟨ω⟩ has order n: +// - Row i contains evaluations at s·⟨ω_arity⟩ where s = g·ω^{bitrev(i)} and ω_arity is a +// primitive `arity`-th root of unity (ω_arity = ω^{n/arity}) +// - Adjacent rows have s values that are negatives (for arity=2) +// - After folding, row i maps to row i in the halved domain + +impl FriPolys +where + F: TwoAdicField, + EF: ExtensionField, + L: Lmcs, +{ + /// Execute the FRI commit phase. + /// + /// Iteratively folds the polynomial by arity at each round — committing intermediate + /// evaluations and sampling a random challenge `beta` — until the degree is small enough to + /// send the polynomial directly. The query phase then spot-checks that each fold was + /// performed correctly. + pub fn new(params: &FriParams, lmcs: &L, evals: Vec, channel: &mut Ch) -> Self + where + Ch: ProverChannel, + { + let log_arity = params.fold.log_arity(); + let arity = params.fold.arity(); + + let mut folded_trees = Vec::new(); + + let mut domain_size = evals.len(); + let log_domain_size = log2_strict_u8(domain_size); + let final_poly_degree = params.final_poly_degree(log_domain_size); + let final_domain_size = final_poly_degree << params.log_blowup; + + // ───────────────────────────────────────────────────────────────────────── + // Precompute s_inv for all cosets + // ───────────────────────────────────────────────────────────────────────── + // The fold performs an iFFT over the coset s * . This is equivalent to an + // iFFT on the subgroup after the variable substitution X -> s_inv * X, + // which is why s_inv appears as a twiddle factor in the fold formula. + // + // Evaluations are in bit-reversed order: evals[i] = f(g^{bitrev(i)}) + // Row k contains [evals[k*arity], evals[k*arity+1], ...] which correspond + // to evaluations at points forming a coset s·⟨ω⟩ where: + // - s = g^{bitrev(k*arity, log_domain_size)} = g^{bitrev(k, log_folded_domain_size)} + // where log_folded_domain_size = log_domain_size - log_arity (because bitrev(k*arity, + // log_domain_size) = bitrev(k, log_folded_domain_size) when arity = 2^log_arity) + // - ω is a primitive arity-th root of unity + // + // We compute s_inv for each row k, where s = g^{bitrev(k, log_folded_domain_size)} + // and g has order 2^log_domain_size. + // + // We generate sequential powers of g_inv and bit-reverse to get s_inv values + // in the correct order for each row. + let g_inv = F::two_adic_generator(log_domain_size as usize).inverse(); + let mut s_invs: Vec = g_inv.powers().take(domain_size >> log_arity as usize).collect(); + reverse_slice_index_bits(&mut s_invs); + + let mut folded_evals = evals; + while domain_size > final_domain_size { + let round = folded_trees.len(); + // ───────────────────────────────────────────────────────────────────── + // Reshape into matrix and wrap with FlatMatrixView for commitment + // ───────────────────────────────────────────────────────────────────── + // domain_size evaluations → matrix with folded_domain_size rows × arity columns. + // FlatMatrixView presents the EF matrix as F matrix without copying. + let folded_domain_size = domain_size >> log_arity as usize; + let matrix = RowMajorMatrix::new(folded_evals, arity); + let flat_view = FlatMatrixView::new(matrix); + // FRI round commitments use `build_tree` (unaligned) rather than + // `build_aligned_tree` because each round commits a single matrix, so there + // is no multi-matrix row interleaving that would require padding to the hash + // rate boundary. + // Wrap in BitReversedMatrixView to present domain order to the LMCS. + // The LMCS extracts the inner FlatMatrixView and stores it. + let natural_view = BitReversalPerm::new_view(flat_view); + let tree = info_span!("FRI round commit", round, domain_size) + .in_scope(|| lmcs.build_tree(alloc::vec![natural_view])); + let commitment = tree.root(); + channel.send_commitment(commitment.clone()); + + // ───────────────────────────────────────────────────────────────────── + // Grind and sample folding challenge beta + // ───────────────────────────────────────────────────────────────────── + let _pow_witness = + info_span!("FRI folding grind", round, bits = params.folding_pow_bits) + .in_scope(|| channel.grind(params.folding_pow_bits)); + let beta: EF = channel.sample_algebra_element(); + + // ───────────────────────────────────────────────────────────────────── + // Fold all rows: interpolate coset evaluations and evaluate at β + // ───────────────────────────────────────────────────────────────────── + // Get the underlying EF matrix from the FlatMatrixView via Deref for folding. + let flat_view_ref = &tree.leaves()[0]; + let ef_matrix: &RowMajorMatrix = flat_view_ref; + folded_evals = info_span!("FRI fold", round, domain_size) + .in_scope(|| params.fold.fold_matrix(ef_matrix.as_view(), &s_invs, beta)); + // No bit-reversal needed: folded evals maintain bit-reversed order + // because s_invs are already bit-reversed to match. + + folded_trees.push(tree); + + // Output of folding becomes the input domain for the next round. + domain_size = folded_domain_size; + + // ───────────────────────────────────────────────────────────────────── + // Update s⁻¹ for next round + // ───────────────────────────────────────────────────────────────────── + // After folding, domain shrinks by `arity`. The new generator g' = g^arity. + // Let L = log(folded_domain_size) and L' = L - log_arity (next folded size). + // We need: s'_inv[k] = g'^{-bitrev(k, L')} = g^{-arity · bitrev(k, L')} + // + // Using the identity bitrev(k, L-log_arity) = bitrev(k·arity, L): + // s'_inv[k] = g^{-arity · bitrev(k·arity, L)} + // = (g^{-bitrev(k·arity, L)})^arity + // = s_inv[k·arity]^arity + // + // So we select every `arity`-th element and raise to power `arity`. + let next_folded_size = domain_size >> log_arity as usize; + s_invs = (0..next_folded_size) + .into_par_iter() + .map(|k| s_invs[k * arity].exp_power_of_2(log_arity as usize)) + .collect(); + } + + // ───────────────────────────────────────────────────────────────────────── + // Extract final polynomial coefficients + // ───────────────────────────────────────────────────────────────────────── + // The remaining evaluations are on a domain of size `final_domain_size`. + // The polynomial has degree < `final_poly_degree` where + // `final_poly_degree = final_domain_size / blowup`. We extract its coefficients: + // 1. Take the first `final_poly_degree` evaluations (others are redundant due to blowup) + // 2. Convert from bit-reversed to standard order + // 3. Apply inverse DFT to get coefficients + // 4. Reverse to descending degree order for direct Horner evaluation + // + // The polynomial has degree < final_poly_degree, so it is determined by that many + // evaluations. In bit-reversed order, the first final_poly_degree entries form a + // valid sub-coset (the "squaring prefix" property), so iDFT on them recovers the + // polynomial exactly. + folded_evals.truncate(final_poly_degree); + reverse_slice_index_bits(&mut folded_evals); + + let mut final_poly = debug_span!("idft final poly") + .in_scope(|| Radix2DFTSmallBatch::default().idft_algebra(folded_evals)); + + // Store in descending degree order [cₙ, ..., c₁, c₀] so the verifier + // can evaluate via Horner without reversing. + final_poly.reverse(); + + // Observe final polynomial coefficients for Fiat-Shamir + channel.send_algebra_slice(&final_poly); + + Self { folded_trees } + } + + /// Stream all FRI query proofs into a transcript channel. + /// + /// `tree_indices` are bit-reversed tree positions (sorted, deduplicated). + /// + /// Indices shift by `log_arity` per round because each FRI folding round groups `arity` + /// consecutive bit-reversed indices into one coset, reducing the domain size by `arity`. + /// The committed matrix at round r has height `domain_size / arity^r`, so the tree index + /// for a query at round r is the original index right-shifted by `log_arity * r` bits — + /// the high bits select the coset (row), and the discarded low bits identify which + /// position within the coset the original query fell in. + pub fn prove_queries( + &self, + params: &FriParams, + mut tree_indices: TreeIndices, + channel: &mut Ch, + ) where + Ch: ProverChannel, + { + let log_arity = params.fold.log_arity(); + + // Shrink indices by log_arity per round, reusing the allocation. + for tree in &self.folded_trees { + tree_indices.shrink_depth(log_arity); + tree.prove_batch(&tree_indices, channel); + } + } +} diff --git a/stark/miden-lifted-stark/src/pcs/fri/tests.rs b/stark/miden-lifted-stark/src/pcs/fri/tests.rs new file mode 100644 index 0000000000..a6ea6a173c --- /dev/null +++ b/stark/miden-lifted-stark/src/pcs/fri/tests.rs @@ -0,0 +1,405 @@ +//! Integration tests for FRI protocol commit/verify cycles. + +use alloc::{collections::BTreeMap, vec, vec::Vec}; + +use miden_stark_transcript::VerifierTranscript; +use p3_challenger::CanObserve; +use p3_dft::{Radix2DFTSmallBatch, TwoAdicSubgroupDft}; +use p3_field::PrimeCharacteristicRing; +use p3_matrix::{Matrix, bitrev::BitReversibleMatrix, dense::RowMajorMatrix}; +use proof::FriTranscript; +use prover::FriPolys; +use rand::{RngExt, SeedableRng, distr::StandardUniform, prelude::SmallRng}; +use verifier::{FriError, FriOracle}; + +use super::*; +use crate::{ + lmcs::{tree_indices::TreeIndices, utils::log2_strict_u8}, + testing::{ + configs::goldilocks_poseidon2::{ + Challenger, Felt, Lmcs as BaseLmcs, QuadFelt, TestDigest, TestTranscriptData, + prover_channel, random_lde_matrix, test_challenger, test_lmcs, verifier_channel, + }, + params::{FRI_FOLD_ARITY_2, FRI_FOLD_ARITY_4, FRI_FOLD_ARITY_8}, + }, +}; + +/// Sample `count` random indices in `[0, upper)`. +fn sample_indices(rng: &mut R, upper: usize, count: usize) -> Vec { + (0..count).map(|_| rng.random_range(0..upper)).collect() +} + +// ============================================================================ +// Integration tests +// ============================================================================ + +struct FriRoundtripCase { + name: &'static str, + log_poly_degree: u8, + log_blowup: u8, + log_final_degree: u8, + fold: FriFold, + folding_pow_bits: usize, + num_queries: usize, +} + +const FRI_ROUNDTRIP_CASES: &[FriRoundtripCase] = &[ + FriRoundtripCase { + name: "arity-2", + log_poly_degree: 10, + log_blowup: 2, + log_final_degree: 2, + fold: FRI_FOLD_ARITY_2, + folding_pow_bits: 1, + num_queries: 3, + }, + FriRoundtripCase { + name: "arity-4", + log_poly_degree: 10, + log_blowup: 2, + log_final_degree: 2, + fold: FRI_FOLD_ARITY_4, + folding_pow_bits: 1, + num_queries: 3, + }, + FriRoundtripCase { + name: "arity-8", + log_poly_degree: 10, + log_blowup: 2, + log_final_degree: 2, + fold: FRI_FOLD_ARITY_8, + folding_pow_bits: 1, + num_queries: 3, + }, + FriRoundtripCase { + name: "blowup-0", + log_poly_degree: 8, + log_blowup: 0, + log_final_degree: 3, + fold: FRI_FOLD_ARITY_2, + folding_pow_bits: 0, + num_queries: 2, + }, +]; + +/// Build initial_evals map from domain indices and bit-reversed evaluation array. +/// +/// `evals` is in bit-reversed order: `evals[bitrev(d)]` = f(g·ω^d). +/// `tree_indices` are domain indices. +/// Returns a map keyed by domain index. +fn build_initial_evals( + evals: &[QuadFelt], + tree_indices: &TreeIndices, +) -> BTreeMap { + let log_n = log2_strict_u8(evals.len()) as usize; + tree_indices + .iter() + .map(|&domain_idx| { + let bitrev_idx = p3_util::reverse_bits_len(domain_idx, log_n); + (domain_idx, evals[bitrev_idx]) + }) + .collect() +} + +fn prove_queries( + params: &FriParams, + lmcs: &BaseLmcs, + evals: Vec, + tree_indices: TreeIndices, +) -> (TestDigest, TestTranscriptData) { + let mut prover_channel = prover_channel(); + let fri_polys = FriPolys::::new(params, lmcs, evals, &mut prover_channel); + fri_polys.prove_queries(params, tree_indices, &mut prover_channel); + prover_channel.finalize() +} + +fn verify_queries( + params: &FriParams, + lmcs: &BaseLmcs, + transcript: &TestTranscriptData, + lde_size: usize, + initial_evals: &BTreeMap, + tree_indices: TreeIndices, + challenger: Option, +) -> Result { + let mut channel = match challenger { + Some(challenger) => VerifierTranscript::from_data(challenger, transcript), + None => verifier_channel(transcript), + }; + let log_domain_size = log2_strict_u8(lde_size); + let oracle = FriOracle::new(params, log_domain_size, &mut channel)?; + oracle.test_low_degree(lmcs, params, initial_evals.clone(), tree_indices, &mut channel)?; + let digest = channel.finalize().expect("transcript should finalize cleanly"); + Ok(digest) +} + +fn run_roundtrip_case(case: &FriRoundtripCase, seed: u64) -> Result<(), FriError> { + let mut rng = SmallRng::seed_from_u64(seed); + let lmcs = test_lmcs(); + + let params = FriParams { + log_blowup: case.log_blowup, + fold: case.fold, + log_final_degree: case.log_final_degree, + folding_pow_bits: case.folding_pow_bits, + }; + + let evals = random_lde_matrix::( + &mut rng, + case.log_poly_degree, + case.log_blowup, + 1, + Felt::ONE, + ) + .values; + let lde_size = evals.len(); + let log_domain_size = log2_strict_u8(lde_size); + // Sample domain indices (no bit-reversal needed — tree is in domain order) + let tree_indices = + TreeIndices::new(sample_indices(&mut rng, lde_size, case.num_queries), log_domain_size) + .expect("indices are in range"); + let initial_evals = build_initial_evals(&evals, &tree_indices); + + let (prover_digest, transcript) = prove_queries(¶ms, &lmcs, evals, tree_indices.clone()); + let verifier_digest = + verify_queries(¶ms, &lmcs, &transcript, lde_size, &initial_evals, tree_indices, None)?; + assert_eq!(prover_digest, verifier_digest); + + // Re-parse FriTranscript (commit phase only) from a fresh channel. + let mut reparse_channel = verifier_channel(&transcript); + FriTranscript::::from_verifier_channel( + ¶ms, + log_domain_size, + &mut reparse_channel, + ) + .expect("FriTranscript re-parse should succeed"); + + Ok(()) +} + +/// Table-driven roundtrip cases that must verify successfully. +#[test] +fn test_fri_roundtrip_cases() { + for (case_idx, case) in FRI_ROUNDTRIP_CASES.iter().enumerate() { + let seed = 42 + case_idx as u64; + let result = run_roundtrip_case(case, seed); + assert!(result.is_ok(), "case {} failed with {:?}", case.name, result); + } +} + +/// Test that verification fails with wrong initial evaluation. +#[test] +fn test_fri_verify_wrong_eval() { + let mut rng = SmallRng::seed_from_u64(42); + let lmcs = test_lmcs(); + + let log_poly_degree: u8 = 8; + let log_blowup: u8 = 2; + let log_final_degree: u8 = 2; + + let params = FriParams { + log_blowup, + fold: FRI_FOLD_ARITY_2, + log_final_degree, + folding_pow_bits: 1, + }; + + let evals = + random_lde_matrix::(&mut rng, log_poly_degree, log_blowup, 1, Felt::ONE).values; + let lde_size = evals.len(); + let log_domain_size = log2_strict_u8(lde_size); + let tree_indices = TreeIndices::new(sample_indices(&mut rng, lde_size, 2), log_domain_size) + .expect("indices are in range"); + let mut initial_evals = build_initial_evals(&evals, &tree_indices); + + // Tamper with the first evaluation + let first_idx = *tree_indices.iter().next().unwrap(); + let correct_eval = initial_evals[&first_idx]; + let mut wrong_eval: QuadFelt = rng.sample(StandardUniform); + while wrong_eval == correct_eval { + wrong_eval = rng.sample(StandardUniform); + } + initial_evals.insert(first_idx, wrong_eval); + + let (_prover_digest, transcript) = prove_queries(¶ms, &lmcs, evals, tree_indices.clone()); + let result = + verify_queries(¶ms, &lmcs, &transcript, lde_size, &initial_evals, tree_indices, None); + + assert!( + matches!(result, Err(FriError::EvaluationMismatch { .. })), + "expected EvaluationMismatch error, got {result:?}" + ); +} + +/// Test that verification fails with mismatched proof data (wrong betas scenario). +/// +/// When the verifier's challenger state differs from the prover's (e.g., due to +/// different commitments being observed), the derived betas will be wrong, +/// causing verification to fail. +#[test] +fn test_fri_verify_wrong_beta() { + let mut rng = SmallRng::seed_from_u64(42); + let lmcs = test_lmcs(); + + let log_poly_degree: u8 = 8; + let log_blowup: u8 = 2; + let log_final_degree: u8 = 2; + + let params = FriParams { + log_blowup, + fold: FRI_FOLD_ARITY_2, + log_final_degree, + folding_pow_bits: 0, // No grinding to simplify test + }; + + // Create two independent provers with different evaluations. + let evals1 = + random_lde_matrix::(&mut rng, log_poly_degree, log_blowup, 1, Felt::ONE).values; + let evals2 = + random_lde_matrix::(&mut rng, log_poly_degree, log_blowup, 1, Felt::ONE).values; + let lde_size = evals1.len(); + let log_domain_size = log2_strict_u8(lde_size); + + // Prover 1: generate FRI transcript (grinds per-round internally). + let tree_indices = TreeIndices::new(sample_indices(&mut rng, lde_size, 2), log_domain_size) + .expect("indices are in range"); + let initial_evals = build_initial_evals(&evals1, &tree_indices); + let (_prover_digest, transcript) = prove_queries(¶ms, &lmcs, evals1, tree_indices.clone()); + + // Prover 2: generate different transcript (different commitments = different betas). + let mut prover2_channel = prover_channel(); + let _ = FriPolys::::new(¶ms, &lmcs, evals2, &mut prover2_channel); + let (_, prover2_transcript) = prover2_channel.finalize(); + let other_commitment = prover2_transcript + .commitments() + .first() + .cloned() + .expect("prover2 should produce commitments"); + + // Verifier: use prover1's transcript but alter challenger state beforehand. + let mut wrong_challenger = test_challenger(); + wrong_challenger.observe(other_commitment); + let result = verify_queries( + ¶ms, + &lmcs, + &transcript, + lde_size, + &initial_evals, + tree_indices, + Some(wrong_challenger), + ); + + // Should fail because wrong betas produce wrong folding results + assert!( + matches!( + result, + Err(FriError::EvaluationMismatch { .. } | FriError::FinalPolyMismatch { .. }) + ), + "expected EvaluationMismatch or FinalPolyMismatch error, got {result:?}" + ); +} + +/// Zero-round FRI: when the evaluation domain is at or below the final polynomial degree. +#[test] +fn test_fri_zero_rounds_final_poly_only() { + let mut rng = SmallRng::seed_from_u64(123); + let lmcs = test_lmcs(); + + let log_poly_degree: u8 = 4; + let log_blowup: u8 = 0; + let log_final_degree: u8 = log_poly_degree; // final degree >= domain size => zero rounds + + let params = FriParams { + log_blowup, + fold: FRI_FOLD_ARITY_2, + log_final_degree, + folding_pow_bits: 0, + }; + + let evals = + random_lde_matrix::(&mut rng, log_poly_degree, log_blowup, 1, Felt::ONE).values; + let lde_size = evals.len(); + let log_domain_size = log2_strict_u8(lde_size); + let tree_indices = TreeIndices::new(sample_indices(&mut rng, lde_size, 2), log_domain_size) + .expect("indices are in range"); + let initial_evals = build_initial_evals(&evals, &tree_indices); + let (prover_digest, transcript) = prove_queries(¶ms, &lmcs, evals, tree_indices.clone()); + + let mut channel = verifier_channel(&transcript); + let fri_transcript: FriTranscript = + FriTranscript::from_verifier_channel(¶ms, log_domain_size, &mut channel) + .expect("transcript parsing should succeed"); + + assert!(fri_transcript.rounds.is_empty(), "expected zero folding rounds"); + assert_eq!( + fri_transcript.final_poly.len(), + lde_size, + "final polynomial should match domain size" + ); + + let verifier_digest = + verify_queries(¶ms, &lmcs, &transcript, lde_size, &initial_evals, tree_indices, None) + .expect("zero-round FRI should verify"); + assert_eq!(prover_digest, verifier_digest); +} + +/// Test that the final polynomial is correctly computed by evaluating it +/// at points in the final domain and comparing with folded values. +#[test] +fn test_final_polynomial_correctness() { + let mut rng = SmallRng::seed_from_u64(123); + let lmcs = test_lmcs(); + + let log_poly_degree: u8 = 6; + let log_blowup: u8 = 2; + let log_final_degree: u8 = 3; + + let params = FriParams { + log_blowup, + fold: FRI_FOLD_ARITY_2, + log_final_degree, + folding_pow_bits: 0, // No grinding for this test + }; + + let poly_degree = 1usize << log_poly_degree; + let final_degree = 1usize << log_final_degree; + let rounds = log_poly_degree - log_final_degree; + let stride = 1usize << rounds; + + let g_coeffs: Vec = (0..final_degree).map(|_| rng.sample(StandardUniform)).collect(); + let mut f_coeffs = vec![QuadFelt::ZERO; poly_degree]; + for (i, coeff) in g_coeffs.iter().enumerate() { + f_coeffs[i * stride] = *coeff; + } + + let coeffs_matrix = RowMajorMatrix::new(f_coeffs, 1); + let dft = Radix2DFTSmallBatch::::default(); + // DFT output is already in standard order for Radix2DFTSmallBatch. + let evals_h = dft.coset_dft_algebra_batch(coeffs_matrix, Felt::ONE); + let lde = dft.coset_lde_algebra_batch(evals_h, log_blowup as usize, Felt::ONE); + let evals = lde.bit_reverse_rows().to_row_major_matrix().values; + + let log_domain_size = log_poly_degree + log_blowup; + + let mut prover_channel = prover_channel(); + let _fri_polys = FriPolys::::new(¶ms, &lmcs, evals, &mut prover_channel); + let (_, transcript) = prover_channel.finalize(); + + let mut v_channel = verifier_channel(&transcript); + let fri_transcript: FriTranscript = + FriTranscript::from_verifier_channel(¶ms, log_domain_size, &mut v_channel) + .expect("transcript parsing should succeed"); + + assert_eq!( + fri_transcript.final_poly.len(), + g_coeffs.len(), + "Final polynomial should have {} coefficients", + g_coeffs.len() + ); + let mut g_coeffs_rev = g_coeffs; + g_coeffs_rev.reverse(); + assert_eq!( + fri_transcript.final_poly, g_coeffs_rev, + "Final polynomial coefficients should be in descending degree order" + ); +} diff --git a/stark/miden-lifted-stark/src/pcs/fri/verifier.rs b/stark/miden-lifted-stark/src/pcs/fri/verifier.rs new file mode 100644 index 0000000000..fadf0346b3 --- /dev/null +++ b/stark/miden-lifted-stark/src/pcs/fri/verifier.rs @@ -0,0 +1,235 @@ +//! FRI Verifier +//! +//! Verifies that a committed polynomial is close to low-degree. +//! +//! # Domain Structure +//! +//! The prover commits to evaluations on domain D of size 2^log_domain_size. The LMCS tree +//! is indexed by domain order (natural index). Internally, evaluations are in bit-reversed +//! order within the committed matrix (wrapped in `BitReversedMatrixView`). +//! +//! # Index Semantics +//! +//! The query `index` is a domain index. For each folding round: +//! - Low bits (`index & (folded_size - 1)`): which row (coset) in the committed matrix +//! - High bits (`index >> (log_domain_size - log_arity)`): position within the coset +//! +//! After each fold, we mask to the new folded domain size. + +use alloc::{collections::BTreeMap, vec::Vec}; + +use miden_stark_transcript::{TranscriptError, VerifierChannel}; +use p3_field::{ExtensionField, TwoAdicField}; +use p3_util::reverse_bits_len; +use thiserror::Error; + +use crate::{ + lmcs::{Lmcs, LmcsError, tree_indices::TreeIndices}, + pcs::{fri::FriParams, utils::horner}, +}; + +/// FRI low-degree test oracle. +/// +/// Created via [`FriOracle::new`], which samples folding challenges from +/// the Fiat-Shamir transcript. The oracle verifies that evaluations are close +/// to a low-degree polynomial by checking that each folding round was performed +/// correctly via spot-check queries, and that the final (small) polynomial +/// matches the prover's claim exactly. +/// +/// Uses a single base-field LMCS. Opened base field values are reconstructed +/// to extension field for folding verification. +pub struct FriOracle +where + F: TwoAdicField, + EF: ExtensionField, + L: Lmcs, +{ + /// Log₂ of the initial domain size. + log_domain_size: u8, + /// Per-round commitment and folding challenge. + rounds: Vec>, + /// Coefficients of the final low-degree polynomial in descending degree order + /// `[cₙ, ..., c₁, c₀]`, ready for direct Horner evaluation. + final_poly: Vec, +} + +struct FriRoundOracle { + commitment: Commitment, + beta: EF, +} + +impl FriOracle +where + F: TwoAdicField, + EF: ExtensionField + Clone, + L: Lmcs, +{ + /// Create oracle by reading from a verifier channel. + pub fn new( + params: &FriParams, + log_domain_size: u8, + channel: &mut Ch, + ) -> Result + where + Ch: VerifierChannel, + { + let num_rounds = params.num_rounds(log_domain_size); + let mut rounds = Vec::with_capacity(num_rounds); + + for _ in 0..num_rounds { + let commitment = channel.receive_commitment()?.clone(); + + channel.grind(params.folding_pow_bits)?; + + let beta: EF = channel.sample_algebra_element(); + rounds.push(FriRoundOracle { commitment, beta }); + } + + let final_degree = params.final_poly_degree(log_domain_size); + let final_poly = channel.receive_algebra_slice(final_degree)?; + + Ok(Self { log_domain_size, rounds, final_poly }) + } + + /// Test low-degree proximity by reading openings from a verifier channel. + /// + /// `evals` maps domain indices to DEEP evaluations. + /// Domain point for index `d` = `g·ω^d`. + /// + /// Empty `evals` will fail at the first round's LMCS `open_batch` call, + /// which rejects empty indices. + /// + /// For each query, the verifier opens the committed row and re-computes the fold + /// locally. A mismatch at any round indicates that the prover did not fold honestly. + /// After all rounds, the final polynomial is checked exactly against the prover's claim. + pub fn test_low_degree( + &self, + lmcs: &L, + params: &FriParams, + mut evals: BTreeMap, + mut tree_indices: TreeIndices, + channel: &mut Ch, + ) -> Result<(), FriError> + where + Ch: VerifierChannel, + { + let log_arity = params.fold.log_arity(); + let arity = params.fold.arity(); + // FRI commits base-field values; each extension element spans DIMENSION base elements. + let base_width = arity * EF::DIMENSION; + let widths = [base_width]; + + let mut log_domain_size = self.log_domain_size; + let mut g_inv = F::two_adic_generator(log_domain_size as usize).inverse(); + + for (round_idx, round) in self.rounds.iter().enumerate() { + let log_folded_domain_size = log_domain_size - log_arity; + + // Shrink indices by log_arity to get this round's row indices. + tree_indices.shrink_depth(log_arity); + + let opened_rows = lmcs + .open_batch(&round.commitment, &widths, &tree_indices, channel) + .map_err(|source| FriError::LmcsError { source, round: round_idx })?; + + // Drain, verify, fold, and rebuild with new keys. + // + // SOUNDNESS NOTE: Multiple indices can map to the same row_idx after folding + // (they share the same coset). This is safe because: + // + // 1. Each closure verifies its specific position: `row[position] == eval`. All closures + // execute (Rust's collect drives the full iterator). + // + // 2. The folded value depends only on (row, s_inv, beta), not on position. Indices in + // the same coset share the same row and s_inv, so they fold to identical values. + // Keeping any one in the BTreeMap is correct. + // + // 3. The prover cannot provide different row data for the same row_idx. LMCS opens each + // row exactly once via `opened_rows[&row_idx]`. + let folded_size = 1usize << log_folded_domain_size; + evals = evals + .into_iter() + .map(|(idx, eval)| { + // Decompose domain index: low bits = row (coset), high bits = position. + // The position bits must be bit-reversed within `log_arity` bits + // because the physical matrix rows store coset evaluations in + // bit-reversed order within each row. + let row_idx = idx & (folded_size - 1); + let position = + reverse_bits_len(idx >> log_folded_domain_size, log_arity as usize); + + // FRI commits one matrix per round; iter_rows().next() yields it safely. + let flat_row = + opened_rows.get(&row_idx).and_then(|rows| rows.iter_rows().next()).ok_or( + FriError::InvalidOpening { tree_index: row_idx, round: round_idx }, + )?; + // Reinterpret base-field elements as extension field for folding. + let row: Vec = EF::reconstitute_from_base(flat_row.to_vec()); + + if row.get(position) != Some(&eval) { + return Err(FriError::EvaluationMismatch { + round: round_idx, + tree_index: row_idx, + position, + }); + } + + // s⁻¹ = ω_N^{-row_idx}, needed for iFFT over . + // In domain order, row_idx is the domain index directly. + let s_inv = g_inv.exp_u64(row_idx as u64); + let folded = params.fold.fold_evals(&row, s_inv, round.beta); + Ok((row_idx, folded)) + }) + .collect::>()?; + + log_domain_size = log_folded_domain_size; + g_inv = g_inv.exp_power_of_2(log_arity as usize); + } + + // After all folding rounds, the polynomial has been reduced to degree < final_degree. + // The prover sent this final polynomial's coefficients; we evaluate it at each + // folded query point on the final domain and check consistency with the folded + // values. This closes the FRI proximity argument: if the original codeword was + // far from low-degree, at least one query fails with high probability. + // + // `final_poly` is in descending degree order [cₙ, ..., c₁, c₀], which is + // the native order for Horner evaluation. + let generator = F::two_adic_generator(log_domain_size as usize); + for (idx, eval) in evals { + // Domain index directly gives the exponent (no bit-reversal needed). + let x = generator.exp_u64(idx as u64); + let final_eval: EF = horner(x, self.final_poly.iter().copied()); + + if final_eval != eval { + return Err(FriError::FinalPolyMismatch { tree_index: idx }); + } + } + + Ok(()) + } +} + +// ============================================================================ +// Error Types +// ============================================================================ + +/// Errors that can occur during FRI verification. +#[derive(Debug, Error)] +pub enum FriError { + #[error("LMCS verification failed at round {round}: {source}")] + LmcsError { source: LmcsError, round: usize }, + #[error("invalid opening for tree index {tree_index} at round {round}")] + InvalidOpening { tree_index: usize, round: usize }, + #[error( + "evaluation mismatch at round {round}, tree index {tree_index}, coset position {position}" + )] + EvaluationMismatch { + round: usize, + tree_index: usize, + position: usize, + }, + #[error("final polynomial mismatch at tree index {tree_index}")] + FinalPolyMismatch { tree_index: usize }, + #[error("transcript error: {0}")] + TranscriptError(#[from] TranscriptError), +} diff --git a/stark/miden-lifted-stark/src/pcs/mod.rs b/stark/miden-lifted-stark/src/pcs/mod.rs new file mode 100644 index 0000000000..006b6187e5 --- /dev/null +++ b/stark/miden-lifted-stark/src/pcs/mod.rs @@ -0,0 +1,42 @@ +//! # Lifted PCS +//! +//! A polynomial commitment scheme (PCS) combining DEEP quotient construction with FRI +//! for efficient low-degree testing over two-adic fields. +//! +//! ## Overview +//! +//! This module provides: +//! +//! - **[`deep`]**: DEEP (Domain Extension for Eliminating Pretenders) quotient construction for +//! batching polynomial evaluation claims into a single low-degree polynomial. +//! +//! - **[`fri`]**: FRI (Fast Reed-Solomon IOP) protocol for low-degree testing, with configurable +//! folding arities and final polynomial degree. +//! +//! - **PCS API (module root)**: complete PCS implementation combining DEEP quotient and FRI via +//! `prover::open_with_channel` and `verifier::verify`, plus `PcsParams`. +//! +//! ## Alignment Padding +//! +//! Alignment padding is a transcript formatting convention. For trace commitments, the +//! padded columns are treated as extra polynomials and are checked for low degree by the PCS; +//! they need not be zero unless the caller enforces that. The PCS is deliberately agnostic +//! about which columns are "real" vs "padding" — enforcing zero-valued padding is the +//! caller's (or AIR's) responsibility. (FRI openings still ignore the padded tail because +//! FRI expects a fixed single-column width.) + +/// DEEP quotient construction for batched polynomial evaluation. +pub mod deep; + +/// FRI protocol for low-degree testing. +pub mod fri; + +pub mod params; +pub mod proof; +pub mod prover; +pub mod verifier; + +pub mod utils; + +#[cfg(test)] +mod tests; diff --git a/stark/miden-lifted-stark/src/pcs/params.rs b/stark/miden-lifted-stark/src/pcs/params.rs new file mode 100644 index 0000000000..361f8f9a85 --- /dev/null +++ b/stark/miden-lifted-stark/src/pcs/params.rs @@ -0,0 +1,125 @@ +//! PCS parameters. + +use thiserror::Error; + +use crate::pcs::{ + deep::DeepParams, + fri::{FriParams, fold::FriFold}, +}; + +/// Maximum log₂ of any domain size. Domains cannot exceed 2⁶⁴ elements. +pub const MAX_LOG_DOMAIN_SIZE: u8 = 64; + +/// Errors from invalid PCS parameter combinations. +#[derive(Clone, Debug, Error)] +pub enum PcsParamsError { + #[error("invalid folding arity: log_arity {0} (must be 1, 2, or 3)")] + InvalidFoldingArity(u8), + #[error("log_blowup must be > 0")] + ZeroBlowup, + #[error("log_final_degree ({log_final_degree}) + log_blowup ({log_blowup}) exceeds 64")] + FinalDomainTooLarge { log_final_degree: u8, log_blowup: u8 }, + #[error("num_queries must be > 0")] + ZeroQueries, +} + +/// Complete PCS parameters combining DEEP and FRI parameters. +/// +/// Constructed via [`PcsParams::new`], which validates all parameters. +/// Internal sub-parameters are accessible to crate-internal code only. +#[derive(Clone, Copy, Debug)] +pub struct PcsParams { + /// DEEP quotient parameters. + pub(crate) deep: DeepParams, + /// FRI protocol parameters. + pub(crate) fri: FriParams, + /// Number of query repetitions. + pub(crate) num_queries: usize, + /// Grinding bits before query index sampling. + pub(crate) query_pow_bits: usize, +} + +impl PcsParams { + /// Create validated PCS parameters. + /// + /// # Errors + /// + /// - [`PcsParamsError::InvalidFoldingArity`] if `log_folding_arity` is not 1, 2, or 3. + /// - [`PcsParamsError::ZeroBlowup`] if `log_blowup` is 0. + /// - [`PcsParamsError::FinalDomainTooLarge`] if `log_final_degree + log_blowup > 64`. + /// - [`PcsParamsError::ZeroQueries`] if `num_queries` is 0. + pub fn new( + log_blowup: u8, + log_folding_arity: u8, + log_final_degree: u8, + folding_pow_bits: usize, + deep_pow_bits: usize, + num_queries: usize, + query_pow_bits: usize, + ) -> Result { + let fold = FriFold::new(log_folding_arity) + .ok_or(PcsParamsError::InvalidFoldingArity(log_folding_arity))?; + if log_blowup == 0 { + return Err(PcsParamsError::ZeroBlowup); + } + if log_final_degree as u16 + log_blowup as u16 > MAX_LOG_DOMAIN_SIZE as u16 { + return Err(PcsParamsError::FinalDomainTooLarge { log_final_degree, log_blowup }); + } + if num_queries == 0 { + return Err(PcsParamsError::ZeroQueries); + } + Ok(Self { + deep: DeepParams { deep_pow_bits }, + fri: FriParams { + log_blowup, + fold, + log_final_degree, + folding_pow_bits, + }, + num_queries, + query_pow_bits, + }) + } + + /// Log₂ of the blowup factor. + #[inline] + pub fn log_blowup(&self) -> u8 { + self.fri.log_blowup + } + + /// Number of query repetitions. + #[inline] + pub fn num_queries(&self) -> usize { + self.num_queries + } + + /// Grinding bits before query index sampling. + #[inline] + pub fn query_pow_bits(&self) -> usize { + self.query_pow_bits + } + + /// Grinding bits before DEEP challenge sampling. + #[inline] + pub fn deep_pow_bits(&self) -> usize { + self.deep.deep_pow_bits + } + + /// Grinding bits before each FRI folding round. + #[inline] + pub fn folding_pow_bits(&self) -> usize { + self.fri.folding_pow_bits + } + + /// Log₂ of the FRI folding arity. + #[inline] + pub fn log_folding_arity(&self) -> u8 { + self.fri.fold.log_arity() + } + + /// Log₂ of the final polynomial degree bound. + #[inline] + pub fn log_final_degree(&self) -> u8 { + self.fri.log_final_degree + } +} diff --git a/stark/miden-lifted-stark/src/pcs/proof.rs b/stark/miden-lifted-stark/src/pcs/proof.rs new file mode 100644 index 0000000000..0c1fef4029 --- /dev/null +++ b/stark/miden-lifted-stark/src/pcs/proof.rs @@ -0,0 +1,126 @@ +//! PCS transcript data structures. + +use alloc::vec::Vec; + +use miden_stark_transcript::{TranscriptError, VerifierChannel}; +use p3_field::{ExtensionField, Field, TwoAdicField}; + +use crate::{ + lmcs::{Lmcs, tree_indices::TreeIndices}, + pcs::{deep::proof::DeepTranscript, fri::proof::FriTranscript, params::PcsParams}, +}; + +/// Structured transcript view for the full PCS interaction. +/// +/// Captures observed transcript data plus parsed LMCS batch openings for inspection. +pub struct PcsTranscript +where + L: Lmcs, + L::F: Field, + EF: ExtensionField, +{ + /// DEEP transcript data (evals, PoW witness, challenges). + pub deep_transcript: DeepTranscript, + /// FRI transcript data (round commitments/challenges, final polynomial). + pub fri_transcript: FriTranscript, + /// Proof-of-work witness for query sampling. + pub query_pow_witness: L::F, + /// Query indices in sampling order (domain indices, may contain duplicates). + pub query_indices: Vec, + /// Batch witness per trace tree (leaf data + Merkle witness). + pub deep_witnesses: Vec, + /// Batch witness per FRI round (leaf data + Merkle witness). + pub fri_witnesses: Vec, +} + +impl PcsTranscript +where + L: Lmcs, + L::F: TwoAdicField, + EF: ExtensionField, +{ + /// Parse a PCS transcript from a verifier channel without validation. + /// + /// Composes [`DeepTranscript`], [`FriTranscript`], and per-query LMCS batch proofs. + /// Does not verify any claims; validation happens in + /// [`verify_multi`](crate::verify_multi). + /// Commitment widths must match the committed rows (including any alignment padding), + /// and all commitments are expected to be lifted to the same `log_lde_height`. + /// + /// `log_lde_height` is the log₂ of the LDE evaluation domain height (i.e. the height of + /// the committed LDE matrices). When a trace degree is known, it is typically + /// `log_trace_height + params.fri.log_blowup` (plus any extension used by the caller). + pub fn from_verifier_channel( + params: &PcsParams, + lmcs: &L, + commitments: &[(L::Commitment, Vec)], + log_lde_height: u8, + eval_points: [EF; N], + channel: &mut Ch, + ) -> Result + where + Ch: VerifierChannel, + { + if commitments.is_empty() { + return Err(TranscriptError::NoMoreFields); + } + + let deep_transcript = DeepTranscript::from_verifier_channel::( + ¶ms.deep, + commitments, + eval_points.len(), + channel, + )?; + + let fri_transcript = + FriTranscript::from_verifier_channel(¶ms.fri, log_lde_height, channel)?; + + let query_pow_witness = channel.grind(params.query_pow_bits())?; + + // Sample query indices (domain indices), matching the prover/verifier convention. + let query_indices: Vec = (0..params.num_queries()) + .map(|_| channel.sample_bits(log_lde_height as usize)) + .collect(); + let tree_indices = TreeIndices::new(query_indices.iter().copied(), log_lde_height) + .expect("sampled indices are in range"); + + let deep_witnesses: Vec<_> = commitments + .iter() + .map(|(_commitment, widths)| { + lmcs.read_batch_proof(widths, &tree_indices, channel).map_err(|e| match e { + crate::lmcs::LmcsError::TranscriptError(te) => te, + _ => TranscriptError::NoMoreFields, + }) + }) + .collect::, _>>()?; + + let log_arity = params.fri.fold.log_arity(); + let arity = params.fri.fold.arity(); + let num_rounds = params.fri.num_rounds(log_lde_height); + + let mut fri_witnesses = Vec::with_capacity(num_rounds); + let mut round_indices = tree_indices; + for _round in 0..num_rounds { + round_indices.shrink_depth(log_arity); + let base_width = arity * EF::DIMENSION; + // FRI round openings are unaligned, so use the base width directly. + let round_widths = [base_width]; + let batch = lmcs.read_batch_proof(&round_widths, &round_indices, channel).map_err( + |e| match e { + crate::lmcs::LmcsError::TranscriptError(te) => te, + _ => TranscriptError::NoMoreFields, + }, + )?; + fri_witnesses.push(batch); + } + + Ok(Self { + deep_transcript, + fri_transcript, + query_pow_witness, + query_indices, + deep_witnesses, + fri_witnesses, + }) + } +} diff --git a/stark/miden-lifted-stark/src/pcs/prover.rs b/stark/miden-lifted-stark/src/pcs/prover.rs new file mode 100644 index 0000000000..6faf70de80 --- /dev/null +++ b/stark/miden-lifted-stark/src/pcs/prover.rs @@ -0,0 +1,110 @@ +//! PCS Prover +//! +//! Opens committed matrices at out-of-domain evaluation points. + +use miden_stark_transcript::ProverChannel; +use p3_field::{ExtensionField, TwoAdicField}; +use p3_matrix::Matrix; +use tracing::{info_span, instrument}; + +use crate::{ + lmcs::{Lmcs, LmcsTree, tree_indices::TreeIndices}, + pcs::{deep::prover::DeepPoly, fri::prover::FriPolys, params::PcsParams}, +}; + +/// Open committed matrices at N evaluation points, writing to a prover channel. +/// +/// # Preconditions +/// - `eval_points` must lie outside both the trace-domain subgroup `H` and the LDE evaluation coset +/// `gK` used by the PCS. If a point lies in either set, denominators `(zⱼ − X)` in the DEEP +/// quotient become zero for some domain element, making the quotient undefined. +/// - All trace trees must be built at the same LDE height `2^log_lde_height`. Multiple LDE heights +/// are not supported yet and will panic. +/// +/// `log_lde_height` is the log₂ of the LDE evaluation domain height (i.e. the height of +/// the committed LDE matrices). When a trace degree is known, it is typically +/// `log_trace_height + params.fri.log_blowup` (plus any extension used by the caller). +/// In that common case, the trace subgroup `H` has size `2^(log_lde_height - +/// params.fri.log_blowup)`, while the LDE coset `gK` has size `2^log_lde_height`. +/// +/// Alignment is derived from the trace trees to pad DEEP evaluations consistently. +/// Trace trees must be built with `build_aligned_tree` to match this padding. +#[instrument(name = "PCS opening", skip_all)] +pub fn open_with_channel( + params: &PcsParams, + lmcs: &L, + log_lde_height: u8, + eval_points: [EF; N], + trace_trees: &[&L::Tree], + channel: &mut Ch, +) where + F: TwoAdicField, + EF: ExtensionField, + L: Lmcs, + M: Matrix, + Ch: ProverChannel, +{ + const { assert!(N > 0, "at least one evaluation point required") }; + + // Determine LDE domain size from the supplied LDE height. + // For now, all trace trees must share this height; mixed LDE heights are not supported yet. + assert!(!trace_trees.is_empty(), "at least one trace tree required"); + let expected_height = 1 << log_lde_height as usize; + assert!( + trace_trees.iter().all(|tree| tree.height() == expected_height), + "mixed LDE heights are not supported yet", + ); + // ───────────────────────────────────────────────────────────────────────── + // Construct DEEP quotient (observes evals, grinds, samples alpha and beta) + // ───────────────────────────────────────────────────────────────────────── + let deep_poly = info_span!("DEEP quotient").in_scope(|| { + DeepPoly::from_trees::( + params.deep, + trace_trees, + eval_points, + params.fri.log_blowup, + channel, + ) + }); + + // ───────────────────────────────────────────────────────────────────────── + // FRI commit phase (observes commitments, grinds per-round, samples betas) + // ───────────────────────────────────────────────────────────────────────── + // The deep_poly contains evaluations on the LDE domain (size 2^log_lde_height). + // FRI will prove that this polynomial is low-degree. + let fri_polys = info_span!("FRI commit phase") + .in_scope(|| FriPolys::::new(¶ms.fri, lmcs, deep_poly.deep_evals, channel)); + + // ───────────────────────────────────────────────────────────────────────── + // Grind for query sampling + // ───────────────────────────────────────────────────────────────────────── + let _query_pow_witness = info_span!("query grind", bits = params.query_pow_bits()) + .in_scope(|| channel.grind(params.query_pow_bits())); + + // ───────────────────────────────────────────────────────────────────────── + // Sample query indices (domain indices) + // ───────────────────────────────────────────────────────────────────────── + // Sampled indices are domain indices: domain point = g·ω^{index}. + // The LMCS tree is indexed by domain order (no bit-reversal needed). + let sampled_indices_iter = + (0..params.num_queries()).map(|_| channel.sample_bits(log_lde_height as usize)); + let tree_indices = TreeIndices::new(sampled_indices_iter, log_lde_height) + .expect("sampled indices are in range"); + + // ───────────────────────────────────────────────────────────────────────── + // Generate query proofs + // ───────────────────────────────────────────────────────────────────────── + info_span!("query phase").in_scope(|| { + // Open input trees at all query indices at once (one proof per tree) + info_span!("open input trees", n_trees = trace_trees.len()).in_scope(|| { + for tree in trace_trees { + tree.prove_batch(&tree_indices, channel); + } + }); + + // Open all FRI rounds at all query indices at once (one proof per round) + info_span!("open FRI trees").in_scope(|| { + fri_polys.prove_queries(¶ms.fri, tree_indices, channel); + }); + }); +} diff --git a/stark/miden-lifted-stark/src/pcs/tests.rs b/stark/miden-lifted-stark/src/pcs/tests.rs new file mode 100644 index 0000000000..c0f3bc430e --- /dev/null +++ b/stark/miden-lifted-stark/src/pcs/tests.rs @@ -0,0 +1,170 @@ +//! Common test fixtures and end-to-end tests for the lifted FRI PCS. + +use alloc::{vec, vec::Vec}; + +use miden_stark_transcript::{ProverTranscript, VerifierTranscript}; +use p3_challenger::CanObserve; +use p3_field::Field; +use p3_matrix::{Matrix, bitrev::BitReversibleMatrix, dense::RowMajorMatrix}; +use params::PcsParams; +use proof::PcsTranscript; +use prover::open_with_channel; +use rand::{RngExt, SeedableRng, distr::StandardUniform, prelude::SmallRng}; +use verifier::{PcsError, verify_aligned}; + +use super::*; +use crate::{ + lmcs::{ + Lmcs, LmcsTree, + utils::{aligned_widths, log2_strict_u8}, + }, + testing::configs::goldilocks_poseidon2::{ + self as gl, Felt, Lmcs as BaseLmcs, QuadFelt, TestTree, random_lde_matrix, test_lmcs, + }, +}; + +fn test_params() -> PcsParams { + PcsParams::new( + 2, // log_blowup + 1, // log_folding_arity (arity 2) + 2, // log_final_degree + 1, // folding_pow_bits + 1, // deep_pow_bits + 5, // num_queries + 1, // query_pow_bits + ) + .expect("valid PCS params") +} + +// ============================================================================ +// End-to-end tests +// ============================================================================ + +/// Run the full prover+verifier roundtrip for the given trees and params. +/// On success, also checks that the transcript is fully consumed. +fn run_pcs_case(params: &PcsParams, trees: Vec, seed: u64) -> Result<(), PcsError> { + let rng = &mut SmallRng::seed_from_u64(seed); + let lmcs = test_lmcs(); + + let lde_height = trees[0].leaves().last().map(Matrix::height).unwrap_or(0); + let log_lde_height = log2_strict_u8(lde_height); + let eval_points: [QuadFelt; 2] = [rng.sample(StandardUniform), rng.sample(StandardUniform)]; + + let commitments: Vec<_> = trees.iter().map(|t| (t.root(), t.widths())).collect(); + let trace_trees: Vec<&_> = trees.iter().collect(); + + // Prover: observe all commitments before opening. + let mut challenger = gl::test_challenger(); + for (c, _) in &commitments { + challenger.observe(*c); + } + let mut prover_channel = ProverTranscript::new(challenger); + + open_with_channel::( + params, + &lmcs, + log_lde_height, + eval_points, + &trace_trees, + &mut prover_channel, + ); + let (prover_digest, transcript) = prover_channel.finalize(); + + // Verifier: observe commitments in the same order. + let mut challenger = gl::test_challenger(); + for (c, _) in &commitments { + challenger.observe(*c); + } + let mut verifier_channel = VerifierTranscript::from_data(challenger, &transcript); + + let result = verify_aligned::( + params, + &lmcs, + &commitments, + log_lde_height, + eval_points, + &mut verifier_channel, + ); + + if result.is_ok() { + let verifier_digest = + verifier_channel.finalize().expect("transcript should finalize cleanly"); + assert_eq!(prover_digest, verifier_digest); + + // Re-parse PcsTranscript from a fresh channel and verify digest agreement. + let alignment = lmcs.alignment(); + let aligned_commitments: Vec<_> = commitments + .iter() + .map(|(c, widths)| (*c, aligned_widths(widths.clone(), alignment))) + .collect(); + + let mut challenger = gl::test_challenger(); + for (c, _) in &commitments { + challenger.observe(*c); + } + let mut reparse_channel = VerifierTranscript::from_data(challenger, &transcript); + + PcsTranscript::::from_verifier_channel::<_, 2>( + params, + &lmcs, + &aligned_commitments, + log_lde_height, + eval_points, + &mut reparse_channel, + ) + .expect("PcsTranscript re-parse should succeed"); + + let reparse_digest = reparse_channel + .finalize() + .expect("re-parsed transcript should finalize cleanly"); + assert_eq!(prover_digest, reparse_digest); + } + result.map(|_| ()) +} + +#[test] +fn test_pcs_cases() { + let lmcs = test_lmcs(); + let params = test_params(); + + // Case 1: single matrix, single tree. + let rng = &mut SmallRng::seed_from_u64(42); + let matrix = random_lde_matrix(rng, 6, params.fri.log_blowup, 3, Felt::GENERATOR); + let tree = lmcs.build_aligned_tree(vec![matrix.bit_reverse_rows()]); + run_pcs_case(¶ms, vec![tree], 100).expect("single-tree roundtrip"); + + // Case 2: two separate trees with different column counts. + let rng = &mut SmallRng::seed_from_u64(24); + let mat_a = random_lde_matrix(rng, 6, params.fri.log_blowup, 2, Felt::GENERATOR); + let mat_b = random_lde_matrix(rng, 6, params.fri.log_blowup, 4, Felt::GENERATOR); + let tree_a = lmcs.build_aligned_tree(vec![mat_a.bit_reverse_rows()]); + let tree_b = lmcs.build_aligned_tree(vec![mat_b.bit_reverse_rows()]); + run_pcs_case(¶ms, vec![tree_a, tree_b], 200).expect("multi-tree roundtrip"); + + // Case 3: mixed heights in one commitment group (LMCS upsampling). + let rng = &mut SmallRng::seed_from_u64(99); + let short = random_lde_matrix(rng, 4, params.fri.log_blowup, 2, Felt::GENERATOR); + let tall = random_lde_matrix(rng, 6, params.fri.log_blowup, 3, Felt::GENERATOR); + let tree = lmcs.build_aligned_tree(vec![short.bit_reverse_rows(), tall.bit_reverse_rows()]); + run_pcs_case(¶ms, vec![tree], 300).expect("mixed-height roundtrip"); + + // Case 4: random (non-low-degree) data — FRI should reject. + let rng = &mut SmallRng::seed_from_u64(77); + let reject_params = PcsParams::new( + 1, // log_blowup + 1, // log_folding_arity (arity 2) + 2, // log_final_degree + 1, // folding_pow_bits + 1, // deep_pow_bits + 20, // num_queries + 1, // query_pow_bits + ) + .expect("valid PCS params"); + let height = 1 << 8; + let matrix = RowMajorMatrix::::rand(rng, height, 3); + let tree = lmcs.build_aligned_tree(vec![matrix.bit_reverse_rows()]); + assert!( + run_pcs_case(&reject_params, vec![tree], 400).is_err(), + "should reject high-degree polynomial" + ); +} diff --git a/stark/miden-lifted-stark/src/pcs/utils.rs b/stark/miden-lifted-stark/src/pcs/utils.rs new file mode 100644 index 0000000000..8bfef3ae0f --- /dev/null +++ b/stark/miden-lifted-stark/src/pcs/utils.rs @@ -0,0 +1,106 @@ +use alloc::vec::Vec; +use core::{ + array, + ops::{Add, Mul}, +}; + +use p3_field::{ + ExtensionField, Field, PackedFieldExtension, PackedValue, TwoAdicField, + coset::TwoAdicMultiplicativeCoset, +}; +use p3_util::reverse_slice_index_bits; + +// ============================================================================ +// Extension trait for PackedFieldExtension methods not in upstream +// ============================================================================ + +/// Horner fold with an explicit accumulator. +/// +/// Computes `acc·xⁿ + v₀·xⁿ⁻¹ + v₁·xⁿ⁻² + ... + vₙ₋₁·x⁰` where n = len(vals). +/// Equivalently: `((acc·x + v₀)·x + v₁)·x + ... + vₙ₋₁`. +/// The first element gets the highest power of `x`. +/// +/// For polynomial evaluation `p(x) = Σᵢ cᵢ·xⁱ`, pass coefficients in +/// descending degree order `[cₙ, ..., c₁, c₀]`. +#[inline] +pub fn horner_acc(acc: Acc, x: X, vals: I) -> Acc +where + I: IntoIterator, + Acc: Mul + Add, + X: Clone, +{ + vals.into_iter().fold(acc, |acc, val| acc * x.clone() + val) +} + +/// Horner fold starting from zero. +/// +/// See [`horner_acc`] for the evaluation convention. +#[inline] +pub fn horner(x: X, vals: I) -> Acc +where + I: IntoIterator, + Acc: Default + Mul + Add, + X: Clone, +{ + horner_acc(Acc::default(), x, vals) +} + +/// Extension trait adding `pack_ext_columns` and `to_ext_slice` methods. +/// +/// These methods enable efficient SIMD operations on arrays of extension field elements +/// by providing column-wise packing and unpacking utilities. +pub trait PackedFieldExtensionExt< + BaseField: Field, + ExtField: ExtensionField, +>: PackedFieldExtension +{ + /// Pack N columns from WIDTH rows into N packed extension field elements. + /// + /// Input: `rows[lane][col]` - WIDTH rows, each with N extension field elements. + /// Output: `result[col]` - N packed values, where each packs WIDTH lanes. + fn pack_ext_columns(rows: &[[ExtField; N]]) -> [Self; N] { + let width = BaseField::Packing::WIDTH; + debug_assert_eq!(rows.len(), width); + array::from_fn(|col| { + let col_elems: Vec = (0..width).map(|lane| rows[lane][col]).collect(); + Self::from_ext_slice(&col_elems) + }) + } + + /// Extract all lanes to an output slice. + fn to_ext_slice(&self, out: &mut [ExtField]) { + let width = BaseField::Packing::WIDTH; + for (lane, slot) in out.iter_mut().enumerate().take(width) { + *slot = self.extract(lane); + } + } +} + +impl< + BaseField: Field, + ExtField: ExtensionField, + P: PackedFieldExtension, +> PackedFieldExtensionExt for P +{ +} + +/// Coset points `gK` in bit-reversed order. +/// +/// Note: the coset shift `g` is fixed to `F::GENERATOR` by convention in this PCS. +/// +/// Bit-reversal gives two properties essential for lifting: +/// - **Adjacent negation**: `gK[2i+1] = -gK[2i]`, so both square to the same value +/// - **Squaring gives prefix**: `(gK[2i])² = (gK)²[i]` — the even-indexed elements, when squared, +/// form the half-size sub-coset. Generalizes to r-th powers. +/// +/// Together these enable iterative weight folding in barycentric evaluation. +/// +/// # Panics +/// Panics if the two-adic coset construction fails (e.g., `log_n` exceeds the field's +/// two-adicity), since this unwraps `TwoAdicMultiplicativeCoset::new`. +pub fn bit_reversed_coset_points(log_n: u8) -> Vec { + let coset = TwoAdicMultiplicativeCoset::new(F::GENERATOR, log_n as usize).unwrap(); + let mut pts: Vec = coset.iter().collect(); + reverse_slice_index_bits(&mut pts); + pts +} diff --git a/stark/miden-lifted-stark/src/pcs/verifier.rs b/stark/miden-lifted-stark/src/pcs/verifier.rs new file mode 100644 index 0000000000..51fc4b9114 --- /dev/null +++ b/stark/miden-lifted-stark/src/pcs/verifier.rs @@ -0,0 +1,169 @@ +//! PCS Verifier +//! +//! Verifies polynomial evaluation claims against commitments. +//! +//! Two entry points with the same signature, differing only in alignment handling: +//! +//! | Function | Alignment | +//! |-------------------|-----------| +//! | [`verify`] | caller | +//! | [`verify_aligned`]| automatic | +//! +//! Callers should use +//! [`VerifierTranscript::finalize`](miden_stark_transcript::VerifierTranscript::finalize) +//! after verification to check that the transcript is fully consumed. + +use alloc::vec::Vec; + +use miden_stark_transcript::{TranscriptError, VerifierChannel}; +use p3_field::{ExtensionField, TwoAdicField}; +use p3_matrix::{Matrix, horizontally_truncated::HorizontallyTruncated}; +use thiserror::Error; + +use crate::{ + lmcs::{Lmcs, tree_indices::TreeIndices, utils::aligned_widths}, + pcs::{ + deep::{ + proof::OpenedValues, + verifier::{DeepError, DeepOracle}, + }, + fri::verifier::{FriError, FriOracle}, + params::PcsParams, + }, +}; + +/// Verify polynomial evaluation claims against commitments. +/// +/// Commitment widths must match the committed rows (including any alignment padding +/// from `build_aligned_tree`). The PCS is alignment-agnostic; callers that use +/// aligned trees must pass aligned widths and handle truncation themselves. +/// See [`verify_aligned`] for automatic alignment handling. +/// +/// Does **not** check that the channel is fully consumed after verification. +/// Callers should use +/// [`VerifierTranscript::finalize`](miden_stark_transcript::VerifierTranscript::finalize) to +/// enforce transcript exhaustion. +/// +/// # Preconditions +/// - `eval_points` must lie outside both the trace-domain subgroup `H` and the LDE evaluation coset +/// `gK`. Otherwise denominators `(zⱼ − X)` in the DEEP quotient become zero, making it undefined. +/// - All commitments must be lifted to the same LDE height `2^log_lde_height`. +/// +/// # Returns +/// `opened[group][matrix]` as a `RowMajorMatrix` with `N` rows +/// (one per evaluation point), using the same widths that were passed in. +pub fn verify( + params: &PcsParams, + lmcs: &L, + commitments: &[(L::Commitment, Vec)], + log_lde_height: u8, + eval_points: [EF; N], + channel: &mut Ch, +) -> Result, PcsError> +where + F: TwoAdicField, + EF: ExtensionField + PartialEq + Clone, + L: Lmcs, + Ch: VerifierChannel, +{ + const { assert!(N > 0, "at least one evaluation point required") }; + + if commitments.is_empty() { + return Err(PcsError::NoCommitments); + } + + // Construct verifier's DEEP oracle (observes evals, checks PoW, samples α/β) + let (deep_oracle, evals) = DeepOracle::::new( + params.deep, + &eval_points, + commitments.to_vec(), + log_lde_height, + channel, + )?; + + // Create FRI oracle (observes commitments + final poly, checks per-round PoW) + let fri_oracle = FriOracle::new(¶ms.fri, log_lde_height, channel)?; + + // Check query PoW witness and sample query indices + channel.grind(params.query_pow_bits())?; + + // Sample query indices (domain indices). The LMCS tree is indexed by domain order. + let sampled_indices_iter = + (0..params.num_queries()).map(|_| channel.sample_bits(log_lde_height as usize)); + let tree_indices = TreeIndices::new(sampled_indices_iter, log_lde_height) + .expect("sampled indices are in range"); + + // Verify DEEP openings for all queries at once + // tree_indices are bit-reversed positions; deep_evals is keyed by tree index + let deep_evals = deep_oracle.open_batch(lmcs, &tree_indices, channel)?; + + // Test low-degree proximity for all queries at once + fri_oracle.test_low_degree(lmcs, ¶ms.fri, deep_evals, tree_indices, channel)?; + + Ok(evals) +} + +/// Like [`verify`], but handles LMCS alignment automatically. +/// +/// Commitment widths should be the original (unpadded) data widths. This function: +/// 1. Aligns widths to `lmcs.alignment()` +/// 2. Calls [`verify`] with aligned widths +/// 3. Truncates returned evals back to original widths +pub fn verify_aligned( + params: &PcsParams, + lmcs: &L, + commitments: &[(L::Commitment, Vec)], + log_lde_height: u8, + eval_points: [EF; N], + channel: &mut Ch, +) -> Result, PcsError> +where + F: TwoAdicField, + EF: ExtensionField + PartialEq + Clone, + L: Lmcs, + Ch: VerifierChannel, +{ + let alignment = lmcs.alignment(); + let aligned_commitments: Vec<_> = commitments + .iter() + .map(|(c, widths)| (c.clone(), aligned_widths(widths.clone(), alignment))) + .collect(); + + let evals = verify(params, lmcs, &aligned_commitments, log_lde_height, eval_points, channel)?; + + // Truncate each matrix back to original widths, removing alignment padding. + let truncated = evals + .into_iter() + .zip(commitments) + .map(|(group, (_, orig_widths))| { + group + .into_iter() + .zip(orig_widths) + .map(|(mat, &orig_w)| { + HorizontallyTruncated::new(mat, orig_w) + .expect("original width must not exceed aligned width") + .to_row_major_matrix() + }) + .collect() + }) + .collect(); + + Ok(truncated) +} + +// ============================================================================ +// Error Types +// ============================================================================ + +/// Errors that can occur during PCS verification. +#[derive(Debug, Error)] +pub enum PcsError { + #[error("no commitments provided")] + NoCommitments, + #[error("DEEP error: {0}")] + DeepError(#[from] DeepError), + #[error("FRI error: {0}")] + FriError(#[from] FriError), + #[error("transcript error: {0}")] + TranscriptError(#[from] TranscriptError), +} diff --git a/stark/miden-lifted-stark/src/proof.rs b/stark/miden-lifted-stark/src/proof.rs new file mode 100644 index 0000000000..c25d20cfa1 --- /dev/null +++ b/stark/miden-lifted-stark/src/proof.rs @@ -0,0 +1,319 @@ +//! STARK proof types and structured transcript. +//! +//! This module defines the proof artifact types shared by prover and verifier: +//! - [`StarkProof`]: raw transcript data (field elements and commitments) +//! - [`StarkDigest`]: binding digest committing to the entire interaction +//! - [`StarkOutput`]: combined prover output (proof + digest) +//! - [`StarkTranscript`]: structured parse-only view of the full protocol interaction +//! +//! [`StarkTranscript`] has a [`from_proof`](StarkTranscript::from_proof) constructor +//! that parses it from proof data and a challenger, following the same pattern as +//! [`PcsTranscript`] alongside the PCS verifier. + +extern crate alloc; + +use alloc::{vec, vec::Vec}; + +use miden_lifted_air::LiftedAir; +use miden_stark_transcript::{Channel, TranscriptData, VerifierChannel, VerifierTranscript}; +use p3_challenger::CanFinalizeDigest; +use p3_field::{ExtensionField, Field, TwoAdicField}; +use serde::{Deserialize, Serialize}; + +use crate::{ + StarkConfig, + coset::LiftedCoset, + instance::{AirInstance, InstanceShapes, validate_air_order, validate_inputs}, + lmcs::{Lmcs, utils::aligned_len}, + pcs::proof::PcsTranscript, + verifier::VerifierError, +}; + +/// Commitment type alias for convenience. +type Commitment = <>::Lmcs as Lmcs>::Commitment; + +/// STARK proof: per-instance shape metadata plus raw transcript data. +/// +/// Fields are opaque. The accessors below expose wire-format summaries +/// (trace count, transcript sizes). Read per-instance log trace heights by +/// parsing via [`StarkTranscript::from_proof`], which validates the shape +/// metadata and binds it into the Fiat-Shamir challenger — +/// [`verify_multi`](crate::verifier::verify_multi) runs the same validation. +// Bounds target `Commitment` directly; `SC` itself isn't `Serialize`/`Debug`. +#[derive(Clone, Serialize, Deserialize)] +#[serde(bound(serialize = "TranscriptData>: Serialize"))] +#[serde(bound(deserialize = "TranscriptData>: Deserialize<'de>"))] +pub struct StarkProof, SC: StarkConfig> { + pub(crate) instance_shapes: InstanceShapes, + pub(crate) transcript: TranscriptData>, +} + +impl core::fmt::Debug for StarkProof +where + F: TwoAdicField + core::fmt::Debug, + EF: ExtensionField, + SC: StarkConfig, + Commitment: core::fmt::Debug, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("StarkProof") + .field("instance_shapes", &self.instance_shapes) + .field("transcript", &self.transcript) + .finish() + } +} + +impl StarkProof +where + F: TwoAdicField, + EF: ExtensionField, + SC: StarkConfig, +{ + /// The AIR ordering used by the proof: `air_order()[j]` is the caller's + /// original index of the instance at position `j`. + /// + /// Read this before building the Fiat-Shamir challenger so you can bind + /// AIR configurations and the ordering — see the prover module-level docs. + pub fn air_order(&self) -> &[u32] { + self.instance_shapes.air_order() + } + + /// Number of traces (instances) the proof was produced for. + pub fn num_traces(&self) -> usize { + self.instance_shapes.log_trace_heights.len() + } + + /// Number of base-field elements in the transcript. + pub fn num_field_elements(&self) -> usize { + self.transcript.fields().len() + } + + /// Number of commitments in the transcript. + pub fn num_commitments(&self) -> usize { + self.transcript.commitments().len() + } + + /// Total byte size of the proof. + pub fn size_in_bytes(&self) -> usize { + self.instance_shapes.size_in_bytes() + self.transcript.size_in_bytes() + } +} + +/// Transcript digest: the challenger's native binding digest that commits to +/// the entire prover–verifier interaction. The prover and verifier must produce +/// the same digest for the proof to be valid. +pub type StarkDigest = + <>::Challenger as CanFinalizeDigest>::Digest; + +/// Output of [`crate::prover::prove_single`] / [`crate::prover::prove_multi`]: the proof data and +/// its transcript digest. +pub struct StarkOutput, SC: StarkConfig> { + /// Transcript digest committing to the entire prover–verifier interaction. + pub digest: StarkDigest, + /// Proof data consumed by the verifier. + pub proof: StarkProof, +} + +impl core::fmt::Debug for StarkOutput +where + F: TwoAdicField + core::fmt::Debug, + EF: ExtensionField, + SC: StarkConfig, + StarkDigest: core::fmt::Debug, + Commitment: core::fmt::Debug, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("StarkOutput") + .field("digest", &self.digest) + .field("proof", &self.proof) + .finish() + } +} + +/// Structured transcript view for the full lifted STARK protocol. +/// +/// Captures instance shape metadata, commitments, sampled challenges, the OOD +/// evaluation point, and the PCS sub-transcript. Constructed via +/// [`from_proof`](Self::from_proof), which mirrors steps 0–9 of +/// [`verify_multi`](crate::verifier::verify_multi) but skips the constraint +/// check. +pub struct StarkTranscript +where + L: Lmcs, + L::F: Field, + EF: ExtensionField, +{ + /// Per-instance shape metadata. Validated and observed into the challenger + /// by [`from_proof`](Self::from_proof). + pub instance_shapes: InstanceShapes, + /// Throwaway challenge squeezed right after observing the instance shapes, + /// used to clear the challenger's absorb buffer so that later sampled + /// challenges depend on the full shape metadata regardless of sponge state. + pub instance_challenge: EF, + /// Main trace commitment. + pub main_commit: L::Commitment, + /// Randomness sampled for auxiliary traces. + pub randomness: Vec, + /// Auxiliary trace commitment. + pub aux_commit: L::Commitment, + /// Aux values per AIR instance, observed into the transcript after the aux commitment. + pub all_aux_values: Vec>, + /// Constraint folding challenge alpha. + pub alpha: EF, + /// AIR accumulation challenge beta. + pub beta: EF, + /// Quotient polynomial commitment. + pub quotient_commit: L::Commitment, + /// Out-of-domain evaluation point z. + pub z: EF, + /// PCS sub-transcript (DEEP evals, FRI rounds, query openings). + pub pcs_transcript: PcsTranscript, +} + +impl StarkTranscript +where + L: Lmcs, + L::F: TwoAdicField, + EF: ExtensionField, +{ + /// Parse a STARK transcript from proof data and a challenger. + /// + /// Mirrors steps 0–9 of [`verify_multi`](crate::verifier::verify_multi): + /// 0. Validate instance shapes, then observe log trace heights into the challenger and squeeze + /// a throwaway `instance_challenge` to clear the absorb buffer + /// 1. Receive main trace commitment + /// 2. Sample randomness for auxiliary traces + /// 3. Receive auxiliary trace commitment + /// 4. Receive aux values (per AIR instance) + /// 5. Sample constraint folding alpha and accumulation beta + /// 6. Receive quotient commitment + /// 7. Sample OOD point z + /// 8. Build commitment widths for PCS + /// 9. Parse PCS sub-transcript via [`PcsTranscript::from_verifier_channel`] + /// + /// Does **not** verify constraints or check the quotient identity. + /// Finalizes the transcript and returns the digest alongside the parsed view. + #[allow(clippy::type_complexity)] + pub fn from_proof( + config: &SC, + instances: &[(&A, AirInstance<'_, L::F>)], + proof: &StarkProof, + mut challenger: SC::Challenger, + ) -> Result<(Self, StarkDigest), VerifierError> + where + A: LiftedAir, + SC: StarkConfig, + { + validate_air_order(proof.instance_shapes.air_order(), instances.len())?; + let instances = proof.instance_shapes.reorder(instances.to_vec())?; + + let log_blowup = config.pcs().log_blowup(); + let log_max_trace_height = validate_inputs(&instances, &proof.instance_shapes, log_blowup)?; + proof.instance_shapes.observe_heights::(&mut challenger); + + let mut channel = VerifierTranscript::from_data(challenger, &proof.transcript); + + // Clear the challenger's absorb buffer after observing instance shapes. + // Mirrors `prove_multi` / `verify_multi`. + let instance_challenge: EF = channel.sample_algebra_element::(); + + let alignment = config.lmcs().alignment(); + + // Infer constraint degree from symbolic AIR analysis (max across all AIRs) + let constraint_degree = + instances.iter().map(|(air, _)| air.constraint_degree()).max().unwrap_or(2); + let log_lde_height = log_max_trace_height + log_blowup; + + // Max LDE coset (for the largest trace, no lifting) + let max_lde_coset = LiftedCoset::unlifted(log_max_trace_height, log_blowup); + + // 1. Receive main trace commitment + let main_commit = channel.receive_commitment()?.clone(); + + // 2. Sample randomness for aux traces + let max_num_randomness = + instances.iter().map(|(air, _)| air.num_randomness()).max().unwrap_or(0); + + let randomness: Vec = (0..max_num_randomness) + .map(|_| channel.sample_algebra_element::()) + .collect(); + + // 3. Receive aux trace commitment + let aux_commit = channel.receive_commitment()?.clone(); + + // 4. Receive aux values from the transcript (one EF element per aux value, per instance). + let all_aux_values: Vec> = instances + .iter() + .map(|(air, _)| { + let count = air.num_aux_values(); + (0..count) + .map(|_| channel.receive_algebra_element::()) + .collect::, _>>() + }) + .collect::, _>>()?; + + // 5. Sample constraint folding alpha and accumulation beta + let alpha: EF = channel.sample_algebra_element::(); + let beta: EF = channel.sample_algebra_element::(); + + // 6. Receive quotient commitment + let quotient_commit = channel.receive_commitment()?.clone(); + + // 7. Sample OOD point (outside max trace domain H and max LDE coset gK) + let z: EF = max_lde_coset.sample_ood_point(&mut channel); + let h = L::F::two_adic_generator(log_max_trace_height.into()); + let z_next = z * h; + + // 8. Build commitment widths for PCS. + // + // The LMCS commits to rows padded to `alignment` boundary, so DEEP evals and + // batch openings are stored at aligned widths in the transcript. We must use + // aligned widths here to parse the transcript correctly. + // (The verifier's `verify_aligned` does the same alignment internally, then + // truncates the returned evals back to original widths for constraint checking.) + let main_widths: Vec = + instances.iter().map(|(air, _)| aligned_len(air.width(), alignment)).collect(); + let quotient_width = aligned_len(constraint_degree * EF::DIMENSION, alignment); + + let aux_widths: Vec = instances + .iter() + .map(|(air, _)| aligned_len(air.aux_width() * EF::DIMENSION, alignment)) + .collect(); + + let commitments = vec![ + (main_commit.clone(), main_widths), + (aux_commit.clone(), aux_widths), + (quotient_commit.clone(), vec![quotient_width]), + ]; + + // 9. Parse PCS sub-transcript + let pcs_transcript = PcsTranscript::from_verifier_channel::<_, 2>( + config.pcs(), + config.lmcs(), + &commitments, + log_lde_height, + [z, z_next], + &mut channel, + )?; + + // 10. Finalize transcript and extract digest + let digest = channel.finalize()?; + + Ok(( + Self { + instance_shapes: proof.instance_shapes.clone(), + instance_challenge, + main_commit, + randomness, + aux_commit, + all_aux_values, + alpha, + beta, + quotient_commit, + z, + pcs_transcript, + }, + digest, + )) + } +} diff --git a/stark/miden-lifted-stark/src/prover/README.md b/stark/miden-lifted-stark/src/prover/README.md new file mode 100644 index 0000000000..6096c6e90a --- /dev/null +++ b/stark/miden-lifted-stark/src/prover/README.md @@ -0,0 +1,221 @@ +# Lifted STARK Prover + +End-to-end proving for the lifted STARK protocol using LMCS commitments +and the lifted FRI PCS. Supports multiple traces of different power-of-two +heights via virtual lifting. + +Protocol-level overview lives in `miden-lifted-stark/README.md`. + +## Entry Points + +| Item | Purpose | +|------|---------| +| `prove_single` | Prove a single-AIR STARK | +| `prove_multi` | Prove a multi-trace STARK | +| `AirWitness` | Bundle a trace with its public values | + +```text +prove_single(config, air, trace, public_values, var_len_public_inputs, aux_builder, challenger) +prove_multi(config, &[(air, witness, aux_builder), ...], challenger) +``` + +The proof is written into the provided transcript channel. This crate does not +prescribe the *initial* challenger state used for Fiat-Shamir. + +## Fiat-Shamir / transcript binding + +The caller must bind protocol parameters, public values, variable-length +public inputs, AIR configurations, and `air_order` into the challenger +before calling `prove_multi`. See the Rust module-level docs for the full contract +and code examples. + +## Protocol flow + +1. Validate trace dimensions against AIR definition. +2. Commit main trace LDE on nested coset (bit-reversed), observe commitment. +3. Sample aux randomness, build aux trace, commit aux LDE. +4. Sample constraint folding challenge `alpha` and cross-trace accumulator `beta`. +5. Build periodic LDEs for periodic columns. +6. Compute folded constraint numerators on each trace's quotient domain `gJ`. +7. Lift and beta-accumulate numerators onto the max quotient domain. +8. Divide by the max vanishing polynomial to obtain Q(gJ). +9. Commit quotient chunks via fused iDFT + scaling + DFT pipeline. +10. Sample OOD point `z` (rejection-sampled outside trace domain), derive `z_next`. +11. Open via PCS at `[z, z_next]` for main, aux, and quotient trees. + +## Mathematical background + +This section assumes familiarity with classic STARKs and focuses on what +changes with **lifting** — how the prover avoids work on the largest +("lifted") domains. All sizes are powers of two. + +### Domains and cosets + +Let: + +- $N = 2^n$ be the **maximum** trace height across all traces in the proof. +- $D = 2^d$ be the **constraint degree** (quotient-domain blowup). +- $B = 2^b$ be the **PCS/FRI blowup** (commitment-domain blowup), with $D \le B$. +- $g$ be the fixed multiplicative shift (`F::GENERATOR`). + +Define two-adic subgroups: + +$$ +H = \langle \omega_H \rangle,\ |H| = N +\qquad +J = \langle \omega_J \rangle,\ |J| = N\,D +\qquad +K = \langle \omega_K \rangle,\ |K| = N\,B +$$ + +with the usual relationships: + +$$ +\omega_H = \omega_J^D = \omega_K^B +\qquad +H = J^D = K^B +\qquad +J = K^{B/D}\ \text{(when } D \le B\text{)} +$$ + +We work over shifted cosets $gH, gJ, gK$. + +### Mixed heights via lifting + +Suppose trace $T_j$ has height + +$$ +n_j = N / r_j +\qquad\text{where } r_j = 2^{\ell_j} \text{ is a power of two.} +$$ + +Intuitively, **lifting** makes $T_j$ look like a height-$N$ trace by stacking $r_j$ +copies of it. Algebraically, if $t_j(X)$ is the degree-$`) +/// - `L`: LMCS configuration type +/// +/// # Usage +/// +/// ```ignore +/// let committed = commit_traces(config, traces); +/// let root = committed.root(); +/// let view = committed.evals_on_quotient_domain(0, constraint_degree); +/// ``` +/// +/// Storing the blowup also avoids re-deriving `trace_height = lde_height / blowup` for each +/// matrix, which is needed for quotient-domain views and lifting shifts. +pub struct Committed +where + F: TwoAdicField, + L: Lmcs, + M: Matrix, +{ + /// The underlying LMCS tree. + tree: L::Tree, + /// Log₂ of the blowup factor used during LDE. + log_blowup: u8, +} + +impl Committed +where + F: TwoAdicField, + L: Lmcs, + M: Matrix, +{ + /// Create a new `Committed` wrapper. + /// + /// # Arguments + /// + /// - `tree`: The LMCS tree containing committed LDE matrices + /// - `log_blowup`: Log₂ of the blowup factor used during LDE + #[inline] + pub fn new(tree: L::Tree, log_blowup: u8) -> Self { + Self { tree, log_blowup } + } + + /// Get the commitment root. + #[inline] + pub fn root(&self) -> L::Commitment { + self.tree.root() + } + + /// Get a reference to the underlying tree. + #[inline] + pub fn tree(&self) -> &L::Tree { + &self.tree + } + + /// Get log₂ of the maximum LDE height across all matrices. + /// + /// This is the height of the tree (the largest matrix height). + #[inline] + fn log_max_lde_height(&self) -> u8 { + log2_strict_u8(self.tree.height()) + } + + /// Returns the [`LiftedCoset`] the `m`-th matrix was committed on. + /// + /// # Panics + /// + /// Panics if `m >= num_matrices()`. + fn lifted_coset(&self, m: usize) -> LiftedCoset { + let matrix = &self.tree.leaves()[m]; + let log_lde_height = log2_strict_u8(matrix.height()); + let log_trace_height = log_lde_height - self.log_blowup; + let log_max_trace_height = self.log_max_lde_height() - self.log_blowup; + + LiftedCoset::new(log_trace_height, self.log_blowup, log_max_trace_height) + } +} + +impl Committed, L> +where + F: TwoAdicField, + L: Lmcs, +{ + /// Return a zero-copy view of matrix `m` on the quotient evaluation domain. + /// + /// This returns evaluations over the quotient coset `gJ ⊆ gK`. + /// + /// The tree commits to LDE evaluations on `gK` (size `N·B`). The `RowMajorMatrix` + /// stores bit-reversed evaluations; `gJ` appears as the first `N·D` rows, so this is + /// a zero-copy prefix view followed by `bit_reverse_rows()` to expose natural order. + /// + /// # Panics + /// + /// Panics if `m >= num_matrices()`. + pub fn evals_on_quotient_domain( + &self, + m: usize, + constraint_degree: usize, + ) -> BitReversedMatrixView> { + let quotient_height = self.lifted_coset(m).trace_height() * constraint_degree; + self.tree.leaves()[m].split_rows(quotient_height).0.bit_reverse_rows() + } +} + +// ============================================================================ +// commit_traces +// ============================================================================ + +/// Commit multiple trace matrices with lifting: LDE → LMCS tree. +/// +/// Traces must be sorted by height in ascending order. Each trace is lifted to +/// the max LDE domain using the appropriate nested coset shift. +/// +/// The DFT output is wrapped in `BitReversedMatrixView` (zero-cost view) and +/// passed directly to the LMCS — no materialization needed. +/// +/// Returns a [`Committed`] wrapper providing: +/// - Commitment root via [`Committed::root()`] +/// - Underlying LMCS tree via [`Committed::tree()`] +/// - Quotient domain views via [`Committed::evals_on_quotient_domain()`] +/// +/// # Arguments +/// - `config`: STARK configuration containing PCS params, LMCS, and DFT +/// - `traces`: Trace matrices sorted by height (ascending) +/// +/// # Panics +/// - If `traces` is empty +/// - If trace heights are not powers of two +/// - If traces are not sorted by height in ascending order +/// +/// Lifting note: for a trace of height `n` embedded into a max height `n_max`, let +/// `r = n_max / n`. The commitment should behave as if it contains evaluations of the +/// lifted polynomial `f_lift(X) = f(Xʳ)` on the max LDE coset. This is achieved by +/// evaluating the original trace on a *nested* coset with shift gʳ: the map +/// `(g·ω)ʳ = gʳ·ωʳ` sends the max domain down to the smaller one. +pub fn commit_traces( + config: &SC, + traces: Vec>, +) -> Committed, SC::Lmcs> +where + F: TwoAdicField, + EF: ExtensionField, + SC: StarkConfig, +{ + assert!(!traces.is_empty(), "at least one trace required"); + + assert!( + traces.windows(2).all(|w| w[0].height() <= w[1].height()), + "traces must be sorted by height in ascending order" + ); + + let log_blowup = config.pcs().log_blowup(); + + // Find max trace height + let max_trace_height = traces.last().unwrap().height(); + let log_max_trace_height = log2_strict_u8(max_trace_height); + + let ldes: Vec<_> = traces + .into_iter() + .enumerate() + .map(|(idx, trace)| { + let trace_height = trace.height(); + let width = trace.width(); + + // Validate height is power of two + assert!( + trace_height.is_power_of_two(), + "trace height must be power of two (index {idx})" + ); + + let log_trace_height = log2_strict_u8(trace_height); + + // Use LiftedCoset to compute the coset shift + let coset = LiftedCoset::new(log_trace_height, log_blowup, log_max_trace_height); + let coset_shift = coset.lde_shift::(); + + info_span!("LDE", trace = idx, log_height = log_trace_height, width).in_scope(|| { + let lde = config.dft().coset_lde_batch(trace, log_blowup.into(), coset_shift); + materialize_bitrev(lde) + }) + }) + .collect(); + + // Build aligned LMCS tree and wrap in Committed + let tree = config.lmcs().build_aligned_tree(ldes); + Committed::new(tree, log_blowup) +} + +// ============================================================================ +// Tests +// ============================================================================ + +#[cfg(test)] +mod tests { + use alloc::vec; + + use p3_field::PrimeCharacteristicRing; + use p3_util::reverse_bits_len; + + use super::*; + use crate::testing::configs::goldilocks_poseidon2::Felt; + + #[test] + fn split_rows_truncates_correctly() { + // Create a 16x4 matrix (LDE height = 16, width = 4) + let data: Vec = (0u64..64).map(Felt::from_u64).collect(); + let matrix = RowMajorMatrix::new(data, 4); + + // Truncate to 8 rows via split_rows + let truncated = matrix.split_rows(8).0; + assert_eq!(truncated.height(), 8); + assert_eq!(truncated.width(), 4); + + // Verify first row is unchanged + let row: Vec = truncated.row(0).unwrap().into_iter().collect(); + assert_eq!( + row, + vec![Felt::from_u64(0), Felt::from_u64(1), Felt::from_u64(2), Felt::from_u64(3)] + ); + } + + #[test] + fn bit_reverse_rows_gives_natural_order() { + // Create an 8x2 matrix with values that let us verify bit-reversal + // Row i (bit-reversed) contains [2*i, 2*i+1] + let data: Vec = (0u64..16).map(Felt::from_u64).collect(); + let matrix = RowMajorMatrix::new(data, 2); + + let natural = matrix.as_view().bit_reverse_rows(); + assert_eq!(natural.height(), 8); + assert_eq!(natural.width(), 2); + + // General verification: natural row i should have values from bit-reversed row bitrev(i) + for i in 0..8 { + let br_i = reverse_bits_len(i, 3); + let natural_row: Vec = natural.row(i).unwrap().into_iter().collect(); + let expected: Vec = + vec![Felt::from_u64((br_i * 2) as u64), Felt::from_u64((br_i * 2 + 1) as u64)]; + assert_eq!(natural_row, expected, "mismatch at natural row {i}"); + } + } + + #[test] + fn truncate_then_bit_reverse() { + // Create a 16x2 matrix + let data: Vec = (0u64..32).map(Felt::from_u64).collect(); + let matrix = RowMajorMatrix::new(data, 2); + + // Truncate to 8 rows and convert to natural order + let truncated_natural = matrix.split_rows(8).0.bit_reverse_rows(); + assert_eq!(truncated_natural.height(), 8); + assert_eq!(truncated_natural.width(), 2); + + for i in 0..8 { + assert_eq!( + truncated_natural.row(i).unwrap().into_iter().count(), + 2, + "row {i} should have 2 elements" + ); + } + } +} diff --git a/stark/miden-lifted-stark/src/prover/constraints/folder.rs b/stark/miden-lifted-stark/src/prover/constraints/folder.rs new file mode 100644 index 0000000000..e65d7c7e63 --- /dev/null +++ b/stark/miden-lifted-stark/src/prover/constraints/folder.rs @@ -0,0 +1,284 @@ +//! SIMD-optimized constraint folder for prover evaluation. +//! +//! [`ProverConstraintFolder`] collects base and extension constraints during `air.eval()`, +//! then combines them via [`Self::finalize_constraints`] using decomposed alpha powers +//! and batched linear combinations. + +use alloc::vec::Vec; +use core::marker::PhantomData; + +use miden_lifted_air::{ + AirBuilder, EmptyWindow, ExtensionBuilder, PeriodicAirBuilder, PermutationAirBuilder, RowWindow, +}; +use p3_field::{ + Algebra, BasedVectorSpace, ExtensionField, Field, PackedField, PrimeCharacteristicRing, +}; + +use crate::selectors::Selectors; + +/// Batch size for constraint linear-combination chunks in [`finalize_constraints`]. +const CONSTRAINT_BATCH: usize = 8; + +/// Batched linear combination of packed extension field values with EF coefficients. +/// +/// Extension-field analogue of [`PackedField::packed_linear_combination`]. Processes +/// `coeffs` and `values` in chunks of [`CONSTRAINT_BATCH`], then handles the remainder. +#[inline] +fn batched_ext_linear_combination(coeffs: &[EF], values: &[PE]) -> PE +where + EF: Field, + PE: PrimeCharacteristicRing + Algebra + Copy, +{ + debug_assert_eq!(coeffs.len(), values.len()); + let len = coeffs.len(); + let mut acc = PE::ZERO; + let mut start = 0; + while start + CONSTRAINT_BATCH <= len { + let batch: [PE; CONSTRAINT_BATCH] = + core::array::from_fn(|i| values[start + i] * coeffs[start + i]); + acc += PE::sum_array::(&batch); + start += CONSTRAINT_BATCH; + } + for (&coeff, &val) in coeffs[start..].iter().zip(&values[start..]) { + acc += val * coeff; + } + acc +} + +/// Batched linear combination of packed base field values with F coefficients. +/// +/// Wraps [`PackedField::packed_linear_combination`] with batched chunking +/// and remainder handling, mirroring [`batched_ext_linear_combination`]. +#[inline] +fn batched_base_linear_combination(coeffs: &[P::Scalar], values: &[P]) -> P { + debug_assert_eq!(coeffs.len(), values.len()); + let len = coeffs.len(); + let mut acc = P::ZERO; + let mut start = 0; + while start + CONSTRAINT_BATCH <= len { + acc += P::packed_linear_combination::( + &coeffs[start..start + CONSTRAINT_BATCH], + &values[start..start + CONSTRAINT_BATCH], + ); + start += CONSTRAINT_BATCH; + } + for (&coeff, &val) in coeffs[start..].iter().zip(&values[start..]) { + acc += val * coeff; + } + acc +} + +/// Packed constraint folder for SIMD-optimized prover evaluation. +/// +/// Uses packed types to evaluate constraints on multiple domain points simultaneously: +/// - `P`: Packed base field (e.g., `PackedGoldilocks`) +/// - `PE`: Packed extension field - must be `Algebra + Algebra

+ BasedVectorSpace

` +/// +/// Collects constraints during `air.eval()` into separate base/ext vectors, then +/// combines them in [`Self::finalize_constraints`] using decomposed alpha powers and +/// `packed_linear_combination` for efficient SIMD accumulation. +/// +/// # Type Parameters +/// - `F`: Base field scalar +/// - `EF`: Extension field scalar +/// - `P`: Packed base field (with `P::Scalar = F`) +/// - `PE`: Packed extension field (must implement appropriate algebra traits) +pub struct ProverConstraintFolder<'a, F, EF, P, PE> +where + F: Field, + EF: ExtensionField, + P: PackedField, + PE: Algebra + Algebra

+ BasedVectorSpace

+ Copy + Send + Sync, +{ + /// Main trace two-row window (packed base field) + pub main: RowWindow<'a, P>, + /// Aux/permutation trace two-row window (packed extension field) + pub aux: RowWindow<'a, PE>, + /// Randomness for aux trace (packed extension field) + pub packed_randomness: &'a [PE], + /// Public values (base field scalars) + pub public_values: &'a [F], + /// Periodic column values (packed base field) + pub periodic_values: &'a [P], + /// Permutation values (packed extension field) + pub permutation_values: &'a [PE], + /// Constraint selectors (packed base field) + pub selectors: Selectors

, + /// Base-field alpha powers, reordered to match base constraint emission order. + /// `base_alpha_powers[d][j]` = d-th basis coefficient of alpha power for j-th base constraint. + pub base_alpha_powers: &'a [Vec], + /// Extension-field alpha powers, reordered to match ext constraint emission order. + pub ext_alpha_powers: &'a [EF], + /// Current constraint index (debug-only bookkeeping) + pub constraint_index: usize, + /// Total expected constraint count (debug-only bookkeeping) + pub constraint_count: usize, + /// Collected base-field constraints for this row + pub base_constraints: Vec

, + /// Collected extension-field constraints for this row + pub ext_constraints: Vec, + pub _phantom: PhantomData, +} + +impl<'a, F, EF, P, PE> ProverConstraintFolder<'a, F, EF, P, PE> +where + F: Field, + EF: ExtensionField, + P: PackedField, + PE: Algebra + Algebra

+ BasedVectorSpace

+ Copy + Send + Sync, +{ + /// Combine all collected constraints with their pre-computed alpha powers. + /// + /// Base constraints use `batched_base_linear_combination` per basis dimension, + /// decomposing the extension-field multiply into D base-field SIMD dot products. + /// Extension constraints use `batched_ext_linear_combination` with scalar EF + /// coefficients. Both process in chunks of `CONSTRAINT_BATCH`. + /// + /// We keep base and extension constraints separate because the base constraints can + /// stay in the base field and use packed SIMD arithmetic. Decomposing EF powers of + /// `alpha` into base-field coordinates turns the base-field fold into a small number + /// of packed dot-products, avoiding repeated cross-field promotions. + #[inline] + pub fn finalize_constraints(self) -> PE { + debug_assert_eq!(self.constraint_index, self.constraint_count); + debug_assert_eq!( + self.base_constraints.len(), + self.base_alpha_powers.first().map_or(0, Vec::len) + ); + debug_assert_eq!(self.ext_constraints.len(), self.ext_alpha_powers.len()); + + // Base constraints: D independent base-field dot products + let base = &self.base_constraints; + let base_powers = self.base_alpha_powers; + let acc = PE::from_basis_coefficients_fn(|d| { + batched_base_linear_combination(&base_powers[d], base) + }); + + // Extension constraints: EF-coefficient dot product + acc + batched_ext_linear_combination(self.ext_alpha_powers, &self.ext_constraints) + } +} + +impl<'a, F, EF, P, PE> AirBuilder for ProverConstraintFolder<'a, F, EF, P, PE> +where + F: Field, + EF: ExtensionField, + P: PackedField, + PE: Algebra + Algebra

+ BasedVectorSpace

+ Copy + Send + Sync, +{ + type F = F; + type Expr = P; + type Var = P; + type PreprocessedWindow = EmptyWindow

; + type MainWindow = RowWindow<'a, P>; + type PublicVar = F; + + #[inline] + fn main(&self) -> Self::MainWindow { + self.main + } + + fn preprocessed(&self) -> &Self::PreprocessedWindow { + EmptyWindow::empty_ref() + } + + #[inline] + fn is_first_row(&self) -> Self::Expr { + self.selectors.is_first_row + } + + #[inline] + fn is_last_row(&self) -> Self::Expr { + self.selectors.is_last_row + } + + #[inline] + fn is_transition_window(&self, size: usize) -> Self::Expr { + if size == 2 { + self.selectors.is_transition + } else { + panic!("only window size 2 supported") + } + } + + #[inline] + fn assert_zero>(&mut self, x: I) { + self.base_constraints.push(x.into()); + self.constraint_index += 1; + } + + #[inline] + fn assert_zeros>(&mut self, array: [I; N]) { + let expr_array = array.map(Into::into); + self.base_constraints.extend(expr_array); + self.constraint_index += N; + } + + #[inline] + fn public_values(&self) -> &[Self::PublicVar] { + self.public_values + } +} + +impl<'a, F, EF, P, PE> ExtensionBuilder for ProverConstraintFolder<'a, F, EF, P, PE> +where + F: Field, + EF: ExtensionField, + P: PackedField, + PE: Algebra + Algebra

+ BasedVectorSpace

+ Copy + Send + Sync, +{ + type EF = EF; + type ExprEF = PE; + type VarEF = PE; + + #[inline] + fn assert_zero_ext(&mut self, x: I) + where + I: Into, + { + self.ext_constraints.push(x.into()); + self.constraint_index += 1; + } +} + +impl<'a, F, EF, P, PE> PermutationAirBuilder for ProverConstraintFolder<'a, F, EF, P, PE> +where + F: Field, + EF: ExtensionField, + P: PackedField, + PE: Algebra + Algebra

+ BasedVectorSpace

+ Copy + Send + Sync, +{ + type MP = RowWindow<'a, PE>; + type RandomVar = PE; + type PermutationVar = PE; + + #[inline] + fn permutation(&self) -> Self::MP { + self.aux + } + + #[inline] + fn permutation_randomness(&self) -> &[Self::RandomVar] { + self.packed_randomness + } + + #[inline] + fn permutation_values(&self) -> &[Self::PermutationVar] { + self.permutation_values + } +} + +impl<'a, F, EF, P, PE> PeriodicAirBuilder for ProverConstraintFolder<'a, F, EF, P, PE> +where + F: Field, + EF: ExtensionField, + P: PackedField, + PE: Algebra + Algebra

+ BasedVectorSpace

+ Copy + Send + Sync, +{ + type PeriodicVar = P; + + #[inline] + fn periodic_values(&self) -> &[Self::PeriodicVar] { + self.periodic_values + } +} diff --git a/stark/miden-lifted-stark/src/prover/constraints/layout.rs b/stark/miden-lifted-stark/src/prover/constraints/layout.rs new file mode 100644 index 0000000000..43df5c4569 --- /dev/null +++ b/stark/miden-lifted-stark/src/prover/constraints/layout.rs @@ -0,0 +1,166 @@ +//! Constraint layout: maps global constraint indices to base/ext streams. +//! +//! Also provides [`ConstraintLayoutBuilder`], a lightweight AIR builder that discovers +//! constraint types without building symbolic expression trees. + +use alloc::{vec, vec::Vec}; + +use miden_lifted_air::{ + AirBuilder, EmptyWindow, ExtensionBuilder, LiftedAir, PeriodicAirBuilder, + PermutationAirBuilder, + symbolic::{AirLayout, ConstraintLayout}, +}; +use p3_field::{ExtensionField, Field}; +use p3_matrix::dense::RowMajorMatrix; +use tracing::instrument; + +// ============================================================================ +// Constraint Layout Builder (lightweight, no symbolic expressions) +// ============================================================================ + +/// Evaluate the AIR on a lightweight builder and return the constraint layout. +/// +/// Runs `air.eval()` on a [`ConstraintLayoutBuilder`] that uses concrete field zeros +/// for all variables. This discovers which constraints are base-field vs extension-field +/// without building symbolic expression trees — only the emission order matters. +#[instrument(name = "compute constraint layout", skip_all, level = "debug")] +pub fn get_constraint_layout(air: &A) -> ConstraintLayout +where + F: Field, + EF: ExtensionField, + A: LiftedAir, +{ + let mut builder = ConstraintLayoutBuilder::::new(air.air_layout()); + debug_assert!(air.is_valid_builder(&builder).is_ok()); + air.eval(&mut builder); + builder.into_layout() +} + +/// Lightweight AIR builder that only tracks constraint types (base vs extension). +/// +/// Uses concrete field zeros for all variables — no symbolic expression trees, no degree +/// tracking, no `Arc` allocations. Builds a [`ConstraintLayout`] directly by recording +/// which `assert_*` method is called for each constraint. +/// +/// Uses `RowMajorMatrix` as `MainWindow` because the builder owns its trace data. +/// `RowWindow` cannot be used here — it borrows, but the associated type can't +/// capture the `&self` lifetime from `main()`. +struct ConstraintLayoutBuilder { + main: RowMajorMatrix, + public_values: Vec, + periodic_values: Vec, + permutation: RowMajorMatrix, + permutation_challenges: Vec, + permutation_values: Vec, + layout: ConstraintLayout, + constraint_count: usize, +} + +impl ConstraintLayoutBuilder { + fn new(layout: AirLayout) -> Self { + let AirLayout { + main_width, + num_public_values, + permutation_width, + num_permutation_challenges, + num_permutation_values, + num_periodic_columns, + .. + } = layout; + Self { + main: RowMajorMatrix::new(vec![F::ZERO; 2 * main_width], main_width), + public_values: vec![F::ZERO; num_public_values], + periodic_values: vec![F::ZERO; num_periodic_columns], + permutation: RowMajorMatrix::new( + vec![F::ZERO; 2 * permutation_width], + permutation_width, + ), + permutation_challenges: vec![F::ZERO; num_permutation_challenges], + permutation_values: vec![F::ZERO; num_permutation_values], + layout: ConstraintLayout::default(), + constraint_count: 0, + } + } + + fn into_layout(self) -> ConstraintLayout { + self.layout + } +} + +impl AirBuilder for ConstraintLayoutBuilder { + type F = F; + type Expr = F; + type Var = F; + type PreprocessedWindow = EmptyWindow; + type MainWindow = RowMajorMatrix; + type PublicVar = F; + + fn main(&self) -> Self::MainWindow { + self.main.clone() + } + + fn preprocessed(&self) -> &Self::PreprocessedWindow { + EmptyWindow::empty_ref() + } + + fn is_first_row(&self) -> Self::Expr { + F::ZERO + } + + fn is_last_row(&self) -> Self::Expr { + F::ZERO + } + + fn is_transition_window(&self, _size: usize) -> Self::Expr { + F::ZERO + } + + fn assert_zero>(&mut self, _x: I) { + self.layout.base_indices.push(self.constraint_count); + self.constraint_count += 1; + } + + fn public_values(&self) -> &[Self::PublicVar] { + &self.public_values + } +} + +impl ExtensionBuilder for ConstraintLayoutBuilder { + type EF = F; + type ExprEF = F; + type VarEF = F; + + fn assert_zero_ext(&mut self, _x: I) + where + I: Into, + { + self.layout.ext_indices.push(self.constraint_count); + self.constraint_count += 1; + } +} + +impl PermutationAirBuilder for ConstraintLayoutBuilder { + type MP = RowMajorMatrix; + type RandomVar = F; + type PermutationVar = F; + + fn permutation(&self) -> Self::MP { + self.permutation.clone() + } + + fn permutation_randomness(&self) -> &[Self::RandomVar] { + &self.permutation_challenges + } + + fn permutation_values(&self) -> &[Self::PermutationVar] { + &self.permutation_values + } +} + +impl PeriodicAirBuilder for ConstraintLayoutBuilder { + type PeriodicVar = F; + + fn periodic_values(&self) -> &[Self::PeriodicVar] { + &self.periodic_values + } +} diff --git a/stark/miden-lifted-stark/src/prover/constraints/mod.rs b/stark/miden-lifted-stark/src/prover/constraints/mod.rs new file mode 100644 index 0000000000..6cbfa864ef --- /dev/null +++ b/stark/miden-lifted-stark/src/prover/constraints/mod.rs @@ -0,0 +1,212 @@ +//! Constraint evaluation for the prover. +//! +//! - `evaluate_constraints_into`: SIMD-parallel constraint evaluation on the quotient domain +//! - `folder`: SIMD-optimized constraint folder and finalization +//! - `layout`: Constraint layout discovery (base vs extension) and alpha decomposition + +mod folder; +pub(crate) mod layout; +mod packed_row_bitrev; + +use alloc::vec::Vec; +use core::marker::PhantomData; + +use folder::ProverConstraintFolder; +use miden_lifted_air::{LiftedAir, RowWindow, symbolic::ConstraintLayout}; +use p3_field::{ + Algebra, BasedVectorSpace, ExtensionField, Field, PackedFieldExtension, PackedValue, + TwoAdicField, +}; +use p3_matrix::{Matrix, bitrev::BitReversedMatrixView, dense::RowMajorMatrixView}; +use p3_maybe_rayon::prelude::*; +use packed_row_bitrev::RowMajorMatrixBitrevPackedExt; + +use crate::{coset::LiftedCoset, prover::periodic::PeriodicLde}; + +/// Row-blocks (`i_start = r * packing_width`) processed per rayon task. +const ROW_BLOCKS_PER_PARALLEL_TASK: usize = 32; + +/// Type alias for packed base field from F. +type PackedVal = ::Packing; + +/// Type alias for packed extension field from EF. +type PackedExt = >::ExtensionPacking; + +/// Evaluate constraints on the quotient domain, adding results into `output`. +/// +/// Here `gJ` is the quotient evaluation coset of size `N * D`, the subset of the +/// committed LDE coset `gK` (size `N * B`) that contains just enough points to +/// evaluate the quotient point-wise. For each point on `gJ`, we evaluate all AIR +/// constraints, fold them with powers of `alpha`, and add the resulting numerator value: +/// +/// `output[i] += folded_constraints(xᵢ)`. +/// +/// The caller is responsible for preparing `output` before calling this function +/// (e.g. cyclically extending and scaling by beta for multi-trace accumulation). +/// Trace views must be [`BitReversedMatrixView`] over dense row-major storage (as returned by +/// [`crate::prover::commit::Committed::evals_on_quotient_domain`]), in natural order on gJ. +/// +/// Uses SIMD-packed parallel iteration via rayon for optimal performance: +/// - Processes `WIDTH` points simultaneously using packed field types +/// - Main trace stays in base field, only aux trace uses extension field +/// - Constraints are collected then finalized in batches via decomposed alpha powers +/// +/// Why we fold with `alpha`: the prover does not want to carry K separate constraint +/// polynomials through the rest of the protocol. A random linear combination +/// +/// `C_fold(x) = Σₖ α^{K−1−k}·Cₖ(x)` +/// +/// collapses them into one numerator polynomial while preserving soundness (a non-zero +/// constraint survives with high probability). +/// +/// Why we only evaluate on `gJ`: `gJ` (size `N * D`) is a subset of the committed LDE +/// coset `gK` (size `N * B`). For `B >= D`, these `N * D` points are sufficient for +/// the quotient-degree bounds used by the protocol; division by the vanishing polynomial +/// happens later. +#[allow(clippy::too_many_arguments)] +pub fn evaluate_constraints_into( + output: &mut [EF], + air: &A, + main_on_gj: &BitReversedMatrixView>, + aux_on_gj: &BitReversedMatrixView>, + coset: &LiftedCoset, + alpha: EF, + randomness: &[EF], + public_values: &[F], + periodic_lde: &PeriodicLde, + layout: &ConstraintLayout, + permutation_values: &[EF], +) where + F: TwoAdicField, + EF: ExtensionField, + PackedExt: Algebra + Algebra> + BasedVectorSpace>, + A: LiftedAir, +{ + type P = PackedVal; + type PE = PackedExt; + + let gj_height = coset.lde_height(); + assert_eq!(output.len(), gj_height); + let constraint_degree = coset.blowup(); + let width = P::::WIDTH; + + assert_eq!(gj_height % width, 0, "quotient height must be divisible by packing width"); + + // Precompute selectors via coset method + let sels = coset.selectors::(); + + // ─── Decompose alpha powers by constraint layout ─── + let aux_ef_width = air.aux_width(); + let constraint_count = layout.total_constraints(); + let base_count = layout.base_indices.len(); + let ext_count = layout.ext_indices.len(); + let (base_alpha_powers, ext_alpha_powers) = layout.decompose_alpha(alpha); + + // Main trace width + let main_width = main_on_gj.width(); + + // Pack randomness for aux trace + let packed_randomness: Vec> = randomness.iter().copied().map(Into::into).collect(); + + // Pack permutation values + let packed_perm_values: Vec> = + permutation_values.iter().copied().map(Into::into).collect(); + + let main_vals = main_on_gj.inner.values; + let aux_vals = aux_on_gj.inner.values; + let aux_scalar_width = aux_on_gj.width(); + let main_trace_view = RowMajorMatrixView::new(main_vals, main_width); + let aux_trace_view = RowMajorMatrixView::new(aux_vals, aux_scalar_width); + + let points_per_task = width * ROW_BLOCKS_PER_PARALLEL_TASK; + + let eval_big_slice = |main_buf: &mut Vec>, + aux_base_buf: &mut Vec>, + aux_pe_buf: &mut Vec>, + g: usize, + big_slice: &mut [EF]| { + for (sub_r, chunk) in big_slice.chunks_exact_mut(width).enumerate() { + let r = g * ROW_BLOCKS_PER_PARALLEL_TASK + sub_r; + let i_start = r * width; + + // Extract packed selectors from precomputed vectors + let selectors = sels.packed_at::>(i_start); + + // Get main trace as packed row pair (stays in base field) + main_trace_view.collect_vertically_packed_row_pair_bitrev_into( + i_start, + constraint_degree, + main_buf, + ); + let main_mat = RowMajorMatrixView::new(main_buf.as_slice(), main_width); + + // Get aux trace as packed row pair and convert to packed extension field + aux_trace_view.collect_vertically_packed_row_pair_bitrev_into( + i_start, + constraint_degree, + aux_base_buf, + ); + + // Convert from packed base field to packed extension field + // Each EF element is formed from DIMENSION consecutive base field elements + aux_pe_buf.clear(); + aux_pe_buf.reserve(aux_ef_width * 2); + for i in 0..aux_ef_width * 2 { + aux_pe_buf.push(PE::::from_basis_coefficients_fn(|j| { + aux_base_buf[i * EF::DIMENSION + j] + })); + } + let aux_mat = RowMajorMatrixView::new(aux_pe_buf.as_slice(), aux_ef_width); + + // Get packed periodic values + let periodic_values: Vec> = periodic_lde.packed_values_at(i_start).collect(); + + // Build packed folder and evaluate constraints + let mut folder: ProverConstraintFolder<'_, F, EF, P, PE> = + ProverConstraintFolder { + main: RowWindow::from_view(&main_mat), + aux: RowWindow::from_view(&aux_mat), + packed_randomness: &packed_randomness, + public_values, + periodic_values: &periodic_values, + permutation_values: &packed_perm_values, + selectors, + base_alpha_powers: &base_alpha_powers, + ext_alpha_powers: &ext_alpha_powers, + constraint_index: 0, + constraint_count, + base_constraints: Vec::with_capacity(base_count), + ext_constraints: Vec::with_capacity(ext_count), + _phantom: PhantomData, + }; + + #[cfg(debug_assertions)] + air.is_valid_builder(&folder).expect("builder dimensions must match AIR"); + air.eval(&mut folder); + let folded = folder.finalize_constraints(); + + // Unpack folded result and add scalars directly into the output chunk. + for (slot, val) in chunk.iter_mut().zip(PE::::to_ext_iter([folded])) { + *slot += val; + } + } + }; + + #[cfg(feature = "parallel")] + output.par_chunks_mut(points_per_task).enumerate().for_each_init( + || (Vec::>::new(), Vec::>::new(), Vec::>::new()), + |(main_buf, aux_base_buf, aux_pe_buf), (g, big_slice)| { + eval_big_slice(main_buf, aux_base_buf, aux_pe_buf, g, big_slice); + }, + ); + + #[cfg(not(feature = "parallel"))] + { + let mut main_buf = Vec::>::new(); + let mut aux_base_buf = Vec::>::new(); + let mut aux_pe_buf = Vec::>::new(); + output.par_chunks_mut(points_per_task).enumerate().for_each(|(g, big_slice)| { + eval_big_slice(&mut main_buf, &mut aux_base_buf, &mut aux_pe_buf, g, big_slice); + }); + } +} diff --git a/stark/miden-lifted-stark/src/prover/constraints/packed_row_bitrev.rs b/stark/miden-lifted-stark/src/prover/constraints/packed_row_bitrev.rs new file mode 100644 index 0000000000..20bda82d2d --- /dev/null +++ b/stark/miden-lifted-stark/src/prover/constraints/packed_row_bitrev.rs @@ -0,0 +1,94 @@ +//! Vertical SIMD view from row-major storage whose **physical** row order is bit-reversed +//! relative to the logical quotient-domain row index (same addressing as +//! `Matrix::vertically_packed_row_pair` on natural-order storage, but with `reverse_bits_len`). + +use alloc::vec::Vec; + +use p3_field::PackedValue; +use p3_matrix::dense::RowMajorMatrixView; +use p3_util::{log2_strict_usize, reverse_bits_len}; + +/// Collect logical vertically packed rows from bit-reversed row-major storage into a reusable +/// buffer. +pub trait RowMajorMatrixBitrevPackedExt { + /// One logical row block starting at logical row index `i_start` (a multiple of `P::WIDTH`). + #[expect(dead_code)] + fn collect_vertically_packed_row_bitrev_into>( + &self, + i_start: usize, + out: &mut Vec

, + ); + + /// Two logical row blocks: rows `i_start` and `i_start + step` (mod height), packed like + /// `Matrix::vertically_packed_row_pair`. + fn collect_vertically_packed_row_pair_bitrev_into>( + &self, + i_start: usize, + step: usize, + out: &mut Vec

, + ); +} + +impl<'a, F: Copy> RowMajorMatrixBitrevPackedExt for RowMajorMatrixView<'a, F> { + fn collect_vertically_packed_row_bitrev_into>( + &self, + i_start: usize, + out: &mut Vec

, + ) { + let values = self.values; + let width = self.width; + let height = values.len() / width; + let log_h = log2_strict_usize(height); + debug_assert_eq!(1usize << log_h, height); + + const MAX_WIDTH: usize = 16; + const { + debug_assert!(P::WIDTH <= MAX_WIDTH); + } + + let mut cur_off = [0usize; MAX_WIDTH]; + for (lane_idx, lane) in cur_off.iter_mut().enumerate().take(P::WIDTH) { + *lane = reverse_bits_len((i_start + lane_idx) % height, log_h) * width; + } + + out.clear(); + out.reserve(width); + for c in 0..width { + out.push(P::from_fn(|lane| values[cur_off[lane] + c])); + } + } + + fn collect_vertically_packed_row_pair_bitrev_into>( + &self, + i_start: usize, + step: usize, + out: &mut Vec

, + ) { + let values = self.values; + let width = self.width; + let height = values.len() / width; + let log_h = log2_strict_usize(height); + debug_assert_eq!(1usize << log_h, height); + + const MAX_WIDTH: usize = 16; + const { + debug_assert!(P::WIDTH <= MAX_WIDTH); + } + + let mut cur_off = [0usize; MAX_WIDTH]; + let mut nxt_off = [0usize; MAX_WIDTH]; + for lane in 0..P::WIDTH { + cur_off[lane] = reverse_bits_len((i_start + lane) % height, log_h) * width; + nxt_off[lane] = reverse_bits_len((i_start + step + lane) % height, log_h) * width; + } + + out.clear(); + out.reserve(2 * width); + for c in 0..width { + out.push(P::from_fn(|lane| values[cur_off[lane] + c])); + } + for c in 0..width { + out.push(P::from_fn(|lane| values[nxt_off[lane] + c])); + } + } +} diff --git a/stark/miden-lifted-stark/src/prover/mod.rs b/stark/miden-lifted-stark/src/prover/mod.rs new file mode 100644 index 0000000000..57ffb65118 --- /dev/null +++ b/stark/miden-lifted-stark/src/prover/mod.rs @@ -0,0 +1,417 @@ +//! Lifted STARK prover. +//! +//! This module provides: +//! - [`prove_single`]: Prove a single AIR instance. +//! - [`prove_multi`]: Prove multiple AIR instances with traces of different heights. +//! +//! These functions write the proof into a [`miden_stark_transcript::ProverChannel`] +//! (commitments, grinding witnesses, and openings). +//! +//! # Fiat-Shamir / transcript binding (initial challenger state) +//! +//! This crate does **not** prescribe the *initial* transcript state. The caller +//! must bind the full statement into the Fiat-Shamir challenger before calling +//! [`prove_multi`]. Both prover and verifier must produce identical challenger +//! states. Concretely, the caller **MUST** observe: +//! +//! 1. **Protocol parameters** — e.g. the STARK configuration, blowup factor, and any +//! application-level domain separator. +//! +//! 2. **Public values and variable-length inputs** — `public_values` and `var_len_public_inputs` +//! for every instance. Without this, Fiat-Shamir challenges are independent of the statement. +//! +//! 3. **AIR configurations and `air_order`** — The proof defines an ordering of AIR instances +//! (`air_order()[j]` is the caller's original index at proof position `j`), queryable via +//! [`InstanceShapes::air_order`]. The ordering is deterministic: instances are sorted by +//! `(log_trace_height, caller_index)`. Neither the AIR configurations nor `air_order` are +//! absorbed into the transcript, so the caller must bind both into the challenger. How this is +//! done is up to the caller — see the examples below. The prover can precompute `air_order` via +//! [`InstanceShapes::from_trace_heights`]; the verifier reads it from the proof. +//! +//! ## Recommended pattern +//! +//! Pre-seed the challenger so statement data stays out of the proof: +//! +//! ```ignore +//! // --- Bind statement into Fiat-Shamir --- +//! let mut ch = Challenger::new(perm.clone()); +//! ch.observe_slice(&b"MY_APP_V1".map(|b| F::from_u8(b))); // domain separator +//! ch.observe(F::from_u8(config.pcs().log_blowup())); // protocol parameters +//! // ... observe remaining protocol parameters ... +//! ch.observe_slice(&public_values); +//! for vl in &var_len_public_inputs { +//! ch.observe_slice(vl); +//! } +//! // For multi-AIR: bind AIR configurations and air_order (see below). +//! +//! // --- Prove --- +//! let output = prove_multi(&config, &instances, ch)?; +//! +//! // --- Verify (identical binding) --- +//! let mut ch = Challenger::new(perm); +//! ch.observe_slice(&b"MY_APP_V1".map(|b| F::from_u8(b))); +//! ch.observe(F::from_u8(config.pcs().log_blowup())); +//! // ... observe remaining protocol parameters ... +//! ch.observe_slice(&public_values); +//! for vl in &var_len_public_inputs { +//! ch.observe_slice(vl); +//! } +//! let verifier_digest = verify_multi(&config, &verifier_instances, &output.proof, ch)?; +//! assert_eq!(output.digest, verifier_digest); +//! ``` +//! +//! ## Multi-AIR binding examples +//! +//! ```text +//! // Prover: precompute air_order before building the challenger. +//! let shapes = InstanceShapes::from_trace_heights(trace_heights)?; +//! let air_order = shapes.air_order(); +//! +//! // Verifier: read air_order from the proof. +//! let air_order = proof.air_order(); +//! +//! // Option A: reorder AIRs to proof order and commit — the ordering is +//! // implicit in the commitment. +//! let ordered_airs: Vec<_> = air_order.iter().map(|&idx| &airs[idx as usize]).collect(); +//! let circuit = Circuit::from_airs(&ordered_airs); +//! challenger.observe(circuit.commitment()); +//! +//! // Option B: commit to AIRs in their natural order, then observe +//! // air_order to bind the ordering explicitly. +//! for air in &airs { +//! challenger.observe(air.commitment()); +//! } +//! challenger.observe_slice(air_order); +//! ``` + +extern crate alloc; + +pub mod commit; +pub mod constraints; +pub mod periodic; +pub mod quotient; + +use alloc::{vec, vec::Vec}; + +use commit::commit_traces; +use constraints::{evaluate_constraints_into, layout::get_constraint_layout}; +use miden_lifted_air::{AuxBuilder, LiftedAir, VarLenPublicInputs, log2_strict_u8}; +use miden_stark_transcript::{Channel, ProverChannel, ProverTranscript}; +use p3_field::{BasedVectorSpace, ExtensionField, TwoAdicField}; +use p3_matrix::{Matrix, dense::RowMajorMatrix}; +use periodic::PeriodicLde; +use thiserror::Error; +use tracing::{info_span, instrument}; + +use crate::{ + StarkConfig, + coset::LiftedCoset, + instance::{AirWitness, InstanceShapes, InstanceValidationError, validate_inputs}, + pcs::prover::open_with_channel, + proof::{StarkOutput, StarkProof}, +}; + +/// Errors that can occur during proving. +#[derive(Debug, Error)] +pub enum ProverError { + #[error("instance validation failed: {0}")] + Instance(#[from] InstanceValidationError), + #[error( + "constraint degree exceeds blowup: \ + log_quotient_degree {log_quotient_degree} > log_blowup {log_blowup}" + )] + ConstraintDegreeTooHigh { log_quotient_degree: u8, log_blowup: u8 }, +} + +/// Prove a single AIR. +/// +/// The caller's challenger must already be bound to the full statement +/// (protocol parameters, AIR configuration, public values, and +/// variable-length inputs) — see the module-level docs. +/// +/// This is a convenience wrapper around [`prove_multi`] for the single-AIR case. +/// +/// # Returns +/// `Ok(StarkOutput { digest, proof })` on success, or a `ProverError` if validation fails. +pub fn prove_single( + config: &SC, + air: &A, + trace: &RowMajorMatrix, + public_values: &[F], + var_len_public_inputs: VarLenPublicInputs<'_, F>, + aux_builder: &B, + challenger: SC::Challenger, +) -> Result, ProverError> +where + F: TwoAdicField, + EF: ExtensionField, + SC: StarkConfig, + A: LiftedAir, + B: AuxBuilder, +{ + let witness = AirWitness::new(trace, public_values, var_len_public_inputs); + prove_multi(config, &[(air, witness, aux_builder)], challenger) +} + +/// Prove multiple AIRs with traces of different heights. +/// +/// The caller's challenger must already be bound to the full statement +/// (protocol parameters, AIR configurations, AIR ordering, and public +/// inputs — both fixed and variable-length) — see the module-level docs. +/// +/// # Arguments +/// - `config`: STARK configuration (PCS params, LMCS, DFT) +/// - `instances`: Pairs of (AIR, witness, aux_builder) +/// - `challenger`: Fiat-Shamir challenger (heights are observed before use) +/// +/// # Returns +/// `Ok(StarkOutput { digest, proof })` on success, or a `ProverError` if validation fails. +#[instrument(name = "prove", skip_all)] +pub fn prove_multi( + config: &SC, + instances: &[(&A, AirWitness<'_, F>, &B)], + mut challenger: SC::Challenger, +) -> Result, ProverError> +where + F: TwoAdicField, + EF: ExtensionField, + SC: StarkConfig, + A: LiftedAir, + B: AuxBuilder, +{ + let trace_heights: Vec = instances.iter().map(|(_, w, _)| w.trace.height()).collect(); + let instance_shapes = InstanceShapes::from_trace_heights(trace_heights)?; + + // Reorder instances to the proof's AIR ordering. + let instances = instance_shapes.reorder(instances.to_vec())?; + + let verifier_instances: Vec<_> = + instances.iter().map(|(air, w, _)| (*air, w.to_instance())).collect(); + + let log_blowup = config.pcs().log_blowup(); + + // Validate AIR structure, instance dimensions, heights, and trace widths. + let log_max_trace_height = validate_inputs(&verifier_instances, &instance_shapes, log_blowup)?; + for &(air, w, _) in &instances { + if w.trace.width() != air.width() { + return Err(InstanceValidationError::WidthMismatch { + expected: air.width(), + actual: w.trace.width(), + } + .into()); + } + } + + // Observe shape metadata before creating the transcript. + instance_shapes.observe_heights::(&mut challenger); + + let mut channel = ProverTranscript::new(challenger); + + // Clear the challenger's absorb buffer after observing instance shapes by + // squeezing a throwaway extension element. This guarantees later sampled + // challenges depend on all prior inputs regardless of sponge state. + let _instance_challenge: EF = channel.sample_algebra_element::(); + + // Infer constraint degree from symbolic AIR analysis (max across all AIRs) + let log_constraint_degree = + instances.iter().map(|(air, ..)| air.log_quotient_degree()).max().unwrap_or(1) as u8; + + if log_constraint_degree > log_blowup { + return Err(ProverError::ConstraintDegreeTooHigh { + log_quotient_degree: log_constraint_degree, + log_blowup, + }); + } + + let log_lde_height = log_max_trace_height + log_blowup; + + // Max LDE coset (for the largest trace, no lifting) + let max_lde_coset = LiftedCoset::unlifted(log_max_trace_height, log_blowup); + let max_quotient_coset = max_lde_coset.quotient_domain(log_constraint_degree); + let max_quotient_height = max_quotient_coset.lde_height(); + + // 1. Commit all main traces (trace order — ascending height). + // + // Clone with blowup × capacity so the DFT resize doesn't reallocate. + let blowup = 1 << log_blowup as usize; + let main_traces: Vec<_> = instances + .iter() + .map(|(_, w, _)| { + let src = &w.trace.values; + let mut values = Vec::with_capacity(src.len() * blowup); + values.extend_from_slice(src); + RowMajorMatrix::new(values, w.trace.width()) + }) + .collect(); + let main_committed = + info_span!("commit to main traces").in_scope(|| commit_traces(config, main_traces)); + channel.send_commitment(main_committed.root()); + + // 2. Sample randomness and build aux traces for all AIRs + let max_num_randomness = + instances.iter().map(|(air, ..)| air.num_randomness()).max().unwrap_or(0); + + let randomness: Vec = (0..max_num_randomness) + .map(|_| channel.sample_algebra_element::()) + .collect(); + + // Build aux traces via AuxBuilder + let (aux_traces_ef, all_aux_values): (Vec>, Vec>) = + info_span!("build aux traces").in_scope(|| { + let mut traces = Vec::with_capacity(instances.len()); + let mut values = Vec::with_capacity(instances.len()); + for (air, w, aux_builder) in &instances { + let num_rand = air.num_randomness(); + let (aux, aux_vals) = aux_builder.build_aux_trace(w.trace, &randomness[..num_rand]); + + assert_eq!(aux.width(), air.aux_width(), "aux trace width mismatch"); + assert_eq!( + aux_vals.len(), + air.num_aux_values(), + "aux values length mismatch: build_aux_trace returned {} values, \ + but num_aux_values() is {}", + aux_vals.len(), + air.num_aux_values() + ); + assert_eq!(aux.height(), w.trace.height()); + traces.push(aux); + values.push(aux_vals); + } + (traces, values) + }); + + // Flatten EF -> F and commit aux traces + let aux_traces: Vec> = aux_traces_ef + .into_iter() + .map(|aux| { + let base_width = aux.width() * EF::DIMENSION; + let base_values = >::flatten_to_base(aux.values); + RowMajorMatrix::new(base_values, base_width) + }) + .collect(); + + let aux_committed = + info_span!("commit to aux traces").in_scope(|| commit_traces(config, aux_traces)); + channel.send_commitment(aux_committed.root()); + + // Observe aux values into the transcript (binds to Fiat-Shamir state). + // When no AIR has aux columns, each entry is empty so nothing is sent. + for vals in &all_aux_values { + for &val in vals { + channel.send_algebra_element(val); + } + } + + // 4. Sample constraint folding alpha and accumulation beta + let alpha: EF = channel.sample_algebra_element::(); + let beta: EF = channel.sample_algebra_element::(); + + // 5. Evaluate constraints and accumulate with beta folding. + // + // Single accumulator, processed in trace order (ascending height): + // 1. Cyclically extend accumulator to the next quotient height + // 2. Multiply every element by beta (Horner) + // 3. Add constraint evaluations in-place: acc[i] += eval(i) + // + // Pre-allocate with LDE capacity so commit_quotient's resize doesn't reallocate. + let constraint_degree = 1 << log_constraint_degree as usize; + let mut accumulator: Vec = Vec::with_capacity(max_quotient_height * blowup); + + // Pre-compute constraint layouts for each AIR (base/ext index mapping) + let layouts: Vec<_> = instances + .iter() + .map(|(air, ..)| get_constraint_layout::(*air)) + .collect(); + + info_span!("evaluate constraints").in_scope(|| { + for (i, (air, w, _)) in instances.iter().enumerate() { + let trace_height = w.trace.height(); + let log_trace_height = log2_strict_u8(trace_height); + + // Create LiftedCoset for this trace (may be lifted relative to max) + let this_lde_coset = + LiftedCoset::new(log_trace_height, log_blowup, log_max_trace_height); + let this_quotient_coset = this_lde_coset.quotient_domain(log_constraint_degree); + let this_quotient_height = this_quotient_coset.lde_height(); + + // Truncate the committed LDE to the quotient evaluation domain gJ (size N·D). + // Since B ≥ D, the committed LDE on gK (size N·B) contains gJ as a prefix in + // bit-reversed storage, so this is a zero-copy view. + let main_on_gj = main_committed.evals_on_quotient_domain(i, constraint_degree); + let aux_on_gj = aux_committed.evals_on_quotient_domain(i, constraint_degree); + + // Build periodic LDE for this trace via coset method + let periodic_lde = + PeriodicLde::build(&this_quotient_coset, air.periodic_columns_matrix()); + + // Cyclically extend accumulator to this quotient height and scale by beta. + // On the first iteration the accumulator is empty, so this is a no-op + // and evaluate_constraints_into writes into a zero-filled buffer. + tracing::debug_span!( + "cyclic_extend", + acc_len = accumulator.len(), + target = this_quotient_height + ) + .in_scope(|| { + quotient::cyclic_extend_and_scale(&mut accumulator, this_quotient_height, beta); + }); + + let aux_values_i = &all_aux_values[i]; + + // Add constraint evaluations in-place: accumulator[i] += eval(i) + info_span!("eval_instance", instance = i, height = this_quotient_height).in_scope( + || { + evaluate_constraints_into::( + &mut accumulator, + *air, + &main_on_gj, + &aux_on_gj, + &this_quotient_coset, + alpha, + &randomness[..air.num_randomness()], + w.public_values, + &periodic_lde, + &layouts[i], + aux_values_i, + ); + }, + ); + } + }); + + // Verify we have the expected size (max quotient domain) + assert_eq!(accumulator.len(), max_quotient_height); + + // 6. Divide by vanishing polynomial once on full gJ (in-place) + tracing::debug_span!("divide_by_vanishing", height = max_quotient_height).in_scope(|| { + quotient::divide_by_vanishing_in_place::(&mut accumulator, &max_quotient_coset); + }); + + // 7. Commit quotient + let quotient_committed = info_span!("commit to quotient poly chunks") + .in_scope(|| quotient::commit_quotient(config, accumulator, &max_lde_coset)); + channel.send_commitment(quotient_committed.root()); + + // 8. Sample OOD point (outside H and gK) + let z: EF = max_lde_coset.sample_ood_point(&mut channel); + let h = F::two_adic_generator(log_max_trace_height.into()); + let z_next = z * h; + + // 9. Open via PCS + let trees = vec![main_committed.tree(), aux_committed.tree(), quotient_committed.tree()]; + + info_span!("open").in_scope(|| { + open_with_channel::, _, 2>( + config.pcs(), + config.lmcs(), + log_lde_height, + [z, z_next], + &trees, + &mut channel, + ) + }); + + let (digest, transcript) = channel.finalize(); + let proof = StarkProof { instance_shapes, transcript }; + Ok(StarkOutput { digest, proof }) +} diff --git a/stark/miden-lifted-stark/src/prover/periodic.rs b/stark/miden-lifted-stark/src/prover/periodic.rs new file mode 100644 index 0000000000..d6ae065ff3 --- /dev/null +++ b/stark/miden-lifted-stark/src/prover/periodic.rs @@ -0,0 +1,204 @@ +//! Prover-side periodic column handling. +//! +//! Periodic columns are stored as LDE values in a row-major matrix for efficient +//! constraint evaluation on the LDE domain. The key optimization is that a periodic +//! column with period `p` only needs `p * blowup` LDE values (not `trace_height * blowup`), +//! which are accessed via modular indexing. +//! +//! Uses NaiveDft since periodic column periods are typically small. + +use miden_lifted_air::log2_strict_u8; +use p3_dft::{NaiveDft, TwoAdicSubgroupDft}; +use p3_field::{PackedValue, TwoAdicField}; +use p3_matrix::{Matrix, dense::RowMajorMatrix}; + +use crate::coset::LiftedCoset; + +/// Prover-side periodic LDE values for constraint evaluation. +/// +/// Stores precomputed LDE values as a row-major matrix in natural order. The key insight +/// is that by repeating each column's values to the maximum period, we can use batch DFT +/// methods and store only `max_period * blowup` rows instead of `trace_height * blowup`. +/// +/// A periodic column of period `p` repeats every `p` rows on the trace domain, so its LDE +/// repeats every `p * blowup` rows on the quotient/LDE domains. We therefore only need to +/// store `p * blowup` rows for that column. To share one buffer across many periodic columns, +/// we repeat each column up to `max_period` and LDE-extend once; columns with smaller periods +/// are accessed via modular indexing. +#[derive(Clone, Debug)] +pub struct PeriodicLde { + /// LDE values in natural order (height = max_period * blowup). + /// `None` when there are no periodic columns. + ldes: Option>, +} + +impl PeriodicLde { + /// Build periodic LDEs from a periodic column matrix. + /// + /// Takes the output of [`crate::air::LiftedAir::periodic_columns_matrix`], where + /// columns with smaller periods have been repeated cyclically to the maximum period. + /// Uses NaiveDft since periodic column periods are typically small. + /// + /// # Arguments + /// - `coset`: The lifted coset providing domain information + /// - `repeated_matrix`: Periodic columns extended to a common height (max period), or `None` if + /// there are no periodic columns + /// + /// # Panics + /// Panics if the matrix height exceeds the trace height or is not a power of two. + pub fn build(coset: &LiftedCoset, repeated_matrix: Option>) -> Self { + let Some(repeated_matrix) = repeated_matrix else { + return Self { ldes: None }; + }; + + let max_period = repeated_matrix.height(); + let log_max_period = log2_strict_u8(max_period); + assert!( + coset.log_trace_height >= log_max_period, + "periodic column period ({max_period}) exceeds trace height ({})", + 1 << coset.log_trace_height as usize, + ); + let log_blowup = coset.log_blowup(); + + // Compute the coset shift for the max-period subgroup. + // + // Periodic polynomials are naturally defined on a subgroup of order `max_period`. + // We derive the corresponding coset shift by taking the lifted coset shift + // gʳ and mapping from trace height down to `max_period` via a power-of-two ratio. + let log_ratio = coset.log_trace_height - log_max_period; + let period_shift: F = coset.lde_shift::().exp_power_of_2(log_ratio as usize); + + // Compute LDE using NaiveDft (periods are small) + let ldes = NaiveDft + .coset_lde_batch(repeated_matrix, log_blowup, period_shift) + .to_row_major_matrix(); + + Self { ldes: Some(ldes) } + } + + /// Get packed values for consecutive natural indices [i, i+1, ..., i+WIDTH-1]. + /// + /// Returns an empty iterator when there are no periodic columns. + #[inline] + pub fn packed_values_at>( + &self, + i: usize, + ) -> impl Iterator + '_ { + self.ldes.iter().flat_map(move |ldes| { + let height = ldes.height(); + (0..ldes.width()).map(move |col| { + P::from_fn(|k| { + let row = (i + k) % height; + // SAFETY: `row < height` is guaranteed by the modulo operation, + // and `col < width` is guaranteed by the iterator bounds (0..ldes.width()). + unsafe { ldes.get_unchecked(row, col) } + }) + }) + }) + } +} + +#[cfg(test)] +mod tests { + extern crate alloc; + + use alloc::{vec, vec::Vec}; + + use p3_dft::TwoAdicSubgroupDft; + use p3_field::{Field, PackedValue, PrimeCharacteristicRing}; + + use super::*; + use crate::testing::configs::goldilocks_poseidon2 as gl; + + /// Verify that periodic LDE values match the full LDE computation. + fn assert_periodic_lde_matches_full( + columns: &[Vec], + log_trace_height: u8, + log_blowup: u8, + ) { + let trace_height = 1 << log_trace_height as usize; + let lde_height = trace_height << log_blowup as usize; + + // Create a coset at max height (no lifting) + let coset = LiftedCoset::unlifted(log_trace_height, log_blowup); + + // Build the repeated matrix (same logic as periodic_columns_matrix) + let max_period = columns.iter().map(Vec::len).max().unwrap(); + let num_cols = columns.len(); + let mut values = Vec::with_capacity(max_period * num_cols); + for row in 0..max_period { + for col in columns { + values.push(col[row % col.len()]); + } + } + let repeated_matrix = RowMajorMatrix::new(values, num_cols); + + let periodic_lde = PeriodicLde::build(&coset, Some(repeated_matrix)); + + // Compute expected LDE for each column via full expansion (natural order) + let expected: Vec> = columns + .iter() + .map(|col| { + let full: Vec = (0..trace_height).map(|i| col[i % col.len()]).collect(); + let matrix = RowMajorMatrix::new(full, 1); + NaiveDft + .coset_lde_batch(matrix, log_blowup.into(), gl::Felt::GENERATOR) + .to_row_major_matrix() + .values + }) + .collect(); + + // Verify all LDE rows match (natural indices) + let ldes = periodic_lde.ldes.as_ref().expect("expected Some for non-empty columns"); + let height = ldes.height(); + for i in 0..lde_height { + let row = i % height; + let actual: Vec = ldes.row_slice(row).unwrap().to_vec(); + for (col_idx, (&actual_val, expected_col)) in actual.iter().zip(&expected).enumerate() { + assert_eq!(actual_val, expected_col[i], "col {col_idx} mismatch at row {i}"); + } + } + + // Verify packed_values_at returns correct packed values + type P = gl::PackedFelt; + let pack_width = P::WIDTH; + for start in (0..lde_height).step_by(pack_width) { + let packed: Vec

= periodic_lde.packed_values_at(start).collect(); + assert_eq!(packed.len(), columns.len()); + + // Verify each lane matches scalar access + for k in 0..pack_width { + let idx = start + k; + let row = idx % height; + let scalar: Vec = ldes.row_slice(row).unwrap().to_vec(); + for (col_idx, (&packed_val, &scalar_val)) in packed.iter().zip(&scalar).enumerate() + { + assert_eq!( + packed_val.as_slice()[k], + scalar_val, + "packed mismatch col {col_idx} row {idx} lane {k}" + ); + } + } + } + } + + #[test] + fn test_periodic_lde_matches_full_lde() { + // Period 2, blowup 2 + assert_periodic_lde_matches_full(&[vec![gl::Felt::ZERO, gl::Felt::ONE]], 3, 1); + + // Period 4, blowup 2 + let col4: Vec = [1, 2, 3, 4].into_iter().map(gl::Felt::from_u64).collect(); + assert_periodic_lde_matches_full(&[col4], 3, 1); + + // Period 2, blowup 8 (higher blowup) + let col2: Vec = [5, 7].into_iter().map(gl::Felt::from_u64).collect(); + assert_periodic_lde_matches_full(&[col2], 4, 3); + + // Multiple columns with different periods + let col_p2: Vec = [1, 2].into_iter().map(gl::Felt::from_u64).collect(); + let col_p4: Vec = [10, 20, 30, 40].into_iter().map(gl::Felt::from_u64).collect(); + assert_periodic_lde_matches_full(&[col_p2, col_p4], 3, 2); + } +} diff --git a/stark/miden-lifted-stark/src/prover/quotient.rs b/stark/miden-lifted-stark/src/prover/quotient.rs new file mode 100644 index 0000000000..e994b294d6 --- /dev/null +++ b/stark/miden-lifted-stark/src/prover/quotient.rs @@ -0,0 +1,238 @@ +//! Quotient polynomial helpers: accumulation, vanishing division, decomposition. +//! +//! The prover orchestrates the quotient pipeline (loop over instances, accumulate, +//! divide, commit). This module provides the building blocks: +//! +//! - [`cyclic_extend_and_scale`]: Horner-style beta scaling + cyclic extension +//! - [`divide_by_vanishing_in_place`]: Divide by Z_H on the quotient evaluation domain +//! - [`commit_quotient`]: Decompose Q(gJ) into chunks and commit on gK + +use alloc::{format, vec, vec::Vec}; + +use p3_dft::TwoAdicSubgroupDft; +use p3_field::{ + BasedVectorSpace, ExtensionField, Field, TwoAdicField, batch_multiplicative_inverse, +}; +use p3_matrix::dense::RowMajorMatrix; +use p3_maybe_rayon::prelude::*; +use p3_util::log2_strict_usize; +use tracing::info_span; + +use crate::{ + StarkConfig, + coset::LiftedCoset, + lmcs::{Lmcs, bitrev::materialize_bitrev}, + prover::commit::Committed, +}; + +// ============================================================================ +// Accumulation +// ============================================================================ + +/// Cyclically extend the accumulator to `target_len` and scale every element by `β`. +/// +/// On the first call (empty accumulator) this simply zero-fills to `target_len`. +/// On subsequent calls it scales the existing buffer by `β` (Horner folding) +/// then doubles via `extend_from_within` until it reaches `target_len`. +/// +/// Both `accumulator.len()` and `target_len` must be powers of two, and +/// `target_len ≥ accumulator.len()`. +/// +/// Cyclic extension is valid because H_small is a subgroup of H_big, so +/// evaluations repeat cyclically. The β scaling implements Horner folding for +/// multi-trace accumulation: `acc = acc·β + Nⱼ`. +pub fn cyclic_extend_and_scale(accumulator: &mut Vec, target_len: usize, beta: EF) { + if accumulator.is_empty() { + accumulator.resize(target_len, EF::ZERO); + } else { + // Horner: scale the smaller buffer by beta before upsampling + accumulator.par_iter_mut().for_each(|v| *v *= beta); + // Cyclic extension by repeated doubling (all sizes are powers of 2) + while accumulator.len() < target_len { + accumulator.extend_from_within(..); + } + } +} + +// ============================================================================ +// Vanishing division +// ============================================================================ + +/// Divide quotient numerator by vanishing polynomial in-place (natural order). +/// +/// Replaces each `numerator[i]` with `numerator[i] / Z_H(xᵢ)` where +/// `Z_H(X) = Xᴺ − 1` and `N` is the trace height. +/// +/// This uses a periodicity trick: on the quotient evaluation coset `gJ` of size `N·D`, +/// the values `Z_H(x)` take only `D` distinct values, so we can batch-invert those `D` +/// values once and reuse them by modular indexing. +/// +/// Note that here `coset.log_blowup()` is `log2(D)` because `coset` is the *quotient* +/// domain (blowup = constraint degree), not the PCS/FRI blowup `B`. +pub fn divide_by_vanishing_in_place(numerator: &mut [EF], coset: &LiftedCoset) +where + F: TwoAdicField, + EF: ExtensionField, +{ + // D = constraint degree. On the quotient coset, log_blowup() = log₂(D). + let log_blowup = coset.log_blowup(); + let num_distinct = 1 << log_blowup; + + // The D distinct values of Z_H on gJ: + // Z_H(g·ω_Jⁱ) = sᴺ·ω_Dⁱ − 1 where + // - s is the coset shift + // - ω_D is a D-th root of unity. + let shift: F = coset.lde_shift(); + let s_pow_n = shift.exp_power_of_2(coset.log_trace_height as usize); + let z_h_evals: Vec = F::two_adic_generator(log_blowup) + .powers() + .take(num_distinct) + .map(|x| s_pow_n * x - F::ONE) + .collect(); + + let inv_van = batch_multiplicative_inverse(&z_h_evals); + + // Parallel division using modular indexing for periodicity. + // Z_H has only num_distinct unique values on gJ; power-of-2 size + // lets us use bitmask: i & (num_distinct - 1) == i % num_distinct. + numerator.par_iter_mut().enumerate().for_each(|(i, n)| { + *n *= inv_van[i & (num_distinct - 1)]; + }); +} + +// ============================================================================ +// Quotient decomposition + commitment +// ============================================================================ + +/// Commit the quotient polynomial by splitting across the `D` quotient cosets. +/// +/// The quotient is naturally evaluated on the quotient evaluation coset `gJ` of size +/// `N·D` (N = trace height, D = constraint degree). We view `J` as `D` disjoint +/// `H`-cosets: `J = ⋃_{t=0..D−1} ω_Jᵗ·H`. Reshaping `Q(gJ)` into an `N×D` +/// matrix makes column `t` the evaluations of a degree-`< N` polynomial qₜ on the +/// coset `g·ω_Jᵗ·H`. +/// +/// We commit to all qₜ by LDE-extending them to the PCS domain `gK` (size `N·B`) and +/// hashing the resulting matrix. Naïvely this would require `D` separate coset-iDFT / +/// coset-DFT pairs (one per chunk). The "fused scaling" trick below collapses all of +/// them into a single plain iDFT, a diagonal scaling pass, and one plain DFT: +/// +/// - a plain iDFT on each column yields coefficients multiplied by `(g·ω_Jᵗ)ᵏ` (the inverse coset +/// shift is absorbed into the coefficients), +/// - multiplying by `(ω_J⁻ᵏ)ᵗ` removes the per-chunk shift ω_Jᵗ while keeping the common factor gᵏ +/// baked in, +/// - a plain (unshifted) forward DFT then evaluates directly on the shifted coset `gK`, because gᵏ +/// already accounts for the coset offset. +/// +/// `q_evals` is consumed and flattened to the base field for commitment. +/// +/// # Panics +/// +/// - If `q_evals.len()` is not divisible by N +/// - If blowup B < constraint degree D +pub fn commit_quotient( + config: &SC, + q_evals: Vec, + coset: &LiftedCoset, +) -> Committed, SC::Lmcs> +where + F: TwoAdicField, + EF: ExtensionField, + SC: StarkConfig, +{ + let n = coset.trace_height(); + let d = q_evals.len() / n; + let log_d = log2_strict_usize(d); + let log_blowup = config.pcs().log_blowup(); + let b = 1usize << log_blowup; + + debug_assert_eq!(q_evals.len() % n, 0, "q_evals length must be divisible by N"); + debug_assert!(b >= d, "blowup B must be >= constraint degree D"); + + // ═══════════════════════════════════════════════════════════════════════ + // Step 0: Reshape to N × D matrix + // ═══════════════════════════════════════════════════════════════════════ + // q_evals[r·D + t] = Q(g·ω_Jᵗ·ω_Hʳ), so column t gives + // qₜ evaluated on the coset g·ω_Jᵗ·H. + let m = RowMajorMatrix::new(q_evals, d); + + // ═══════════════════════════════════════════════════════════════════════ + // Step 1: Batched iDFT over H + // ═══════════════════════════════════════════════════════════════════════ + // iDFT treats each column as evaluations on H (not the actual coset + // g·ω_Jᵗ·H), producing shifted coefficients: + // c_hat[t, k] = a[t, k]·(g·ω_Jᵗ)ᵏ + // where a[t, k] are the true coefficients of qₜ. + let mut coeffs = info_span!("quotient iDFT", dims = %format!("{n}x{d}")) + .in_scope(|| config.dft().idft_algebra_batch(m)); + + // ═══════════════════════════════════════════════════════════════════════ + // Step 2: Fused coefficient scaling + // ═══════════════════════════════════════════════════════════════════════ + // Multiply c_hat[t, k] by (ω_Jᵗ)⁻ᵏ → a[t, k]·gᵏ. + // This removes the per-coset shift ω_Jᵗ while keeping gᵏ baked in. + info_span!("quotient scaling", n).in_scope(|| { + let omega_j_inv = F::two_adic_generator(coset.log_trace_height as usize + log_d).inverse(); + + // Precompute ω_J⁻ᵏ for k = 0..N with sequential multiplications + let row_bases: Vec = omega_j_inv.powers().take(n).collect(); + + // Row k, column t: multiply by (ω_J⁻ᵏ)ᵗ + coeffs.par_rows_mut().zip(row_bases.par_iter()).for_each(|(row, &row_base)| { + for (val, scale) in row.iter_mut().zip(row_base.powers()) { + *val *= scale; + } + }); + }); + + // ═══════════════════════════════════════════════════════════════════════ + // Step 3: Flatten EF → F, zero-pad to N·B rows + // ═══════════════════════════════════════════════════════════════════════ + // We flatten before the DFT (rather than using dft_algebra_batch) because + // we need base field for commitment anyway — this skips the reconstitute. + // + // Zero-padding from N to N·B rows is needed because `dft_batch` expects + // the full target-size buffer. The extra rows are zero because each qₜ has + // degree < N. We pad here (after iDFT + scaling) so those two steps work + // on the smaller N-row buffer. + // + // PERF: the full N·B-size DFT processes N·(B−1) zero rows through every + // butterfly stage, costing O(N·B·log(N·B)) instead of O(N·B·log N). For + // B = 4, N = 2^20 that is ≈ 9% overhead on this step (small relative to + // total proving time since the quotient matrix has only D·DIM columns). + // + // The existing `lde_batch`/`coset_lde_batch` APIs cannot help: they take + // *evaluations*, not coefficients. Using them would add a redundant DFT(N) + // → iDFT(N) round-trip. + // + // What is conceptually missing from `TwoAdicSubgroupDft` is an + // `added_bits` parameter on `dft_batch` / `coset_dft_batch` that evaluates + // degree-< N coefficients on a larger domain of size N·2^added_bits. The + // default would be zero-pad + the existing same-size DFT, but an optimized + // implementation (like `Radix2DftParallel`) could run B separate N-size + // DFTs — one per coset of H inside K — matching what its `coset_lde_batch` + // already does internally after the iDFT phase. + let base_width = d * EF::DIMENSION; + let mut base_coeffs = >::flatten_to_base(coeffs.values); + base_coeffs.resize(n * b * base_width, F::ZERO); + let coeffs_padded = RowMajorMatrix::new(base_coeffs, base_width); + + // ═══════════════════════════════════════════════════════════════════════ + // Step 4: Plain DFT (not coset DFT) on base field + // ═══════════════════════════════════════════════════════════════════════ + // Because gᵏ is baked into the coefficients, the plain DFT evaluates + // on gK directly: entry (i, t) gives qₜ(g·ω_Kⁱ). + let quotient_matrix = info_span!("quotient DFT", dims = %format!("{}x{base_width}", n * b)) + .in_scope(|| { + let lde = config.dft().dft_batch(coeffs_padded); + + // ═══════════════════════════════════════════════════════════════ + // Step 5: Wrap for commitment + // ═══════════════════════════════════════════════════════════════ + materialize_bitrev(lde) + }); + + let tree = config.lmcs().build_aligned_tree(vec![quotient_matrix]); + + Committed::new(tree, log_blowup) +} diff --git a/stark/miden-lifted-stark/src/selectors.rs b/stark/miden-lifted-stark/src/selectors.rs new file mode 100644 index 0000000000..441937069d --- /dev/null +++ b/stark/miden-lifted-stark/src/selectors.rs @@ -0,0 +1,90 @@ +//! Selector container for constraint folding. +//! +//! The [`Selectors`] struct is a plain container holding selector values. +//! Computation is done via [`LiftedCoset`](crate::coset::LiftedCoset) methods: +//! - [`LiftedCoset::selectors`](crate::coset::LiftedCoset::selectors) for coset evaluation (prover) +//! - [`LiftedCoset::selectors_at`](crate::coset::LiftedCoset::selectors_at) for lifted OOD point +//! evaluation (verifier) + +use alloc::vec::Vec; + +use p3_field::{PackedField, TwoAdicField}; + +/// Selector values for constraint evaluation. +/// +/// Plain container for selector values. Use [`LiftedCoset`](crate::coset::LiftedCoset) methods +/// to compute selectors. +/// +/// Generic over `T` to support: +/// - `EF` for single-point OOD evaluation (verifier) +/// - `Vec` for coset evaluation (prover) +#[derive(Clone, Debug)] +pub struct Selectors { + pub is_first_row: T, + pub is_last_row: T, + pub is_transition: T, +} + +impl Selectors> { + /// Get packed selectors for indices `i..i + P::WIDTH`. + /// + /// Returns selector values for consecutive coset points in natural order. + #[inline] + pub fn packed_at

(&self, i: usize) -> Selectors

+ where + P: PackedField, + { + Selectors { + is_first_row: *P::from_slice(&self.is_first_row[i..i + P::WIDTH]), + is_last_row: *P::from_slice(&self.is_last_row[i..i + P::WIDTH]), + is_transition: *P::from_slice(&self.is_transition[i..i + P::WIDTH]), + } + } +} + +#[cfg(test)] +mod tests { + extern crate std; + + use std::vec::Vec; + + use p3_field::PrimeCharacteristicRing; + + use super::*; + use crate::{ + coset::LiftedCoset, + testing::configs::goldilocks_poseidon2::{Felt, QuadFelt}, + }; + + #[test] + fn test_selectors_at_point() { + let log_n = 4; + let coset = LiftedCoset::unlifted(log_n, 0); + + // Sample a point outside the domain + let z = QuadFelt::from(Felt::from_u32(12345)); + + let _sels = coset.selectors_at::(z); + + // Verify vanishing_at matches manual computation + let vanishing = coset.vanishing_at::(z); + let n = 1usize << log_n; + let expected = z.exp_u64(n as u64) - QuadFelt::ONE; + assert_eq!(vanishing, expected); + } + + #[test] + fn test_selectors_on_coset() { + let log_trace = 3; + let log_blowup = 2; // 4x blowup + let coset = LiftedCoset::unlifted(log_trace, log_blowup); + + let sels: Selectors> = coset.selectors(); + + // Check lengths + let coset_size = 1 << (log_trace + log_blowup); + assert_eq!(sels.is_first_row.len(), coset_size); + assert_eq!(sels.is_last_row.len(), coset_size); + assert_eq!(sels.is_transition.len(), coset_size); + } +} diff --git a/stark/miden-lifted-stark/src/testing/airs/blake3.rs b/stark/miden-lifted-stark/src/testing/airs/blake3.rs new file mode 100644 index 0000000000..58cc9246ad --- /dev/null +++ b/stark/miden-lifted-stark/src/testing/airs/blake3.rs @@ -0,0 +1,57 @@ +//! Wraps Plonky3's [`Blake3Air`] as a [`LiftedAir`]. + +use alloc::vec::Vec; + +use miden_lifted_air::{Air, BaseAir, LiftedAir, LiftedAirBuilder}; +pub use p3_blake3_air::{Blake3Air, NUM_BLAKE3_COLS}; +use p3_field::{Field, PrimeField64}; +use p3_matrix::dense::RowMajorMatrix; + +/// [`Blake3Air`] adapted for the lifted STARK prover. +/// +/// Blake3 is a main-trace-only AIR with no preprocessed, periodic, or auxiliary columns. +/// Each row represents one full Blake3 compression (1 row per hash). +pub struct LiftedBlake3Air; + +impl Default for LiftedBlake3Air { + fn default() -> Self { + Self + } +} + +impl BaseAir for LiftedBlake3Air { + fn width(&self) -> usize { + NUM_BLAKE3_COLS + } +} + +impl LiftedAir for LiftedBlake3Air { + fn num_randomness(&self) -> usize { + 1 + } + + fn aux_width(&self) -> usize { + 1 + } + + fn num_aux_values(&self) -> usize { + 0 + } + + fn num_var_len_public_inputs(&self) -> usize { + 0 + } + + fn eval>(&self, builder: &mut AB) { + Air::eval(&Blake3Air {}, builder); + } +} + +/// Generate a Blake3 trace for the given inputs. +/// +/// Each input is 24 `u32` values: 16 block words followed by 8 chaining values. +/// The trace has `inputs.len()` rows (must be a power of two) and +/// [`NUM_BLAKE3_COLS`] columns. +pub fn generate_blake3_trace(inputs: Vec<[u32; 24]>) -> RowMajorMatrix { + p3_blake3_air::generate_trace_rows(inputs, 0) +} diff --git a/stark/miden-lifted-stark/src/testing/airs/keccak.rs b/stark/miden-lifted-stark/src/testing/airs/keccak.rs new file mode 100644 index 0000000000..f10a047168 --- /dev/null +++ b/stark/miden-lifted-stark/src/testing/airs/keccak.rs @@ -0,0 +1,59 @@ +//! Wraps Plonky3's [`KeccakAir`] as a [`LiftedAir`]. + +use alloc::vec::Vec; + +use miden_lifted_air::{Air, BaseAir, LiftedAir, LiftedAirBuilder}; +use p3_field::{Field, PrimeField64}; +use p3_keccak_air::{KeccakAir, NUM_KECCAK_COLS, NUM_ROUNDS}; +use p3_matrix::dense::RowMajorMatrix; + +/// [`KeccakAir`] adapted for the lifted STARK prover. +/// +/// Keccak is a main-trace-only AIR with no preprocessed, periodic, or auxiliary columns. +pub struct LiftedKeccakAir; + +impl Default for LiftedKeccakAir { + fn default() -> Self { + Self + } +} + +impl BaseAir for LiftedKeccakAir { + fn width(&self) -> usize { + NUM_KECCAK_COLS + } +} + +impl LiftedAir for LiftedKeccakAir { + fn num_randomness(&self) -> usize { + 1 + } + + fn aux_width(&self) -> usize { + 1 + } + + fn num_aux_values(&self) -> usize { + 0 + } + + fn num_var_len_public_inputs(&self) -> usize { + 0 + } + + fn eval>(&self, builder: &mut AB) { + Air::eval(&KeccakAir {}, builder); + } +} + +/// Generate a Keccak trace for the given inputs. +/// +/// Each input is a Keccak-f\[1600\] state (5x5 = 25 `u64` values). +/// The trace has `next_power_of_two(inputs.len() * 24)` rows and +/// [`NUM_KECCAK_COLS`] columns. +pub fn generate_keccak_trace(inputs: Vec<[u64; 25]>) -> RowMajorMatrix { + p3_keccak_air::generate_trace_rows(inputs, 0) +} + +/// The number of trace rows per Keccak-f permutation (24 rounds). +pub const ROWS_PER_HASH: usize = NUM_ROUNDS; diff --git a/stark/miden-lifted-stark/src/testing/airs/miden.rs b/stark/miden-lifted-stark/src/testing/airs/miden.rs new file mode 100644 index 0000000000..895bb49679 --- /dev/null +++ b/stark/miden-lifted-stark/src/testing/airs/miden.rs @@ -0,0 +1,113 @@ +//! Dummy AIR mimicking the Miden VM workload profile. +//! +//! Two trace shapes: 51-column (2^18 rows) and 20-column (2^19 rows), with a +//! single degree-9 base constraint producing 8 quotient chunks, and 8 extension-field +//! auxiliary columns (= 16 base-field columns with Goldilocks `ext_degree=2`). + +use miden_lifted_air::{AirBuilder, BaseAir, LiftedAir, LiftedAirBuilder, WindowAccess}; +use p3_field::{Field, PrimeCharacteristicRing}; +use p3_matrix::dense::RowMajorMatrix; + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +/// Main trace width for the first (shorter) trace. +pub const TRACE1_WIDTH: usize = 51; +/// Main trace width for the second (taller) trace. +pub const TRACE2_WIDTH: usize = 20; +/// Log₂ height of the first trace (2^18 = 262144 rows). +pub const TRACE1_LOG_HEIGHT: u8 = 18; +/// Log₂ height of the second trace (2^19 = 524288 rows). +pub const TRACE2_LOG_HEIGHT: u8 = 19; +/// Number of extension-field auxiliary columns. +pub const NUM_AUX_COLS: usize = 8; + +// --------------------------------------------------------------------------- +// AIR definition +// --------------------------------------------------------------------------- + +/// A dummy AIR with a single degree-9 constraint and auxiliary columns. +/// +/// The constraint is `local[0] * local[1] * ... * local[8] == 0`, which has +/// degree 9 and produces `log_quotient_degree = 3` (8 quotient chunks). +pub struct DummyMidenAir { + width: usize, + num_aux_cols: usize, +} + +impl DummyMidenAir { + pub fn new(width: usize, num_aux_cols: usize) -> Self { + assert!(width >= 9, "DummyMidenAir needs at least 9 columns for the degree-9 constraint"); + Self { width, num_aux_cols } + } +} + +/// Shared constraint logic: `local[0] * local[1] * ... * local[8] == 0`. +fn eval_miden_constraints(builder: &mut AB) { + let main = builder.main(); + let local = main.current_slice(); + let product = (0..9).fold(AB::Expr::ONE, |acc, j| acc * local[j].into()); + builder.assert_zero(product); +} + +// --------------------------------------------------------------------------- +// Trait impls for lifted STARK path +// --------------------------------------------------------------------------- + +impl BaseAir for DummyMidenAir { + fn width(&self) -> usize { + self.width + } +} + +impl LiftedAir for DummyMidenAir { + fn num_randomness(&self) -> usize { + 2 + } + + fn aux_width(&self) -> usize { + self.num_aux_cols + } + + fn num_aux_values(&self) -> usize { + self.num_aux_cols + } + + fn num_var_len_public_inputs(&self) -> usize { + 0 + } + + fn eval>(&self, builder: &mut AB) { + eval_miden_constraints(builder); + } +} + +// --------------------------------------------------------------------------- +// Trace generation +// --------------------------------------------------------------------------- + +/// Generate a dummy trace with the given width and log₂ height. +/// +/// Column 0 is zero everywhere (satisfying the product constraint). +/// Columns 1..width are filled with deterministic pseudo-random values. +pub fn generate_dummy_trace(width: usize, log_height: u8, rng: &mut R) -> RowMajorMatrix +where + F: Field, + R: rand::Rng, + rand::distr::StandardUniform: rand::distr::Distribution, +{ + use rand::RngExt; + + let height = 1 << log_height as usize; + let mut values = F::zero_vec(height * width); + + for row in 0..height { + // Column 0 stays zero (already initialized). + for col in 1..width { + values[row * width + col] = rng.random(); + } + } + + RowMajorMatrix::new(values, width) +} diff --git a/stark/miden-lifted-stark/src/testing/airs/mod.rs b/stark/miden-lifted-stark/src/testing/airs/mod.rs new file mode 100644 index 0000000000..5be7bea35d --- /dev/null +++ b/stark/miden-lifted-stark/src/testing/airs/mod.rs @@ -0,0 +1,53 @@ +//! Example AIRs wrapped for the lifted STARK prover. +//! +//! Each module adapts an upstream Plonky3 AIR into a `LiftedAir` so it can be proven +//! and verified with the lifted STARK protocol. + +use alloc::{vec, vec::Vec}; + +use miden_lifted_air::AuxBuilder; +use p3_field::{ExtensionField, Field}; +use p3_matrix::{Matrix, dense::RowMajorMatrix}; + +#[cfg(feature = "testing")] +pub mod blake3; +#[cfg(feature = "testing")] +pub mod keccak; +pub mod miden; +#[cfg(feature = "testing")] +pub mod poseidon2; + +/// Aux builder that produces an all-zero auxiliary trace. +/// +/// Every `LiftedAir` must have at least one aux column, so this builder +/// satisfies the requirement with minimal cost. +/// +/// Use [`ZeroAuxBuilder::dummy()`] for AIRs with `num_aux_values() == 0` +/// (1-column all-zero trace, no aux values). +pub struct ZeroAuxBuilder { + pub num_aux_cols: usize, + pub num_aux_values: usize, +} + +impl ZeroAuxBuilder { + /// 1-column all-zero auxiliary trace with no aux values. + /// + /// Suitable for AIRs where `num_aux_values() == 0`. + pub fn dummy() -> Self { + Self { num_aux_cols: 1, num_aux_values: 0 } + } +} + +impl> AuxBuilder for ZeroAuxBuilder { + fn build_aux_trace( + &self, + main: &RowMajorMatrix, + _challenges: &[EF], + ) -> (RowMajorMatrix, Vec) { + let height = main.height(); + let values = EF::zero_vec(height * self.num_aux_cols); + let aux_trace = RowMajorMatrix::new(values, self.num_aux_cols); + let aux_values = vec![EF::ZERO; self.num_aux_values]; + (aux_trace, aux_values) + } +} diff --git a/stark/miden-lifted-stark/src/testing/airs/poseidon2.rs b/stark/miden-lifted-stark/src/testing/airs/poseidon2.rs new file mode 100644 index 0000000000..b9b1dee24a --- /dev/null +++ b/stark/miden-lifted-stark/src/testing/airs/poseidon2.rs @@ -0,0 +1,97 @@ +//! Wraps Plonky3's [`Poseidon2Air`] as a [`LiftedAir`]. +//! +//! Uses the standard Goldilocks configuration: WIDTH=12, SBOX_DEGREE=7, SBOX_REGISTERS=1, +//! HALF_FULL_ROUNDS=4, PARTIAL_ROUNDS=22. + +use alloc::vec::Vec; + +use miden_lifted_air::{Air, BaseAir, LiftedAir, LiftedAirBuilder}; +use p3_field::Field; +use p3_goldilocks::{GenericPoseidon2LinearLayersGoldilocks, Goldilocks}; +use p3_matrix::dense::RowMajorMatrix; +use p3_poseidon2_air::{Poseidon2Air, RoundConstants, num_cols}; + +/// Goldilocks Poseidon2 configuration constants. +pub const WIDTH: usize = 12; +pub const SBOX_DEGREE: u64 = 7; +pub const SBOX_REGISTERS: usize = 1; +pub const HALF_FULL_ROUNDS: usize = 4; +pub const PARTIAL_ROUNDS: usize = 22; + +/// Number of trace columns for the Goldilocks Poseidon2 AIR. +pub const NUM_POSEIDON2_COLS: usize = + num_cols::(); + +type GoldilocksRoundConstants = RoundConstants; + +type InnerAir = Poseidon2Air< + Goldilocks, + GenericPoseidon2LinearLayersGoldilocks, + WIDTH, + SBOX_DEGREE, + SBOX_REGISTERS, + HALF_FULL_ROUNDS, + PARTIAL_ROUNDS, +>; + +/// [`Poseidon2Air`] adapted for the lifted STARK prover. +/// +/// Poseidon2 is a main-trace-only AIR with no preprocessed, periodic, or auxiliary columns. +/// Each row represents one full Poseidon2 permutation (1 row per hash). +pub struct LiftedPoseidon2Air { + inner: InnerAir, +} + +impl LiftedPoseidon2Air { + pub fn new(constants: GoldilocksRoundConstants) -> Self { + Self { inner: InnerAir::new(constants) } + } +} + +impl BaseAir for LiftedPoseidon2Air { + fn width(&self) -> usize { + NUM_POSEIDON2_COLS + } +} + +impl LiftedAir for LiftedPoseidon2Air { + fn num_randomness(&self) -> usize { + 1 + } + + fn aux_width(&self) -> usize { + 1 + } + + fn num_aux_values(&self) -> usize { + 0 + } + + fn num_var_len_public_inputs(&self) -> usize { + 0 + } + + fn eval>(&self, builder: &mut AB) { + Air::eval(&self.inner, builder); + } +} + +/// Generate a Poseidon2 trace for the given inputs. +/// +/// Each input is a WIDTH=12 element Goldilocks array. The number of inputs must +/// be a power of two. The trace has `inputs.len()` rows and +/// [`NUM_POSEIDON2_COLS`] columns. +pub fn generate_poseidon2_trace( + inputs: Vec<[Goldilocks; WIDTH]>, + constants: &GoldilocksRoundConstants, +) -> RowMajorMatrix { + p3_poseidon2_air::generate_trace_rows::< + Goldilocks, + GenericPoseidon2LinearLayersGoldilocks, + WIDTH, + SBOX_DEGREE, + SBOX_REGISTERS, + HALF_FULL_ROUNDS, + PARTIAL_ROUNDS, + >(inputs, constants, 0) +} diff --git a/stark/miden-lifted-stark/src/testing/configs/goldilocks_blake3.rs b/stark/miden-lifted-stark/src/testing/configs/goldilocks_blake3.rs new file mode 100644 index 0000000000..9dbdaa295d --- /dev/null +++ b/stark/miden-lifted-stark/src/testing/configs/goldilocks_blake3.rs @@ -0,0 +1,50 @@ +//! Goldilocks + BLAKE3 (32-byte digest) test configuration. + +use alloc::vec; + +use miden_stateful_hasher::ChainingHasher; +use p3_blake3::Blake3; +use p3_challenger::{HashChallenger, SerializingChallenger64}; +use p3_symmetric::CompressionFunctionFromHasher; + +pub use super::{Felt, PackedFelt, QuadFelt}; + +/// Chaining state / digest width in bytes. +pub const WIDTH: usize = 32; + +/// Digest size in bytes. +pub const DIGEST: usize = 32; + +/// Chaining sponge over BLAKE3 on serialized field elements (LMCS `StatefulHasher`). +pub type Sponge = ChainingHasher; + +/// 2-to-1 compression via BLAKE3. +pub type Compress = CompressionFunctionFromHasher; + +/// Fiat-Shamir challenger over serialized field elements. +pub type Challenger = SerializingChallenger64>; + +/// Sponge + compressor for Merkle construction. +pub fn test_components() -> (Sponge, Compress) { + (ChainingHasher::new(Blake3), CompressionFunctionFromHasher::new(Blake3)) +} + +/// Fresh hash challenger (empty initial state). +pub fn test_challenger() -> Challenger { + SerializingChallenger64::new(HashChallenger::::new(vec![], Blake3)) +} + +// ============================================================================= +// LMCS layer +// ============================================================================= + +/// LMCS configured with Goldilocks + Blake3. +pub type Lmcs = crate::lmcs::config::LmcsConfig; + +crate::testing::define_lmcs_test_helpers!(); + +/// Create a test LMCS instance. +pub fn test_lmcs() -> Lmcs { + let (sponge, compress) = test_components(); + crate::lmcs::config::LmcsConfig::new(sponge, compress) +} diff --git a/stark/miden-lifted-stark/src/testing/configs/goldilocks_blake3_192.rs b/stark/miden-lifted-stark/src/testing/configs/goldilocks_blake3_192.rs new file mode 100644 index 0000000000..5bd4a94aa6 --- /dev/null +++ b/stark/miden-lifted-stark/src/testing/configs/goldilocks_blake3_192.rs @@ -0,0 +1,56 @@ +//! Goldilocks + BLAKE3-192 (24-byte digest) test configuration. + +use alloc::vec; + +use miden_stateful_hasher::{ChainingHasher, TruncatingHasher}; +use p3_blake3::Blake3; +use p3_challenger::{HashChallenger, SerializingChallenger64}; +use p3_symmetric::CompressionFunctionFromHasher; + +pub use super::{Felt, PackedFelt, QuadFelt}; + +pub type Blake3_192 = TruncatingHasher; + +/// Chaining state / digest width in bytes (matches digest size). +pub const WIDTH: usize = 24; + +/// Digest size in bytes. +pub const DIGEST: usize = 24; + +/// Chaining sponge over BLAKE3-192 on serialized field elements (LMCS `StatefulHasher`). +pub type Sponge = ChainingHasher; + +/// 2-to-1 compression via BLAKE3-192. +pub type Compress = CompressionFunctionFromHasher; + +/// Fiat-Shamir challenger over serialized field elements. +pub type Challenger = SerializingChallenger64>; + +/// Sponge + compressor for Merkle construction. +pub fn test_components() -> (Sponge, Compress) { + let h = Blake3_192::new(Blake3); + (ChainingHasher::new(h), CompressionFunctionFromHasher::new(h)) +} + +/// Fresh hash challenger (empty initial state). +pub fn test_challenger() -> Challenger { + SerializingChallenger64::new(HashChallenger::::new( + vec![], + Blake3_192::new(Blake3), + )) +} + +// ============================================================================= +// LMCS layer +// ============================================================================= + +/// LMCS configured with Goldilocks + Blake3-192. +pub type Lmcs = crate::lmcs::config::LmcsConfig; + +crate::testing::define_lmcs_test_helpers!(); + +/// Create a test LMCS instance. +pub fn test_lmcs() -> Lmcs { + let (sponge, compress) = test_components(); + crate::lmcs::config::LmcsConfig::new(sponge, compress) +} diff --git a/stark/miden-lifted-stark/src/testing/configs/goldilocks_keccak.rs b/stark/miden-lifted-stark/src/testing/configs/goldilocks_keccak.rs new file mode 100644 index 0000000000..69fb7a4330 --- /dev/null +++ b/stark/miden-lifted-stark/src/testing/configs/goldilocks_keccak.rs @@ -0,0 +1,71 @@ +//! Goldilocks + Keccak test configuration. + +use alloc::vec; + +use miden_stateful_hasher::{SerializingStatefulSponge, StatefulSponge}; +use p3_challenger::{HashChallenger, SerializingChallenger64}; +use p3_keccak::{Keccak256Hash, KeccakF}; +use p3_symmetric::{CompressionFunctionFromHasher, Hash, PaddingFreeSponge}; + +pub use super::{Felt, PackedFelt, QuadFelt}; + +/// Keccak permutation width (fixed). +pub const WIDTH: usize = 25; + +/// Sponge rate (fixed for Keccak). +pub const RATE: usize = 17; + +/// Digest size in u64 elements (fixed for Keccak). +pub const DIGEST: usize = 4; + +/// Sponge for MMCS-style hashing (field-agnostic, operates on `u64` lanes). +pub type KeccakMmcsSponge = PaddingFreeSponge; + +/// 2-to-1 compression for Merkle trees. +pub type Compress = CompressionFunctionFromHasher; + +/// Inner stateful sponge operating on `u64` lanes. +pub type InnerSponge = StatefulSponge; + +/// Serializing sponge that converts field elements to `u64` before absorption. +pub type Sponge = SerializingStatefulSponge; + +/// Commitment type (hash of `u64` digest elements). +pub type Commitment = Hash; + +/// Fiat-Shamir challenger (serializing, byte-based via Keccak-256). +pub type Challenger = SerializingChallenger64>; + +/// Create standard test components for Merkle tree construction. +pub fn test_components() -> (Sponge, Compress) { + let sponge = Sponge::new(InnerSponge::new(KeccakF)); + let compress = Compress::new(KeccakMmcsSponge::new(KeccakF)); + (sponge, compress) +} + +/// Create a standard challenger for Fiat-Shamir. +pub fn test_challenger() -> Challenger { + Challenger::from_hasher(vec![], Keccak256Hash) +} + +// ============================================================================= +// LMCS layer +// ============================================================================= + +/// LMCS configured with Goldilocks + Keccak (SIMD-parallel). +pub type Lmcs = crate::lmcs::config::LmcsConfig< + [Felt; p3_keccak::VECTOR_LEN], + [u64; p3_keccak::VECTOR_LEN], + Sponge, + Compress, + WIDTH, + DIGEST, +>; + +crate::testing::define_lmcs_test_helpers!(); + +/// Create a test LMCS instance. +pub fn test_lmcs() -> Lmcs { + let (sponge, compress) = test_components(); + crate::lmcs::config::LmcsConfig::new(sponge, compress) +} diff --git a/stark/miden-lifted-stark/src/testing/configs/goldilocks_poseidon2.rs b/stark/miden-lifted-stark/src/testing/configs/goldilocks_poseidon2.rs new file mode 100644 index 0000000000..fff799e0dd --- /dev/null +++ b/stark/miden-lifted-stark/src/testing/configs/goldilocks_poseidon2.rs @@ -0,0 +1,191 @@ +//! Goldilocks + Poseidon2 test configuration. +//! +//! Provides a complete set of type aliases, constructors, and helpers for testing +//! LMCS, PCS, and full STARK with Goldilocks field and Poseidon2 hashing. + +use alloc::vec::Vec; + +use p3_challenger::DuplexChallenger; +use p3_field::PrimeCharacteristicRing; +use p3_goldilocks::Poseidon2Goldilocks; +use p3_matrix::dense::RowMajorMatrix; +use p3_symmetric::{Hash, TruncatedPermutation}; +use rand::{SeedableRng, rngs::SmallRng}; + +pub use super::{Felt, PackedFelt, QuadFelt}; +use crate::{AirWitness, testing::TEST_SEED}; + +// ============================================================================= +// Base field/hash configuration +// ============================================================================= + +/// Poseidon2 permutation width. +pub const WIDTH: usize = 12; + +/// Sponge rate (elements absorbed per permutation). +pub const RATE: usize = 8; + +/// Digest size in field elements. +pub const DIGEST: usize = 4; + +/// Poseidon2 permutation. +pub type Perm = Poseidon2Goldilocks; + +/// Stateful sponge for hashing (can be used for LMCS). +pub type Sponge = miden_stateful_hasher::StatefulSponge; + +/// Truncated permutation for 2-to-1 compression. +pub type Compress = TruncatedPermutation; + +/// Commitment type (truncated permutation output). +pub type Commitment = Hash; + +/// Duplex challenger for Fiat-Shamir. +pub type Challenger = DuplexChallenger; + +/// Create the permutation with standard seed. +pub fn create_perm() -> Perm { + let mut rng = SmallRng::seed_from_u64(TEST_SEED); + Perm::new_from_rng_128(&mut rng) +} + +/// Create standard test components with a consistent seed. +/// +/// Returns the permutation, sponge, and compressor for Merkle tree construction. +pub fn test_components() -> (Perm, Sponge, Compress) { + let perm = create_perm(); + let sponge = Sponge::new(perm.clone()); + let compress = Compress::new(perm.clone()); + (perm, sponge, compress) +} + +/// Create a standard challenger for Fiat-Shamir. +pub fn test_challenger() -> Challenger { + Challenger::new(create_perm()) +} + +// ============================================================================= +// LMCS layer +// ============================================================================= + +/// LMCS configured with Goldilocks + Poseidon2. +pub type Lmcs = + crate::lmcs::config::LmcsConfig; + +crate::testing::define_lmcs_test_helpers!(); + +/// Create a test LMCS instance. +pub fn test_lmcs() -> Lmcs { + let (_, sponge, compress) = test_components(); + crate::lmcs::config::LmcsConfig::new(sponge, compress) +} + +// ============================================================================= +// PCS layer +// ============================================================================= + +/// Generate a matrix of LDE evaluations for random low-degree polynomials. +pub fn random_lde_matrix( + rng: &mut SmallRng, + log_poly_degree: u8, + log_blowup: u8, + num_columns: usize, + shift: Felt, +) -> RowMajorMatrix +where + V: p3_field::BasedVectorSpace + Clone + Send + Sync + Default, + rand::distr::StandardUniform: rand::distr::Distribution, +{ + use p3_dft::{Radix2DFTSmallBatch, TwoAdicSubgroupDft}; + use p3_matrix::{Matrix as _, bitrev::BitReversibleMatrix}; + + let poly_degree = 1 << log_poly_degree as usize; + let dft = Radix2DFTSmallBatch::::default(); + + let evals = RowMajorMatrix::rand(rng, poly_degree, num_columns); + let lde = dft.coset_lde_algebra_batch(evals, log_blowup as usize, shift); + lde.bit_reverse_rows().to_row_major_matrix() +} + +// ============================================================================= +// STARK layer +// ============================================================================= + +pub type Dft = p3_dft::Radix2DitParallel; + +pub type TestConfig = crate::config::GenericStarkConfig; + +pub fn test_config() -> TestConfig { + crate::config::GenericStarkConfig::new( + crate::testing::params::TEST_PCS_PARAMS, + test_lmcs(), + Dft::default(), + test_challenger(), + ) +} + +/// Generate a power-of-4 chain trace: `[start, start⁴, start¹⁶, start⁶⁴, ...]` +pub fn generate_pow4_trace(start: Felt, height: usize) -> RowMajorMatrix { + let mut values = Vec::with_capacity(height); + let mut current = start; + for _ in 0..height { + values.push(current); + current = current.exp_power_of_2(2); + } + RowMajorMatrix::new(values, 1) +} + +/// Prove and verify from pre-built prover instances. +/// +/// Runs the full prove → verify → transcript-reparse cycle. +pub fn prove_and_verify_instances(instances: &[(&A, AirWitness<'_, Felt>, &B)]) +where + A: crate::air::LiftedAir, + B: crate::air::AuxBuilder, +{ + let config = test_config(); + + let output = crate::prover::prove_multi(&config, instances, test_challenger()) + .expect("proving should succeed"); + + let verifier_instances: Vec<_> = + instances.iter().map(|(a, w, _)| (*a, w.to_instance())).collect(); + + let verifier_digest = crate::verifier::verify_multi( + &config, + &verifier_instances, + &output.proof, + test_challenger(), + ) + .expect("verification should succeed"); + assert_eq!(output.digest, verifier_digest); + + // Re-parse transcript from a fresh challenger and verify digest agreement. + let (_, reparse_digest) = crate::proof::StarkTranscript::from_proof( + &config, + &verifier_instances, + &output.proof, + test_challenger(), + ) + .expect("transcript re-parse should succeed"); + assert_eq!(output.digest, reparse_digest); +} + +/// Prove and verify multiple traces, each with its own public values. +/// +/// `instances` is a slice of `(trace, public_values)` pairs. +pub fn prove_and_verify( + air: &A, + aux_builder: &B, + instances: &[(RowMajorMatrix, Vec)], +) where + A: crate::air::LiftedAir, + B: crate::air::AuxBuilder, +{ + let prover_instances: Vec<_> = instances + .iter() + .map(|(t, pv)| (air, AirWitness::new(t, pv, &[]), aux_builder)) + .collect(); + + prove_and_verify_instances(&prover_instances); +} diff --git a/stark/miden-lifted-stark/src/testing/configs/mod.rs b/stark/miden-lifted-stark/src/testing/configs/mod.rs new file mode 100644 index 0000000000..a4984bf64c --- /dev/null +++ b/stark/miden-lifted-stark/src/testing/configs/mod.rs @@ -0,0 +1,27 @@ +//! Field/hash configuration modules for testing. +//! +//! Each module provides complete type aliases, constructors, and helpers +//! for testing at any level (LMCS, PCS, or full STARK). +//! +//! The common field types (`Felt`, `QuadFelt`, `PackedFelt`) are defined here +//! and re-exported by each hash configuration module. + +use p3_field::{Field, extension::BinomialExtensionField}; +use p3_goldilocks::Goldilocks; + +/// Goldilocks base field. +pub type Felt = Goldilocks; + +/// Quadratic extension of Goldilocks. +pub type QuadFelt = BinomialExtensionField; + +/// Packed base field for SIMD operations. +pub type PackedFelt = ::Packing; + +#[cfg(feature = "testing")] +pub mod goldilocks_blake3; +#[cfg(feature = "testing")] +pub mod goldilocks_blake3_192; +#[cfg(feature = "testing")] +pub mod goldilocks_keccak; +pub mod goldilocks_poseidon2; diff --git a/stark/miden-lifted-stark/src/testing/mod.rs b/stark/miden-lifted-stark/src/testing/mod.rs new file mode 100644 index 0000000000..dc2e019e92 --- /dev/null +++ b/stark/miden-lifted-stark/src/testing/mod.rs @@ -0,0 +1,148 @@ +//! Unified testing infrastructure for the lifted STARK crate. +//! +//! Provides three complete configuration variants, each containing everything +//! needed to test at any level (LMCS, PCS, or full STARK): +//! +//! - `configs::goldilocks_poseidon2` +//! - `configs::goldilocks_keccak` +//! - `configs::goldilocks_blake3_192` +//! +//! Also provides shared fixtures, matrix generation utilities, and test helpers. + +pub mod airs; +pub mod configs; +pub mod params; + +#[cfg(test)] +mod test_aux_shape; +#[cfg(test)] +mod test_bus; +#[cfg(test)] +mod test_multi_aux_alignment; +#[cfg(test)] +mod test_tiny_air; + +// Re-export commonly used params at the module level for convenience. +use alloc::vec::Vec; + +use p3_field::Field; +use p3_matrix::{Matrix, dense::RowMajorMatrix}; +pub use params::{ + BENCH_PCS_PARAMS, FRI_FOLD_ARITY_2, FRI_FOLD_ARITY_4, FRI_FOLD_ARITY_8, LOG_HEIGHTS, + PARALLEL_STR, QC_CONSTRAINT_DEGREE, QC_PCS_PARAMS, RELATIVE_SPECS, TEST_SEED, +}; +use rand::{ + SeedableRng, + distr::{Distribution, StandardUniform}, + rngs::SmallRng, +}; + +// ============================================================================= +// Matrix generation +// ============================================================================= + +/// Generate benchmark matrices from relative specs. +/// +/// Creates matrices with heights relative to `max_height = 1 << log_max_height`. +/// Each spec `(offset, width)` creates a matrix with: +/// - height = `max_height >> offset` +/// - width = `width` +/// +/// Matrices in each group are sorted by ascending height. +pub fn generate_matrices_from_specs( + specs: &[&[(usize, usize)]], + log_max_height: u8, +) -> Vec>> +where + StandardUniform: Distribution, +{ + let rng = &mut SmallRng::seed_from_u64(TEST_SEED); + let max_height = 1 << log_max_height as usize; + + specs + .iter() + .map(|group_specs| { + let mut matrices: Vec> = group_specs + .iter() + .map(|&(offset, width)| { + let height = max_height >> offset; + RowMajorMatrix::rand(rng, height, width) + }) + .collect(); + // Sort by ascending height (required by LMCS) + matrices.sort_by_key(Matrix::height); + matrices + }) + .collect() +} + +/// Calculate total elements across all matrices. +pub fn total_elements(matrix_groups: &[Vec>]) -> u64 { + matrix_groups + .iter() + .flat_map(|g| g.iter()) + .map(|m| { + let dims = m.dimensions(); + (dims.height * dims.width) as u64 + }) + .sum() +} + +// ============================================================================= +// define_test_config! macro +// ============================================================================= + +/// Generates LMCS type aliases and channel helper functions for a test config. +/// +/// Requires these items in scope from the base config module: +/// `Felt`, `Sponge`, `Compress`, `Challenger`, `test_challenger` +/// +/// Also requires `Lmcs` to be defined as a type alias in the invoking module. +macro_rules! define_lmcs_test_helpers { + () => { + use $crate::lmcs::Lmcs as LmcsTrait; + + pub type TestTree = ::Tree>; + pub type TestCommitment = ::Commitment; + pub type TestTranscriptData = miden_stark_transcript::TranscriptData; + pub type TestDigest = ::Digest; + pub type TestProverChannel = + miden_stark_transcript::ProverTranscript; + pub type TestVerifierChannel<'a> = + miden_stark_transcript::VerifierTranscript<'a, Felt, TestCommitment, Challenger>; + + pub fn prover_channel() -> TestProverChannel { + miden_stark_transcript::ProverTranscript::new(test_challenger()) + } + + pub fn prover_channel_with_commitment(commitment: &TestCommitment) -> TestProverChannel { + let mut challenger = test_challenger(); + p3_challenger::CanObserve::observe(&mut challenger, commitment.clone()); + miden_stark_transcript::ProverTranscript::new(challenger) + } + + pub fn verifier_channel(data: &TestTranscriptData) -> TestVerifierChannel<'_> { + miden_stark_transcript::VerifierTranscript::from_data(test_challenger(), data) + } + + pub fn verifier_channel_with_commitment<'a>( + data: &'a TestTranscriptData, + commitment: &TestCommitment, + ) -> TestVerifierChannel<'a> { + let mut challenger = test_challenger(); + p3_challenger::CanObserve::observe(&mut challenger, commitment.clone()); + miden_stark_transcript::VerifierTranscript::from_data(challenger, data) + } + }; +} + +pub(crate) use define_lmcs_test_helpers; + +// ============================================================================= +// Internal re-exports for benchmarks +// ============================================================================= +pub use crate::pcs::{ + deep::interpolate::PointQuotients, fri::fold::FriFold, prover::open_with_channel, + utils::bit_reversed_coset_points, +}; +pub use crate::prover::quotient::commit_quotient; diff --git a/stark/miden-lifted-stark/src/testing/params.rs b/stark/miden-lifted-stark/src/testing/params.rs new file mode 100644 index 0000000000..5abf036de9 --- /dev/null +++ b/stark/miden-lifted-stark/src/testing/params.rs @@ -0,0 +1,98 @@ +//! Shared constants and parameter sets for tests and benchmarks. +//! +//! Centralizes magic numbers so they can be tuned in one place and +//! referenced consistently across unit tests, criterion benches, and +//! profiling binaries. + +use crate::pcs::{ + deep::DeepParams, + fri::{FriParams, fold::FriFold}, + params::PcsParams, +}; + +// ============================================================================= +// FRI fold arities +// ============================================================================= + +pub const FRI_FOLD_ARITY_2: FriFold = FriFold { log_arity: 1 }; +pub const FRI_FOLD_ARITY_4: FriFold = FriFold { log_arity: 2 }; +pub const FRI_FOLD_ARITY_8: FriFold = FriFold { log_arity: 3 }; + +// ============================================================================= +// Seeds +// ============================================================================= + +/// Standard seed for reproducible tests and benchmarks. +pub const TEST_SEED: u64 = 2025; + +// ============================================================================= +// Benchmark matrix shapes +// ============================================================================= + +/// Standard log heights for benchmarking: 2^16, 2^18, 2^20 leaves. +pub const LOG_HEIGHTS: &[u8] = &[16, 18, 20]; + +/// Standard relative specs for benchmark matrix groups. +/// +/// Each inner slice is a separate commitment group. +/// Tuple format: `(offset_from_max, width)` where `log_height = log_max_height - offset`. +/// +/// This gives realistic matrix configurations similar to STARK traces: +/// - Group 0: Main trace columns at various heights +/// - Group 1: Auxiliary/permutation columns +/// - Group 2: Quotient polynomial chunks +pub const RELATIVE_SPECS: &[&[(usize, usize)]] = + &[&[(4, 10), (2, 100), (0, 50)], &[(4, 8), (2, 20), (0, 20)], &[(0, 16)]]; + +/// Label for benchmark group names indicating parallelism mode. +pub const PARALLEL_STR: &str = if cfg!(feature = "parallel") { + "parallel" +} else { + "single" +}; + +// ============================================================================= +// PCS parameter sets +// ============================================================================= + +/// PCS parameters for unit tests (fast, minimal security). +pub const TEST_PCS_PARAMS: PcsParams = PcsParams { + deep: DeepParams { deep_pow_bits: 0 }, + fri: FriParams { + log_blowup: 2, + fold: FRI_FOLD_ARITY_4, + log_final_degree: 2, + folding_pow_bits: 0, + }, + num_queries: 2, + query_pow_bits: 0, +}; + +/// PCS parameters for benchmarks (realistic security, zero PoW). +pub const BENCH_PCS_PARAMS: PcsParams = PcsParams { + deep: DeepParams { deep_pow_bits: 0 }, + fri: FriParams { + log_blowup: 2, + fold: FRI_FOLD_ARITY_4, + log_final_degree: 8, + folding_pow_bits: 0, + }, + num_queries: 30, + query_pow_bits: 0, +}; + +/// PCS parameters for quotient commit benchmarks (lower blowup, single query). +pub const QC_PCS_PARAMS: PcsParams = PcsParams { + deep: DeepParams { deep_pow_bits: 0 }, + fri: FriParams { + log_blowup: 1, + fold: FRI_FOLD_ARITY_4, + log_final_degree: 0, + folding_pow_bits: 0, + }, + num_queries: 1, + query_pow_bits: 0, +}; + +/// Constraint degree used in quotient commit benchmarks (matches KeccakAir). +pub const QC_CONSTRAINT_DEGREE: usize = 2; diff --git a/stark/miden-lifted-stark/src/testing/test_aux_shape.rs b/stark/miden-lifted-stark/src/testing/test_aux_shape.rs new file mode 100644 index 0000000000..390cfecb5c --- /dev/null +++ b/stark/miden-lifted-stark/src/testing/test_aux_shape.rs @@ -0,0 +1,70 @@ +//! Tests that the prover rejects aux trace width mismatches. + +use alloc::{vec, vec::Vec}; + +use p3_field::PrimeCharacteristicRing; +use p3_matrix::{Matrix, dense::RowMajorMatrix}; + +use crate::{ + air::{AuxBuilder, BaseAir, LiftedAir, LiftedAirBuilder}, + prove_single, + testing::configs::goldilocks_poseidon2::{Felt, QuadFelt, test_challenger, test_config}, +}; + +#[derive(Clone, Copy, Debug)] +struct BadAuxWidthAir; + +impl BaseAir for BadAuxWidthAir { + fn width(&self) -> usize { + 1 + } +} + +impl LiftedAir for BadAuxWidthAir { + fn num_randomness(&self) -> usize { + 1 + } + + fn aux_width(&self) -> usize { + 1 + } + + fn num_aux_values(&self) -> usize { + 0 + } + + fn num_var_len_public_inputs(&self) -> usize { + 0 + } + + fn eval>(&self, _builder: &mut AB) {} +} + +/// AuxBuilder that returns 2 EF columns when BadAuxWidthAir declares 1. +struct BadAuxBuilder; + +impl AuxBuilder for BadAuxBuilder { + fn build_aux_trace( + &self, + main: &RowMajorMatrix, + _challenges: &[QuadFelt], + ) -> (RowMajorMatrix, Vec) { + let height = main.height(); + // Return 2 QuadFelt columns when aux_width() declares 1 + let aux = RowMajorMatrix::new(vec![QuadFelt::ZERO; height * 2], 2); + (aux, vec![QuadFelt::ZERO, QuadFelt::ZERO]) + } +} + +#[test] +#[should_panic(expected = "aux trace width mismatch")] +fn aux_width_mismatch_panics() { + let config = test_config(); + let air = BadAuxWidthAir; + + let trace = RowMajorMatrix::new(vec![Felt::ZERO, Felt::ONE, Felt::ONE, Felt::ZERO], 1); + let public_values = vec![]; + + let _result = + prove_single(&config, &air, &trace, &public_values, &[], &BadAuxBuilder, test_challenger()); +} diff --git a/stark/miden-lifted-stark/src/testing/test_bus.rs b/stark/miden-lifted-stark/src/testing/test_bus.rs new file mode 100644 index 0000000000..213f240b74 --- /dev/null +++ b/stark/miden-lifted-stark/src/testing/test_bus.rs @@ -0,0 +1,312 @@ +//! Tests reduced auxiliary values (multiset and logup bus identities). + +use alloc::{vec, vec::Vec}; + +use miden_lifted_air::ReductionError; +use p3_field::{Field, PrimeCharacteristicRing}; +use p3_matrix::{Matrix, dense::RowMajorMatrix}; + +use crate::{ + AirInstance, AirWitness, + air::{ + AirBuilder, AuxBuilder, BaseAir, ExtensionBuilder, LiftedAir, LiftedAirBuilder, + ReducedAuxValues, VarLenPublicInputs, WindowAccess, + }, + prove_multi, + testing::configs::goldilocks_poseidon2::{ + Felt, QuadFelt, generate_pow4_trace, test_challenger, test_config, + }, + verify_multi, +}; + +// --------------------------------------------------------------------------- +// BusTestAir: exercises reduced_aux_values with multiset + logup buses. +// +// Main trace: 1 column, power-of-4 chain (same as TinyAir). +// Aux trace: 2 constant columns (all rows identical): +// col 0: 1/(pi_0 + challenge[0]) — inverse for multiset bus +// col 1: pi_1 + challenge[1] — accumulator for logup bus +// +// Aux values (committed to transcript, constrained to match aux trace last row): +// aux_values[0] = col 0 value = 1/(pi_0 + c0) +// aux_values[1] = col 1 value = pi_1 + c1 +// +// reduced_aux_values (verifier-side bus identity check): +// Bus 0 (multiset): prod = aux_values[0] * (c0 + pi_0) == 1 +// Bus 1 (logup): sum = (aux_values[1] - c1) - pi_1 == 0 +// +// pi_0, pi_1 appear in two places: +// - public_values[1..]: used by eval() for aux trace constraints +// - var_len_public_inputs: used by reduced_aux_values() for bus check +// Both must agree for the proof to verify. +// --------------------------------------------------------------------------- + +#[derive(Clone, Debug)] +struct BusTestAir; + +impl BaseAir for BusTestAir { + fn width(&self) -> usize { + 1 + } + + fn num_public_values(&self) -> usize { + 3 // [start, pi_0, pi_1] + } +} + +impl LiftedAir for BusTestAir { + fn num_randomness(&self) -> usize { + 2 + } + + fn aux_width(&self) -> usize { + 2 + } + + fn num_aux_values(&self) -> usize { + 2 + } + + fn num_var_len_public_inputs(&self) -> usize { + 2 + } + + fn reduced_aux_values( + &self, + aux_values: &[QuadFelt], + challenges: &[QuadFelt], + _public_values: &[Felt], + var_len_public_inputs: VarLenPublicInputs<'_, Felt>, + ) -> Result, ReductionError> { + // Bus 0 (multiset): prod = aux_values[0] * (challenges[0] + pi_0) + // aux_values[0] = 1/(pi_0 + c0), so prod == 1 when pi_0 matches. + let pi_0 = QuadFelt::from(var_len_public_inputs[0][0]); + let prod = aux_values[0] * (challenges[0] + pi_0); + + // Bus 1 (logup): sum = (aux_values[1] - challenges[1]) - pi_1 + // aux_values[1] = pi_1 + c1, so sum == 0 when pi_1 matches. + let pi_1 = QuadFelt::from(var_len_public_inputs[1][0]); + let sum = (aux_values[1] - challenges[1]) - pi_1; + + Ok(ReducedAuxValues { prod, sum }) + } + + fn eval>(&self, builder: &mut AB) { + // Copy public values upfront (PublicVar: Copy) to release borrow. + let pv0 = builder.public_values()[0]; + let pv1 = builder.public_values()[1]; + let pv2 = builder.public_values()[2]; + + let main = builder.main(); + let (local, next) = (main.current_slice(), main.next_slice()); + + // Main trace: power-of-4 chain + builder.when_first_row().assert_eq(local[0], pv0); + let main_pow4: AB::Expr = local[0].into().exp_power_of_2(2); + builder.when_transition().assert_eq(next[0], main_pow4); + + // Copy challenges and aux values (RandomVar/VarEF: Copy) to release borrow. + let c0: AB::RandomVar = builder.permutation_randomness()[0]; + let c1: AB::RandomVar = builder.permutation_randomness()[1]; + let av0: AB::PermutationVar = builder.permutation_values()[0].clone(); + let av1: AB::PermutationVar = builder.permutation_values()[1].clone(); + + let aux = builder.permutation(); + let aux_local = aux.current_slice(); + let aux_next = aux.next_slice(); + + // pi_0 = public_values[1], pi_1 = public_values[2] + let pi_0: AB::ExprEF = Into::::into(pv1).into(); + let pi_1: AB::ExprEF = Into::::into(pv2).into(); + let c0: AB::ExprEF = c0.into(); + let c1: AB::ExprEF = c1.into(); + + // First row: aux[0] * (pi_0 + c0) == 1 + let a0: AB::ExprEF = aux_local[0].into(); + builder.when_first_row().assert_eq_ext(a0 * (pi_0 + c0), AB::ExprEF::ONE); + + // First row: aux[1] == pi_1 + c1 + let a1: AB::ExprEF = aux_local[1].into(); + builder.when_first_row().assert_eq_ext(a1, pi_1 + c1); + + // Transition: constant columns + builder + .when_transition() + .assert_eq_ext::(aux_next[0].into(), aux_local[0].into()); + builder + .when_transition() + .assert_eq_ext::(aux_next[1].into(), aux_local[1].into()); + + // Last row: aux columns match aux_values + builder + .when_last_row() + .assert_eq_ext::(aux_local[0].into(), av0.into()); + builder + .when_last_row() + .assert_eq_ext::(aux_local[1].into(), av1.into()); + } +} + +// --------------------------------------------------------------------------- +// AuxBuilder: constant aux columns. +// --------------------------------------------------------------------------- + +struct BusTestAuxBuilder { + pi_0: Felt, + pi_1: Felt, +} + +impl AuxBuilder for BusTestAuxBuilder { + fn build_aux_trace( + &self, + main: &RowMajorMatrix, + challenges: &[QuadFelt], + ) -> (RowMajorMatrix, Vec) { + let height = main.height(); + let c0 = challenges[0]; + let c1 = challenges[1]; + + // col 0: 1/(pi_0 + c0), col 1: pi_1 + c1 + let col0_val = (QuadFelt::from(self.pi_0) + c0).inverse(); + let col1_val = QuadFelt::from(self.pi_1) + c1; + + let mut values = Vec::with_capacity(height * 2); + for _ in 0..height { + values.push(col0_val); + values.push(col1_val); + } + + let aux_trace = RowMajorMatrix::new(values, 2); + let aux_values = vec![col0_val, col1_val]; + (aux_trace, aux_values) + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[test] +fn bus_identity_check() { + let config = test_config(); + + let pi_0 = Felt::from_u64(42); + let pi_1 = Felt::from_u64(67); + let start = Felt::from_u64(2); + let height = 8; + + let air = BusTestAir; + let aux_builder = BusTestAuxBuilder { pi_0, pi_1 }; + let trace = generate_pow4_trace(start, height); + let public_values = vec![start, pi_0, pi_1]; + + // Build var_len_public_inputs (one reducible input per bus) + let input_0 = [pi_0]; + let input_1 = [pi_1]; + let var_len_pi: [&[Felt]; 2] = [&input_0, &input_1]; + + // Prove + let prover_instances = + [(&air, AirWitness::new(&trace, &public_values, &var_len_pi), &aux_builder)]; + let output = + prove_multi(&config, &prover_instances, test_challenger()).expect("proving should succeed"); + + let instance = AirInstance { + public_values: &public_values, + var_len_public_inputs: &var_len_pi, + }; + + // Verify + let verifier_digest = + verify_multi(&config, &[(&air, instance)], &output.proof, test_challenger()) + .expect("verification should succeed"); + assert_eq!(output.digest, verifier_digest); +} + +#[test] +fn bus_wrong_var_len_pi_fails() { + let config = test_config(); + + let pi_0 = Felt::from_u64(42); + let pi_1 = Felt::from_u64(67); + let start = Felt::from_u64(2); + let height = 8; + + let air = BusTestAir; + let aux_builder = BusTestAuxBuilder { pi_0, pi_1 }; + let trace = generate_pow4_trace(start, height); + let public_values = vec![start, pi_0, pi_1]; + + // Prove with correct values + let input_0 = [pi_0]; + let input_1 = [pi_1]; + let var_len_pi: [&[Felt]; 2] = [&input_0, &input_1]; + + let prover_instances = + [(&air, AirWitness::new(&trace, &public_values, &var_len_pi), &aux_builder)]; + let output = + prove_multi(&config, &prover_instances, test_challenger()).expect("proving should succeed"); + + // Verify with WRONG var_len_public_inputs (99 instead of 42) + let wrong_pi_0 = Felt::from_u64(99); + let wrong_input_0 = [wrong_pi_0]; + let wrong_var_len_pi: [&[Felt]; 2] = [&wrong_input_0, &input_1]; + + let instance = AirInstance { + public_values: &public_values, + var_len_public_inputs: &wrong_var_len_pi, + }; + + let err = verify_multi(&config, &[(&air, instance)], &output.proof, test_challenger()) + .expect_err("wrong var_len_pi should fail verification"); + + assert!( + matches!(err, crate::VerifierError::InvalidReducedAux), + "expected InvalidReducedAux, got {err:?}" + ); +} + +#[test] +fn bus_wrong_input_count_fails() { + let config = test_config(); + + let pi_0 = Felt::from_u64(42); + let pi_1 = Felt::from_u64(67); + let start = Felt::from_u64(2); + let height = 8; + + let air = BusTestAir; + let aux_builder = BusTestAuxBuilder { pi_0, pi_1 }; + let trace = generate_pow4_trace(start, height); + let public_values = vec![start, pi_0, pi_1]; + + // Prove with correct values + let input_0 = [pi_0]; + let input_1 = [pi_1]; + let var_len_pi: [&[Felt]; 2] = [&input_0, &input_1]; + + let prover_instances = + [(&air, AirWitness::new(&trace, &public_values, &var_len_pi), &aux_builder)]; + let output = + prove_multi(&config, &prover_instances, test_challenger()).expect("proving should succeed"); + + // Verify with WRONG input count (1 instead of 2) + let only_one: [&[Felt]; 1] = [&input_0]; + let instance = AirInstance { + public_values: &public_values, + var_len_public_inputs: &only_one, + }; + + let err = verify_multi(&config, &[(&air, instance)], &output.proof, test_challenger()) + .expect_err("wrong input count should fail verification"); + + assert!( + matches!( + err, + crate::VerifierError::Instance( + crate::InstanceValidationError::VarLenPublicInputsMismatch { .. } + ) + ), + "expected VarLenPublicInputsMismatch, got {err:?}" + ); +} diff --git a/stark/miden-lifted-stark/src/testing/test_multi_aux_alignment.rs b/stark/miden-lifted-stark/src/testing/test_multi_aux_alignment.rs new file mode 100644 index 0000000000..2bbaf71d28 --- /dev/null +++ b/stark/miden-lifted-stark/src/testing/test_multi_aux_alignment.rs @@ -0,0 +1,166 @@ +//! Tests LMCS alignment with padding for multi-trace proving/verification. + +use alloc::{vec, vec::Vec}; + +use p3_field::PrimeCharacteristicRing; +use p3_matrix::{Matrix, dense::RowMajorMatrix}; + +use crate::{ + AirWitness, Lmcs, VerifierError, + air::{ + AirBuilder, AuxBuilder, BaseAir, ExtensionBuilder, LiftedAir, LiftedAirBuilder, + WindowAccess, + }, + prove_multi, + testing::configs::goldilocks_poseidon2::{ + Felt, QuadFelt, prove_and_verify_instances, test_challenger, test_config, + }, + transcript::TranscriptData, + verify_multi, +}; + +#[derive(Clone, Debug)] +struct PaddingAir { + width: usize, + aux_width: usize, +} + +impl PaddingAir { + fn new(width: usize, aux_width: usize) -> Self { + Self { width, aux_width } + } +} + +impl BaseAir for PaddingAir { + fn width(&self) -> usize { + self.width + } + + fn num_public_values(&self) -> usize { + 1 + } +} + +impl LiftedAir for PaddingAir { + fn num_randomness(&self) -> usize { + 1 + } + + fn aux_width(&self) -> usize { + self.aux_width + } + + fn num_aux_values(&self) -> usize { + 0 + } + + fn num_var_len_public_inputs(&self) -> usize { + 0 + } + + fn eval>(&self, builder: &mut AB) { + let main = builder.main(); + let start = builder.public_values()[0]; + let (local, next) = (main.current_slice(), main.next_slice()); + + builder.when_first_row().assert_eq(local[0], start); + builder.when_transition().assert_eq(next[0], local[0]); + + let aux = builder.permutation(); + let aux_local = aux.current_slice(); + let aux_next = aux.next_slice(); + let challenge: AB::ExprEF = builder.permutation_randomness()[0].into(); + builder.when_first_row().assert_eq_ext(aux_local[0].into(), challenge); + builder.when_transition().assert_eq_ext(aux_next[0].into(), aux_local[0].into()); + } +} + +struct PaddingAuxBuilder { + aux_width: usize, +} + +impl AuxBuilder for PaddingAuxBuilder { + fn build_aux_trace( + &self, + main: &RowMajorMatrix, + challenges: &[QuadFelt], + ) -> (RowMajorMatrix, Vec) { + let height = main.height(); + let mut values = Vec::with_capacity(height * self.aux_width); + let challenge = challenges[0]; + for _ in 0..height { + values.push(challenge); + values.extend(core::iter::repeat_n(QuadFelt::ZERO, self.aux_width - 1)); + } + let aux_trace = RowMajorMatrix::new(values, self.aux_width); + (aux_trace, vec![]) + } +} + +fn generate_trace(start: Felt, height: usize, width: usize) -> RowMajorMatrix { + let mut values = Vec::with_capacity(height * width); + for _ in 0..height { + values.push(start); + values.extend(core::iter::repeat_n(Felt::ZERO, width - 1)); + } + RowMajorMatrix::new(values, width) +} + +fn instance(idx: usize, height: usize, width: usize) -> (RowMajorMatrix, Vec) { + let start = Felt::from_u64((idx + 2) as u64); + (generate_trace(start, height, width), vec![start]) +} + +#[test] +fn multi_trace_with_aux_padding() { + let config = test_config(); + let alignment = config.lmcs.alignment(); + let width = alignment + 1; + let aux_width = alignment + 1; + + let air = PaddingAir::new(width, aux_width); + let aux_builder = PaddingAuxBuilder { aux_width }; + let instances = [instance(0, 8, width), instance(1, 16, width)]; + + let prover_instances: Vec<_> = instances + .iter() + .map(|(t, pv)| (&air, AirWitness::new(t, pv, &[]), &aux_builder)) + .collect(); + + prove_and_verify_instances(&prover_instances); +} + +#[test] +fn multi_trace_rejects_trailing_transcript_data() { + let config = test_config(); + let alignment = config.lmcs.alignment(); + let width = alignment + 1; + let aux_width = alignment + 1; + + let air = PaddingAir::new(width, aux_width); + let aux_builder = PaddingAuxBuilder { aux_width }; + let instances = [instance(0, 8, width), instance(1, 16, width)]; + + let prover_instances: Vec<_> = instances + .iter() + .map(|(t, pv)| (&air, AirWitness::new(t, pv, &[]), &aux_builder)) + .collect(); + + let output = + prove_multi(&config, &prover_instances, test_challenger()).expect("proving should succeed"); + + let mut bad_proof = output.proof; + let (mut fields, commitments) = bad_proof.transcript.into_parts(); + fields.push(Felt::ONE); + bad_proof.transcript = TranscriptData::new(fields, commitments); + + let verifier_instances: Vec<_> = + prover_instances.iter().map(|(a, w, _)| (*a, w.to_instance())).collect(); + + let err = verify_multi(&config, &verifier_instances, &bad_proof, test_challenger()) + .expect_err("extra transcript data should fail verification"); + assert!(matches!( + err, + VerifierError::Transcript(crate::transcript::TranscriptError::TrailingData) + )); +} diff --git a/stark/miden-lifted-stark/src/testing/test_tiny_air.rs b/stark/miden-lifted-stark/src/testing/test_tiny_air.rs new file mode 100644 index 0000000000..2f09430d9a --- /dev/null +++ b/stark/miden-lifted-stark/src/testing/test_tiny_air.rs @@ -0,0 +1,421 @@ +//! Integration tests for the lifted STARK prove/verify cycle using a minimal +//! single-column AIR with periodic columns and auxiliary traces. + +use alloc::{vec, vec::Vec}; + +use p3_field::PrimeCharacteristicRing; +use p3_matrix::{Matrix, dense::RowMajorMatrix}; + +use crate::{ + AirWitness, InstanceValidationError, ProverError, VerifierError, + air::{ + AirBuilder, AuxBuilder, BaseAir, ExtensionBuilder, LiftedAir, LiftedAirBuilder, + WindowAccess, + }, + prove_multi, prove_single, + testing::configs::goldilocks_poseidon2::{ + Felt, QuadFelt, generate_pow4_trace, prove_and_verify, test_challenger, test_config, + }, + transcript::TranscriptData, + verify_single, +}; + +// --------------------------------------------------------------------------- +// TinyAir: main[0] starts at public_values[0], each row is previous^4. +// Optional periodic columns with pattern [1, 0, ..., 0, 1] per period. +// --------------------------------------------------------------------------- + +#[derive(Clone, Debug)] +struct TinyAir { + /// Pre-computed periodic column data. + periodic_cols: Vec>, +} + +impl TinyAir { + fn new(periods: Vec) -> Self { + let periodic_cols = periods + .iter() + .map(|&p| { + let mut col = vec![Felt::ZERO; p]; + col[0] = Felt::ONE; + col[p - 1] = Felt::ONE; + col + }) + .collect(); + Self { periodic_cols } + } +} + +impl BaseAir for TinyAir { + fn width(&self) -> usize { + 1 + } + + fn num_public_values(&self) -> usize { + 1 + } +} + +impl LiftedAir for TinyAir { + fn periodic_columns(&self) -> Vec> { + self.periodic_cols.clone() + } + + fn num_randomness(&self) -> usize { + 1 + } + + fn aux_width(&self) -> usize { + 1 + } + + fn num_aux_values(&self) -> usize { + 1 + } + + fn num_var_len_public_inputs(&self) -> usize { + 0 + } + + fn eval>(&self, builder: &mut AB) { + let main = builder.main(); + let start = builder.public_values()[0]; + let periodic = builder.periodic_values().to_vec(); + let (local, next) = (main.current_slice(), main.next_slice()); + + // First row: main[0] = public_values[0] + builder.when_first_row().assert_eq(local[0], start); + + // Transition: main_next = main^4 + let main_pow4: AB::Expr = local[0].into().exp_power_of_2(2); + builder.when_transition().assert_eq(next[0], main_pow4); + + // Periodic column constraints: first and last row see 1 + for p in &periodic { + let p_expr: AB::Expr = (*p).into(); + builder.when_first_row().assert_one(p_expr.clone()); + builder.when_last_row().assert_one(p_expr); + } + + // Aux trace constraints + let aux = builder.permutation(); + let aux_local = aux.current_slice(); + let aux_next = aux.next_slice(); + let challenge: AB::ExprEF = builder.permutation_randomness()[0].into(); + + let aux_local_ef: AB::ExprEF = aux_local[0].into(); + builder.when_first_row().assert_eq_ext(aux_local_ef.clone(), challenge); + + let aux_pow4: AB::ExprEF = aux_local_ef.exp_power_of_2(2); + builder.when_transition().assert_eq_ext(aux_next[0].into(), aux_pow4); + } +} + +/// AuxBuilder for TinyAir: aux column = challenge^{4^row}. +struct TinyAuxBuilder; + +impl AuxBuilder for TinyAuxBuilder { + fn build_aux_trace( + &self, + main: &RowMajorMatrix, + challenges: &[QuadFelt], + ) -> (RowMajorMatrix, Vec) { + let height = main.height(); + let challenge = challenges[0]; + + let mut col_values = Vec::with_capacity(height); + let mut current = challenge; + for _ in 0..height { + col_values.push(current); + current = current.exp_power_of_2(2); + } + + let aux_trace = RowMajorMatrix::new(col_values.clone(), 1); + let aux_values = vec![col_values[height - 1]]; + (aux_trace, aux_values) + } +} + +/// Build a (trace, public_values) pair for instance `idx`. +fn instance(idx: usize, height: usize) -> (RowMajorMatrix, Vec) { + let start = Felt::from_u64((idx + 2) as u64); + (generate_pow4_trace(start, height), vec![start]) +} + +// --------------------------------------------------------------------------- +// Single-trace tests +// --------------------------------------------------------------------------- + +#[test] +fn single_trace() { + prove_and_verify(&TinyAir::new(vec![]), &TinyAuxBuilder, &[instance(0, 8)]); +} + +#[test] +fn malformed_transcript_is_rejected() { + let config = test_config(); + let air = TinyAir::new(vec![]); + + let (trace, public_values) = instance(0, 4); + + let output = prove_single( + &config, + &air, + &trace, + &public_values, + &[], + &TinyAuxBuilder, + test_challenger(), + ) + .expect("proving should succeed"); + + // Baseline should verify + let _digest = + verify_single(&config, &air, &public_values, &[], &output.proof, test_challenger()) + .expect("baseline proof should verify"); + + // Extra field element should cause rejection + let mut bad_proof = output.proof; + let (mut fields, commitments) = bad_proof.transcript.into_parts(); + fields.push(Felt::ONE); + bad_proof.transcript = TranscriptData::new(fields, commitments); + + let err = verify_single(&config, &air, &public_values, &[], &bad_proof, test_challenger()) + .expect_err("extra transcript data should fail verification"); + assert!(matches!( + err, + VerifierError::Transcript(crate::transcript::TranscriptError::TrailingData) + )); +} + +#[test] +fn malformed_log_trace_heights_is_rejected() { + let config = test_config(); + let air = TinyAir::new(vec![]); + + let (trace, public_values) = instance(0, 4); + + let output = prove_single( + &config, + &air, + &trace, + &public_values, + &[], + &TinyAuxBuilder, + test_challenger(), + ) + .expect("proving should succeed"); + + // Push straight to the `pub(crate)` field to bypass + // `InstanceShapes::from_trace_heights` and exercise the verifier-path + // bound check in `validate_inputs`. + let mut bad_proof = output.proof.clone(); + bad_proof.instance_shapes.log_trace_heights.push(2); + bad_proof.instance_shapes.air_order.push(1); + let err = verify_single(&config, &air, &public_values, &[], &bad_proof, test_challenger()) + .expect_err("extra log trace height should fail verification"); + assert!(matches!( + err, + VerifierError::Instance(InstanceValidationError::AirOrderLengthMismatch { + instances: 1, + air_order: 2, + }) + )); + + // Empty heights → air_order / instance count mismatch. + let mut bad_proof = output.proof.clone(); + bad_proof.instance_shapes.log_trace_heights.clear(); + bad_proof.instance_shapes.air_order.clear(); + let err = verify_single(&config, &air, &public_values, &[], &bad_proof, test_challenger()) + .expect_err("empty log trace heights should fail verification"); + assert!(matches!( + err, + VerifierError::Instance(InstanceValidationError::AirOrderLengthMismatch { + instances: 1, + air_order: 0, + }) + )); + + // Out-of-range log height must surface as an error, not panic on + // `1usize << log_h` or `two_adic_generator(log_h + log_blowup)`. + let mut bad_proof = output.proof.clone(); + bad_proof.instance_shapes.log_trace_heights = vec![200]; + let err = verify_single(&config, &air, &public_values, &[], &bad_proof, test_challenger()) + .expect_err("oversized log trace height should fail verification"); + assert!(matches!( + err, + VerifierError::Instance(InstanceValidationError::LdeDomainExceedsTwoAdicity { + log_h: 200, + .. + }) + )); + + // Boundary case: `log_h` fits the raw bound (`log_h ≤ TWO_ADICITY`) but + // the LDE domain `log_h + log_blowup` does not. With `log_blowup = 2` + // from `TEST_PCS_PARAMS` and `Felt::TWO_ADICITY = 32`, `31 + 2 = 33 > 32` + // must be rejected before any `two_adic_generator` call on the LDE domain. + let mut bad_proof = output.proof; + bad_proof.instance_shapes.log_trace_heights = vec![31]; + let err = verify_single(&config, &air, &public_values, &[], &bad_proof, test_challenger()) + .expect_err("log_h + log_blowup exceeding two-adicity should fail verification"); + assert!(matches!( + err, + VerifierError::Instance(InstanceValidationError::LdeDomainExceedsTwoAdicity { + log_h: 31, + log_blowup: 2, + .. + }) + )); +} + +#[test] +fn prover_rejects_non_power_of_two_trace_height() { + // Build the witness directly (via `pub` fields) to skip the + // power-of-two assertion in `AirWitness::new`. `InstanceShapes::from_trace_heights` + // must reject it rather than panicking inside `log2_strict_u8`. + let config = test_config(); + let air = TinyAir::new(vec![]); + + let trace = + RowMajorMatrix::new(vec![Felt::from_u64(2), Felt::from_u64(16), Felt::from_u64(65536)], 1); + let public_values = vec![Felt::from_u64(2)]; + let bad_witness = AirWitness { + trace: &trace, + public_values: &public_values, + var_len_public_inputs: &[], + }; + + let result = prove_multi(&config, &[(&air, bad_witness, &TinyAuxBuilder)], test_challenger()); + match result { + Err(ProverError::Instance(InstanceValidationError::InvalidTraceHeight { height: 3 })) => {}, + Err(other) => panic!("expected InvalidTraceHeight {{ height: 3 }}, got {other:?}"), + Ok(_) => panic!("non-power-of-two trace height should fail proving"), + } +} + +// --------------------------------------------------------------------------- +// Multi-trace tests +// --------------------------------------------------------------------------- + +#[test] +fn two_traces_same_height() { + prove_and_verify(&TinyAir::new(vec![]), &TinyAuxBuilder, &[instance(0, 8), instance(1, 8)]); +} + +#[test] +fn two_traces_different_heights() { + prove_and_verify(&TinyAir::new(vec![]), &TinyAuxBuilder, &[instance(0, 4), instance(1, 8)]); +} + +#[test] +fn three_traces_ascending_heights() { + prove_and_verify( + &TinyAir::new(vec![]), + &TinyAuxBuilder, + &[instance(0, 4), instance(1, 8), instance(2, 16)], + ); +} + +// --------------------------------------------------------------------------- +// Unordered multi-trace tests (instances not in ascending height order) +// --------------------------------------------------------------------------- + +#[test] +fn two_traces_reversed_order() { + prove_and_verify(&TinyAir::new(vec![]), &TinyAuxBuilder, &[instance(1, 8), instance(0, 4)]); +} + +#[test] +fn three_traces_descending_heights() { + prove_and_verify( + &TinyAir::new(vec![]), + &TinyAuxBuilder, + &[instance(2, 16), instance(1, 8), instance(0, 4)], + ); +} + +#[test] +fn three_traces_shuffled_order() { + prove_and_verify( + &TinyAir::new(vec![]), + &TinyAuxBuilder, + &[instance(1, 8), instance(2, 16), instance(0, 4)], + ); +} + +#[test] +fn periodic_columns_reversed_order() { + prove_and_verify(&TinyAir::new(vec![2, 4]), &TinyAuxBuilder, &[instance(1, 8), instance(0, 4)]); +} + +#[test] +fn air_order_reflects_caller_order() { + let config = test_config(); + let air = TinyAir::new(vec![]); + + // Pass instances in reverse height order: [height=8, height=4]. + let (t0, pv0) = instance(0, 8); + let (t1, pv1) = instance(1, 4); + + let w0 = AirWitness::new(&t0, &pv0, &[]); + let w1 = AirWitness::new(&t1, &pv1, &[]); + + let output = prove_multi( + &config, + &[(&air, w0, &TinyAuxBuilder), (&air, w1, &TinyAuxBuilder)], + test_challenger(), + ) + .expect("proving should succeed"); + + // Proof ordering is ascending height: [height=4, height=8]. + // Caller index 1 (height=4) is at position 0 in the proof's ordering. + // Caller index 0 (height=8) is at position 1 in the proof's ordering. + let air_order = output.proof.instance_shapes.air_order(); + assert_eq!( + air_order, + &[1, 0], + "air_order should map ascending-height position → caller index" + ); + + // Log trace heights should be in ascending order. + let log_heights = output.proof.instance_shapes.log_trace_heights(); + assert_eq!(log_heights, &[2, 3], "log heights should be ascending (4=2^2, 8=2^3)"); +} + +// --------------------------------------------------------------------------- +// Periodic column tests +// --------------------------------------------------------------------------- + +#[test] +fn single_periodic_column() { + prove_and_verify(&TinyAir::new(vec![2]), &TinyAuxBuilder, &[instance(0, 8)]); +} + +#[test] +fn periodic_column_period_4() { + prove_and_verify(&TinyAir::new(vec![4]), &TinyAuxBuilder, &[instance(0, 8)]); +} + +#[test] +fn multiple_periodic_columns() { + prove_and_verify(&TinyAir::new(vec![2, 4]), &TinyAuxBuilder, &[instance(0, 8)]); +} + +#[test] +fn periodic_columns_multi_trace_same_height() { + prove_and_verify(&TinyAir::new(vec![2]), &TinyAuxBuilder, &[instance(0, 8), instance(1, 8)]); +} + +#[test] +fn periodic_columns_multi_trace_different_heights() { + prove_and_verify(&TinyAir::new(vec![2, 4]), &TinyAuxBuilder, &[instance(0, 4), instance(1, 8)]); +} + +#[test] +fn periodic_columns_three_traces() { + prove_and_verify( + &TinyAir::new(vec![2, 4]), + &TinyAuxBuilder, + &[instance(0, 4), instance(1, 8), instance(2, 16)], + ); +} diff --git a/stark/miden-lifted-stark/src/verifier/README.md b/stark/miden-lifted-stark/src/verifier/README.md new file mode 100644 index 0000000000..7fc5c69a4a --- /dev/null +++ b/stark/miden-lifted-stark/src/verifier/README.md @@ -0,0 +1,227 @@ +# Lifted STARK Verifier + +End-to-end verification for the lifted STARK protocol using LMCS +commitments and the lifted FRI PCS. Supports multiple traces of different +power-of-two heights via virtual lifting. + +Protocol-level overview lives in `miden-lifted-stark/README.md`. + +## Entry Points + +| Item | Purpose | +|------|---------| +| `verify_single` | Verify a single-AIR proof | +| `verify_multi` | Verify a multi-trace proof | +| `AirInstance` | Public values + variable-length inputs for one AIR | +| `StarkProof` | Log trace heights + raw transcript data | + +```text +verify_single(config, air, public_values, var_len_public_inputs, proof, challenger) +verify_multi(config, &[(air, instance), ...], proof, challenger) +``` + +The proof is read from the provided transcript channel. This crate does not +prescribe the *initial* challenger state used for Fiat-Shamir. + +## Fiat-Shamir / statement binding + +The caller must produce the same challenger state as the prover — see the +prover module-level docs for the full binding contract. + +## Transcript boundaries + +`verify_multi` rejects trailing transcript data (`TranscriptNotConsumed`). If you +bundle extra data in the same transcript, you must manage boundaries yourself. + +## Protocol flow + +0. Validate `air_order` from the proof and reorder caller instances to match. +1. Observe log trace heights into the challenger (from proof, not transcript). +2. Receive main trace commitment. +3. Sample aux randomness. +4. Receive aux trace commitment. +5. Sample constraint folding challenge `alpha` and cross-trace accumulator `beta`. +6. Receive quotient commitment. +7. Sample OOD point `z` (rejection-sampled outside trace domain), derive `z_next`. +8. Verify PCS openings at `[z, z_next]` for main, aux, and quotient. +9. Reconstruct `Q(z)` from the opened quotient chunks. +10. For each trace instance j, set `y_j = z^{r_j}` and evaluate folded constraints at `y_j`. +11. Accumulate across traces with `beta`. +12. Check quotient identity: `accumulated == Q(z) * (z^N - 1)`. +13. Ensure transcript is fully consumed. + +## Mathematical background + +This section assumes familiarity with STARK verifiers and explains how +*lifting* lets us verify mixed-height traces using a single uniform +opening point and a single quotient identity. All sizes are powers of two. + +### Domains and cosets + +Let: + +- $N = 2^n$ be the **maximum** trace height across all traces. +- $D = 2^d$ be the **constraint degree** (quotient-domain blowup). +- $B = 2^b$ be the **PCS/FRI blowup**, with $D \le B$. +- $g$ be the fixed multiplicative shift (`F::GENERATOR`). + +Define subgroups: + +$$ +H = \langle \omega_H \rangle,\ |H| = N +\qquad +J = \langle \omega_J \rangle,\ |J| = N\,D +\qquad +K = \langle \omega_K \rangle,\ |K| = N\,B +$$ + +and shifted cosets $gH, gJ, gK$. + +### What "lifted traces" mean to the verifier + +Suppose instance $j$ has trace height + +$$ +n_j = N / r_j +\qquad\text{with } r_j = 2^{\ell_j}. +$$ + +Let $t_j(X)$ be the degree-$ +where + F: Field, + EF: ExtensionField, +{ + pub main: RowWindow<'a, EF>, + pub aux: RowWindow<'a, EF>, + pub randomness: &'a [EF], + pub public_values: &'a [F], + pub periodic_values: &'a [EF], + pub permutation_values: &'a [EF], + pub selectors: Selectors, + pub alpha: EF, + pub accumulator: EF, + pub _phantom: PhantomData, +} + +impl<'a, F, EF> AirBuilder for ConstraintFolder<'a, F, EF> +where + F: Field, + EF: ExtensionField, +{ + type F = F; + type Expr = EF; + type Var = EF; + type PreprocessedWindow = EmptyWindow; + type MainWindow = RowWindow<'a, EF>; + type PublicVar = F; + + fn main(&self) -> Self::MainWindow { + self.main + } + + fn preprocessed(&self) -> &Self::PreprocessedWindow { + EmptyWindow::empty_ref() + } + + fn is_first_row(&self) -> Self::Expr { + self.selectors.is_first_row + } + + fn is_last_row(&self) -> Self::Expr { + self.selectors.is_last_row + } + + fn is_transition_window(&self, size: usize) -> Self::Expr { + if size == 2 { + self.selectors.is_transition + } else { + panic!("only window size 2 supported in this prototype") + } + } + + fn assert_zero>(&mut self, x: I) { + self.accumulator = self.accumulator * self.alpha + x.into(); + } + + fn public_values(&self) -> &[Self::PublicVar] { + self.public_values + } +} + +impl<'a, F, EF> ExtensionBuilder for ConstraintFolder<'a, F, EF> +where + F: Field, + EF: ExtensionField, +{ + type EF = EF; + type ExprEF = EF; + type VarEF = EF; + + fn assert_zero_ext(&mut self, x: I) + where + I: Into, + { + self.accumulator = self.accumulator * self.alpha + x.into(); + } +} + +impl<'a, F, EF> PermutationAirBuilder for ConstraintFolder<'a, F, EF> +where + F: Field, + EF: ExtensionField, +{ + type MP = RowWindow<'a, EF>; + type RandomVar = EF; + type PermutationVar = EF; + + fn permutation(&self) -> Self::MP { + self.aux + } + + fn permutation_randomness(&self) -> &[Self::RandomVar] { + self.randomness + } + + fn permutation_values(&self) -> &[Self::PermutationVar] { + self.permutation_values + } +} + +impl<'a, F, EF> PeriodicAirBuilder for ConstraintFolder<'a, F, EF> +where + F: Field, + EF: ExtensionField, +{ + type PeriodicVar = EF; + + fn periodic_values(&self) -> &[Self::PeriodicVar] { + self.periodic_values + } +} + +// ============================================================================ +// Quotient Reconstruction +// ============================================================================ + +/// Reconstruct `Q(z)` from `D` quotient chunk evaluations. +/// +/// The quotient `Q` is committed as `D` chunk polynomials qₜ of degree `< N`, one for +/// each `H`-coset inside `J`: +/// +/// qₜ agrees with `Q` on the coset `g·ω_Jᵗ·H`. +/// +/// During verification we open all qₜ(z) at the same OOD point `z` and need to +/// recombine them into `Q(z)`. +/// +/// The key observation is that the map `x → xᴺ` collapses each coset +/// `g·ω_Jᵗ·H` to a single `D`-th root of unity. Let +/// - ωₛ = ω_Jᴺ (a `D`-th root of unity), +/// - u = (z/s)ᴺ where s = coset.lde_shift(). +/// +/// Then `Q(z)` is the barycentric interpolation of the values qₜ(z) at the points +/// ωₛᵗ: +/// +/// ```text +/// wₜ = ωₛᵗ / (u − ωₛᵗ) +/// Q(z) = (Σₜ wₜ·qₜ(z)) / (Σₜ wₜ) +/// ``` +pub fn reconstruct_quotient(z: EF, coset: &LiftedCoset, chunks: &[EF]) -> EF +where + F: TwoAdicField, + EF: ExtensionField, +{ + let log_d = log2_strict_usize(chunks.len()); + let shift: F = coset.lde_shift(); + let omega_s = F::two_adic_generator(log_d); + + // u = (z/s)ᴺ where s = lde_shift + let u = (z * shift.inverse()).exp_power_of_2(coset.log_trace_height as usize); + + // Compute weighted sum: Σₜ wₜ·qₜ(z) and Σₜ wₜ + let mut numerator = EF::ZERO; + let mut denominator = EF::ZERO; + let mut omega_s_t = F::ONE; // ωₛᵗ + + for &q_t in chunks.iter() { + let a_t = u - omega_s_t; // aₜ = u − ωₛᵗ + let w_t = a_t.inverse() * omega_s_t; // wₜ = ωₛᵗ / aₜ + + numerator += w_t * q_t; + denominator += w_t; + + omega_s_t *= omega_s; + } + + numerator * denominator.inverse() +} + +/// Reconstitute EF elements from opened base field polynomial evaluations. +/// +/// When an EF polynomial is committed, it becomes DIM base field polynomials. +/// Opening at EF point z gives DIM EF values (F-polys evaluated at EF point). +/// Reconstruct each EF element: `vᵢ = Σⱼ basisⱼ·row[i·DIM + j]`. +/// +/// An EF element `v = Σⱼ cⱼ·basisⱼ` is committed as DIM base field polynomials pⱼ +/// (one per basis coordinate cⱼ). Opening at `z` returns the DIM values pⱼ(z), and we +/// recover the original EF value as `v(z) = Σⱼ basisⱼ·pⱼ(z)`. +pub fn row_to_packed_ext(row: &[EF]) -> Result, VerifierError> +where + F: TwoAdicField, + EF: ExtensionField, +{ + if !row.len().is_multiple_of(EF::DIMENSION) { + return Err(VerifierError::InvalidAuxShape); + } + let num_elements = row.len() / EF::DIMENSION; + Ok((0..num_elements) + .map(|i| { + let start = i * EF::DIMENSION; + (0..EF::DIMENSION) + .map(|j| EF::ith_basis_element(j).unwrap() * row[start + j]) + .sum() + }) + .collect()) +} diff --git a/stark/miden-lifted-stark/src/verifier/mod.rs b/stark/miden-lifted-stark/src/verifier/mod.rs new file mode 100644 index 0000000000..0c33fff097 --- /dev/null +++ b/stark/miden-lifted-stark/src/verifier/mod.rs @@ -0,0 +1,340 @@ +//! Lifted STARK verifier. +//! +//! This module provides: +//! - [`verify_single`]: Verify a single AIR instance. +//! - [`verify_multi`]: Verify multiple AIR instances with traces of different heights. +//! +//! These functions take a challenger (consumed by value) and proof data, construct +//! the verifier transcript internally, and return a [`StarkDigest`] on success. +//! The caller must check that the digest matches the prover's digest. +//! +//! # Fiat-Shamir / transcript binding +//! +//! The caller must produce the same challenger state as the prover — see the +//! prover module-level docs for the full binding contract and recommended +//! pattern. +//! +//! Log trace heights are carried on the [`StarkProof`] and observed into the +//! challenger by [`verify_multi`]. Callers must not pre-observe them. +//! +//! # Statement-bound trace heights +//! +//! The verifier accepts whatever trace heights the proof carries; it never +//! compares them against a caller-supplied expectation. If your statement +//! fixes the trace size (e.g. a proof for a 2^16-row execution), parse it +//! with +//! [`StarkTranscript::from_proof`](crate::proof::StarkTranscript::from_proof) +//! and check `transcript.instance_shapes.log_trace_heights()` yourself. +//! +//! # Transcript boundaries (strict consumption) +//! +//! [`verify_multi`] finalizes the transcript internally: it rejects proofs with +//! trailing data (via [`TranscriptError::TrailingData`]) and returns a binding +//! digest that must match the prover's digest. +//! +//! If you want to bundle extra data alongside the proof, you must manage +//! boundaries yourself (e.g. parse and validate that data first, then pass the +//! remaining transcript to [`verify_multi`]). + +extern crate alloc; + +pub mod constraints; +pub mod periodic; + +use alloc::{vec, vec::Vec}; +use core::marker::PhantomData; + +use constraints::{ConstraintFolder, reconstruct_quotient, row_to_packed_ext}; +use miden_lifted_air::{ + LiftedAir, ReducedAuxValues, ReductionError, RowWindow, VarLenPublicInputs, +}; +use miden_stark_transcript::{Channel, TranscriptError, VerifierChannel, VerifierTranscript}; +use p3_field::{ExtensionField, TwoAdicField}; +use p3_matrix::Matrix; +use periodic::PeriodicPolys; +use thiserror::Error; + +use crate::{ + StarkConfig, + coset::LiftedCoset, + instance::{AirInstance, InstanceValidationError, validate_air_order, validate_inputs}, + pcs::verifier::{PcsError, verify_aligned}, + proof::{StarkDigest, StarkProof}, +}; + +/// Errors that can occur during verification. +#[derive(Debug, Error)] +pub enum VerifierError { + #[error("instance validation failed: {0}")] + Instance(#[from] InstanceValidationError), + #[error("PCS verification failed: {0}")] + Pcs(#[from] PcsError), + #[error("transcript error: {0}")] + Transcript(#[from] TranscriptError), + #[error("invalid aux shape")] + InvalidAuxShape, + #[error("constraint mismatch: quotient * vanishing != folded constraints")] + ConstraintMismatch, + #[error( + "constraint degree exceeds blowup: \ + log_quotient_degree {log_quotient_degree} > log_blowup {log_blowup}" + )] + ConstraintDegreeTooHigh { log_quotient_degree: u8, log_blowup: u8 }, + #[error("global reduced aux identity check failed")] + InvalidReducedAux, + #[error("aux value reduction failed: {0}")] + Reduction(ReductionError), +} + +/// Verify a single AIR. Convenience wrapper around [`verify_multi`]. +/// +/// The caller's challenger must already be bound to the full statement +/// — see the prover module-level docs. +pub fn verify_single( + config: &SC, + air: &A, + public_values: &[F], + var_len_public_inputs: VarLenPublicInputs<'_, F>, + proof: &StarkProof, + challenger: SC::Challenger, +) -> Result, VerifierError> +where + F: TwoAdicField, + EF: ExtensionField, + SC: StarkConfig, + A: LiftedAir, +{ + let instance = AirInstance { public_values, var_len_public_inputs }; + verify_multi(config, &[(air, instance)], proof, challenger) +} + +/// Verify multiple AIRs with traces of different heights. +/// +/// The verifier uses [`InstanceShapes::air_order`](crate::InstanceShapes::air_order) from the proof +/// to match the caller's instances to the proof's ordering. The caller's challenger +/// must already be bound to the full statement (protocol parameters, AIR +/// configurations, AIR ordering, and public inputs — both fixed and +/// variable-length) — see the prover module-level docs. +/// +/// The verifier mirrors the prover's protocol: +/// +/// 1. Validate instance shapes and observe log trace heights into the challenger +/// 2. Receive commitments and sample challenges in the same order as the prover +/// 3. For each AIR, evaluate constraints at the lifted OOD point yⱼ = z^{rⱼ} +/// 4. Accumulate folded constraints with β: acc = acc·β + foldedⱼ +/// 5. Check quotient identity: `acc == Q(z) * Z_{H_max}(z)` +/// +/// Lifting: for a trace of height nⱼ lifted by factor rⱼ, the committed +/// codeword encodes `p_lift(X) = p(X^{rⱼ})`; opening at `[z, z · h_max]` +/// yields the local/next row pair for the original trace domain. +/// +/// **Statement-bound heights:** this function does not compare the proof's +/// declared heights against any caller expectation. If your statement fixes +/// trace dimensions, parse via +/// [`StarkTranscript::from_proof`](crate::proof::StarkTranscript::from_proof) +/// and check `instance_shapes.log_trace_heights()` before calling this. See +/// the module-level docs for the full contract. +pub fn verify_multi( + config: &SC, + instances: &[(&A, AirInstance<'_, F>)], + proof: &StarkProof, + mut challenger: SC::Challenger, +) -> Result, VerifierError> +where + F: TwoAdicField, + EF: ExtensionField, + SC: StarkConfig, + A: LiftedAir, +{ + let instance_shapes = &proof.instance_shapes; + let air_order = instance_shapes.air_order(); + + // Validate air_order and reorder caller instances to the proof's AIR ordering. + validate_air_order(air_order, instances.len())?; + let instances = instance_shapes.reorder(instances.to_vec())?; + + let log_blowup = config.pcs().log_blowup(); + + let log_max_trace_height = validate_inputs(&instances, instance_shapes, log_blowup)?; + let log_trace_heights = instance_shapes.log_trace_heights(); + + instance_shapes.observe_heights::(&mut challenger); + + let mut channel = VerifierTranscript::from_data(challenger, &proof.transcript); + + // Clear the challenger's absorb buffer after observing instance shapes by + // squeezing a throwaway extension element. Must mirror the prover exactly. + let _instance_challenge: EF = channel.sample_algebra_element::(); + + // Infer constraint degree from symbolic AIR analysis (max across all AIRs). + // NOTE: `log_quotient_degree()` runs symbolic eval and may panic if the AIR is + // invalid. Callers must ensure `validate_inputs` (above) passes first. + let log_constraint_degree = + instances.iter().map(|(air, _)| air.log_quotient_degree()).max().unwrap_or(1) as u8; + + if log_constraint_degree > log_blowup { + return Err(VerifierError::ConstraintDegreeTooHigh { + log_quotient_degree: log_constraint_degree, + log_blowup, + }); + } + + let constraint_degree = 1 << log_constraint_degree as usize; + + let max_trace_height = 1 << log_max_trace_height as usize; + let log_lde_height = log_max_trace_height + log_blowup; + + // Max LDE coset (for the largest trace, no lifting) + let max_lde_coset = LiftedCoset::unlifted(log_max_trace_height, log_blowup); + + // 1. Receive main trace commitment + let main_commit = channel.receive_commitment()?.clone(); + + // 2. Sample randomness for aux traces + let max_num_randomness = + instances.iter().map(|(air, _)| air.num_randomness()).max().unwrap_or(0); + + let randomness: Vec = (0..max_num_randomness) + .map(|_| channel.sample_algebra_element::()) + .collect(); + + // 3. Receive aux trace commitment + let aux_commit = channel.receive_commitment()?.clone(); + + // Receive aux values from the transcript (one EF element per aux value, per instance). + // When no AIR has aux columns, each entry is empty so nothing is received. + let all_aux_values: Vec> = instances + .iter() + .map(|(air, _)| { + let count = air.num_aux_values(); + (0..count) + .map(|_| channel.receive_algebra_element::()) + .collect::, _>>() + }) + .collect::, _>>()?; + + // 4. Sample constraint folding alpha and accumulation beta + let alpha: EF = channel.sample_algebra_element::(); + let beta: EF = channel.sample_algebra_element::(); + + // 5. Receive quotient commitment + let quotient_commit = channel.receive_commitment()?.clone(); + + // 6. Sample OOD point (outside max trace domain H and max LDE coset gK) + let z: EF = max_lde_coset.sample_ood_point(&mut channel); + let h = F::two_adic_generator(log_max_trace_height.into()); + let z_next = z * h; + + // 7. Widths per commitment group (unpadded data widths). + let main_widths: Vec = instances.iter().map(|(air, _)| air.width()).collect(); + let aux_widths: Vec = + instances.iter().map(|(air, _)| air.aux_width() * EF::DIMENSION).collect(); + let quotient_widths: Vec = vec![constraint_degree * EF::DIMENSION]; + + // Build commitments with original (unpadded) widths. + // The PCS aligned wrapper handles alignment and truncation internally. + let commitments = vec![ + (main_commit, main_widths), + (aux_commit, aux_widths), + (quotient_commit, quotient_widths), + ]; + + // 8. Verify PCS openings (returns per-matrix RowMajorMatrix, truncated to original widths) + let opened = verify_aligned::( + config.pcs(), + config.lmcs(), + &commitments, + log_lde_height, + [z, z_next], + &mut channel, + )?; + + // 9. Group indices for accessing opened matrices: [main, aux, quotient]. + let (main_g, aux_g, quot_g) = (0, 1, 2); + + // 10. Per-AIR constraint evaluation and beta accumulation. + // + // opened[g] has one matrix per AIR (for main/aux) or one matrix total (quotient). + // Each matrix has N=2 rows: row 0 = local (z), row 1 = next (z·h). + // + // Instances are in the proof's AIR ordering (ascending height), so j + // indexes both AIR and trace position directly. + debug_assert_eq!(opened[main_g].len(), instances.len()); + debug_assert_eq!(opened[aux_g].len(), instances.len()); + let mut accumulated = EF::ZERO; + let mut reduced_aux = ReducedAuxValues::::identity(); + + for (j, (air, inst)) in instances.iter().enumerate() { + let coset_j = LiftedCoset::new(log_trace_heights[j], log_blowup, log_max_trace_height); + + // opened[main_g][j] is a 2-row RowMajorMatrix (local, next) already truncated. + let main_window = RowWindow::from_view(&opened[main_g][j].as_view()); + + // Extract aux trace opened values (reconstitute EF from base field components). + let aux_mat = &opened[aux_g][j]; + let aux_local = row_to_packed_ext::(&aux_mat.row_slice(0).expect("aux row 0"))?; + let aux_next = row_to_packed_ext::(&aux_mat.row_slice(1).expect("aux row 1"))?; + let aux_window = RowWindow::from_two_rows(&aux_local, &aux_next); + + // Selectors at the lifted OOD point yⱼ = z^{rⱼ} (encapsulated in LiftedCoset). + let selectors = coset_j.selectors_at::(z); + + // Periodic values: for a column with period p, eval_at computes z^{n/p}. + // Using (max_trace_height, z) gives z^{max_n / p}, which equals + // y_j^{n_j / p} = (z^{max_n/n_j})^{n_j/p} = z^{max_n/p}. This avoids + // computing y_j = z^{r_j} explicitly. + let periodic_polys = PeriodicPolys::new(&air.periodic_columns()); + let periodic_values = periodic_polys.eval_at::(max_trace_height, z); + + let aux_values_j = &all_aux_values[j]; + let num_rand = air.num_randomness(); + let mut folder = ConstraintFolder { + main: main_window, + aux: aux_window, + randomness: &randomness[..num_rand], + public_values: inst.public_values, + periodic_values: &periodic_values, + permutation_values: aux_values_j, + selectors, + alpha, + accumulator: EF::ZERO, + _phantom: PhantomData, + }; + + air.is_valid_builder(&folder).map_err(InstanceValidationError::from)?; + air.eval(&mut folder); + + // Accumulate: acc = acc * beta + folded_j + accumulated = accumulated * beta + folder.accumulator; + + // Compute reduced aux contribution and accumulate. + let contribution = air + .reduced_aux_values( + aux_values_j, + &randomness[..num_rand], + inst.public_values, + inst.var_len_public_inputs, + ) + .map_err(VerifierError::Reduction)?; + reduced_aux.combine_in_place(&contribution); + } + + // 11. Reconstruct Q(z) and check quotient identity Q(z) * Z_{H_max}(z) + // Quotient group has a single matrix; row 0 is the evaluation at z. + let quot_row = opened[quot_g][0].row_slice(0).expect("quotient row 0"); + let quotient_chunks = row_to_packed_ext::("_row)?; + let quotient_z = reconstruct_quotient::(z, &max_lde_coset, "ient_chunks); + + let vanishing = max_lde_coset.vanishing_at::(z); + if accumulated != quotient_z * vanishing { + return Err(VerifierError::ConstraintMismatch); + } + + // 12. Check global reduced aux identity (all bus contributions combine to identity) + if !reduced_aux.is_identity() { + return Err(VerifierError::InvalidReducedAux); + } + + // 13. Finalize transcript: check emptiness and return digest + Ok(channel.finalize()?) +} diff --git a/stark/miden-lifted-stark/src/verifier/periodic.rs b/stark/miden-lifted-stark/src/verifier/periodic.rs new file mode 100644 index 0000000000..d5b773b33f --- /dev/null +++ b/stark/miden-lifted-stark/src/verifier/periodic.rs @@ -0,0 +1,92 @@ +//! Verifier-side periodic column handling. +//! +//! Periodic columns are stored as polynomial coefficients for efficient evaluation +//! at the OOD point using Horner's method. + +extern crate alloc; + +use alloc::vec::Vec; + +use p3_dft::{NaiveDft, TwoAdicSubgroupDft}; +use p3_field::{ExtensionField, TwoAdicField}; + +/// Verifier-side periodic polynomials for OOD evaluation. +/// +/// Stores polynomial coefficients computed from the AIR's periodic columns. +/// Used to evaluate periodic values at the OOD point during verification. +#[derive(Clone, Debug)] +pub struct PeriodicPolys { + /// Polynomial coefficients for each column. + polys: Vec>, +} + +impl PeriodicPolys { + /// Construct from subgroup evaluations (canonical order). + /// + /// Converts subgroup evaluations to polynomial coefficients via inverse DFT. + /// + /// # Panics + /// Panics if any column length is zero or not a power of two. + /// This is a trusted path — the AIR should pass + /// [`LiftedAir::validate`](miden_lifted_air::LiftedAir::validate). + pub fn new(column_evals: &[Vec]) -> Self { + let dft = NaiveDft; + let mut polys = Vec::with_capacity(column_evals.len()); + + for (i, column) in column_evals.iter().enumerate() { + let p = column.len(); + assert!( + p > 0 && p.is_power_of_two(), + "periodic column {i}: length must be positive power of two, got {p}" + ); + let coeffs = dft.idft(column.clone()); + polys.push(coeffs); + } + + Self { polys } + } + + /// Evaluate all periodic polynomials at the OOD point. + /// + /// For a column with period `p`, evaluates at `z^(trace_height / p)`. + /// Uses Horner's method for efficient polynomial evaluation. + /// + /// # Arguments + /// - `trace_height`: Height of the trace + /// - `z`: The OOD evaluation point + /// + /// The evaluation point is `z^{n/p}` rather than `z` directly. A periodic + /// column with period p is a polynomial P(X) of degree < p defined on the subgroup of + /// order p. At trace row i, the value is P(ωₙⁱ) = P(ωₚ^{i mod p}). + /// Since ωₙ^{n/p} = ωₚ, the map X → X^{n/p} collapses the trace domain H + /// (order n) onto the periodic subgroup (order p). So evaluating P at z^{n/p} gives + /// the same result as evaluating the periodic extension of P at z on the full trace + /// domain. + pub fn eval_at(&self, trace_height: usize, z: EF) -> Vec + where + EF: ExtensionField, + { + let mut result = Vec::with_capacity(self.polys.len()); + + for coeffs in &self.polys { + let period = coeffs.len(); + let y = z.exp_u64((trace_height / period) as u64); + result.push(horner_eval(coeffs, y)); + } + + result + } +} + +/// Evaluate a polynomial at a point using Horner's method. +fn horner_eval(coeffs: &[F], x: EF) -> EF +where + F: TwoAdicField, + EF: ExtensionField, +{ + let mut acc = EF::ZERO; + for coeff in coeffs.iter().rev() { + acc = acc * x + *coeff; + } + acc +} diff --git a/stark/miden-stark-transcript/Cargo.toml b/stark/miden-stark-transcript/Cargo.toml new file mode 100644 index 0000000000..26a3ae2d7a --- /dev/null +++ b/stark/miden-stark-transcript/Cargo.toml @@ -0,0 +1,23 @@ +[package] +description = "Transcript channels for Fiat-Shamir protocols with raw field/commitment storage" +edition = "2024" +homepage = "https://github.com/0xMiden/crypto/tree/main/crates" +license = "MIT OR Apache-2.0" +name = "miden-stark-transcript" +readme = "../README.md" +repository = "https://github.com/0xMiden/crypto" +rust-version.workspace = true +version.workspace = true + +[lib] +doctest = false +test = false + +[dependencies] +p3-challenger.workspace = true +p3-field.workspace = true +serde.workspace = true +thiserror.workspace = true + +[lints] +workspace = true diff --git a/stark/miden-stark-transcript/src/channel.rs b/stark/miden-stark-transcript/src/channel.rs new file mode 100644 index 0000000000..cf6146e366 --- /dev/null +++ b/stark/miden-stark-transcript/src/channel.rs @@ -0,0 +1,54 @@ +//! Shared `Channel` trait and `TranscriptChallenger` supertrait. + +use p3_challenger::{CanFinalizeDigest, CanObserve, CanSample, CanSampleBits, GrindingChallenger}; +use p3_field::{BasedVectorSpace, Field}; + +/// Bundle of challenger bounds required by transcript channels. +/// +/// Any challenger that satisfies `CanObserve`, `CanObserve`, `CanSample`, +/// `CanSampleBits`, `GrindingChallenger`, and +/// `CanFinalizeDigest` automatically implements this trait via a blanket impl. +pub trait TranscriptChallenger: + Clone + + CanObserve + + CanObserve + + CanSample + + CanSampleBits + + GrindingChallenger + + CanFinalizeDigest +{ +} + +impl TranscriptChallenger for Ch +where + F: Field, + Ch: Clone + + CanObserve + + CanObserve + + CanSample + + CanSampleBits + + GrindingChallenger + + CanFinalizeDigest, +{ +} + +/// Shared base trait for [`ProverChannel`](crate::ProverChannel) and +/// [`VerifierChannel`](crate::VerifierChannel). +/// +/// Provides sampling methods common to both sides of the transcript. +pub trait Channel { + type F: Field; + type Commitment: Clone; + type Challenger: TranscriptChallenger; + + /// Sample a random field element from the challenger. + fn sample(&mut self) -> Self::F; + + /// Sample a random `bits`-bit integer from the challenger. + fn sample_bits(&mut self, bits: usize) -> usize; + + /// Sample a random algebra element (e.g. extension field) from the challenger. + fn sample_algebra_element>(&mut self) -> A { + A::from_basis_coefficients_fn(|_| self.sample()) + } +} diff --git a/stark/miden-stark-transcript/src/data.rs b/stark/miden-stark-transcript/src/data.rs new file mode 100644 index 0000000000..56d93f21d0 --- /dev/null +++ b/stark/miden-stark-transcript/src/data.rs @@ -0,0 +1,46 @@ +//! Transcript data container for external transport. + +use alloc::vec::Vec; + +use serde::{Deserialize, Serialize}; + +/// Raw transcript data captured by a prover and replayed by a verifier. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(bound(serialize = "F: Serialize, C: Serialize"))] +#[serde(bound(deserialize = "F: Deserialize<'de>, C: Deserialize<'de>"))] +pub struct TranscriptData { + fields: Vec, + commitments: Vec, +} + +impl TranscriptData { + /// Create transcript data from field and commitment streams. + pub fn new(fields: Vec, commitments: Vec) -> Self { + Self { fields, commitments } + } + + /// Returns the recorded field elements. + pub fn fields(&self) -> &[F] { + &self.fields + } + + /// Returns the recorded commitments. + pub fn commitments(&self) -> &[C] { + &self.commitments + } + + /// Returns field and commitment slices for verifier construction. + pub fn as_slices(&self) -> (&[F], &[C]) { + (&self.fields, &self.commitments) + } + + /// Consume and return the underlying vectors. + pub fn into_parts(self) -> (Vec, Vec) { + (self.fields, self.commitments) + } + + /// Returns the total byte size of the recorded transcript data. + pub fn size_in_bytes(&self) -> usize { + size_of_val(self.fields.as_slice()) + size_of_val(self.commitments.as_slice()) + } +} diff --git a/stark/miden-stark-transcript/src/lib.rs b/stark/miden-stark-transcript/src/lib.rs new file mode 100644 index 0000000000..8e4518a3b4 --- /dev/null +++ b/stark/miden-stark-transcript/src/lib.rs @@ -0,0 +1,20 @@ +//! Transcript channels for Fiat-Shamir protocols with raw field/commitment storage. +//! +//! This crate provides: +//! - [`ProverTranscript`] and [`ProverChannel`] for prover-side recording. +//! - [`VerifierTranscript`] and [`VerifierChannel`] for verifier-side reading. + +#![no_std] + +extern crate alloc; + +mod channel; +mod data; +mod prover; +mod verifier; + +// Public API re-exports. +pub use channel::{Channel, TranscriptChallenger}; +pub use data::TranscriptData; +pub use prover::{ProverChannel, ProverTranscript}; +pub use verifier::{TranscriptError, VerifierChannel, VerifierTranscript}; diff --git a/stark/miden-stark-transcript/src/prover.rs b/stark/miden-stark-transcript/src/prover.rs new file mode 100644 index 0000000000..a9143469f9 --- /dev/null +++ b/stark/miden-stark-transcript/src/prover.rs @@ -0,0 +1,178 @@ +//! Prover-side transcript channel. + +use alloc::vec::Vec; + +use p3_challenger::{CanSample, CanSampleBits, CanSampleUniformBits}; +use p3_field::{BasedVectorSpace, Field}; + +use crate::{ + TranscriptData, + channel::{Channel, TranscriptChallenger}, +}; + +/// Prover channel that records transcript data and observes into the challenger. +#[derive(Clone, Debug)] +pub struct ProverTranscript { + challenger: Ch, + fields: Vec, + commitments: Vec, +} + +impl ProverTranscript { + /// Creates a new prover transcript backed by the provided challenger. + pub fn new(challenger: Ch) -> Self { + Self { + challenger, + fields: Vec::new(), + commitments: Vec::new(), + } + } + + /// Finalize the transcript, producing a binding digest and returning the proof data. + /// + /// Delegates to [`CanFinalizeDigest::finalize`](p3_challenger::CanFinalizeDigest::finalize) on + /// the inner challenger, which unconditionally applies a final state transition before + /// extracting the digest. The digest commits to the entire transcript interaction. + pub fn finalize(self) -> (Ch::Digest, TranscriptData) + where + F: Field, + C: Clone, + Ch: TranscriptChallenger, + { + let digest = self.challenger.finalize(); + (digest, TranscriptData::new(self.fields, self.commitments)) + } + + /// Returns the total byte size of the recorded transcript data. + pub fn size_in_bytes(&self) -> usize { + size_of_val(self.fields.as_slice()) + size_of_val(self.commitments.as_slice()) + } +} + +/// Prover-side channel interface for transcript operations. +pub trait ProverChannel: Channel { + fn send_field_slice(&mut self, values: &[Self::F]); + + fn send_commitment_slice(&mut self, values: &[Self::Commitment]); + + fn send_field_element(&mut self, value: Self::F) { + self.send_field_slice(core::slice::from_ref(&value)); + } + + fn send_algebra_element(&mut self, value: A) + where + A: BasedVectorSpace, + { + self.send_field_slice(value.as_basis_coefficients_slice()); + } + + fn send_algebra_slice(&mut self, values: &[A]) + where + A: BasedVectorSpace, + { + for value in values { + self.send_field_slice(value.as_basis_coefficients_slice()); + } + } + + fn send_commitment(&mut self, value: Self::Commitment) { + self.send_commitment_slice(core::slice::from_ref(&value)); + } + + fn hint_field_slice(&mut self, values: &[Self::F]); + + fn hint_commitment_slice(&mut self, values: &[Self::Commitment]); + + fn hint_field_element(&mut self, value: Self::F) { + self.hint_field_slice(core::slice::from_ref(&value)); + } + + fn hint_commitment(&mut self, value: Self::Commitment) { + self.hint_commitment_slice(core::slice::from_ref(&value)); + } + + fn grind(&mut self, bits: usize) -> Self::F; +} + +impl Channel for ProverTranscript +where + F: Field, + C: Clone, + Ch: TranscriptChallenger, +{ + type F = F; + type Commitment = C; + type Challenger = Ch; + + fn sample(&mut self) -> F { + self.challenger.sample() + } + + fn sample_bits(&mut self, bits: usize) -> usize { + self.challenger.sample_bits(bits) + } +} + +impl ProverChannel for ProverTranscript +where + F: Field, + C: Clone, + Ch: TranscriptChallenger, +{ + fn send_field_slice(&mut self, values: &[Self::F]) { + self.fields.extend_from_slice(values); + self.challenger.observe_slice(values); + } + + fn send_commitment_slice(&mut self, values: &[Self::Commitment]) { + self.commitments.extend_from_slice(values); + self.challenger.observe_slice(values); + } + + fn hint_field_slice(&mut self, values: &[Self::F]) { + self.fields.extend_from_slice(values); + } + + fn hint_commitment_slice(&mut self, values: &[Self::Commitment]) { + self.commitments.extend_from_slice(values); + } + + fn grind(&mut self, bits: usize) -> Self::F { + let witness = self.challenger.grind(bits); + self.fields.push(witness); + witness + } +} + +impl CanSample for ProverTranscript +where + Ch: CanSample, +{ + #[inline] + fn sample(&mut self) -> T { + self.challenger.sample() + } +} + +impl CanSampleBits for ProverTranscript +where + Ch: CanSampleBits, +{ + #[inline] + fn sample_bits(&mut self, bits: usize) -> usize { + self.challenger.sample_bits(bits) + } +} + +impl CanSampleUniformBits for ProverTranscript +where + Ch: CanSampleUniformBits, +{ + #[inline] + fn sample_uniform_bits( + &mut self, + bits: usize, + ) -> Result { + self.challenger.sample_uniform_bits::(bits) + } +} diff --git a/stark/miden-stark-transcript/src/verifier.rs b/stark/miden-stark-transcript/src/verifier.rs new file mode 100644 index 0000000000..349e4740ec --- /dev/null +++ b/stark/miden-stark-transcript/src/verifier.rs @@ -0,0 +1,248 @@ +//! Verifier-side transcript channel. + +use alloc::vec::Vec; + +use p3_challenger::{CanSample, CanSampleBits, CanSampleUniformBits}; +use p3_field::{BasedVectorSpace, Field}; +use thiserror::Error; + +use crate::{ + TranscriptData, + channel::{Channel, TranscriptChallenger}, +}; + +/// Verifier channel that reads transcript data and observes into the challenger. +#[derive(Clone, Debug)] +pub struct VerifierTranscript<'a, F, C, Ch> { + challenger: Ch, + fields: &'a [F], + commitments: &'a [C], +} + +impl<'a, F, C, Ch> VerifierTranscript<'a, F, C, Ch> { + /// Creates a new verifier transcript backed by the provided challenger. + pub fn new(challenger: Ch, fields: &'a [F], commitments: &'a [C]) -> Self { + Self { challenger, fields, commitments } + } + + /// Creates a verifier transcript backed by a `TranscriptData` container. + pub fn from_data(challenger: Ch, data: &'a TranscriptData) -> Self { + let (fields, commitments) = data.as_slices(); + Self::new(challenger, fields, commitments) + } + + /// Finalize the transcript, checking emptiness and producing a binding digest. + /// + /// Returns [`TranscriptError::TrailingData`] if any unread fields or commitments + /// remain. On success, delegates to + /// [`CanFinalizeDigest::finalize`](p3_challenger::CanFinalizeDigest::finalize) on the inner + /// challenger — the digest must match the prover's digest for the proof to be valid. + pub fn finalize(self) -> Result + where + F: Field, + C: Clone, + Ch: TranscriptChallenger, + { + if !self.fields.is_empty() || !self.commitments.is_empty() { + return Err(TranscriptError::TrailingData); + } + Ok(self.challenger.finalize()) + } + + /// Returns the total byte size of the remaining unconsumed transcript data. + pub fn size_in_bytes(&self) -> usize { + size_of_val(self.fields) + size_of_val(self.commitments) + } +} + +/// Verifier-side channel interface for transcript operations. +pub trait VerifierChannel: Channel { + fn receive_field_slice(&mut self, count: usize) -> Result<&[Self::F], TranscriptError>; + + fn receive_commitment_slice( + &mut self, + count: usize, + ) -> Result<&[Self::Commitment], TranscriptError>; + + fn receive_field(&mut self) -> Result<&Self::F, TranscriptError> { + self.receive_field_slice(1).map(|values| values.first().unwrap()) + } + + fn receive_algebra_element(&mut self) -> Result + where + Self::F: Field, + A: BasedVectorSpace, + { + let coeffs = self.receive_field_slice(A::DIMENSION)?; + Ok(A::from_basis_coefficients_slice(coeffs).unwrap()) + } + + fn receive_algebra_slice(&mut self, count: usize) -> Result, TranscriptError> + where + Self::F: Field, + A: BasedVectorSpace, + { + let mut values = Vec::with_capacity(count); + for _ in 0..count { + let coeffs = self.receive_field_slice(A::DIMENSION)?; + values.push(A::from_basis_coefficients_slice(coeffs).unwrap()); + } + Ok(values) + } + + fn receive_commitment(&mut self) -> Result<&Self::Commitment, TranscriptError> { + self.receive_commitment_slice(1).map(|values| values.first().unwrap()) + } + + fn receive_hint_field_slice(&mut self, count: usize) -> Result<&[Self::F], TranscriptError>; + + fn receive_hint_commitment_slice( + &mut self, + count: usize, + ) -> Result<&[Self::Commitment], TranscriptError>; + + fn receive_hint_field(&mut self) -> Result<&Self::F, TranscriptError> { + self.receive_hint_field_slice(1).map(|values| values.first().unwrap()) + } + + /// Read exactly `N` hint field elements as a fixed-size array. + fn receive_hint_field_array(&mut self) -> Result<[Self::F; N], TranscriptError> + where + Self::F: Copy, + { + self.receive_hint_field_slice(N)? + .try_into() + .map_err(|_| TranscriptError::NoMoreFields) + } + + fn receive_hint_commitment(&mut self) -> Result<&Self::Commitment, TranscriptError> { + self.receive_hint_commitment_slice(1).map(|values| values.first().unwrap()) + } + + fn grind(&mut self, bits: usize) -> Result; + + fn is_empty(&self) -> bool; +} + +impl<'a, F, C, Ch> Channel for VerifierTranscript<'a, F, C, Ch> +where + F: Field, + C: Clone, + Ch: TranscriptChallenger, +{ + type F = F; + type Commitment = C; + type Challenger = Ch; + + fn sample(&mut self) -> F { + CanSample::::sample(&mut self.challenger) + } + + fn sample_bits(&mut self, bits: usize) -> usize { + self.challenger.sample_bits(bits) + } +} + +impl<'a, F, C, Ch> VerifierChannel for VerifierTranscript<'a, F, C, Ch> +where + F: Field, + C: Clone, + Ch: TranscriptChallenger, +{ + // === Observed data === + fn receive_field_slice(&mut self, count: usize) -> Result<&'a [Self::F], TranscriptError> { + let values = pop_slice(&mut self.fields, count).ok_or(TranscriptError::NoMoreFields)?; + self.challenger.observe_slice(values); + Ok(values) + } + + fn receive_commitment_slice( + &mut self, + count: usize, + ) -> Result<&'a [Self::Commitment], TranscriptError> { + let values = + pop_slice(&mut self.commitments, count).ok_or(TranscriptError::NoMoreCommitments)?; + self.challenger.observe_slice(values); + Ok(values) + } + + fn receive_hint_field_slice(&mut self, count: usize) -> Result<&'a [Self::F], TranscriptError> { + pop_slice(&mut self.fields, count).ok_or(TranscriptError::NoMoreFields) + } + + fn receive_hint_commitment_slice( + &mut self, + count: usize, + ) -> Result<&'a [Self::Commitment], TranscriptError> { + pop_slice(&mut self.commitments, count).ok_or(TranscriptError::NoMoreCommitments) + } + + fn grind(&mut self, bits: usize) -> Result { + let (witness, rest) = self.fields.split_first().ok_or(TranscriptError::NoMoreFields)?; + self.fields = rest; + if self.challenger.check_witness(bits, *witness) { + Ok(*witness) + } else { + Err(TranscriptError::InvalidGrinding) + } + } + + fn is_empty(&self) -> bool { + self.fields.is_empty() && self.commitments.is_empty() + } +} + +impl<'a, F, C, Ch, T> CanSample for VerifierTranscript<'a, F, C, Ch> +where + Ch: CanSample, +{ + #[inline] + fn sample(&mut self) -> T { + self.challenger.sample() + } +} + +impl<'a, F, C, Ch> CanSampleBits for VerifierTranscript<'a, F, C, Ch> +where + Ch: CanSampleBits, +{ + #[inline] + fn sample_bits(&mut self, bits: usize) -> usize { + self.challenger.sample_bits(bits) + } +} + +impl<'a, F, C, Ch> CanSampleUniformBits for VerifierTranscript<'a, F, C, Ch> +where + Ch: CanSampleUniformBits, +{ + #[inline] + fn sample_uniform_bits( + &mut self, + bits: usize, + ) -> Result { + self.challenger.sample_uniform_bits::(bits) + } +} + +fn pop_slice<'a, T>(values: &mut &'a [T], count: usize) -> Option<&'a [T]> { + if values.len() < count { + return None; + } + let (slice, rest) = values.split_at(count); + *values = rest; + Some(slice) +} + +/// Errors that can occur during transcript consumption. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] +pub enum TranscriptError { + #[error("no more field elements in transcript")] + NoMoreFields, + #[error("no more commitments in transcript")] + NoMoreCommitments, + #[error("invalid grinding witness")] + InvalidGrinding, + #[error("trailing data in transcript")] + TrailingData, +} diff --git a/stark/miden-stateful-hasher/Cargo.toml b/stark/miden-stateful-hasher/Cargo.toml new file mode 100644 index 0000000000..c71dc401c6 --- /dev/null +++ b/stark/miden-stateful-hasher/Cargo.toml @@ -0,0 +1,25 @@ +[package] +description = "Stateful sponge-like hashers for cryptographic hashing" +edition = "2024" +homepage = "https://github.com/0xMiden/crypto/tree/main/crates" +license = "MIT OR Apache-2.0" +name = "miden-stateful-hasher" +readme = "../README.md" +repository = "https://github.com/0xMiden/crypto" +rust-version.workspace = true +version.workspace = true + +[lib] +doctest = false + +[dependencies] +p3-field.workspace = true +p3-symmetric.workspace = true + +[dev-dependencies] +p3-bn254.workspace = true +p3-goldilocks.workspace = true +p3-mersenne-31.workspace = true + +[lints] +workspace = true diff --git a/stark/miden-stateful-hasher/src/chaining.rs b/stark/miden-stateful-hasher/src/chaining.rs new file mode 100644 index 0000000000..0889f6a0d5 --- /dev/null +++ b/stark/miden-stateful-hasher/src/chaining.rs @@ -0,0 +1,248 @@ +//! Chaining-mode stateful hasher. +//! +//! This module provides [`ChainingHasher`] which wraps a regular hasher and +//! implements stateful hashing via chaining: `H(state || input)`. + +use p3_field::Field; +use p3_symmetric::CryptographicHasher; + +use crate::{Alignable, StatefulHasher}; + +/// An adapter that chains state with new input, hashing `state || encode(input)`. +/// +/// This mirrors `SerializingHasher`'s conversions from fields to bytes/u32/u64 streams, +/// but implements the `StatefulHasher` interface where the state is the digest itself. +/// +/// Unlike `SerializingStatefulSponge` (which preserves proper sponge semantics), +/// this adapter uses chaining mode where each absorption computes `H(state || input)`. +#[derive(Copy, Clone, Debug)] +pub struct ChainingHasher { + inner: Inner, +} + +impl ChainingHasher { + pub const fn new(inner: Inner) -> Self { + Self { inner } + } +} + +// ----------------------------------------------------------------------------- +// Scalar implementations: F -> [T; N] +// For ChainingHasher, State = Digest since the state IS the digest. +// ----------------------------------------------------------------------------- + +// Scalar field -> byte digest +impl StatefulHasher for ChainingHasher +where + F: Field, + Inner: CryptographicHasher, + [u8; N]: Default, +{ + type State = [u8; N]; + + fn absorb_into(&self, state: &mut Self::State, input: impl IntoIterator) { + let prev = *state; + *state = self.inner.hash_iter(prev.into_iter().chain(F::into_byte_stream(input))); + } + + fn squeeze(&self, state: &Self::State) -> [u8; N] { + *state + } +} + +// Scalar field -> u32 digest +impl StatefulHasher for ChainingHasher +where + F: Field, + Inner: CryptographicHasher, + [u32; N]: Default, +{ + type State = [u32; N]; + + fn absorb_into(&self, state: &mut Self::State, input: impl IntoIterator) { + let prev = *state; + *state = self.inner.hash_iter(prev.into_iter().chain(F::into_u32_stream(input))); + } + + fn squeeze(&self, state: &Self::State) -> [u32; N] { + *state + } +} + +// Scalar field -> u64 digest +impl StatefulHasher for ChainingHasher +where + F: Field, + Inner: CryptographicHasher, + [u64; N]: Default, +{ + type State = [u64; N]; + + fn absorb_into(&self, state: &mut Self::State, input: impl IntoIterator) { + let prev = *state; + *state = self.inner.hash_iter(prev.into_iter().chain(F::into_u64_stream(input))); + } + + fn squeeze(&self, state: &Self::State) -> [u64; N] { + *state + } +} + +// ----------------------------------------------------------------------------- +// Parallel implementations: [F; M] -> [[T; M]; OUT] +// For ChainingHasher, State = Digest since the state IS the digest. +// ----------------------------------------------------------------------------- + +// Parallel lanes (array-based) implemented via per-lane scalar hashing. +impl StatefulHasher<[F; M], [[u8; M]; OUT]> + for ChainingHasher +where + F: Field, + Inner: CryptographicHasher<[u8; M], [[u8; M]; OUT]>, + [[u8; M]; OUT]: Default, +{ + type State = [[u8; M]; OUT]; + + fn absorb_into(&self, state: &mut Self::State, input: impl IntoIterator) { + let prev = *state; + *state = self + .inner + .hash_iter(prev.into_iter().chain(F::into_parallel_byte_streams(input))); + } + + fn squeeze(&self, state: &Self::State) -> [[u8; M]; OUT] { + *state + } +} + +impl StatefulHasher<[F; M], [[u32; M]; OUT]> + for ChainingHasher +where + F: Field, + Inner: CryptographicHasher<[u32; M], [[u32; M]; OUT]>, + [[u32; M]; OUT]: Default, +{ + type State = [[u32; M]; OUT]; + + fn absorb_into(&self, state: &mut Self::State, input: impl IntoIterator) { + let prev = *state; + *state = self + .inner + .hash_iter(prev.into_iter().chain(F::into_parallel_u32_streams(input))); + } + + fn squeeze(&self, state: &Self::State) -> [[u32; M]; OUT] { + *state + } +} + +impl StatefulHasher<[F; M], [[u64; M]; OUT]> + for ChainingHasher +where + F: Field, + Inner: CryptographicHasher<[u64; M], [[u64; M]; OUT]>, + [[u64; M]; OUT]: Default, +{ + type State = [[u64; M]; OUT]; + + fn absorb_into(&self, state: &mut Self::State, input: impl IntoIterator) { + let prev = *state; + *state = self + .inner + .hash_iter(prev.into_iter().chain(F::into_parallel_u64_streams(input))); + } + + fn squeeze(&self, state: &Self::State) -> [[u64; M]; OUT] { + *state + } +} + +// ----------------------------------------------------------------------------- +// Alignable implementations for ChainingHasher +// Chaining mode has no padding, so alignment is always 1. +// ----------------------------------------------------------------------------- + +// For any Input and Target, ChainingHasher has alignment 1 (no padding). +// We use a blanket impl with no bounds since alignment doesn't depend on types. +impl Alignable for ChainingHasher { + const ALIGNMENT: usize = 1; +} + +#[cfg(test)] +mod tests { + use core::array; + + use p3_bn254::Bn254; + use p3_field::{Field, RawDataSerializable}; + use p3_goldilocks::Goldilocks; + use p3_mersenne_31::Mersenne31; + + use super::*; + use crate::testing::MockBinaryHasher; + + /// Verifies ChainingHasher produces same result as manual H(state || input) chaining. + fn test_scalar_matches_manual() { + let hasher = ChainingHasher::new(MockBinaryHasher); + let inputs: [F; 17] = array::from_fn(|i| F::from_usize(i * 7 + 3)); + let segments: &[core::ops::Range] = &[0..3, 3..5, 5..9, 9..17]; + + // Test via adapter + let mut state_adapter = [0u64; 4]; + for seg in segments { + StatefulHasher::::absorb_into( + &hasher, + &mut state_adapter, + inputs[seg.clone()].iter().copied(), + ); + } + + // Test via manual chaining + let mut state_manual = [0u64; 4]; + for seg in segments { + let prefix = state_manual.into_iter(); + let words = F::into_u64_stream(inputs[seg.clone()].iter().copied()); + state_manual = MockBinaryHasher.hash_iter(prefix.chain(words)); + } + + assert_eq!(state_adapter, state_manual); + } + + #[test] + fn scalar_matches_manual() { + test_scalar_matches_manual::(); // 4 bytes + test_scalar_matches_manual::(); // 8 bytes + test_scalar_matches_manual::(); // 32 bytes + } + + /// Verifies parallel hashing matches per-lane scalar hashing. + fn test_parallel_matches_scalar() { + let hasher = ChainingHasher::new(MockBinaryHasher); + let input: [F; 64] = array::from_fn(|i| F::from_usize(i * 7 + 3)); + + let parallel_input: [[F; 4]; 16] = array::from_fn(|i| array::from_fn(|j| input[i * 4 + j])); + let unzipped_input: [[F; 16]; 4] = array::from_fn(|i| parallel_input.map(|x| x[i])); + + let mut state_parallel = [[0u64; 4]; 4]; + StatefulHasher::<[F; 4], [[u64; 4]; 4]>::absorb_into( + &hasher, + &mut state_parallel, + parallel_input, + ); + + let per_lane: [[u64; 4]; 4] = array::from_fn(|lane| { + let mut s = [0u64; 4]; + StatefulHasher::::absorb_into(&hasher, &mut s, unzipped_input[lane]); + s + }); + let per_lane_transposed: [[u64; 4]; 4] = array::from_fn(|i| per_lane.map(|x| x[i])); + + assert_eq!(state_parallel, per_lane_transposed); + } + + #[test] + fn parallel_matches_scalar() { + test_parallel_matches_scalar::(); // 4 bytes + test_parallel_matches_scalar::(); // 8 bytes + test_parallel_matches_scalar::(); // 32 bytes + } +} diff --git a/stark/miden-stateful-hasher/src/field_sponge.rs b/stark/miden-stateful-hasher/src/field_sponge.rs new file mode 100644 index 0000000000..3fb48e65ee --- /dev/null +++ b/stark/miden-stateful-hasher/src/field_sponge.rs @@ -0,0 +1,144 @@ +//! Field-based stateful sponge. +//! +//! This module provides [`StatefulSponge`] which wraps a cryptographic permutation +//! and implements proper sponge absorption semantics. + +use p3_symmetric::CryptographicPermutation; + +use crate::{Alignable, StatefulHasher}; + +/// A stateful sponge wrapper around a cryptographic permutation. +/// +/// This implements proper sponge absorption semantics where the state evolves +/// with each absorption. Unlike `PaddingFreeSponge` (which implements `CryptographicHasher`), +/// this struct only implements `StatefulHasher`. +/// +/// `WIDTH` is the sponge's rate plus capacity. +/// `RATE` is the number of elements absorbed per permutation. +/// `OUT` is the number of elements squeezed from the state. +#[derive(Copy, Clone, Debug)] +pub struct StatefulSponge { + permutation: P, +} + +impl + StatefulSponge +{ + pub const fn new(permutation: P) -> Self { + Self { permutation } + } +} + +impl StatefulHasher + for StatefulSponge +where + T: Default + Clone, + P: CryptographicPermutation<[T; WIDTH]>, + [T; WIDTH]: Default, +{ + type State = [T; WIDTH]; + + fn absorb_into(&self, state: &mut Self::State, input: impl IntoIterator) { + const { assert!(OUT < WIDTH) } + let mut input = input.into_iter(); + + 'outer: loop { + for i in 0..RATE { + if let Some(x) = input.next() { + state[i] = x; + } else { + if i != 0 { + state[i..RATE].fill(T::default()); + self.permutation.permute_mut(state); + } + break 'outer; + } + } + self.permutation.permute_mut(state); + } + } + + fn squeeze(&self, state: &Self::State) -> [T; OUT] { + const { assert!(OUT < WIDTH) } + core::array::from_fn(|i| state[i].clone()) + } +} + +impl Alignable + for StatefulSponge +{ + const ALIGNMENT: usize = RATE; +} + +#[cfg(test)] +mod tests { + use alloc::vec::Vec; + + use super::*; + use crate::testing::MockBinaryPermutation; + + #[test] + fn basic() { + const WIDTH: usize = 4; + const RATE: usize = 2; + const OUT: usize = 2; + + let sponge = StatefulSponge::<_, WIDTH, RATE, OUT>::new( + MockBinaryPermutation::::default(), + ); + + let input = [1u64, 2, 3, 4, 5]; + let mut state = [0u64; WIDTH]; + sponge.absorb_into(&mut state, input); + let output: [u64; OUT] = sponge.squeeze(&state); + + // StatefulSponge pads partial blocks to rate boundary (proper sponge semantics): + // MockPermutation computes: sum(state[i] * (i + 1)) + // + // Initial state: [0, 0, 0, 0] + // First input chunk [1, 2] overwrites positions 0,1: [1, 2, 0, 0] + // Permute: 1*1 + 2*2 + 0*3 + 0*4 = 5 -> [5, 5, 5, 5] + // Second input chunk [3, 4] overwrites positions 0,1: [3, 4, 5, 5] + // Permute: 3*1 + 4*2 + 5*3 + 5*4 = 46 -> [46, 46, 46, 46] + // Third input chunk [5] overwrites position 0: [5, 46, 46, 46] + // Pad position 1 with 0: [5, 0, 46, 46] + // Permute: 5*1 + 0*2 + 46*3 + 46*4 = 327 -> [327, 327, 327, 327] + assert_eq!(output, [327; OUT]); + } + + /// Verifies implicit zero-padding equals explicit zeros. + fn test_alignment_semantic() + where + [u64; WIDTH]: Default, + { + let sponge = StatefulSponge::<_, WIDTH, RATE, OUT>::new( + MockBinaryPermutation::::default(), + ); + + for input_len in 1..=(RATE * 3) { + let input: Vec = (1..=input_len as u64).collect(); + + let mut state_unpadded = [0u64; WIDTH]; + sponge.absorb_into(&mut state_unpadded, input.iter().copied()); + let output_unpadded: [u64; OUT] = sponge.squeeze(&state_unpadded); + + let remainder = input_len % RATE; + let zeros_needed = if remainder == 0 { 0 } else { RATE - remainder }; + let mut padded_input = input.clone(); + padded_input.extend(core::iter::repeat_n(0u64, zeros_needed)); + + let mut state_padded = [0u64; WIDTH]; + sponge.absorb_into(&mut state_padded, padded_input.iter().copied()); + let output_padded: [u64; OUT] = sponge.squeeze(&state_padded); + + assert_eq!(output_unpadded, output_padded); + } + } + + #[test] + fn alignment_semantic() { + test_alignment_semantic::<4, 2, 2>(); + test_alignment_semantic::<6, 3, 2>(); + test_alignment_semantic::<8, 4, 2>(); + } +} diff --git a/stark/miden-stateful-hasher/src/lib.rs b/stark/miden-stateful-hasher/src/lib.rs new file mode 100644 index 0000000000..52f4f51786 --- /dev/null +++ b/stark/miden-stateful-hasher/src/lib.rs @@ -0,0 +1,126 @@ +//! Stateful sponge-like hashers for cryptographic hashing. +//! +//! This crate provides the [`StatefulHasher`] trait and implementations that maintain +//! an evolving state during hashing. This interface is used by commitment schemes and +//! Merkle trees to incrementally absorb data and squeeze out digests. +//! +//! # Implementations +//! +//! - [`StatefulSponge`]: Wraps a permutation with proper sponge semantics +//! - [`SerializingStatefulSponge`]: Serializes field elements to binary before absorbing +//! - [`ChainingHasher`]: Uses chaining mode `H(state || input)` with a regular hasher +//! - [`TruncatingHasher`]: Wraps a hasher and returns a shorter fixed digest (prefix) + +#![no_std] + +#[cfg(test)] +extern crate alloc; + +mod chaining; +mod field_sponge; +mod serializing_sponge; +mod truncating; + +#[cfg(test)] +pub mod testing; + +pub use chaining::*; +pub use field_sponge::*; +pub use serializing_sponge::*; +pub use truncating::*; + +/// Trait for stateful sponge-like hashers. +/// +/// A stateful hasher maintains an external state value that evolves as input is +/// absorbed, and from which fixed-size outputs can be squeezed. This interface +/// is used pervasively by commitment schemes and Merkle trees to incrementally +/// absorb rows of matrices and later read out the final digest. +/// +/// # Alignment +/// +/// Alignment semantics are defined by the separate [`Alignable`] trait. +/// Types implementing `StatefulHasher` typically also implement `Alignable` +/// to expose their alignment characteristics. Callers needing alignment +/// information should require `H: StatefulHasher<...> + Alignable<...>`. +pub trait StatefulHasher: Clone { + /// The internal state type that evolves during absorption. + type State; + + /// Absorb elements into the state with overwrite-mode and zero-padding semantics if applicable. + fn absorb_into(&self, state: &mut Self::State, input: impl IntoIterator); + + /// Squeeze an output from the current state. + fn squeeze(&self, state: &Self::State) -> Out; + + /// One-shot hash of multiple row slices. + /// + /// Creates a fresh state, absorbs all rows, and squeezes the result. + fn hash_rows<'a>(&self, rows: impl IntoIterator) -> Out + where + Item: Copy + 'a, + Self::State: Default, + { + let mut state = Self::State::default(); + for row in rows { + self.absorb_into(&mut state, row.iter().copied()); + } + self.squeeze(&state) + } +} + +/// Defines alignment for stateful hashers. +/// +/// `ALIGNMENT` is the maximum number of "virtual zero input elements" that could +/// be added due to padding. Padding always uses `Default::default()` (zero). +/// +/// - `ALIGNMENT = 1` means no padding (always aligned) +/// - `ALIGNMENT = N` means up to `N-1` virtual zero elements could be added +/// +/// # Type Parameters +/// +/// - `Input`: The type being absorbed (e.g., field element `F`) +/// - `Target`: The type underlying the hasher's state. For a field-native sponge this equals +/// `Input` (e.g., `Alignable`); for a serializing sponge it is the binary word type of the +/// inner hasher (e.g., `u32`, `u64`) +/// +/// The two type parameters allow distinguishing between different serialization +/// targets. For example, `SerializingStatefulSponge` can implement both +/// `Alignable` and `Alignable` with different alignment values. +/// +/// # Examples +/// +/// - A field-native sponge with rate `R` implements `Alignable` with `ALIGNMENT = R` +/// - A chaining hasher implements `Alignable` with `ALIGNMENT = 1` (no padding) +/// - A serializing wrapper implements `Alignable` and `Alignable` with alignment +/// derived from field size and inner hasher's alignment +pub trait Alignable { + /// The alignment width in units of `Input`. + /// + /// This represents the maximum number of virtual zero input elements that + /// could be added due to padding when absorbing input. + const ALIGNMENT: usize; +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::testing::{MockBinaryHasher, MockBinaryPermutation}; + + /// Compile-time verification that all StatefulHasher implementations + /// satisfy their generic bounds with mock types. + #[test] + fn types_instantiate() { + // Native sponge: T -> [T; WIDTH] -> [T; OUT] + let _sponge = StatefulSponge::<_, 8, 4, 2>::new(MockBinaryPermutation::::default()); + let _: [u64; 8] = Default::default(); + + // Serializing sponge: F -> [binary; WIDTH] -> [binary; OUT] + let inner = StatefulSponge::<_, 8, 4, 2>::new(MockBinaryPermutation::::default()); + let _serializing: SerializingStatefulSponge<_> = SerializingStatefulSponge::new(inner); + let _: [u64; 8] = Default::default(); + + // Chaining hasher: F -> [binary; N] (digest = state) + let _chaining: ChainingHasher = ChainingHasher::new(MockBinaryHasher); + let _: [u64; 4] = Default::default(); + } +} diff --git a/stark/miden-stateful-hasher/src/serializing_sponge.rs b/stark/miden-stateful-hasher/src/serializing_sponge.rs new file mode 100644 index 0000000000..e31bb53a15 --- /dev/null +++ b/stark/miden-stateful-hasher/src/serializing_sponge.rs @@ -0,0 +1,312 @@ +//! Serializing stateful sponge. +//! +//! This module provides [`SerializingStatefulSponge`] which serializes field elements +//! to binary before absorption into an inner `StatefulHasher`. + +use core::mem::size_of; + +use p3_field::Field; + +use crate::{Alignable, StatefulHasher}; + +/// An adapter that serializes field elements to binary and delegates to an inner `StatefulHasher`. +/// +/// This mirrors `SerializingHasher`'s conversions from fields to bytes/u32/u64 streams, +/// but implements the `StatefulHasher` interface by delegating to an inner stateful hasher +/// that operates on binary data. +/// +/// Unlike `ChainingHasher` (which uses chaining mode `H(state || input)`), this adapter +/// preserves proper sponge absorption semantics by directly calling the inner hasher's +/// `absorb_into` method. +#[derive(Copy, Clone, Debug)] +pub struct SerializingStatefulSponge { + inner: Inner, +} + +impl SerializingStatefulSponge { + pub const fn new(inner: Inner) -> Self { + Self { inner } + } +} + +// ----------------------------------------------------------------------------- +// Scalar implementations: F -> [B; OUT] +// The digest type [B; OUT] distinguishes these from parallel implementations. +// ----------------------------------------------------------------------------- + +// Scalar field -> u8 based inner +impl StatefulHasher for SerializingStatefulSponge +where + F: Field, + Inner: StatefulHasher, +{ + type State = Inner::State; + + fn absorb_into(&self, state: &mut Self::State, input: impl IntoIterator) { + self.inner.absorb_into(state, F::into_byte_stream(input)); + } + + fn squeeze(&self, state: &Self::State) -> [u8; OUT] { + self.inner.squeeze(state) + } +} + +// Scalar field -> u32 based inner +impl StatefulHasher for SerializingStatefulSponge +where + F: Field, + Inner: StatefulHasher, +{ + type State = Inner::State; + + fn absorb_into(&self, state: &mut Self::State, input: impl IntoIterator) { + self.inner.absorb_into(state, F::into_u32_stream(input)); + } + + fn squeeze(&self, state: &Self::State) -> [u32; OUT] { + self.inner.squeeze(state) + } +} + +// Scalar field -> u64 based inner +impl StatefulHasher for SerializingStatefulSponge +where + F: Field, + Inner: StatefulHasher, +{ + type State = Inner::State; + + fn absorb_into(&self, state: &mut Self::State, input: impl IntoIterator) { + self.inner.absorb_into(state, F::into_u64_stream(input)); + } + + fn squeeze(&self, state: &Self::State) -> [u64; OUT] { + self.inner.squeeze(state) + } +} + +// ----------------------------------------------------------------------------- +// Parallel implementations: [F; M] -> [[B; M]; OUT] +// The digest type [[B; M]; OUT] is structurally different from [B; OUT], +// which prevents coherence conflicts with scalar implementations. +// ----------------------------------------------------------------------------- + +// Parallel [F; M] -> [u8; M] based inner +impl StatefulHasher<[F; M], [[u8; M]; OUT]> + for SerializingStatefulSponge +where + F: Field, + Inner: StatefulHasher<[u8; M], [[u8; M]; OUT]>, +{ + type State = Inner::State; + + fn absorb_into(&self, state: &mut Self::State, input: impl IntoIterator) { + self.inner.absorb_into(state, F::into_parallel_byte_streams(input)); + } + + fn squeeze(&self, state: &Self::State) -> [[u8; M]; OUT] { + self.inner.squeeze(state) + } +} + +// Parallel [F; M] -> [u32; M] based inner +impl StatefulHasher<[F; M], [[u32; M]; OUT]> + for SerializingStatefulSponge +where + F: Field, + Inner: StatefulHasher<[u32; M], [[u32; M]; OUT]>, +{ + type State = Inner::State; + + fn absorb_into(&self, state: &mut Self::State, input: impl IntoIterator) { + self.inner.absorb_into(state, F::into_parallel_u32_streams(input)); + } + + fn squeeze(&self, state: &Self::State) -> [[u32; M]; OUT] { + self.inner.squeeze(state) + } +} + +// Parallel [F; M] -> [u64; M] based inner +impl StatefulHasher<[F; M], [[u64; M]; OUT]> + for SerializingStatefulSponge +where + F: Field, + Inner: StatefulHasher<[u64; M], [[u64; M]; OUT]>, +{ + type State = Inner::State; + + fn absorb_into(&self, state: &mut Self::State, input: impl IntoIterator) { + self.inner.absorb_into(state, F::into_parallel_u64_streams(input)); + } + + fn squeeze(&self, state: &Self::State) -> [[u64; M]; OUT] { + self.inner.squeeze(state) + } +} + +// ----------------------------------------------------------------------------- +// Alignable implementations for SerializingStatefulSponge +// ----------------------------------------------------------------------------- + +/// Compute alignment for a serializing wrapper that converts field elements to binary items. +/// +/// Given: +/// - `field_bytes`: The field's byte size (`F::NUM_BYTES`) +/// - `item_bytes`: The inner item's byte size (1 for u8, 4 for u32, 8 for u64) +/// - `inner_alignment`: The inner hasher's alignment in items (e.g., sponge rate) +/// +/// Returns the alignment in field elements that corresponds to the inner alignment. +/// +/// The formula ensures that serializing `alignment` field elements produces exactly +/// `inner_alignment` inner items (or a multiple thereof). +const fn compute_field_alignment( + field_bytes: usize, + item_bytes: usize, + inner_alignment: usize, +) -> usize { + // We need the smallest number of field elements that, when serialized, + // produce a byte count divisible by the inner hasher's block size. + // This is lcm(field_bytes, inner_bytes) / field_bytes. + // + // Example: 4-byte field, inner rate = 3 u64s (24 bytes) + // lcm(4, 24) = 24, so alignment = 24/4 = 6 fields + // Verify: 6 fields × 4 bytes = 24 bytes = 3 u64s ✓ + // + // When field_bytes > inner_bytes, alignment is often 1: + // Example: 32-byte field, inner rate = 2 u64s (16 bytes) + // lcm(32, 16) = 32, so alignment = 32/32 = 1 + // Each field spans 2 complete blocks, so every field ends aligned. + let inner_bytes = inner_alignment * item_bytes; + + // gcd via Euclidean algorithm + let mut a = field_bytes; + let mut b = inner_bytes; + while b != 0 { + let t = b; + b = a % b; + a = t; + } + inner_bytes / a +} + +impl Alignable for SerializingStatefulSponge +where + F: Field, + Inner: Alignable, +{ + const ALIGNMENT: usize = + compute_field_alignment(F::NUM_BYTES, size_of::(), Inner::ALIGNMENT); +} + +#[cfg(test)] +mod tests { + use alloc::vec::Vec; + + use p3_bn254::Bn254; + use p3_field::Field; + use p3_goldilocks::Goldilocks; + use p3_mersenne_31::Mersenne31; + + use super::*; + use crate::{StatefulSponge, testing::MockBinaryPermutation}; + + /// Verifies implicit zero-padding equals explicit zeros for serialized fields. + fn test_alignment_semantic() + where + SerializingStatefulSponge< + StatefulSponge, WIDTH, RATE, OUT>, + >: Alignable, + [u64; WIDTH]: Default, + { + let inner = StatefulSponge::<_, WIDTH, RATE, OUT>::new( + MockBinaryPermutation::::default(), + ); + let hasher = SerializingStatefulSponge::new(inner); + + let alignment = , WIDTH, RATE, OUT>, + > as Alignable>::ALIGNMENT; + + for input_len in 1..=(alignment * 3) { + let input: Vec = (1..=input_len).map(|i| F::from_usize(i)).collect(); + + let mut state_unpadded = [0u64; WIDTH]; + StatefulHasher::::absorb_into( + &hasher, + &mut state_unpadded, + input.iter().copied(), + ); + let output_unpadded: [u64; OUT] = + StatefulHasher::::squeeze(&hasher, &state_unpadded); + + let remainder = input_len % alignment; + let zeros_needed = if remainder == 0 { 0 } else { alignment - remainder }; + let mut padded_input = input.clone(); + padded_input.extend(core::iter::repeat_n(F::ZERO, zeros_needed)); + + let mut state_padded = [0u64; WIDTH]; + StatefulHasher::::absorb_into( + &hasher, + &mut state_padded, + padded_input.iter().copied(), + ); + let output_padded: [u64; OUT] = + StatefulHasher::::squeeze(&hasher, &state_padded); + + assert_eq!(output_unpadded, output_padded); + } + } + + #[test] + fn alignment_semantic() { + // Different field sizes exercise different alignment calculations + test_alignment_semantic::(); // 4 bytes -> alignment 16 + test_alignment_semantic::(); // 8 bytes -> alignment 8 + test_alignment_semantic::(); // 32 bytes -> alignment 2 + } + + #[test] + fn test_compute_field_alignment() { + // 4-byte field (e.g., Mersenne31) to u32 (4 bytes), inner alignment 8 + // inner_bytes = 32, gcd(4, 32) = 4, alignment = 32/4 = 8 + assert_eq!(compute_field_alignment(4, 4, 8), 8); + + // 4-byte field to u64 (8 bytes), inner alignment 4 + // inner_bytes = 32, gcd(4, 32) = 4, alignment = 32/4 = 8 + assert_eq!(compute_field_alignment(4, 8, 4), 8); + + // 8-byte field (e.g., Goldilocks) to u32 (4 bytes), inner alignment 8 + // inner_bytes = 32, gcd(8, 32) = 8, alignment = 32/8 = 4 + assert_eq!(compute_field_alignment(8, 4, 8), 4); + + // 8-byte field to u64 (8 bytes), inner alignment 4 + // inner_bytes = 32, gcd(8, 32) = 8, alignment = 32/8 = 4 + assert_eq!(compute_field_alignment(8, 8, 4), 4); + + // 32-byte field (e.g., Bn254) to u64 (8 bytes), inner alignment 2 + // inner_bytes = 16, gcd(32, 16) = 16, alignment = 16/16 = 1 + assert_eq!(compute_field_alignment(32, 8, 2), 1); + } + + #[test] + fn test_compute_field_alignment_non_power_of_2_rate() { + // 4-byte field (e.g., Mersenne31) to u64 (8 bytes), rate 3 + // inner_bytes = 24, gcd(4, 24) = 4, alignment = 24/4 = 6 + // Verify: 6 fields * 4 bytes = 24 bytes = 3 u64s ✓ + assert_eq!(compute_field_alignment(4, 8, 3), 6); + + // 8-byte field (e.g., Goldilocks) to u32 (4 bytes), rate 3 + // inner_bytes = 12, gcd(8, 12) = 4, alignment = 12/4 = 3 + // Verify: 3 fields * 8 bytes = 24 bytes = 6 u32s = 2 * rate ✓ + assert_eq!(compute_field_alignment(8, 4, 3), 3); + + // 4-byte field to u32 (4 bytes), rate 7 + // inner_bytes = 28, gcd(4, 28) = 4, alignment = 28/4 = 7 + assert_eq!(compute_field_alignment(4, 4, 7), 7); + + // 8-byte field to u64 (8 bytes), rate 5 + // inner_bytes = 40, gcd(8, 40) = 8, alignment = 40/8 = 5 + assert_eq!(compute_field_alignment(8, 8, 5), 5); + } +} diff --git a/stark/miden-stateful-hasher/src/testing.rs b/stark/miden-stateful-hasher/src/testing.rs new file mode 100644 index 0000000000..058119c485 --- /dev/null +++ b/stark/miden-stateful-hasher/src/testing.rs @@ -0,0 +1,111 @@ +//! Mock implementations for testing stateful hashers. +//! +//! This module provides mock permutations and hashers for testing adapter behavior +//! without real cryptographic primitives. All mocks use position-weighted sums +//! (`sum(input[i] * (i + 1))`) to detect position-related bugs. + +use core::{array, marker::PhantomData}; + +use p3_symmetric::{CryptographicHasher, CryptographicPermutation, Permutation}; + +// ============================================================================= +// MockBinaryPermutation +// ============================================================================= + +/// A position-sensitive mock permutation for binary types. +/// +/// Computes `sum(state[i] * (i + 1))` using wrapping arithmetic, making the result +/// dependent on WHERE values appear, not just WHAT values appear. This catches bugs +/// where values are in wrong positions. +#[derive(Clone, Debug)] +pub struct MockBinaryPermutation(PhantomData<[T; N]>); + +impl Default for MockBinaryPermutation { + fn default() -> Self { + Self(PhantomData) + } +} + +impl MockBinaryPermutation { + pub fn new() -> Self { + Self::default() + } +} + +// Binary type implementations (using wrapping arithmetic) +macro_rules! impl_mock_binary_permutation { + ($($t:ty),*) => {$( + impl Permutation<[$t; N]> for MockBinaryPermutation<$t, N> { + fn permute_mut(&self, input: &mut [$t; N]) { + // Compute position-weighted sum: sum(input[i] * (i + 1)) + let weighted_sum: $t = input + .iter() + .enumerate() + .fold(0, |acc, (i, &val)| { + acc.wrapping_add(val.wrapping_mul((i as $t).wrapping_add(1))) + }); + *input = [weighted_sum; N]; + } + } + impl CryptographicPermutation<[$t; N]> for MockBinaryPermutation<$t, N> {} + )*}; +} + +impl_mock_binary_permutation!(u8, u16, u32, u64); + +// ============================================================================= +// MockBinaryHasher +// ============================================================================= + +/// A position-sensitive mock hasher for binary types. +/// +/// Computes `sum(input[i] * (i + 1))` using wrapping arithmetic, consistent with +/// [`MockBinaryPermutation`]. This catches bugs where values end up in wrong positions. +/// +/// Supports both scalar hashing (`T -> [T; N]`) and parallel hashing (`[T; M] -> [[T; M]; N]`). +#[derive(Clone, Debug, Default)] +pub struct MockBinaryHasher; + +// Scalar hashing: T -> [T; N] +macro_rules! impl_mock_binary_hasher_scalar { + ($($t:ty),*) => {$( + impl CryptographicHasher<$t, [$t; N]> for MockBinaryHasher { + fn hash_iter>(&self, iter: I) -> [$t; N] { + // Position-weighted sum: sum(input[i] * (i + 1)) + let weighted_sum: $t = iter + .into_iter() + .enumerate() + .fold(0, |acc, (i, val)| { + acc.wrapping_add(val.wrapping_mul((i as $t).wrapping_add(1))) + }); + [weighted_sum; N] + } + } + )*}; +} + +impl_mock_binary_hasher_scalar!(u8, u16, u32, u64); + +// Parallel hashing: [T; M] -> [[T; M]; N] +// Each lane is hashed independently with position-weighting. +macro_rules! impl_mock_binary_hasher_parallel { + ($($t:ty),*) => {$( + impl CryptographicHasher<[$t; M], [[$t; M]; N]> for MockBinaryHasher { + fn hash_iter>(&self, iter: I) -> [[$t; M]; N] { + // Position-weighted sum per lane: sum(input[i][lane] * (i + 1)) + let weighted_sum: [$t; M] = iter + .into_iter() + .enumerate() + .fold([0; M], |acc, (i, vals)| { + let multiplier = (i as $t).wrapping_add(1); + array::from_fn(|lane| { + acc[lane].wrapping_add(vals[lane].wrapping_mul(multiplier)) + }) + }); + [weighted_sum; N] + } + } + )*}; +} + +impl_mock_binary_hasher_parallel!(u8, u16, u32, u64); diff --git a/stark/miden-stateful-hasher/src/truncating.rs b/stark/miden-stateful-hasher/src/truncating.rs new file mode 100644 index 0000000000..a8694946d1 --- /dev/null +++ b/stark/miden-stateful-hasher/src/truncating.rs @@ -0,0 +1,96 @@ +//! Truncating wrapper for cryptographic hashers. +//! +//! This module provides [`TruncatingHasher`], the hasher analogue of `p3-symmetric`'s +//! [`TruncatedPermutation`](p3_symmetric::TruncatedPermutation). + +use p3_symmetric::CryptographicHasher; + +/// Hasher analogue of `p3-symmetric`'s `TruncatedPermutation`. +#[derive(Copy, Clone, Debug)] +pub struct TruncatingHasher { + inner: Inner, +} + +impl TruncatingHasher { + pub const fn new(inner: Inner) -> Self { + Self { inner } + } +} + +impl CryptographicHasher + for TruncatingHasher +where + T: Clone, + Inner: CryptographicHasher, +{ + #[inline] + fn hash_iter(&self, input: I) -> [T; OUT] + where + I: IntoIterator, + { + const { assert!(OUT <= IN) } + let full = self.inner.hash_iter(input); + core::array::from_fn(|i| full[i].clone()) + } + + #[inline] + fn hash_iter_slices<'a, I>(&self, input: I) -> [T; OUT] + where + I: IntoIterator, + T: 'a, + { + const { assert!(OUT <= IN) } + let full = self.inner.hash_iter_slices(input); + core::array::from_fn(|i| full[i].clone()) + } + + #[inline] + fn hash_slice(&self, input: &[T]) -> [T; OUT] { + const { assert!(OUT <= IN) } + let full = self.inner.hash_slice(input); + core::array::from_fn(|i| full[i].clone()) + } + + #[inline] + fn hash_item(&self, input: T) -> [T; OUT] { + const { assert!(OUT <= IN) } + let full = self.inner.hash_item(input); + core::array::from_fn(|i| full[i].clone()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::testing::MockBinaryHasher; + + #[test] + fn truncates_prefix_of_inner_digest() { + type H = TruncatingHasher; + let h = H::new(MockBinaryHasher); + let input: [u64; 11] = core::array::from_fn(|i| (i * 13 + 7) as u64); + let full: [u64; 4] = MockBinaryHasher.hash_iter(input); + let short = h.hash_iter(input); + assert_eq!(short, [full[0], full[1]]); + } + + #[test] + fn hash_iter_slices_delegates() { + type H = TruncatingHasher; + let h = H::new(MockBinaryHasher); + let a = [1u64, 2, 3]; + let b = [4u64, 5]; + let got_slices = h.hash_iter_slices([&a[..], &b[..]]); + let got_flat = h.hash_iter(a.iter().chain(b.iter()).copied()); + assert_eq!(got_slices, got_flat); + } + + #[test] + fn hash_slice_and_hash_item_match_hash_iter() { + type H = TruncatingHasher; + let h = H::new(MockBinaryHasher); + let xs = [9u64, 8, 7, 6, 5]; + assert_eq!(h.hash_slice(&xs), h.hash_iter(xs)); + assert_eq!(h.hash_item(42u64), h.hash_iter([42u64])); + } +}