diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000000..e724ea0856 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[env] +RUST_BACKTRACE = "1" diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..12f32366ab --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +# EditorConfig is awesome:http://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*.md] +indent_style = space diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..9d5816faad --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +zcash_client_backend/src/proto/compact_formats.rs linguist-generated=true +zcash_client_backend/src/proto/service.rs linguist-generated=true +zcash_client_backend/src/proto/proposal.rs linguist-generated=true diff --git a/.github/actions/prepare/action.yml b/.github/actions/prepare/action.yml new file mode 100644 index 0000000000..9ac597d495 --- /dev/null +++ b/.github/actions/prepare/action.yml @@ -0,0 +1,39 @@ +name: 'Prepare CI' +description: 'Prepares feature flags' +inputs: + extra-features: + description: 'Extra feature flags to enable' + required: false + default: '' + test-dependencies: + description: 'Include test dependencies' + required: false + default: true +outputs: + feature-flags: + description: 'Feature flags' + value: ${{ steps.prepare.outputs.flags }} +runs: + using: 'composite' + steps: + - id: test + shell: bash + run: echo "feature=test-dependencies" >> $GITHUB_OUTPUT + if: inputs.test-dependencies == 'true' + - name: Prepare feature flags + id: prepare + shell: bash + run: > + echo "flags=--features ' + bundled-prover + download-params + lightwalletd-tonic + sync + temporary-zcashd + transparent-inputs + unstable + unstable-serialization + unstable-spanning-tree + ${{ inputs.extra-features }} + ${{ steps.test.outputs.feature }} + '" >> $GITHUB_OUTPUT diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..5c4156c6ae --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +version: 2 +updates: +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: daily + timezone: Etc/UTC + open-pull-requests-limit: 10 + reviewers: + - str4d + assignees: + - str4d + labels: + - "A-CI" diff --git a/.github/workflows/aggregate-audits.yml b/.github/workflows/aggregate-audits.yml new file mode 100644 index 0000000000..546d37d3b6 --- /dev/null +++ b/.github/workflows/aggregate-audits.yml @@ -0,0 +1,24 @@ +name: Aggregate audits + +on: + push: + branches: main + paths: + - '.github/workflows/aggregate-audits.yml' + - 'supply-chain/audits.toml' + +permissions: + contents: read + +jobs: + trigger: + name: Trigger + runs-on: ubuntu-latest + steps: + - name: Trigger aggregation in zcash/rust-ecosystem + run: > + gh api repos/zcash/rust-ecosystem/dispatches + --field event_type="aggregate-audits" + --field client_payload[sha]="$GITHUB_SHA" + env: + GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/audits.yml b/.github/workflows/audits.yml new file mode 100644 index 0000000000..a3f8d42143 --- /dev/null +++ b/.github/workflows/audits.yml @@ -0,0 +1,42 @@ +name: Audits + +on: + pull_request: + push: + branches: main + +permissions: + contents: read + +jobs: + cargo-vet: + name: Vet Rust dependencies + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + id: toolchain + - run: rustup override set ${{steps.toolchain.outputs.name}} + - run: cargo install cargo-vet --version ~0.10 + - run: cargo vet --locked + + cargo-deny: + name: Check licenses + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: EmbarkStudios/cargo-deny-action@v2 + with: + command: check licenses + + required-audits: + name: Required audits have passed + needs: + - cargo-vet + - cargo-deny + if: ${{ always() }} + runs-on: ubuntu-latest + steps: + - name: Determine whether all required-pass steps succeeded + run: | + echo '${{ toJSON(needs) }}' | jq -e '[ .[] | .result == "success" ] | all' diff --git a/.github/workflows/book.yml b/.github/workflows/book.yml new file mode 100644 index 0000000000..c649b89417 --- /dev/null +++ b/.github/workflows/book.yml @@ -0,0 +1,37 @@ +name: librustzcash documentation + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - id: prepare + uses: ./.github/actions/prepare + - uses: dtolnay/rust-toolchain@nightly + id: toolchain + - run: rustup override set ${{steps.toolchain.outputs.name}} + + - name: Build latest rustdocs + run: > + cargo doc + --no-deps + --workspace + ${{ steps.prepare.outputs.feature-flags }} + env: + RUSTDOCFLAGS: -Z unstable-options --enable-index-page --cfg docsrs + + - name: Move latest rustdocs into book + run: | + mkdir -p ./book/book/rustdoc + mv ./target/doc ./book/book/rustdoc/latest + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./book/book diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ad90d53f3..4d2699f6c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,219 +1,512 @@ -name: CI checks +name: CI -on: [push, pull_request] +on: + pull_request: + push: + branches: main + merge_group: jobs: - test: - name: Test on ${{ matrix.os }} + required-test: + name: Test ${{ matrix.state }} on ${{ matrix.target }} runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, windows-latest, macOS-latest] + target: + - Linux + state: + - no-Orchard + - Orchard + - NU7 + + include: + - target: Linux + os: ubuntu-latest-8cores + + - state: Orchard + extra_flags: orchard + - state: NU7 + extra_flags: orchard + rustflags: '--cfg zcash_unstable="nu7"' + + env: + RUSTFLAGS: ${{ matrix.rustflags }} + RUSTDOCFLAGS: ${{ matrix.rustflags }} steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 + - uses: actions/checkout@v4 + - id: prepare + uses: ./.github/actions/prepare with: - toolchain: 1.51.0 - override: true - - - name: Fetch path to Zcash parameters - working-directory: ./zcash_proofs - shell: bash - run: echo "ZCASH_PARAMS=$(cargo run --release --example get-params-path --features directories)" >> $GITHUB_ENV - - name: Cache Zcash parameters - id: cache-params - uses: actions/cache@v2 + extra-features: ${{ matrix.extra_flags || '' }} + - uses: actions/cache@v4 with: - path: ${{ env.ZCASH_PARAMS }} - key: ${{ runner.os }}-params - - name: Fetch Zcash parameters - if: steps.cache-params.outputs.cache-hit != 'true' - working-directory: ./zcash_proofs - run: cargo run --release --example download-params --features download-params + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-msrv-${{ hashFiles('**/Cargo.lock') }} + - name: Run tests + run: > + cargo test + --workspace + ${{ steps.prepare.outputs.feature-flags }} + - name: Verify working directory is clean + run: git diff --exit-code + + test: + name: Test ${{ matrix.state }} on ${{ matrix.target }} + runs-on: ${{ matrix.os }} + continue-on-error: true + strategy: + matrix: + target: + - macOS + - Windows + state: + - no-Orchard + - Orchard + - NU7 + + include: + - target: macOS + os: macOS-latest + - target: Windows + os: windows-latest-8cores + + - state: Orchard + extra_flags: orchard + - state: NU7 + extra_flags: orchard + rustflags: '--cfg zcash_unstable="nu7"' + exclude: + - target: macOS + state: NU7 + + env: + RUSTFLAGS: ${{ matrix.rustflags }} + RUSTDOCFLAGS: ${{ matrix.rustflags }} + + steps: + - uses: actions/checkout@v4 + - id: prepare + uses: ./.github/actions/prepare + with: + extra-features: ${{ matrix.extra_flags || '' }} + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-msrv-${{ hashFiles('**/Cargo.lock') }} - name: Run tests - uses: actions-rs/cargo@v1 + run: > + cargo test + --workspace + ${{ steps.prepare.outputs.feature-flags }} + - name: Verify working directory is clean + run: git diff --exit-code + + test-slow: + name: Slow Test ${{ matrix.state }} on ${{ matrix.target }} + runs-on: ${{ matrix.os }} + continue-on-error: true + strategy: + matrix: + target: + - Linux + state: + - Orchard + - NU7 + + include: + - target: Linux + os: ubuntu-latest-8cores + + - state: Orchard + extra_flags: orchard + - state: NU7 + extra_flags: orchard + rustflags: '--cfg zcash_unstable="nu7"' + + env: + RUSTFLAGS: ${{ matrix.rustflags }} + RUSTDOCFLAGS: ${{ matrix.rustflags }} + + steps: + - uses: actions/checkout@v4 + - id: prepare + uses: ./.github/actions/prepare with: - command: test - args: --all-features --verbose --release --all + extra-features: ${{ matrix.extra_flags || '' }} + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-msrv-${{ hashFiles('**/Cargo.lock') }} - name: Run slow tests - uses: actions-rs/cargo@v1 + run: > + cargo test + --workspace + ${{ steps.prepare.outputs.feature-flags }} + --features expensive-tests + -- --ignored + - name: Verify working directory is clean + run: git diff --exit-code + + # States that we want to ensure can be built, but that we don't actively run tests for. + check-msrv: + name: Check ${{ matrix.state }} build on ${{ matrix.target }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + target: + - Linux + - macOS + - Windows + state: + - ZFuture + + include: + - target: Linux + os: ubuntu-latest + - target: macOS + os: macOS-latest + - target: Windows + os: windows-latest + + - state: ZFuture + rustflags: '--cfg zcash_unstable="zfuture"' + + env: + RUSTFLAGS: ${{ matrix.rustflags }} + RUSTDOCFLAGS: ${{ matrix.rustflags }} + + steps: + - uses: actions/checkout@v4 + - id: prepare + uses: ./.github/actions/prepare with: - command: test - args: --all-features --verbose --release --all -- --ignored + extra-features: ${{ matrix.extra_flags || '' }} + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-msrv-${{ hashFiles('**/Cargo.lock') }} + - name: Run check + run: > + cargo check + --release + --workspace + --tests + ${{ steps.prepare.outputs.feature-flags }} + - name: Verify working directory is clean + run: git diff --exit-code - build: + build-latest: + name: Latest build on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest] + steps: + - uses: actions/checkout@v4 + - id: prepare + uses: ./.github/actions/prepare + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-latest + - uses: dtolnay/rust-toolchain@stable + id: toolchain + - run: rustup override set ${{steps.toolchain.outputs.name}} + - name: Remove lockfile to build with latest dependencies + run: rm Cargo.lock + - name: Build crates + run: > + cargo build + --workspace + --all-targets + ${{ steps.prepare.outputs.feature-flags }} + --verbose + - name: Verify working directory is clean (excluding lockfile) + run: git diff --exit-code ':!Cargo.lock' + + build-nodefault: name: Build target ${{ matrix.target }} runs-on: ubuntu-latest strategy: matrix: target: - - wasm32-unknown-unknown - wasm32-wasi - steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 + - uses: actions/checkout@v4 with: - toolchain: 1.51.0 - override: true + path: crates + # We use a synthetic crate to ensure no dev-dependencies are enabled, which can + # be incompatible with some of these targets. + - name: Create synthetic crate for testing + run: cargo init --lib ci-build + - name: Copy Rust version into synthetic crate + run: cp crates/rust-toolchain.toml ci-build/ + - name: Copy patch directives into synthetic crate + run: | + echo "[patch.crates-io]" >> ./ci-build/Cargo.toml + cat ./crates/Cargo.toml | sed "0,/.\+\(patch.crates.\+\)/d" >> ./ci-build/Cargo.toml + - name: Add zcash_proofs as a dependency of the synthetic crate + working-directory: ./ci-build + run: cargo add --no-default-features --path ../crates/zcash_proofs + - name: Add zcash_client_backend as a dependency of the synthetic crate + working-directory: ./ci-build + run: cargo add --path ../crates/zcash_client_backend + - name: Copy pinned dependencies into synthetic crate + run: cp crates/Cargo.lock ci-build/ - name: Add target + working-directory: ./ci-build run: rustup target add ${{ matrix.target }} - - name: cargo fetch - uses: actions-rs/cargo@v1 + - name: Build for target + working-directory: ./ci-build + run: cargo build --verbose --target ${{ matrix.target }} + + build-nostd: + name: Build target ${{ matrix.target }} + runs-on: ubuntu-latest + strategy: + matrix: + target: + - thumbv7em-none-eabihf + steps: + - uses: actions/checkout@v4 with: - command: fetch - - name: Build zcash_proofs for target - working-directory: ./zcash_proofs - run: cargo build --verbose --no-default-features --target ${{ matrix.target }} - - name: Build zcash_client_backend for target - working-directory: ./zcash_client_backend - run: cargo build --verbose --no-default-features --target ${{ matrix.target }} + path: crates + # We use a synthetic crate to ensure no dev-dependencies are enabled, which can + # be incompatible with some of these targets. + - name: Create synthetic crate for testing + run: cargo init --lib ci-build + - name: Copy Rust version into synthetic crate + run: cp crates/rust-toolchain.toml ci-build/ + - name: Copy patch directives into synthetic crate + run: | + echo "[patch.crates-io]" >> ./ci-build/Cargo.toml + cat ./crates/Cargo.toml | sed "0,/.\+\(patch.crates.\+\)/d" >> ./ci-build/Cargo.toml + - name: Add no_std pragma to lib.rs + run: | + echo "#![no_std]" > ./ci-build/src/lib.rs + - name: Add zcash_keys as a dependency of the synthetic crate + working-directory: ./ci-build + run: cargo add --no-default-features --path ../crates/zcash_keys + - name: Add pczt as a dependency of the synthetic crate + working-directory: ./ci-build + run: cargo add --no-default-features --path ../crates/pczt + - name: Add zcash_primitives as a dependency of the synthetic crate + working-directory: ./ci-build + run: cargo add --no-default-features --path ../crates/zcash_primitives + - name: Add lazy_static with the spin_no_std feature + working-directory: ./ci-build + run: cargo add lazy_static --features "spin_no_std" + - name: Add target + working-directory: ./ci-build + run: rustup target add ${{ matrix.target }} + - name: Build for target + working-directory: ./ci-build + run: cargo build --verbose --target ${{ matrix.target }} bitrot: name: Bitrot check runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - toolchain: 1.51.0 - override: true + - uses: actions/checkout@v4 # Build benchmarks to prevent bitrot - name: Build benchmarks - uses: actions-rs/cargo@v1 - with: - command: build - args: --all --benches + run: cargo build --all --benches clippy: - name: Clippy (1.51.0) - timeout-minutes: 30 + name: Clippy (MSRV) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - toolchain: 1.51.0 - components: clippy - override: true + - uses: actions/checkout@v4 + - id: prepare + uses: ./.github/actions/prepare - name: Run clippy uses: actions-rs/clippy-check@v1 with: - name: Clippy (1.51.0) + name: Clippy (MSRV) token: ${{ secrets.GITHUB_TOKEN }} - args: --all-features --all-targets -- -D warnings + args: > + ${{ steps.prepare.outputs.feature-flags }} + --all-targets + -- + -D warnings - clippy-nightly: - name: Clippy (nightly) - timeout-minutes: 30 + clippy-beta: + name: Clippy (beta) runs-on: ubuntu-latest continue-on-error: true steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - toolchain: nightly - components: clippy - override: true - - name: Run Clippy (nightly) + - uses: actions/checkout@v4 + - id: prepare + uses: ./.github/actions/prepare + - uses: dtolnay/rust-toolchain@beta + id: toolchain + - run: rustup override set ${{steps.toolchain.outputs.name}} + - name: Run Clippy (beta) uses: actions-rs/clippy-check@v1 continue-on-error: true with: - name: Clippy (nightly) + name: Clippy (beta) token: ${{ secrets.GITHUB_TOKEN }} - args: --all-features --all-targets -- -W clippy::all + args: > + ${{ steps.prepare.outputs.feature-flags }} + --all-targets + -- + -W clippy::all codecov: name: Code coverage runs-on: ubuntu-latest + container: + image: xd009642/tarpaulin:develop-nightly + options: --security-opt seccomp=unconfined steps: - - uses: actions/checkout@v2 - # Use stable for this to ensure that cargo-tarpaulin can be built. - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - - name: Install cargo-tarpaulin - uses: actions-rs/cargo@v1 - with: - command: install - args: cargo-tarpaulin - - - name: Fetch path to Zcash parameters - working-directory: ./zcash_proofs - shell: bash - run: echo "ZCASH_PARAMS=$(cargo run --release --example get-params-path --features directories)" >> $GITHUB_ENV - - name: Cache Zcash parameters - id: cache-params - uses: actions/cache@v2 + - uses: actions/checkout@v4 + - id: prepare + uses: ./.github/actions/prepare + - uses: actions/cache@v4 with: - path: ${{ env.ZCASH_PARAMS }} - key: ${{ runner.os }}-params - - name: Fetch Zcash parameters - if: steps.cache-params.outputs.cache-hit != 'true' - working-directory: ./zcash_proofs - run: cargo run --release --example download-params --features download-params - + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: codecov-cargo-${{ hashFiles('**/Cargo.lock') }} - name: Generate coverage report - uses: actions-rs/cargo@v1 - with: - command: tarpaulin - args: --all-features --release --timeout 600 --out Xml + run: > + cargo tarpaulin + --engine llvm + ${{ steps.prepare.outputs.feature-flags }} + --release + --timeout 600 + --out xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v5.1.2 with: - token: ${{secrets.CODECOV_TOKEN}} + token: ${{ secrets.CODECOV_TOKEN }} doc-links: name: Intra-doc links runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - toolchain: 1.51.0 - override: true - - name: cargo fetch - uses: actions-rs/cargo@v1 - with: - command: fetch - - # Ensure intra-documentation links all resolve correctly - # Requires #![deny(intra_doc_link_resolution_failure)] in crates. + - uses: actions/checkout@v4 + - id: prepare + uses: ./.github/actions/prepare + - run: cargo fetch + # Requires #![deny(rustdoc::broken_intra_doc_links)] in crates. - name: Check intra-doc links - uses: actions-rs/cargo@v1 - with: - command: doc - args: --all --document-private-items + run: > + cargo doc + --all + ${{ steps.prepare.outputs.feature-flags }} + --document-private-items fmt: name: Rustfmt - timeout-minutes: 30 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - toolchain: 1.51.0 - override: true + - uses: actions/checkout@v4 + - name: Check formatting + run: cargo fmt --all -- --check - # cargo fmt does not build the code, and running it in a fresh clone of - # the codebase will fail because the protobuf code has not been generated. - - name: cargo build - uses: actions-rs/cargo@v1 + protobuf: + name: protobuf consistency + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - id: prepare + uses: ./.github/actions/prepare + - name: Install protoc + uses: supplypike/setup-bin@v4 with: - command: build - args: --all + uri: 'https://github.com/protocolbuffers/protobuf/releases/download/v25.1/protoc-25.1-linux-x86_64.zip' + name: 'protoc' + version: '25.1' + subPath: 'bin' + - name: Trigger protobuf regeneration + run: > + cargo check + --workspace + ${{ steps.prepare.outputs.feature-flags }} + - name: Verify working directory is clean + run: git diff --exit-code - # Ensure all code has been formatted with rustfmt - - run: rustup component add rustfmt - - name: Check formatting - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check + uuid: + name: UUID validity + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Extract UUIDs + id: extract + run: | + { + echo 'UUIDS<> "$GITHUB_OUTPUT" + - name: Check UUID validity + env: + UUIDS: ${{ steps.extract.outputs.UUIDS }} + run: uuidparse -n -o type $UUIDS | xargs -L 1 test "invalid" != + - name: Check UUID type + env: + UUIDS: ${{ steps.extract.outputs.UUIDS }} + run: uuidparse -n -o type $UUIDS | xargs -L 1 test "random" = + - name: Check UUID uniqueness + env: + UUIDS: ${{ steps.extract.outputs.UUIDS }} + run: > + test $( + uuidparse -n -o uuid $U4 | wc -l + ) -eq $( + uuidparse -n -o uuid $U4 | sort | uniq | wc -l + ) + + required-checks: + name: Required status checks have passed + needs: + - required-test + - check-msrv + - build-latest + - build-nodefault + - bitrot + - clippy + - doc-links + - fmt + - protobuf + - uuid + if: ${{ always() }} + runs-on: ubuntu-latest + steps: + - name: Determine whether all required-pass steps succeeded + run: | + echo '${{ toJSON(needs) }}' | jq -e '[ .[] | .result == "success" ] | all' diff --git a/.gitignore b/.gitignore index fa8d85ac52..eb5a316cbd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ -Cargo.lock target diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 001f415bb4..0000000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,66 +0,0 @@ - -# /************************************************************************ - # File: .gitlab-ci.yml - # Author: mdr0id - # Date: 9/10/2018 - # Description: Used to setup runners/jobs for librustzcash - # Usage: Commit source and the pipeline will trigger the according jobs. - # For now the build and test are done in the same jobs. - # - # Known bugs/missing features: - # - # ************************************************************************/ - -stages: - - build - - test - - deploy - -rust-latest: - stage: build - image: rust:latest - script: - - cargo --verbose --version - - time cargo build --verbose - -rust-nightly: - stage: build - image: rustlang/rust:nightly - script: - - cargo --verbose --version - - cargo build --verbose - allow_failure: true - -librustzcash-test-latest: - stage: test - image: rust:latest - script: - - cargo --verbose --version - - time cargo test --release --verbose - -librustzcash-test-rust-nightly: - stage: test - image: rustlang/rust:nightly - script: - - cargo --verbose --version - - cargo test --release --verbose - allow_failure: true - -#used to manually deploy a given release -librustzcash-rust-rc: - stage: deploy - image: rust:latest - script: - - cargo --verbose --version - - time cargo build --release --verbose - when: manual - -#used to manually deploy a given release -librustzcash-rust-nightly-rc: - stage: deploy - image: rustlang/rust:nightly - script: - - cargo --verbose --version - - cargo build --release --verbose - allow_failure: true - when: manual diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..b31e2934f4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "rust-analyzer.cargo.features": "all", + "rust-analyzer.server.extraEnv": { "RUSTUP_TOOLCHAIN": "stable" } +} diff --git a/COPYING.md b/COPYING.md new file mode 100644 index 0000000000..4f1b0b77b4 --- /dev/null +++ b/COPYING.md @@ -0,0 +1,16 @@ +# License + +Licensed under either of + + * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +# Contribution + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you, as defined in the Apache-2.0 +license, shall be dual licensed as above, without any additional terms or +conditions. + diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000000..2aa7cfa5c5 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,6547 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.6", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", + "zeroize", +] + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "ambassador" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b27ba24e4d8a188489d5a03c7fabc167a60809a383cdb4d15feb37479cd2a48" +dependencies = [ + "itertools 0.10.5", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "amplify" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e711289a6cb28171b4f0e6c8019c69ff9476050508dc082167575d458ff74d0" +dependencies = [ + "amplify_derive", + "amplify_num", + "ascii", + "wasm-bindgen", +] + +[[package]] +name = "amplify_derive" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759dcbfaf94d838367a86d493ec34ccc8aa6fe365cb7880d6bf89006de24d9c1" +dependencies = [ + "amplify_syn", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "amplify_num" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04c009c5c4de814911b177e2ea59e4930bb918978ed3cce4900d846a6ceb0838" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "amplify_syn" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7736fb8d473c0d83098b5bac44df6a561e20470375cd8bcae30516dc889fd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anyhow" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" + +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "arti-client" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5a726d39f22ddf0dcf0ea903d900592167d60a32e46f202e83651a1be8c452e" +dependencies = [ + "async-trait", + "cfg-if", + "derive-deftly", + "derive_builder_fork_arti", + "derive_more", + "educe", + "fs-mistrust", + "futures", + "hostname-validator", + "humantime", + "humantime-serde", + "libc", + "postage", + "rand", + "safelog", + "serde", + "thiserror", + "tor-async-utils", + "tor-basic-utils", + "tor-chanmgr", + "tor-circmgr", + "tor-config", + "tor-dirmgr", + "tor-error", + "tor-guardmgr", + "tor-keymgr", + "tor-linkspec", + "tor-llcrypto", + "tor-memquota", + "tor-netdir", + "tor-netdoc", + "tor-persist", + "tor-proto", + "tor-rtcompat", + "tracing", + "void", +] + +[[package]] +name = "ascii" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" + +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + +[[package]] +name = "async-compression" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd066d0b4ef8ecb03a55319dc13aa6910616d0f44008a045bb1835af830abff5" +dependencies = [ + "flate2", + "futures-core", + "futures-io", + "memchr", + "pin-project-lite", + "xz2", + "zstd", + "zstd-safe", +] + +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "async-trait" +version = "0.1.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "async_executors" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a982d2f86de6137cc05c9db9a915a19886c97911f9790d04f174cede74be01a5" +dependencies = [ + "blanket", + "futures-core", + "futures-task", + "futures-util", + "pin-project", + "rustc_version", + "tokio", +] + +[[package]] +name = "asynchronous-codec" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a860072022177f903e59730004fb5dc13db9275b79bb2aef7ba8ce831956c233" +dependencies = [ + "bytes", + "futures-sink", + "futures-util", + "memchr", + "pin-project-lite", +] + +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + +[[package]] +name = "atomic" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "axum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper 1.0.1", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 0.1.2", + "tower-layer", + "tower-service", +] + +[[package]] +name = "backtrace" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bech32" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" + +[[package]] +name = "bellman" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afceed28bac7f9f5a508bca8aeeff51cdfa4770c0b967ac55c621e2ddfd6171" +dependencies = [ + "bitvec", + "blake2s_simd", + "byteorder", + "crossbeam-channel", + "ff", + "group", + "lazy_static", + "log", + "num_cpus", + "pairing", + "rand_core", + "rayon", + "subtle", +] + +[[package]] +name = "bip32" +version = "0.6.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143f5327f23168716be068f8e1014ba2ea16a6c91e8777bc8927da7b51e1df1f" +dependencies = [ + "bs58", + "hmac 0.13.0-pre.4", + "rand_core", + "ripemd 0.2.0-pre.4", + "secp256k1", + "sha2 0.11.0-pre.4", + "subtle", + "zeroize", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2b_simd" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23285ad32269793932e830392f2fe2f83e26488fd3ec778883a93c8323735780" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "blake2s_simd" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94230421e395b9920d23df13ea5d77a20e1725331f90fbbf6df6040b33f756ae" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "blanket" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0b121a9fe0df916e362fb3271088d071159cdf11db0e4182d02152850756eff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.11.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fd016a0ddc7cb13661bf5576073ce07330a693f8608a1320b4e20561cc12cdc" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bls12_381" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bc6d6292be3a19e6379786dac800f551e5865a5bb51ebbe3064ab80433f403" +dependencies = [ + "ff", + "group", + "pairing", + "rand_core", + "subtle", +] + +[[package]] +name = "bounded-vec-deque" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2225b558afc76c596898f5f1b3fc35cfce0eb1b13635cbd7d1b2a7177dc10ccd" + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "sha2 0.10.8", + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + +[[package]] +name = "bytemuck" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "102087e286b4677862ea56cf8fc58bb2cdfa8725c40ffb80fe3a008eb7f2fc83" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" + +[[package]] +name = "caret" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df55dc0c84d5a555c4b8b84ecf3cff724df77a7b1a8c4a70cd66a981524cff0" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets 0.52.6", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.6", + "inout", + "zeroize", +] + +[[package]] +name = "clap" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "coarsetime" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b3839cf01bb7960114be3ccf2340f541b6d0c81f8690b007b2b39f750f7e5d" +dependencies = [ + "libc", + "wasix", + "wasm-bindgen", +] + +[[package]] +name = "cobs" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "constant_time_eq" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "core2" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239fa3ae9b63c2dc74bd3fa852d4792b8b305ae64eeede946265b6af62f1fff3" +dependencies = [ + "memchr", +] + +[[package]] +name = "cpp_demangle" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96e58d342ad113c2b878f16d5d034c03be492ae460cdbc02b7f0f2284d310c7d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b8ce8218c97789f16356e7896b3714f26c2ee1079b79c0b7ae7064bb9089fa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "daggy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91a9304e55e9d601a39ae4deaba85406d5c0980e106f65afcf0460e9af1e7602" +dependencies = [ + "petgraph", +] + +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core 0.14.4", + "darling_macro 0.14.4", +] + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core 0.20.10", + "darling_macro 0.20.10", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 1.0.109", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.63", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core 0.14.4", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core 0.20.10", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derive-adhoc" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5283ac2881753c76c0892406705553f0d9ab30649f81e18964d3408f4501edb8" +dependencies = [ + "derive-adhoc-macros", + "heck 0.4.1", +] + +[[package]] +name = "derive-adhoc-macros" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c21b673a9b8c78c34908e6fcb42b922e11c4df2de5237f1c3f58d3285904a84b" +dependencies = [ + "heck 0.4.1", + "itertools 0.11.0", + "proc-macro-crate", + "proc-macro2", + "quote", + "sha3", + "strum 0.25.0", + "syn 1.0.109", + "void", +] + +[[package]] +name = "derive-deftly" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f9bc3564f74be6c35d49a7efee54380d7946ccc631323067f33fabb9246027" +dependencies = [ + "derive-deftly-macros", + "heck 0.5.0", +] + +[[package]] +name = "derive-deftly-macros" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b84d32b18d9a256d81e4fec2e4cfd0ab6dde5e5ff49be1713ae0adbd0060c2" +dependencies = [ + "heck 0.5.0", + "indexmap 2.6.0", + "itertools 0.13.0", + "proc-macro-crate", + "proc-macro2", + "quote", + "sha3", + "strum 0.26.3", + "syn 2.0.63", + "void", +] + +[[package]] +name = "derive_builder_core_fork_arti" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24c1b715c79be6328caa9a5e1a387a196ea503740f0722ec3dd8f67a9e72314d" +dependencies = [ + "darling 0.14.4", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder_fork_arti" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3eae24d595f4d0ecc90a9a5a6d11c2bd8dafe2375ec4a1ec63250e5ade7d228" +dependencies = [ + "derive_builder_macro_fork_arti", +] + +[[package]] +name = "derive_builder_macro_fork_arti" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69887769a2489cd946bf782eb2b1bb2cb7bc88551440c94a765d4f040c08ebf3" +dependencies = [ + "derive_builder_core_fork_arti", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn 2.0.63", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid", + "crypto-common 0.1.6", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.0-pre.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf2e3d6615d99707295a9673e889bf363a04b2a466bd320c65a72536f7577379" +dependencies = [ + "block-buffer 0.11.0-rc.3", + "crypto-common 0.2.0-rc.1", + "subtle", +] + +[[package]] +name = "directories" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "displaydoc" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "document-features" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef5282ad69563b5fc40319526ba27e0e7363d552a896f0297d54f767717f9b95" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + +[[package]] +name = "dynosaur" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47baef31f408cc1a54eadcb0160ffe2b935b5963bd2b519719f3a101749af524" +dependencies = [ + "dynosaur_derive", + "trait-variant", +] + +[[package]] +name = "dynosaur_derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320c6861ca45ea23faee2bd86a6c7f20ec4b7a64a2773971cd4f52ae61615463" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "merlin", + "rand_core", + "serde", + "sha2 0.10.8", + "subtle", + "zeroize", +] + +[[package]] +name = "educe" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0042ff8246a363dbe77d2ceedb073339e85a804b9a47636c6e016a9a32c05f" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "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 = "enum-ordinalize" +version = "3.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf1fa3f06bbff1ea5b1a9c7b14aa992a39657db60a2759457328d7e058f49ee" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "equihash" +version = "0.2.0" +dependencies = [ + "blake2b_simd", + "core2", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "f4jumble" +version = "0.1.1" +dependencies = [ + "blake2b_simd", + "hex", + "proptest", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "bitvec", + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "figment" +version = "0.10.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" +dependencies = [ + "atomic 0.6.0", + "serde", + "toml", + "uncased", + "version_check", +] + +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.59.0", +] + +[[package]] +name = "findshlibs" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40b9e59cd0f7e0806cca4be089683ecb6434e602038df21fe6bf6711b2f07f64" +dependencies = [ + "cc", + "lazy_static", + "libc", + "winapi", +] + +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "flate2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fluid-let" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "749cff877dc1af878a0b31a41dd221a753634401ea0ef2f87b62d3171522485a" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "fpe" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c4b37de5ae15812a764c958297cfc50f5c010438f60c6ce75d11b802abd404" +dependencies = [ + "cbc", + "cipher", + "libm", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "fs-mistrust" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf84fbaf375e6a485fa126f6335c0cfa7741114aa4f86ba37960a42cd1994b8" +dependencies = [ + "derive_builder_fork_arti", + "dirs", + "libc", + "once_cell", + "pwd-grp", + "serde", + "thiserror", + "walkdir", +] + +[[package]] +name = "fslock" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04412b8935272e3a9bae6f48c7bfff74c2911f60525404edfdd28e49884c3bfb" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "futures-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" +dependencies = [ + "futures-io", + "rustls 0.23.12", + "rustls-pki-types", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getset" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f636605b743120a8d32ed92fc27b6cde1a769f8f936c065151eb66f88ded513c" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "glob-match" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985c9503b412198aa4197559e9a318524ebc4519c229bfa05a535828c950b9d" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "memuse", + "rand_core", + "subtle", +] + +[[package]] +name = "gumdrop" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bc700f989d2f6f0248546222d9b4258f5b02a171a431f8285a81c08142629e3" +dependencies = [ + "gumdrop_derive", +] + +[[package]] +name = "gumdrop_derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "729f9bd3449d77e7831a18abfb7ba2f99ee813dfd15b8c2167c9a54ba20aa99d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "h2" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.6.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0" +dependencies = [ + "crunchy", +] + +[[package]] +name = "halo2_gadgets" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73a5e510d58a07d8ed238a5a8a436fe6c2c79e1bb2611f62688bc65007b4e6e7" +dependencies = [ + "arrayvec", + "bitvec", + "ff", + "group", + "halo2_poseidon", + "halo2_proofs", + "lazy_static", + "pasta_curves", + "rand", + "sinsemilla", + "subtle", + "uint", +] + +[[package]] +name = "halo2_legacy_pdqsort" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47716fe1ae67969c5e0b2ef826f32db8c3be72be325e1aa3c1951d06b5575ec5" + +[[package]] +name = "halo2_poseidon" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa3da60b81f02f9b33ebc6252d766f843291fb4d2247a07ae73d20b791fc56f" +dependencies = [ + "bitvec", + "ff", + "group", + "pasta_curves", +] + +[[package]] +name = "halo2_proofs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b867a8d9bbb85fca76fff60652b5cd19b853a1c4d0665cb89bee68b18d2caf0" +dependencies = [ + "blake2b_simd", + "ff", + "group", + "halo2_legacy_pdqsort", + "maybe-rayon", + "pasta_curves", + "rand_core", + "tracing", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin 0.9.8", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0-pre.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4b1fb14e4df79f9406b434b60acef9f45c26c50062cccf1346c6103b8c47d58" +dependencies = [ + "digest 0.11.0-pre.9", +] + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "hostname-validator" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f558a64ac9af88b5ba400d99b579451af0d39c6d360980045b91aac966d705e2" + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "humantime-serde" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c" +dependencies = [ + "humantime", + "serde", +] + +[[package]] +name = "hybrid-array" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2d35805454dc9f8662a98d6d61886ffe26bd465f5960e0e55345c70d5c0d2a9" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3203a961e5c83b6f5498933e78b6b263e208c197b63e9c6c53cc82ffd3f63793" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "incrementalmerkletree" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30821f91f0fa8660edca547918dc59812893b497d07c1144f326f07fdd94aba9" +dependencies = [ + "either", + "proptest", + "rand", + "rand_core", +] + +[[package]] +name = "incrementalmerkletree-testing" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad20fb6cf815e76ce9b9eca74f347740ab99059fe4b5e4a002403d0441a02983" +dependencies = [ + "incrementalmerkletree", + "proptest", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown 0.15.0", + "serde", +] + +[[package]] +name = "inferno" +version = "0.11.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "232929e1d75fe899576a3d5c7416ad0d88dbfbb3c3d6aa00873a7408a50ddb88" +dependencies = [ + "ahash", + "indexmap 2.6.0", + "is-terminal", + "itoa", + "log", + "num-format", + "once_cell", + "quick-xml", + "rgb", + "str_stack", +] + +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "inventory" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f958d3d68f4167080a18141e10381e7634563984a537f2a49a30fd8e53ac5767" + +[[package]] +name = "is-terminal" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "jobserver" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "jubjub" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8499f7a74008aafbecb2a2e608a3e13e4dd3e84df198b604451efe93f2de6e61" +dependencies = [ + "bitvec", + "bls12_381", + "ff", + "group", + "rand_core", + "subtle", +] + +[[package]] +name = "keccak" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "known-folders" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4397c789f2709d23cfcb703b316e0766a8d4b17db2d47b0ab096ef6047cae1d8" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "kqueue" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin 0.9.8", +] + +[[package]] +name = "libc" +version = "0.2.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", + "redox_syscall", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memmap2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +dependencies = [ + "libc", +] + +[[package]] +name = "memuse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d97bbf43eb4f088f8ca469930cde17fa036207c9a5e02ccc5107c4e8b17c964" + +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core", + "zeroize", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +dependencies = [ + "adler", +] + +[[package]] +name = "minreq" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763d142cdff44aaadd9268bebddb156ef6c65a0e13486bb81673cf2d8739f9b0" +dependencies = [ + "log", + "once_cell", + "rustls 0.21.12", + "rustls-webpki 0.101.7", + "webpki-roots 0.25.4", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nonempty" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "549e471b99ccaf2f89101bec68f4d244457d5a95a9c3d0672e9564124397741d" + +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.6.0", + "filetime", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "walkdir", + "windows-sys 0.48.0", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-bigint" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-format" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" +dependencies = [ + "arrayvec", + "itoa", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "oneshot-fused-workaround" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f49cbc8293c5ba37516d29aba392a94a34638367d17d67617cea34e4f9acd05" +dependencies = [ + "futures", +] + +[[package]] +name = "oorandom" +version = "11.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "orchard" +version = "0.10.1" +source = "git+https://github.com/zcash/orchard.git?rev=b1c22c07300db22239235d16dab096e23369948f#b1c22c07300db22239235d16dab096e23369948f" +dependencies = [ + "aes", + "bitvec", + "blake2b_simd", + "core2", + "ff", + "fpe", + "getset", + "group", + "halo2_gadgets", + "halo2_poseidon", + "halo2_proofs", + "hex", + "incrementalmerkletree", + "lazy_static", + "memuse", + "nonempty", + "pasta_curves", + "proptest", + "rand", + "reddsa", + "serde", + "sinsemilla", + "subtle", + "tracing", + "visibility", + "zcash_note_encryption", + "zcash_spec", + "zip32", +] + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.8", +] + +[[package]] +name = "p384" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.8", +] + +[[package]] +name = "p521" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" +dependencies = [ + "base16ct", + "ecdsa", + "elliptic-curve", + "primeorder", + "rand_core", + "sha2 0.10.8", +] + +[[package]] +name = "pairing" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" +dependencies = [ + "group", +] + +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "pasta_curves" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e57598f73cc7e1b2ac63c79c517b31a0877cd7c402cdcaa311b5208de7a095" +dependencies = [ + "blake2b_simd", + "ff", + "group", + "lazy_static", + "rand", + "static_assertions", + "subtle", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pczt" +version = "0.1.0" +dependencies = [ + "blake2b_simd", + "bls12_381", + "ff", + "getset", + "incrementalmerkletree", + "jubjub", + "nonempty", + "orchard", + "pasta_curves", + "postcard", + "rand_core", + "redjubjub", + "sapling-crypto", + "secp256k1", + "serde", + "serde_with", + "shardtree", + "zcash_note_encryption", + "zcash_primitives", + "zcash_proofs", + "zcash_protocol", + "zcash_transparent", + "zip32", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap 2.6.0", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "plotters" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" + +[[package]] +name = "plotters-svg" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "postage" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3fb618632874fb76937c2361a7f22afd393c982a2165595407edc75b06d3c1" +dependencies = [ + "atomic 0.5.3", + "crossbeam-queue", + "futures", + "parking_lot", + "pin-project", + "static_assertions", + "thiserror", +] + +[[package]] +name = "postcard" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170a2601f67cc9dba8edd8c4870b15f71a6a2dc196daec8c83f72b59dff628a8" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "serde", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "pprof" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef5c97c51bd34c7e742402e216abdeb44d415fbe6ae41d56b114723e953711cb" +dependencies = [ + "backtrace", + "cfg-if", + "criterion", + "findshlibs", + "inferno", + "libc", + "log", + "nix", + "once_cell", + "parking_lot", + "smallvec", + "symbolic-demangle", + "tempfile", + "thiserror", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "prettyplease" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" +dependencies = [ + "proc-macro2", + "syn 2.0.63", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "primitive-types" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" +dependencies = [ + "fixed-hash", + "uint", +] + +[[package]] +name = "priority-queue" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714c75db297bc88a63783ffc6ab9f830698a6705aa0201416931759ef4c8183d" +dependencies = [ + "autocfg", + "equivalent", + "indexmap 2.6.0", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.6.0", + "lazy_static", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax 0.8.4", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "prost" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f3e5beed80eb580c68e2c600937ac2c4eedabdfd5ef1e5b7ea4f3fba84497b" +dependencies = [ + "heck 0.5.0", + "itertools 0.13.0", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.63", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3" +dependencies = [ + "anyhow", + "itertools 0.13.0", + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "prost-types" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2f1e56baa61e93533aebc21af4d2134b70f66275e0fcdf3cbe43d77ff7e8fc" +dependencies = [ + "prost", +] + +[[package]] +name = "pwd-grp" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6955c41fd7e4283bdf6ff3e7218b7e3f8ef24c4236b31d22be050f4cfd5e2a2c" +dependencies = [ + "derive-adhoc", + "libc", + "paste", + "thiserror", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-xml" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "reddsa" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78a5191930e84973293aa5f532b513404460cd2216c1cfb76d08748c15b40b02" +dependencies = [ + "blake2b_simd", + "byteorder", + "group", + "hex", + "jubjub", + "pasta_curves", + "rand_core", + "serde", + "thiserror", + "zeroize", +] + +[[package]] +name = "redjubjub" +version = "0.7.0" +source = "git+https://github.com/ZcashFoundation/redjubjub?rev=eae848c5c14d9c795d000dd9f4c4762d1aee7ee1#eae848c5c14d9c795d000dd9f4c4762d1aee7ee1" +dependencies = [ + "rand_core", + "reddsa", + "thiserror", + "zeroize", +] + +[[package]] +name = "redox_syscall" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "redox_users" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.4", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "retry-error" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ef93545b13f6dd83a9f98c8a656ccbd7bc2b95a747844346c70f7e6f5a5932" + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac 0.12.1", + "subtle", +] + +[[package]] +name = "rgb" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "ripemd" +version = "0.2.0-pre.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48cf93482ea998ad1302c42739bc73ab3adc574890c373ec89710e219357579" +dependencies = [ + "digest 0.11.0-pre.9", +] + +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "sha2 0.10.8", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags 2.6.0", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", + "time", + "uuid", +] + +[[package]] +name = "rust_decimal" +version = "1.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1790d1c4c0ca81211399e0e0af16333276f375209e71a37b67698a373db5b47a" +dependencies = [ + "arrayvec", + "num-traits", + "serde", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring 0.17.8", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.23.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" +dependencies = [ + "log", + "once_cell", + "ring 0.17.8", + "rustls-pki-types", + "rustls-webpki 0.102.6", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +dependencies = [ + "base64", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring 0.17.8", + "untrusted 0.9.0", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" +dependencies = [ + "ring 0.17.8", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "safelog" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3939f5e7c65f96a54e7d2a6853e3994d691c95f54f5263916a2d1877dad70789" +dependencies = [ + "derive_more", + "educe", + "either", + "fluid-let", + "thiserror", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "sanitize-filename" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ed72fbaf78e6f2d41744923916966c4fbe3d7c74e3037a8ee482f1115572603" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "sapling-crypto" +version = "0.4.0" +source = "git+https://github.com/zcash/sapling-crypto.git?rev=6ca338532912adcd82369220faeea31aab4720c5#6ca338532912adcd82369220faeea31aab4720c5" +dependencies = [ + "aes", + "bellman", + "bitvec", + "blake2b_simd", + "blake2s_simd", + "bls12_381", + "core2", + "document-features", + "ff", + "fpe", + "getset", + "group", + "hex", + "incrementalmerkletree", + "jubjub", + "lazy_static", + "memuse", + "proptest", + "rand", + "rand_core", + "redjubjub", + "subtle", + "tracing", + "zcash_note_encryption", + "zcash_spec", + "zip32", +] + +[[package]] +name = "schemerz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e82960ac11ccabd77d53c933532612079e01205b5873ec5095f4b3426493434" +dependencies = [ + "daggy", + "indexmap 1.9.3", + "log", + "thiserror", + "uuid", +] + +[[package]] +name = "schemerz-rusqlite" +version = "0.320.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65ff99b7d9e8790fb20a7e52a482f66fddb3c28c3ce700c6c2665cacbf1b5529" +dependencies = [ + "rusqlite", + "schemerz", + "uuid", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring 0.17.8", + "untrusted 0.9.0", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "secp256k1" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +dependencies = [ + "rand", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.204" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.204" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "serde_ignored" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8e319a36d1b52126a0d608f24e93b2d81297091818cd70625fcf50a15d84ddf" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_json" +version = "1.0.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_with" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.6.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" +dependencies = [ + "darling 0.20.10", + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0-pre.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "540c0893cce56cdbcfebcec191ec8e0f470dd1889b6e7a0b503e310a94a168f5" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.11.0-pre.9", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.7", + "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 = "shardtree" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "637e95dcd06bc1bb3f86ed9db1e1832a70125f32daae071ef37dcb7701b7d4fe" +dependencies = [ + "assert_matches", + "bitflags 2.6.0", + "either", + "incrementalmerkletree", + "incrementalmerkletree-testing", + "proptest", + "tracing", +] + +[[package]] +name = "shellexpand" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b" +dependencies = [ + "dirs", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core", +] + +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + +[[package]] +name = "sinsemilla" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d268ae0ea06faafe1662e9967cd4f9022014f5eeb798e0c302c876df8b7af9c" +dependencies = [ + "group", + "pasta_curves", + "subtle", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "ssh-cipher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f" +dependencies = [ + "cipher", + "ssh-encoding", +] + +[[package]] +name = "ssh-encoding" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15" +dependencies = [ + "base64ct", + "pem-rfc7468", + "sha2 0.10.8", +] + +[[package]] +name = "ssh-key" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca9b366a80cf18bb6406f4cf4d10aebfb46140a8c0c33f666a144c5c76ecbafc" +dependencies = [ + "p256", + "p384", + "p521", + "rand_core", + "rsa", + "sec1", + "sha2 0.10.8", + "signature", + "ssh-cipher", + "ssh-encoding", + "subtle", + "zeroize", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "str_stack" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9091b6114800a5f2141aee1d1b9d6ca3592ac062dc5decb3764ec5895a47b4eb" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros 0.25.3", +] + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros 0.26.4", +] + +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.63", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.63", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "symbolic-common" +version = "12.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13a4dfe4bbeef59c1f32fc7524ae7c95b9e1de5e79a43ce1604e181081d71b0c" +dependencies = [ + "debugid", + "memmap2", + "stable_deref_trait", + "uuid", +] + +[[package]] +name = "symbolic-demangle" +version = "12.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cf6a95abff97de4d7ff3473f33cacd38f1ddccad5c1feab435d6760300e3b6" +dependencies = [ + "cpp_demangle", + "rustc-demangle", + "symbolic-common", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0d409377ff5b1e3ca6437aa86c1eb7d40c134bfec254e44c830defa92669db5" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls 0.23.12", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.22", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.6.0", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap 2.6.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.6.20", +] + +[[package]] +name = "tonic" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38659f4a91aba8598d27821589f5db7dddd94601e7a01b1e485a50e5484c7401" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "prost", + "rustls-pemfile", + "socket2", + "tokio", + "tokio-rustls 0.26.0", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", + "webpki-roots 0.26.3", +] + +[[package]] +name = "tonic-build" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "tor-async-utils" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2adb0fa957cad5a1f408e357a4450931366a9d35b78235d72260b6842518ba" +dependencies = [ + "educe", + "futures", + "oneshot-fused-workaround", + "pin-project", + "postage", + "void", +] + +[[package]] +name = "tor-basic-utils" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d080757c5d7404813f936147e3d72f7c035eba541c1422de29acd2242b98f36" +dependencies = [ + "derive_more", + "hex", + "itertools 0.13.0", + "libc", + "paste", + "rand", + "rand_chacha", + "serde", + "slab", + "thiserror", +] + +[[package]] +name = "tor-bytes" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23db3a71001bc0a3f25b0f6ee824b559d128768275811de0d913369990f9a83b" +dependencies = [ + "bytes", + "derive-deftly", + "digest 0.10.7", + "educe", + "getrandom", + "safelog", + "thiserror", + "tor-error", + "tor-llcrypto", + "zeroize", +] + +[[package]] +name = "tor-cell" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "637cf998679d463138d9bffe07c55bec1c3ccc43a66e4a587d8952ceaa6ee7d4" +dependencies = [ + "bitflags 2.6.0", + "bytes", + "caret", + "derive_more", + "educe", + "paste", + "rand", + "smallvec", + "thiserror", + "tor-basic-utils", + "tor-bytes", + "tor-cert", + "tor-error", + "tor-linkspec", + "tor-llcrypto", + "tor-units", +] + +[[package]] +name = "tor-cert" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "437567f6b43fab396478d01ff15ce717587bda41a15f06c4de87f8fee1fefbb0" +dependencies = [ + "caret", + "derive_more", + "digest 0.10.7", + "thiserror", + "tor-bytes", + "tor-checkable", + "tor-llcrypto", +] + +[[package]] +name = "tor-chanmgr" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc267bf4aba055859d51c34dcab067c109b89cefed9501409377772cbb296d8b" +dependencies = [ + "async-trait", + "derive_builder_fork_arti", + "derive_more", + "educe", + "futures", + "oneshot-fused-workaround", + "postage", + "rand", + "safelog", + "serde", + "thiserror", + "tor-async-utils", + "tor-basic-utils", + "tor-cell", + "tor-config", + "tor-error", + "tor-linkspec", + "tor-llcrypto", + "tor-netdir", + "tor-proto", + "tor-rtcompat", + "tor-socksproto", + "tor-units", + "tracing", + "void", +] + +[[package]] +name = "tor-checkable" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a052723dc4fc53605232652d75997940cd51abc4cd3ff74daca746c150f5ac7" +dependencies = [ + "humantime", + "signature", + "thiserror", + "tor-llcrypto", +] + +[[package]] +name = "tor-circmgr" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00458ecadba3e0f8566f6be9fcf41bd2a50b3f45f0a349057510b2d37322c21" +dependencies = [ + "amplify", + "async-trait", + "bounded-vec-deque", + "cfg-if", + "derive_builder_fork_arti", + "derive_more", + "downcast-rs", + "dyn-clone", + "educe", + "futures", + "humantime-serde", + "itertools 0.13.0", + "once_cell", + "oneshot-fused-workaround", + "pin-project", + "rand", + "retry-error", + "safelog", + "serde", + "static_assertions", + "thiserror", + "tor-async-utils", + "tor-basic-utils", + "tor-chanmgr", + "tor-config", + "tor-error", + "tor-guardmgr", + "tor-linkspec", + "tor-netdir", + "tor-netdoc", + "tor-persist", + "tor-proto", + "tor-protover", + "tor-relay-selection", + "tor-rtcompat", + "tracing", + "void", + "weak-table", +] + +[[package]] +name = "tor-config" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7281aaa74794b23cf570547ff7706fbca5f8564d421921943420f2c70ddecfa" +dependencies = [ + "amplify", + "derive-deftly", + "derive_builder_fork_arti", + "directories", + "educe", + "either", + "figment", + "fs-mistrust", + "futures", + "itertools 0.13.0", + "notify", + "once_cell", + "paste", + "postage", + "regex", + "serde", + "serde-value", + "serde_ignored", + "shellexpand", + "strum 0.26.3", + "thiserror", + "toml", + "tor-basic-utils", + "tor-error", + "tor-rtcompat", + "tracing", + "void", +] + +[[package]] +name = "tor-consdiff" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0da0ead1be2509af9558ff9b8004d25e7e4795a3dfa4d78968d8c43d231e76c7" +dependencies = [ + "digest 0.10.7", + "hex", + "thiserror", + "tor-llcrypto", +] + +[[package]] +name = "tor-dirclient" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b3fb55282536a5aaf998ceaa1d2f5a268119c3b3cf8de741eaf98f5ed5af079" +dependencies = [ + "async-compression", + "base64ct", + "derive_more", + "futures", + "hex", + "http", + "httparse", + "httpdate", + "itertools 0.13.0", + "memchr", + "thiserror", + "tor-circmgr", + "tor-error", + "tor-linkspec", + "tor-llcrypto", + "tor-netdoc", + "tor-proto", + "tor-rtcompat", + "tracing", +] + +[[package]] +name = "tor-dirmgr" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8bf47d53f83a55f4cc61fa77bcd051c283d1a1da60f67e9370d845df3480256" +dependencies = [ + "async-trait", + "base64ct", + "derive_builder_fork_arti", + "derive_more", + "digest 0.10.7", + "educe", + "event-listener", + "fs-mistrust", + "fslock", + "futures", + "hex", + "humantime", + "humantime-serde", + "itertools 0.13.0", + "memmap2", + "once_cell", + "oneshot-fused-workaround", + "paste", + "postage", + "rand", + "rusqlite", + "safelog", + "scopeguard", + "serde", + "signature", + "strum 0.26.3", + "thiserror", + "time", + "tor-async-utils", + "tor-basic-utils", + "tor-checkable", + "tor-circmgr", + "tor-config", + "tor-consdiff", + "tor-dirclient", + "tor-error", + "tor-guardmgr", + "tor-llcrypto", + "tor-netdir", + "tor-netdoc", + "tor-persist", + "tor-proto", + "tor-rtcompat", + "tracing", +] + +[[package]] +name = "tor-error" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d764640168b176da6c44e4d5ceda97db3aca89d6f7e5ee398f454c56ba6a34c2" +dependencies = [ + "derive_more", + "futures", + "once_cell", + "paste", + "retry-error", + "static_assertions", + "strum 0.26.3", + "thiserror", + "tracing", + "void", +] + +[[package]] +name = "tor-guardmgr" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82444165f5f3a2587e4c93258c7a554718a4985780d262ae158a5e24fcbee019" +dependencies = [ + "amplify", + "base64ct", + "derive-deftly", + "derive_builder_fork_arti", + "derive_more", + "dyn-clone", + "educe", + "futures", + "humantime", + "humantime-serde", + "itertools 0.13.0", + "num_enum", + "oneshot-fused-workaround", + "pin-project", + "postage", + "rand", + "safelog", + "serde", + "strum 0.26.3", + "thiserror", + "tor-async-utils", + "tor-basic-utils", + "tor-config", + "tor-error", + "tor-linkspec", + "tor-llcrypto", + "tor-netdir", + "tor-netdoc", + "tor-persist", + "tor-proto", + "tor-relay-selection", + "tor-rtcompat", + "tor-units", + "tracing", +] + +[[package]] +name = "tor-hscrypto" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda7d9ec707605c7bdd71c29a94e5cf6c1a61ed42f815b5dd2da3f1546393ca1" +dependencies = [ + "data-encoding", + "derive_more", + "digest 0.10.7", + "itertools 0.13.0", + "paste", + "rand", + "safelog", + "signature", + "subtle", + "thiserror", + "tor-basic-utils", + "tor-bytes", + "tor-error", + "tor-llcrypto", + "tor-units", +] + +[[package]] +name = "tor-key-forge" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06c2a026f0a21f9870799eb3ac24340bfa71efb6126d9c7294a6d6c401a8ba67" +dependencies = [ + "derive-deftly", + "derive_more", + "downcast-rs", + "paste", + "rand", + "signature", + "ssh-key", + "thiserror", + "tor-error", + "tor-hscrypto", + "tor-llcrypto", +] + +[[package]] +name = "tor-keymgr" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f66ea6684cd1110c8b405443611b1c8f24cca4d8ca05a41ee747ea4479ad4c5c" +dependencies = [ + "amplify", + "arrayvec", + "derive-deftly", + "derive_builder_fork_arti", + "derive_more", + "downcast-rs", + "dyn-clone", + "fs-mistrust", + "glob-match", + "humantime", + "inventory", + "itertools 0.13.0", + "rand", + "serde", + "ssh-key", + "thiserror", + "tor-basic-utils", + "tor-config", + "tor-error", + "tor-hscrypto", + "tor-key-forge", + "tor-llcrypto", + "tor-persist", + "tracing", + "walkdir", + "zeroize", +] + +[[package]] +name = "tor-linkspec" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53ccbd9372da197987699399c88695ecd1737c2b0e6267c540a3febdf92b5643" +dependencies = [ + "base64ct", + "by_address", + "caret", + "derive-deftly", + "derive_builder_fork_arti", + "derive_more", + "hex", + "itertools 0.13.0", + "safelog", + "serde", + "serde_with", + "strum 0.26.3", + "thiserror", + "tor-basic-utils", + "tor-bytes", + "tor-config", + "tor-llcrypto", + "tor-protover", +] + +[[package]] +name = "tor-llcrypto" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b0c146b5c77f609dda6992f60940586fee2aa711dab5d59b3ecc7c8c958d787" +dependencies = [ + "aes", + "base64ct", + "ctr", + "curve25519-dalek", + "derive_more", + "digest 0.10.7", + "ed25519-dalek", + "educe", + "getrandom", + "hex", + "rand_core", + "rsa", + "safelog", + "serde", + "sha1", + "sha2 0.10.8", + "sha3", + "signature", + "simple_asn1", + "subtle", + "thiserror", + "visibility", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "tor-log-ratelim" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a6fada5f9dae169bd0caf2d61932c528e8136790a5a553eac9de30d6290fe7e" +dependencies = [ + "futures", + "humantime", + "once_cell", + "thiserror", + "tor-error", + "tor-rtcompat", + "tracing", + "weak-table", +] + +[[package]] +name = "tor-memquota" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12551616f15f9d1fb4e37ee14ffc0b6743d0c31affbb4643374f456c0a820073" +dependencies = [ + "derive-deftly", + "derive_more", + "educe", + "futures", + "pin-project", + "serde", + "slotmap", + "static_assertions", + "thiserror", + "tor-async-utils", + "tor-basic-utils", + "tor-config", + "tor-error", + "tor-log-ratelim", + "tor-rtcompat", + "tracing", + "void", +] + +[[package]] +name = "tor-netdir" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d9b832338169a882cb895c79ea60089641749e1c2d6d4f7b8e23cc010185dfa" +dependencies = [ + "bitflags 2.6.0", + "derive_more", + "futures", + "humantime", + "itertools 0.13.0", + "num_enum", + "rand", + "serde", + "static_assertions", + "strum 0.26.3", + "thiserror", + "tor-basic-utils", + "tor-error", + "tor-linkspec", + "tor-llcrypto", + "tor-netdoc", + "tor-protover", + "tor-units", + "tracing", + "typed-index-collections", +] + +[[package]] +name = "tor-netdoc" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a5fbfe1444d96e08bb97cdf717a62738e5f1bd5a594c5520a05f683b8a516e3" +dependencies = [ + "amplify", + "base64ct", + "bitflags 2.6.0", + "cipher", + "derive_builder_fork_arti", + "derive_more", + "digest 0.10.7", + "educe", + "hex", + "humantime", + "itertools 0.13.0", + "once_cell", + "phf", + "serde", + "serde_with", + "signature", + "smallvec", + "subtle", + "thiserror", + "time", + "tinystr", + "tor-basic-utils", + "tor-bytes", + "tor-cell", + "tor-cert", + "tor-checkable", + "tor-error", + "tor-llcrypto", + "tor-protover", + "void", + "weak-table", + "zeroize", +] + +[[package]] +name = "tor-persist" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5acf1de8a8ad4817d8ebdf1250bddbe9efa4b25dea0a339ae8dbb1f7d7f28583" +dependencies = [ + "derive-deftly", + "derive_more", + "filetime", + "fs-mistrust", + "fslock", + "futures", + "itertools 0.13.0", + "oneshot-fused-workaround", + "paste", + "sanitize-filename", + "serde", + "serde_json", + "thiserror", + "tor-async-utils", + "tor-basic-utils", + "tor-error", + "tracing", + "void", +] + +[[package]] +name = "tor-proto" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "073a996250f73d7a814d9049de321a1f59a710dc18b92a4da8d26f002a5735dd" +dependencies = [ + "asynchronous-codec", + "bitvec", + "bytes", + "cipher", + "coarsetime", + "derive_builder_fork_arti", + "derive_more", + "digest 0.10.7", + "educe", + "futures", + "hkdf", + "hmac 0.12.1", + "oneshot-fused-workaround", + "pin-project", + "rand", + "rand_core", + "safelog", + "subtle", + "thiserror", + "tokio", + "tokio-util", + "tor-async-utils", + "tor-basic-utils", + "tor-bytes", + "tor-cell", + "tor-cert", + "tor-checkable", + "tor-config", + "tor-error", + "tor-linkspec", + "tor-llcrypto", + "tor-log-ratelim", + "tor-rtcompat", + "tor-rtmock", + "tor-units", + "tracing", + "typenum", + "void", + "zeroize", +] + +[[package]] +name = "tor-protover" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae806e57efcbd59694fd3c3c8100bc7dabc2dfd0bda70f122c7489fc0e889704" +dependencies = [ + "caret", + "thiserror", +] + +[[package]] +name = "tor-relay-selection" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ca1a4d767cd52ee8723dfe9dc3ec640b7a37527e009bc0860301a1aad249871" +dependencies = [ + "rand", + "serde", + "tor-basic-utils", + "tor-linkspec", + "tor-netdir", + "tor-netdoc", +] + +[[package]] +name = "tor-rtcompat" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df693852fb9a1c1a8bca1673fa3a8e1bca929fb2753165b1efce70f9381323b0" +dependencies = [ + "async-trait", + "async_executors", + "coarsetime", + "derive_more", + "educe", + "futures", + "futures-rustls", + "paste", + "pin-project", + "rustls-pki-types", + "thiserror", + "tokio", + "tokio-util", + "tor-error", + "tracing", + "void", + "x509-signature", +] + +[[package]] +name = "tor-rtmock" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f540f59b3194f4ad697d6ffa13013e2b92e1058cd1b3beb9c8a500d2b805a92" +dependencies = [ + "amplify", + "async-trait", + "derive-deftly", + "derive_more", + "educe", + "futures", + "humantime", + "itertools 0.13.0", + "oneshot-fused-workaround", + "pin-project", + "priority-queue", + "slotmap", + "strum 0.26.3", + "thiserror", + "tor-error", + "tor-rtcompat", + "tracing", + "tracing-test", + "void", +] + +[[package]] +name = "tor-socksproto" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308849623b9aa2a604c42a68081f7956ea6d4b5f378dd94ef0a36f05e86399ff" +dependencies = [ + "caret", + "subtle", + "thiserror", + "tor-bytes", + "tor-error", +] + +[[package]] +name = "tor-units" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de60d366ac1128f4cd3e1bae601a3d0393cd0c4e9e0e05eee605d28a8a5ce704" +dependencies = [ + "derive_more", + "thiserror", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tracing-test" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "557b891436fe0d5e0e363427fc7f217abf9ccd510d5136549847bdcbcd011d68" +dependencies = [ + "tracing-core", + "tracing-subscriber", + "tracing-test-macro", +] + +[[package]] +name = "tracing-test-macro" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" +dependencies = [ + "quote", + "syn 2.0.63", +] + +[[package]] +name = "trait-variant" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typed-index-collections" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "183496e014253d15abbe6235677b1392dba2d40524c88938991226baa38ac7c4" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "uncased" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.6", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "uuid" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +dependencies = [ + "getrandom", + "serde", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "visibility" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "wagyu-zcash-parameters" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c904628658374e651288f000934c33ef738b2d8b3e65d4100b70b395dbe2bb" +dependencies = [ + "wagyu-zcash-parameters-1", + "wagyu-zcash-parameters-2", + "wagyu-zcash-parameters-3", + "wagyu-zcash-parameters-4", + "wagyu-zcash-parameters-5", + "wagyu-zcash-parameters-6", +] + +[[package]] +name = "wagyu-zcash-parameters-1" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bf2e21bb027d3f8428c60d6a720b54a08bf6ce4e6f834ef8e0d38bb5695da8" + +[[package]] +name = "wagyu-zcash-parameters-2" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a616ab2e51e74cc48995d476e94de810fb16fc73815f390bf2941b046cc9ba2c" + +[[package]] +name = "wagyu-zcash-parameters-3" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14da1e2e958ff93c0830ee68e91884069253bf3462a67831b02b367be75d6147" + +[[package]] +name = "wagyu-zcash-parameters-4" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f058aeef03a2070e8666ffb5d1057d8bb10313b204a254a6e6103eb958e9a6d6" + +[[package]] +name = "wagyu-zcash-parameters-5" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ffe916b30e608c032ae1b734f02574a3e12ec19ab5cc5562208d679efe4969d" + +[[package]] +name = "wagyu-zcash-parameters-6" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7b6d5a78adc3e8f198e9cd730f219a695431467f7ec29dcfc63ade885feebe1" + +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasix" +version = "0.12.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1fbb4ef9bbca0c1170e0b00dd28abc9e3b68669821600cad1caaed606583c6d" +dependencies = [ + "wasi", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.63", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "weak-table" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "323f4da9523e9a669e1eaf9c6e763892769b1d38c623913647bfdc1532fe4549" + +[[package]] +name = "web-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "webpki-roots" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "which" +version = "6.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" +dependencies = [ + "either", + "home", + "rustix", + "winsafe", +] + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +dependencies = [ + "windows-sys 0.52.0", +] + +[[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-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core", + "serde", + "zeroize", +] + +[[package]] +name = "x509-signature" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb2bc2a902d992cd5f471ee3ab0ffd6603047a4207384562755b9d6de977518" +dependencies = [ + "ring 0.16.20", + "untrusted 0.7.1", +] + +[[package]] +name = "xdg" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" + +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + +[[package]] +name = "zcash" +version = "0.1.0" +dependencies = [ + "document-features", + "zcash_primitives", +] + +[[package]] +name = "zcash_address" +version = "0.6.2" +dependencies = [ + "assert_matches", + "bech32", + "bs58", + "core2", + "f4jumble", + "proptest", + "zcash_encoding", + "zcash_protocol", +] + +[[package]] +name = "zcash_client_backend" +version = "0.16.0" +dependencies = [ + "ambassador", + "arti-client", + "assert_matches", + "async-trait", + "base64", + "bech32", + "bip32", + "bls12_381", + "bs58", + "byteorder", + "crossbeam-channel", + "document-features", + "dynosaur", + "futures-util", + "group", + "gumdrop", + "hex", + "http-body-util", + "hyper", + "hyper-util", + "incrementalmerkletree", + "jubjub", + "memuse", + "nom", + "nonempty", + "orchard", + "pasta_curves", + "pczt", + "percent-encoding", + "postcard", + "proptest", + "prost", + "rand", + "rand_chacha", + "rand_core", + "rayon", + "rust_decimal", + "sapling-crypto", + "secrecy", + "serde", + "serde_json", + "shardtree", + "subtle", + "time", + "tokio", + "tokio-rustls 0.24.0", + "tonic", + "tonic-build", + "tor-rtcompat", + "tower", + "tracing", + "trait-variant", + "webpki-roots 0.25.4", + "which", + "zcash_address", + "zcash_encoding", + "zcash_keys", + "zcash_note_encryption", + "zcash_primitives", + "zcash_proofs", + "zcash_protocol", + "zcash_transparent", + "zip32", + "zip321", +] + +[[package]] +name = "zcash_client_sqlite" +version = "0.14.0" +dependencies = [ + "ambassador", + "assert_matches", + "bip32", + "bls12_381", + "bs58", + "byteorder", + "document-features", + "group", + "incrementalmerkletree", + "incrementalmerkletree-testing", + "jubjub", + "maybe-rayon", + "nonempty", + "orchard", + "pasta_curves", + "proptest", + "prost", + "rand_chacha", + "rand_core", + "regex", + "rusqlite", + "sapling-crypto", + "schemerz", + "schemerz-rusqlite", + "secrecy", + "serde", + "shardtree", + "static_assertions", + "subtle", + "tempfile", + "time", + "tracing", + "uuid", + "zcash_address", + "zcash_client_backend", + "zcash_encoding", + "zcash_keys", + "zcash_note_encryption", + "zcash_primitives", + "zcash_proofs", + "zcash_protocol", + "zcash_transparent", + "zip32", + "zip321", +] + +[[package]] +name = "zcash_encoding" +version = "0.2.2" +dependencies = [ + "core2", + "nonempty", +] + +[[package]] +name = "zcash_extensions" +version = "0.1.0" +dependencies = [ + "blake2b_simd", + "ff", + "jubjub", + "orchard", + "rand_core", + "sapling-crypto", + "zcash_address", + "zcash_primitives", + "zcash_proofs", + "zcash_protocol", + "zcash_transparent", +] + +[[package]] +name = "zcash_history" +version = "0.4.0" +dependencies = [ + "assert_matches", + "blake2b_simd", + "byteorder", + "primitive-types", + "proptest", +] + +[[package]] +name = "zcash_keys" +version = "0.6.0" +dependencies = [ + "bech32", + "bip32", + "blake2b_simd", + "bls12_381", + "bs58", + "byteorder", + "core2", + "document-features", + "group", + "hex", + "jubjub", + "memuse", + "nonempty", + "orchard", + "proptest", + "rand_core", + "sapling-crypto", + "secrecy", + "subtle", + "tracing", + "zcash_address", + "zcash_encoding", + "zcash_protocol", + "zcash_transparent", + "zip32", +] + +[[package]] +name = "zcash_note_encryption" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77efec759c3798b6e4d829fcc762070d9b229b0f13338c40bf993b7b609c2272" +dependencies = [ + "chacha20", + "chacha20poly1305", + "cipher", + "rand_core", + "subtle", +] + +[[package]] +name = "zcash_primitives" +version = "0.21.0" +dependencies = [ + "assert_matches", + "bip32", + "blake2b_simd", + "bs58", + "chacha20poly1305", + "core2", + "criterion", + "document-features", + "equihash", + "ff", + "fpe", + "getset", + "group", + "hex", + "incrementalmerkletree", + "jubjub", + "memuse", + "nonempty", + "orchard", + "pprof", + "proptest", + "rand", + "rand_core", + "rand_xorshift", + "redjubjub", + "ripemd 0.1.3", + "sapling-crypto", + "secp256k1", + "sha2 0.10.8", + "subtle", + "tracing", + "zcash_address", + "zcash_encoding", + "zcash_note_encryption", + "zcash_protocol", + "zcash_spec", + "zcash_transparent", + "zip32", +] + +[[package]] +name = "zcash_proofs" +version = "0.21.0" +dependencies = [ + "bellman", + "blake2b_simd", + "bls12_381", + "byteorder", + "document-features", + "group", + "home", + "jubjub", + "known-folders", + "lazy_static", + "minreq", + "rand_core", + "redjubjub", + "sapling-crypto", + "tracing", + "wagyu-zcash-parameters", + "xdg", + "zcash_primitives", +] + +[[package]] +name = "zcash_protocol" +version = "0.4.3" +dependencies = [ + "core2", + "document-features", + "hex", + "incrementalmerkletree", + "incrementalmerkletree-testing", + "memuse", + "proptest", +] + +[[package]] +name = "zcash_spec" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cede95491c2191d3e278cab76e097a44b17fde8d6ca0d4e3a22cf4807b2d857" +dependencies = [ + "blake2b_simd", +] + +[[package]] +name = "zcash_transparent" +version = "0.1.0" +dependencies = [ + "bip32", + "blake2b_simd", + "bs58", + "core2", + "document-features", + "getset", + "hex", + "proptest", + "ripemd 0.1.3", + "secp256k1", + "sha2 0.10.8", + "subtle", + "zcash_address", + "zcash_encoding", + "zcash_protocol", + "zcash_spec", + "zip32", +] + +[[package]] +name = "zerocopy" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + +[[package]] +name = "zip32" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9943793abf9060b68e1889012dafbd5523ab5b125c0fcc24802d69182f2ac9" +dependencies = [ + "blake2b_simd", + "memuse", + "subtle", + "zcash_spec", +] + +[[package]] +name = "zip321" +version = "0.2.0" +dependencies = [ + "base64", + "nom", + "percent-encoding", + "proptest", + "zcash_address", + "zcash_protocol", +] + +[[package]] +name = "zstd" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.10+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 846f8c7a7b..270af38626 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,16 +1,199 @@ [workspace] members = [ "components/equihash", - "components/zcash_note_encryption", + "components/f4jumble", + "components/zcash_address", + "components/zcash_encoding", + "components/zcash_protocol", + "components/zip321", + "pczt", + "zcash", "zcash_client_backend", "zcash_client_sqlite", "zcash_extensions", "zcash_history", + "zcash_keys", "zcash_primitives", "zcash_proofs", + "zcash_transparent", ] +[workspace.package] +edition = "2021" +rust-version = "1.81" +repository = "https://github.com/zcash/librustzcash" +license = "MIT OR Apache-2.0" +categories = ["cryptography::cryptocurrencies"] + +# Common dependencies across all of our crates. Dependencies used only by a single crate +# (and that don't have cross-crate versioning needs) are specified by the crate itself. +# +# See the individual crate `Cargo.toml` files for information about which dependencies are +# part of a public API, and which can be updated without a SemVer bump. +[workspace.dependencies] +# Intra-workspace dependencies +equihash = { version = "0.2", path = "components/equihash", default-features = false } +zcash_address = { version = "0.6", path = "components/zcash_address", default-features = false } +zcash_client_backend = { version = "0.16", path = "zcash_client_backend" } +zcash_encoding = { version = "0.2.1", path = "components/zcash_encoding", default-features = false } +zcash_keys = { version = "0.6", path = "zcash_keys" } +zcash_protocol = { version = "0.4.1", path = "components/zcash_protocol", default-features = false } +zip321 = { version = "0.2", path = "components/zip321" } + +zcash_note_encryption = "0.4.1" +zcash_primitives = { version = "0.21", path = "zcash_primitives", default-features = false } +zcash_proofs = { version = "0.21", path = "zcash_proofs", default-features = false } + +pczt = { version = "0.1", path = "pczt" } + +# Shielded protocols +bellman = { version = "0.14", default-features = false, features = ["groth16"] } +ff = { version = "0.13", default-features = false } +group = "0.13" +incrementalmerkletree = { version = "0.8.2", default-features = false } +shardtree = "0.6.1" +zcash_spec = "0.1" + +# Payment protocols +# - Sapling +bitvec = { version = "1", default-features = false, features = ["alloc"] } +blake2s_simd = { version = "1", default-features = false } +bls12_381 = "0.8" +jubjub = "0.10" +redjubjub = { version = "0.7", default-features = false } +sapling = { package = "sapling-crypto", version = "0.4", default-features = false } + +# - Orchard +orchard = { version = "0.10.1", default-features = false } +pasta_curves = "0.5" + +# - Transparent +bip32 = { version = "=0.6.0-pre.1", default-features = false } +ripemd = { version = "0.1", default-features = false } +secp256k1 = { version = "0.29", default-features = false, features = ["alloc"] } +transparent = { package = "zcash_transparent", version = "0.1", path = "zcash_transparent", default-features = false } + +# Boilerplate & missing stdlib +getset = "0.1" +nonempty = { version = "0.11", default-features = false } + +# CSPRNG +rand = { version = "0.8", default-features = false } +rand_core = { version = "0.6", default-features = false } + +# Currency conversions +rust_decimal = { version = "1.35", default-features = false, features = ["serde"] } + +# Digests +blake2b_simd = { version = "1", default-features = false } +sha2 = { version = "0.10", default-features = false } + +# Documentation +document-features = "0.2" + +# Encodings +base64 = "0.22" +bech32 = { version = "0.11", default-features = false, features = ["alloc"] } +bs58 = { version = "0.5", default-features = false, features = ["alloc", "check"] } +byteorder = "1" +hex = { version = "0.4", default-features = false, features = ["alloc"] } +percent-encoding = "2.1.0" +postcard = { version = "1", features = ["alloc"] } +serde = { version = "1", default-features = false, features = ["derive"] } +serde_json = "1" + +# HTTP +hyper = "1" +http-body-util = "0.1" +hyper-util = { version = "0.1.1", features = ["tokio"] } +tokio-rustls = "0.24" +webpki-roots = "0.25" + +# Logging and metrics +memuse = { version = "0.2.2", default-features = false } +tracing = { version = "0.1", default-features = false } + +# No-std support +core2 = { version = "0.3", default-features = false, features = ["alloc"] } + +# Parallel processing +crossbeam-channel = "0.5" +maybe-rayon = { version = "0.1.0", default-features = false } +rayon = "1.5" + +# Protobuf and gRPC +prost = "0.13" +tonic = { version = "0.12", default-features = false } +tonic-build = { version = "0.12.3", default-features = false } + +# Secret management +secrecy = "0.8" +subtle = { version = "2.2.3", default-features = false } + +# SQLite databases +# - Warning: One of the downstream consumers requires that SQLite be available through +# CocoaPods, due to being bound to React Native. We need to ensure that the SQLite +# version required for `rusqlite` is a version that is available through CocoaPods. +rusqlite = { version = "0.32", features = ["bundled"] } +schemerz = "0.2" +schemerz-rusqlite = "0.320" +time = "0.3.22" +uuid = "1.1" + +# Static constants and assertions +lazy_static = "1" +static_assertions = "1" + +# Tests and benchmarks +ambassador = "0.4" +assert_matches = "1.5" +criterion = "0.5" +proptest = "1" +rand_chacha = "0.3" +rand_xorshift = "0.3" +incrementalmerkletree-testing = "0.3" + +# Tor +# - `arti-client` depends on `rusqlite`, and a version mismatch there causes a compilation +# failure due to incompatible `libsqlite3-sys` versions. +arti-client = { version = "0.23", default-features = false, features = ["compression", "rustls", "tokio"] } +dynosaur = "0.1.1" +tokio = "1" +tor-rtcompat = "0.23" +tower = "0.4" +trait-variant = "0.1" + +# ZIP 32 +aes = "0.8" +fpe = { version = "0.6", default-features = false, features = ["alloc"] } +zip32 = { version = "0.1.1", default-features = false } + [profile.release] lto = true panic = 'abort' codegen-units = 1 + +[profile.test] +# Since we have many computationally expensive tests, this changes the test profile to +# compile with optimizations by default, but keep full debug info. +# +# This differs from the release profile in the following ways: +# - it does not set `lto = true`, which increases compile times without substantially +# speeding up tests; +# - it does not set `codegen-units = 1`, which increases compile times and is only +# useful to improve determinism of release builds; +# - it does not set `panic = 'abort'`, which is in any case ignored for tests. +# +# To get results as close as possible to a release build, use `cargo test --release`. +# To speed up compilation and avoid optimizations potentially resulting in lower-quality +# debug info, use `cargo test --profile=dev`. +opt-level = 3 +debug = true + +[workspace.lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(zcash_unstable, values("zfuture"))'] } + +[patch.crates-io] +orchard = { git = "https://github.com/zcash/orchard.git", rev = "b1c22c07300db22239235d16dab096e23369948f" } +redjubjub = { git = "https://github.com/ZcashFoundation/redjubjub", rev = "eae848c5c14d9c795d000dd9f4c4762d1aee7ee1" } +sapling = { package = "sapling-crypto", git = "https://github.com/zcash/sapling-crypto.git", rev = "6ca338532912adcd82369220faeea31aab4720c5" } diff --git a/LICENSE-MIT b/LICENSE-MIT index 1581c90d16..17173028f5 100644 --- a/LICENSE-MIT +++ b/LICENSE-MIT @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2017-2019 Electric Coin Company +Copyright (c) 2017-2021 The Electric Coin Company Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 3df799fe0e..696e4caddd 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # Zcash Rust crates -This repository contains a (work-in-progress) set of Rust crates for -working with Zcash. +This repository contains a (work-in-progress) set of Rust crates for working +with Zcash. ## Security Warnings -These libraries are currently under development and have not been fully-reviewed. +These libraries are under development and have not been fully reviewed. ## License @@ -18,7 +18,6 @@ at your option. ### Contribution -Unless you explicitly state otherwise, any contribution intentionally -submitted for inclusion in the work by you, as defined in the Apache-2.0 -license, shall be dual licensed as above, without any additional terms or -conditions. +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall +be dual licensed as above, without any additional terms or conditions. diff --git a/components/equihash/CHANGELOG.md b/components/equihash/CHANGELOG.md index a0585d64df..7bad997dd4 100644 --- a/components/equihash/CHANGELOG.md +++ b/components/equihash/CHANGELOG.md @@ -7,5 +7,10 @@ and this library adheres to Rust's notion of ## [Unreleased] +## [0.2.0] - 2022-06-24 +### Changed +- MSRV is now 1.56.1. +- Bumped dependencies to `blake2b_simd 1`. + ## [0.1.0] - 2020-07-10 Initial release. diff --git a/components/equihash/Cargo.toml b/components/equihash/Cargo.toml index 86bacb9e37..b7de2df128 100644 --- a/components/equihash/Cargo.toml +++ b/components/equihash/Cargo.toml @@ -1,13 +1,24 @@ [package] name = "equihash" description = "The Equihash Proof-of-Work function" -version = "0.1.0" +version = "0.2.0" authors = ["Jack Grigg "] homepage = "https://github.com/zcash/librustzcash" repository = "https://github.com/zcash/librustzcash" license = "MIT OR Apache-2.0" -edition = "2018" +edition = "2021" +rust-version = "1.56.1" [dependencies] -blake2b_simd = "0.5" -byteorder = "1" +core2.workspace = true +blake2b_simd.workspace = true + +[lib] +bench = false + +[lints] +workspace = true + +[features] +default = ["std"] +std = [] diff --git a/components/equihash/src/lib.rs b/components/equihash/src/lib.rs index aa275c61ca..c65fc2d7d1 100644 --- a/components/equihash/src/lib.rs +++ b/components/equihash/src/lib.rs @@ -18,8 +18,17 @@ //! [BK16]: https://www.internetsociety.org/sites/default/files/blogs-media/equihash-asymmetric-proof-of-work-based-generalized-birthday-problem.pdf // Catch documentation errors caused by code changes. -#![deny(broken_intra_doc_links)] +#![deny(rustdoc::broken_intra_doc_links)] +#![no_std] +#[cfg(feature = "std")] +extern crate std; + +#[macro_use] +extern crate alloc; + +mod minimal; +mod params; mod verify; #[cfg(test)] diff --git a/components/equihash/src/minimal.rs b/components/equihash/src/minimal.rs new file mode 100644 index 0000000000..4edc717d6f --- /dev/null +++ b/components/equihash/src/minimal.rs @@ -0,0 +1,195 @@ +use alloc::vec::Vec; +use core::mem::size_of; +use core2::io::{Cursor, Read}; + +use crate::params::Params; + +pub(crate) fn expand_array(vin: &[u8], bit_len: usize, byte_pad: usize) -> Vec { + assert!(bit_len >= 8); + assert!(u32::BITS as usize >= 7 + bit_len); + + let out_width = (bit_len + 7) / 8 + byte_pad; + let out_len = 8 * out_width * vin.len() / bit_len; + + // Shortcut for parameters where expansion is a no-op + if out_len == vin.len() { + return vin.to_vec(); + } + + let mut vout: Vec = vec![0; out_len]; + let bit_len_mask: u32 = (1 << bit_len) - 1; + + // The acc_bits least-significant bits of acc_value represent a bit sequence + // in big-endian order. + let mut acc_bits = 0; + let mut acc_value: u32 = 0; + + let mut j = 0; + for b in vin { + acc_value = (acc_value << 8) | u32::from(*b); + acc_bits += 8; + + // When we have bit_len or more bits in the accumulator, write the next + // output element. + if acc_bits >= bit_len { + acc_bits -= bit_len; + for x in byte_pad..out_width { + vout[j + x] = (( + // Big-endian + acc_value >> (acc_bits + (8 * (out_width - x - 1))) + ) & ( + // Apply bit_len_mask across byte boundaries + (bit_len_mask >> (8 * (out_width - x - 1))) & 0xFF + )) as u8; + } + j += out_width; + } + } + + vout +} + +fn read_u32_be(csr: &mut Cursor>) -> core2::io::Result { + let mut n = [0; 4]; + csr.read_exact(&mut n)?; + Ok(u32::from_be_bytes(n)) +} + +/// Returns `None` if the parameters are invalid for this minimal encoding. +pub(crate) fn indices_from_minimal(p: Params, minimal: &[u8]) -> Option> { + let c_bit_len = p.collision_bit_length(); + // Division is exact because k >= 3. + if minimal.len() != ((1 << p.k) * (c_bit_len + 1)) / 8 { + return None; + } + + assert!(((c_bit_len + 1) + 7) / 8 <= size_of::()); + let len_indices = u32::BITS as usize * minimal.len() / (c_bit_len + 1); + let byte_pad = size_of::() - ((c_bit_len + 1) + 7) / 8; + + let mut csr = Cursor::new(expand_array(minimal, c_bit_len + 1, byte_pad)); + let mut ret = Vec::with_capacity(len_indices); + + // Big-endian so that lexicographic array comparison is equivalent to integer + // comparison + while let Ok(i) = read_u32_be(&mut csr) { + ret.push(i); + } + + Some(ret) +} + +#[cfg(test)] +mod tests { + use super::{expand_array, indices_from_minimal, Params}; + + #[test] + fn array_expansion() { + let check_array = |(bit_len, byte_pad), compact, expanded| { + assert_eq!(expand_array(compact, bit_len, byte_pad), expanded); + }; + + // 8 11-bit chunks, all-ones + check_array( + (11, 0), + &[ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + ], + &[ + 0x07, 0xff, 0x07, 0xff, 0x07, 0xff, 0x07, 0xff, 0x07, 0xff, 0x07, 0xff, 0x07, 0xff, + 0x07, 0xff, + ][..], + ); + // 8 21-bit chunks, alternating 1s and 0s + check_array( + (21, 0), + &[ + 0xaa, 0xaa, 0xad, 0x55, 0x55, 0x6a, 0xaa, 0xab, 0x55, 0x55, 0x5a, 0xaa, 0xaa, 0xd5, + 0x55, 0x56, 0xaa, 0xaa, 0xb5, 0x55, 0x55, + ], + &[ + 0x15, 0x55, 0x55, 0x15, 0x55, 0x55, 0x15, 0x55, 0x55, 0x15, 0x55, 0x55, 0x15, 0x55, + 0x55, 0x15, 0x55, 0x55, 0x15, 0x55, 0x55, 0x15, 0x55, 0x55, + ][..], + ); + // 8 21-bit chunks, based on example in the spec + check_array( + (21, 0), + &[ + 0x00, 0x02, 0x20, 0x00, 0x0a, 0x7f, 0xff, 0xfe, 0x00, 0x12, 0x30, 0x22, 0xb3, 0x82, + 0x26, 0xac, 0x19, 0xbd, 0xf2, 0x34, 0x56, + ], + &[ + 0x00, 0x00, 0x44, 0x00, 0x00, 0x29, 0x1f, 0xff, 0xff, 0x00, 0x01, 0x23, 0x00, 0x45, + 0x67, 0x00, 0x89, 0xab, 0x00, 0xcd, 0xef, 0x12, 0x34, 0x56, + ][..], + ); + // 16 14-bit chunks, alternating 11s and 00s + check_array( + (14, 0), + &[ + 0xcc, 0xcf, 0x33, 0x3c, 0xcc, 0xf3, 0x33, 0xcc, 0xcf, 0x33, 0x3c, 0xcc, 0xf3, 0x33, + 0xcc, 0xcf, 0x33, 0x3c, 0xcc, 0xf3, 0x33, 0xcc, 0xcf, 0x33, 0x3c, 0xcc, 0xf3, 0x33, + ], + &[ + 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, + 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, + 0x33, 0x33, 0x33, 0x33, + ][..], + ); + // 8 11-bit chunks, all-ones, 2-byte padding + check_array( + (11, 2), + &[ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + ], + &[ + 0x00, 0x00, 0x07, 0xff, 0x00, 0x00, 0x07, 0xff, 0x00, 0x00, 0x07, 0xff, 0x00, 0x00, + 0x07, 0xff, 0x00, 0x00, 0x07, 0xff, 0x00, 0x00, 0x07, 0xff, 0x00, 0x00, 0x07, 0xff, + 0x00, 0x00, 0x07, 0xff, + ][..], + ); + } + + #[test] + fn minimal_solution_repr() { + let check_repr = |minimal, indices| { + assert_eq!( + indices_from_minimal(Params { n: 80, k: 3 }, minimal).unwrap(), + indices, + ); + }; + + // The solutions here are not intended to be valid. + check_repr( + &[ + 0x00, 0x00, 0x08, 0x00, 0x00, 0x40, 0x00, 0x02, 0x00, 0x00, 0x10, 0x00, 0x00, 0x80, + 0x00, 0x04, 0x00, 0x00, 0x20, 0x00, 0x01, + ], + &[1, 1, 1, 1, 1, 1, 1, 1], + ); + check_repr( + &[ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + ], + &[ + 2097151, 2097151, 2097151, 2097151, 2097151, 2097151, 2097151, 2097151, + ], + ); + check_repr( + &[ + 0x0f, 0xff, 0xf8, 0x00, 0x20, 0x03, 0xff, 0xfe, 0x00, 0x08, 0x00, 0xff, 0xff, 0x80, + 0x02, 0x00, 0x3f, 0xff, 0xe0, 0x00, 0x80, + ], + &[131071, 128, 131071, 128, 131071, 128, 131071, 128], + ); + check_repr( + &[ + 0x00, 0x02, 0x20, 0x00, 0x0a, 0x7f, 0xff, 0xfe, 0x00, 0x4d, 0x10, 0x01, 0x4c, 0x80, + 0x0f, 0xfc, 0x00, 0x00, 0x2f, 0xff, 0xff, + ], + &[68, 41, 2097151, 1233, 665, 1023, 1, 1048575], + ); + } +} diff --git a/components/equihash/src/params.rs b/components/equihash/src/params.rs new file mode 100644 index 0000000000..2e17700065 --- /dev/null +++ b/components/equihash/src/params.rs @@ -0,0 +1,37 @@ +#[derive(Clone, Copy)] +pub(crate) struct Params { + pub(crate) n: u32, + pub(crate) k: u32, +} + +impl Params { + /// Returns `None` if the parameters are invalid. + pub(crate) fn new(n: u32, k: u32) -> Option { + // We place the following requirements on the parameters: + // - n is a multiple of 8, so the hash output has an exact byte length. + // - k >= 3 so the encoded solutions have an exact byte length. + // - k < n, so the collision bit length is at least 1. + // - n is a multiple of k + 1, so we have an integer collision bit length. + if (n % 8 == 0) && (k >= 3) && (k < n) && (n % (k + 1) == 0) { + Some(Params { n, k }) + } else { + None + } + } + pub(crate) fn indices_per_hash_output(&self) -> u32 { + 512 / self.n + } + pub(crate) fn hash_output(&self) -> u8 { + (self.indices_per_hash_output() * self.n / 8) as u8 + } + pub(crate) fn collision_bit_length(&self) -> usize { + (self.n / (self.k + 1)) as usize + } + pub(crate) fn collision_byte_length(&self) -> usize { + (self.collision_bit_length() + 7) / 8 + } + #[cfg(test)] + pub(crate) fn hash_length(&self) -> usize { + ((self.k as usize) + 1) * self.collision_byte_length() + } +} diff --git a/components/equihash/src/test_vectors/invalid.rs b/components/equihash/src/test_vectors/invalid.rs index 11da849e0d..5dbec5a33d 100644 --- a/components/equihash/src/test_vectors/invalid.rs +++ b/components/equihash/src/test_vectors/invalid.rs @@ -1,4 +1,4 @@ -use crate::verify::{Kind, Params}; +use crate::{params::Params, verify::Kind}; pub(crate) struct TestVector { pub(crate) params: Params, diff --git a/components/equihash/src/test_vectors/valid.rs b/components/equihash/src/test_vectors/valid.rs index a55de1b96a..4df20c642a 100644 --- a/components/equihash/src/test_vectors/valid.rs +++ b/components/equihash/src/test_vectors/valid.rs @@ -1,4 +1,4 @@ -use crate::verify::Params; +use crate::params::Params; pub(crate) struct TestVector { pub(crate) params: Params, diff --git a/components/equihash/src/verify.rs b/components/equihash/src/verify.rs index f356acdf95..e0eafe4f11 100644 --- a/components/equihash/src/verify.rs +++ b/components/equihash/src/verify.rs @@ -2,17 +2,15 @@ //! //! [Equihash]: https://zips.z.cash/protocol/protocol.pdf#equihash +use alloc::vec::Vec; use blake2b_simd::{Hash as Blake2bHash, Params as Blake2bParams, State as Blake2bState}; -use byteorder::{BigEndian, LittleEndian, ReadBytesExt, WriteBytesExt}; -use std::fmt; -use std::io::Cursor; -use std::mem::size_of; - -#[derive(Clone, Copy)] -pub(crate) struct Params { - pub(crate) n: u32, - pub(crate) k: u32, -} +use core::fmt; +use core2::io::Write; + +use crate::{ + minimal::{expand_array, indices_from_minimal}, + params::Params, +}; #[derive(Clone)] struct Node { @@ -20,37 +18,6 @@ struct Node { indices: Vec, } -impl Params { - fn new(n: u32, k: u32) -> Result { - // We place the following requirements on the parameters: - // - n is a multiple of 8, so the hash output has an exact byte length. - // - k >= 3 so the encoded solutions have an exact byte length. - // - k < n, so the collision bit length is at least 1. - // - n is a multiple of k + 1, so we have an integer collision bit length. - if (n % 8 == 0) && (k >= 3) && (k < n) && (n % (k + 1) == 0) { - Ok(Params { n, k }) - } else { - Err(Error(Kind::InvalidParams)) - } - } - fn indices_per_hash_output(&self) -> u32 { - 512 / self.n - } - fn hash_output(&self) -> u8 { - (self.indices_per_hash_output() * self.n / 8) as u8 - } - fn collision_bit_length(&self) -> usize { - (self.n / (self.k + 1)) as usize - } - fn collision_byte_length(&self) -> usize { - (self.collision_bit_length() + 7) / 8 - } - #[cfg(test)] - fn hash_length(&self) -> usize { - ((self.k as usize) + 1) * self.collision_byte_length() - } -} - impl Node { fn new(p: &Params, state: &Blake2bState, i: u32) -> Self { let hash = generate_hash(state, i / p.indices_per_hash_output()); @@ -125,6 +92,7 @@ impl fmt::Display for Error { } } +#[cfg(feature = "std")] impl std::error::Error for Error {} #[derive(Debug, PartialEq)] @@ -150,8 +118,8 @@ impl fmt::Display for Kind { fn initialise_state(n: u32, k: u32, digest_len: u8) -> Blake2bState { let mut personalization: Vec = Vec::from("ZcashPoW"); - personalization.write_u32::(n).unwrap(); - personalization.write_u32::(k).unwrap(); + personalization.write_all(&n.to_le_bytes()).unwrap(); + personalization.write_all(&k.to_le_bytes()).unwrap(); Blake2bParams::new() .hash_length(digest_len as usize) @@ -161,81 +129,13 @@ fn initialise_state(n: u32, k: u32, digest_len: u8) -> Blake2bState { fn generate_hash(base_state: &Blake2bState, i: u32) -> Blake2bHash { let mut lei = [0u8; 4]; - (&mut lei[..]).write_u32::(i).unwrap(); + (&mut lei[..]).write_all(&i.to_le_bytes()).unwrap(); let mut state = base_state.clone(); state.update(&lei); state.finalize() } -fn expand_array(vin: &[u8], bit_len: usize, byte_pad: usize) -> Vec { - assert!(bit_len >= 8); - assert!(8 * size_of::() >= 7 + bit_len); - - let out_width = (bit_len + 7) / 8 + byte_pad; - let out_len = 8 * out_width * vin.len() / bit_len; - - // Shortcut for parameters where expansion is a no-op - if out_len == vin.len() { - return vin.to_vec(); - } - - let mut vout: Vec = vec![0; out_len]; - let bit_len_mask: u32 = (1 << bit_len) - 1; - - // The acc_bits least-significant bits of acc_value represent a bit sequence - // in big-endian order. - let mut acc_bits = 0; - let mut acc_value: u32 = 0; - - let mut j = 0; - for b in vin { - acc_value = (acc_value << 8) | u32::from(*b); - acc_bits += 8; - - // When we have bit_len or more bits in the accumulator, write the next - // output element. - if acc_bits >= bit_len { - acc_bits -= bit_len; - for x in byte_pad..out_width { - vout[j + x] = (( - // Big-endian - acc_value >> (acc_bits + (8 * (out_width - x - 1))) - ) & ( - // Apply bit_len_mask across byte boundaries - (bit_len_mask >> (8 * (out_width - x - 1))) & 0xFF - )) as u8; - } - j += out_width; - } - } - - vout -} - -fn indices_from_minimal(p: Params, minimal: &[u8]) -> Result, Error> { - let c_bit_len = p.collision_bit_length(); - // Division is exact because k >= 3. - if minimal.len() != ((1 << p.k) * (c_bit_len + 1)) / 8 { - return Err(Error(Kind::InvalidParams)); - } - - assert!(((c_bit_len + 1) + 7) / 8 <= size_of::()); - let len_indices = 8 * size_of::() * minimal.len() / (c_bit_len + 1); - let byte_pad = size_of::() - ((c_bit_len + 1) + 7) / 8; - - let mut csr = Cursor::new(expand_array(minimal, c_bit_len + 1, byte_pad)); - let mut ret = Vec::with_capacity(len_indices); - - // Big-endian so that lexicographic array comparison is equivalent to integer - // comparison - while let Ok(i) = csr.read_u32::() { - ret.push(i); - } - - Ok(ret) -} - fn has_collision(a: &Node, b: &Node, len: usize) -> bool { a.hash .iter() @@ -314,7 +214,7 @@ fn tree_validator(p: &Params, state: &Blake2bState, indices: &[u32]) -> Result Result<(), Error> { - let p = Params::new(n, k)?; - let indices = indices_from_minimal(p, soln)?; + let p = Params::new(n, k).ok_or(Error(Kind::InvalidParams))?; + let indices = indices_from_minimal(p, soln).ok_or(Error(Kind::InvalidParams))?; // Recursive validation is faster is_valid_solution_recursive(p, input, nonce, &indices) @@ -356,122 +256,9 @@ pub fn is_valid_solution( #[cfg(test)] mod tests { - use super::{ - expand_array, indices_from_minimal, is_valid_solution, is_valid_solution_iterative, - is_valid_solution_recursive, Params, - }; + use super::{is_valid_solution, is_valid_solution_iterative, is_valid_solution_recursive}; use crate::test_vectors::{INVALID_TEST_VECTORS, VALID_TEST_VECTORS}; - #[test] - fn array_expansion() { - let check_array = |(bit_len, byte_pad), compact, expanded| { - assert_eq!(expand_array(compact, bit_len, byte_pad), expanded); - }; - - // 8 11-bit chunks, all-ones - check_array( - (11, 0), - &[ - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - ], - &[ - 0x07, 0xff, 0x07, 0xff, 0x07, 0xff, 0x07, 0xff, 0x07, 0xff, 0x07, 0xff, 0x07, 0xff, - 0x07, 0xff, - ][..], - ); - // 8 21-bit chunks, alternating 1s and 0s - check_array( - (21, 0), - &[ - 0xaa, 0xaa, 0xad, 0x55, 0x55, 0x6a, 0xaa, 0xab, 0x55, 0x55, 0x5a, 0xaa, 0xaa, 0xd5, - 0x55, 0x56, 0xaa, 0xaa, 0xb5, 0x55, 0x55, - ], - &[ - 0x15, 0x55, 0x55, 0x15, 0x55, 0x55, 0x15, 0x55, 0x55, 0x15, 0x55, 0x55, 0x15, 0x55, - 0x55, 0x15, 0x55, 0x55, 0x15, 0x55, 0x55, 0x15, 0x55, 0x55, - ][..], - ); - // 8 21-bit chunks, based on example in the spec - check_array( - (21, 0), - &[ - 0x00, 0x02, 0x20, 0x00, 0x0a, 0x7f, 0xff, 0xfe, 0x00, 0x12, 0x30, 0x22, 0xb3, 0x82, - 0x26, 0xac, 0x19, 0xbd, 0xf2, 0x34, 0x56, - ], - &[ - 0x00, 0x00, 0x44, 0x00, 0x00, 0x29, 0x1f, 0xff, 0xff, 0x00, 0x01, 0x23, 0x00, 0x45, - 0x67, 0x00, 0x89, 0xab, 0x00, 0xcd, 0xef, 0x12, 0x34, 0x56, - ][..], - ); - // 16 14-bit chunks, alternating 11s and 00s - check_array( - (14, 0), - &[ - 0xcc, 0xcf, 0x33, 0x3c, 0xcc, 0xf3, 0x33, 0xcc, 0xcf, 0x33, 0x3c, 0xcc, 0xf3, 0x33, - 0xcc, 0xcf, 0x33, 0x3c, 0xcc, 0xf3, 0x33, 0xcc, 0xcf, 0x33, 0x3c, 0xcc, 0xf3, 0x33, - ], - &[ - 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, - 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, - 0x33, 0x33, 0x33, 0x33, - ][..], - ); - // 8 11-bit chunks, all-ones, 2-byte padding - check_array( - (11, 2), - &[ - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - ], - &[ - 0x00, 0x00, 0x07, 0xff, 0x00, 0x00, 0x07, 0xff, 0x00, 0x00, 0x07, 0xff, 0x00, 0x00, - 0x07, 0xff, 0x00, 0x00, 0x07, 0xff, 0x00, 0x00, 0x07, 0xff, 0x00, 0x00, 0x07, 0xff, - 0x00, 0x00, 0x07, 0xff, - ][..], - ); - } - - #[test] - fn minimal_solution_repr() { - let check_repr = |minimal, indices| { - assert_eq!( - indices_from_minimal(Params { n: 80, k: 3 }, minimal).unwrap(), - indices, - ); - }; - - // The solutions here are not intended to be valid. - check_repr( - &[ - 0x00, 0x00, 0x08, 0x00, 0x00, 0x40, 0x00, 0x02, 0x00, 0x00, 0x10, 0x00, 0x00, 0x80, - 0x00, 0x04, 0x00, 0x00, 0x20, 0x00, 0x01, - ], - &[1, 1, 1, 1, 1, 1, 1, 1], - ); - check_repr( - &[ - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - ], - &[ - 2097151, 2097151, 2097151, 2097151, 2097151, 2097151, 2097151, 2097151, - ], - ); - check_repr( - &[ - 0x0f, 0xff, 0xf8, 0x00, 0x20, 0x03, 0xff, 0xfe, 0x00, 0x08, 0x00, 0xff, 0xff, 0x80, - 0x02, 0x00, 0x3f, 0xff, 0xe0, 0x00, 0x80, - ], - &[131071, 128, 131071, 128, 131071, 128, 131071, 128], - ); - check_repr( - &[ - 0x00, 0x02, 0x20, 0x00, 0x0a, 0x7f, 0xff, 0xfe, 0x00, 0x4d, 0x10, 0x01, 0x4c, 0x80, - 0x0f, 0xfc, 0x00, 0x00, 0x2f, 0xff, 0xff, - ], - &[68, 41, 2097151, 1233, 665, 1023, 1, 1048575], - ); - } - #[test] fn valid_test_vectors() { for tv in VALID_TEST_VECTORS { @@ -486,13 +273,13 @@ mod tests { fn invalid_test_vectors() { for tv in INVALID_TEST_VECTORS { assert_eq!( - is_valid_solution_iterative(tv.params, tv.input, &tv.nonce, &tv.solution) + is_valid_solution_iterative(tv.params, tv.input, &tv.nonce, tv.solution) .unwrap_err() .0, tv.error ); assert_eq!( - is_valid_solution_recursive(tv.params, tv.input, &tv.nonce, &tv.solution) + is_valid_solution_recursive(tv.params, tv.input, &tv.nonce, tv.solution) .unwrap_err() .0, tv.error diff --git a/components/f4jumble/CHANGELOG.md b/components/f4jumble/CHANGELOG.md new file mode 100644 index 0000000000..0d0e46fc84 --- /dev/null +++ b/components/f4jumble/CHANGELOG.md @@ -0,0 +1,17 @@ +# Changelog +All notable changes to this library will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this library adheres to Rust's notion of +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.1.1] - 2024-12-13 +### Added +- `alloc` feature flag as a mid-point between full `no-std` support and the + `std` feature flag. + +## [0.1.0] - 2022-05-11 +Initial release. +MSRV is 1.51 diff --git a/components/f4jumble/Cargo.toml b/components/f4jumble/Cargo.toml new file mode 100644 index 0000000000..ffccee0351 --- /dev/null +++ b/components/f4jumble/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "f4jumble" +description = "Implementation of Zcash's F4Jumble algorithm" +version = "0.1.1" +authors = [ + "Jack Grigg ", + "Kris Nuttycombe ", + "Daira Hopwood ", +] +homepage = "https://github.com/zcash/librustzcash" +repository = "https://github.com/zcash/librustzcash" +readme = "README.md" +license = "MIT OR Apache-2.0" +edition = "2018" +rust-version = "1.51" +categories = ["encoding"] +keywords = ["feistel"] + +[package.metadata.cargo-udeps.ignore] +development = ["hex"] + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +blake2b_simd = { version = "1", default-features = false } + +[dev-dependencies] +hex = "0.4" +proptest = "1" + +[features] +default = ["std"] +alloc = [] +std = [ + "alloc", + "blake2b_simd/std", +] + +[lints] +workspace = true diff --git a/components/f4jumble/LICENSE-APACHE b/components/f4jumble/LICENSE-APACHE new file mode 100644 index 0000000000..1e5006dc14 --- /dev/null +++ b/components/f4jumble/LICENSE-APACHE @@ -0,0 +1,202 @@ + 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 [yyyy] [name of copyright owner] + +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/components/f4jumble/LICENSE-MIT b/components/f4jumble/LICENSE-MIT new file mode 100644 index 0000000000..9500c140cc --- /dev/null +++ b/components/f4jumble/LICENSE-MIT @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2021 Electric Coin Company + +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/components/f4jumble/README.md b/components/f4jumble/README.md new file mode 100644 index 0000000000..ebf3250b4f --- /dev/null +++ b/components/f4jumble/README.md @@ -0,0 +1,24 @@ +# f4jumble + +An implementation of the F4Jumble algorithm, an unkeyed variable-width +permutation. + +F4Jumble is used by Zcash Unified Addresses and Unified Viewing Keys to +prevent certain kinds of malleation attacks, and is specified by [ZIP 316](https://zips.z.cash/zip-0316). + +## License + +Licensed under either of + + * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or + http://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you, as defined in the Apache-2.0 +license, shall be dual licensed as above, without any additional terms or +conditions. diff --git a/components/f4jumble/src/lib.rs b/components/f4jumble/src/lib.rs new file mode 100644 index 0000000000..982e9d64ee --- /dev/null +++ b/components/f4jumble/src/lib.rs @@ -0,0 +1,392 @@ +//! This crate provides a mechanism for "jumbling" byte slices in a reversible way. +//! +//! Many byte encodings such as [Base64] and [Bech32] do not have "cascading" behaviour: +//! changing an input byte at one position has no effect on the encoding of bytes at +//! distant positions. This can be a problem if users generally check the correctness of +//! encoded strings by eye, as they will tend to only check the first and/or last few +//! characters of the encoded string. In some situations (for example, a hardware device +//! displaying on its screen an encoded string provided by an untrusted computer), it is +//! potentially feasible for an adversary to change some internal portion of the encoded +//! string in a way that is beneficial to them, without the user noticing. +//! +//! [Base64]: https://en.wikipedia.org/wiki/Base64 +//! [Bech32]: https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#Bech32 +//! +//! The function F4Jumble (and its inverse function, F4Jumble⁻¹) are length-preserving +//! transformations can be used to trivially introduce cascading behaviour to existing +//! encodings: +//! - Prepare the raw `message` bytes as usual. +//! - Pass `message` through [`f4jumble`] or [`f4jumble_mut`] to obtain the jumbled bytes. +//! - Encode the jumbled bytes with the encoding scheme. +//! +//! Changing any byte of `message` will result in a completely different sequence of +//! jumbled bytes. Specifically, F4Jumble uses an unkeyed 4-round Feistel construction to +//! approximate a random permutation. +//! +//! ![Diagram of 4-round unkeyed Feistel construction](https://zips.z.cash/zip-0316-f4.png) +//! +//! ## Efficiency +//! +//! The cost is dominated by 4 BLAKE2b compressions for message lengths up to 128 bytes. +//! For longer messages, the cost increases to 6 BLAKE2b compressions for 128 < lₘ ≤ 192, +//! and 10 BLAKE2b compressions for 192 < lₘ ≤ 256, for example. The maximum cost for +//! which the algorithm is defined would be 196608 BLAKE2b compressions at lₘ = 4194368. +//! +//! The implementations in this crate require memory of roughly lₘ bytes plus the size of +//! a BLAKE2b hash state. It is possible to reduce this by (for example, with F4Jumble⁻¹) +//! streaming the `d` part of the jumbled encoding three times from a less +//! memory-constrained device. It is essential that the streamed value of `d` is the same +//! on each pass, which can be verified using a Message Authentication Code (with key held +//! only by the Consumer) or collision-resistant hash function. After the first pass of +//! `d`, the implementation is able to compute `y`; after the second pass it is able to +//! compute `a`; and the third allows it to compute and incrementally parse `b`. The +//! maximum memory usage during this process would be 128 bytes plus two BLAKE2b hash +//! states. + +#![no_std] +#![cfg_attr(docsrs, feature(doc_cfg))] + +use blake2b_simd::{Params as Blake2bParams, OUTBYTES}; + +use core::cmp::min; +use core::fmt; +use core::ops::RangeInclusive; +use core::result::Result; + +#[cfg(feature = "alloc")] +extern crate alloc; + +#[cfg(feature = "std")] +#[macro_use] +extern crate std; + +#[cfg(feature = "alloc")] +use alloc::vec::Vec; + +#[cfg(test)] +mod test_vectors; +#[cfg(all(test, feature = "std"))] +mod test_vectors_long; + +/// Length of F4Jumbled message must lie in the range VALID_LENGTH. +/// +/// VALID_LENGTH = 48..=4194368 +pub const VALID_LENGTH: RangeInclusive = 48..=4194368; + +/// Errors produced by F4Jumble. +#[derive(Debug)] +pub enum Error { + /// Value error indicating that length of F4Jumbled message does not + /// lie in the range [`VALID_LENGTH`]. + InvalidLength, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidLength => write!( + f, + "Message length must be in interval ({}..={})", + *VALID_LENGTH.start(), + *VALID_LENGTH.end() + ), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for Error {} + +macro_rules! H_PERS { + ( $i:expr ) => { + [ + 85, 65, 95, 70, 52, 74, 117, 109, 98, 108, 101, 95, 72, $i, 0, 0, + ] + }; +} + +macro_rules! G_PERS { + ( $i:expr, $j:expr ) => { + [ + 85, + 65, + 95, + 70, + 52, + 74, + 117, + 109, + 98, + 108, + 101, + 95, + 71, + $i, + ($j & 0xFF) as u8, + ($j >> 8) as u8, + ] + }; +} + +struct State<'a> { + left: &'a mut [u8], + right: &'a mut [u8], +} + +impl<'a> State<'a> { + fn new(message: &'a mut [u8]) -> Self { + let left_length = min(OUTBYTES, message.len() / 2); + let (left, right) = message.split_at_mut(left_length); + State { left, right } + } + + fn h_round(&mut self, i: u8) { + let hash = Blake2bParams::new() + .hash_length(self.left.len()) + .personal(&H_PERS!(i)) + .hash(self.right); + xor(self.left, hash.as_bytes()) + } + + fn g_round(&mut self, i: u8) { + for j in 0..ceildiv(self.right.len(), OUTBYTES) { + let hash = Blake2bParams::new() + .hash_length(OUTBYTES) + .personal(&G_PERS!(i, j as u16)) + .hash(self.left); + xor(&mut self.right[j * OUTBYTES..], hash.as_bytes()); + } + } + + fn apply_f4jumble(&mut self) { + self.g_round(0); + self.h_round(0); + self.g_round(1); + self.h_round(1); + } + + fn apply_f4jumble_inv(&mut self) { + self.h_round(1); + self.g_round(1); + self.h_round(0); + self.g_round(0); + } +} + +/// XORs bytes of the `source` to bytes of the `target`. +/// +/// This method operates over the first `min(source.len(), target.len())` bytes. +fn xor(target: &mut [u8], source: &[u8]) { + for (source, target) in source.iter().zip(target.iter_mut()) { + *target ^= source; + } +} + +fn ceildiv(num: usize, den: usize) -> usize { + (num + den - 1) / den +} + +/// Encodes the given message in-place using F4Jumble. +/// +/// Returns an error if the message is an invalid length. `message` will be unmodified in +/// this case. +/// +/// # Examples +/// +/// ``` +/// let mut message_a = *b"The package from Alice arrives tomorrow morning."; +/// f4jumble::f4jumble_mut(&mut message_a[..]).unwrap(); +/// assert_eq!( +/// hex::encode(message_a), +/// "861c51ee746b0313476967a3483e7e1ff77a2952a17d3ed9e0ab0f502e1179430322da9967b613545b1c36353046ca27", +/// ); +/// +/// let mut message_b = *b"The package from Sarah arrives tomorrow morning."; +/// f4jumble::f4jumble_mut(&mut message_b[..]).unwrap(); +/// assert_eq!( +/// hex::encode(message_b), +/// "af1d55f2695aea02440867bbbfae3b08e8da55b625de3fa91432ab7b2c0a7dff9033ee666db1513ba5761ef482919fb8", +/// ); +/// ``` +pub fn f4jumble_mut(message: &mut [u8]) -> Result<(), Error> { + if VALID_LENGTH.contains(&message.len()) { + State::new(message).apply_f4jumble(); + Ok(()) + } else { + Err(Error::InvalidLength) + } +} + +/// Decodes the given message in-place using F4Jumble⁻¹. +/// +/// Returns an error if the message is an invalid length. `message` will be unmodified in +/// this case. +/// +/// # Examples +/// +/// ``` +/// let mut message_a = hex::decode( +/// "861c51ee746b0313476967a3483e7e1ff77a2952a17d3ed9e0ab0f502e1179430322da9967b613545b1c36353046ca27") +/// .unwrap(); +/// f4jumble::f4jumble_inv_mut(&mut message_a).unwrap(); +/// assert_eq!(message_a, b"The package from Alice arrives tomorrow morning."); +/// +/// let mut message_b = hex::decode( +/// "af1d55f2695aea02440867bbbfae3b08e8da55b625de3fa91432ab7b2c0a7dff9033ee666db1513ba5761ef482919fb8") +/// .unwrap(); +/// f4jumble::f4jumble_inv_mut(&mut message_b).unwrap(); +/// assert_eq!(message_b, b"The package from Sarah arrives tomorrow morning."); +/// ``` +pub fn f4jumble_inv_mut(message: &mut [u8]) -> Result<(), Error> { + if VALID_LENGTH.contains(&message.len()) { + State::new(message).apply_f4jumble_inv(); + Ok(()) + } else { + Err(Error::InvalidLength) + } +} + +/// Encodes the given message using F4Jumble, and returns the encoded message as a vector +/// of bytes. +/// +/// Returns an error if the message is an invalid length. +/// +/// # Examples +/// +/// ``` +/// let message_a = b"The package from Alice arrives tomorrow morning."; +/// let encoded_a = f4jumble::f4jumble(message_a).unwrap(); +/// assert_eq!( +/// hex::encode(encoded_a), +/// "861c51ee746b0313476967a3483e7e1ff77a2952a17d3ed9e0ab0f502e1179430322da9967b613545b1c36353046ca27", +/// ); +/// +/// let message_b = b"The package from Sarah arrives tomorrow morning."; +/// let encoded_b = f4jumble::f4jumble(message_b).unwrap(); +/// assert_eq!( +/// hex::encode(encoded_b), +/// "af1d55f2695aea02440867bbbfae3b08e8da55b625de3fa91432ab7b2c0a7dff9033ee666db1513ba5761ef482919fb8", +/// ); +/// ``` +#[cfg(feature = "alloc")] +#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] +pub fn f4jumble(message: &[u8]) -> Result, Error> { + let mut result = message.to_vec(); + f4jumble_mut(&mut result).map(|()| result) +} + +/// Decodes the given message using F4Jumble⁻¹, and returns the decoded message as a +/// vector of bytes. +/// +/// Returns an error if the message is an invalid length. +/// +/// # Examples +/// +/// ``` +/// let encoded_a = hex::decode( +/// "861c51ee746b0313476967a3483e7e1ff77a2952a17d3ed9e0ab0f502e1179430322da9967b613545b1c36353046ca27") +/// .unwrap(); +/// let message_a = f4jumble::f4jumble_inv(&encoded_a).unwrap(); +/// assert_eq!(message_a, b"The package from Alice arrives tomorrow morning."); +/// +/// let encoded_b = hex::decode( +/// "af1d55f2695aea02440867bbbfae3b08e8da55b625de3fa91432ab7b2c0a7dff9033ee666db1513ba5761ef482919fb8") +/// .unwrap(); +/// let message_b = f4jumble::f4jumble_inv(&encoded_b).unwrap(); +/// assert_eq!(message_b, b"The package from Sarah arrives tomorrow morning."); +/// ``` +#[cfg(feature = "alloc")] +#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] +pub fn f4jumble_inv(message: &[u8]) -> Result, Error> { + let mut result = message.to_vec(); + f4jumble_inv_mut(&mut result).map(|()| result) +} + +#[cfg(test)] +mod common_tests { + use super::{f4jumble_inv_mut, f4jumble_mut, test_vectors}; + + #[test] + fn h_pers() { + assert_eq!(&H_PERS!(7), b"UA_F4Jumble_H\x07\x00\x00"); + } + + #[test] + fn g_pers() { + assert_eq!(&G_PERS!(7, 13), b"UA_F4Jumble_G\x07\x0d\x00"); + assert_eq!(&G_PERS!(7, 65535), b"UA_F4Jumble_G\x07\xff\xff"); + } + + #[test] + fn f4jumble_check_vectors_mut() { + #[cfg(not(feature = "std"))] + let mut cache = [0u8; test_vectors::MAX_VECTOR_LENGTH]; + #[cfg(feature = "std")] + let mut cache = vec![0u8; test_vectors::MAX_VECTOR_LENGTH]; + for v in test_vectors::TEST_VECTORS { + let data = &mut cache[..v.normal.len()]; + data.clone_from_slice(v.normal); + f4jumble_mut(data).unwrap(); + assert_eq!(data, v.jumbled); + f4jumble_inv_mut(data).unwrap(); + assert_eq!(data, v.normal); + } + } +} + +#[cfg(feature = "std")] +#[cfg(test)] +mod std_tests { + use blake2b_simd::blake2b; + use proptest::collection::vec; + use proptest::prelude::*; + use std::format; + use std::vec::Vec; + + use super::{f4jumble, f4jumble_inv, test_vectors, test_vectors_long, VALID_LENGTH}; + + proptest! { + #![proptest_config(ProptestConfig::with_cases(5))] + + #[test] + fn f4jumble_roundtrip(msg in vec(any::(), VALID_LENGTH)) { + let jumbled = f4jumble(&msg).unwrap(); + let jumbled_len = jumbled.len(); + prop_assert_eq!( + msg.len(), jumbled_len, + "Jumbled length {} was not equal to message length {}", + jumbled_len, msg.len() + ); + + let unjumbled = f4jumble_inv(&jumbled).unwrap(); + prop_assert_eq!( + jumbled_len, unjumbled.len(), + "Unjumbled length {} was not equal to jumbled length {}", + unjumbled.len(), jumbled_len + ); + + prop_assert_eq!(msg, unjumbled, "Unjumbled message did not match original message."); + } + } + + #[test] + fn f4jumble_check_vectors() { + for v in test_vectors::TEST_VECTORS { + let jumbled = f4jumble(v.normal).unwrap(); + assert_eq!(jumbled, v.jumbled); + let unjumbled = f4jumble_inv(v.jumbled).unwrap(); + assert_eq!(unjumbled, v.normal); + } + } + + #[test] + fn f4jumble_check_vectors_long() { + for v in test_vectors_long::TEST_VECTORS { + let normal: Vec = (0..v.length).map(|i| i as u8).collect(); + let jumbled = f4jumble(&normal).unwrap(); + assert_eq!(blake2b(&jumbled).as_bytes(), v.jumbled_hash); + let unjumbled = f4jumble_inv(&jumbled).unwrap(); + assert_eq!(unjumbled, normal); + } + } +} diff --git a/components/f4jumble/src/test_vectors.rs b/components/f4jumble/src/test_vectors.rs new file mode 100644 index 0000000000..ac1474e3ae --- /dev/null +++ b/components/f4jumble/src/test_vectors.rs @@ -0,0 +1,4879 @@ +pub(crate) struct TestVector { + pub(crate) normal: &'static [u8], + pub(crate) jumbled: &'static [u8], +} + +#[cfg(not(feature = "std"))] +pub(crate) const MAX_VECTOR_LENGTH: usize = 193; +#[cfg(feature = "std")] +pub(crate) const MAX_VECTOR_LENGTH: usize = 16449; + +// From https://github.com/zcash-hackworks/zcash-test-vectors/blob/master/f4jumble.py +pub(crate) const TEST_VECTORS: &[TestVector] = &[ + TestVector { + normal: &[ + 0x5d, 0x7a, 0x8f, 0x73, 0x9a, 0x2d, 0x9e, 0x94, 0x5b, 0x0c, 0xe1, 0x52, 0xa8, 0x04, + 0x9e, 0x29, 0x4c, 0x4d, 0x6e, 0x66, 0xb1, 0x64, 0x93, 0x9d, 0xaf, 0xfa, 0x2e, 0xf6, + 0xee, 0x69, 0x21, 0x48, 0x1c, 0xdd, 0x86, 0xb3, 0xcc, 0x43, 0x18, 0xd9, 0x61, 0x4f, + 0xc8, 0x20, 0x90, 0x5d, 0x04, 0x2b, + ], + jumbled: &[ + 0x03, 0x04, 0xd0, 0x29, 0x14, 0x1b, 0x99, 0x5d, 0xa5, 0x38, 0x7c, 0x12, 0x59, 0x70, + 0x67, 0x35, 0x04, 0xd6, 0xc7, 0x64, 0xd9, 0x1e, 0xa6, 0xc0, 0x82, 0x12, 0x37, 0x70, + 0xc7, 0x13, 0x9c, 0xcd, 0x88, 0xee, 0x27, 0x36, 0x8c, 0xd0, 0xc0, 0x92, 0x1a, 0x04, + 0x44, 0xc8, 0xe5, 0x85, 0x8d, 0x22, + ], + }, + TestVector { + normal: &[ + 0xb1, 0xef, 0x9c, 0xa3, 0xf2, 0x49, 0x88, 0xc7, 0xb3, 0x53, 0x42, 0x01, 0xcf, 0xb1, + 0xcd, 0x8d, 0xbf, 0x69, 0xb8, 0x25, 0x0c, 0x18, 0xef, 0x41, 0x29, 0x4c, 0xa9, 0x79, + 0x93, 0xdb, 0x54, 0x6c, 0x1f, 0xe0, 0x1f, 0x7e, 0x9c, 0x8e, 0x36, 0xd6, 0xa5, 0xe2, + 0x9d, 0x4e, 0x30, 0xa7, 0x35, 0x94, 0xbf, 0x50, 0x98, 0x42, 0x1c, 0x69, 0x37, 0x8a, + 0xf1, 0xe4, 0x0f, 0x64, 0xe1, 0x25, 0x94, 0x6f, + ], + jumbled: &[ + 0x52, 0x71, 0xfa, 0x33, 0x21, 0xf3, 0xad, 0xbc, 0xfb, 0x07, 0x51, 0x96, 0x88, 0x3d, + 0x54, 0x2b, 0x43, 0x8e, 0xc6, 0x33, 0x91, 0x76, 0x53, 0x7d, 0xaf, 0x85, 0x98, 0x41, + 0xfe, 0x6a, 0x56, 0x22, 0x2b, 0xff, 0x76, 0xd1, 0x66, 0x2b, 0x55, 0x09, 0xa9, 0xe1, + 0x07, 0x9e, 0x44, 0x6e, 0xee, 0xdd, 0x2e, 0x68, 0x3c, 0x31, 0xaa, 0xe3, 0xee, 0x18, + 0x51, 0xd7, 0x95, 0x43, 0x28, 0x52, 0x6b, 0xe1, + ], + }, + TestVector { + normal: &[ + 0x62, 0xc2, 0xfa, 0x7b, 0x2f, 0xec, 0xbc, 0xb6, 0x4b, 0x69, 0x68, 0x91, 0x2a, 0x63, + 0x81, 0xce, 0x3d, 0xc1, 0x66, 0xd5, 0x6a, 0x1d, 0x62, 0xf5, 0xa8, 0xd7, 0x55, 0x1d, + 0xb5, 0xfd, 0x93, 0x13, 0xe8, 0xc7, 0x20, 0x3d, 0x99, 0x6a, 0xf7, 0xd4, 0x77, 0x08, + 0x37, 0x56, 0xd5, 0x9a, 0xf8, 0x0d, 0x06, 0xa7, 0x45, 0xf4, 0x4a, 0xb0, 0x23, 0x75, + 0x2c, 0xb5, 0xb4, 0x06, 0xed, 0x89, 0x85, 0xe1, 0x81, 0x30, 0xab, 0x33, 0x36, 0x26, + 0x97, 0xb0, 0xe4, 0xe4, 0xc7, 0x63, 0xcc, 0xb8, 0xf6, 0x76, 0x49, 0x5c, 0x22, 0x2f, + 0x7f, 0xba, 0x1e, 0x31, 0xde, 0xfa, 0x3d, 0x5a, 0x57, 0xef, 0xc2, 0xe1, 0xe9, 0xb0, + 0x1a, 0x03, 0x55, 0x87, 0xd5, 0xfb, 0x1a, 0x38, 0xe0, 0x1d, 0x94, 0x90, 0x3d, 0x3c, + 0x3e, 0x0a, 0xd3, 0x36, 0x0c, 0x1d, 0x37, 0x10, 0xac, 0xd2, 0x0b, 0x18, 0x3e, 0x31, + 0xd4, 0x9f, + ], + jumbled: &[ + 0x49, 0x8c, 0xf1, 0xb1, 0xba, 0x6f, 0x45, 0x77, 0xef, 0xfe, 0x64, 0x15, 0x1d, 0x67, + 0x46, 0x9a, 0xdc, 0x30, 0xac, 0xc3, 0x25, 0xe3, 0x26, 0x20, 0x7e, 0x7d, 0x78, 0x48, + 0x70, 0x85, 0xb4, 0x16, 0x26, 0x69, 0xf8, 0x2f, 0x02, 0xf9, 0x77, 0x4c, 0x0c, 0xc2, + 0x6a, 0xe6, 0xe1, 0xa7, 0x6f, 0x1e, 0x26, 0x6c, 0x6a, 0x9a, 0x8a, 0x2f, 0x4f, 0xfe, + 0x8d, 0x2d, 0x67, 0x6b, 0x1e, 0xd7, 0x1c, 0xc4, 0x71, 0x95, 0xa3, 0xf1, 0x92, 0x08, + 0x99, 0x8f, 0x7d, 0x8c, 0xdf, 0xc0, 0xb7, 0x4d, 0x2a, 0x96, 0x36, 0x4d, 0x73, 0x3a, + 0x62, 0xb4, 0x27, 0x3c, 0x77, 0xd9, 0x82, 0x8a, 0xa1, 0xfa, 0x06, 0x15, 0x88, 0xa7, + 0xc4, 0xc8, 0x8d, 0xd3, 0xd3, 0xdd, 0xe0, 0x22, 0x39, 0x55, 0x7a, 0xcf, 0xaa, 0xd3, + 0x5c, 0x55, 0x85, 0x4f, 0x45, 0x41, 0xe1, 0xa1, 0xb3, 0xbc, 0x8c, 0x17, 0x07, 0x6e, + 0x73, 0x16, + ], + }, + TestVector { + normal: &[ + 0x25, 0xc9, 0xa1, 0x38, 0xf4, 0x9b, 0x1a, 0x53, 0x7e, 0xdc, 0xf0, 0x4b, 0xe3, 0x4a, + 0x98, 0x51, 0xa7, 0xaf, 0x9d, 0xb6, 0x99, 0x0e, 0xd8, 0x3d, 0xd6, 0x4a, 0xf3, 0x59, + 0x7c, 0x04, 0x32, 0x3e, 0xa5, 0x1b, 0x00, 0x52, 0xad, 0x80, 0x84, 0xa8, 0xb9, 0xda, + 0x94, 0x8d, 0x32, 0x0d, 0xad, 0xd6, 0x4f, 0x54, 0x31, 0xe6, 0x1d, 0xdf, 0x65, 0x8d, + 0x24, 0xae, 0x67, 0xc2, 0x2c, 0x8d, 0x13, 0x09, 0x13, 0x1f, 0xc0, 0x0f, 0xe7, 0xf2, + 0x35, 0x73, 0x42, 0x76, 0xd3, 0x8d, 0x47, 0xf1, 0xe1, 0x91, 0xe0, 0x0c, 0x7a, 0x1d, + 0x48, 0xaf, 0x04, 0x68, 0x27, 0x59, 0x1e, 0x97, 0x33, 0xa9, 0x7f, 0xa6, 0xb6, 0x79, + 0xf3, 0xdc, 0x60, 0x1d, 0x00, 0x82, 0x85, 0xed, 0xcb, 0xda, 0xe6, 0x9c, 0xe8, 0xfc, + 0x1b, 0xe4, 0xaa, 0xc0, 0x0f, 0xf2, 0x71, 0x1e, 0xbd, 0x93, 0x1d, 0xe5, 0x18, 0x85, + 0x68, 0x78, 0xf7, + ], + jumbled: &[ + 0x75, 0x08, 0xa3, 0xa1, 0x46, 0x71, 0x4f, 0x22, 0x9d, 0xb9, 0x1b, 0x54, 0x3e, 0x24, + 0x06, 0x33, 0xed, 0x57, 0x85, 0x3f, 0x64, 0x51, 0xc9, 0xdb, 0x6d, 0x64, 0xc6, 0xe8, + 0x6a, 0xf1, 0xb8, 0x8b, 0x28, 0x70, 0x4f, 0x60, 0x85, 0x82, 0xc5, 0x3c, 0x51, 0xce, + 0x7d, 0x5b, 0x85, 0x48, 0x82, 0x7a, 0x97, 0x1d, 0x2b, 0x98, 0xd4, 0x1b, 0x7f, 0x62, + 0x58, 0x65, 0x59, 0x02, 0x44, 0x0c, 0xd6, 0x6e, 0xe1, 0x1e, 0x84, 0xdb, 0xfa, 0xc7, + 0xd2, 0xa4, 0x36, 0x96, 0xfd, 0x04, 0x68, 0x81, 0x0a, 0x3d, 0x96, 0x37, 0xc3, 0xfa, + 0x58, 0xe7, 0xd2, 0xd3, 0x41, 0xef, 0x25, 0x0f, 0xa0, 0x9b, 0x9f, 0xb7, 0x1a, 0x78, + 0xa4, 0x1d, 0x38, 0x93, 0x70, 0x13, 0x8a, 0x55, 0xea, 0x58, 0xfc, 0xde, 0x77, 0x9d, + 0x71, 0x4a, 0x04, 0xe0, 0xd3, 0x0e, 0x61, 0xdc, 0x2d, 0x8b, 0xe0, 0xda, 0x61, 0xcd, + 0x68, 0x45, 0x09, + ], + }, + TestVector { + normal: &[ + 0x34, 0x76, 0xf2, 0x1a, 0x48, 0x2e, 0xc9, 0x37, 0x83, 0x65, 0xc8, 0xf7, 0x39, 0x3c, + 0x94, 0xe2, 0x88, 0x53, 0x15, 0xeb, 0x46, 0x71, 0x09, 0x8b, 0x79, 0x53, 0x5e, 0x79, + 0x0f, 0xe5, 0x3e, 0x29, 0xfe, 0xf2, 0xb3, 0x76, 0x66, 0x97, 0xac, 0x32, 0xb4, 0xf4, + 0x73, 0xf4, 0x68, 0xa0, 0x08, 0xe7, 0x23, 0x89, 0xfc, 0x03, 0x88, 0x0d, 0x78, 0x0c, + 0xb0, 0x7f, 0xcf, 0xaa, 0xbe, 0x3f, 0x1a, 0x84, 0xb2, 0x7d, 0xb5, 0x9a, 0x4a, 0x15, + 0x3d, 0x88, 0x2d, 0x2b, 0x21, 0x03, 0x59, 0x65, 0x55, 0xed, 0x94, 0x94, 0xc6, 0xac, + 0x89, 0x3c, 0x49, 0x72, 0x38, 0x33, 0xec, 0x89, 0x26, 0xc1, 0x03, 0x95, 0x86, 0xa7, + 0xaf, 0xcf, 0x4a, 0x0d, 0x9c, 0x73, 0x1e, 0x98, 0x5d, 0x99, 0x58, 0x9c, 0x8b, 0xb8, + 0x38, 0xe8, 0xaa, 0xf7, 0x45, 0x53, 0x3e, 0xd9, 0xe8, 0xae, 0x3a, 0x1c, 0xd0, 0x74, + 0xa5, 0x1a, 0x20, 0xda, 0x8a, 0xba, 0x18, 0xd1, 0xdb, 0xeb, 0xbc, 0x86, 0x2d, 0xed, + 0x42, 0x43, 0x5e, 0x92, 0x47, 0x69, 0x30, 0xd0, 0x69, 0x89, 0x6c, 0xff, 0x30, 0xeb, + 0x41, 0x4f, 0x72, 0x7b, 0x89, 0xe0, 0x01, 0xaf, 0xa2, 0xfb, 0x8d, 0xc3, 0x43, 0x6d, + 0x75, 0xa4, 0xa6, 0xf2, 0x65, 0x72, 0x50, 0x4b, 0x19, 0x22, 0x32, 0xec, 0xb9, 0xf0, + 0xc0, 0x24, 0x11, 0xe5, 0x25, 0x96, 0xbc, 0x5e, 0x90, 0x45, + ], + jumbled: &[ + 0x51, 0x39, 0x91, 0x2f, 0xe8, 0xb9, 0x54, 0x92, 0xc1, 0x27, 0x31, 0x99, 0x5a, 0x0f, + 0x44, 0x78, 0xdb, 0xeb, 0x81, 0xec, 0x36, 0x65, 0x3a, 0x21, 0xbc, 0x80, 0xd6, 0x73, + 0xf3, 0xc6, 0xa0, 0xfe, 0xef, 0x70, 0xb6, 0xc5, 0x66, 0xf9, 0xd3, 0x4b, 0xb7, 0x26, + 0xc0, 0x98, 0x64, 0x83, 0x82, 0xd1, 0x05, 0xaf, 0xb1, 0x9b, 0x2b, 0x84, 0x86, 0xb7, + 0x3c, 0xbd, 0x47, 0xa1, 0x7a, 0x0d, 0x2d, 0x1f, 0xd5, 0x93, 0xb1, 0x4b, 0xb9, 0x82, + 0x6c, 0x5d, 0x11, 0x4b, 0x85, 0x0c, 0x6f, 0x0c, 0xf3, 0x08, 0x3a, 0x6f, 0x61, 0xe3, + 0x8e, 0x42, 0x71, 0x3a, 0x37, 0xef, 0x79, 0x97, 0xeb, 0xd2, 0xb3, 0x76, 0xc8, 0xa4, + 0x10, 0xd7, 0x97, 0xb3, 0x93, 0x2e, 0x5a, 0x6e, 0x39, 0xe7, 0x26, 0xb2, 0x89, 0x4c, + 0xe7, 0x96, 0x04, 0xb4, 0xae, 0x3c, 0x00, 0xac, 0xae, 0xa3, 0xbe, 0x2c, 0x1d, 0xfe, + 0x69, 0x7f, 0xa6, 0x44, 0x75, 0x51, 0x02, 0xcf, 0x9a, 0xd7, 0x87, 0x94, 0xd0, 0x59, + 0x45, 0x85, 0x49, 0x4f, 0xe3, 0x8a, 0xb5, 0x6f, 0xa6, 0xef, 0x32, 0x71, 0xa6, 0x8a, + 0x33, 0x48, 0x10, 0x15, 0xad, 0xf3, 0x94, 0x4c, 0x11, 0x53, 0x11, 0x42, 0x1a, 0x7d, + 0xc3, 0xce, 0x73, 0xef, 0x2a, 0xbf, 0x47, 0xe1, 0x8a, 0x6a, 0xca, 0x7f, 0x9d, 0xd2, + 0x5a, 0x85, 0xce, 0x8d, 0xbd, 0x6f, 0x1a, 0xd8, 0x9c, 0x8d, + ], + }, + TestVector { + normal: &[ + 0x7e, 0x74, 0x59, 0x39, 0xff, 0xed, 0xbd, 0x12, 0x86, 0x3c, 0xe7, 0x1a, 0x02, 0xaf, + 0x11, 0x7d, 0x41, 0x7a, 0xdb, 0x3d, 0x15, 0xcc, 0x54, 0xdc, 0xb1, 0xfc, 0xe4, 0x67, + 0x50, 0x0c, 0x6b, 0x8f, 0xb8, 0x6b, 0x12, 0xb5, 0x6d, 0xa9, 0xc3, 0x82, 0x85, 0x7d, + 0xee, 0xcc, 0x40, 0xa9, 0x8d, 0x5f, 0x29, 0x35, 0x39, 0x5e, 0xe4, 0x76, 0x2d, 0xd2, + 0x1a, 0xfd, 0xbb, 0x5d, 0x47, 0xfa, 0x9a, 0x6d, 0xd9, 0x84, 0xd5, 0x67, 0xdb, 0x28, + 0x57, 0xb9, 0x27, 0xb7, 0xfa, 0xe2, 0xdb, 0x58, 0x71, 0x05, 0x41, 0x5d, 0x46, 0x42, + 0x78, 0x9d, 0x38, 0xf5, 0x0b, 0x8d, 0xbc, 0xc1, 0x29, 0xca, 0xb3, 0xd1, 0x7d, 0x19, + 0xf3, 0x35, 0x5b, 0xcf, 0x73, 0xce, 0xcb, 0x8c, 0xb8, 0xa5, 0xda, 0x01, 0x30, 0x71, + 0x52, 0xf1, 0x39, 0x36, 0xa2, 0x70, 0x57, 0x26, 0x70, 0xdc, 0x82, 0xd3, 0x90, 0x26, + 0xc6, 0xcb, 0x4c, 0xd4, 0xb0, 0xf7, 0xf5, 0xaa, 0x2a, 0x4f, 0x5a, 0x53, 0x41, 0xec, + 0x5d, 0xd7, 0x15, 0x40, 0x6f, 0x2f, 0xdd, 0x2a, 0xfa, 0x73, 0x3f, 0x5f, 0x64, 0x1c, + 0x8c, 0x21, 0x86, 0x2a, 0x1b, 0xaf, 0xce, 0x26, 0x09, 0xd9, 0xee, 0xcf, 0xa1, 0x58, + 0xcf, 0xb5, 0xcd, 0x79, 0xf8, 0x80, 0x08, 0xe3, 0x15, 0xdc, 0x7d, 0x83, 0x88, 0xe7, + 0x6c, 0x17, 0x82, 0xfd, 0x27, 0x95, 0xd1, 0x8a, 0x76, 0x36, 0x24, + ], + jumbled: &[ + 0x1a, 0x52, 0x58, 0x5e, 0x65, 0x2d, 0xa6, 0xea, 0x46, 0x99, 0x49, 0x54, 0x90, 0x5c, + 0xb7, 0x9f, 0x55, 0xfc, 0xa5, 0x81, 0x71, 0xa4, 0xd7, 0xf7, 0x73, 0xa5, 0x7d, 0x23, + 0xed, 0x9d, 0xde, 0xc0, 0xc7, 0x45, 0xef, 0x0f, 0x45, 0x88, 0xfa, 0x7b, 0x2b, 0x68, + 0xd6, 0x9c, 0xdd, 0x25, 0xe5, 0xeb, 0x0e, 0x08, 0xc2, 0x05, 0x23, 0xa3, 0x95, 0x71, + 0x71, 0xf1, 0x73, 0x0a, 0xb0, 0x63, 0x6f, 0xae, 0xe7, 0x5d, 0xa2, 0xdc, 0x9e, 0x89, + 0x56, 0x2f, 0x06, 0x53, 0xd4, 0xe9, 0x42, 0x21, 0x79, 0x28, 0x6a, 0xe8, 0x30, 0x5f, + 0x01, 0x37, 0x1f, 0x47, 0xab, 0x16, 0xee, 0xd6, 0x92, 0xc3, 0x89, 0x5c, 0xe2, 0xfd, + 0x65, 0x5e, 0x4b, 0x19, 0x65, 0x1c, 0x35, 0xd8, 0x3c, 0x81, 0x89, 0x4f, 0x68, 0x70, + 0x55, 0xb5, 0x81, 0x11, 0x44, 0x40, 0x64, 0x65, 0x08, 0xe3, 0x9a, 0x49, 0xb0, 0xd5, + 0xa9, 0x90, 0x04, 0x56, 0x0a, 0xf7, 0x36, 0x7c, 0xc2, 0x73, 0x83, 0x44, 0xd4, 0xe7, + 0x97, 0xa9, 0x95, 0xed, 0x66, 0xdf, 0x72, 0x22, 0x8e, 0x3d, 0x37, 0x46, 0x67, 0x43, + 0x37, 0x10, 0x47, 0x00, 0x14, 0x4c, 0x73, 0xb6, 0xdb, 0x27, 0xd2, 0x38, 0xc9, 0xe1, + 0x77, 0x06, 0x62, 0xfe, 0xb0, 0x95, 0x7d, 0x50, 0x28, 0xb5, 0x08, 0x6f, 0x38, 0x39, + 0xaa, 0xcf, 0x27, 0x50, 0x22, 0xdd, 0x7e, 0x7e, 0x98, 0x3b, 0x6d, + ], + }, + // This test vector is too long for most no-std environments. + #[cfg(feature = "std")] + TestVector { + normal: &[ + 0xc2, 0x5f, 0xa9, 0x59, 0xcc, 0x97, 0x48, 0x9c, 0xe7, 0x57, 0x45, 0x82, 0x4b, 0x77, + 0x86, 0x8c, 0x53, 0x23, 0x9c, 0xfb, 0xdf, 0x73, 0xca, 0xec, 0x65, 0x60, 0x40, 0x37, + 0x31, 0x4f, 0xaa, 0xce, 0xb5, 0x62, 0x18, 0xc6, 0xbd, 0x30, 0xf8, 0x37, 0x4a, 0xc1, + 0x33, 0x86, 0x79, 0x3f, 0x21, 0xa9, 0xfb, 0x80, 0xad, 0x03, 0xbc, 0x0c, 0xda, 0x4a, + 0x44, 0x94, 0x6c, 0x00, 0xe1, 0xb1, 0xa1, 0xdf, 0x0e, 0x5b, 0x87, 0xb5, 0xbe, 0xce, + 0x47, 0x7a, 0x70, 0x96, 0x49, 0xe9, 0x50, 0x06, 0x05, 0x91, 0x39, 0x48, 0x12, 0x95, + 0x1e, 0x1f, 0xe3, 0x89, 0x5b, 0x8c, 0xc3, 0xd1, 0x4d, 0x2c, 0xf6, 0x55, 0x6d, 0xf6, + 0xed, 0x4b, 0x4d, 0xdd, 0x3d, 0x9a, 0x69, 0xf5, 0x33, 0x57, 0xd7, 0x76, 0x7f, 0x4f, + 0x5c, 0xcb, 0xdb, 0xc5, 0x96, 0x63, 0x12, 0x77, 0xf8, 0xfe, 0xcd, 0x08, 0xcb, 0x05, + 0x6b, 0x95, 0xe3, 0x02, 0x5b, 0x97, 0x92, 0xff, 0xf7, 0xf2, 0x44, 0xfc, 0x71, 0x62, + 0x69, 0xb9, 0x26, 0xd6, 0x2e, 0x95, 0x96, 0xfa, 0x82, 0x5c, 0x6b, 0xf2, 0x1a, 0xff, + 0x9e, 0x68, 0x62, 0x5a, 0x19, 0x24, 0x40, 0xea, 0x06, 0x82, 0x81, 0x23, 0xd9, 0x78, + 0x84, 0x80, 0x6f, 0x15, 0xfa, 0x08, 0xda, 0x52, 0x75, 0x4a, 0x10, 0x95, 0xe3, 0xff, + 0x1a, 0xbd, 0x5c, 0xe4, 0xfd, 0xdf, 0xcc, 0xfc, 0x3a, 0x61, 0x28, 0xae, 0xf7, 0x84, + 0xa6, 0x46, 0x10, 0xa8, 0x9d, 0x1a, 0x70, 0x99, 0x21, 0x6d, 0x08, 0x14, 0xd3, 0xa2, + 0xd4, 0x52, 0x43, 0x1c, 0x32, 0xd4, 0x11, 0xac, 0x1c, 0xce, 0x82, 0xad, 0x02, 0x29, + 0x40, 0x7b, 0xbc, 0x48, 0x98, 0x56, 0x75, 0xe3, 0xf8, 0x74, 0xa4, 0x53, 0x3f, 0x1d, + 0x63, 0xa8, 0x4d, 0xfa, 0x3e, 0x0f, 0x46, 0x0f, 0xe2, 0xf5, 0x7e, 0x34, 0xfb, 0xc7, + 0x54, 0x23, 0xc3, 0x73, 0x7f, 0x5b, 0x2a, 0x06, 0x15, 0xf5, 0x72, 0x2d, 0xb0, 0x41, + 0xa3, 0xef, 0x66, 0xfa, 0x48, 0x3a, 0xfd, 0x3c, 0x2e, 0x19, 0xe5, 0x94, 0x44, 0xa6, + 0x4a, 0xdd, 0x6d, 0xf1, 0xd9, 0x63, 0xf5, 0xdd, 0x5b, 0x50, 0x10, 0xd3, 0xd0, 0x25, + 0xf0, 0x28, 0x7c, 0x4c, 0xf1, 0x9c, 0x75, 0xf3, 0x3d, 0x51, 0xdd, 0xdd, 0xba, 0x5d, + 0x65, 0x7b, 0x43, 0xee, 0x8d, 0xa6, 0x45, 0x44, 0x38, 0x14, 0xcc, 0x73, 0x29, 0xf3, + 0xe9, 0xb4, 0xe5, 0x4c, 0x23, 0x6c, 0x29, 0xaf, 0x39, 0x23, 0x10, 0x17, 0x56, 0xd9, + 0xfa, 0x4b, 0xd0, 0xf7, 0xd2, 0xdd, 0xaa, 0xcb, 0x6b, 0x0f, 0x86, 0xa2, 0x65, 0x8e, + 0x0a, 0x07, 0xa0, 0x5a, 0xc5, 0xb9, 0x50, 0x05, 0x1c, 0xd2, 0x4c, 0x47, 0xa8, 0x8d, + 0x13, 0xd6, 0x59, 0xba, 0x2a, 0x46, 0xca, 0x18, 0x30, 0x81, 0x6d, 0x09, 0xcd, 0x76, + 0x46, 0xf7, 0x6f, 0x71, 0x6a, 0xbe, 0xc5, 0xde, 0x07, 0xfe, 0x9b, 0x52, 0x34, 0x10, + 0x80, 0x6e, 0xa6, 0xf2, 0x88, 0xf8, 0x73, 0x6c, 0x23, 0x35, 0x7c, 0x85, 0xf4, 0x57, + 0x91, 0xe1, 0x70, 0x80, 0x29, 0xd9, 0x82, 0x4d, 0x90, 0x70, 0x46, 0x07, 0xf3, 0x87, + 0xa0, 0x3e, 0x49, 0xbf, 0x98, 0x36, 0x57, 0x44, 0x31, 0x34, 0x5a, 0x78, 0x77, 0xef, + 0xaa, 0x8a, 0x08, 0xe7, 0x30, 0x81, 0xef, 0x8d, 0x62, 0xcb, 0x78, 0x0a, 0xb6, 0x88, + 0x3a, 0x50, 0xa0, 0xd4, 0x70, 0x19, 0x0d, 0xfb, 0xa1, 0x0a, 0x85, 0x7f, 0x82, 0x84, + 0x2d, 0x38, 0x25, 0xb3, 0xd6, 0xda, 0x05, 0x73, 0xd3, 0x16, 0xeb, 0x16, 0x0d, 0xc0, + 0xb7, 0x16, 0xc4, 0x8f, 0xbd, 0x46, 0x7f, 0x75, 0xb7, 0x80, 0x14, 0x9a, 0xe8, 0x80, + 0x8f, 0x4e, 0x68, 0xf5, 0x0c, 0x05, 0x36, 0xac, 0xdd, 0xf6, 0xf1, 0xae, 0xab, 0x01, + 0x6b, 0x6b, 0xc1, 0xec, 0x14, 0x4b, 0x4e, 0x55, 0x3a, 0xcf, 0xd6, 0x70, 0xf7, 0x7e, + 0x75, 0x5f, 0xc8, 0x8e, 0x06, 0x77, 0xe3, 0x1b, 0xa4, 0x59, 0xb4, 0x4e, 0x30, 0x77, + 0x68, 0x95, 0x8f, 0xe3, 0x78, 0x9d, 0x41, 0xc2, 0xb1, 0xff, 0x43, 0x4c, 0xb3, 0x0e, + 0x15, 0x91, 0x4f, 0x01, 0xbc, 0x6b, 0xc2, 0x30, 0x7b, 0x48, 0x8d, 0x25, 0x56, 0xd7, + 0xb7, 0x38, 0x0e, 0xa4, 0xff, 0xd7, 0x12, 0xf6, 0xb0, 0x2f, 0xe8, 0x06, 0xb9, 0x45, + 0x69, 0xcd, 0x40, 0x59, 0xf3, 0x96, 0xbf, 0x29, 0xb9, 0x9d, 0x0a, 0x40, 0xe5, 0xe1, + 0x71, 0x1c, 0xa9, 0x44, 0xf7, 0x2d, 0x43, 0x6a, 0x10, 0x2f, 0xca, 0x4b, 0x97, 0x69, + 0x3d, 0xa0, 0xb0, 0x86, 0xfe, 0x9d, 0x2e, 0x71, 0x62, 0x47, 0x0d, 0x02, 0xe0, 0xf0, + 0x5d, 0x4b, 0xec, 0x95, 0x12, 0xbf, 0xb3, 0xf3, 0x83, 0x27, 0x29, 0x6e, 0xfa, 0xa7, + 0x43, 0x28, 0xb1, 0x18, 0xc2, 0x74, 0x02, 0xc7, 0x0c, 0x3a, 0x90, 0xb4, 0x9a, 0xd4, + 0xbb, 0xc6, 0x8e, 0x37, 0xc0, 0xaa, 0x7d, 0x9b, 0x3f, 0xe1, 0x77, 0x99, 0xd7, 0x3b, + 0x84, 0x1e, 0x75, 0x17, 0x13, 0xa0, 0x29, 0x43, 0x90, 0x5a, 0xae, 0x08, 0x03, 0xfd, + 0x69, 0x44, 0x2e, 0xb7, 0x68, 0x1e, 0xc2, 0xa0, 0x56, 0x00, 0x05, 0x4e, 0x92, 0xee, + 0xd5, 0x55, 0x02, 0x8f, 0x21, 0xb6, 0xa1, 0x55, 0x26, 0x8a, 0x2d, 0xd6, 0x64, 0x0a, + 0x69, 0x30, 0x1a, 0x52, 0xa3, 0x8d, 0x4d, 0x9f, 0x9f, 0x95, 0x7a, 0xe3, 0x5a, 0xf7, + 0x16, 0x71, 0x18, 0x14, 0x1c, 0xe4, 0xc9, 0xbe, 0x0a, 0x6a, 0x49, 0x2f, 0xe7, 0x9f, + 0x15, 0x81, 0xa1, 0x55, 0xfa, 0x3a, 0x2b, 0x9d, 0xaf, 0xd8, 0x2e, 0x65, 0x0b, 0x38, + 0x6a, 0xd3, 0xa0, 0x8c, 0xb6, 0xb8, 0x31, 0x31, 0xac, 0x30, 0x0b, 0x08, 0x46, 0x35, + 0x4a, 0x7e, 0xef, 0x9c, 0x41, 0x0e, 0x4b, 0x62, 0xc4, 0x7c, 0x54, 0x26, 0x90, 0x7d, + 0xfc, 0x66, 0x85, 0xc5, 0xc9, 0x9b, 0x71, 0x41, 0xac, 0x62, 0x6a, 0xb4, 0x76, 0x1f, + 0xd3, 0xf4, 0x1e, 0x72, 0x8e, 0x1a, 0x28, 0xf8, 0x9d, 0xb8, 0x9f, 0xfd, 0xec, 0xa3, + 0x64, 0xdd, 0x2f, 0x0f, 0x07, 0x39, 0xf0, 0x53, 0x45, 0x56, 0x48, 0x31, 0x99, 0xc7, + 0x1f, 0x18, 0x93, 0x41, 0xac, 0x9b, 0x78, 0xa2, 0x69, 0x16, 0x42, 0x06, 0xa0, 0xea, + 0x1c, 0xe7, 0x3b, 0xfb, 0x2a, 0x94, 0x2e, 0x73, 0x70, 0xb2, 0x47, 0xc0, 0x46, 0xf8, + 0xe7, 0x5e, 0xf8, 0xe3, 0xf8, 0xbd, 0x82, 0x1c, 0xf5, 0x77, 0x49, 0x18, 0x64, 0xe2, + 0x0e, 0x6d, 0x08, 0xfd, 0x2e, 0x32, 0xb5, 0x55, 0xc9, 0x2c, 0x66, 0x1f, 0x19, 0x58, + 0x8b, 0x72, 0xa8, 0x95, 0x99, 0x71, 0x0a, 0x88, 0x06, 0x12, 0x53, 0xca, 0x28, 0x5b, + 0x63, 0x04, 0xb3, 0x7d, 0xa2, 0xb5, 0x29, 0x4f, 0x5c, 0xb3, 0x54, 0xa8, 0x94, 0x32, + 0x28, 0x48, 0xcc, 0xbd, 0xc7, 0xc2, 0x54, 0x5b, 0x7d, 0xa5, 0x68, 0xaf, 0xac, 0x87, + 0xff, 0xa0, 0x05, 0xc3, 0x12, 0x24, 0x1c, 0x2d, 0x57, 0xf4, 0xb4, 0x5d, 0x64, 0x19, + 0xf0, 0xd2, 0xe2, 0xc5, 0xaf, 0x33, 0xae, 0x24, 0x37, 0x85, 0xb3, 0x25, 0xcd, 0xab, + 0x95, 0x40, 0x4f, 0xc7, 0xae, 0xd7, 0x05, 0x25, 0xcd, 0xdb, 0x41, 0x87, 0x2c, 0xfc, + 0xc2, 0x14, 0xb1, 0x32, 0x32, 0xed, 0xc7, 0x86, 0x09, 0x75, 0x3d, 0xbf, 0xf9, 0x30, + 0xeb, 0x0d, 0xc1, 0x56, 0x61, 0x2b, 0x9c, 0xb4, 0x34, 0xbc, 0x4b, 0x69, 0x33, 0x92, + 0xde, 0xb8, 0x7c, 0x53, 0x04, 0x35, 0x31, 0x2e, 0xdc, 0xed, 0xc6, 0xa9, 0x61, 0x13, + 0x33, 0x38, 0xd7, 0x86, 0xc4, 0xa3, 0xe1, 0x03, 0xf6, 0x01, 0x10, 0xa1, 0x6b, 0x13, + 0x37, 0x12, 0x97, 0x04, 0xbf, 0x47, 0x54, 0xff, 0x6b, 0xa9, 0xfb, 0xe6, 0x59, 0x51, + 0xe6, 0x10, 0x62, 0x0f, 0x71, 0xcd, 0xa8, 0xfc, 0x87, 0x76, 0x25, 0xf2, 0xc5, 0xbb, + 0x04, 0xcb, 0xe1, 0x22, 0x8b, 0x1e, 0x88, 0x6f, 0x40, 0x50, 0xaf, 0xd8, 0xfe, 0x94, + 0xe9, 0x7d, 0x2e, 0x9e, 0x85, 0xc6, 0xbb, 0x74, 0x8c, 0x00, 0x42, 0xd3, 0x24, 0x9a, + 0xbb, 0x13, 0x42, 0xbb, 0x0e, 0xeb, 0xf6, 0x20, 0x58, 0xbf, 0x3d, 0xe0, 0x80, 0xd9, + 0x46, 0x11, 0xa3, 0x75, 0x09, 0x15, 0xb5, 0xdc, 0x6c, 0x0b, 0x38, 0x99, 0xd4, 0x12, + 0x22, 0xba, 0xce, 0x76, 0x0e, 0xe9, 0xc8, 0x81, 0x8d, 0xed, 0x59, 0x9e, 0x34, 0xc5, + 0x6d, 0x73, 0x72, 0xaf, 0x1e, 0xb8, 0x68, 0x52, 0xf2, 0xa7, 0x32, 0x10, 0x4b, 0xdb, + 0x75, 0x07, 0x39, 0xde, 0x6c, 0x2c, 0x6e, 0x0f, 0x9e, 0xb7, 0xcb, 0x17, 0xf1, 0x94, + 0x2b, 0xfc, 0x9f, 0x4f, 0xd6, 0xeb, 0xb6, 0xb4, 0xcd, 0xd4, 0xda, 0x2b, 0xca, 0x26, + 0xfa, 0xc4, 0x57, 0x8e, 0x9f, 0x54, 0x34, 0x05, 0xac, 0xc7, 0xd8, 0x6f, 0xf5, 0x91, + 0x58, 0xbd, 0x0c, 0xba, 0x3a, 0xef, 0x6f, 0x4a, 0x84, 0x72, 0xd1, 0x44, 0xd9, 0x9f, + 0x8b, 0x8d, 0x1d, 0xed, 0xaa, 0x90, 0x77, 0xd4, 0xf0, 0x1d, 0x4b, 0xb2, 0x7b, 0xbe, + 0x31, 0xd8, 0x8f, 0xbe, 0xfa, 0xc3, 0xdc, 0xd4, 0x79, 0x75, 0x63, 0xa2, 0x6b, 0x1d, + 0x61, 0xfc, 0xd9, 0xa4, 0x64, 0xab, 0x21, 0xed, 0x55, 0x0f, 0xe6, 0xfa, 0x09, 0x69, + 0x5b, 0xa0, 0xb2, 0xf1, 0x0e, 0xea, 0x64, 0x68, 0xcc, 0x6e, 0x20, 0xa6, 0x6f, 0x82, + 0x6e, 0x3d, 0x14, 0xc5, 0x00, 0x6f, 0x05, 0x63, 0x88, 0x7f, 0x5e, 0x12, 0x89, 0xbe, + 0x1b, 0x20, 0x04, 0xca, 0xca, 0x8d, 0x3f, 0x34, 0xd6, 0xe8, 0x4b, 0xf5, 0x9c, 0x1e, + 0x04, 0x61, 0x9a, 0x7c, 0x23, 0xa9, 0x96, 0x94, 0x1d, 0x88, 0x9e, 0x46, 0x22, 0xa9, + 0xb9, 0xb1, 0xd5, 0x9d, 0x5e, 0x31, 0x90, 0x94, 0x31, 0x8c, 0xd4, 0x05, 0xba, 0x27, + 0xb7, 0xe2, 0xc0, 0x84, 0x76, 0x2d, 0x31, 0x45, 0x3e, 0xc4, 0x54, 0x9a, 0x4d, 0x97, + 0x72, 0x9d, 0x03, 0x34, 0x60, 0xfc, 0xf8, 0x9d, 0x64, 0x94, 0xf2, 0xff, 0xd7, 0x89, + 0xe9, 0x80, 0x82, 0xea, 0x5c, 0xe9, 0x53, 0x4b, 0x3a, 0xcd, 0x60, 0xfe, 0x49, 0xe3, + 0x7e, 0x4f, 0x66, 0x69, 0x31, 0x67, 0x73, 0x19, 0xed, 0x89, 0xf8, 0x55, 0x88, 0x74, + 0x1b, 0x31, 0x28, 0x90, 0x1a, 0x93, 0xbd, 0x78, 0xe4, 0xbe, 0x02, 0x25, 0xa9, 0xe2, + 0x69, 0x2c, 0x77, 0xc9, 0x69, 0xed, 0x01, 0x76, 0xbd, 0xf9, 0x55, 0x59, 0x48, 0xcb, + 0xd5, 0xa3, 0x32, 0xd0, 0x45, 0xde, 0x6b, 0xa6, 0xbf, 0x44, 0x90, 0xad, 0xfe, 0x74, + 0x44, 0xcd, 0x46, 0x7a, 0x09, 0x07, 0x54, 0x17, 0xfc, 0xc0, 0x06, 0x2e, 0x49, 0xf0, + 0x08, 0xc5, 0x1a, 0xd4, 0x22, 0x74, 0x39, 0xc1, 0xb4, 0x47, 0x6c, 0xcd, 0x8e, 0x97, + 0x86, 0x2d, 0xab, 0x7b, 0xe1, 0xe8, 0xd3, 0x99, 0xc0, 0x5e, 0xf2, 0x7c, 0x6e, 0x22, + 0xee, 0x27, 0x3e, 0x15, 0x78, 0x6e, 0x39, 0x4c, 0x8f, 0x1b, 0xe3, 0x16, 0x82, 0xa3, + 0x01, 0x47, 0x96, 0x3a, 0xc8, 0xda, 0x8d, 0x41, 0xd8, 0x04, 0x25, 0x84, 0x26, 0xa3, + 0xf7, 0x02, 0x89, 0xb8, 0xad, 0x19, 0xd8, 0xde, 0x13, 0xbe, 0x4e, 0xeb, 0xe3, 0xbd, + 0x4c, 0x8a, 0x6f, 0x55, 0xd6, 0xe0, 0xc3, 0x73, 0xd4, 0x56, 0x85, 0x18, 0x79, 0xf5, + 0xfb, 0xc2, 0x82, 0xdb, 0x9e, 0x13, 0x48, 0x06, 0xbf, 0xf7, 0x1e, 0x11, 0xbc, 0x33, + 0xab, 0x75, 0xdd, 0x6c, 0xa0, 0x67, 0xfb, 0x73, 0xa0, 0x43, 0xb6, 0x46, 0xa7, 0xcf, + 0x39, 0xca, 0xb4, 0x92, 0x83, 0x86, 0x78, 0x6d, 0x2f, 0x24, 0x14, 0x1e, 0xe1, 0x20, + 0xfd, 0xc3, 0x4d, 0x67, 0x64, 0xea, 0xfc, 0x66, 0x88, 0x0e, 0xe0, 0x20, 0x4f, 0x53, + 0xcc, 0x11, 0x67, 0xed, 0x20, 0xb4, 0x3a, 0x52, 0xde, 0xa3, 0xca, 0x7c, 0xff, 0x8e, + 0xf3, 0x5c, 0xd8, 0xe6, 0xd7, 0xc1, 0x11, 0xa6, 0x8e, 0xf4, 0x4b, 0xcd, 0x0c, 0x15, + 0x13, 0xad, 0x47, 0xca, 0x61, 0xc6, 0x59, 0xcc, 0x5d, 0x32, 0x5b, 0x44, 0x0f, 0x6b, + 0x9f, 0x59, 0xaf, 0xf6, 0x68, 0x79, 0xbb, 0x66, 0x88, 0xfd, 0x28, 0x59, 0x36, 0x2b, + 0x18, 0x2f, 0x20, 0x7b, 0x31, 0x75, 0x96, 0x1f, 0x64, 0x11, 0xa4, 0x93, 0xbf, 0xfd, + 0x04, 0x8e, 0x7d, 0x0d, 0x87, 0xd8, 0x2f, 0xe6, 0xf9, 0x90, 0xa2, 0xb0, 0xa2, 0x5f, + 0x5a, 0xa0, 0x11, 0x1a, 0x6e, 0x68, 0xf3, 0x7b, 0xf6, 0xf3, 0xac, 0x2d, 0x26, 0xb8, + 0x46, 0x86, 0xe5, 0x69, 0xd5, 0x8d, 0x99, 0xc1, 0x38, 0x35, 0x97, 0xfa, 0xd8, 0x11, + 0x93, 0xc4, 0xc1, 0xb1, 0x6e, 0x6a, 0x90, 0xe2, 0xd5, 0x07, 0xcd, 0xfe, 0x6f, 0xbd, + 0xaa, 0x86, 0x16, 0x3e, 0x9c, 0xf5, 0xde, 0x31, 0x00, 0xfb, 0xca, 0x7e, 0x8d, 0xa0, + 0x47, 0xb0, 0x90, 0xdb, 0x9f, 0x37, 0x95, 0x2f, 0xbf, 0xee, 0x76, 0xaf, 0x61, 0x66, + 0x81, 0x90, 0xbd, 0x52, 0xed, 0x49, 0x0e, 0x67, 0x7b, 0x51, 0x5d, 0x01, 0x43, 0x84, + 0xaf, 0x07, 0x21, 0x9c, 0x7c, 0x0e, 0xe7, 0xfc, 0x7b, 0xfc, 0x79, 0xf3, 0x25, 0x64, + 0x4e, 0x4d, 0xf4, 0xc0, 0xd7, 0xdb, 0x08, 0xe9, 0xf0, 0xbd, 0x02, 0x49, 0x43, 0xc7, + 0x05, 0xab, 0xff, 0x89, 0x94, 0xbf, 0xa6, 0x05, 0xcf, 0xbc, 0x7e, 0xd7, 0x46, 0xa7, + 0xd3, 0xf7, 0xc3, 0x7d, 0x9e, 0x8b, 0xdc, 0x43, 0x3b, 0x7d, 0x79, 0xe0, 0x8a, 0x12, + 0xf7, 0x38, 0xa8, 0xf0, 0xdb, 0xdd, 0xfe, 0xf2, 0xf2, 0x65, 0x7e, 0xf3, 0xe4, 0x7d, + 0x1b, 0x0f, 0xd1, 0x1e, 0x6a, 0x13, 0x31, 0x1f, 0xb7, 0x99, 0xc7, 0x9c, 0x64, 0x1d, + 0x9d, 0xa4, 0x3b, 0x33, 0xe7, 0xad, 0x01, 0x2e, 0x28, 0x25, 0x53, 0x98, 0x78, 0x92, + 0x62, 0x27, 0x5f, 0x11, 0x75, 0xbe, 0x84, 0x62, 0xc0, 0x14, 0x91, 0xc4, 0xd8, 0x42, + 0x40, 0x6d, 0x0e, 0xc4, 0x28, 0x2c, 0x95, 0x26, 0x17, 0x4a, 0x09, 0x87, 0x8f, 0xe8, + 0xfd, 0xde, 0x33, 0xa2, 0x96, 0x04, 0xe5, 0xe5, 0xe7, 0xb2, 0xa0, 0x25, 0xd6, 0x65, + 0x0b, 0x97, 0xdb, 0xb5, 0x2b, 0xef, 0xb5, 0x9b, 0x1d, 0x30, 0xa5, 0x74, 0x33, 0xb0, + 0xa3, 0x51, 0x47, 0x44, 0x44, 0x09, 0x9d, 0xaa, 0x37, 0x10, 0x46, 0x61, 0x32, 0x60, + 0xcf, 0x33, 0x54, 0xcf, 0xcd, 0xad, 0xa6, 0x63, 0xec, 0xe8, 0x24, 0xff, 0xd7, 0xe4, + 0x43, 0x93, 0x88, 0x6a, 0x86, 0x16, 0x5d, 0xdd, 0xdf, 0x2b, 0x4c, 0x41, 0x77, 0x35, + 0x54, 0xc8, 0x69, 0x95, 0x26, 0x94, 0x08, 0xb1, 0x1e, 0x67, 0x37, 0xa4, 0xc4, 0x47, + 0x58, 0x6f, 0x69, 0x17, 0x34, 0x46, 0xd8, 0xe4, 0x8b, 0xf8, 0x4c, 0xbc, 0x00, 0x0a, + 0x80, 0x78, 0x99, 0x97, 0x3e, 0xb9, 0x3c, 0x5e, 0x81, 0x9a, 0xad, 0x66, 0x94, 0x13, + 0xf8, 0x38, 0x79, 0x33, 0xad, 0x15, 0x84, 0xaa, 0x35, 0xe4, 0x3f, 0x4e, 0xcd, 0x1e, + 0x2d, 0x04, 0x07, 0xc0, 0xb1, 0xb8, 0x99, 0x20, 0xff, 0xdf, 0xdb, 0x9b, 0xea, 0x51, + 0xac, 0x95, 0xb5, 0x57, 0xaf, 0x71, 0xb8, 0x9f, 0x90, 0x3f, 0x5d, 0x98, 0x48, 0xf1, + 0x4f, 0xcb, 0xeb, 0x18, 0x37, 0x57, 0x0f, 0x54, 0x4d, 0x63, 0x59, 0xeb, 0x23, 0xfa, + 0xf3, 0x8a, 0x08, 0x22, 0xda, 0x36, 0xce, 0x42, 0x6c, 0x4a, 0x2f, 0xbe, 0xff, 0xeb, + 0x0a, 0x8a, 0x2e, 0x29, 0x7a, 0x9d, 0x19, 0xba, 0x15, 0x02, 0x45, 0x90, 0xe3, 0x32, + 0x9d, 0x9f, 0xa9, 0x26, 0x1f, 0x99, 0x38, 0xa4, 0x03, 0x2d, 0xd3, 0x46, 0x06, 0xc9, + 0xcf, 0x9f, 0x3d, 0xd3, 0x3e, 0x57, 0x6f, 0x05, 0xcd, 0x1d, 0xd6, 0x81, 0x1c, 0x62, + 0x98, 0x75, 0x7d, 0x77, 0xd9, 0xe8, 0x10, 0xab, 0xdb, 0x22, 0x6a, 0xfc, 0xaa, 0x43, + 0x46, 0xa6, 0x56, 0x0f, 0x89, 0x32, 0xb3, 0x18, 0x1f, 0xd3, 0x55, 0xd5, 0xd3, 0x91, + 0x97, 0x61, 0x83, 0xf8, 0xd9, 0x93, 0x88, 0x83, 0x96, 0x32, 0xd6, 0x35, 0x4f, 0x66, + 0x6d, 0x09, 0xd3, 0xe5, 0x62, 0x9e, 0xa1, 0x97, 0x37, 0x38, 0x86, 0x13, 0xd3, 0x8a, + 0x34, 0xfd, 0x0f, 0x6e, 0x50, 0xee, 0x5a, 0x0c, 0xc9, 0x67, 0x71, 0x77, 0xf5, 0x00, + 0x28, 0xc1, 0x41, 0x37, 0x81, 0x87, 0xbd, 0x28, 0x19, 0x40, 0x3f, 0xc5, 0x34, 0xf8, + 0x00, 0x76, 0xe9, 0x38, 0x0c, 0xb4, 0x96, 0x4d, 0x3b, 0x6b, 0x45, 0x81, 0x9d, 0x3b, + 0x8e, 0x9c, 0xaf, 0x54, 0xf0, 0x51, 0x85, 0x2d, 0x67, 0x1b, 0xf8, 0xc1, 0xff, 0xde, + 0x2d, 0x15, 0x10, 0x75, 0x64, 0x18, 0xcb, 0x48, 0x10, 0x93, 0x6a, 0xa5, 0x7e, 0x69, + 0x65, 0xd6, 0xfb, 0x65, 0x6a, 0x76, 0x0b, 0x7f, 0x19, 0xad, 0xf9, 0x6c, 0x17, 0x34, + 0x88, 0x55, 0x21, 0x93, 0xb1, 0x47, 0xee, 0x58, 0x85, 0x80, 0x33, 0xda, 0xc7, 0xcd, + 0x0e, 0xb2, 0x04, 0xc0, 0x64, 0x90, 0xbb, 0xde, 0xdf, 0x5f, 0x75, 0x71, 0xac, 0xb2, + 0xeb, 0xe7, 0x6a, 0xce, 0xf3, 0xf2, 0xa0, 0x1e, 0xe9, 0x87, 0x48, 0x6d, 0xfe, 0x6c, + 0x3f, 0x0a, 0x5e, 0x23, 0x4c, 0x12, 0x72, 0x58, 0xf9, 0x7a, 0x28, 0xfb, 0x5d, 0x16, + 0x4a, 0x81, 0x76, 0xbe, 0x94, 0x6b, 0x80, 0x97, 0xd0, 0xe3, 0x17, 0x28, 0x7f, 0x33, + 0xbf, 0x9c, 0x16, 0xf9, 0xa5, 0x45, 0x40, 0x9c, 0xe2, 0x9b, 0x1f, 0x42, 0x73, 0x72, + 0x5f, 0xc0, 0xdf, 0x02, 0xa0, 0x4e, 0xba, 0xe1, 0x78, 0xb3, 0x41, 0x4f, 0xb0, 0xa8, + 0x2d, 0x50, 0xde, 0xb0, 0x9f, 0xcf, 0x4e, 0x6e, 0xe9, 0xd1, 0x80, 0xff, 0x4f, 0x56, + 0xff, 0x3b, 0xc1, 0xd3, 0x60, 0x1f, 0xc2, 0xdc, 0x90, 0xd8, 0x14, 0xc3, 0x25, 0x6f, + 0x49, 0x67, 0xd3, 0xa8, 0xd6, 0x4c, 0x83, 0xfe, 0xa3, 0x39, 0xc5, 0x1f, 0x5a, 0x8e, + 0x58, 0x01, 0xfb, 0xb9, 0x78, 0x35, 0x58, 0x1b, 0x60, 0x24, 0x65, 0xde, 0xe0, 0x4b, + 0x59, 0x22, 0xc2, 0x76, 0x1b, 0x54, 0x24, 0x5b, 0xec, 0x0c, 0x9e, 0xef, 0x2d, 0xb9, + 0x7d, 0x22, 0xb2, 0xb3, 0x55, 0x6c, 0xc9, 0x69, 0xfb, 0xb1, 0x3d, 0x06, 0x50, 0x97, + 0x65, 0xa5, 0x2b, 0x3f, 0xac, 0x54, 0xb9, 0x3f, 0x42, 0x1b, 0xf0, 0x8e, 0x18, 0xd5, + 0x2d, 0xdd, 0x52, 0xcc, 0x1c, 0x8c, 0xa8, 0xad, 0xfa, 0xcc, 0xab, 0x7e, 0x5c, 0xc2, + 0xf4, 0x57, 0x3f, 0xbb, 0xf8, 0x23, 0x9b, 0xb0, 0xb8, 0xae, 0xdb, 0xf8, 0xda, 0xd1, + 0x62, 0x82, 0xda, 0x5c, 0x91, 0x25, 0xdb, 0xa1, 0xc0, 0x59, 0xd0, 0xdf, 0x8a, 0xbf, + 0x62, 0x10, 0x78, 0xf0, 0x2d, 0x6c, 0x4b, 0xc8, 0x6d, 0x40, 0x84, 0x5a, 0xc1, 0xd5, + 0x97, 0x10, 0xc4, 0x5f, 0x07, 0xd5, 0x85, 0xeb, 0x48, 0xb3, 0x2f, 0xc0, 0x16, 0x7b, + 0xa2, 0x56, 0xe7, 0x3c, 0xa3, 0xb9, 0x31, 0x1c, 0x62, 0xd1, 0x09, 0x49, 0x79, 0x57, + 0xd8, 0xdb, 0xe1, 0x0a, 0xa3, 0xe8, 0x66, 0xb4, 0x0c, 0x0b, 0xaa, 0x2b, 0xc4, 0x92, + 0xc1, 0x9a, 0xd1, 0xe6, 0x37, 0x2d, 0x96, 0x22, 0xbf, 0x16, 0x3f, 0xbf, 0xfe, 0xae, + 0xee, 0x79, 0x6a, 0x3c, 0xd9, 0xb6, 0xfb, 0xbf, 0xa4, 0xd7, 0x92, 0xf3, 0x4d, 0x7f, + 0xd6, 0xe7, 0x63, 0xcd, 0x58, 0x59, 0xdd, 0x26, 0x83, 0x3d, 0x21, 0xd9, 0xbc, 0x54, + 0x52, 0xbd, 0x19, 0x51, 0x5d, 0xff, 0x9f, 0x49, 0x95, 0xb3, 0x5b, 0xc0, 0xc1, 0xf8, + 0x76, 0xe6, 0xad, 0x11, 0xf2, 0x45, 0x2d, 0xc9, 0xae, 0x85, 0xae, 0xc0, 0x1f, 0xc5, + 0x6f, 0x8c, 0xbf, 0xda, 0x75, 0xa7, 0x72, 0x7b, 0x75, 0xeb, 0xbd, 0x6b, 0xbf, 0xfb, + 0x43, 0xb6, 0x3a, 0x3b, 0x1b, 0x67, 0x1e, 0x40, 0xfe, 0xb0, 0xdb, 0x00, 0x29, 0x74, + 0xa3, 0xc3, 0xb1, 0xa7, 0x88, 0x56, 0x72, 0x31, 0xbf, 0x63, 0x99, 0xff, 0x89, 0x23, + 0x69, 0x81, 0x14, 0x9d, 0x42, 0x38, 0x02, 0xd2, 0x34, 0x1a, 0x3b, 0xed, 0xb9, 0xdd, + 0xcb, 0xac, 0x1f, 0xe7, 0xb6, 0x43, 0x5e, 0x14, 0x79, 0xc7, 0x2e, 0x70, 0x89, 0xd0, + 0x29, 0xe7, 0xfb, 0xba, 0xf3, 0xcf, 0x37, 0xe9, 0xb9, 0xa6, 0xb7, 0x76, 0x79, 0x1e, + 0x4c, 0x5e, 0x6f, 0xda, 0x57, 0xe8, 0xd5, 0xf1, 0x4c, 0x8c, 0x35, 0xa2, 0xd2, 0x70, + 0x84, 0x6b, 0x9d, 0xbe, 0x00, 0x5c, 0xda, 0x16, 0xaf, 0x44, 0x08, 0xf3, 0xab, 0x06, + 0xa9, 0x16, 0xee, 0xeb, 0x9c, 0x95, 0x94, 0xb7, 0x04, 0x24, 0xa4, 0xc1, 0xd1, 0x71, + 0x29, 0x5b, 0x67, 0x63, 0xb2, 0x2f, 0x47, 0xf8, 0x0b, 0x53, 0xcc, 0xbb, 0x90, 0x4b, + 0xd6, 0x8f, 0xd6, 0x5f, 0xbd, 0x3f, 0xbd, 0xea, 0x10, 0x35, 0xe9, 0x8c, 0x21, 0xa7, + 0xdb, 0xc9, 0x1a, 0x9b, 0x5b, 0xc7, 0x69, 0x0f, 0x05, 0xec, 0x31, 0x7c, 0x97, 0xf8, + 0x76, 0x4e, 0xb4, 0x8e, 0x91, 0x1d, 0x42, 0x8e, 0xc8, 0xd8, 0x61, 0xb7, 0x08, 0xe8, + 0x29, 0x8a, 0xcb, 0x62, 0x15, 0x51, 0x45, 0x15, 0x5a, 0xe9, 0x5f, 0x0a, 0x1d, 0x15, + 0x01, 0x03, 0x47, 0x53, 0x14, 0x6e, 0x22, 0xd0, 0x5f, 0x58, 0x6d, 0x7f, 0x6b, 0x4f, + 0xe1, 0x2d, 0xad, 0x9a, 0x17, 0xf5, 0xdb, 0x70, 0xb1, 0xdb, 0x96, 0xb8, 0xd9, 0xa8, + 0x3e, 0xda, 0xdc, 0x96, 0x6c, 0x8a, 0x54, 0x66, 0xb6, 0x1f, 0xc9, 0x98, 0xc3, 0x1f, + 0x10, 0x70, 0xd9, 0xa5, 0xc9, 0xa6, 0xd2, 0x68, 0xd3, 0x04, 0xfe, 0x6b, 0x8f, 0xd3, + 0xb4, 0x01, 0x03, 0x48, 0x61, 0x1a, 0xbd, 0xcb, 0xd4, 0x9f, 0xe4, 0xf8, 0x5b, 0x62, + 0x3c, 0x78, 0x28, 0xc7, 0x13, 0x82, 0xe1, 0x03, 0x4e, 0xa6, 0x7b, 0xc8, 0xae, 0x97, + 0x40, 0x4b, 0x0c, 0x50, 0xb2, 0xa0, 0x4f, 0x55, 0x9e, 0x49, 0x95, 0x0a, 0xfc, 0xb0, + 0xef, 0x46, 0x2a, 0x2a, 0xe0, 0x24, 0xb0, 0xf0, 0x22, 0x4d, 0xfd, 0x73, 0x68, 0x4b, + 0x88, 0xc7, 0xfb, 0xe9, 0x2d, 0x02, 0xb6, 0x8f, 0x75, 0x9c, 0x47, 0x52, 0x66, 0x3c, + 0xd7, 0xb9, 0x7a, 0x14, 0x94, 0x36, 0x49, 0x30, 0x55, 0x21, 0x32, 0x6b, 0xde, 0x08, + 0x56, 0x30, 0x86, 0x46, 0x29, 0x29, 0x1b, 0xae, 0x25, 0xff, 0x88, 0x22, 0xa1, 0x4c, + 0x4b, 0x66, 0x6a, 0x92, 0x59, 0xad, 0x0d, 0xc4, 0x2a, 0x82, 0x90, 0xac, 0x7b, 0xc7, + 0xf5, 0x3a, 0x16, 0xf3, 0x79, 0xf7, 0x58, 0xe5, 0xde, 0x75, 0x0f, 0x04, 0xfd, 0x7c, + 0xad, 0x47, 0x70, 0x1c, 0x85, 0x97, 0xf9, 0x78, 0x88, 0xbe, 0xa6, 0xfa, 0x0b, 0xf2, + 0x99, 0x99, 0x56, 0xfb, 0xfd, 0x0e, 0xe6, 0x8e, 0xc3, 0x6e, 0x46, 0x88, 0x80, 0x9a, + 0xe2, 0x31, 0xeb, 0x8b, 0xc4, 0x36, 0x9f, 0x5f, 0xe1, 0x57, 0x3f, 0x57, 0xe0, 0x99, + 0xd9, 0xc0, 0x99, 0x01, 0xbf, 0x39, 0xca, 0xac, 0x48, 0xdc, 0x11, 0x95, 0x6a, 0x8a, + 0xe9, 0x05, 0xea, 0xd8, 0x69, 0x54, 0x54, 0x7c, 0x44, 0x8a, 0xe4, 0x3d, 0x31, 0x5e, + 0x66, 0x9c, 0x42, 0x42, 0xda, 0x56, 0x59, 0x38, 0xf4, 0x17, 0xbf, 0x43, 0xce, 0x7b, + 0x2b, 0x30, 0xb1, 0xcd, 0x40, 0x18, 0x38, 0x8e, 0x1a, 0x91, 0x0f, 0x0f, 0xc4, 0x1f, + 0xb0, 0x87, 0x7a, 0x59, 0x25, 0xe4, 0x66, 0x81, 0x9d, 0x37, 0x5b, 0x0a, 0x91, 0x2d, + 0x4f, 0xe8, 0x43, 0xb7, 0x6e, 0xf6, 0xf2, 0x23, 0xf0, 0xf7, 0xc8, 0x94, 0xf3, 0x8f, + 0x7a, 0xb7, 0x80, 0xdf, 0xd7, 0x5f, 0x66, 0x9c, 0x8c, 0x06, 0xcf, 0xfa, 0x43, 0xeb, + 0x47, 0x56, 0x5a, 0x50, 0xe3, 0xb1, 0xfa, 0x45, 0xad, 0x61, 0xce, 0x9a, 0x1c, 0x47, + 0x27, 0xb7, 0xaa, 0xa5, 0x35, 0x62, 0xf5, 0x23, 0xe7, 0x39, 0x52, 0xbb, 0xf3, 0x3d, + 0x8a, 0x41, 0x04, 0x07, 0x8a, 0xde, 0x3e, 0xaa, 0xa4, 0x96, 0x99, 0xa6, 0x9f, 0xdf, + 0x1c, 0x5a, 0xc7, 0x73, 0x21, 0x46, 0xee, 0x5e, 0x1d, 0x6b, 0x6c, 0xa9, 0xb9, 0x18, + 0x0f, 0x96, 0x4c, 0xc9, 0xd0, 0x87, 0x8a, 0xe1, 0x37, 0x35, 0x24, 0xd7, 0xd5, 0x10, + 0xe5, 0x82, 0x27, 0xdf, 0x6d, 0xe9, 0xd3, 0x0d, 0x27, 0x18, 0x67, 0x64, 0x01, 0x77, + 0xb0, 0xf1, 0x85, 0x6e, 0x28, 0xd5, 0xc8, 0xaf, 0xb0, 0x95, 0xef, 0x61, 0x84, 0xfe, + 0xd6, 0x51, 0x58, 0x90, 0x22, 0xee, 0xae, 0xa4, 0xc0, 0xce, 0x1f, 0xa6, 0xf0, 0x85, + 0x09, 0x2b, 0x04, 0x97, 0x94, 0x89, 0x17, 0x2b, 0x3e, 0xf8, 0x19, 0x4a, 0x79, 0x8d, + 0xf5, 0x72, 0x4d, 0x6b, 0x05, 0xf1, 0xae, 0x00, 0x00, 0x13, 0xa0, 0x8d, 0x61, 0x2b, + 0xca, 0x8a, 0x8c, 0x31, 0x44, 0x3c, 0x10, 0x34, 0x6d, 0xbf, 0x61, 0xde, 0x84, 0x75, + 0xc0, 0xbb, 0xec, 0x51, 0x04, 0xb4, 0x75, 0x56, 0xaf, 0x3d, 0x51, 0x44, 0x58, 0xe2, + 0x32, 0x1d, 0x14, 0x60, 0x71, 0x78, 0x9d, 0x23, 0x35, 0x93, 0x4a, 0x68, 0x06, 0x14, + 0xe8, 0x35, 0x62, 0xf8, 0x2d, 0xfd, 0x40, 0x5b, 0x54, 0xa4, 0x5e, 0xb3, 0x2c, 0x16, + 0x54, 0x48, 0xd4, 0xd5, 0xd6, 0x1c, 0xa2, 0x85, 0x95, 0x85, 0x36, 0x9f, 0x53, 0xf1, + 0xa1, 0x37, 0xe9, 0xe8, 0x2b, 0x67, 0xb8, 0xfd, 0xaf, 0x01, 0xbd, 0xa5, 0x4a, 0x31, + 0x73, 0x11, 0x89, 0x6a, 0xe1, 0x02, 0x80, 0xa0, 0x32, 0x44, 0x0c, 0x42, 0x0a, 0x42, + 0x1e, 0x94, 0x4d, 0x1e, 0x95, 0x2b, 0x70, 0xd5, 0x82, 0x6c, 0xd3, 0xb0, 0x8b, 0x7d, + 0xb9, 0x63, 0x0f, 0xe4, 0xfd, 0x5f, 0x22, 0x12, 0x5d, 0xe8, 0x40, 0xfc, 0xc4, 0x0b, + 0x98, 0x03, 0x8a, 0xf1, 0x1d, 0x55, 0xbe, 0x25, 0x43, 0x25, 0x97, 0xb4, 0xb6, 0x5b, + 0x9e, 0xc1, 0xc7, 0xa8, 0xbb, 0xfd, 0x05, 0x2c, 0xbf, 0x7e, 0x1c, 0x17, 0x85, 0x31, + 0x49, 0x34, 0xb2, 0x62, 0xd5, 0x85, 0x37, 0x54, 0xf1, 0xf1, 0x77, 0x71, 0xcf, 0xb7, + 0x50, 0x30, 0x72, 0x65, 0x57, 0x53, 0xfa, 0x3f, 0x54, 0xec, 0xc5, 0x87, 0xe9, 0xf8, + 0x3b, 0x58, 0x19, 0x16, 0x09, 0x2d, 0xf2, 0x6e, 0x63, 0xe1, 0x89, 0x94, 0xcb, 0x0d, + 0xb9, 0x1a, 0x0b, 0xbd, 0xc7, 0xb6, 0x11, 0x9b, 0x32, 0x22, 0x2a, 0xdf, 0x5e, 0x61, + 0xd8, 0xd8, 0xae, 0x89, 0xda, 0xe4, 0x95, 0x4b, 0x54, 0x81, 0x3b, 0xb3, 0x3f, 0x08, + 0xd5, 0x62, 0xba, 0x51, 0x3f, 0xee, 0x1b, 0x09, 0xc0, 0xfc, 0xd5, 0x16, 0x05, 0x54, + 0x19, 0x47, 0x4d, 0xd7, 0xfd, 0xa0, 0x38, 0xa8, 0x9c, 0x84, 0xea, 0x7b, 0x94, 0x68, + 0x28, 0x7f, 0x0e, 0xb0, 0xc1, 0x0c, 0x4b, 0x13, 0x25, 0x20, 0x19, 0x4d, 0x3d, 0x8d, + 0x53, 0x51, 0xfc, 0x10, 0xd0, 0x9c, 0x15, 0xc8, 0xcc, 0x10, 0x1a, 0xa1, 0x66, 0x3b, + 0xbf, 0x17, 0xb8, 0x41, 0x11, 0xf3, 0x8b, 0xb4, 0x39, 0xf0, 0x73, 0x53, 0xbd, 0xea, + 0x35, 0x96, 0xd1, 0x5e, 0x71, 0x3e, 0x1e, 0x2e, 0x7d, 0x3f, 0x1c, 0x38, 0x31, 0x35, + 0xb4, 0x7f, 0xa7, 0xf8, 0x1f, 0x46, 0xdf, 0x7a, 0x90, 0x2a, 0x40, 0x46, 0x99, 0xec, + 0x91, 0x2f, 0x56, 0x56, 0xc3, 0x5b, 0x85, 0x76, 0x3e, 0x4d, 0xe5, 0x83, 0xae, 0xca, + 0xa1, 0xdf, 0xd5, 0xd2, 0x67, 0x7d, 0x9c, 0x8f, 0xfe, 0xe8, 0x77, 0xf6, 0x3f, 0x40, + 0xa5, 0xca, 0x0d, 0x67, 0xf6, 0xe5, 0x54, 0x12, 0x47, 0x39, 0xf8, 0x05, 0xaf, 0x87, + 0x6a, 0xee, 0xde, 0x53, 0xaa, 0x8b, 0x0f, 0x8e, 0x56, 0x04, 0xa7, 0x3c, 0x30, 0xcb, + 0xd0, 0x9d, 0xad, 0x96, 0x3d, 0x6f, 0x8a, 0x5d, 0xcc, 0x40, 0xde, 0xf4, 0x07, 0x97, + 0x34, 0x21, 0x13, 0xba, 0x20, 0x6f, 0xae, 0x8e, 0xbe, 0x4f, 0x3b, 0xc3, 0xca, 0xf6, + 0x92, 0x59, 0xe4, 0x62, 0xef, 0xf9, 0xba, 0x8b, 0x3f, 0x4b, 0xfa, 0xa1, 0x30, 0x0c, + 0x26, 0x92, 0x5a, 0x87, 0x29, 0xcd, 0x32, 0x91, 0x5b, 0xfc, 0x96, 0x60, 0x86, 0xf0, + 0xd5, 0x56, 0x0b, 0xbe, 0x32, 0xa5, 0x98, 0xc2, 0x2a, 0xdf, 0xb4, 0x8c, 0xef, 0x72, + 0xba, 0x5d, 0x42, 0x87, 0xc0, 0xce, 0xfb, 0xac, 0xfd, 0x8c, 0xe1, 0x95, 0xb4, 0x96, + 0x3c, 0x34, 0xa9, 0x4b, 0xba, 0x7a, 0x17, 0x5d, 0xae, 0x4b, 0xbe, 0x3e, 0xf4, 0x86, + 0x3d, 0x53, 0x70, 0x89, 0x15, 0x09, 0x0f, 0x47, 0xa0, 0x68, 0xe2, 0x27, 0x43, 0x3f, + 0x9e, 0x49, 0xd3, 0xaa, 0x09, 0xe3, 0x56, 0xd8, 0xd6, 0x6d, 0x0c, 0x01, 0x21, 0xe9, + 0x1a, 0x3c, 0x4a, 0xa3, 0xf2, 0x7f, 0xa1, 0xb6, 0x33, 0x96, 0xe2, 0xb4, 0x1d, 0xb9, + 0x08, 0xfd, 0xab, 0x8b, 0x18, 0xcc, 0x73, 0x04, 0xe9, 0x4e, 0x97, 0x05, 0x68, 0xf9, + 0x42, 0x1c, 0x0d, 0xbb, 0xba, 0xf8, 0x45, 0x98, 0xd9, 0x72, 0xb0, 0x53, 0x4f, 0x48, + 0xa5, 0xe5, 0x26, 0x70, 0x43, 0x6a, 0xaa, 0x77, 0x6e, 0xd2, 0x48, 0x2a, 0xd7, 0x03, + 0x43, 0x02, 0x01, 0xe5, 0x34, 0x43, 0xc3, 0x6d, 0xcf, 0xd3, 0x4a, 0x0c, 0xb6, 0x63, + 0x78, 0x76, 0x10, 0x5e, 0x79, 0xbf, 0x3b, 0xd5, 0x8e, 0xc1, 0x48, 0xcb, 0x64, 0x97, + 0x0e, 0x32, 0x23, 0xa9, 0x1f, 0x71, 0xdf, 0xcf, 0xd5, 0xa0, 0x4b, 0x66, 0x7f, 0xba, + 0xf3, 0xd4, 0xb3, 0xb9, 0x08, 0xb9, 0x82, 0x88, 0x20, 0xdf, 0xec, 0xdd, 0x75, 0x37, + 0x50, 0xb5, 0xf9, 0xd2, 0x21, 0x6e, 0x56, 0xc6, 0x15, 0x27, 0x2f, 0x85, 0x44, 0x64, + 0xc0, 0xca, 0x4b, 0x1e, 0x85, 0xae, 0xdd, 0x03, 0x82, 0x92, 0xc4, 0xe1, 0xa5, 0x77, + 0x44, 0xeb, 0xba, 0x01, 0x0b, 0x9e, 0xbf, 0xbb, 0x01, 0x1b, 0xd6, 0xf0, 0xb7, 0x88, + 0x05, 0x02, 0x5d, 0x27, 0xf3, 0xc1, 0x77, 0x46, 0xba, 0xe1, 0x16, 0xc1, 0x5d, 0x9f, + 0x47, 0x1f, 0x0f, 0x62, 0x88, 0xa1, 0x50, 0x64, 0x7b, 0x2a, 0xfe, 0x9d, 0xf7, 0xcc, + 0xcf, 0x01, 0xf5, 0xcd, 0xe5, 0xf0, 0x46, 0x80, 0xbb, 0xfe, 0xd8, 0x7f, 0x6c, 0xf4, + 0x29, 0xfb, 0x27, 0xad, 0x6b, 0xab, 0xe7, 0x91, 0x76, 0x66, 0x11, 0xcf, 0x5b, 0xc2, + 0x0e, 0x48, 0xbe, 0xf1, 0x19, 0x25, 0x9b, 0x9b, 0x8a, 0x0e, 0x39, 0xc3, 0xdf, 0x28, + 0xcb, 0x95, 0x82, 0xea, 0x33, 0x86, 0x01, 0xcd, 0xc4, 0x81, 0xb3, 0x2f, 0xb8, 0x2a, + 0xde, 0xeb, 0xb3, 0xda, 0xde, 0x25, 0xd1, 0xa3, 0xdf, 0x20, 0xc3, 0x7e, 0x71, 0x25, + 0x06, 0xb5, 0xd9, 0x96, 0xc4, 0x9a, 0x9f, 0x0f, 0x30, 0xdd, 0xcb, 0x91, 0xfe, 0x90, + 0x04, 0xe1, 0xe8, 0x32, 0x94, 0xa6, 0xc9, 0x20, 0x3d, 0x94, 0xe8, 0xdc, 0x2c, 0xbb, + 0x44, 0x9d, 0xe4, 0x15, 0x50, 0x32, 0x60, 0x4e, 0x47, 0x99, 0x70, 0x16, 0xb3, 0x04, + 0xfd, 0x43, 0x7d, 0x82, 0x35, 0x04, 0x5e, 0x25, 0x5a, 0x19, 0xb7, 0x43, 0xa0, 0xa9, + 0xf2, 0xe3, 0x36, 0xb4, 0x4c, 0xae, 0x30, 0x7b, 0xb3, 0x98, 0x7b, 0xd3, 0xe4, 0xe7, + 0x77, 0xfb, 0xb3, 0x4c, 0x0a, 0xb8, 0xcc, 0x3d, 0x67, 0x46, 0x6c, 0x0a, 0x88, 0xdd, + 0x4c, 0xca, 0xd1, 0x8a, 0x07, 0xa8, 0xd1, 0x06, 0x8d, 0xf5, 0xb6, 0x29, 0xe5, 0x71, + 0x8d, 0x0f, 0x6d, 0xf5, 0xc9, 0x57, 0xcf, 0x71, 0xbb, 0x00, 0xa5, 0x17, 0x8f, 0x17, + 0x5c, 0xac, 0xa9, 0x44, 0xe6, 0x35, 0xc5, 0x15, 0x9f, 0x73, 0x8e, 0x24, 0x02, 0xa2, + 0xd2, 0x1a, 0xa0, 0x81, 0xe1, 0x0e, 0x45, 0x6a, 0xfb, 0x00, 0xb9, 0xf6, 0x24, 0x16, + 0xc8, 0xb9, 0xc0, 0xf7, 0x22, 0x8f, 0x51, 0x07, 0x29, 0xe0, 0xbe, 0x3f, 0x30, 0x53, + 0x13, 0xd7, 0x7f, 0x73, 0x79, 0xdc, 0x2a, 0xf2, 0x48, 0x69, 0xc6, 0xc7, 0x4e, 0xe4, + 0x47, 0x14, 0x98, 0x86, 0x1d, 0x19, 0x2f, 0x0f, 0xf0, 0xf5, 0x08, 0x28, 0x5d, 0xab, + 0x6b, 0x6a, 0x36, 0xcc, 0xf7, 0xd1, 0x22, 0x56, 0xcc, 0x76, 0xb9, 0x55, 0x03, 0x72, + 0x0a, 0xc6, 0x72, 0xd0, 0x82, 0x68, 0xd2, 0xcf, 0x77, 0x73, 0xb6, 0xba, 0x2a, 0x5f, + 0x66, 0x48, 0x47, 0xbf, 0x70, 0x7f, 0x2f, 0xc1, 0x0c, 0x98, 0xf2, 0xf0, 0x06, 0xec, + 0x22, 0xcc, 0xb5, 0xa8, 0xc8, 0xb7, 0xc4, 0x0c, 0x7c, 0x2d, 0x49, 0xa6, 0x63, 0x9b, + 0x9f, 0x2c, 0xe3, 0x3c, 0x25, 0xc0, 0x4b, 0xc4, 0x61, 0xe7, 0x44, 0xdf, 0xa5, 0x36, + 0xb0, 0x0d, 0x94, 0xba, 0xdd, 0xf4, 0xf4, 0xd1, 0x40, 0x44, 0xc6, 0x95, 0xa3, 0x38, + 0x81, 0x47, 0x7d, 0xf1, 0x24, 0xf0, 0xfc, 0xf2, 0x06, 0xa9, 0xfb, 0x2e, 0x65, 0xe3, + 0x04, 0xcd, 0xbf, 0x0c, 0x4d, 0x23, 0x90, 0x17, 0x0c, 0x13, 0x0a, 0xb8, 0x49, 0xc2, + 0xf2, 0x2b, 0x5c, 0xdd, 0x39, 0x21, 0x64, 0x0c, 0x8c, 0xf1, 0x97, 0x6a, 0xe1, 0x01, + 0x0b, 0x0d, 0xfd, 0x9c, 0xb2, 0x54, 0x3e, 0x45, 0xf9, 0x97, 0x49, 0xcc, 0x4d, 0x61, + 0xf2, 0xe8, 0xaa, 0xbf, 0xe9, 0x8b, 0xd9, 0x05, 0xfa, 0x39, 0x95, 0x1b, 0x33, 0xea, + 0x76, 0x9c, 0x45, 0xab, 0x95, 0x31, 0xc5, 0x72, 0x09, 0x86, 0x2a, 0xd1, 0x2f, 0xd7, + 0x6b, 0xa4, 0x80, 0x7e, 0x65, 0x41, 0x7b, 0x6c, 0xd1, 0x2f, 0xa8, 0xec, 0x91, 0x6f, + 0x01, 0x3e, 0xbb, 0x87, 0x06, 0xa9, 0x6e, 0xff, 0xed, 0xa0, 0x6c, 0x4b, 0xe2, 0x4b, + 0x04, 0x84, 0x63, 0x92, 0xe9, 0xd1, 0xe6, 0x93, 0x0e, 0xae, 0x01, 0xfa, 0x21, 0xfb, + 0xd7, 0x00, 0x58, 0x3f, 0xb5, 0x98, 0xb9, 0x2c, 0x8f, 0x4e, 0xb8, 0xa6, 0x1a, 0xa6, + 0x23, 0x5d, 0xb6, 0x0f, 0x28, 0x41, 0xcf, 0x3a, 0x1c, 0x6a, 0xb5, 0x4c, 0x67, 0x06, + 0x68, 0x44, 0x71, 0x1d, 0x09, 0x1e, 0xb9, 0x31, 0xa1, 0xbd, 0x62, 0x81, 0xae, 0xdf, + 0x2a, 0x0e, 0x8f, 0xab, 0x18, 0x81, 0x72, 0x02, 0xa9, 0xbe, 0x06, 0x40, 0x2e, 0xd9, + 0xcc, 0x72, 0x0c, 0x16, 0xbf, 0xe8, 0x81, 0xe4, 0xdf, 0x42, 0x55, 0xe8, 0x7a, 0xfb, + 0x7f, 0xc6, 0x2f, 0x38, 0x11, 0x6b, 0xbe, 0x03, 0xcd, 0x8a, 0x3c, 0xb1, 0x1a, 0x27, + 0xd5, 0x68, 0x41, 0x47, 0x82, 0xf4, 0x7b, 0x1a, 0x44, 0xc9, 0x7c, 0x68, 0x04, 0x67, + 0x69, 0x4b, 0xc9, 0x70, 0x9d, 0x32, 0x91, 0x6c, 0x97, 0xe8, 0x00, 0x6c, 0xbb, 0x07, + 0xba, 0x0e, 0x41, 0x80, 0xa3, 0x73, 0x80, 0x38, 0xc3, 0x74, 0xc4, 0xcc, 0xe8, 0xf3, + 0x29, 0x59, 0xaf, 0xb2, 0x5f, 0x30, 0x3f, 0x58, 0x15, 0xc4, 0x53, 0x31, 0x24, 0xac, + 0xf9, 0xd1, 0x89, 0x40, 0xe7, 0x75, 0x22, 0xac, 0x5d, 0xc4, 0xb9, 0x57, 0x0a, 0xae, + 0x8f, 0x47, 0xb7, 0xf5, 0x7f, 0xd8, 0x76, 0x7b, 0xea, 0x1a, 0x24, 0xae, 0x7b, 0xed, + 0x65, 0xb4, 0xaf, 0xdc, 0x8f, 0x12, 0x78, 0xc3, 0x0e, 0x2d, 0xb9, 0x8f, 0xd1, 0x72, + 0x73, 0x0a, 0xc6, 0xbb, 0xed, 0x4f, 0x11, 0x27, 0xcd, 0x32, 0xb0, 0x4a, 0x95, 0xb2, + 0x05, 0x52, 0x6c, 0xfc, 0xb4, 0xc4, 0xe1, 0xcc, 0x95, 0x51, 0x75, 0xb3, 0xe8, 0xde, + 0x1f, 0x5d, 0x81, 0xb1, 0x86, 0x69, 0x69, 0x23, 0x50, 0xaa, 0xa1, 0xa1, 0xd7, 0x97, + 0x61, 0x75, 0x82, 0xe5, 0x4d, 0x7a, 0x5b, 0x57, 0xa6, 0x83, 0xb3, 0x2f, 0xb1, 0x09, + 0x80, 0x62, 0xda, 0xd7, 0xb0, 0xc2, 0xeb, 0x51, 0x8f, 0x68, 0x62, 0xe8, 0x3d, 0xb2, + 0x5e, 0x3d, 0xba, 0xf7, 0xae, 0xd5, 0x04, 0xde, 0x93, 0x2a, 0xcb, 0x99, 0xd7, 0x35, + 0x99, 0x2c, 0xe6, 0x2b, 0xae, 0x9e, 0xf8, 0x93, 0xff, 0x6a, 0xcc, 0x0f, 0xfc, 0xf8, + 0xe3, 0x48, 0x3e, 0x14, 0x6b, 0x9d, 0x49, 0xdd, 0x8c, 0x78, 0x35, 0xf4, 0x3a, 0x37, + 0xdc, 0xa0, 0x78, 0x7e, 0x3e, 0xc9, 0xf6, 0x60, 0x52, 0x23, 0xd5, 0xba, 0x7a, 0xe0, + 0xab, 0x90, 0x25, 0xb7, 0x3b, 0xc0, 0x3f, 0x7f, 0xac, 0x36, 0xc0, 0x09, 0xa5, 0x6d, + 0x4d, 0x95, 0xd1, 0xe8, 0x1d, 0x3b, 0x3e, 0xbc, 0xa7, 0xe5, 0x4c, 0xc1, 0xa1, 0x2d, + 0x12, 0x7b, 0x57, 0xc8, 0x13, 0x89, 0x76, 0xe7, 0x91, 0x01, 0x3b, 0x01, 0x5f, 0x06, + 0xa6, 0x24, 0xf5, 0x21, 0xb6, 0xee, 0x04, 0xec, 0x98, 0x08, 0x93, 0xc7, 0xe5, 0xe0, + 0x1a, 0x33, 0x62, 0x03, 0x59, 0x40, 0x94, 0xf8, 0x28, 0x33, 0xd7, 0x44, 0x5f, 0xe2, + 0xd0, 0x91, 0x30, 0xf6, 0x35, 0x11, 0xda, 0x54, 0x83, 0x2d, 0xe9, 0x13, 0x6b, 0x39, + 0xf4, 0x59, 0x9f, 0x5a, 0xa5, 0xdf, 0xbb, 0x45, 0xda, 0x60, 0xcd, 0xce, 0xab, 0x7e, + 0xef, 0xde, 0x89, 0xbe, 0x63, 0xf3, 0xf7, 0xc0, 0xd2, 0x32, 0x48, 0x47, 0xcc, 0xe1, + 0x40, 0x5d, 0xef, 0x7c, 0x46, 0x9b, 0x0e, 0x27, 0x24, 0x94, 0xe5, 0xdf, 0x54, 0xf5, + 0x68, 0x65, 0x6c, 0xb9, 0xc8, 0x81, 0x8d, 0x92, 0xb7, 0x2b, 0x8b, 0xc3, 0x4d, 0xb7, + 0xbb, 0x31, 0x12, 0x48, 0x7e, 0x74, 0x6e, 0xef, 0xe4, 0xe8, 0x08, 0xbb, 0xb2, 0x87, + 0xd9, 0x9b, 0xf0, 0x7d, 0x00, 0xda, 0xbe, 0xde, 0xdc, 0x5e, 0x5f, 0x07, 0x4f, 0xfe, + 0xae, 0x0c, 0xba, 0x7d, 0xa3, 0xa5, 0x16, 0xc1, 0x73, 0xbe, 0x1c, 0x51, 0x33, 0x23, + 0xe1, 0x19, 0xf6, 0x35, 0xe8, 0x20, 0x9a, 0x07, 0x4b, 0x21, 0x6b, 0x70, 0x23, 0xfa, + 0xdc, 0x2d, 0x25, 0x94, 0x9c, 0x90, 0x03, 0x7e, 0x71, 0xe3, 0xe5, 0x50, 0x72, 0x6d, + 0x21, 0x0a, 0x2c, 0x68, 0x83, 0x42, 0xe5, 0x24, 0x40, 0x63, 0x5e, 0x9c, 0xc1, 0x4a, + 0xfe, 0x10, 0x10, 0x26, 0x21, 0xa9, 0xc9, 0xac, 0xcb, 0x78, 0x2e, 0x9e, 0x4a, 0x5f, + 0xa8, 0x7f, 0x0a, 0x95, 0x6f, 0x5b, 0x85, 0x50, 0x99, 0x60, 0x28, 0x5c, 0x22, 0x62, + 0x7c, 0x59, 0x48, 0x3a, 0x5a, 0x4c, 0x28, 0xcc, 0xe4, 0xb1, 0x56, 0xe5, 0x51, 0x40, + 0x6a, 0x7e, 0xe8, 0x35, 0x56, 0x56, 0xa2, 0x1e, 0x43, 0xe3, 0x8c, 0xe1, 0x29, 0xfd, + 0xad, 0xb7, 0x59, 0xed, 0xdf, 0xa0, 0x8f, 0x00, 0xfc, 0x8e, 0x56, 0x7c, 0xef, 0x93, + 0xc6, 0x79, 0x2d, 0x01, 0xdf, 0x05, 0xe6, 0xd5, 0x80, 0xf4, 0xd5, 0xd4, 0x8d, 0xf0, + 0x42, 0x45, 0x1a, 0x33, 0x59, 0x0d, 0x3e, 0x8c, 0xf4, 0x9b, 0x26, 0x27, 0x21, 0x8f, + 0x0c, 0x29, 0x2f, 0xa6, 0x6a, 0xda, 0x94, 0x5f, 0xa5, 0x5b, 0xb2, 0x35, 0x48, 0xe3, + 0x3a, 0x83, 0xa5, 0x62, 0x95, 0x7a, 0x31, 0x49, 0xa9, 0x93, 0xcc, 0x47, 0x23, 0x62, + 0x29, 0x87, 0x36, 0xa8, 0xb7, 0x78, 0xd9, 0x7c, 0xe4, 0x23, 0x01, 0x3d, 0x64, 0xb3, + 0x2c, 0xd1, 0x72, 0xef, 0xa5, 0x51, 0xbf, 0x7f, 0x36, 0x8f, 0x04, 0xbd, 0xae, 0xc6, + 0x09, 0x1a, 0x30, 0x04, 0xa7, 0x57, 0x59, 0x8b, 0x80, 0x1d, 0xcf, 0x67, 0x5c, 0xb8, + 0x3e, 0x43, 0xa5, 0x3a, 0xe8, 0xb2, 0x54, 0xd3, 0x33, 0xbc, 0xda, 0x20, 0xd4, 0x81, + 0x7d, 0x34, 0x77, 0xab, 0xfb, 0xa2, 0x5b, 0xb8, 0x3d, 0xf5, 0x94, 0x9c, 0x12, 0x6f, + 0x14, 0x9b, 0x1d, 0x99, 0x34, 0x1e, 0x4e, 0x6f, 0x91, 0x20, 0xf4, 0xd4, 0x1e, 0x62, + 0x91, 0x85, 0x00, 0x2c, 0x72, 0xc0, 0x12, 0xc4, 0x14, 0xd2, 0x38, 0x2a, 0x6d, 0x47, + 0xc7, 0xb3, 0xde, 0xab, 0xa7, 0x70, 0xc4, 0x00, 0xca, 0x96, 0xb2, 0x81, 0x4f, 0x6b, + 0x26, 0xc3, 0xef, 0x17, 0x42, 0x9f, 0x1a, 0x98, 0xc8, 0x5d, 0x83, 0xdb, 0x20, 0xef, + 0xad, 0x48, 0xbe, 0x89, 0x96, 0xfb, 0x1b, 0xff, 0x59, 0x1e, 0xff, 0xf3, 0x60, 0xfe, + 0x11, 0x99, 0x05, 0x6c, 0x56, 0xe5, 0xfe, 0xec, 0x61, 0xa7, 0xb8, 0xb9, 0xf6, 0x99, + 0xd6, 0x01, 0x2c, 0x28, 0x49, 0x23, 0x2f, 0x32, 0x9f, 0xef, 0x95, 0xc7, 0xaf, 0x37, + 0x00, 0x98, 0xff, 0xe4, 0x91, 0x8e, 0x0c, 0xa1, 0xdf, 0x47, 0xf2, 0x75, 0x86, 0x7b, + 0x73, 0x9e, 0x0a, 0x51, 0x4d, 0x32, 0x09, 0x32, 0x5e, 0x21, 0x70, 0x45, 0x92, 0x7b, + 0x47, 0x9c, 0x1c, 0xe2, 0xe5, 0xd5, 0x4f, 0x25, 0x48, 0x8c, 0xad, 0x15, 0x13, 0xe3, + 0xf4, 0x4a, 0x21, 0x26, 0x6c, 0xfd, 0x84, 0x16, 0x33, 0x32, 0x7d, 0xee, 0x6c, 0xf8, + 0x10, 0xfb, 0xf7, 0x39, 0x3e, 0x31, 0x7d, 0x9e, 0x53, 0xd1, 0xbe, 0x1d, 0x5a, 0xe7, + 0x83, 0x9b, 0x66, 0xb9, 0x43, 0xb9, 0xed, 0x18, 0xf2, 0xc5, 0x30, 0xe9, 0x75, 0x42, + 0x23, 0x32, 0xc3, 0x43, 0x9c, 0xce, 0x49, 0xa2, 0x9f, 0x2a, 0x33, 0x6a, 0x48, 0x51, + 0x26, 0x3c, 0x5e, 0x9b, 0xd1, 0x3d, 0x73, 0x11, 0x09, 0xe8, 0x44, 0xb7, 0xf8, 0xc3, + 0x92, 0xa5, 0xc1, 0xdc, 0xaa, 0x2a, 0xe5, 0xf5, 0x0f, 0xf6, 0x3f, 0xab, 0x97, 0x65, + 0xe0, 0x16, 0x70, 0x2c, 0x35, 0xa6, 0x7c, 0xd7, 0x36, 0x4d, 0x3f, 0xab, 0x55, 0x2f, + 0xb3, 0x49, 0xe3, 0x5c, 0x15, 0xc5, 0x02, 0x50, 0x45, 0x3f, 0xd1, 0x8f, 0x7b, 0x85, + 0x59, 0x92, 0x63, 0x2e, 0x2c, 0x76, 0xc0, 0xfb, 0xf1, 0xef, 0x96, 0x3e, 0xa8, 0x0e, + 0x32, 0x23, 0xde, 0x32, 0x77, 0xbc, 0x55, 0x92, 0x51, 0x72, 0x58, 0x29, 0xec, 0x03, + 0xf2, 0x13, 0xba, 0x89, 0x55, 0xca, 0xb2, 0x82, 0x2f, 0xf2, 0x1a, 0x9b, 0x0a, 0x49, + 0x04, 0xd6, 0x68, 0xfc, 0xd7, 0x72, 0x24, 0xbd, 0xe3, 0xdd, 0x01, 0xf6, 0xff, 0xc4, + 0x82, 0x8f, 0x6b, 0x64, 0x23, 0x0b, 0x35, 0xc6, 0xa0, 0x49, 0x87, 0x34, 0x94, 0x27, + 0x6e, 0xa1, 0xd7, 0xed, 0x5e, 0x92, 0xcb, 0x4f, 0x90, 0xba, 0x83, 0xa9, 0xe4, 0x96, + 0x01, 0xb1, 0x94, 0x04, 0x2f, 0x29, 0x00, 0xd9, 0x9d, 0x31, 0x2d, 0x7b, 0x70, 0x50, + 0x8c, 0xf1, 0x76, 0x06, 0x6d, 0x15, 0x4d, 0xbe, 0x96, 0xef, 0x9d, 0x43, 0x67, 0xe4, + 0xc8, 0x40, 0xe4, 0xa1, 0x7b, 0x5e, 0x51, 0x22, 0xe8, 0xeb, 0xe2, 0x15, 0x8a, 0x3c, + 0x5f, 0x4c, 0xba, 0xe2, 0x1e, 0xa3, 0xfa, 0x1a, 0xe6, 0xc2, 0x5a, 0x94, 0x62, 0xeb, + 0xcb, 0xb0, 0xfd, 0x5f, 0x14, 0x55, 0x4b, 0xc9, 0x77, 0x47, 0xc3, 0x3e, 0x34, 0xda, + 0x90, 0xc8, 0x16, 0xd8, 0xd0, 0xd5, 0x0b, 0xfe, 0x37, 0x61, 0x8c, 0x58, 0x12, 0x89, + 0x14, 0x84, 0xfa, 0x25, 0x93, 0x22, 0xc1, 0x50, 0x92, 0xd4, 0x15, 0x5d, 0x86, 0x96, + 0xd6, 0xf1, 0x2f, 0x24, 0xfd, 0x36, 0x44, 0x96, 0xb3, 0xbe, 0x08, 0x71, 0xca, 0x3d, + 0xd9, 0x62, 0x53, 0x48, 0xa6, 0x14, 0xb5, 0x9b, 0xde, 0x45, 0x88, 0x56, 0x49, 0xba, + 0xe3, 0x6d, 0xe3, 0x4d, 0xef, 0x8f, 0xce, 0xc8, 0x53, 0x43, 0x47, 0x5d, 0x97, 0x6a, + 0xe1, 0xe9, 0xb2, 0x78, 0x29, 0xce, 0x2a, 0xc5, 0xef, 0xd0, 0xb3, 0x99, 0xa8, 0xb4, + 0x48, 0xbe, 0x65, 0x04, 0x29, 0x4e, 0xe6, 0xb3, 0xc1, 0xc6, 0xa5, 0x34, 0x2d, 0x7c, + 0x01, 0xae, 0x9d, 0x8a, 0xd3, 0x07, 0x0c, 0x2b, 0x1a, 0x91, 0x57, 0x3a, 0xf5, 0xe0, + 0xc5, 0xe4, 0xcb, 0xbf, 0x4a, 0xcd, 0xc6, 0xb5, 0x4c, 0x92, 0x72, 0x20, 0x0d, 0x99, + 0x70, 0x25, 0x0c, 0x17, 0xc1, 0x03, 0x6f, 0x06, 0x08, 0x5c, 0x41, 0x85, 0x8e, 0xd3, + 0xa0, 0xc4, 0x81, 0x50, 0xbc, 0x69, 0x7e, 0x4a, 0x69, 0x5f, 0xef, 0x33, 0x5f, 0x7a, + 0xd0, 0x7e, 0x1a, 0x46, 0xdc, 0x76, 0x7f, 0xf8, 0x22, 0xdb, 0x70, 0xe6, 0x66, 0x90, + 0x80, 0xb9, 0x81, 0x6b, 0x22, 0x32, 0xc8, 0x1a, 0x4c, 0x66, 0xcc, 0x58, 0x6a, 0xbf, + 0xe1, 0xea, 0xa8, 0xca, 0x6c, 0xf4, 0x1f, 0xc3, 0xc3, 0xe6, 0xc7, 0xb8, 0x86, 0xfb, + 0x6d, 0xac, 0x9f, 0x48, 0x22, 0xb4, 0xfc, 0x6f, 0xff, 0x9d, 0x05, 0x13, 0xd6, 0x1a, + 0x21, 0xc8, 0x0a, 0x37, 0x76, 0x71, 0xd1, 0x35, 0xa6, 0x68, 0xa0, 0xae, 0x2b, 0xb9, + 0x34, 0xc8, 0x2c, 0x41, 0x42, 0xda, 0x69, 0xd1, 0x2c, 0xa7, 0xde, 0x9a, 0x7d, 0xf7, + 0x06, 0x40, 0x0e, 0xc7, 0x98, 0x78, 0xd8, 0x68, 0xe1, 0x7e, 0x8f, 0x71, 0xea, 0x31, + 0x49, 0x5a, 0xf8, 0x19, 0xa0, 0x16, 0xcc, 0x41, 0x9e, 0x07, 0xc5, 0x01, 0xaa, 0x83, + 0x09, 0xb2, 0xe6, 0xc8, 0x5b, 0x79, 0xb2, 0x76, 0x37, 0x33, 0xa3, 0x7b, 0xbc, 0x04, + 0x20, 0xd4, 0x25, 0x37, 0xb8, 0x71, 0xb4, 0x29, 0x4a, 0x65, 0xd3, 0xe0, 0x55, 0xff, + 0x71, 0x8d, 0xd9, 0xdc, 0x8c, 0x75, 0xe7, 0xe5, 0xb2, 0xef, 0xe4, 0x42, 0x63, 0x73, + 0x71, 0xb7, 0xc4, 0x8f, 0x6e, 0xe9, 0x9e, 0x3e, 0xa3, 0x8a, 0x4b, 0x0f, 0x2f, 0x67, + 0xfc, 0x2b, 0x90, 0x8c, 0xda, 0x65, 0x7e, 0xae, 0x75, 0x4e, 0x03, 0x7e, 0x26, 0x2e, + 0x9a, 0x9f, 0x9b, 0xd7, 0xec, 0x42, 0x67, 0xed, 0x8e, 0x96, 0x93, 0x0e, 0x10, 0x84, + 0x78, 0x3c, 0x37, 0xd6, 0xf9, 0xdd, 0x15, 0xfd, 0x29, 0xf4, 0xcc, 0x47, 0x7e, 0x66, + 0xf1, 0x30, 0xd6, 0x30, 0x43, 0x0d, 0xcc, 0x01, 0x04, 0x89, 0x9b, 0x4f, 0x9f, 0x46, + 0xeb, 0x09, 0x0e, 0xf7, 0xfc, 0x90, 0xb4, 0x79, 0xab, 0xf6, 0x1f, 0x93, 0x95, 0x5e, + 0xe0, 0x0e, 0x6a, 0x18, 0x48, 0xf1, 0xab, 0x14, 0xad, 0x33, 0x4f, 0x2b, 0x68, 0x03, + 0x58, 0x08, 0xcd, 0xf1, 0xbb, 0x9e, 0x9d, 0x9a, 0x81, 0x6b, 0xaf, 0x72, 0x8a, 0x95, + 0x5b, 0x96, 0x0b, 0x77, 0x01, 0xfa, 0x62, 0x66, 0x87, 0xdc, 0x3c, 0x9c, 0xba, 0x64, + 0x63, 0x37, 0xb5, 0x3e, 0x29, 0x81, 0x6e, 0x94, 0x82, 0xdd, 0xf5, 0x57, 0x8a, 0x87, + 0x68, 0xaa, 0xe4, 0x77, 0xfc, 0xe4, 0x10, 0xac, 0x2d, 0x5d, 0xe6, 0x09, 0x58, 0x61, + 0xc1, 0x11, 0xd7, 0xfe, 0xb3, 0xe6, 0xbb, 0x4f, 0xbb, 0x5a, 0x54, 0x95, 0x54, 0x95, + 0x97, 0x27, 0x98, 0x35, 0x0a, 0x25, 0x3f, 0x05, 0xf6, 0x6c, 0x2e, 0xcf, 0xcb, 0xc0, + 0xed, 0x43, 0xf5, 0xec, 0x2e, 0x6d, 0x8d, 0xba, 0x15, 0xa5, 0x12, 0x54, 0xd9, 0x7b, + 0x18, 0x21, 0x10, 0x7c, 0x07, 0xdd, 0x9a, 0x16, 0xef, 0x84, 0x06, 0xf9, 0x43, 0xe2, + 0x82, 0xb9, 0x5d, 0x4b, 0x36, 0x25, 0x30, 0xc9, 0x13, 0xd6, 0xba, 0x42, 0x1d, 0xf6, + 0x02, 0x7d, 0xe5, 0xaf, 0x1e, 0x47, 0x45, 0xd5, 0x86, 0x81, 0x06, 0x95, 0x4b, 0xe6, + 0xc1, 0x96, 0x27, 0x80, 0xa2, 0x94, 0x10, 0x72, 0xe9, 0x51, 0x31, 0xb1, 0x67, 0x9d, + 0xf0, 0x63, 0x76, 0x25, 0x04, 0x2c, 0x37, 0xd4, 0x8f, 0xfb, 0x15, 0x2e, 0x5e, 0xbc, + 0x18, 0x5c, 0x8a, 0x2b, 0x7d, 0x43, 0x85, 0xf1, 0xc9, 0x5a, 0xf9, 0x37, 0xdf, 0x78, + 0xdf, 0xd8, 0x75, 0x7f, 0xab, 0x43, 0x49, 0x68, 0xb0, 0xb5, 0x7c, 0x66, 0x57, 0x44, + 0x68, 0xf1, 0x60, 0xb4, 0x47, 0xac, 0x82, 0x21, 0xe5, 0x06, 0x06, 0x76, 0xa8, 0x42, + 0xa1, 0xc6, 0xb7, 0x17, 0x2d, 0xd3, 0x34, 0x0f, 0x76, 0x40, 0x70, 0xab, 0x1f, 0xe0, + 0x91, 0xc5, 0xc7, 0x4c, 0x95, 0xa5, 0xdc, 0x04, 0x33, 0x90, 0x72, 0x3a, 0x4c, 0x12, + 0x7d, 0xa1, 0x4c, 0xdd, 0xe1, 0xdc, 0x26, 0x75, 0xa6, 0x23, 0x40, 0xb3, 0xe6, 0xaf, + 0xd0, 0x52, 0x2a, 0x31, 0xde, 0x26, 0xe7, 0xd1, 0xec, 0x3a, 0x9c, 0x8a, 0x09, 0x1f, + 0xfd, 0xc7, 0x5b, 0x7e, 0xcf, 0xdc, 0x7c, 0x12, 0x99, 0x5a, 0x5e, 0x37, 0xce, 0x34, + 0x88, 0xbd, 0x29, 0xf8, 0x62, 0x9d, 0x68, 0xf6, 0x96, 0x49, 0x24, 0x48, 0xdd, 0x52, + 0x66, 0x97, 0x47, 0x6d, 0xc0, 0x61, 0x34, 0x6e, 0xbe, 0x3f, 0x67, 0x72, 0x17, 0xff, + 0x9c, 0x60, 0xef, 0xce, 0x94, 0x3a, 0xf2, 0x8d, 0xfd, 0x3f, 0x9e, 0x59, 0x69, 0x25, + 0x98, 0xa6, 0x04, 0x7c, 0x23, 0xc4, 0xc0, 0x14, 0x00, 0xf1, 0xab, 0x57, 0x30, 0xea, + 0xc0, 0xae, 0x8d, 0x58, 0x43, 0xd5, 0x05, 0x1c, 0x37, 0x62, 0x40, 0x17, 0x2a, 0xf2, + 0x18, 0xd7, 0xa1, 0xec, 0xfe, 0x65, 0xb4, 0xf7, 0x51, 0x00, 0x63, 0x89, 0x83, 0xc1, + 0x4d, 0xe4, 0x97, 0x47, 0x55, 0xda, 0xde, 0x80, 0x18, 0xc9, 0xb8, 0xf4, 0x54, 0x3f, + 0xb0, 0x95, 0x96, 0x15, 0x13, 0xe6, 0x7c, 0x61, 0xdb, 0xc5, 0x9c, 0x60, 0x7f, 0x9b, + 0x51, 0xf8, 0xd0, 0x9b, 0xdc, 0xad, 0x28, 0xbc, 0xfb, 0x9e, 0x5d, 0x27, 0x44, 0xea, + 0x88, 0x48, 0xb2, 0x62, 0x3a, 0xc0, 0x7f, 0x8e, 0xf6, 0x1a, 0x81, 0xa3, 0x59, 0x10, + 0xb8, 0xa1, 0xba, 0xf3, 0x9a, 0x91, 0x9a, 0x7b, 0x60, 0xbc, 0x60, 0x4d, 0x63, 0x18, + 0x5f, 0x75, 0x92, 0x21, 0xd8, 0x47, 0xcc, 0x54, 0xa2, 0x27, 0x65, 0xa4, 0xc3, 0x34, + 0x75, 0xb5, 0x79, 0x1e, 0x9a, 0xf3, 0x27, 0x1f, 0xc8, 0xd9, 0x35, 0x06, 0x67, 0x09, + 0x0d, 0x81, 0x84, 0xec, 0x50, 0x52, 0x2d, 0x80, 0x4f, 0x23, 0xc4, 0xfb, 0x44, 0xff, + 0xa4, 0x81, 0xbc, 0x92, 0xae, 0x40, 0x8d, 0x1b, 0x9f, 0x2b, 0x13, 0x19, 0x04, 0xf9, + 0x70, 0x5c, 0x59, 0xe2, 0xf4, 0xbd, 0xe7, 0xa3, 0xb2, 0xc0, 0x85, 0xd9, 0x3f, 0xd2, + 0xab, 0xc5, 0xe1, 0x4d, 0x16, 0x30, 0x01, 0xa1, 0x2f, 0x51, 0x93, 0x8d, 0x02, 0x1a, + 0xfa, 0x92, 0x23, 0x9b, 0x87, 0x3d, 0xc6, 0xc3, 0x57, 0xea, 0xa8, 0xaf, 0x4e, 0xe6, + 0xd0, 0x05, 0x40, 0x65, 0x7f, 0xe3, 0x29, 0x14, 0x10, 0x3b, 0x5d, 0x98, 0xf6, 0x8b, + 0xd3, 0xe2, 0xb5, 0x35, 0x9f, 0x08, 0xcc, 0xd8, 0x8d, 0x0c, 0x81, 0x1e, 0x4c, 0x31, + 0xfb, 0xb4, 0x9f, 0x3a, 0x90, 0xbb, 0xd0, 0x5d, 0xce, 0x62, 0xf3, 0x44, 0xe7, 0x07, + 0x75, 0x93, 0x15, 0x9a, 0xe3, 0x50, 0x50, 0xb0, 0x4c, 0x9e, 0x6b, 0x86, 0xbc, 0x43, + 0x2d, 0xc8, 0xb0, 0x48, 0xc7, 0x3c, 0x00, 0x18, 0xca, 0x5b, 0x69, 0x41, 0x12, 0x97, + 0x73, 0x2a, 0x4e, 0x1a, 0xa9, 0x9a, 0x92, 0x8c, 0x71, 0xe7, 0xa2, 0x4f, 0xd2, 0x77, + 0x85, 0x6a, 0xa4, 0x25, 0x01, 0xe5, 0x1b, 0x01, 0x2a, 0xea, 0x94, 0x46, 0xa2, 0x10, + 0x4e, 0x93, 0xf8, 0x15, 0xa0, 0xb3, 0xa2, 0x9b, 0x45, 0x83, 0x14, 0xf3, 0xd8, 0xbe, + 0x2b, 0x98, 0x23, 0xd3, 0x42, 0xf4, 0x62, 0x13, 0xe9, 0x42, 0xa7, 0xe1, 0x9a, 0x46, + 0xe9, 0x70, 0xb5, 0xc5, 0x06, 0x70, 0x84, 0x30, 0x31, 0x7b, 0x1b, 0xb3, 0xb3, 0x5d, + 0xf6, 0x8a, 0xe3, 0x3a, 0x49, 0x26, 0xa0, 0x3e, 0x6b, 0xfe, 0xb5, 0x51, 0x04, 0x16, + 0xfc, 0xbb, 0x05, 0x24, 0xc9, 0xca, 0x50, 0x74, 0x15, 0x6c, 0xc5, 0xa5, 0xd6, 0xfe, + 0x1c, 0x99, 0x5e, 0xdc, 0x60, 0xa2, 0xf5, 0x50, 0x41, 0x1a, 0xa4, 0x1e, 0x3d, 0xa3, + 0xbd, 0xcf, 0x64, 0xbc, 0xf0, 0x4a, 0x05, 0x10, 0x57, 0x1b, 0x93, 0x6d, 0x47, 0xe5, + 0x5c, 0xec, 0x03, 0x30, 0xee, 0x8d, 0xfe, 0x73, 0x56, 0x34, 0x04, 0xf0, 0x47, 0xd7, + 0xf3, 0xa8, 0xa3, 0xd7, 0x74, 0x3b, 0xc5, 0x54, 0x95, 0x52, 0x10, 0xf1, 0xeb, 0x0d, + 0x08, 0x59, 0x9e, 0xa7, 0x7d, 0x5f, 0x97, 0x4d, 0x87, 0x17, 0x6d, 0x37, 0xd9, 0x8b, + 0x9c, 0x0a, 0xd4, 0x40, 0x40, 0x72, 0x09, 0xed, 0x6a, 0x9f, 0x08, 0x46, 0x4d, 0x56, + 0x55, 0x93, 0xe1, 0xa6, 0x3b, 0x93, 0x85, 0x36, 0xb4, 0x92, 0x44, 0xe9, 0x7d, 0x88, + 0x01, 0x73, 0xb6, 0x40, 0xf2, 0xdd, 0xb7, 0x4d, 0x06, 0x8e, 0xcb, 0x46, 0xcf, 0x28, + 0x9b, 0x7d, 0x89, 0x13, 0x07, 0xbb, 0xa3, 0x70, 0x54, 0xcf, 0x91, 0xb3, 0x1f, 0xc8, + 0x2f, 0x74, 0xd5, 0xfc, 0xc0, 0x00, 0x94, 0x2e, 0xde, 0x91, 0x18, 0x25, 0xf5, 0x3f, + 0xe6, 0x09, 0x68, 0x6f, 0x46, 0x32, 0x23, 0xb1, 0xe9, 0xbc, 0x03, 0xbd, 0xe8, 0x95, + 0xd1, 0x23, 0x8f, 0xad, 0x04, 0xa3, 0xbf, 0xce, 0x68, 0xa0, 0x75, 0xe8, 0xa3, 0x7c, + 0x0e, 0x87, 0xbf, 0x46, 0xdd, 0x01, 0x55, 0x45, 0xf9, 0xb4, 0xfb, 0x0e, 0xec, 0x64, + 0x5f, 0xfc, 0xbb, 0xe0, 0xca, 0x5f, 0x8c, 0x56, 0x1b, 0x25, 0x7d, 0x52, 0xd6, 0x02, + 0xd8, 0xc9, 0x4c, 0x50, 0x28, 0x73, 0xa0, 0x1d, 0x92, 0x51, 0xd8, 0xc8, 0x60, 0xc0, + 0x41, 0x52, 0x5b, 0x3b, 0xf4, 0xe3, 0xa2, 0xeb, 0x92, 0x72, 0x81, 0x5c, 0x75, 0x86, + 0x76, 0x84, 0x28, 0xb4, 0xc2, 0xb2, 0x5e, 0x37, 0x45, 0xf0, 0x09, 0xc5, 0xdc, 0xe2, + 0x0b, 0x69, 0xd5, 0xd7, 0xc4, 0x3c, 0xeb, 0x73, 0x6b, 0x68, 0x31, 0xe8, 0xc1, 0x10, + 0xf1, 0x6c, 0xfd, 0xb3, 0xa4, 0x67, 0xe9, 0x41, 0x4c, 0x00, 0xec, 0xf1, 0x37, 0x31, + 0x50, 0x08, 0x94, 0x55, 0x56, 0x78, 0xc4, 0x97, 0xfa, 0xba, 0x9a, 0x95, 0xd0, 0x1c, + 0xc4, 0x64, 0x39, 0x0f, 0xc4, 0xa7, 0x6b, 0xfa, 0x8b, 0x0e, 0x1c, 0x68, 0xa5, 0x25, + 0xd7, 0x06, 0xd6, 0x60, 0x4b, 0x23, 0x30, 0xb6, 0xb3, 0x48, 0x52, 0x15, 0xf6, 0x06, + 0xf1, 0x88, 0x3a, 0x75, 0x15, 0x88, 0xc7, 0xef, 0xa5, 0x06, 0xc3, 0xe8, 0xd0, 0xc6, + 0x01, 0x92, 0xe8, 0x47, 0x6b, 0xd1, 0x17, 0x5d, 0x95, 0x62, 0x08, 0x7b, 0xdb, 0x81, + 0x8e, 0x66, 0x21, 0x62, 0x86, 0xba, 0xfe, 0x47, 0xff, 0x4d, 0xbc, 0xce, 0xd5, 0x14, + 0x44, 0x48, 0x0a, 0x9a, 0x56, 0x73, 0xec, 0xe7, 0xfa, 0xc7, 0x3a, 0x0e, 0xd4, 0x1a, + 0xb0, 0x05, 0x17, 0x53, 0xa7, 0xca, 0xa8, 0x9b, 0xe3, 0x13, 0x9a, 0xfd, 0x97, 0x93, + 0xb3, 0xe0, 0x2f, 0x27, 0xf0, 0x40, 0x04, 0x65, 0x95, 0xac, 0xd4, 0x7b, 0xf1, 0x3f, + 0xd0, 0xda, 0x27, 0xf0, 0x9e, 0xda, 0x48, 0x03, 0x6d, 0x3e, 0xe4, 0x37, 0xf2, 0xee, + 0x8f, 0x86, 0x06, 0xea, 0x97, 0x34, 0x3c, 0x33, 0x58, 0x46, 0x57, 0xf4, 0x6d, 0xba, + 0x99, 0xdb, 0x5c, 0xfe, 0x6c, 0xa1, 0x76, 0xfa, 0xb7, 0xb0, 0xf3, 0xbf, 0xa0, 0xab, + 0x61, 0xe3, 0x40, 0xc3, 0x4e, 0xb9, 0xf1, 0x7c, 0x7e, 0xc2, 0xbe, 0x03, 0xb1, 0x80, + 0xf0, 0xbb, 0x6f, 0x43, 0x4c, 0x2a, 0x65, 0x42, 0xe0, 0x0e, 0x84, 0x37, 0x3f, 0x4f, + 0x46, 0x49, 0xcd, 0xa3, 0x2b, 0xf6, 0x86, 0x66, 0x61, 0x43, 0xf6, 0x22, 0xaa, 0x48, + 0x04, 0x60, 0xb5, 0xaf, 0xac, 0x51, 0x86, 0x07, 0xcd, 0x9a, 0xf8, 0xbc, 0xd6, 0xb5, + 0x8c, 0x30, 0x12, 0x73, 0x16, 0xb2, 0x5d, 0x5e, 0xa7, 0xbf, 0x6b, 0x0c, 0xab, 0x85, + 0x42, 0xff, 0x69, 0xd9, 0xb2, 0xf1, 0x80, 0xbe, 0x12, 0xed, 0x75, 0x34, 0x4a, 0x39, + 0x5a, 0xa1, 0x0f, 0x85, 0x2f, 0x08, 0x3a, 0xd6, 0x4e, 0xf4, 0x0e, 0x9c, 0x03, 0x09, + 0xe9, 0xbb, 0xa5, 0x4b, 0x8c, 0xb3, 0x3c, 0x95, 0x49, 0x8a, 0x69, 0x53, 0x8d, 0x3a, + 0xe5, 0xb2, 0x5e, 0x24, 0x70, 0x98, 0x30, 0x6f, 0xa8, 0xc7, 0x4a, 0x8e, 0xe5, 0xbc, + 0xa9, 0x41, 0x53, 0x1d, 0x61, 0xaa, 0xc2, 0x7a, 0xab, 0x3d, 0xc5, 0x61, 0x7d, 0x56, + 0x06, 0xc9, 0x57, 0x7a, 0x2a, 0x83, 0x46, 0xe8, 0xd8, 0x5b, 0x32, 0xb8, 0x50, 0x57, + 0x75, 0x10, 0x8d, 0xc8, 0x5e, 0x2a, 0xde, 0x2e, 0xac, 0x1e, 0x63, 0x6e, 0x1a, 0xf4, + 0x05, 0x4c, 0x8b, 0x6f, 0x57, 0x63, 0x2d, 0xf2, 0x69, 0xc3, 0x72, 0x3b, 0x32, 0x08, + 0x72, 0xe4, 0xc5, 0x7b, 0x21, 0x83, 0x58, 0xdc, 0x7e, 0x99, 0x05, 0xbb, 0x04, 0xed, + 0xf9, 0x2e, 0xdf, 0x0d, 0xf6, 0x35, 0xf3, 0xbf, 0x36, 0x1e, 0x57, 0xa1, 0x32, 0x96, + 0xe1, 0x44, 0x7a, 0xf5, 0x08, 0x78, 0x72, 0xd6, 0x36, 0xe2, 0x75, 0x18, 0xa9, 0x87, + 0x6e, 0x15, 0xeb, 0x01, 0xf5, 0xe8, 0xde, 0xd8, 0x18, 0x92, 0x51, 0x1c, 0xc2, 0x85, + 0x1b, 0x00, 0xb8, 0x32, 0x71, 0x2a, 0x6d, 0x3b, 0xa5, 0x66, 0x65, 0x17, 0xbc, 0xd3, + 0x56, 0x76, 0x21, 0xa7, 0xcf, 0x84, 0x45, 0x58, 0x96, 0x53, 0x26, 0x20, 0x20, 0xc3, + 0x3b, 0xf7, 0x80, 0x31, 0xb8, 0xee, 0x07, 0x07, 0xde, 0x07, 0x20, 0x68, 0xc1, 0x70, + 0x57, 0x03, 0x27, 0xe6, 0xd9, 0xf5, 0xc6, 0xdd, 0xc3, 0x35, 0x40, 0x2e, 0xfc, 0x54, + 0x88, 0x62, 0xf5, 0xa0, 0x70, 0x94, 0xfd, 0x42, 0x8a, 0x7b, 0xbc, 0x15, 0xd7, 0xb3, + 0x8d, 0x05, 0x36, 0x2c, 0x9c, 0xa9, 0x85, 0xf5, 0x8a, 0x76, 0x64, 0x7d, 0x2b, 0xe4, + 0xc2, 0xcd, 0x6b, 0x3d, 0x17, 0xd6, 0x87, 0x09, 0x71, 0xd7, 0xa0, 0x98, 0xba, 0xf7, + 0x2c, 0x6f, 0x6f, 0x12, 0x14, 0xcf, 0x1f, 0xaa, 0xe4, 0x88, 0xbd, 0x7d, 0xe2, 0x59, + 0xd3, 0x41, 0x5c, 0x2f, 0x0d, 0xde, 0xc7, 0x45, 0x70, 0x04, 0xf3, 0x57, 0x08, 0xd1, + 0xec, 0xcc, 0xcc, 0x0d, 0xf6, 0x5a, 0x04, 0x94, 0x3a, 0xd5, 0xcb, 0xc1, 0x3f, 0x29, + 0x5f, 0x00, 0x0f, 0xe0, 0x56, 0xc4, 0x0b, 0x2d, 0x88, 0xf2, 0x7d, 0xc3, 0x4c, 0xfe, + 0xb8, 0x03, 0xbe, 0x34, 0x83, 0xa9, 0xeb, 0xf9, 0xb5, 0xa9, 0x02, 0x60, 0x57, 0x72, + 0x5d, 0x63, 0xea, 0xd2, 0xc0, 0xc0, 0xff, 0x1f, 0xe2, 0x6a, 0xc1, 0xe7, 0xbd, 0xfc, + 0xd6, 0xfa, 0xd8, 0x75, 0x84, 0x2d, 0x19, 0x4f, 0x33, 0x17, 0x50, 0x46, 0x2c, 0x06, + 0xb8, 0xd7, 0x98, 0x2d, 0x67, 0x99, 0x5e, 0xd5, 0xd3, 0xae, 0x96, 0xa0, 0x5a, 0xe0, + 0x06, 0x7f, 0x4e, 0xb1, 0xc7, 0xc9, 0x32, 0x31, 0xbd, 0x39, 0x77, 0x3c, 0xbe, 0x0a, + 0x9d, 0x66, 0xb0, 0xc9, 0xaa, 0x8c, 0xff, 0x6a, 0x37, 0x6e, 0x1f, 0x37, 0x2e, 0xac, + 0x6a, 0xc4, 0xe4, 0x6c, 0xc0, 0x94, 0x22, 0x45, 0xd4, 0xc2, 0xdc, 0xf0, 0x2d, 0x76, + 0x40, 0xff, 0xcc, 0x5a, 0x6a, 0xc3, 0xa8, 0x7f, 0x5c, 0x41, 0x15, 0x51, 0xbc, 0xc2, + 0xf2, 0x6c, 0xb9, 0x49, 0x61, 0xd5, 0x3f, 0x95, 0xdd, 0xb1, 0x9a, 0xe9, 0x30, 0xc8, + 0xd7, 0x0f, 0x03, 0x1b, 0x29, 0xa5, 0xdf, 0x99, 0xff, 0x36, 0x69, 0x5e, 0x80, 0x2c, + 0xbc, 0xb6, 0xb5, 0x8c, 0x1b, 0xa7, 0xed, 0x5e, 0xac, 0xfa, 0x76, 0x41, 0x4a, 0x41, + 0xad, 0x4a, 0x44, 0xf7, 0x1f, 0x1b, 0x58, 0x0d, 0x34, 0xc3, 0xa9, 0x52, 0x92, 0x0b, + 0x25, 0x4a, 0x14, 0x5f, 0xea, 0x51, 0x7f, 0x5b, 0x42, 0xb2, 0xf6, 0x5e, 0xcd, 0x0f, + 0x82, 0x59, 0x54, 0x78, 0xd8, 0x0a, 0xe5, 0xc8, 0xce, 0xea, 0x12, 0xa1, 0x61, 0xcc, + 0xbb, 0x5e, 0xac, 0x09, 0x99, 0x0f, 0xc6, 0x19, 0xa4, 0x60, 0x80, 0x43, 0x6d, 0xbd, + 0x08, 0xd7, 0x47, 0x84, 0xaf, 0x00, 0x2d, 0x58, 0xe0, 0x6f, 0xaf, 0x7f, 0x3c, 0xea, + 0xe7, 0xd3, 0x41, 0x9b, 0x1f, 0xca, 0x26, 0x5a, 0x55, 0x59, 0xcf, 0x9e, 0x2d, 0x3b, + 0x60, 0x97, 0x8d, 0x81, 0xa6, 0x78, 0xb9, 0xed, 0x8e, 0x44, 0x86, 0xb4, 0xd1, 0x46, + 0x09, 0xd6, 0xc1, 0x27, 0xc0, 0xc2, 0xfb, 0xff, 0xe3, 0x0a, 0x60, 0xf7, 0xbf, 0xf1, + 0xd9, 0xfb, 0x83, 0x00, 0xed, 0x00, 0x92, 0x53, 0xba, 0x9b, 0x99, 0x6f, 0xa0, 0x52, + 0x41, 0xb1, 0x0f, 0x5a, 0xc9, 0xa8, 0x40, 0x8e, 0x92, 0x5b, 0x62, 0x6b, 0xb2, 0x1a, + 0x47, 0x1f, 0xe3, 0xbe, 0xde, 0x52, 0xbb, 0xa0, 0x97, 0xb2, 0xa9, 0x9a, 0x9b, 0xa5, + 0xa8, 0x66, 0x58, 0xc3, 0xfd, 0x9e, 0xc5, 0x5b, 0xfa, 0x9b, 0x32, 0x85, 0x67, 0x25, + 0x4a, 0xb3, 0x6d, 0x2c, 0x7f, 0x44, 0xd2, 0xc7, 0xe1, 0x3e, 0xb5, 0x4b, 0xeb, 0x70, + 0xea, 0x8f, 0xa9, 0x4b, 0x6c, 0x6e, 0x01, 0x2d, 0x79, 0xe3, 0xf5, 0x36, 0x89, 0xc2, + 0xb1, 0xa1, 0x8e, 0xaf, 0x2d, 0x47, 0x1d, 0x13, 0xc1, 0xab, 0x39, 0xd9, 0x19, 0x4a, + 0xe8, 0x43, 0xab, 0x1d, 0x28, 0xff, 0xa8, 0xf6, 0x9d, 0xc7, 0xe1, 0x5c, 0xc3, 0x8b, + 0x12, 0xe8, 0xfc, 0xd7, 0x92, 0x55, 0xb7, 0x21, 0x60, 0x56, 0xd9, 0xed, 0xb7, 0x48, + 0x2f, 0xb9, 0x8a, 0xa0, 0x33, 0xb6, 0x5e, 0x51, 0xc1, 0xa0, 0x8b, 0x8a, 0x11, 0xd8, + 0x4d, 0x04, 0x09, 0xb7, 0x34, 0xf4, 0x52, 0xaa, 0xf0, 0xd6, 0xb1, 0x8f, 0x50, 0x25, + 0x86, 0x83, 0xd3, 0xf9, 0xa7, 0x6d, 0x39, 0x9f, 0xd0, 0x47, 0xee, 0xe2, 0x88, 0xbb, + 0x45, 0x85, 0x85, 0x1d, 0xc9, 0x3e, 0xcc, 0xc6, 0x23, 0x22, 0x92, 0x4c, 0xd1, 0x3b, + 0x5d, 0xd4, 0xee, 0xd6, 0x6e, 0xd8, 0xd9, 0x97, 0x2d, 0x77, 0x26, 0x29, 0xea, 0x64, + 0x74, 0x2e, 0x54, 0x73, 0x39, 0x81, 0xb0, 0x06, 0xc0, 0x62, 0x46, 0x8e, 0x4b, 0xd8, + 0xf7, 0xdd, 0x9a, 0xf6, 0x98, 0xf5, 0x2a, 0xe8, 0x14, 0x63, 0x4e, 0x81, 0xd7, 0xf3, + 0xe0, 0xc4, 0x20, 0x31, 0x7c, 0xac, 0xa9, 0xae, 0x48, 0x11, 0xc6, 0xaf, 0x06, 0xfe, + 0x80, 0xa8, 0xc0, 0x2a, 0xb7, 0xa0, 0x0e, 0x18, 0xe4, 0xa6, 0xaa, 0x1e, 0xa1, 0xb7, + 0x69, 0x45, 0xd2, 0x61, 0x5d, 0x43, 0xac, 0x11, 0x8b, 0x56, 0xc2, 0xf2, 0x96, 0x0f, + 0xe9, 0x3a, 0x02, 0x5f, 0x13, 0xec, 0x91, 0xff, 0xc6, 0xd2, 0xc3, 0x53, 0x69, 0x9a, + 0xbb, 0x09, 0x2d, 0xed, 0xc0, 0x65, 0xdb, 0x8f, 0xa2, 0x14, 0xdb, 0xc4, 0x64, 0x66, + 0xf8, 0x97, 0xb8, 0x8c, 0x58, 0xb3, 0x01, 0x52, 0x13, 0x3a, 0xa3, 0x83, 0x1a, 0xf3, + 0x7c, 0x74, 0xd9, 0x9e, 0x9e, 0x36, 0xff, 0x70, 0x11, 0xd3, 0x23, 0x83, 0x05, 0x69, + 0x15, 0x08, 0xa2, 0xc3, 0xa4, 0x3e, 0x75, 0x5d, 0xc0, 0x81, 0xb5, 0x11, 0xd6, 0x48, + 0x2a, 0x7d, 0xb6, 0x5f, 0xa9, 0x69, 0x9e, 0xa8, 0x7f, 0xf4, 0x70, 0x99, 0xed, 0x36, + 0x37, 0xdb, 0xb0, 0xa3, 0xd0, 0xef, 0x79, 0x79, 0x6a, 0x8e, 0xf1, 0xe4, 0xd9, 0x4d, + 0x42, 0xb4, 0xbc, 0x2b, 0x4a, 0x03, 0x8a, 0xe6, 0xe4, 0x6b, 0x24, 0xcf, 0xc8, 0x41, + 0x53, 0xd3, 0x1e, 0xaf, 0x89, 0x50, 0x63, 0xa5, 0xca, 0x95, 0x9b, 0xe6, 0x3f, 0x37, + 0xf2, 0xba, 0x0d, 0x43, 0x23, 0x66, 0x73, 0x6d, 0x86, 0x32, 0xfc, 0xe0, 0x72, 0xb6, + 0xae, 0x5b, 0x6f, 0x3f, 0xd5, 0x9d, 0x3f, 0xaf, 0xf6, 0x38, 0x27, 0x5a, 0x99, 0x2f, + 0xef, 0xc8, 0x7e, 0x60, 0xd4, 0x4c, 0x2c, 0xad, 0xc2, 0xb5, 0xc4, 0x94, 0xe3, 0xe7, + 0x2e, 0xb4, 0x59, 0x7c, 0x96, 0xb4, 0x01, 0x67, 0x79, 0x9a, 0x90, 0x01, 0xa2, 0xed, + 0x36, 0x76, 0xa8, 0xb4, 0x03, 0xae, 0x25, 0xff, 0xd7, 0x72, 0xf7, 0x08, 0x1e, 0x9a, + 0x32, 0xbc, 0xc1, 0xc5, 0xe2, 0xed, 0xd4, 0xe2, 0xa6, 0x57, 0x6b, 0x78, 0x3c, 0xce, + 0x3a, 0xae, 0x11, 0xfa, 0x43, 0x22, 0x62, 0x54, 0x88, 0x56, 0x18, 0x3e, 0xe6, 0x82, + 0xd5, 0xdc, 0x31, 0xbe, 0xb3, 0x8f, 0x06, 0x1c, 0xbd, 0xec, 0xa7, 0x02, 0x1a, 0x44, + 0x4e, 0x2d, 0xd4, 0x17, 0xdf, 0x26, 0xdc, 0xd2, 0x20, 0xf2, 0xb7, 0x31, 0x77, 0x2b, + 0x43, 0x9e, 0x96, 0xd6, 0x14, 0xe1, 0xfa, 0xcb, 0x48, 0x6c, 0x7a, 0x7d, 0x51, 0x71, + 0xb1, 0xde, 0x35, 0x9f, 0x6a, 0xd3, 0xa9, 0x6f, 0x64, 0x9c, 0x96, 0x91, 0x02, 0xa1, + 0x96, 0x4f, 0xb4, 0xb4, 0xa1, 0xa4, 0x27, 0x9c, 0x68, 0xe6, 0xc3, 0x72, 0xe4, 0x21, + 0x87, 0xd7, 0x54, 0xe8, 0x04, 0xa6, 0x16, 0x53, 0x09, 0x20, 0x69, 0xfb, 0x9b, 0x6d, + 0x25, 0x26, 0x68, 0x90, 0x80, 0x8b, 0x01, 0x5d, 0xf2, 0x8c, 0x80, 0x10, 0x65, 0xda, + 0x6f, 0xeb, 0xdc, 0x1a, 0x56, 0xbf, 0xd0, 0x02, 0x62, 0x5a, 0xcf, 0xaa, 0x53, 0x73, + 0xfd, 0xe1, 0x49, 0xc1, 0xcf, 0xc3, 0x64, 0x9b, 0x48, 0x69, 0x69, 0x6d, 0x44, 0xec, + 0xb1, 0x24, 0x79, 0xc5, 0xeb, 0xef, 0x99, 0x5f, 0x10, 0x02, 0x9f, 0x8b, 0x53, 0x0e, + 0xeb, 0x3f, 0xdc, 0x2e, 0x50, 0xe8, 0x75, 0x7f, 0xc0, 0xbb, 0x9e, 0x26, 0x30, 0x23, + 0xdb, 0x82, 0xf8, 0x78, 0xd9, 0xac, 0x7f, 0xfb, 0x0b, 0xd4, 0x39, 0x1d, 0xf1, 0xd8, + 0x79, 0x89, 0x9a, 0x3e, 0xf5, 0x7b, 0xfd, 0x0d, 0x1f, 0x77, 0x55, 0x64, 0x8e, 0xdd, + 0x85, 0xbb, 0x05, 0x2a, 0x6e, 0xdf, 0x71, 0xcd, 0x26, 0x28, 0xc9, 0x87, 0x42, 0x9f, + 0x36, 0xdc, 0x50, 0x5c, 0xcc, 0x43, 0xf3, 0x0e, 0x7a, 0x86, 0x9c, 0x9e, 0x25, 0x5e, + 0x2a, 0xf9, 0xfc, 0xf3, 0x0c, 0x12, 0x17, 0x96, 0xd1, 0x90, 0x00, 0x09, 0x60, 0xcb, + 0x6f, 0xe2, 0xf1, 0xbf, 0x24, 0x61, 0x18, 0xb4, 0x98, 0xf3, 0x24, 0x7f, 0x9d, 0x48, + 0x4c, 0x73, 0xcf, 0x09, 0x39, 0x30, 0x39, 0xe4, 0x53, 0x26, 0xb8, 0xff, 0xff, 0xb3, + 0xe7, 0xe6, 0x15, 0x9c, 0x46, 0x69, 0x9f, 0x10, 0x07, 0x92, 0xd4, 0x67, 0x29, 0x50, + 0x34, 0x8a, 0x90, 0x55, 0x2e, 0x45, 0x94, 0x3b, 0xee, 0xac, 0xf0, 0x3f, 0x32, 0x16, + 0xf9, 0x4e, 0x27, 0x4d, 0x63, 0xd6, 0x37, 0xd9, 0xf1, 0x90, 0xe8, 0xa2, 0x66, 0xcd, + 0xee, 0xf1, 0x53, 0x53, 0x0b, 0xee, 0x5c, 0xb8, 0x35, 0x52, 0x60, 0x50, 0x5c, 0x2c, + 0x2e, 0x5d, 0x99, 0x0f, 0xff, 0xdc, 0x34, 0xec, 0x0f, 0xf7, 0xf1, 0xaf, 0x81, 0xb2, + 0x4c, 0xed, 0x0e, 0xfa, 0x62, 0x13, 0xda, 0x6c, 0x7c, 0x60, 0xc4, 0x87, 0xf5, 0xf7, + 0xb0, 0x3f, 0x81, 0x60, 0xa0, 0x57, 0xf4, 0x6d, 0x05, 0xbf, 0x82, 0x18, 0xb3, 0xad, + 0xd9, 0xc0, 0x68, 0x93, 0xbd, 0x02, 0xdb, 0x9b, 0x61, 0x19, 0x1d, 0xfb, 0x13, 0x3b, + 0xfa, 0xbe, 0x48, 0x58, 0xe4, 0x7a, 0x4c, 0xc3, 0x2e, 0x41, 0x6e, 0xc0, 0x8b, 0x8a, + 0xc7, 0x91, 0x5a, 0x43, 0x73, 0x3f, 0x44, 0x06, 0xe9, 0xd9, 0x67, 0xc5, 0x60, 0xf3, + 0x44, 0xd7, 0xe9, 0x04, 0xa2, 0x80, 0x45, 0xd9, 0x9f, 0x3a, 0xf8, 0xc8, 0x2e, 0x97, + 0xe1, 0xb9, 0xc1, 0xb2, 0x05, 0xe5, 0x85, 0xfb, 0xeb, 0xb4, 0x8f, 0xaf, 0x58, 0xf1, + 0xb6, 0x5d, 0xca, 0x24, 0x97, 0xe0, 0x9a, 0x70, 0xaa, 0xd4, 0x86, 0x5f, 0x85, 0x71, + 0x5a, 0x28, 0x0e, 0x18, 0x6f, 0x3f, 0xc1, 0x74, 0x0d, 0x81, 0x84, 0xd3, 0x3e, 0x83, + 0x22, 0x16, 0x95, 0x21, 0xcd, 0xc1, 0x32, 0x21, 0x29, 0x39, 0xc8, 0x4a, 0x10, 0x89, + 0x64, 0xe2, 0xde, 0x74, 0xb6, 0xea, 0x55, 0xb4, 0xcb, 0x8f, 0x6f, 0x9b, 0xee, 0x98, + 0xb1, 0x0d, 0x41, 0x51, 0x09, 0x45, 0x5f, 0x48, 0xb7, 0x76, 0x08, 0x2d, 0xc3, 0x0b, + 0x4b, 0xc7, 0x34, 0x77, 0x07, 0x55, 0x11, 0x70, 0x03, 0x08, 0x15, 0x8c, 0xe2, 0xf2, + 0xf9, 0xbf, 0x0f, 0x69, 0x1b, 0x2c, 0xe5, 0x3e, 0x61, 0x14, 0x2c, 0xb7, 0x40, 0xc1, + 0x5b, 0x7b, 0x62, 0x3c, 0xf4, 0x8b, 0x3f, 0x7b, 0xfe, 0xfa, 0x31, 0xbc, 0xdc, 0x66, + 0x5c, 0x6d, 0x71, 0x23, 0xe9, 0x53, 0x50, 0x81, 0x13, 0x75, 0x94, 0x7b, 0x05, 0x5a, + 0x43, 0xdb, 0x07, 0xe0, 0x3f, 0x33, 0x62, 0x7d, 0xf5, 0xc6, 0x38, 0xbf, 0xad, 0x95, + 0x6d, 0xdc, 0x1e, 0xa7, 0xd7, 0x62, 0x0a, 0x20, 0xf2, 0x79, 0x2f, 0x63, 0x81, 0x7a, + 0x1c, 0xf3, 0x25, 0x80, 0xd0, 0x42, 0x74, 0x23, 0x4a, 0xf2, 0xa5, 0x1b, 0x56, 0xbb, + 0x68, 0xa2, 0x9e, 0x43, 0xa9, 0x54, 0x14, 0x2b, 0xa4, 0xca, 0x68, 0x23, 0xbd, 0xe9, + 0x05, 0x3d, 0x72, 0xfd, 0xad, 0xbc, 0x61, 0xad, 0x59, 0x36, 0xc5, 0x3f, 0xdd, 0x75, + 0x79, 0x44, 0x6d, 0x11, 0xc4, 0x46, 0x07, 0xf4, 0x16, 0x30, 0xe4, 0xc0, 0x89, 0x15, + 0xe6, 0x31, 0x77, 0x15, 0x50, 0xe9, 0xce, 0x1f, 0xca, 0x2c, 0x63, 0xfe, 0x06, 0xb7, + 0x98, 0x9d, 0x58, 0x4f, 0xa7, 0xd7, 0x82, 0xa8, 0x8c, 0x1e, 0x7d, 0x64, 0xb6, 0xfb, + 0xf5, 0x5e, 0x35, 0x96, 0xaf, 0x9b, 0xcb, 0x75, 0x85, 0xf8, 0xc7, 0xd3, 0xaa, 0x5c, + 0x20, 0x82, 0xb2, 0x65, 0x24, 0x9d, 0xf0, 0x57, 0x01, 0xda, 0xb0, 0x31, 0xc4, 0xba, + 0xc1, 0xea, 0x26, 0x7a, 0x29, 0x96, 0xa2, 0x02, 0x8d, 0x1e, 0x6a, 0x0f, 0x80, 0xa3, + 0x84, 0x7c, 0x53, 0x1d, 0xba, 0x96, 0xee, 0x65, 0xa2, 0x41, 0x89, 0xbd, 0x27, 0x12, + 0xe4, 0x0e, 0x95, 0x96, 0x64, 0x98, 0x1e, 0x58, 0xb2, 0xa4, 0xf9, 0x51, 0xef, 0x8f, + 0x49, 0x7d, 0xff, 0xf2, 0xf2, 0xf2, 0x71, 0xea, 0xb8, 0x9c, 0x62, 0x8e, 0x18, 0xb5, + 0xfc, 0xb4, 0x38, 0x82, 0x53, 0x7e, 0xaf, 0x6a, 0xd2, 0xa6, 0xb1, 0x75, 0x46, 0x33, + 0xca, 0xa8, 0x6b, 0xf2, 0xc7, 0x6f, 0x39, 0x93, 0x15, 0x4f, 0xc7, 0x3e, 0x6f, 0xbb, + 0xa2, 0x21, 0x0c, 0x27, 0x43, 0xf5, 0x30, 0xa4, 0x27, 0x84, 0x9a, 0x30, 0x1e, 0x00, + 0xe0, 0x11, 0x29, 0xf0, 0x3a, 0x46, 0x07, 0xf8, 0x7c, 0xbe, 0x07, 0x62, 0xc0, 0xb1, + 0xc6, 0x58, 0x55, 0xde, 0xba, 0x84, 0x22, 0xca, 0x4b, 0x88, 0xab, 0xee, 0xa6, 0xa4, + 0x38, 0x2c, 0xf1, 0x6c, 0xcd, 0x6d, 0xc7, 0xc3, 0x7c, 0x44, 0xe5, 0x49, 0xc4, 0x53, + 0x48, 0x19, 0xac, 0xd8, 0xbb, 0x0a, 0x02, 0xa5, 0xfa, 0x7a, 0x1c, 0x1d, 0x38, 0x06, + 0xfb, 0xc3, 0x40, 0x7f, 0xd7, 0xda, 0x93, 0xfd, 0x0d, 0xe6, 0x40, 0x0d, 0x3a, 0xb8, + 0x97, 0x74, 0x85, 0xcd, 0xdf, 0xbe, 0xd5, 0x93, 0x2f, 0x50, 0x7b, 0x79, 0x94, 0x7a, + 0xdb, 0x2f, 0xad, 0x37, 0x61, 0x5a, 0xa7, 0x17, 0xdb, 0x5f, 0x29, 0x80, 0x99, 0xf2, + 0x0f, 0x26, 0x3b, 0x35, 0x9a, 0x11, 0x51, 0xa6, 0xb7, 0x5c, 0x01, 0x36, 0x5e, 0xb1, + 0x54, 0xae, 0x42, 0x14, 0x0d, 0x6e, 0x10, 0x34, 0x2f, 0x14, 0xf3, 0x4d, 0xc3, 0x3e, + 0x07, 0xff, 0x0e, 0x4d, 0x1a, 0x6b, 0xe3, 0x75, 0xb3, 0x2f, 0x84, 0xb9, 0x2e, 0x5d, + 0x81, 0xeb, 0xb6, 0x39, 0xc4, 0xf2, 0x7e, 0x71, 0x5a, 0xa4, 0x2c, 0xc7, 0x57, 0x07, + 0xd4, 0xeb, 0xd1, 0xbb, 0xfb, 0xe8, 0xf9, 0x0f, 0xc7, 0xc9, 0x53, 0xe7, 0xa9, 0x71, + 0x5e, 0x65, 0xaf, 0x82, 0x67, 0x37, 0x3d, 0x34, 0x51, 0x67, 0x4f, 0xf0, 0x84, 0xef, + 0xd9, 0x2c, 0xcf, 0x3b, 0xcc, 0x7a, 0xca, 0x14, 0x67, 0xb6, 0x32, 0x7e, 0x4f, 0x95, + 0x22, 0xb2, 0xcc, 0x57, 0x9a, 0x7a, 0x8f, 0xff, 0x7c, 0xa7, 0xcf, 0x14, 0x5d, 0xfc, + 0x13, 0xea, 0xfc, 0x34, 0x15, 0x3b, 0x2c, 0x3e, 0x8a, 0xfb, 0xe5, 0x34, 0x44, 0xd0, + 0xc7, 0x3b, 0x3b, 0xd5, 0xbc, 0x87, 0x0b, 0x01, 0xcd, 0x45, 0x79, 0x11, 0xe3, 0x56, + 0x31, 0x3f, 0xd1, 0xda, 0xfb, 0x4c, 0x81, 0x51, 0x63, 0x4a, 0x01, 0xaf, 0xf7, 0xcf, + 0x11, 0x6d, 0x43, 0x3c, 0x3d, 0x2b, 0x3a, 0xdd, 0xa9, 0xce, 0xbe, 0x18, 0xf7, 0xd1, + 0x72, 0x44, 0x3e, 0x5e, 0x7b, 0x5a, 0xc9, 0xab, 0xe8, 0xdb, 0x22, 0x56, 0xd7, 0xeb, + 0xe2, 0xff, 0x28, 0x02, 0x09, 0x39, 0x50, 0x38, 0x70, 0x59, 0x7b, 0x9a, 0x95, 0x58, + 0x92, 0xc7, 0x38, 0x96, 0x50, 0xa2, 0xd4, 0x2e, 0xc9, 0x2b, 0xe7, 0x23, 0xfe, 0xdf, + 0x2f, 0x2e, 0xde, 0x5a, 0x47, 0x2a, 0xa1, 0xe7, 0x4f, 0x33, 0xad, 0x41, 0x90, 0x15, + 0x44, 0xed, 0xbb, 0xe3, 0xac, 0x46, 0x4c, 0xf4, 0x39, 0x19, 0x60, 0x15, 0xf4, 0xf2, + 0x2a, 0xc2, 0xb8, 0xfc, 0x01, 0x49, 0x6b, 0xea, 0xb4, 0xd4, 0x59, 0x07, 0xf4, 0x79, + 0x81, 0x2a, 0x25, 0x94, 0x31, 0xa2, 0xcb, 0xc9, 0x3d, 0x4f, 0x3b, 0x84, 0xe4, 0xdd, + 0x36, 0x60, 0x20, 0x27, 0x3a, 0x67, 0x52, 0xe5, 0x01, 0xaf, 0x6f, 0xf1, 0xb7, 0x8d, + 0xdc, 0x81, 0x7e, 0x6e, 0xa3, 0x51, 0xd6, 0x00, 0x6b, 0xec, 0xf8, 0xd2, 0xff, 0xb0, + 0x39, 0x90, 0xf6, 0x77, 0x74, 0xa8, 0x1e, 0x05, 0xb7, 0xf4, 0xbb, 0xad, 0x85, 0x77, + 0xfa, 0x27, 0xc9, 0xde, 0x64, 0xe1, 0xb1, 0x1d, 0xcf, 0x38, 0x4f, 0x59, 0x56, 0x44, + 0x37, 0x48, 0x75, 0x5a, 0x9f, 0xc6, 0xf2, 0xa0, 0x0b, 0x10, 0xc3, 0x65, 0x7e, 0xba, + 0xc0, 0x3b, 0xfc, 0x0b, 0x58, 0x7b, 0xef, 0x2f, 0x45, 0xec, 0x8a, 0xcd, 0xaa, 0x51, + 0xc1, 0x43, 0xb0, 0xcb, 0x25, 0xb9, 0x14, 0x2c, 0x61, 0xbd, 0x79, 0x0a, 0x80, 0xd7, + 0xc2, 0x3f, 0x90, 0xcc, 0x03, 0x49, 0x5b, 0x51, 0xe4, 0xd2, 0x84, 0x3e, 0x55, 0x7f, + 0x9e, 0x25, 0x45, 0x10, 0x8c, 0x6c, 0x6f, 0xae, 0x35, 0x9f, 0x64, 0x5c, 0x27, 0x68, + 0x91, 0xc0, 0xdc, 0xab, 0x3f, 0xaf, 0x18, 0x77, 0x00, 0xc0, 0x82, 0xdc, 0x47, 0x77, + 0x40, 0xfb, 0x3f, 0x2c, 0xd7, 0xbb, 0x59, 0xfb, 0x35, 0x85, 0x54, 0xe9, 0x4c, 0x7e, + 0x67, 0x8c, 0xe0, 0x1a, 0xeb, 0xf9, 0x4e, 0x51, 0x5e, 0x49, 0x72, 0x29, 0x67, 0x99, + 0x5a, 0xea, 0x85, 0x8d, 0x64, 0xe7, 0x78, 0x9f, 0xf3, 0x06, 0x36, 0x95, 0x77, 0x22, + 0x81, 0x80, 0x32, 0x6a, 0x5b, 0x0a, 0xf4, 0x75, 0xe2, 0x7a, 0x54, 0xb2, 0x07, 0xb4, + 0x1f, 0x92, 0xe3, 0x76, 0x17, 0x0e, 0x3f, 0xb0, 0x05, 0x02, 0x82, 0x61, 0xc9, 0x9c, + 0x2d, 0xbd, 0x0e, 0xed, 0xee, 0x87, 0x1c, 0x1c, 0x0f, 0x48, 0xb8, 0xe9, 0xb8, 0xe4, + 0xbe, 0x77, 0xd1, 0xb7, 0x37, 0xfe, 0x21, 0xf0, 0xfa, 0x5a, 0x18, 0xeb, 0xb5, 0x27, + 0x55, 0xb5, 0xa6, 0xcf, 0x61, 0x30, 0xfb, 0x56, 0x94, 0x4c, 0xfa, 0xb8, 0x75, 0x27, + 0xc2, 0x50, 0xd1, 0x13, 0xb2, 0x9b, 0xca, 0xc9, 0xaa, 0xa1, 0x0c, 0x2e, 0x7d, 0xe4, + 0x15, 0xed, 0xb0, 0x80, 0x6c, 0x6d, 0xa0, 0x30, 0x20, 0xa1, 0x34, 0xca, 0x7e, 0xcd, + 0xc8, 0xda, 0x1b, 0xd5, 0x7a, 0x37, 0xf5, 0x5a, 0x46, 0x94, 0x0b, 0x45, 0xb2, 0x41, + 0xb1, 0xc1, 0x6e, 0xe1, 0x00, 0x92, 0x7d, 0x1b, 0xd8, 0x60, 0xd4, 0x45, 0xa9, 0xde, + 0x50, 0xd4, 0xc3, 0x84, 0xd6, 0xe1, 0xd0, 0x01, 0x08, 0x02, 0x6c, 0x0e, 0xa5, 0xeb, + 0xbf, 0x0b, 0x72, 0xfb, 0xf5, 0xc3, 0x70, 0xbc, 0xe1, 0x8d, 0x3a, 0xcb, 0xc4, 0x65, + 0x99, 0x09, 0x9b, 0xaa, 0xe1, 0xd8, 0x02, 0xf7, 0x73, 0x33, 0x49, 0x4a, 0x7a, 0xe1, + 0x30, 0xfe, 0x86, 0xe8, 0xf8, 0x18, 0xf9, 0x26, 0x1a, 0x2d, 0xad, 0xb4, 0x12, 0x52, + 0x29, 0xba, 0x0f, 0xfc, 0x0e, 0x70, 0x90, 0x32, 0x44, 0x30, 0xb5, 0x21, 0xa9, 0x0d, + 0x22, 0x4a, 0xb7, 0xa1, 0x02, 0x4e, 0x1d, 0x89, 0x3e, 0x74, 0x04, 0xfe, 0xdb, 0x34, + 0x8e, 0x4d, 0x5e, 0x22, 0x35, 0xc5, 0x9a, 0x78, 0x76, 0xa0, 0xfc, 0x60, 0x14, 0x5c, + 0x6a, 0x00, 0x96, 0x87, 0x68, 0x44, 0x60, 0x27, 0x1e, 0xe1, 0x33, 0xa4, 0x37, 0xfe, + 0x52, 0xfb, 0x6c, 0xfb, 0xa9, 0x7f, 0xce, 0xc1, 0x61, 0xdf, 0x51, 0x5d, 0xde, 0x90, + 0x5a, 0x24, 0xda, 0x6d, 0x37, 0xbd, 0xc3, 0x40, 0x44, 0xa9, 0x55, 0xe6, 0x82, 0xb4, + 0x74, 0x71, 0xca, 0x1e, 0x8c, 0x78, 0xc5, 0x1e, 0xd3, 0x77, 0xcd, 0x4a, 0xfa, 0x89, + 0x4b, 0xd9, 0xbd, 0x12, 0xe7, 0x07, 0x15, 0x6d, 0xa0, 0x72, 0x6f, 0x7c, 0xf5, 0x72, + 0x9f, 0xab, 0xe3, 0x72, 0x16, 0x04, 0x63, 0xfe, 0x04, 0x29, 0x24, 0x4d, 0x06, 0x74, + 0x89, 0xba, 0x5d, 0x09, 0x47, 0x2e, 0xcd, 0x9b, 0xcd, 0xc4, 0xd5, 0xe4, 0xdf, 0x10, + 0x1e, 0x18, 0x9d, 0xb8, 0x46, 0x3e, 0xb5, 0x38, 0x30, 0x7b, 0x58, 0x7d, 0xef, 0xf7, + 0x8d, 0xe9, 0xc7, 0x3a, 0xf2, 0x80, 0x80, 0xb2, 0xfd, 0x05, 0x00, 0x3e, 0x11, 0xd3, + 0xe1, 0xb3, 0x29, 0x9d, 0xc9, 0x52, 0x1f, 0x8b, 0x51, 0x3b, 0xad, 0xb0, 0x10, 0xe9, + 0x1b, 0xfe, 0xb9, 0x1b, 0x0b, 0x2a, 0x6c, 0xb1, 0x29, 0xc2, 0xe8, 0x25, 0xa5, 0x97, + 0xb8, 0xfb, 0x75, 0xbc, 0x56, 0x2d, 0x65, 0x4d, 0x62, 0x10, 0x46, 0x40, 0xdd, 0x74, + 0xe5, 0x6c, 0xd1, 0x4b, 0xaa, 0xba, 0x56, 0x5b, 0x84, 0xb8, 0x45, 0xe1, 0x63, 0xd1, + 0xca, 0xef, 0x25, 0x33, 0xc3, 0x98, 0x16, 0x37, 0x20, 0x4f, 0x96, 0xa5, 0x9c, 0x8e, + 0x80, 0x24, 0xd9, 0x04, 0x1b, 0x20, 0x29, 0xe9, 0x4c, 0x15, 0x24, 0x5f, 0x1a, 0x95, + 0x88, 0x40, 0xba, 0x3f, 0x38, 0x0a, 0x4d, 0x20, 0xf1, 0x18, 0x4e, 0x77, 0x82, 0x7d, + 0xe3, 0xff, 0x8f, 0x3d, 0x73, 0x45, 0x9a, 0xfe, 0x24, 0x1f, 0x72, 0x3c, 0x08, 0x48, + 0x23, 0x23, 0x0e, 0x00, 0x3d, 0x3d, 0x21, 0xe5, 0x35, 0x01, 0xec, 0x04, 0x99, 0xb0, + 0x83, 0xa7, 0xda, 0xd6, 0x85, 0xc5, 0x71, 0x27, 0xf4, 0xde, 0x64, 0x73, 0x3a, 0x88, + 0x0c, 0x2d, 0xb2, 0x8f, 0xda, 0xab, 0xf1, 0xb5, 0x42, 0xd2, 0x05, 0xf6, 0x64, 0xa3, + 0x51, 0x35, 0x71, 0x27, 0x11, 0xdc, 0xcc, 0xd9, 0x31, 0xa5, 0x0b, 0x9c, 0x56, 0x61, + 0x88, 0x23, 0x60, 0xd4, 0xca, 0xc0, 0x04, 0x76, 0x81, 0xbc, 0x2e, 0x2b, 0x3b, 0xf6, + 0xc9, 0x97, 0x60, 0xd7, 0xcf, 0xb4, 0xfa, 0x21, 0x39, 0x43, 0x77, 0xa4, 0x55, 0x1c, + 0x76, 0xd1, 0xf7, 0x5a, 0xc0, 0x3c, 0x26, 0x20, 0x54, 0xdf, 0xfd, 0x79, 0xa9, 0xde, + 0xd0, 0x5e, 0x88, 0x89, 0x58, 0x19, 0x9e, 0xea, 0x45, 0x01, 0xe2, 0x99, 0x0a, 0x53, + 0xa5, 0xcd, 0x2a, 0x46, 0xa4, 0x01, 0x57, 0x65, 0x88, 0xfd, 0x7d, 0x05, 0x8a, 0x26, + 0xf2, 0x84, 0x38, 0xe5, 0x78, 0x2f, 0x45, 0xac, 0x1d, 0x07, 0xf6, 0xf6, 0xf5, 0xed, + 0x73, 0x74, 0x1d, 0x57, 0x85, 0x83, 0x7a, 0x6b, 0x84, 0x4b, 0x47, 0x47, 0x75, 0x71, + 0x8c, 0x29, 0xdd, 0x99, 0x08, 0x4e, 0x9f, 0x88, 0xef, 0x15, 0x3a, 0x83, 0x29, 0xf5, + 0x32, 0xa6, 0x90, 0x17, 0xdc, 0x3a, 0x97, 0xed, 0x75, 0x43, 0x67, 0x72, 0x30, 0x98, + 0xe5, 0x76, 0x58, 0x40, 0xb0, 0x22, 0x89, 0x72, 0x44, 0x74, 0x5f, 0xbb, 0xbb, 0x30, + 0xa7, 0xcb, 0x54, 0xfa, 0x05, 0x11, 0x16, 0x6e, 0x95, 0x44, 0x12, 0x20, 0x00, 0x61, + 0x0b, 0xd2, 0xaa, 0xcb, 0xd8, 0x23, 0x25, 0xa5, 0x9b, 0x95, 0x15, 0x4e, 0xcd, 0x82, + 0xc8, 0x8d, 0x23, 0xab, 0xd1, 0xe2, 0x07, 0x70, 0xff, 0xb8, 0xaa, 0xbf, 0x83, 0xfc, + 0x07, 0x34, 0x96, 0x4c, 0xcd, 0x41, 0x1d, 0x1c, 0x93, 0x57, 0x14, 0xe2, 0x4a, 0xab, + 0x56, 0x6f, 0x4f, 0x08, 0x42, 0x40, 0x14, 0xc4, 0xec, 0xa9, 0x1b, 0x59, 0x0f, 0x08, + 0x2b, 0x47, 0x3f, 0x36, 0x1c, 0x87, 0x41, 0x5d, 0x37, 0xbd, 0x20, 0xd7, 0x0f, 0xd0, + 0xb5, 0x2b, 0x6d, 0xdf, 0x18, 0x65, 0xf7, 0x66, 0x70, 0x2e, 0x32, 0xb0, 0x5b, 0x3c, + 0xf1, 0x63, 0x0e, 0xe8, 0x59, 0x7a, 0xae, 0x19, 0x63, 0x3f, 0x35, 0x16, 0xa8, 0x55, + 0x5a, 0xc5, 0xbe, 0x32, 0xc6, 0x75, 0xbe, 0x18, 0x17, 0xef, 0xbf, 0xfd, 0x93, 0x69, + 0x04, 0x1a, 0x08, 0x9c, 0x28, 0x3f, 0x19, 0x64, 0x99, 0x68, 0xc2, 0x49, 0x8c, 0xde, + 0x56, 0xf5, 0x00, 0x43, 0x4f, 0x28, 0x0d, 0x77, 0xa9, 0xc6, 0x2e, 0x43, 0xcb, 0xd3, + 0xf1, 0x36, 0xa4, 0xc6, 0xa0, 0x0a, 0x43, 0xe6, 0xed, 0x53, 0x0c, 0xb2, 0xe8, 0xae, + 0x83, 0x88, 0x60, 0xad, 0xc8, 0x8a, 0xac, 0xc7, 0xbd, 0x6a, 0x00, 0xae, 0x0c, 0x19, + 0xff, 0x45, 0x33, 0xa4, 0x85, 0xef, 0xde, 0x08, 0x2b, 0x5f, 0x4d, 0x1f, 0x7a, 0x8e, + 0xbe, 0x7e, 0xd8, 0x2b, 0x7b, 0x05, 0xa8, 0xcf, 0xe1, 0xe3, 0x73, 0x45, 0x9f, 0x1b, + 0xdc, 0xbf, 0x95, 0x25, 0x74, 0x7e, 0x8c, 0x95, 0x08, 0xa5, 0x55, 0xfa, 0xcb, 0x79, + 0x87, 0x40, 0xe0, 0xbd, 0xf9, 0x94, 0xd9, 0x73, 0x9b, 0xbe, 0x55, 0x38, 0xa0, 0xae, + 0x0f, 0x07, 0x6c, 0x58, 0x2c, 0x0f, 0x5b, 0xa8, 0x78, 0xb9, 0x9b, 0x82, 0x49, 0xdb, + 0x1d, 0x7e, 0x95, 0x05, 0x6c, 0x98, 0xaf, 0x08, 0x3d, 0x98, 0xcb, 0x0e, 0xd9, 0xe3, + 0xf7, 0x43, 0x6e, 0x1c, 0x76, 0x43, 0x76, 0x6f, 0x96, 0x6b, 0x83, 0xe9, 0x99, 0x20, + 0x6e, 0xbd, 0x13, 0x93, 0xb9, 0xb2, 0xa7, 0xf4, 0x14, 0x48, 0x0f, 0xa0, 0x17, 0x48, + 0x00, 0x69, 0xf8, 0x5c, 0x77, 0x49, 0xc4, 0x35, 0xae, 0x2f, 0xba, 0x2d, 0xdc, 0x10, + 0x38, 0xd5, 0x47, 0xd8, 0x48, 0x54, 0x81, 0x7e, 0xf3, 0x96, 0x35, 0xc2, 0x98, 0x27, + 0xaa, 0xd8, 0x67, 0x26, 0xc9, 0xad, 0xe3, 0xb2, 0x65, 0xb9, 0x08, 0x6c, 0x8b, 0x5b, + 0x75, 0xef, 0x56, 0xfe, 0x4b, 0xd8, 0xb4, 0xd6, 0x28, 0x93, 0x89, 0x5b, 0x3f, 0xd2, + 0x73, 0x4f, 0xda, 0xc4, 0x64, 0x15, 0x6d, 0x7e, 0x5e, 0xbc, 0x7e, 0xcf, 0x1d, 0x83, + 0xb8, 0x6f, 0x65, 0x96, 0x37, 0xe3, 0xb1, 0x42, 0xc1, 0x64, 0x96, 0x3b, 0x8c, 0xdc, + 0xf4, 0xba, 0x4f, 0x40, 0x35, 0xdf, 0xfc, 0x5a, 0x78, 0x94, 0x58, 0x84, 0x77, 0x81, + 0x91, 0x8a, 0xc7, 0x2f, 0xc1, 0x8b, 0xbb, 0xf5, 0x11, 0x00, 0x32, 0xe6, 0x6d, 0x75, + 0xb3, 0x17, 0x1e, 0xf4, 0xb5, 0x13, 0x29, 0x01, 0x64, 0xa7, 0x7b, 0x42, 0xb0, 0xa4, + 0xcf, 0xb8, 0x96, 0x39, 0xab, 0x23, 0x84, 0x5e, 0x1a, 0xa2, 0xa4, 0x52, 0xf3, 0x73, + 0x1c, 0x8c, 0xb6, 0x50, 0x82, 0xa6, 0x22, 0xa7, 0xc2, 0xe0, 0x01, 0x3e, 0xa4, 0x7d, + 0x0b, 0xdd, 0x42, 0xd6, 0x99, 0x04, 0x66, 0x64, 0x9a, 0x90, 0x5c, 0x68, 0x4c, 0x32, + 0x51, 0x71, 0x6d, 0x61, 0xf7, 0x60, 0xd5, 0x3d, 0xe6, 0xe3, 0xf7, 0x90, 0xfb, 0xa7, + 0xf5, 0xf1, 0xf4, 0xde, 0x26, 0x71, 0x13, 0xbd, 0xfc, 0xd7, 0x42, 0x28, 0x22, 0x33, + 0x0b, 0x32, 0xd5, 0x8e, 0x67, 0x77, 0x76, 0x5f, 0x22, 0xa4, 0x11, 0x63, 0x44, 0xee, + 0xb6, 0x5b, 0x2e, 0xc5, 0x16, 0x39, 0x3a, 0xb3, 0x75, 0x1b, 0x53, 0x56, 0xd2, 0xb0, + 0xc9, 0x50, 0x0c, 0x0f, 0x3e, 0x46, 0x91, 0x81, 0x03, 0x5b, 0xc3, 0x66, 0x0f, 0x0b, + 0x8f, 0x9f, 0xbe, 0x6e, 0x40, 0xb5, 0xe8, 0x9c, 0xb7, 0x9b, 0x06, 0x37, 0x14, 0xca, + 0x75, 0xe7, 0x2e, 0x2e, 0x10, 0x0a, 0x10, 0xd6, 0x3b, 0xf7, 0x84, 0xdf, 0x08, 0x20, + 0xef, 0x25, 0xf8, 0xef, 0x40, 0xfe, 0x5f, 0x05, 0xfb, 0x95, 0x68, 0x3f, 0x91, 0x05, + 0xff, 0x3c, 0xb2, 0xd2, 0x19, 0xab, 0x76, 0x60, 0x5a, 0x06, 0x4f, 0x69, 0x21, 0x9f, + 0x1d, 0xc0, 0xd0, 0x0b, 0x3b, 0x48, 0x64, 0x2f, 0x97, 0x0d, 0xc0, 0x0c, 0xca, 0x4b, + 0x8b, 0x43, 0x30, 0x8b, 0xe1, 0x82, 0x86, 0xec, 0x5a, 0x42, 0x88, 0xd6, 0x00, 0xa3, + 0x78, 0x5c, 0xb6, 0x22, 0xd4, 0x68, 0xa4, 0xc6, 0x96, 0x9b, 0x37, 0x92, 0xf2, 0x48, + 0x50, 0x27, 0xd0, 0xad, 0x9a, 0xa4, 0xa9, 0xc2, 0xcc, 0x97, 0x2f, 0x9e, 0xe5, 0x19, + 0x0a, 0x95, 0xb1, 0xeb, 0x05, 0x8d, 0xdd, 0xd8, 0xc0, 0x8e, 0x7d, 0x75, 0x3f, 0x5e, + 0x01, 0x1b, 0x2b, 0xcf, 0xee, 0x1d, 0x52, 0xc1, 0xc4, 0xf2, 0xca, 0xcd, 0xa3, 0x0b, + 0xdb, 0x69, 0x30, 0x65, 0x3c, 0x0c, 0xc4, 0x48, 0x6e, 0x60, 0xe8, 0x9f, 0xa8, 0x49, + 0xb3, 0x20, 0x83, 0xba, 0x9d, 0xb4, 0x53, 0xfb, 0x8d, 0xf6, 0x83, 0xcd, 0x68, 0x75, + 0x4c, 0x87, 0xda, 0xa7, 0x31, 0xf5, 0x70, 0xa7, 0xa4, 0x06, 0x0a, 0xf0, 0xce, 0x70, + 0x0d, 0x31, 0xbc, 0xa7, 0xe7, 0x4b, 0x3e, 0x3b, 0xa3, 0xd0, 0xe8, 0xa6, 0x39, 0x2a, + 0x06, 0x2b, 0x8e, 0x86, 0xd9, 0xd7, 0xd0, 0x0b, 0x21, 0x70, 0x1e, 0x7b, 0x06, 0x2e, + 0x06, 0xb1, 0xbc, 0xd8, 0x2a, 0x01, 0xd3, 0x75, 0x62, 0x6f, 0xbf, 0x87, 0x2d, 0x27, + 0xfa, 0x45, 0x11, 0xf5, 0xf8, 0xcf, 0x8c, 0x9a, 0xbc, 0xef, 0x2a, 0x99, 0x01, 0x76, + 0xae, 0x33, 0x93, 0x25, 0xd5, 0xa5, 0x88, 0xda, 0x57, 0x96, 0xfa, 0xae, 0x5b, 0xab, + 0x7c, 0x82, 0x97, 0x7c, 0x0f, 0xf7, 0x97, 0x09, 0x3e, 0x2c, 0x1f, 0x3a, 0xe8, 0x55, + 0xf6, 0x5a, 0xea, 0x91, 0xe1, 0x31, 0x2f, 0xc6, 0xb8, 0xa4, 0x35, 0x1a, 0x2e, 0xc0, + 0x3e, 0x02, 0xe5, 0xd0, 0x2f, 0x53, 0x35, 0x4b, 0x05, 0x2f, 0xd3, 0xda, 0x0d, 0xff, + 0x82, 0xcd, 0x1f, 0x55, 0xeb, 0xca, 0x57, 0xb6, 0x33, 0x7c, 0x85, 0x93, 0x8a, 0x79, + 0x81, 0x3d, 0x20, 0x21, 0xd6, 0x09, 0x4c, 0x68, 0xb3, 0x75, 0xe9, 0x84, 0xf6, 0x83, + 0x93, 0x30, 0x08, 0x71, 0xe3, 0x48, 0xfc, 0x52, 0x36, 0xcc, 0xa6, 0x33, 0x05, 0x44, + 0xe5, 0x46, 0x39, 0xb5, 0x41, 0x87, 0x01, 0xff, 0x4c, 0xc4, 0x5a, 0x31, 0xf6, 0x2e, + 0xdd, 0x84, 0x3d, 0xbb, 0xdc, 0x5a, 0xa7, 0x27, 0xab, 0x79, 0xb4, 0x42, 0x68, 0x3c, + 0x49, 0x56, 0xbb, 0xb1, 0x95, 0xa4, 0xfa, 0x66, 0xdc, 0x9c, 0xd5, 0x42, 0xc7, 0x6b, + 0x91, 0x50, 0xc8, 0x4b, 0xf8, 0x90, 0x78, 0x99, 0x42, 0xf5, 0x5c, 0x20, 0x0b, 0x77, + 0x3e, 0xcd, 0xd7, 0x99, 0x2c, 0xff, 0x3e, 0xca, 0x24, 0xde, 0x3e, 0x09, 0x84, 0xe1, + 0x0e, 0x68, 0xae, 0x38, 0x75, 0x34, 0xb9, 0x6c, 0xde, 0x37, 0x92, 0xf1, 0x35, 0xbf, + 0x5f, 0x68, 0x78, 0x7d, 0x37, 0x0c, 0xa8, 0xc4, 0xc4, 0x07, 0x4d, 0xc5, 0xd6, 0x01, + 0xae, 0x90, 0x49, 0x54, 0x37, 0xc3, 0xc2, 0xd4, 0x8a, 0x3d, 0x96, 0x66, 0x83, 0xac, + 0x05, 0x16, 0x0b, 0x7a, 0x84, 0xea, 0xa7, 0xaa, 0xb7, 0x40, 0x09, 0xe5, 0x7a, 0x85, + 0xf7, 0xbf, 0x68, 0xa2, 0xe4, 0x82, 0x00, 0x0f, 0x82, 0x9c, 0x54, 0x50, 0x73, 0xa1, + 0x5d, 0x5c, 0xd0, 0xfc, 0xc5, 0x74, 0x39, 0xa4, 0x35, 0x0e, 0xaf, 0x09, 0x8d, 0xfb, + 0x82, 0xa0, 0x85, 0xea, 0x8a, 0x4a, 0xf6, 0xfa, 0x83, 0x81, 0xf0, 0x65, 0x88, 0x19, + 0xea, 0xb4, 0x83, 0xf6, 0x5b, 0x32, 0x5d, 0x5a, 0xed, 0xa1, 0x52, 0x32, 0xcf, 0xad, + 0xec, 0x75, 0xab, 0x18, 0x66, 0xe4, 0xc0, 0x15, 0x5a, 0x9c, 0x74, 0xa7, 0xa5, 0x7c, + 0xcf, 0x34, 0xc4, 0x83, 0xac, 0x7d, 0xa1, 0x58, 0x8a, 0x1b, 0x6b, 0x99, 0x41, 0xf1, + 0x10, 0x40, 0xf9, 0x4c, 0xf7, 0x8f, 0xad, 0x89, 0xbf, 0x11, 0xfe, 0xd6, 0x9a, 0xa0, + 0xd8, 0x31, 0x05, 0xad, 0xac, 0xdd, 0x4e, 0x5f, 0x04, 0xa6, 0x24, 0x24, 0x02, 0x3c, + 0x9b, 0x9e, 0x33, 0xc4, 0xfb, 0x7f, 0x12, 0xbd, 0xf2, 0x1f, 0x07, 0xf2, 0x65, 0xc5, + 0x37, 0xd5, 0x1c, 0x65, 0x51, 0xf4, 0x61, 0x7b, 0x91, 0x5d, 0x21, 0x99, 0x18, 0x39, + 0xc3, 0xd0, 0xd3, 0x63, 0x93, 0xd6, 0x46, 0xe0, 0xa8, 0xa4, 0x15, 0x09, 0x21, 0x7d, + 0x0e, 0x7d, 0x2c, 0xa1, 0xa0, 0xa0, 0xd6, 0x77, 0xa3, 0xea, 0xca, 0x23, 0xed, 0xeb, + 0x07, 0xb7, 0x4e, 0x65, 0x2a, 0x0b, 0xc5, 0x0c, 0x6c, 0x08, 0x3a, 0x55, 0xd6, 0xc7, + 0x30, 0x6e, 0x74, 0x08, 0x6f, 0x47, 0x68, 0x93, 0x3a, 0xa2, 0x48, 0x73, 0x68, 0x18, + 0x67, 0xa7, 0x89, 0x3d, 0x77, 0xcb, 0x7f, 0x29, 0xb8, 0xc8, 0x47, 0xc5, 0x83, 0xf2, + 0xd0, 0x71, 0xa6, 0x86, 0x61, 0x6e, 0x20, 0x67, 0x19, 0xf7, 0x61, 0xae, 0x39, 0xc1, + 0x10, 0x44, 0x2e, 0x06, 0x16, 0x3d, 0x2b, 0x84, 0x59, 0x03, 0x60, 0x69, 0x5d, 0x4e, + 0x19, 0x84, 0x9e, 0x63, 0x4f, 0x24, 0xd9, 0xad, 0x39, 0x6c, 0x19, 0xff, 0x83, 0xce, + 0x74, 0xf4, 0x6e, 0x64, 0x5f, 0x93, 0x2e, 0x14, 0x1a, 0x41, 0x19, 0x59, 0x36, 0xc8, + 0x5d, 0x51, 0x44, 0x14, 0xf1, 0x12, 0xe6, 0x0b, 0x1a, 0x25, 0x37, 0xc3, 0x8d, 0x6d, + 0xc6, 0xc4, 0x63, 0x83, 0x05, 0xc9, 0xbd, 0x6c, 0x62, 0xe3, 0x66, 0xbc, 0x63, 0x12, + 0x3e, 0x3e, 0x6d, 0xd3, 0x6e, 0xed, 0xd3, 0x13, 0x6f, 0xce, 0x8d, 0xee, 0xca, 0x2a, + 0xa0, 0x9a, 0x32, 0x98, 0xa3, 0x9d, 0x83, 0x85, 0x9e, 0xfc, 0x9b, 0x2b, 0x69, 0xcf, + 0x9a, 0x7d, 0xee, 0x08, 0xa9, 0x8e, 0x4b, 0xe5, 0x58, 0xac, 0x79, 0x12, 0xfd, 0xcb, + 0x42, 0x20, 0x90, 0x75, 0x42, 0x02, 0x60, 0xf7, 0xca, 0xd0, 0xf2, 0xc0, 0x1f, 0x2a, + 0xfe, 0x33, 0x07, 0x3f, 0x26, 0x24, 0x9d, 0x94, 0x4f, 0x7a, 0x50, 0xdd, 0x84, 0x83, + 0x9b, 0xc3, 0xea, 0x7f, 0xde, 0xe4, 0xed, 0x71, 0x44, 0x9c, 0xf0, 0x75, 0x33, 0xd2, + 0x6e, 0x1e, 0x27, 0xa3, 0xef, 0xb0, 0x32, 0xc3, 0xa3, 0xb3, 0x4b, 0xd3, 0x09, 0x26, + 0x22, 0xd2, 0x06, 0x2a, 0xe5, 0x36, 0xef, 0x51, 0x49, 0xc4, 0x9b, 0x5b, 0xc9, 0x47, + 0x5e, 0xaf, 0xab, 0x6e, 0x67, 0x57, 0x61, 0x00, 0x8b, 0x0d, 0xad, 0xde, 0xec, 0xaa, + 0x60, 0x44, 0x70, 0xbb, 0xe0, 0xfa, 0xda, 0x25, 0x5d, 0x29, 0x0e, 0x92, 0xb1, 0x90, + 0xc2, 0xc2, 0xd8, 0xc2, 0xde, 0xe5, 0x45, 0x5d, 0x1f, 0xa9, 0xa9, 0xf3, 0xdb, 0x77, + 0x79, 0xb5, 0x84, 0x64, 0x34, 0x64, 0xaa, 0x80, 0x14, 0xba, 0x66, 0x99, 0x4d, 0xe2, + 0x55, 0x17, 0xf8, 0x39, 0x80, 0xe6, 0x6e, 0xe4, 0xf6, 0x23, 0x14, 0xae, 0x6d, 0xbe, + 0xf4, 0x52, 0xd5, 0xd3, 0x8b, 0x0a, 0x16, 0xf3, 0x99, 0x1f, 0x36, 0xd8, 0xa8, 0xb3, + 0x9d, 0xdc, 0x0d, 0x55, 0x95, 0xee, 0xd9, 0x87, 0x62, 0x87, 0x8c, 0xdf, 0x3f, 0x4a, + 0x2e, 0xdc, 0x5c, 0xda, 0x77, 0xd5, 0xfe, 0x4f, 0xaf, 0x63, 0xa1, 0x5f, 0x56, 0x8a, + 0x54, 0x0d, 0xa5, 0x7d, 0xd9, 0xbe, 0xb6, 0xfb, 0x1a, 0x97, 0x7c, 0xcb, 0x91, 0xb4, + 0xd7, 0x9c, 0xb3, 0x9b, 0x28, 0x91, 0x1a, 0x29, 0xe7, 0xbf, 0x02, 0x8a, 0xc6, 0x10, + 0x37, 0x96, 0xdf, 0xb6, 0xb2, 0x09, 0x67, 0x23, 0x9a, 0xd3, 0x73, 0xc3, 0x8c, 0x53, + 0xf6, 0xdf, 0x18, 0x23, 0xd4, 0x95, 0x0a, 0x02, 0x83, 0xe9, 0x9b, 0x9c, 0x06, 0xab, + 0x29, 0x66, 0x66, 0x7c, 0x9d, 0xf6, 0x77, 0x71, 0x6b, 0x0c, 0xad, 0xed, 0x81, 0x8d, + 0xf9, 0xe4, 0x49, 0xc0, 0x72, 0xe2, 0x2f, 0x9d, 0x98, 0xbb, 0x0f, 0x9b, 0x03, 0xbd, + 0x5f, 0xd0, 0x13, 0xfc, 0xef, 0x3e, 0xd6, 0xa4, 0x9a, 0xeb, 0x98, 0x72, 0x02, 0x54, + 0x08, 0x7e, 0xf7, 0x28, 0xe3, 0x19, 0x47, 0xff, 0xe8, 0xf7, 0x66, 0xe6, 0x3e, 0xe4, + 0x6f, 0xf2, 0x08, 0x16, 0xd5, 0xfa, 0x8f, 0xf5, 0x5a, 0x26, 0x39, 0x89, 0x61, 0x49, + 0x0a, 0xb9, 0xae, 0x36, 0x6f, 0xc5, 0xa2, 0xd1, 0x99, 0x6e, 0xd6, 0x93, 0xcc, 0xca, + 0x82, 0x35, 0x6f, 0x60, 0x0a, 0xb0, 0x99, 0xf6, 0xec, 0xa8, 0xbf, 0xe6, 0x45, 0x27, + 0x0d, 0x3f, 0x95, 0xed, 0xba, 0x5b, 0x0d, 0xe7, 0xa3, 0x28, 0x19, 0x23, 0x3b, 0xcc, + 0x75, 0x4a, 0x5c, 0xe2, 0xe5, 0xea, 0x07, 0x84, 0x2e, 0x5f, 0xf2, 0xce, 0xbe, 0x62, + 0xad, 0x76, 0xe8, 0xef, 0xf8, 0xd1, 0x5e, 0xa4, 0xc2, 0x4a, 0x5f, 0x20, 0x78, 0x68, + 0x31, 0x9a, 0x5a, 0xf6, 0xb0, 0x35, 0xbe, 0x3f, 0x44, 0xf4, 0x34, 0x09, 0x4f, 0x6e, + 0x52, 0x5b, 0xe6, 0x14, 0xda, 0xc9, 0x20, 0xa3, 0x30, 0xbd, 0xfb, 0x26, 0xd7, 0x5f, + 0xe7, 0xb4, 0xb3, 0x65, 0xd0, 0x94, 0x45, 0x92, 0x50, 0xaa, 0xa5, 0x54, 0x44, 0x89, + 0xfb, 0x1d, 0x99, 0x25, 0x81, 0x80, 0x0a, 0x77, 0xb8, 0x91, 0x21, 0x57, 0xfc, 0x97, + 0x13, 0xaa, 0xac, 0x25, 0xb4, 0xc2, 0x6e, 0xb0, 0x3f, 0x71, 0x66, 0x46, 0x61, 0x9a, + 0xf0, 0x24, 0x56, 0xae, 0x69, 0x59, 0x62, 0xfe, 0x5e, 0x93, 0x1a, 0x63, 0xb5, 0xc7, + 0x90, 0x52, 0xec, 0xd3, 0x33, 0xe1, 0x84, 0x12, 0xdb, 0x91, 0xe1, 0x5f, 0x7c, 0xbc, + 0x70, 0xb4, 0xcd, 0x7e, 0x8e, 0x3c, 0x95, 0x1f, 0x35, 0x85, 0x72, 0xe3, 0x77, 0x67, + 0xe7, 0xd5, 0x27, 0x04, 0xa6, 0x72, 0x1b, 0x30, 0xef, 0xc4, 0x10, 0x17, 0xae, 0x4d, + 0x23, 0x15, 0x58, 0xc5, 0xc8, 0x2c, 0xc7, 0xdd, 0x7e, 0x33, 0x56, 0xc0, 0x9d, 0xc2, + 0x49, 0x06, 0xf0, 0x43, 0x8d, 0xfc, 0xc3, 0x00, 0x85, 0x6a, 0xc2, 0xce, 0xd8, 0xf7, + 0x7f, 0xa8, 0x01, 0x57, 0x36, 0xc6, 0x61, 0xe8, 0x02, 0x48, 0xae, 0xeb, 0x77, 0x48, + 0x74, 0xaa, 0x79, 0xd2, 0x90, 0xb8, 0xf5, 0x02, 0x7a, 0x0a, 0x50, 0x95, 0x37, 0xfc, + 0x7c, 0x68, 0x9b, 0x7a, 0xd8, 0x61, 0x16, 0xcf, 0xec, 0x26, 0x47, 0xcc, 0xaa, 0xe1, + 0xc7, 0x4b, 0x41, 0x6f, 0x3e, 0x6a, 0xe8, 0xf7, 0xcc, 0x60, 0xea, 0xaf, 0x7b, 0x6a, + 0x59, 0x0d, 0x51, 0x54, 0x41, 0x38, 0xe1, 0x73, 0x29, 0x45, 0x60, 0x3a, 0x53, 0x46, + 0x2c, 0x60, 0xe1, 0xf6, 0xcb, 0x0c, 0x9c, 0xa0, 0x39, 0x0c, 0x48, 0x82, 0x24, 0xc3, + 0x13, 0x26, 0x9f, 0xcd, 0x59, 0xfc, 0xb6, 0x11, 0xfb, 0x2d, 0x9b, 0x4c, 0x8f, 0xa6, + 0x01, 0xbb, 0x1c, 0xb8, 0xd0, 0x7d, 0x79, 0x7b, 0xf5, 0xde, 0x52, 0xbc, 0xee, 0xb0, + 0x23, 0x01, 0xc8, 0x96, 0x2a, 0xc1, 0xfc, 0x04, 0x91, 0xdc, 0x81, 0xaf, 0xfd, 0x6c, + 0x1e, 0xbf, 0x89, 0xa1, 0x3d, 0x6f, 0x29, 0x0e, 0xda, 0x5d, 0x5c, 0xef, 0x38, 0x22, + 0x15, 0xc5, 0xe9, 0x51, 0xd7, 0x13, 0x05, 0xef, 0x33, 0xd9, 0x73, 0x71, 0x26, 0xd0, + 0xe6, 0x62, 0x90, 0x5f, 0x12, 0x50, 0x92, 0x6f, 0x6a, 0x22, 0x99, 0x90, 0xe3, 0x8f, + 0x69, 0xad, 0x9a, 0x91, 0x92, 0xb3, 0x02, 0xf2, 0x6b, 0xdd, 0xa4, 0x65, 0xd9, 0x0b, + 0x94, 0xb1, 0x2c, 0x57, 0xfa, 0x3f, 0xd6, 0x93, 0x00, 0x83, 0xf1, 0x84, 0x43, 0x8d, + 0x8a, 0x88, 0x9d, 0x3f, 0x5e, 0xce, 0xa2, 0xc6, 0xd2, 0x3d, 0x67, 0x36, 0xf2, 0xa0, + 0xf1, 0x8e, 0x26, 0xf4, 0xfa, 0x45, 0xd1, 0xbe, 0x8f, 0x3d, 0xc4, 0xa7, 0x07, 0x13, + 0x7e, 0x95, 0xd2, 0xad, 0x59, 0x4f, 0x6c, 0x03, 0xd2, 0x49, 0x23, 0x06, 0x7a, 0xe4, + 0x7f, 0xd6, 0x42, 0x5e, 0xfb, 0x9c, 0x1d, 0x50, 0x4e, 0x6f, 0xd5, 0x57, 0x53, 0x40, + 0x94, 0x56, 0x01, 0xfe, 0x80, 0x6f, 0x57, 0x56, 0xac, 0xb5, 0x62, 0xf1, 0x3c, 0x0c, + 0xa1, 0xd8, 0x03, 0xa1, 0x95, 0xc2, 0xeb, 0xb2, 0xef, 0x02, 0xac, 0x33, 0xe6, 0xa8, + 0x8d, 0xea, 0x07, 0x5b, 0xa9, 0x96, 0xd3, 0xc3, 0x36, 0x64, 0x8e, 0x86, 0x94, 0xd3, + 0xa1, 0x9d, 0x3d, 0xca, 0x53, 0x1b, 0xeb, 0x50, 0xd4, 0x32, 0x7c, 0x5c, 0x0c, 0x23, + 0xcb, 0x7c, 0xfd, 0xb0, 0x8c, 0xa7, 0xcf, 0x2c, 0xac, 0x6b, 0xc1, 0x39, 0xd0, 0x74, + 0x14, 0x73, 0xd3, 0x76, 0x02, 0x9c, 0xb4, 0xab, 0x6b, 0xf0, 0x54, 0x55, 0x7c, 0xe2, + 0x94, 0xc7, 0x28, 0xa4, 0x68, 0x7d, 0x57, 0xec, 0x89, 0x09, 0xff, 0x51, 0xa4, 0xd0, + 0x2f, 0x9d, 0xcd, 0x11, 0x19, 0x3d, 0x7d, 0x1c, 0x9f, 0xda, 0xe6, 0xa1, 0x73, 0x96, + 0xa1, 0xbf, 0x57, 0xa9, 0x94, 0x93, 0x4f, 0x5e, 0x7a, 0x59, 0xf0, 0x45, 0xde, 0xbe, + 0xaf, 0xf6, 0x2e, 0xf3, 0x26, 0xb9, 0x47, 0xf2, 0xa8, 0xb4, 0x95, 0x55, 0xe4, 0xd9, + 0x9b, 0x3b, 0xf5, 0xc8, 0x1f, 0xf9, 0xfe, 0x31, 0x4e, 0x04, 0x7a, 0xf1, 0x52, 0x50, + 0x8f, 0x57, 0x01, 0x5c, 0xa4, 0x02, 0xc6, 0x7d, 0x92, 0x5c, 0x99, 0xac, 0xea, 0x3e, + 0xe8, 0xcc, 0x4b, 0x00, 0x8c, 0x5c, 0xb4, 0x39, 0x66, 0xe7, 0x14, 0xef, 0x48, 0x0f, + 0xd0, 0x5e, 0x07, 0xc7, 0xb2, 0xdd, 0xa9, 0xaa, 0x39, 0x66, 0x11, 0x3e, 0xaa, 0x29, + 0x3d, 0x3f, 0x62, 0x2b, 0x30, 0x9d, 0x64, 0x80, 0x3c, 0xe1, 0xe6, 0x37, 0x8b, 0x6a, + 0xac, 0x4f, 0xab, 0x52, 0x7c, 0x43, 0xcd, 0x45, 0xed, 0x0a, 0x3c, 0x1a, 0x4b, 0x9f, + 0xb1, 0x8d, 0xcc, 0xcf, 0xcd, 0xb6, 0xac, 0x0c, 0x24, 0x21, 0x63, 0x9c, 0xda, 0x00, + 0x75, 0xa2, 0x0d, 0xc5, 0x11, 0x1b, 0x8d, 0x3d, 0x31, 0x99, 0x49, 0x5b, 0xd9, 0x13, + 0x3d, 0xba, 0xb9, 0x45, 0x41, 0x41, 0x0e, 0x4f, 0xba, 0x92, 0xc7, 0xb6, 0x06, 0xa5, + 0xcb, 0x12, 0x2f, 0x14, 0x0c, 0xf1, 0xa3, 0x59, 0x6f, 0x27, 0x88, 0xf3, 0xc8, 0xb9, + 0x26, 0x60, 0xf1, 0x4c, 0xb6, 0x5a, 0xf5, 0xdd, 0x23, 0xdf, 0xdb, 0xac, 0x13, 0x71, + 0xec, 0xf4, 0xb3, 0x37, 0x12, 0xfe, 0xd2, 0x29, 0x2c, 0x44, 0xf7, 0x08, 0x34, 0xcf, + 0x96, 0xc0, 0x5d, 0x58, 0x82, 0x7e, 0x69, 0xbf, 0xc2, 0xe6, 0x96, 0xfa, 0x08, 0x74, + 0x86, 0x9c, 0x02, 0xf3, 0xdc, 0xa1, 0x1c, 0x3b, 0x90, 0xcb, 0x21, 0x4e, 0x68, 0xbc, + 0x1c, 0xae, 0x03, 0x9d, 0x7a, 0x14, 0x6c, 0xdc, 0x1d, 0x60, 0x9d, 0x7a, 0x6b, 0x3f, + 0xd5, 0xd4, 0x61, 0xb0, 0x95, 0x1c, 0x82, 0xcf, 0xb3, 0xe7, 0x63, 0xfa, 0xd2, 0xd1, + 0xbc, 0x76, 0x78, 0xcd, 0xf8, 0x27, 0x79, 0xf8, 0xfd, 0x5a, 0x1c, 0xe2, 0x2a, 0x8d, + 0x3c, 0x45, 0x47, 0xab, 0xd9, 0x59, 0x83, 0x8a, 0x46, 0xfb, 0x80, 0xaf, 0xe0, 0x1f, + 0x8e, 0xcc, 0x99, 0x31, 0x51, 0x3b, 0x19, 0x62, 0xec, 0x54, 0x08, 0x56, 0xcb, 0x18, + 0x93, 0x87, 0xcf, 0xbf, 0xcc, 0x0f, 0x7c, 0x68, 0x22, 0x3c, 0xba, 0x47, 0xfb, 0x0c, + 0x9b, 0x48, 0x6e, 0x4d, 0x99, 0x17, 0x19, 0x41, 0xf7, 0x67, 0x5a, 0x8b, 0x46, 0x32, + 0x8a, 0x3b, 0xc1, 0x09, 0xbf, 0x07, 0xc6, 0x6d, 0x5e, 0xde, 0x77, 0x1c, 0xc4, 0xc7, + 0x4c, 0xe8, 0x03, 0x33, 0x82, 0x91, 0x91, 0xee, 0xdc, 0x49, 0x35, 0x08, 0xa6, 0x44, + 0x53, 0x0a, 0x61, 0x44, 0xf2, 0x2d, 0xcf, 0x97, 0x52, 0x5a, 0x4c, 0xdc, 0xa1, 0xad, + 0x71, 0x07, 0x3b, 0x08, 0x0b, 0x73, 0xea, 0x45, 0x49, 0xf5, 0x40, 0x1b, 0xff, 0x43, + 0x18, 0x26, 0x8e, 0x6a, 0xd6, 0x37, 0x36, 0x31, 0x57, 0xa1, 0x9a, 0x53, 0xf1, 0x23, + 0xa0, 0xb0, 0xe1, 0x6d, 0x0b, 0x77, 0xf0, 0x20, 0x28, 0xda, 0x46, 0x41, 0x00, 0xfd, + 0xe7, 0x6d, 0x83, 0xdd, 0x0b, 0xb2, 0x24, 0xf7, 0xb5, 0x7a, 0x00, 0xc0, 0x2f, 0x68, + 0xae, 0x64, 0x8f, 0xdc, 0x52, 0x99, 0x57, 0xa1, 0x04, 0x90, 0xdc, 0xe1, 0xfd, 0xdb, + 0xb0, 0x90, 0x4f, 0x0d, 0x51, 0x8b, 0xb3, 0x87, 0x54, 0x40, 0x19, 0x98, 0x3b, 0x61, + 0x69, 0x75, 0xa7, 0x8e, 0x74, 0xd8, 0x54, 0xfd, 0xdc, 0x49, 0xb2, 0x55, 0x16, 0x7b, + 0x55, 0xef, 0x4b, 0xee, 0x46, 0x56, 0x68, 0xb2, 0x0e, 0xa4, 0x11, 0x8c, 0xa5, 0x69, + 0xae, 0x48, 0x0e, 0x0f, 0x6e, 0x5e, 0x04, 0x3a, 0x35, 0x7b, 0x36, 0xd3, 0xab, 0x36, + 0xc8, 0x61, 0xf2, 0x27, 0x83, 0x01, 0xdc, 0xe5, 0x76, 0x74, 0xd5, 0x07, 0x3b, 0x3a, + 0x6f, 0x51, 0x03, 0xa0, 0x79, 0x3a, 0xf1, 0xb7, 0xd4, 0x6f, 0x95, 0x7e, 0x22, 0xd8, + 0xd2, 0x58, 0x3b, 0xf1, 0x81, 0x83, 0x6c, 0x3b, 0xe9, 0x93, 0x0b, 0xac, 0x8f, 0xa4, + 0x60, 0xe9, 0x68, 0xaa, 0x71, 0x09, 0x87, 0x0b, 0xbe, 0xd1, 0x7d, 0xf5, 0xf8, 0x88, + 0xc8, 0xca, 0x14, 0x67, 0xae, 0x17, 0xdb, 0xbc, 0xde, 0x31, 0xc1, 0x10, 0x5c, 0xb5, + 0xbd, 0xa8, 0x8a, 0xc6, 0xc6, 0x27, 0x00, 0x2c, 0xe2, 0x1c, 0x02, 0x14, 0x0f, 0xfe, + 0x81, 0xec, 0x58, 0xbf, 0x1e, 0x6d, 0x1b, 0xb7, 0xaa, 0xad, 0xa4, 0x1f, 0xba, 0x0b, + 0xb5, 0x88, 0x77, 0x8a, 0x7f, 0x65, 0x20, 0x2a, 0xd8, 0x11, 0xea, 0x73, 0xd2, 0x6c, + 0x74, 0x55, 0x03, 0x95, 0xaf, 0xf7, 0x53, 0x25, 0x10, 0x7c, 0x9b, 0x3f, 0x9a, 0xe9, + 0xdc, 0xdc, 0xd8, 0x6e, 0xd0, 0x81, 0xa2, 0xe7, 0x42, 0x47, 0x19, 0xa3, 0xd1, 0x85, + 0xb7, 0xe0, 0xa4, 0x3a, 0x47, 0x2e, 0x29, 0x8a, 0xc0, 0xaf, 0xdc, 0x52, 0x87, 0xd7, + 0xad, 0x12, 0x4c, 0xd9, 0x40, 0x5a, 0x62, 0xcd, 0x1c, 0xa0, 0x8b, 0x28, 0x2e, 0xfe, + 0xf7, 0xf9, 0x28, 0xdf, 0x76, 0xe2, 0x82, 0x1a, 0x41, 0x84, 0x13, 0xeb, 0x7c, 0xea, + 0xa5, 0xff, 0x12, 0x90, 0xb0, 0x3e, 0xc9, 0x1c, 0xe6, 0xdd, 0x28, 0x13, 0x0c, 0x3a, + 0xb0, 0xb2, 0x3b, 0x60, 0x2b, 0xd5, 0xbe, 0x5d, 0xc2, 0x60, 0x03, 0xaa, 0xe0, 0x4b, + 0x33, 0xd7, 0xbd, 0x25, 0x90, 0xe9, 0x0c, 0x8c, 0x38, 0x8e, 0xa7, 0x95, 0x51, 0x22, + 0xdb, 0xac, 0xa6, 0x7b, 0x30, 0x39, 0x5a, 0x92, 0x8b, 0x57, 0xb8, 0x57, 0x51, 0x23, + 0x20, 0x5a, 0xe1, 0x91, 0x52, 0xe4, 0x1e, 0x00, 0x29, 0x31, 0xb4, 0x57, 0x46, 0x19, + 0x8e, 0x5d, 0xd9, 0x57, 0x1a, 0x56, 0xa7, 0xe0, 0xd4, 0x23, 0xff, 0x27, 0x98, 0x9d, + 0x3e, 0xb4, 0x17, 0xec, 0xd3, 0xc3, 0x09, 0x3f, 0xb8, 0x2c, 0x56, 0x58, 0xe2, 0x96, + 0x24, 0xc5, 0x32, 0x19, 0xa6, 0x0c, 0xd0, 0xa8, 0xc4, 0xda, 0x36, 0x7e, 0x29, 0xa7, + 0x17, 0x79, 0xa7, 0x30, 0x32, 0x98, 0x5a, 0x3d, 0x1f, 0xd0, 0x3d, 0xd4, 0xd0, 0x6e, + 0x05, 0x56, 0x6f, 0x3b, 0x84, 0x36, 0x7c, 0xf0, 0xfa, 0xee, 0x9b, 0xc3, 0xbd, 0x7a, + 0x3a, 0x60, 0x6a, 0x9f, 0xdb, 0x84, 0x9c, 0x5d, 0x82, 0xd0, 0xa6, 0x19, 0x23, 0xc2, + 0xe5, 0xd8, 0xaa, 0x63, 0xa8, 0xa5, 0x0c, 0x38, 0xbd, 0x03, 0x87, 0x72, 0xc4, 0x14, + 0x3d, 0x8b, 0x7a, 0xcf, 0xd7, 0x4e, 0x72, 0xc0, 0x4d, 0x89, 0x24, 0x8d, 0xff, 0x20, + 0xfe, 0x8d, 0xc5, 0xec, 0x21, 0x49, 0x05, 0x4e, 0xa2, 0x41, 0x64, 0xe8, 0x5f, 0x67, + 0x44, 0xad, 0x0c, 0xac, 0xf1, 0xa8, 0xb7, 0x01, 0x26, 0xf4, 0x82, 0xc0, 0x92, 0xed, + 0x9f, 0x61, 0x27, 0xd2, 0x05, 0x0d, 0x12, 0xe8, 0x78, 0xa7, 0x96, 0x53, 0xa1, 0xe8, + 0x4d, 0xae, 0xc3, 0xeb, 0xe6, 0x2d, 0x5f, 0x6c, 0x4a, 0xbe, 0x5c, 0xe9, 0x0a, 0x7f, + 0xe2, 0xe5, 0x2a, 0x8d, 0x78, 0x46, 0xe8, 0xed, 0xf2, 0xf2, 0xbc, 0xe0, 0x5a, 0x03, + 0x7c, 0x82, 0x6f, 0x22, 0xca, 0xad, 0x12, 0x61, 0x46, 0x7d, 0xcf, 0xb7, 0xd6, 0xb6, + 0x13, 0x3d, 0xc2, 0x1e, 0x80, 0x96, 0xc7, 0xe9, 0xf8, 0xe9, 0xe1, 0x0c, 0x1e, 0x3f, + 0xac, 0x40, 0x58, 0xb6, 0x82, 0xc6, 0x8e, 0x54, 0xfa, 0xca, 0xe0, 0xf9, 0xc2, 0xdd, + 0x4d, 0x64, 0xd9, 0x04, 0x61, 0x52, 0xb4, 0x76, 0x23, 0x32, 0x93, 0x9f, 0x17, 0xe6, + 0xaa, 0xf7, 0xd8, 0xb9, 0xd3, 0x58, 0xe2, 0x21, 0x8d, 0x4e, 0x0d, 0x69, 0xa4, 0xf1, + 0x19, 0xe1, 0xc6, 0x4e, 0xec, 0x4c, 0x8b, 0x53, 0x28, 0x09, 0x70, 0x71, 0x31, 0xf0, + 0x1f, 0x55, 0xc7, 0xad, 0x04, 0xcf, 0xb6, 0x3f, 0x7c, 0x4a, 0x3d, 0x0a, 0x2b, 0x0f, + 0xfb, 0x0b, 0x05, 0xa6, 0xbe, 0x05, 0x5b, 0x8c, 0x94, 0xca, 0x80, 0xbb, 0x0a, 0x1d, + 0x13, 0xcd, 0x4c, 0xd6, 0x9a, 0xb9, 0x83, 0x04, 0xae, 0x25, 0x15, 0xd5, 0xf7, 0x69, + 0x9d, 0x4a, 0xbe, 0xe5, 0xc2, 0x0b, 0xe6, 0x09, 0xd8, 0x73, 0x51, 0x10, 0x12, 0xf2, + 0x34, 0xbd, 0x85, 0xa7, 0xef, 0xf5, 0xfb, 0x63, 0x4c, 0xff, 0x26, 0x58, 0xba, 0x65, + 0x16, 0x04, 0x85, 0x63, 0x09, 0x5e, 0xce, 0xfb, 0x30, 0x15, 0xee, 0x3f, 0x03, 0xca, + 0x52, 0xa1, 0x77, 0xf2, 0x61, 0xec, 0xdc, 0x26, 0xbc, 0x08, 0x9d, 0x34, 0xc6, 0x40, + 0x48, 0x46, 0xe9, 0xc6, 0x47, 0xfc, 0xfe, 0x98, 0xcc, 0x6a, 0xcd, 0xbb, 0x46, 0x4f, + 0x64, 0x27, 0x8a, 0xd8, 0xce, 0x9d, 0x1a, 0xe0, 0xd4, 0x15, 0xbc, 0x0c, 0x05, 0x24, + 0x5f, 0xdd, 0xaf, 0x4e, 0xbc, 0x8d, 0xc7, 0x03, 0xa8, 0x5c, 0xb2, 0x70, 0xf7, 0x96, + 0xad, 0x2d, 0x93, 0x7e, 0x2a, 0xc0, 0xd5, 0xe0, 0xa3, 0x48, 0x21, 0x75, 0x80, 0x00, + 0xaa, 0x59, 0xc9, 0xd4, 0x65, 0x24, 0x85, 0x29, 0x4e, 0xe0, 0xab, 0x29, 0x69, 0x6b, + 0x21, 0x43, 0x0f, 0xa5, 0x4d, 0xcf, 0xbf, 0x2b, 0x9c, 0x49, 0xd1, 0x42, 0x06, 0x42, + 0x09, 0xee, 0xee, 0xd4, 0xd4, 0x71, 0xff, 0xc0, 0x17, 0xd4, 0xe2, 0x0a, 0x79, 0x6b, + 0x09, 0x27, 0x80, 0x4c, 0x06, 0x1b, 0x9f, 0x4a, 0x70, 0x91, 0xfe, 0x01, 0x5a, 0xda, + 0x68, 0xfd, 0x84, 0x42, 0xe0, 0x18, 0x25, 0xc8, 0x8d, 0xfe, 0x55, 0xcf, 0x5d, 0xe3, + 0x89, 0x36, 0xf7, 0xce, 0x25, 0x31, 0x1b, 0x90, 0x2b, 0xa9, 0x7a, 0x3c, 0x12, 0xa9, + 0x5c, 0xfa, 0x1c, 0x3a, 0x59, 0x1b, 0x81, 0x8f, 0x60, 0x83, 0x27, 0x09, 0xd9, 0xe4, + 0x83, 0x9e, 0x41, 0x0f, 0xb3, 0x6b, 0x84, 0xf3, 0xac, 0x4f, 0x07, 0x0f, 0xc3, 0x5e, + 0x16, 0x19, 0x78, 0x25, 0x9e, 0x5b, 0x8e, 0xdc, 0x74, 0x4d, 0x90, 0x91, 0x9a, 0xa7, + 0x70, 0xbb, 0x36, 0x21, 0x51, 0x28, 0xe5, 0x82, 0xb5, 0x96, 0x41, 0xe2, 0x38, 0x52, + 0xe9, 0x58, 0xeb, 0x8f, 0xc3, 0xc0, 0xaa, 0x96, 0x15, 0x2b, 0xa4, 0xf7, 0x7f, 0x13, + 0x8d, 0x6a, 0x67, 0x12, 0xa3, 0xae, 0x32, 0x26, 0x01, 0x58, 0x83, 0xf8, 0x1d, 0xb2, + 0x3e, 0x58, 0x3c, 0x86, 0x9c, 0x4c, 0x71, 0x14, 0x3a, 0x6f, 0xff, 0xd6, 0x5e, 0x8d, + 0xfd, 0xc5, 0x0c, 0x99, 0xa2, 0xf1, 0xf3, 0x14, 0xcd, 0xcc, 0x71, 0x35, 0x9e, 0x23, + 0x5f, 0x1d, 0x7d, 0xc2, 0xb5, 0xf3, 0x8e, 0xf7, 0xb9, 0x70, 0x84, 0x31, 0x63, 0xc0, + 0x3f, 0x9d, 0xd4, 0x0a, 0x80, 0x15, 0xef, 0xdc, 0x87, 0x91, 0x95, 0x6a, 0x3f, 0x3c, + 0xed, 0xd9, 0xea, 0x64, 0xf8, 0xef, 0xa7, 0xa0, 0x81, 0x5a, 0x70, 0x38, 0x1d, 0x71, + 0x46, 0x78, 0x17, 0xbd, 0x04, 0xca, 0x52, 0x9a, 0xed, 0xe0, 0x7f, 0xf6, 0x0d, 0x17, + 0x6a, 0xed, 0x0f, 0x85, 0x5a, 0x2e, 0xae, 0xa8, 0x9e, 0xae, 0xac, 0xa8, 0x93, 0x58, + 0xc0, 0x81, 0x82, 0x6a, 0x08, 0x12, 0xa5, 0xbc, 0xa2, 0x8b, 0xe1, 0x37, 0x3f, 0x08, + 0x6d, 0xbd, 0xba, 0x7e, 0x43, 0xe2, 0x03, 0x21, 0x2c, 0x9f, 0xed, 0x21, 0x47, 0x4b, + 0xa1, 0x9a, 0x05, 0x5f, 0xfc, 0xc1, 0x79, 0x41, 0x2e, 0x89, 0x3a, 0x74, 0x48, 0x32, + 0x29, 0x8c, 0x5f, 0xe2, 0x4c, 0xc6, 0xb1, 0x86, 0x67, 0xf4, 0x9b, 0x34, 0xdf, 0xb1, + 0x23, 0x79, 0x26, 0x74, 0x19, 0xa9, 0xcb, 0x94, 0x03, 0xd8, 0x16, 0x7d, 0x8d, 0x1e, + 0x91, 0xd2, 0x81, 0x1a, 0x04, 0x3b, 0x29, 0x24, 0x3b, 0x06, 0x9b, 0x37, 0x58, 0x78, + 0x47, 0xdc, 0x6f, 0xcd, 0xdb, 0x18, 0x31, 0xbd, 0x1c, 0xc2, 0x56, 0x7c, 0xa0, 0x33, + 0xac, 0x40, 0xf7, 0x4a, 0xb6, 0x95, 0x5f, 0x68, 0x3b, 0x12, 0xe4, 0xe8, 0x25, 0x4e, + 0x4e, 0xa7, 0x60, 0xd3, 0x8b, 0x3f, 0x46, 0x79, 0x1c, 0x5c, 0x4c, 0xb1, 0x2b, 0xc7, + 0xcc, 0xb0, 0xed, 0x18, 0x65, 0xf2, 0x5d, 0x60, 0x1c, 0x30, 0x3f, 0x81, 0xfb, 0x1f, + 0xa1, 0xdb, 0x48, 0x53, 0x3d, 0x3d, 0x6b, 0x28, 0x8e, 0x4d, 0x9a, 0x4d, 0xff, 0x8e, + 0xc2, 0x1c, 0x96, 0xf5, 0x78, 0x39, 0x97, 0x10, 0xc8, 0x25, 0xfe, 0x7e, 0x32, 0xf9, + 0x3a, 0x8c, 0x07, 0x43, 0xf9, 0xeb, 0xd5, 0x4c, 0xc1, 0x51, 0xc7, 0x61, 0x03, 0x37, + 0xae, 0xbf, 0x7e, 0x9b, 0x91, 0x57, 0x20, 0xa5, 0x43, 0x51, 0xd4, 0x9a, 0xb8, 0xc2, + 0x2f, 0xa3, 0x49, 0x98, 0xdc, 0xf5, 0x83, 0xd4, 0x38, 0x73, 0x61, 0xef, 0x3f, 0xf8, + 0x6f, 0x50, 0xec, 0x53, 0xf4, 0x92, 0x49, 0xe4, 0xad, 0x34, 0x96, 0x03, 0x06, 0x6f, + 0xc9, 0xc6, 0x61, 0xd6, 0x9f, 0x91, 0x1d, 0xfa, 0x72, 0x41, 0xc8, 0xd5, 0x79, 0x2d, + 0x43, 0xc4, 0x57, 0xd5, 0xde, 0x96, 0x52, 0x3a, 0x53, 0xd6, 0x67, 0xec, 0x5c, 0x4e, + 0xf9, 0xd5, 0x02, 0xa1, 0x6f, 0x15, 0x22, 0x47, 0x58, 0x96, 0xd7, 0x9b, 0xc5, 0x78, + 0x33, 0xe9, 0x77, 0x17, 0x1c, 0x32, 0x4d, 0xce, 0x2a, 0x1e, 0xa1, 0xe4, 0x30, 0x4f, + 0x49, 0xe4, 0x3a, 0xe0, 0x65, 0xe3, 0xfb, 0x19, 0x6f, 0x76, 0xd9, 0xb8, 0x79, 0xc7, + 0x20, 0x08, 0x62, 0xea, 0xd1, 0x8d, 0xea, 0x5f, 0xb6, 0xa1, 0x7a, 0xce, 0xa3, 0x33, + 0x86, 0xeb, 0x4c, 0xa1, 0xb5, 0x14, 0x86, 0xa9, 0x14, 0x8f, 0xbd, 0xf9, 0xa9, 0x53, + 0x32, 0xaa, 0x60, 0x5c, 0x5d, 0x54, 0x83, 0xce, 0x4b, 0xa8, 0xec, 0xe0, 0x1a, 0x8f, + 0xf2, 0xb7, 0xef, 0x82, 0xd0, 0x5c, 0x0b, 0x6e, 0x86, 0x1b, 0x91, 0x5f, 0x13, 0xca, + 0x0e, 0xb3, 0xea, 0x13, 0xd5, 0x07, 0x08, 0x07, 0xa2, 0xcb, 0x66, 0x80, 0xa2, 0x49, + 0xea, 0x9c, 0x72, 0x24, 0x39, 0x2c, 0xbc, 0x8a, 0xb8, 0x25, 0x01, 0xb2, 0x6f, 0x11, + 0x2a, 0xc7, 0x89, 0xa1, 0x2a, 0x31, 0xad, 0x13, 0x14, 0xe2, 0xed, 0xe0, 0x8f, 0xad, + 0x31, 0x43, 0xaf, 0x30, 0xc2, 0x7f, 0x40, 0x3b, 0xc8, 0x66, 0xc7, 0x55, 0x17, 0x78, + 0x52, 0xaf, 0xd0, 0xab, 0xb9, 0x0a, 0xde, 0x1d, 0x68, 0x27, 0x26, 0xf4, 0x20, 0x08, + 0xb4, 0x6a, 0xd7, 0xf8, 0xab, 0xdb, 0x18, 0x11, 0x7f, 0x72, 0x64, 0x13, 0x90, 0xf0, + 0x86, 0xb6, 0xe1, 0x49, 0x8b, 0xe6, 0x95, 0x48, 0x52, 0x7e, 0x6a, 0xda, 0x2b, 0x38, + 0xb9, 0xfe, 0x12, 0x1e, 0xf6, 0x70, 0xaf, 0x74, 0x37, 0xd3, 0x25, 0x36, 0xd5, 0xcf, + 0x5c, 0x4a, 0xb1, 0x9d, 0xd9, 0x97, 0x71, 0x58, 0x2d, 0x03, 0x81, 0x04, 0xb7, 0xe0, + 0x39, 0xa3, 0x76, 0xf7, 0xac, 0xbb, 0xea, 0xdb, 0x34, 0xf9, 0x45, 0xbe, 0xb9, 0xd7, + 0xca, 0x0e, 0x4e, 0x3d, 0x5c, 0x5e, 0x4e, 0xb1, 0xd8, 0x52, 0x6e, 0xbd, 0x13, 0xda, + 0xcb, 0x1b, 0xa3, 0x57, 0x35, 0xc6, 0xd0, 0x4a, 0x45, 0x55, 0xac, 0xf4, 0xbf, 0x11, + 0x76, 0x26, 0x50, 0x0d, 0x77, 0xb3, 0x81, 0x89, 0xdd, 0x48, 0x88, 0x04, 0x12, 0x25, + 0xac, 0xbe, 0x38, 0x74, 0xa4, 0xc0, 0xf6, 0x07, 0xfe, 0x67, 0x45, 0xf9, 0x35, 0x5b, + 0x3f, 0xa1, 0x88, 0xf1, 0xd6, 0x5c, 0x09, 0xf3, 0x89, 0xaf, 0x1b, 0x9d, 0x62, 0x32, + 0xaa, 0x79, 0x44, 0x79, 0x19, 0xc5, 0x50, 0xf6, 0xf3, 0x1f, 0xec, 0x35, 0x48, 0x1c, + 0xb9, 0x22, 0xde, 0x2d, 0xb5, 0xb4, 0xda, 0x2f, 0x81, 0x94, 0x86, 0x17, 0x02, 0x8e, + 0x32, 0x17, 0x06, 0xa3, 0xa7, 0x78, 0xc1, 0x93, 0x8c, 0x44, 0x3b, 0xb0, 0x0e, 0x5b, + 0x0f, 0xf0, 0x6a, 0xd8, 0xab, 0x9b, 0x1a, 0xb0, 0xc1, 0x14, 0x77, 0x67, 0x3f, 0x85, + 0xdf, 0x95, 0x61, 0xdb, 0xea, 0x45, 0xd5, 0xf9, 0x78, 0x1e, 0xbe, 0x31, 0x7a, 0x07, + 0x10, 0xae, 0x54, 0x61, 0xe3, 0x4f, 0xe6, 0xf1, 0xb1, 0xaa, 0x9b, 0x4e, 0x67, 0xb1, + 0x49, 0x10, 0x98, 0x48, 0x02, 0xc2, 0xa7, 0xe3, 0x81, 0x93, 0xbc, 0x7b, 0xdc, 0x8b, + 0xa3, 0xe4, 0xe3, 0xd1, 0xd9, 0x33, 0xbf, 0xb5, 0x80, 0xf5, 0xb3, 0xe8, 0x7a, 0x2a, + 0x06, 0x51, 0x70, 0x51, 0x41, 0x0f, 0xe1, 0xb4, 0xff, 0x1e, 0xa0, 0xad, 0xe8, 0x24, + 0xf3, 0x38, 0x51, 0x54, 0x56, 0xa5, 0x7c, 0x7a, 0x91, 0x6a, 0x74, 0x38, 0x8e, 0xe8, + 0xf1, 0x28, 0x1f, 0x9a, 0xde, 0x0a, 0xe2, 0xa2, 0x61, 0x3a, 0x06, 0x12, 0xc4, 0x69, + 0xdf, 0x79, 0x2b, 0x8d, 0xf4, 0xca, 0xe4, 0xfc, 0x25, 0xc1, 0xca, 0xdb, 0xa9, 0x5a, + 0x80, 0x7c, 0xe6, 0x1e, 0x5a, 0x53, 0x03, 0xfa, 0xaf, 0x9e, 0x14, 0x65, 0x39, 0x96, + 0xb5, 0xa8, 0xad, 0xc3, 0x4f, 0xd4, 0x75, 0xef, 0x14, 0x99, 0x09, 0x4b, 0xab, 0xaf, + 0x1f, 0x3f, 0x07, 0xda, 0x9a, 0x39, 0x0b, 0x1d, 0x9f, 0xc9, 0xa0, 0x83, 0x27, 0x98, + 0x7a, 0xdf, 0xe9, 0x56, 0x48, 0x63, 0xfb, 0xdf, 0xa8, 0xf6, 0xb4, 0x6a, 0x88, 0x41, + 0x58, 0x30, 0x99, 0xaf, 0xb7, 0x87, 0x01, 0x18, 0xfa, 0xce, 0x76, 0x34, 0x7e, 0x40, + 0xb6, 0xfd, 0x8c, 0xd1, 0x55, 0x82, 0xae, 0x8e, 0x23, 0xbe, 0x9a, 0x02, 0x19, 0xbc, + 0x3e, 0x4e, 0x45, 0x46, 0xa3, 0x0d, 0x3b, 0xbb, 0xbd, 0x16, 0x86, 0x08, 0x68, 0x76, + 0xbe, 0x0e, 0x4c, 0x85, 0x9b, 0xe7, 0x1f, 0xb5, 0x8f, 0x4f, 0xab, 0x3d, 0x28, 0xc0, + 0xb4, 0xf7, 0xe7, 0x5a, 0xd1, 0xed, 0xb7, 0xf8, 0x89, 0x46, 0xfb, 0x40, 0xcf, 0xa5, + 0x78, 0x6a, 0x0f, 0xcb, 0xa1, 0x30, 0x3c, 0x83, 0x47, 0xec, 0xee, 0x93, 0xd4, 0x6d, + 0x14, 0x0b, 0xb5, 0xf6, 0x95, 0x31, 0xd6, 0x66, 0x54, 0x8b, 0x10, 0x9c, 0xe7, 0x64, + 0xbe, 0xad, 0x7c, 0x87, 0xbd, 0x4c, 0x87, 0x64, 0x94, 0xde, 0x82, 0xdb, 0x6e, 0x50, + 0x73, 0xa6, 0xc9, 0x4f, 0x7c, 0x09, 0x9a, 0x40, 0xd7, 0xa3, 0x1c, 0x4a, 0x04, 0xb6, + 0x9c, 0x9f, 0xcc, 0xf3, 0xc7, 0xdd, 0x56, 0xf5, 0x54, 0x47, 0x76, 0xc5, 0x3b, 0x4d, + 0xf7, 0x95, 0x39, 0x81, 0xd5, 0x5a, 0x96, 0xa6, 0xdc, 0xff, 0x99, 0x04, 0xa9, 0x08, + 0x42, 0xe5, 0xba, 0xfe, 0xc8, 0x84, 0x0c, 0x2d, 0x25, 0x5b, 0xf5, 0xad, 0x61, 0xc4, + 0x60, 0xf9, 0x8f, 0xeb, 0x82, 0xa1, 0x0f, 0xa1, 0xc0, 0x99, 0xf6, 0x27, 0x76, 0x79, + 0x82, 0x36, 0xc5, 0xca, 0x7f, 0x1e, 0x46, 0xeb, 0xdb, 0x2b, 0x14, 0x4d, 0x87, 0x13, + 0xe5, 0x6c, 0x77, 0x2f, 0x2c, 0x3b, 0x86, 0x0e, 0xa5, 0xb0, 0x3a, 0x88, 0x54, 0xbc, + 0x6e, 0x65, 0x90, 0xd6, 0x3c, 0xc0, 0xea, 0x54, 0xf1, 0x0b, 0x73, 0xba, 0x24, 0x1b, + 0xf7, 0x4b, 0x63, 0x55, 0x51, 0xa2, 0xaa, 0xca, 0x96, 0x87, 0xac, 0x52, 0x69, 0xfd, + 0x36, 0x8b, 0x26, 0xd7, 0x0a, 0x73, 0x7f, 0x26, 0x76, 0x85, 0x99, 0x8a, 0x3f, 0x7d, + 0x26, 0x37, 0x91, 0x49, 0x09, 0xc7, 0x46, 0x49, 0x5d, 0x24, 0xc4, 0x98, 0x63, 0x5e, + 0xf9, 0x7a, 0xc6, 0x6a, 0x40, 0x08, 0x94, 0xc0, 0x9f, 0x73, 0x48, 0x8e, 0xb7, 0xcf, + 0x33, 0xf6, 0xda, 0xd1, 0x66, 0x6a, 0x05, 0xf9, 0x1a, 0xd7, 0x75, 0x79, 0x65, 0xc2, + 0x99, 0x36, 0xe7, 0xfa, 0x48, 0xd7, 0x7e, 0x89, 0xee, 0x09, 0x62, 0xf5, 0x8c, 0x05, + 0x1d, 0x11, 0xd0, 0x55, 0xfc, 0xe2, 0x04, 0xa5, 0x62, 0xde, 0x68, 0x08, 0x8a, 0x1b, + 0x26, 0x48, 0xb8, 0x17, 0x4c, 0xbc, 0xfc, 0x8b, 0x5b, 0x5c, 0xd0, 0x77, 0x11, 0x5a, + 0xfd, 0xe1, 0x84, 0x05, 0x05, 0x4e, 0x5d, 0xa9, 0xa0, 0x43, 0x10, 0x34, 0x2c, 0x5d, + 0x3b, 0x52, 0x6e, 0x0b, 0x02, 0xc5, 0xca, 0x17, 0x22, 0xba, 0xde, 0xee, 0x23, 0xd1, + 0x45, 0xe8, 0xeb, 0x22, 0x13, 0xfc, 0x4a, 0xf1, 0xe4, 0x50, 0xe4, 0xd5, 0x21, 0x7c, + 0x66, 0x17, 0x00, 0x8c, 0x78, 0xf4, 0xfb, 0x11, 0x12, 0xf4, 0x02, 0x8a, 0x70, 0x4f, + 0xc5, 0xa9, 0x38, 0x2c, 0x6b, 0x03, 0xe7, 0xd8, 0x08, 0x5e, 0x90, 0x6c, 0xf8, 0x4c, + 0xa2, 0xc1, 0x20, 0x7c, 0x87, 0xa2, 0xbc, 0xe2, 0x08, 0x0a, 0x98, 0x91, 0x66, 0x8d, + 0x69, 0xb0, 0x44, 0xbe, 0xce, 0xd6, 0xcd, 0xa3, 0x2c, 0x22, 0x9c, 0x91, 0x17, 0x91, + 0x7a, 0xa0, 0x7d, 0xdf, 0xfc, 0xd3, 0x77, 0x39, 0x5c, 0xba, 0x61, 0x6d, 0x63, 0xc0, + 0xb6, 0x9c, 0x01, 0xfc, 0xc4, 0x53, 0x91, 0xfd, 0x5b, 0x87, 0x63, 0xfb, 0x96, 0xd7, + 0xca, 0x33, 0x3a, 0x12, 0xde, 0x3c, 0xef, 0xa9, 0x1c, 0x6c, 0x98, 0xf9, 0x47, 0x3b, + 0x8e, 0x10, 0x4a, 0x71, 0x29, 0x3e, 0x46, 0x37, 0x47, 0x05, 0xba, 0xf6, 0x5f, 0xa4, + 0x13, 0x84, 0xba, 0x5c, 0x8e, 0x0c, 0x88, 0xa3, 0xeb, 0x07, 0xe0, 0xbe, 0x34, 0xda, + 0xdd, 0xfa, 0xbb, 0x7b, 0x65, 0x54, 0x3b, 0x5f, 0x39, 0xcb, 0x20, 0x23, 0xd4, 0x67, + 0x89, 0xeb, 0x7d, 0x98, 0x9a, 0xf7, 0x79, 0xe5, 0xb8, 0xd2, 0x83, 0x85, 0xa8, 0x5b, + 0x0d, 0xa2, 0xab, 0xe0, 0x7f, 0x0c, 0x2b, 0xb4, 0x25, 0x5f, 0xce, 0xa0, 0x31, 0x88, + 0x52, 0x7a, 0x30, 0x7d, 0x40, 0x91, 0x59, 0xe9, 0x01, 0x66, 0xfa, 0xc6, 0xa0, 0x70, + 0xba, 0x05, 0xb3, 0xe4, 0xdb, 0xfd, 0x3a, 0x2b, 0xfc, 0xc9, 0xee, 0x6e, 0xd0, 0x16, + 0xc0, 0xf6, 0x65, 0xbe, 0x81, 0x33, 0xb7, 0xdc, 0x1d, 0x86, 0x04, 0x4d, 0xb0, 0xf9, + 0xdb, 0x40, 0xfb, 0x0e, 0x9f, 0x8b, 0xc2, 0xe4, 0xdb, 0x53, 0x82, 0xa8, 0xb4, 0xf8, + 0x15, 0xb4, 0xe8, 0x43, 0x4a, 0xd0, 0xdf, 0xbc, 0x51, 0xa5, 0xe9, 0xb1, 0x45, 0xe1, + 0x59, 0x6c, 0xbf, 0x46, 0x70, 0xb7, 0xe0, 0x5d, 0xfd, 0xaf, 0xbb, 0x0c, 0xf3, 0xdd, + 0xee, 0x28, 0xd7, 0x6a, 0x82, 0x42, 0x8e, 0x8a, 0xba, 0x43, 0x64, 0xe8, 0x4b, 0xac, + 0x37, 0x92, 0x98, 0xdf, 0x29, 0x32, 0xe6, 0x9b, 0xb5, 0xd0, 0x45, 0x51, 0x6e, 0xfc, + 0x33, 0xae, 0x6c, 0xc3, 0x94, 0x7c, 0xeb, 0x09, 0xed, 0x37, 0x16, 0x67, 0x21, 0x2a, + 0x83, 0x1b, 0x54, 0x85, 0xea, 0xfc, 0xe8, 0x48, 0x81, 0x88, 0xea, 0x4e, 0x27, 0xd0, + 0xcd, 0xf7, 0xdd, 0xd3, 0x48, 0xab, 0xff, 0x77, 0x7f, 0x4a, 0x13, 0xbb, 0xc7, 0x16, + 0xb6, 0xa5, 0x94, 0x4e, 0xe7, 0x27, 0x96, 0x56, 0x90, 0xe2, 0x09, 0xb4, 0x9e, 0xb9, + 0x62, 0xc0, 0x39, 0x97, 0x5f, 0x93, 0x9e, 0xd5, 0xc6, 0xe4, 0xc4, 0x00, 0xd8, 0x87, + 0x75, 0x94, 0x33, 0xd3, 0xad, 0x71, 0x6d, 0xa0, 0xcb, 0x44, 0x61, 0x13, 0xc7, 0x72, + 0x7a, 0x64, 0xb5, 0x8c, 0x3f, 0x8a, 0x0f, 0x81, 0x18, 0x9f, 0x98, 0x00, 0x52, 0x33, + 0xa8, 0x13, 0x66, 0xae, 0xe7, 0x3c, 0xec, 0x85, 0x22, 0x8e, 0xbc, 0xfd, 0x5e, 0xe3, + 0xc3, 0xfb, 0x44, 0xdb, 0x76, 0xba, 0x24, 0x3f, 0x28, 0x42, 0xb7, 0xb5, 0xfc, 0x74, + 0x6a, 0xe5, 0x1b, 0x0b, 0xc4, 0xbd, 0x4f, 0xc9, 0xfd, 0x83, 0x35, 0x65, 0xea, 0x85, + 0x2b, 0x92, 0xb2, 0x24, 0xf6, 0x99, 0x03, 0x18, 0xad, 0x8c, 0x7d, 0x94, 0x37, 0xe2, + 0x0e, 0x2a, 0x1f, 0x20, 0xe8, 0x18, 0xf9, 0x05, 0x7c, 0x5a, 0xba, 0xaa, 0x2e, 0x5c, + 0x15, 0xb9, 0x49, 0x45, 0xcd, 0x42, 0x4c, 0x28, 0xa5, 0xfa, 0x38, 0x5d, 0xad, 0xfe, + 0x49, 0x07, 0xb2, 0x74, 0xd8, 0x42, 0x70, 0x7d, 0xb3, 0x69, 0x7a, 0x5a, 0xe6, 0xc8, + 0xf5, 0x42, 0xe5, 0xec, 0xc0, 0x7f, 0xe4, 0x73, 0x50, 0xd1, 0x01, 0x46, 0x70, 0x21, + 0x2e, 0xfe, 0x81, 0xfb, 0x7c, 0x73, 0xe8, 0x45, 0x0d, 0xf8, 0x14, 0xef, 0x62, 0x32, + 0xf7, 0x49, 0x0f, 0x63, 0xcc, 0xf0, 0x74, 0x80, 0xf8, 0x84, 0xa6, 0x6e, 0xaf, 0xfc, + 0x28, 0xfe, 0xa4, 0x48, 0xd7, 0xb4, 0x01, 0xcd, 0xae, 0x10, 0xe7, 0xc0, 0xc7, 0xf9, + 0xa7, 0xb1, 0x53, 0x31, 0x96, 0x9f, 0xc8, 0xcb, 0x36, 0x39, 0x67, 0x73, 0xde, 0x19, + 0x19, 0x31, 0xc7, 0x50, 0xf6, 0xce, 0x5c, 0xaa, 0xf2, 0x97, 0x68, 0xeb, 0xb2, 0x7d, + 0xac, 0xc7, 0x38, 0x05, 0x6a, 0x81, 0x25, 0xb4, 0x77, 0x2b, 0xf8, 0x7a, 0xe1, 0x0a, + 0x8a, 0x30, 0x9b, 0x9b, 0xd6, 0x55, 0x04, 0x3c, 0xfc, 0x31, 0x59, 0x49, 0x43, 0x68, + 0xc5, 0xab, 0x8c, 0xad, 0xb7, 0xf6, 0x71, 0xe9, 0x62, 0x6b, 0xd2, 0x63, 0xe3, 0x11, + 0x81, 0xa6, 0x04, 0xb5, 0x06, 0xa0, 0x3b, 0x43, 0x9a, 0x7f, 0xfe, 0x43, 0x55, 0x89, + 0x24, 0x77, 0xe2, 0xbd, 0xf3, 0x38, 0xc6, 0x2c, 0x39, 0x22, 0xf7, 0xd3, 0xc9, 0xa5, + 0x6c, 0x71, 0x03, 0xd9, 0x11, 0x94, 0x8a, 0x84, 0xb5, 0xae, 0x2d, 0xbb, 0x16, 0xa3, + 0x76, 0x1a, 0xdd, 0x05, 0x3a, 0x0f, 0x96, 0x7e, 0x6b, 0x5b, 0xc9, 0x42, 0x11, 0xb6, + 0x54, 0x71, 0x53, 0x26, 0x7c, 0x6e, 0xe1, 0xca, 0xd0, 0xd9, 0x74, 0xa7, 0x10, 0x88, + 0x58, 0x37, 0x35, 0xe4, 0xf6, 0x3d, 0x33, 0x15, 0x6d, 0xad, 0xd5, 0x4c, 0x2f, 0xaf, + 0x89, 0x11, 0x4a, 0x12, 0x7b, 0x97, 0xb9, 0x4c, 0xc2, 0xa2, 0x2e, 0xf3, 0x03, 0xf4, + 0x59, 0xd0, 0x4f, 0xc0, 0xb5, 0x3a, 0xce, 0x59, 0x18, 0xd4, 0x7f, 0xf3, 0x3a, 0x55, + 0x8b, 0xd7, 0x1a, 0x75, 0xf3, 0x55, 0xfb, 0xd0, 0x6b, 0xbc, 0xcf, 0x4e, 0x02, 0xc3, + 0xc0, 0xa4, 0xb6, 0x3d, 0x0c, 0xc9, 0x49, 0x80, 0x1d, 0x63, 0xa6, 0x4c, 0xb2, 0xd3, + 0x23, 0x73, 0xb2, 0xc7, 0xb2, 0x74, 0xab, 0x2d, 0xb4, 0x68, 0x21, 0x42, 0xc8, 0xb2, + 0x1d, 0x84, 0xc4, 0x81, 0xf5, 0xef, 0x21, 0xe4, 0xb5, 0xe3, 0x60, 0x34, 0x51, 0xbf, + 0x94, 0x77, 0x4d, 0x0e, 0xf4, 0x7f, 0x63, 0xfa, 0x6a, 0xbb, 0x78, 0xd2, 0x1c, 0x19, + 0x3c, 0xbe, 0x65, 0xb6, 0x95, 0xfe, 0x67, 0x42, 0x3c, 0x1e, 0x2d, 0x31, 0x2e, 0x27, + 0x76, 0xfa, 0x24, 0xec, 0xe8, 0x46, 0x83, 0xe7, 0x48, 0x76, 0xc5, 0x5e, 0xa0, 0x36, + 0x9e, 0x4e, 0xa0, 0xe8, 0x64, 0x94, 0xe0, 0x0d, 0xde, 0x23, 0x6a, 0x16, 0x89, 0x73, + 0x1f, 0x0a, 0x5d, 0x82, 0x03, 0xaf, 0xde, 0x5c, 0x42, 0x36, 0x40, 0xb8, 0x1e, 0x4f, + 0x63, 0x1c, 0x98, 0x1c, 0x11, 0xa2, 0xe1, 0xd1, 0x84, 0xc6, 0x7c, 0x52, 0x8d, 0xf9, + 0x2d, 0x53, 0xae, 0xc4, 0x4a, 0x40, 0xa4, 0xea, 0x2a, 0x13, 0x1b, 0x47, 0x33, 0xcf, + 0xe4, 0x5c, 0x6b, 0x00, 0x12, 0xc3, 0xe9, 0xe2, 0x09, 0x75, 0xba, 0xae, 0xcb, 0x02, + 0x32, 0xdf, 0x88, 0x0b, 0xd7, 0xd1, 0xde, 0x13, 0xe1, 0x34, 0x94, 0x62, 0xec, 0x8d, + 0x5d, 0xf3, 0xe7, 0x80, 0xff, 0xa7, 0x2e, 0xba, 0x8a, 0x8d, 0xf7, 0xfc, 0xf3, 0x98, + 0xec, 0x23, 0x05, 0x13, 0xca, 0x9d, 0x61, 0x23, 0xf8, 0xb9, 0xd8, 0x17, 0x85, 0x60, + 0xda, 0xf9, 0x75, 0x11, 0x19, 0x55, 0xa2, 0xbc, 0xa3, 0x42, 0x3e, 0xee, 0xfc, 0x52, + 0x7b, 0xe3, 0xa8, 0x54, 0x3e, 0xb9, 0x0a, 0x5e, 0xc0, 0x2f, 0x35, 0xa7, 0xc6, 0x4b, + 0x7d, 0xd5, 0x9a, 0x72, 0xda, 0x00, 0x74, 0x63, 0x4e, 0x01, 0xd2, 0xab, 0xf3, 0x63, + 0x7a, 0xdd, 0x77, 0xc7, 0x35, 0x0f, 0x12, 0xb0, 0x11, 0xb2, 0x94, 0x16, 0x8e, 0xc7, + 0x55, 0x76, 0xe4, 0x7d, 0x16, 0x9e, 0x39, 0x38, 0xbf, 0x6a, 0xe2, 0xaa, 0x8f, 0xf7, + 0xcf, 0xba, 0x7c, 0xac, 0xb1, 0xf9, 0x2b, 0x6e, 0x4c, 0x24, 0x97, 0xbf, 0xfa, 0x9f, + 0x17, 0xca, 0xd2, 0x42, 0xfa, 0x9c, 0x31, 0x79, 0xc1, 0xa3, 0xaa, 0x81, 0xf7, 0x36, + 0x16, 0x49, 0x57, 0x2c, 0x71, 0x5c, 0x25, 0xa1, 0xf6, 0xcd, 0x5a, 0xce, 0x82, 0xc0, + 0x0a, 0xb2, 0x34, 0x2b, 0x9c, 0x3c, 0xb4, 0xff, 0xfd, 0xda, 0x16, 0x0c, 0xa5, 0xab, + 0x9e, 0x9b, 0xaf, 0x21, 0x39, 0xef, 0x9a, 0xfb, 0xe1, 0xb1, 0xf3, 0x09, 0x46, 0x2a, + 0xfc, 0xe4, 0x62, 0xa7, 0x9b, 0xb9, 0x69, 0x8e, 0x22, 0xc9, 0x57, 0xc5, 0x90, 0xa7, + 0x53, 0xa7, 0x6b, 0x87, 0xe0, 0x09, 0x12, 0x1e, 0x06, 0xf6, 0xa1, 0xbf, 0x62, 0xa0, + 0x8b, 0xf4, 0x35, 0xd9, 0x2e, 0x2f, 0xff, 0xe8, 0x6e, 0x2a, 0x9c, 0xbb, 0xa9, 0x13, + 0x3a, 0x68, 0xe4, 0xae, 0xbf, 0x33, 0xc3, 0x84, 0x36, 0xf2, 0x54, 0x5f, 0xc2, 0xd5, + 0x28, 0x32, 0xd1, 0x65, 0xaf, 0x41, 0x5b, 0x24, 0x4a, 0xdc, 0x5f, 0x57, 0x37, 0x7d, + 0xee, 0xdf, 0x46, 0x0a, 0xa3, 0xbe, 0xb4, 0x34, 0x19, 0xc6, 0xb0, 0x82, 0xe8, 0x35, + 0xce, 0x84, 0xca, 0x13, 0xb6, 0x90, 0x8a, 0x88, 0x13, 0xc0, 0x21, 0xde, 0x9f, 0xa9, + 0xa4, 0x4e, 0x4c, 0x18, 0xdc, 0xb3, 0xd2, 0x1f, 0xaa, 0xbd, 0xb4, 0x19, 0x31, 0xb2, + 0xfd, 0x49, 0x76, 0x44, 0xdc, 0x3a, 0x15, 0x07, 0xfa, 0x5a, 0xc7, 0xc7, 0x6b, 0xee, + 0xbb, 0xdb, 0xd1, 0xd4, 0x92, 0x99, 0xa5, 0x5b, 0xd4, 0x99, 0x27, 0xe9, 0xd7, 0xf4, + 0x88, 0x4e, 0x6e, 0xd3, 0xfd, 0x5e, 0x4b, 0x7c, 0xb8, 0x35, 0xb8, 0x33, 0x08, 0x96, + 0x4e, 0x3c, 0x46, 0x87, 0x3f, 0xd6, 0x13, 0x31, 0x7b, 0x91, 0xd2, 0x92, 0x36, 0xea, + 0x90, 0xe3, 0x65, 0xd1, 0x62, 0xcc, 0x05, 0x1c, 0x84, 0x6d, 0x24, 0x21, 0x76, 0xda, + 0xf6, 0xd2, 0x86, 0x18, 0xae, 0x31, 0xfb, 0xaa, 0xe9, 0x99, 0xa9, 0x3f, 0x17, 0x5c, + 0x69, 0x38, 0xe6, 0x31, 0xa0, 0x81, 0xf2, 0xc1, 0xf3, 0xfd, 0x78, 0x25, 0x49, 0xd3, + 0xf3, 0x24, 0x57, 0x59, 0x60, 0x6d, 0x9f, 0x92, 0xd5, 0x54, 0x8a, 0xcf, 0xea, 0xdb, + 0xaf, 0x9c, 0xaa, 0x6b, 0x93, 0xdc, 0x08, 0x82, 0x8d, 0x74, 0xf6, 0xd5, 0xfd, 0xd8, + 0x33, 0x31, 0xf0, 0x96, 0x91, 0x45, 0x95, 0x52, 0x97, 0xe6, 0x9f, 0x00, 0xfd, 0x29, + 0x87, 0xf2, 0xda, 0x2b, 0x94, 0xb9, 0x95, 0xfe, 0xcb, 0xe6, 0x22, 0xa7, 0x35, 0xef, + 0x7f, 0x12, 0x07, 0xf6, 0x71, 0x62, 0x94, 0x89, 0x20, 0x2b, 0xea, 0x0b, 0x47, 0x5e, + 0x51, 0x68, 0x1a, 0xa1, 0x67, 0x78, 0xb3, 0x9b, 0xd9, 0x23, 0xc9, 0x8d, 0xc6, 0xff, + 0x83, 0x73, 0xc7, 0x9b, 0xb1, 0x70, 0x30, 0x41, 0x7b, 0xc2, 0x00, 0xc8, 0xf0, 0xb8, + 0x55, 0xac, 0xfe, 0xc1, 0x79, 0xf7, 0x67, 0x4c, 0xec, 0x27, 0x21, 0xa1, 0x0f, 0xca, + 0x69, 0x3d, 0x83, 0xcf, 0xe5, 0xb8, 0xcd, 0xcc, 0x18, 0xf8, 0x1a, 0xd6, 0x17, 0xfa, + 0x26, 0xf0, 0xdf, 0xb8, 0x36, 0x55, 0xb8, 0xa2, 0x9a, 0x7f, 0x83, 0x42, 0x32, 0x42, + 0x5e, 0x8c, 0x47, 0x45, 0x88, 0xf1, 0x8d, 0xd3, 0x26, 0xaa, 0x39, 0x6c, 0x3e, 0x47, + 0x75, 0xe0, 0x02, 0x05, 0xfc, 0x9e, 0x45, 0xf7, 0xb7, 0xd2, 0xe6, 0xd5, 0x5d, 0xcb, + 0x90, 0xe2, 0x3f, 0xf6, 0xb5, 0x08, 0x45, 0x9a, 0xa6, 0x99, 0xbf, 0xcb, + ], + jumbled: &[ + 0x5c, 0xce, 0x97, 0x19, 0xb6, 0x96, 0x34, 0x80, 0x75, 0xfb, 0x60, 0x90, 0xad, 0x64, + 0xaa, 0x2e, 0xa2, 0x1a, 0x9f, 0xc1, 0xa7, 0xe9, 0xb2, 0xd7, 0xcc, 0x6b, 0xd0, 0x1c, + 0x63, 0xbe, 0x1c, 0x81, 0xfa, 0x7a, 0xde, 0x0b, 0x98, 0x8d, 0xd8, 0xb2, 0xb6, 0xce, + 0x0b, 0xe2, 0x9b, 0x9d, 0x66, 0xb6, 0x68, 0xc7, 0x9b, 0xf5, 0xfd, 0x78, 0xb5, 0x1c, + 0xb1, 0xcd, 0x5e, 0xa1, 0x29, 0x3a, 0x0c, 0x49, 0x30, 0x16, 0x75, 0xf1, 0xfd, 0x71, + 0x90, 0xa3, 0x60, 0x1d, 0x7f, 0x03, 0x74, 0xee, 0x0e, 0xb7, 0x6b, 0x79, 0x87, 0x2a, + 0x3d, 0x27, 0xb0, 0x54, 0x03, 0x0d, 0x0e, 0x4e, 0x42, 0xb3, 0xec, 0x8b, 0xdf, 0x65, + 0x42, 0x25, 0x6c, 0x67, 0xa9, 0xa1, 0xdf, 0xef, 0xb2, 0xfc, 0xef, 0xb9, 0x41, 0x32, + 0x8f, 0xe4, 0x3a, 0xd4, 0x8b, 0xf6, 0xeb, 0x69, 0xdb, 0xb7, 0x88, 0x25, 0xee, 0x15, + 0x3c, 0x03, 0x5b, 0xaa, 0xe2, 0xb8, 0xaa, 0x58, 0x4a, 0xba, 0x96, 0x9a, 0xa3, 0xff, + 0x27, 0xd8, 0x96, 0x21, 0xab, 0x9e, 0x4b, 0x93, 0x95, 0x7a, 0xf5, 0x8f, 0xd5, 0x0e, + 0x0a, 0xdb, 0xc8, 0xf5, 0xf3, 0xba, 0x30, 0x84, 0x6e, 0xdb, 0x86, 0xf5, 0x66, 0xce, + 0x06, 0xc4, 0xad, 0xf3, 0x8e, 0x65, 0xbc, 0xe4, 0xc0, 0x2f, 0xd8, 0x4b, 0x24, 0xf9, + 0x9d, 0x33, 0xd3, 0x6d, 0x19, 0x4d, 0x0f, 0x72, 0xaf, 0xda, 0xce, 0xc0, 0xf2, 0xf4, + 0x13, 0xa6, 0x5d, 0x22, 0x67, 0xea, 0x5e, 0x40, 0x77, 0xdd, 0xa8, 0x79, 0x44, 0xc6, + 0x34, 0x85, 0x07, 0x33, 0xbb, 0x90, 0xba, 0x3a, 0xe7, 0x53, 0xd5, 0x0e, 0xc4, 0x01, + 0x37, 0x3f, 0xf6, 0xf5, 0xae, 0xe7, 0xd5, 0xcf, 0x0f, 0x44, 0xe6, 0x1d, 0x98, 0x20, + 0xe5, 0xc1, 0x99, 0xf1, 0xe4, 0x81, 0xd8, 0x58, 0xab, 0x61, 0x07, 0x13, 0x0c, 0x18, + 0x12, 0x60, 0x1f, 0x77, 0xaf, 0x74, 0xc7, 0x01, 0xde, 0x2b, 0x60, 0xc3, 0x30, 0xba, + 0x59, 0xf6, 0x65, 0x81, 0xa5, 0x02, 0x0c, 0x74, 0x57, 0x8e, 0x94, 0x96, 0xb7, 0x7b, + 0x2d, 0x26, 0x79, 0x32, 0xa6, 0x51, 0xc7, 0x2c, 0xfc, 0x65, 0xdf, 0x7e, 0xc9, 0x15, + 0x02, 0xfb, 0x73, 0x00, 0x40, 0x6a, 0xa4, 0x1c, 0xa0, 0xd1, 0x7c, 0x83, 0xf8, 0x8b, + 0x73, 0xf2, 0x3c, 0x13, 0xae, 0x7f, 0xf1, 0x8e, 0xcf, 0x34, 0xe3, 0xb8, 0x6f, 0x25, + 0x0f, 0xc0, 0x45, 0x3e, 0x3d, 0x61, 0x4d, 0xc6, 0x60, 0xc6, 0x30, 0xb3, 0xa4, 0x60, + 0x17, 0xc5, 0x8e, 0xaa, 0x80, 0x9b, 0x50, 0x59, 0x8b, 0xe7, 0xf1, 0x68, 0xa0, 0x64, + 0x1b, 0xe9, 0x16, 0x48, 0xb0, 0x42, 0x16, 0x17, 0x6c, 0xf2, 0xdc, 0xab, 0xec, 0x38, + 0x15, 0xe0, 0xf3, 0x5f, 0x79, 0xb6, 0xde, 0xa1, 0x00, 0xc5, 0xcf, 0xc2, 0xdd, 0xda, + 0x9a, 0xcd, 0x83, 0x42, 0xa1, 0x00, 0xed, 0xaf, 0x48, 0x5a, 0xd7, 0x34, 0x56, 0xaa, + 0x77, 0xcd, 0x59, 0x85, 0xf5, 0x2f, 0x27, 0x8b, 0xed, 0x92, 0x0f, 0xd9, 0xe8, 0x1f, + 0x34, 0x68, 0xe4, 0x5a, 0x47, 0xde, 0x16, 0x7a, 0x99, 0x4a, 0x01, 0x45, 0xbe, 0x2e, + 0xf0, 0x61, 0x3c, 0x40, 0x22, 0x74, 0x7d, 0x0c, 0xc6, 0xb4, 0xd7, 0x1a, 0x80, 0xf5, + 0xfb, 0x8e, 0x49, 0x52, 0x77, 0x85, 0xf8, 0x71, 0xb0, 0xc4, 0x00, 0x63, 0xe0, 0x9a, + 0xd4, 0x02, 0xc0, 0x8e, 0xfb, 0x5f, 0x3f, 0xc8, 0x0c, 0xa3, 0xe1, 0xf0, 0xcf, 0x35, + 0xe2, 0xb9, 0xd0, 0xf7, 0x03, 0xa9, 0x5f, 0x6a, 0xd5, 0xe7, 0x50, 0x70, 0x67, 0x5d, + 0x93, 0x1d, 0x3c, 0x09, 0xbe, 0xad, 0x03, 0x15, 0x4d, 0x53, 0xd0, 0x3d, 0x2a, 0x78, + 0x38, 0x49, 0xa9, 0x25, 0x2a, 0x4f, 0xba, 0x56, 0x40, 0x5e, 0x26, 0x5e, 0x94, 0x5b, + 0x40, 0xd4, 0x80, 0x78, 0x00, 0x6c, 0x51, 0xaf, 0xe6, 0x09, 0x3d, 0x94, 0xcc, 0x0a, + 0x37, 0x4e, 0x61, 0x3d, 0x3d, 0xcd, 0x2a, 0xae, 0xbe, 0x70, 0xe9, 0xe5, 0x8a, 0x84, + 0x02, 0xdb, 0x96, 0x4a, 0xab, 0x99, 0x2f, 0x62, 0x5c, 0x50, 0x9f, 0xd4, 0x9a, 0x71, + 0x5f, 0x3a, 0x6f, 0xf3, 0x99, 0x0e, 0xf6, 0x27, 0x71, 0xe5, 0xba, 0x13, 0xa4, 0xc3, + 0xa6, 0x2a, 0xf5, 0x6d, 0xe7, 0x9e, 0xa2, 0x41, 0x77, 0x15, 0x60, 0xb9, 0xf9, 0xc6, + 0xbb, 0x0e, 0xe2, 0x93, 0xe3, 0x68, 0x13, 0x03, 0x9d, 0xda, 0x99, 0x7e, 0x70, 0xb5, + 0x1b, 0xb6, 0x3c, 0x9e, 0xe1, 0x4c, 0x22, 0x64, 0x2a, 0xfd, 0x21, 0xdb, 0x26, 0x17, + 0x47, 0xb1, 0x68, 0x97, 0xfd, 0x25, 0x23, 0x77, 0xd1, 0xdb, 0x07, 0x2b, 0xd9, 0xc3, + 0xbd, 0x89, 0x5c, 0xc2, 0xae, 0xdb, 0xeb, 0xf5, 0xe7, 0xe4, 0x07, 0x77, 0x18, 0x99, + 0xe3, 0x64, 0xa0, 0x99, 0x4a, 0x2c, 0xf8, 0x7b, 0x32, 0x04, 0x0c, 0xaf, 0x77, 0x2d, + 0x61, 0x8f, 0x39, 0xd5, 0x35, 0x8f, 0x55, 0xa4, 0x0a, 0x12, 0x33, 0x74, 0x4a, 0x90, + 0xe3, 0x5d, 0x1c, 0x98, 0x62, 0x72, 0x1e, 0xd1, 0xeb, 0x08, 0xb7, 0x56, 0x0a, 0xd8, + 0xf4, 0xa4, 0x0a, 0x3f, 0x9d, 0x76, 0x0d, 0xb9, 0xaa, 0xb6, 0x15, 0x79, 0xd8, 0xa0, + 0x5c, 0xf8, 0xb9, 0xa6, 0x46, 0x5a, 0x7e, 0x93, 0x50, 0x7e, 0x42, 0x4a, 0x78, 0xec, + 0x71, 0xf5, 0x1a, 0x18, 0x0a, 0xb2, 0x68, 0xf1, 0x73, 0xa6, 0x29, 0xc4, 0xff, 0xd9, + 0x43, 0xc5, 0xf8, 0xec, 0x5f, 0xcc, 0xaf, 0x0d, 0x00, 0x09, 0xf0, 0xe9, 0xe7, 0x59, + 0xe6, 0x62, 0xa0, 0x59, 0x0e, 0xed, 0xa5, 0x18, 0x84, 0x7a, 0xc8, 0xac, 0x8b, 0x4b, + 0x12, 0xa4, 0xa9, 0x91, 0xfe, 0xbb, 0x54, 0x50, 0xfc, 0x5f, 0x7a, 0xac, 0x4f, 0x5a, + 0x6f, 0x0f, 0x59, 0xd0, 0xa2, 0xd1, 0xf9, 0xd2, 0x0e, 0x27, 0xc9, 0x2c, 0xdf, 0x16, + 0xdc, 0xaf, 0x51, 0x30, 0x78, 0x66, 0x15, 0x68, 0x2f, 0x5b, 0x7c, 0x93, 0xff, 0x04, + 0x23, 0x04, 0xd0, 0xad, 0xae, 0x15, 0xcb, 0x4c, 0x4a, 0x23, 0x1e, 0xd6, 0x7c, 0xcb, + 0xdf, 0x5f, 0xad, 0x5a, 0x6d, 0x17, 0x52, 0xf1, 0x46, 0x63, 0xab, 0x1a, 0x47, 0x17, + 0xea, 0x23, 0x21, 0x3a, 0x68, 0xa9, 0xfd, 0x27, 0xea, 0x10, 0xa7, 0x71, 0xd1, 0x9c, + 0xc7, 0x34, 0x5f, 0xc7, 0x5a, 0x29, 0x97, 0xec, 0x1b, 0x9e, 0xb2, 0x50, 0xcd, 0x2c, + 0x7d, 0x64, 0x23, 0x9a, 0x1c, 0x96, 0x0b, 0x69, 0x14, 0xac, 0x23, 0x6f, 0xd9, 0x1d, + 0xf3, 0x64, 0x0f, 0x18, 0x9f, 0xfd, 0xec, 0x5b, 0x31, 0xe3, 0x61, 0xb9, 0x6f, 0xe4, + 0x69, 0x99, 0x08, 0x72, 0xc4, 0xaa, 0x41, 0xd7, 0x86, 0xf0, 0x09, 0xdd, 0xed, 0xa8, + 0xfc, 0xc4, 0xe5, 0xcc, 0x39, 0xd0, 0xf8, 0x5a, 0x17, 0xf0, 0xbd, 0x74, 0x3a, 0xf4, + 0x5a, 0x7f, 0x43, 0x2c, 0xcf, 0xe9, 0x1e, 0x74, 0xfc, 0x73, 0x0d, 0xd3, 0xff, 0xf3, + 0x0f, 0x0b, 0x4f, 0x69, 0x27, 0xd6, 0xcd, 0xd2, 0x57, 0xf4, 0x47, 0xf9, 0x59, 0x1c, + 0x6b, 0xac, 0xfc, 0x51, 0xd7, 0x28, 0x76, 0xca, 0xb5, 0xec, 0x47, 0x16, 0x39, 0x16, + 0x4f, 0x14, 0x6d, 0xb9, 0x1c, 0x52, 0x36, 0xb2, 0x93, 0xce, 0xfe, 0x8b, 0x8c, 0xb9, + 0xb4, 0xd1, 0x0e, 0xa8, 0x77, 0x93, 0xe5, 0x43, 0x87, 0x78, 0xd2, 0xdd, 0xb8, 0x99, + 0xdc, 0xbf, 0x4a, 0x42, 0xee, 0xa5, 0x51, 0x1b, 0xfe, 0xd4, 0xff, 0xa3, 0x81, 0x51, + 0xf2, 0x2b, 0x85, 0xc9, 0x41, 0x74, 0x7d, 0x34, 0x95, 0xdd, 0xe2, 0xdf, 0xdd, 0x8c, + 0x40, 0x90, 0x20, 0x74, 0x16, 0x6e, 0xda, 0x85, 0x1e, 0x6c, 0x5a, 0xee, 0x3a, 0x70, + 0x3b, 0xdc, 0xa9, 0x71, 0x70, 0x23, 0x45, 0xe3, 0x3e, 0x36, 0x6f, 0x28, 0x26, 0x2b, + 0x64, 0xba, 0xbe, 0xf4, 0x69, 0x36, 0xad, 0xb7, 0xbd, 0x14, 0x9e, 0x36, 0x1f, 0xb0, + 0x3a, 0xbe, 0x82, 0xf0, 0x36, 0xda, 0x42, 0xf0, 0x58, 0x2a, 0x73, 0x96, 0xc4, 0xfb, + 0x4a, 0xaa, 0xec, 0xfb, 0xd9, 0xf7, 0xa5, 0xef, 0x85, 0xaa, 0x28, 0xee, 0x5a, 0x1b, + 0x47, 0x28, 0xbe, 0xbe, 0x4a, 0x7c, 0xb5, 0xf7, 0x81, 0x37, 0x44, 0xc1, 0x76, 0xc1, + 0xf0, 0x93, 0x1d, 0xd1, 0x5e, 0xbf, 0xcd, 0x6b, 0xc1, 0x35, 0xb8, 0xe2, 0x92, 0x8f, + 0x83, 0x19, 0x72, 0x89, 0x8e, 0xbc, 0xb5, 0xeb, 0x9c, 0x73, 0x9d, 0xfe, 0x9f, 0xf0, + 0xbb, 0xe3, 0xf8, 0xe9, 0x8a, 0x8c, 0xa9, 0xaf, 0xe3, 0x46, 0x39, 0x51, 0xab, 0x0f, + 0x74, 0x30, 0xfd, 0xcc, 0x22, 0x73, 0x3c, 0x23, 0x62, 0xcc, 0xf6, 0x5f, 0xcd, 0xf7, + 0x62, 0xa7, 0xa8, 0x6d, 0x50, 0xb9, 0x4a, 0xa7, 0xae, 0x1b, 0x3d, 0x9f, 0x4f, 0xa4, + 0x03, 0xec, 0x51, 0xd2, 0x17, 0x4f, 0x49, 0xdd, 0x1c, 0x45, 0x0b, 0x0a, 0x8e, 0x80, + 0x31, 0x1d, 0x34, 0x6c, 0x33, 0xe7, 0x5e, 0x00, 0xbe, 0x5c, 0x4f, 0xd5, 0x9a, 0x61, + 0x99, 0x49, 0xbd, 0xe2, 0x74, 0x9f, 0xd1, 0xd1, 0xaa, 0x92, 0xff, 0x02, 0x9b, 0x59, + 0x53, 0x00, 0x33, 0xfe, 0xf9, 0x54, 0x00, 0xa2, 0x0f, 0x5c, 0xc7, 0xc6, 0x2e, 0x1c, + 0x91, 0x1e, 0xc7, 0x19, 0xbb, 0x2b, 0xe8, 0x6d, 0x44, 0xa5, 0x9b, 0xbe, 0x48, 0x2b, + 0x65, 0xfa, 0x46, 0xd9, 0xb3, 0x4c, 0x02, 0xe7, 0xb4, 0xd7, 0x1c, 0x1c, 0x24, 0x5d, + 0x54, 0x91, 0x7b, 0xe7, 0x2a, 0x68, 0xd9, 0xd3, 0xd4, 0xe6, 0x0a, 0x51, 0xc5, 0xee, + 0xa1, 0xb8, 0xd4, 0xfe, 0xa5, 0x59, 0x6f, 0xd6, 0x33, 0xfa, 0xd4, 0x35, 0x90, 0xa5, + 0xc4, 0xe3, 0xff, 0x70, 0x6e, 0x06, 0x54, 0x77, 0x0a, 0xe1, 0xd0, 0x1e, 0xe5, 0x1f, + 0xf9, 0xfd, 0x3e, 0x1e, 0xcc, 0x3d, 0x17, 0xa8, 0xb9, 0xbc, 0x9e, 0x3b, 0x60, 0x6d, + 0xd5, 0x8f, 0xb2, 0x88, 0xfa, 0xc6, 0xb5, 0xe1, 0xbc, 0xa9, 0xb7, 0xa3, 0xfd, 0x8a, + 0x2d, 0x8d, 0xab, 0x59, 0xc4, 0x76, 0x52, 0xf5, 0xc8, 0xea, 0xf9, 0xd0, 0x6e, 0x5d, + 0xcc, 0x82, 0x4c, 0x47, 0xe4, 0x5c, 0xe4, 0xed, 0xb5, 0x5a, 0x02, 0xbb, 0x11, 0xb0, + 0x76, 0xbb, 0x74, 0x89, 0x4b, 0x56, 0xd4, 0xe9, 0x4b, 0xe6, 0x92, 0x44, 0xc5, 0xdc, + 0xcc, 0xc8, 0x13, 0x68, 0xb5, 0x53, 0xb9, 0x77, 0x96, 0xbe, 0xed, 0x54, 0x26, 0x72, + 0x0c, 0x65, 0x1a, 0xbb, 0x0e, 0x33, 0x21, 0x8e, 0x41, 0x37, 0x15, 0xad, 0xd9, 0x91, + 0x23, 0xd4, 0x95, 0x1f, 0x71, 0xd8, 0xa9, 0xd4, 0xee, 0x35, 0xb2, 0xc3, 0xc1, 0xd0, + 0x7b, 0xd4, 0x47, 0x63, 0xac, 0x59, 0xa7, 0xe7, 0x8e, 0xd0, 0xb5, 0x9d, 0xce, 0xf1, + 0xb0, 0x9d, 0x69, 0xc5, 0x0f, 0xa4, 0xa1, 0x94, 0x0b, 0x42, 0xb6, 0x23, 0x3e, 0xfc, + 0xfb, 0xe7, 0xea, 0x64, 0x92, 0x28, 0xe9, 0x1e, 0x99, 0xb3, 0x8c, 0x0d, 0xe3, 0x01, + 0xe6, 0x92, 0x74, 0x50, 0x18, 0xef, 0xb3, 0x75, 0xe9, 0xd6, 0x03, 0x99, 0x60, 0x29, + 0x93, 0xbc, 0x06, 0x05, 0xb5, 0xad, 0x08, 0xa2, 0x52, 0xc0, 0x13, 0xa2, 0x82, 0xe6, + 0x5f, 0xfe, 0x54, 0x87, 0x61, 0xa0, 0xf4, 0x54, 0x2b, 0x57, 0xd3, 0xa5, 0xdb, 0x83, + 0x0c, 0x68, 0xda, 0x8a, 0x7e, 0x19, 0xe8, 0x12, 0x11, 0x40, 0xc0, 0x99, 0x5b, 0xfb, + 0x9a, 0xd7, 0x77, 0x3d, 0x57, 0x8c, 0x3c, 0x88, 0x31, 0xf2, 0x5a, 0x1f, 0xb2, 0x7f, + 0x89, 0x59, 0x33, 0xd6, 0x39, 0xd9, 0x92, 0x5a, 0x58, 0x3c, 0x2b, 0xc6, 0x28, 0x78, + 0x10, 0x46, 0x0f, 0x62, 0xe1, 0x81, 0xc8, 0x0a, 0x25, 0x60, 0x8e, 0x55, 0x49, 0x2c, + 0x2a, 0x2a, 0xde, 0x11, 0x1f, 0x99, 0x85, 0x61, 0x00, 0x74, 0x2a, 0x91, 0x7f, 0xd0, + 0xe9, 0x27, 0xb3, 0xb0, 0x46, 0x79, 0xbd, 0xf7, 0x4a, 0x60, 0x0e, 0xe5, 0x11, 0xc7, + 0x0f, 0xd1, 0x6b, 0x08, 0xd6, 0x67, 0x50, 0xcf, 0xa2, 0x1d, 0xb3, 0x20, 0x11, 0x64, + 0xaa, 0x4f, 0x2d, 0xe3, 0xea, 0xf5, 0xf4, 0x60, 0x57, 0xad, 0xf1, 0xdc, 0x2b, 0xeb, + 0x69, 0x8a, 0xea, 0x01, 0x98, 0x9b, 0x94, 0x33, 0xc3, 0xf8, 0x25, 0xec, 0xbb, 0x30, + 0x81, 0x44, 0x0f, 0x15, 0x04, 0xdb, 0xd8, 0x58, 0x74, 0xe3, 0x69, 0xda, 0x41, 0xbc, + 0xc1, 0x69, 0x34, 0xbc, 0xd2, 0x22, 0x6c, 0xbc, 0xbc, 0x4e, 0xf8, 0x40, 0x24, 0xea, + 0x4a, 0x2b, 0x7b, 0x15, 0xf6, 0xe4, 0xc5, 0x4c, 0x8f, 0xa4, 0x63, 0xce, 0x4e, 0x96, + 0x98, 0xa6, 0x01, 0xac, 0xfa, 0x96, 0x5e, 0xe5, 0x74, 0xb3, 0xb8, 0xd6, 0xa2, 0x4d, + 0xdb, 0x2f, 0x5e, 0xb8, 0x7f, 0xf5, 0x79, 0xa3, 0x9e, 0x93, 0x26, 0xe7, 0xc0, 0xb5, + 0xf6, 0x26, 0x4c, 0x9c, 0x87, 0x9f, 0x8c, 0x11, 0x80, 0x4a, 0xcb, 0x0c, 0x32, 0xff, + 0xca, 0x96, 0x40, 0xc7, 0x59, 0x15, 0xdb, 0xde, 0x5b, 0x54, 0x3b, 0x7f, 0xca, 0xf7, + 0xfc, 0x1d, 0x9e, 0xf4, 0x94, 0xa5, 0x8c, 0x10, 0xbd, 0x9a, 0xeb, 0xd6, 0x04, 0xeb, + 0x6b, 0xd6, 0x45, 0xac, 0xb5, 0xed, 0x69, 0xb9, 0x4c, 0xc5, 0xb8, 0xc8, 0x53, 0x6e, + 0xce, 0x44, 0xbe, 0xc4, 0xf0, 0xa4, 0x46, 0xba, 0x87, 0xa0, 0x2c, 0xd9, 0xb3, 0x34, + 0x66, 0xb2, 0xbc, 0x00, 0x10, 0x97, 0xd4, 0x5b, 0xcc, 0x34, 0xe6, 0x27, 0x7d, 0x00, + 0x17, 0xfe, 0xc9, 0x24, 0xa4, 0x8c, 0xcd, 0xb5, 0x56, 0x8c, 0x79, 0x43, 0x0a, 0x38, + 0xcd, 0x3c, 0x2c, 0xc4, 0x46, 0x95, 0x14, 0xf9, 0xb8, 0x19, 0x61, 0x7c, 0xde, 0xcc, + 0xa3, 0x7b, 0x7c, 0x2e, 0xb0, 0x0e, 0x76, 0xb9, 0x48, 0xad, 0x06, 0x9e, 0x3d, 0x66, + 0xe4, 0xf0, 0x0b, 0x04, 0x50, 0xa4, 0x04, 0xb1, 0x75, 0xaa, 0x81, 0xfb, 0xf0, 0xc8, + 0x6b, 0x2a, 0xa3, 0xf4, 0x5a, 0x91, 0x8f, 0xc1, 0x9e, 0xaa, 0x3c, 0x45, 0xa8, 0xb5, + 0xa4, 0x29, 0x93, 0x3e, 0x28, 0xef, 0x4a, 0xa6, 0x3a, 0x15, 0xdc, 0x5b, 0xc5, 0xf9, + 0xe3, 0xbd, 0xa2, 0x87, 0x2f, 0x9e, 0xe8, 0x86, 0x7c, 0xbb, 0xa6, 0xc6, 0xdb, 0xbc, + 0x70, 0xb9, 0xde, 0x64, 0xee, 0x51, 0xb8, 0xd4, 0x98, 0x18, 0x5d, 0x02, 0x15, 0xbb, + 0x16, 0xcf, 0xe6, 0xbc, 0x4d, 0x9a, 0x77, 0xf2, 0x36, 0xa9, 0x48, 0x9e, 0x78, 0xc7, + 0x6e, 0x5a, 0x68, 0x30, 0xae, 0xc0, 0x48, 0x20, 0xa1, 0xfb, 0xd7, 0x67, 0x99, 0xe2, + 0xc6, 0x8e, 0x59, 0xfb, 0x09, 0xbb, 0xec, 0x28, 0x2e, 0x62, 0xbe, 0x68, 0xab, 0x27, + 0x39, 0xce, 0x0c, 0x04, 0xf1, 0x5d, 0x80, 0x2c, 0xe7, 0xc7, 0xc8, 0xce, 0x99, 0xbd, + 0x78, 0x23, 0x9f, 0xfe, 0x90, 0x55, 0x96, 0xce, 0xdc, 0x89, 0x8a, 0x9b, 0x57, 0x05, + 0x3d, 0x93, 0xa0, 0x87, 0x71, 0xf5, 0x8b, 0x27, 0xc6, 0x86, 0x48, 0x93, 0xb0, 0xdd, + 0x98, 0x51, 0xf7, 0x0e, 0x7b, 0xc8, 0x80, 0x1d, 0x11, 0xc2, 0x68, 0xb6, 0x11, 0x26, + 0xbf, 0x9c, 0x9f, 0xfe, 0xcd, 0xdc, 0xc8, 0x2e, 0xf5, 0x07, 0x38, 0x77, 0xa9, 0xe7, + 0x49, 0x71, 0x4d, 0xe7, 0x8b, 0x78, 0x46, 0x7f, 0xa4, 0x0d, 0x79, 0xb5, 0x4a, 0x02, + 0x06, 0x47, 0xfe, 0x52, 0x29, 0xcc, 0x19, 0xc6, 0xad, 0xe2, 0xd7, 0xcb, 0xb0, 0x47, + 0x0b, 0xdd, 0xcb, 0x52, 0xa8, 0x93, 0x8f, 0x51, 0xfb, 0x4d, 0xc2, 0x0a, 0x3b, 0x42, + 0x5f, 0x50, 0xdb, 0x62, 0xa9, 0x6a, 0x19, 0x3c, 0x0a, 0x59, 0xbe, 0x7d, 0xfe, 0x82, + 0x7b, 0xed, 0xfc, 0x2b, 0x9e, 0x78, 0xed, 0xef, 0x1f, 0x62, 0x5b, 0x01, 0x09, 0x52, + 0x3a, 0x9d, 0x43, 0x4c, 0x83, 0x9a, 0x96, 0xe5, 0x8d, 0xd5, 0x78, 0x86, 0x49, 0x79, + 0x46, 0x8b, 0xc2, 0xee, 0x8e, 0x1e, 0x41, 0xa4, 0x89, 0xa8, 0xe6, 0xb9, 0xbd, 0xd4, + 0x0d, 0x51, 0x52, 0x1e, 0x30, 0x58, 0xbd, 0xcc, 0xd6, 0xfb, 0x33, 0xb6, 0x50, 0x52, + 0xfc, 0xc7, 0x7c, 0x07, 0x18, 0x29, 0x45, 0x10, 0x5f, 0x08, 0xd3, 0x28, 0xfc, 0xa0, + 0xb9, 0xc2, 0x11, 0xc5, 0xd8, 0xeb, 0x4c, 0xac, 0xda, 0x09, 0xbd, 0x7f, 0xf2, 0x61, + 0xde, 0xd5, 0xee, 0x63, 0xce, 0xde, 0x38, 0xbc, 0xe9, 0x7e, 0xf3, 0xe0, 0x22, 0x51, + 0x42, 0x1d, 0xf9, 0xf1, 0xb7, 0xd7, 0x5c, 0xb6, 0xce, 0x8a, 0x23, 0x5c, 0x87, 0x78, + 0x00, 0xae, 0x21, 0x8c, 0x86, 0xc5, 0xd1, 0x47, 0xf3, 0x27, 0xb0, 0xdb, 0x50, 0x04, + 0x34, 0xcd, 0xf2, 0xc1, 0x7e, 0xdf, 0xd8, 0xb8, 0xb4, 0x28, 0xdd, 0xd6, 0x3a, 0xe0, + 0x6b, 0xcc, 0xfa, 0xa2, 0x1a, 0x77, 0x2b, 0x74, 0xff, 0xdf, 0x9d, 0x30, 0x58, 0xa5, + 0xc6, 0x11, 0x6a, 0xbc, 0x98, 0x39, 0x14, 0x2c, 0x56, 0x44, 0x4c, 0x4b, 0xa4, 0x70, + 0x0b, 0x6d, 0x09, 0xdd, 0x9a, 0xb1, 0x74, 0x9c, 0x02, 0x44, 0x4a, 0x06, 0xee, 0xed, + 0xde, 0x5e, 0xe2, 0x5d, 0x14, 0x59, 0x13, 0x79, 0x2b, 0x53, 0x86, 0xe9, 0xc2, 0x25, + 0xb3, 0xc8, 0xe3, 0xb1, 0x33, 0x10, 0xcd, 0xf5, 0x3a, 0xa9, 0xc8, 0xb7, 0xb0, 0x78, + 0x3a, 0x25, 0xf1, 0x49, 0x73, 0xf8, 0xf5, 0x38, 0x20, 0x97, 0x26, 0xcb, 0x08, 0x98, + 0xed, 0xbe, 0x1e, 0x95, 0x1b, 0x71, 0x2e, 0xee, 0xfa, 0xf7, 0x9d, 0x51, 0xbd, 0x5f, + 0xff, 0xde, 0xc5, 0x3e, 0x75, 0xa8, 0x36, 0x00, 0x8f, 0xa6, 0xe2, 0xf9, 0xa6, 0xb6, + 0x70, 0x9d, 0x99, 0xc9, 0x6a, 0x55, 0x75, 0xa1, 0x38, 0xd3, 0x1e, 0xc4, 0xeb, 0xbe, + 0xb0, 0x76, 0xdb, 0x31, 0x8d, 0x67, 0xa6, 0xc5, 0x01, 0xeb, 0xe5, 0xd5, 0xe2, 0xf8, + 0x93, 0x96, 0x84, 0xee, 0xd7, 0x27, 0x95, 0xd3, 0xac, 0x57, 0x3f, 0xd3, 0x56, 0x20, + 0xaa, 0x87, 0xcf, 0xf9, 0x13, 0xf2, 0xab, 0x16, 0x15, 0x15, 0xd8, 0x26, 0xa8, 0x6c, + 0x54, 0xe0, 0x4c, 0x07, 0xc0, 0xb6, 0x62, 0x91, 0x62, 0x2e, 0xe6, 0x7c, 0x82, 0x13, + 0x25, 0x40, 0x74, 0x88, 0xad, 0xe0, 0x7e, 0xd0, 0xc2, 0xef, 0xe8, 0x16, 0x2b, 0x0f, + 0x29, 0xfc, 0x72, 0x5d, 0x8f, 0xbf, 0x9e, 0xe2, 0x92, 0xa4, 0x4b, 0xb1, 0x7d, 0x73, + 0xdb, 0x94, 0x31, 0xa9, 0xe6, 0x12, 0x26, 0xa3, 0xab, 0xe7, 0x38, 0x0e, 0x8e, 0xb0, + 0x49, 0x0e, 0x5a, 0x08, 0x66, 0x23, 0x92, 0xf9, 0xca, 0xef, 0xdb, 0xd1, 0x4e, 0xdd, + 0x9f, 0x83, 0xba, 0xce, 0x71, 0xf9, 0x4f, 0x65, 0xe8, 0xc3, 0x16, 0xd6, 0x40, 0xb9, + 0x30, 0xd3, 0x73, 0x86, 0x2c, 0xdd, 0x0c, 0x19, 0x6a, 0xbc, 0x38, 0xf8, 0x55, 0xc5, + 0xf7, 0xbc, 0xe1, 0x01, 0xc6, 0x2f, 0x5a, 0x3f, 0x20, 0x0f, 0x05, 0x7b, 0xc2, 0x92, + 0x9c, 0x0e, 0x9d, 0x0d, 0xe4, 0x54, 0x97, 0xc4, 0x26, 0x53, 0x36, 0xb8, 0x65, 0xfa, + 0x48, 0xf4, 0x1d, 0xfa, 0x7b, 0xb2, 0xb2, 0x08, 0x6c, 0x42, 0x4d, 0x0c, 0x9b, 0x1b, + 0x24, 0xfb, 0x04, 0x4a, 0xdf, 0x40, 0x4b, 0x78, 0x4b, 0x5c, 0x6b, 0xa6, 0x0b, 0xa3, + 0x6d, 0xbf, 0xaa, 0x52, 0x2a, 0xf6, 0x1c, 0x86, 0x70, 0xba, 0x0c, 0xab, 0xf1, 0x3d, + 0xa1, 0x5f, 0xc6, 0x23, 0x2c, 0xfd, 0x35, 0x47, 0xa3, 0x2e, 0x9e, 0xa6, 0x4a, 0x10, + 0x28, 0x7a, 0x35, 0x21, 0x09, 0xdc, 0xbf, 0xc0, 0x6e, 0x35, 0xa2, 0x06, 0xda, 0x57, + 0x47, 0x34, 0x4d, 0x26, 0x03, 0x4a, 0xf2, 0xb7, 0x3a, 0x5a, 0x9a, 0xa3, 0xdc, 0x7d, + 0xb1, 0xc9, 0x68, 0xbc, 0xcf, 0x6f, 0x97, 0x5e, 0xed, 0xa7, 0xf2, 0x0c, 0x71, 0xca, + 0xa3, 0x5b, 0xdf, 0x75, 0xa9, 0xe3, 0x8e, 0x00, 0x5d, 0xc8, 0xc7, 0x0c, 0x75, 0xe5, + 0xba, 0x13, 0x3d, 0xa9, 0xd1, 0xf1, 0xe7, 0x07, 0xc5, 0x9b, 0x58, 0xe3, 0xe5, 0xa5, + 0xf8, 0xe2, 0xb7, 0x0d, 0x76, 0x6a, 0x33, 0x9f, 0xbc, 0xd2, 0x62, 0x22, 0x26, 0x02, + 0xe2, 0xa0, 0x80, 0x26, 0x5b, 0xd0, 0xd6, 0x67, 0x32, 0xf9, 0xa4, 0x44, 0x8c, 0xc7, + 0x2b, 0x50, 0x51, 0xc4, 0x3c, 0x5e, 0x3d, 0xfa, 0x70, 0xab, 0xd3, 0x73, 0xee, 0x69, + 0x78, 0x67, 0xdb, 0x2c, 0x2b, 0x46, 0xae, 0xbf, 0x2b, 0x5e, 0x99, 0x64, 0x0e, 0xf9, + 0x21, 0xfd, 0xb1, 0x04, 0x80, 0x6e, 0xb2, 0x52, 0x43, 0x49, 0x51, 0xd1, 0xac, 0x9e, + 0x1f, 0xc4, 0x09, 0x56, 0xc2, 0x1a, 0xfe, 0xf0, 0x1d, 0x9b, 0xea, 0x33, 0x4e, 0x45, + 0x04, 0xa4, 0x49, 0x43, 0x83, 0x5c, 0x2a, 0xea, 0x8a, 0x1a, 0xda, 0x2d, 0xd9, 0xcc, + 0x0d, 0xc7, 0x2c, 0xe6, 0x69, 0xc6, 0x29, 0xd8, 0x83, 0xaa, 0xf8, 0x14, 0x2f, 0x7d, + 0x11, 0x24, 0x55, 0x3e, 0x9a, 0x6e, 0xa9, 0xfa, 0x30, 0x46, 0xd2, 0x68, 0xae, 0xe2, + 0x1d, 0x3e, 0xd0, 0xc9, 0x56, 0x56, 0x3c, 0x22, 0x69, 0x5e, 0x74, 0x27, 0x5e, 0xe7, + 0x5e, 0xa7, 0xda, 0xd6, 0xd4, 0x54, 0xaa, 0xa5, 0x50, 0xf4, 0xaf, 0xa4, 0x30, 0x98, + 0xee, 0xda, 0xd3, 0x25, 0xbd, 0x43, 0x80, 0x5b, 0x62, 0xe0, 0xfa, 0xfc, 0xce, 0xb5, + 0x10, 0x1a, 0x70, 0x78, 0xba, 0xcb, 0x09, 0x18, 0x77, 0xfc, 0x86, 0xdc, 0x2f, 0x96, + 0x12, 0x88, 0xed, 0x79, 0xdb, 0x52, 0x46, 0xbc, 0x44, 0x5a, 0x0a, 0x1a, 0xbb, 0x8c, + 0x8a, 0x65, 0x24, 0xba, 0x42, 0xf7, 0x13, 0x4c, 0xb6, 0x61, 0x96, 0x05, 0xcd, 0x40, + 0x80, 0x8d, 0xcc, 0xf2, 0x74, 0x64, 0xf3, 0x1d, 0x4c, 0xf4, 0x69, 0x0e, 0x81, 0x66, + 0x4b, 0x77, 0x08, 0x1a, 0x56, 0x48, 0x1b, 0x63, 0x26, 0x44, 0xa2, 0x90, 0xc7, 0x3d, + 0xb8, 0x8f, 0xd7, 0x03, 0x2f, 0xf6, 0xd6, 0x1f, 0xf3, 0x51, 0x70, 0xe3, 0x01, 0x59, + 0x63, 0x50, 0x7b, 0xbc, 0x09, 0x4a, 0x6d, 0xfa, 0x5b, 0x8f, 0x0b, 0xc4, 0x9a, 0x10, + 0x61, 0x89, 0xc4, 0x93, 0x55, 0x6d, 0x92, 0x3f, 0x67, 0xa1, 0x38, 0x40, 0xac, 0x58, + 0x50, 0xd1, 0x27, 0x9f, 0xd2, 0x4c, 0xf0, 0xf6, 0x2d, 0x68, 0x4e, 0x06, 0xa7, 0x18, + 0xee, 0xd9, 0x6f, 0xf5, 0x6f, 0x36, 0x84, 0x6d, 0xd3, 0xc9, 0x3e, 0x26, 0x1a, 0x5f, + 0x47, 0xe6, 0xbd, 0x10, 0xa1, 0x1a, 0xfe, 0x7c, 0xab, 0x98, 0x4a, 0x6d, 0x5f, 0x01, + 0x5a, 0xe8, 0x4d, 0x1a, 0xd1, 0xea, 0x4c, 0x9c, 0x51, 0x89, 0xc9, 0x9f, 0xd4, 0x84, + 0x50, 0xf8, 0xe7, 0xb6, 0x30, 0x14, 0xde, 0xe3, 0x4d, 0xd7, 0x95, 0x26, 0x93, 0xcd, + 0xb9, 0x58, 0xf3, 0xfd, 0x3b, 0x7a, 0x06, 0xef, 0xa5, 0x6c, 0x1f, 0xae, 0x43, 0xcb, + 0x54, 0xd1, 0x4d, 0x20, 0xe7, 0xfc, 0xdd, 0xf0, 0x1d, 0x97, 0xe8, 0xf2, 0x11, 0x80, + 0x86, 0xcb, 0x90, 0x2b, 0xcd, 0x54, 0x98, 0x05, 0xf8, 0x21, 0xa3, 0x9f, 0x71, 0xd6, + 0x82, 0xc6, 0xfb, 0x2a, 0x04, 0xe8, 0xa1, 0x53, 0xcd, 0xf3, 0xfc, 0x63, 0x24, 0x17, + 0x29, 0x39, 0x64, 0xfd, 0x6e, 0x46, 0xfe, 0xb7, 0x82, 0x24, 0x7c, 0xd5, 0x9c, 0x78, + 0xb9, 0x32, 0x2b, 0x40, 0x77, 0x27, 0x3c, 0xb8, 0x5e, 0x9e, 0x45, 0x06, 0xfe, 0x64, + 0x9a, 0x6b, 0x65, 0x3c, 0x85, 0x49, 0xa0, 0xea, 0x23, 0x1c, 0x25, 0x38, 0xc2, 0xdb, + 0x5d, 0x9f, 0x97, 0xb7, 0x79, 0x27, 0x7d, 0x7c, 0x99, 0x50, 0x65, 0x19, 0x2d, 0xcd, + 0x7c, 0x0b, 0x3e, 0x96, 0x60, 0xcd, 0x5f, 0x5f, 0x96, 0xdd, 0x93, 0x6e, 0x0a, 0xac, + 0xc3, 0x2e, 0xd8, 0x38, 0x6e, 0x1c, 0xaf, 0x91, 0xee, 0x77, 0x18, 0xc3, 0x52, 0x24, + 0xb8, 0xbc, 0x46, 0x3f, 0x01, 0x79, 0x6b, 0xb8, 0x4b, 0x23, 0x93, 0x27, 0x75, 0x1b, + 0x28, 0x29, 0x86, 0xc9, 0xcd, 0xde, 0x6c, 0x97, 0x74, 0xf0, 0x74, 0x11, 0x96, 0xfe, + 0x55, 0x9a, 0xb6, 0x9e, 0xc4, 0xb1, 0xfb, 0x7c, 0xc4, 0x5e, 0x95, 0x91, 0x80, 0xd4, + 0xc2, 0xd9, 0xe5, 0xe6, 0x57, 0x64, 0xee, 0x5a, 0x4a, 0x6d, 0xaa, 0xef, 0x72, 0xed, + 0xb6, 0x97, 0xb5, 0x1d, 0x7c, 0xbd, 0xfe, 0xdf, 0x5f, 0x96, 0x45, 0x60, 0x77, 0x84, + 0x03, 0x30, 0x0e, 0x38, 0xd0, 0x01, 0xdf, 0x2a, 0x90, 0xd1, 0x73, 0x24, 0x96, 0x74, + 0xe4, 0x3c, 0x36, 0xe3, 0x22, 0xe8, 0xfe, 0x6b, 0xe4, 0x27, 0xd2, 0x2f, 0x22, 0x13, + 0xbb, 0x08, 0x4e, 0x6c, 0x09, 0x55, 0x09, 0xcf, 0x11, 0xbe, 0xc9, 0xed, 0x77, 0xdf, + 0x83, 0x66, 0x03, 0x56, 0x48, 0x4d, 0x95, 0x33, 0x2b, 0xa2, 0x15, 0xb3, 0x49, 0x75, + 0x10, 0x1e, 0xc7, 0x8d, 0x2a, 0x3f, 0xc3, 0x9d, 0xab, 0x58, 0xd1, 0x4a, 0x85, 0x40, + 0x1f, 0x23, 0x4b, 0x68, 0x58, 0x00, 0x5b, 0xde, 0x47, 0x0c, 0x58, 0xd1, 0x3f, 0x9b, + 0x9e, 0x65, 0xae, 0xda, 0x0b, 0xce, 0xb1, 0x75, 0x34, 0xa7, 0x92, 0x8e, 0x34, 0x40, + 0x9f, 0x98, 0xcd, 0x25, 0x93, 0xa3, 0x5a, 0xde, 0x8f, 0xb7, 0x9e, 0x0b, 0x17, 0x5f, + 0x5a, 0x12, 0x4e, 0xbd, 0x55, 0xac, 0x50, 0x7c, 0xc9, 0x26, 0x9a, 0x01, 0x4d, 0xfa, + 0x59, 0xd5, 0x10, 0x99, 0xfa, 0x1f, 0x8e, 0xb1, 0xbc, 0xdb, 0x72, 0xdf, 0xfb, 0xaa, + 0x31, 0x35, 0x7e, 0xac, 0x2b, 0xc4, 0xd0, 0x35, 0xe3, 0x9c, 0xc7, 0x08, 0x17, 0x19, + 0x0f, 0x55, 0x1c, 0x7e, 0xdd, 0x44, 0x5c, 0x8a, 0x13, 0x2a, 0xb2, 0x68, 0xd5, 0xc3, + 0x9c, 0xdc, 0x62, 0x04, 0x63, 0x96, 0x22, 0xb5, 0xef, 0x0d, 0xbb, 0xc6, 0x30, 0xd7, + 0x3a, 0x30, 0x69, 0x62, 0xa6, 0xa4, 0x21, 0x00, 0x7d, 0xc9, 0xe6, 0x50, 0xa2, 0x8f, + 0xb1, 0x53, 0x79, 0x6d, 0x71, 0x29, 0x41, 0xa9, 0x75, 0x86, 0xd9, 0xb5, 0x07, 0xbe, + 0x5a, 0xba, 0xbb, 0xd4, 0xe3, 0x25, 0xb2, 0x30, 0x96, 0x85, 0x63, 0xe7, 0xa0, 0xa2, + 0x32, 0x5c, 0x02, 0x5e, 0x28, 0x87, 0x8b, 0xad, 0x61, 0xf7, 0xc5, 0xbf, 0x6d, 0x83, + 0x79, 0x77, 0x5f, 0xaa, 0xd4, 0x34, 0x86, 0x8e, 0xaf, 0x3a, 0x99, 0x79, 0x44, 0x4c, + 0x53, 0x4b, 0xfd, 0x41, 0xa9, 0x65, 0x57, 0x81, 0xbe, 0xe9, 0xbb, 0x56, 0xee, 0x51, + 0xff, 0xc9, 0x04, 0x24, 0x49, 0x68, 0xf5, 0xd5, 0x68, 0x1d, 0x89, 0xbf, 0x99, 0x02, + 0x9d, 0xd2, 0x09, 0x3c, 0x93, 0x83, 0x9b, 0x30, 0x7c, 0x18, 0x4c, 0x68, 0xb5, 0xfd, + 0xc2, 0x1d, 0x4a, 0xbe, 0xc8, 0xe6, 0x42, 0xc8, 0xd9, 0xe9, 0xf8, 0x12, 0x16, 0xc7, + 0xff, 0xfd, 0xce, 0x5e, 0xaa, 0xf5, 0x9b, 0x4f, 0x9a, 0xe8, 0x4c, 0x81, 0x42, 0x3b, + 0xf4, 0xac, 0x97, 0x6e, 0x5e, 0x06, 0x22, 0x54, 0x91, 0x3c, 0x2d, 0xc2, 0xc4, 0x99, + 0x1e, 0x0f, 0x8e, 0x03, 0x5d, 0xe7, 0xbb, 0x9c, 0x8f, 0x71, 0x5c, 0x85, 0x8e, 0x29, + 0x0d, 0xe2, 0x7e, 0xeb, 0x34, 0xd6, 0x2d, 0x36, 0x78, 0xf1, 0x60, 0x5e, 0x88, 0x09, + 0x2c, 0x0f, 0xee, 0xcd, 0x3a, 0x0f, 0x01, 0xf2, 0xcc, 0xee, 0x7f, 0x5e, 0xbe, 0x35, + 0xc4, 0xc5, 0xbf, 0x88, 0xe8, 0x9e, 0x54, 0x4a, 0x62, 0x7e, 0x6b, 0x36, 0xb0, 0xae, + 0xf6, 0x4b, 0xaa, 0xbd, 0x85, 0xe5, 0x8f, 0x65, 0x8f, 0x81, 0xca, 0x39, 0x6b, 0x68, + 0x7c, 0xd8, 0xc5, 0x2f, 0xb9, 0xa6, 0x26, 0x9b, 0x40, 0x3b, 0x20, 0xe0, 0xc1, 0x61, + 0xa3, 0xb2, 0x27, 0xdc, 0x8b, 0xba, 0xcc, 0x30, 0x06, 0xdf, 0x7f, 0xb1, 0x00, 0xf6, + 0x41, 0xe3, 0xe3, 0x73, 0x22, 0x1d, 0xa9, 0x21, 0x2c, 0x6b, 0x8a, 0x2b, 0xde, 0x32, + 0x33, 0x15, 0xc0, 0x90, 0x49, 0x1b, 0xed, 0xb9, 0x74, 0xdd, 0x17, 0x11, 0x6c, 0x35, + 0xaf, 0x16, 0x45, 0x7f, 0xf2, 0xb5, 0x35, 0xf1, 0x81, 0xe9, 0x6d, 0x1d, 0x66, 0x3a, + 0x08, 0xb9, 0x0a, 0x21, 0xbe, 0x31, 0xd5, 0x04, 0xe9, 0x0f, 0x9f, 0xf8, 0x3f, 0x88, + 0xf9, 0xd9, 0xe4, 0x91, 0x0d, 0xe4, 0x3f, 0xf8, 0x41, 0x77, 0x07, 0x5d, 0x0d, 0x76, + 0x8c, 0x99, 0x3e, 0xe5, 0xb0, 0xb0, 0x57, 0x9e, 0x08, 0x96, 0x46, 0xd4, 0x78, 0x09, + 0xb4, 0x2e, 0x9d, 0xf1, 0xe2, 0x72, 0x63, 0xac, 0x99, 0x18, 0x36, 0x0f, 0x5a, 0x65, + 0xcd, 0x66, 0x9b, 0xdb, 0x54, 0xa7, 0x3d, 0x99, 0x32, 0x97, 0x7b, 0x26, 0xbf, 0x43, + 0x8d, 0x92, 0x9c, 0xd7, 0x4d, 0x36, 0x42, 0xd7, 0xbd, 0x26, 0x65, 0x50, 0xe5, 0x66, + 0x3a, 0x33, 0x7c, 0x8c, 0x83, 0x51, 0xdf, 0x0d, 0x25, 0xfd, 0x1b, 0x70, 0x89, 0x66, + 0x98, 0xc3, 0x8d, 0x6a, 0xa3, 0xc8, 0xab, 0xc2, 0x9d, 0x4e, 0x5b, 0x92, 0xa2, 0xfd, + 0x03, 0xe7, 0x1b, 0xd3, 0xdc, 0x62, 0x01, 0x29, 0x86, 0xba, 0x18, 0x01, 0x73, 0x27, + 0xaa, 0x6c, 0xbb, 0x5a, 0x9c, 0xec, 0x05, 0xb3, 0x66, 0xae, 0x97, 0x38, 0x3e, 0x20, + 0x79, 0x38, 0x81, 0x6d, 0x71, 0x32, 0xb5, 0x12, 0xd5, 0x4d, 0xd3, 0xd4, 0xe1, 0x4f, + 0x08, 0x11, 0x44, 0x79, 0x41, 0xa1, 0xcf, 0x3b, 0xa6, 0x53, 0xd2, 0x75, 0xa7, 0x1f, + 0x0e, 0x48, 0x66, 0xa2, 0x18, 0xb9, 0x74, 0x87, 0x08, 0xed, 0x56, 0x6a, 0x26, 0xbc, + 0x21, 0x33, 0xb8, 0x1d, 0x1c, 0x74, 0x5f, 0xe4, 0x86, 0x6f, 0x93, 0xfd, 0xa1, 0x9a, + 0x3d, 0x5b, 0xf2, 0x0a, 0x0c, 0x03, 0x65, 0x89, 0x99, 0x3d, 0xb9, 0x91, 0x9f, 0x94, + 0x8d, 0xdf, 0x17, 0xc6, 0xce, 0x44, 0x22, 0x5a, 0x87, 0xbc, 0x48, 0x40, 0xf4, 0xe7, + 0x9c, 0x5e, 0xc7, 0x54, 0xed, 0x2b, 0x6a, 0x33, 0x80, 0xf6, 0xba, 0x0e, 0x92, 0x90, + 0x18, 0x66, 0xa4, 0x07, 0xde, 0xa1, 0x59, 0xc7, 0x25, 0xdd, 0xe7, 0x44, 0xb7, 0x59, + 0x26, 0xd0, 0x6d, 0x2e, 0x57, 0x57, 0x0b, 0xc0, 0xae, 0x90, 0xf4, 0xea, 0xb4, 0xac, + 0x53, 0x5e, 0x77, 0x32, 0xf4, 0x3f, 0xad, 0x27, 0x95, 0x9e, 0x87, 0xcb, 0x88, 0x4f, + 0x76, 0x79, 0x93, 0x8e, 0xe1, 0xb8, 0x2e, 0x62, 0x7a, 0xf5, 0x60, 0x4c, 0x9a, 0xdb, + 0xe8, 0x24, 0x8e, 0xf4, 0xe8, 0x99, 0x80, 0xa9, 0x32, 0xf9, 0x4b, 0xa2, 0x6a, 0x4c, + 0x3d, 0xe8, 0x59, 0x98, 0xa4, 0xda, 0x77, 0xb4, 0xf2, 0x89, 0xd6, 0x9f, 0x2b, 0xde, + 0x22, 0x1f, 0xcc, 0x1d, 0x3c, 0x2c, 0x79, 0x24, 0x01, 0xae, 0xca, 0x0d, 0x99, 0xc7, + 0x01, 0x66, 0xcc, 0x68, 0x71, 0xa9, 0x4e, 0x15, 0x24, 0xdd, 0x8b, 0xc5, 0x08, 0x53, + 0x01, 0xcc, 0x8d, 0x2a, 0xde, 0xce, 0x2c, 0x28, 0xf4, 0xc4, 0x89, 0x45, 0x75, 0xbd, + 0xc7, 0xb7, 0xc4, 0x70, 0x0c, 0x8b, 0xb3, 0xf6, 0x55, 0x55, 0x0f, 0xc0, 0xb6, 0xa4, + 0x62, 0x51, 0x60, 0x52, 0x94, 0x63, 0xe7, 0xb2, 0x3d, 0x98, 0x98, 0x71, 0x45, 0x39, + 0x78, 0x77, 0x1a, 0xf9, 0x2d, 0xa1, 0xa4, 0x4f, 0x6a, 0x17, 0xa4, 0x1a, 0xbb, 0xec, + 0x86, 0x2b, 0xca, 0xb1, 0xcb, 0xd3, 0x1b, 0x97, 0x91, 0xe4, 0x1c, 0xd2, 0x89, 0x5c, + 0x24, 0xe8, 0x4c, 0x25, 0x89, 0x68, 0xb6, 0x9d, 0x15, 0x83, 0x4d, 0xec, 0xa4, 0x53, + 0xcd, 0xa7, 0x25, 0x5b, 0xd8, 0x72, 0xe3, 0x4e, 0x2d, 0xd5, 0x07, 0xa6, 0xe0, 0x13, + 0xd6, 0x15, 0x00, 0xc3, 0x61, 0x14, 0x46, 0x8a, 0x64, 0xd2, 0x5a, 0x95, 0x51, 0xd0, + 0xea, 0x00, 0xc0, 0x2b, 0x23, 0xb5, 0x5a, 0xbd, 0x73, 0x9e, 0x23, 0x28, 0x4b, 0x4c, + 0x35, 0xd3, 0xc1, 0xf9, 0x31, 0xce, 0xa7, 0x26, 0xb8, 0x49, 0xef, 0xb0, 0xe2, 0x0f, + 0xac, 0x1c, 0xe0, 0x1f, 0xc5, 0x6b, 0xf1, 0xe2, 0x0f, 0xa5, 0x61, 0xe6, 0x9d, 0xcb, + 0xc3, 0xaa, 0xc6, 0x32, 0x80, 0x82, 0xab, 0xd4, 0x65, 0x6e, 0x27, 0xf6, 0x17, 0x8f, + 0x35, 0x15, 0xe0, 0xab, 0x8a, 0x95, 0xd0, 0xae, 0x71, 0x55, 0x12, 0xe1, 0xee, 0x59, + 0x20, 0x9b, 0xaf, 0x2e, 0xa2, 0xdf, 0xfd, 0xa2, 0x0c, 0x35, 0xf7, 0x40, 0x8b, 0x58, + 0x24, 0x36, 0x9c, 0x91, 0x73, 0x6c, 0x8c, 0x64, 0x10, 0x1b, 0xd7, 0x9d, 0xaa, 0x69, + 0xff, 0x6a, 0xa9, 0x9a, 0x63, 0x41, 0x5c, 0xbe, 0x23, 0xae, 0x1b, 0x11, 0x87, 0xa3, + 0x28, 0xa2, 0x04, 0xe9, 0xc3, 0x41, 0x78, 0x8a, 0xd8, 0xcc, 0xd2, 0x5e, 0xc8, 0xf5, + 0xcb, 0x7e, 0x22, 0x43, 0x60, 0x25, 0x5e, 0x42, 0x42, 0x81, 0x64, 0x36, 0xa7, 0x27, + 0xcc, 0x28, 0xc8, 0x29, 0x2c, 0xa0, 0xe9, 0x2f, 0xad, 0x73, 0xaf, 0xa1, 0x3f, 0xa1, + 0xa3, 0x7e, 0xd4, 0x62, 0x25, 0x19, 0x8e, 0xd9, 0xbe, 0x42, 0xa1, 0xdd, 0xda, 0x35, + 0x4e, 0xc7, 0x9f, 0xc4, 0xe0, 0xf8, 0xf8, 0x21, 0x39, 0x0b, 0xcd, 0x39, 0x60, 0x83, + 0xf1, 0x5d, 0x16, 0x65, 0x61, 0xc0, 0xab, 0xbf, 0xb0, 0x2c, 0xf6, 0x50, 0x18, 0x6d, + 0x49, 0x6f, 0xf1, 0x40, 0xf8, 0x0c, 0xb8, 0xed, 0x66, 0x36, 0x8c, 0x42, 0x3b, 0xff, + 0x9d, 0x67, 0x72, 0xea, 0x97, 0xd7, 0x1a, 0xb0, 0x98, 0x2a, 0x63, 0x17, 0x91, 0x86, + 0x2d, 0xa1, 0xd8, 0x5e, 0x13, 0xc2, 0x85, 0x5e, 0x89, 0xfa, 0xe5, 0xd4, 0x69, 0x75, + 0xc4, 0x77, 0xef, 0xda, 0x75, 0xde, 0x60, 0xc2, 0xbb, 0xa4, 0xe0, 0xc5, 0x58, 0x5f, + 0xa6, 0x79, 0xf5, 0x4f, 0xba, 0x87, 0x11, 0xdb, 0x54, 0xf8, 0x69, 0x90, 0x8d, 0xb4, + 0xe2, 0x23, 0x11, 0xb4, 0xd7, 0x67, 0x40, 0xd4, 0xff, 0x98, 0x40, 0x44, 0x5e, 0x57, + 0xa1, 0x80, 0xaf, 0xac, 0xd3, 0x10, 0xd9, 0xa1, 0xbe, 0x6f, 0xc0, 0xb1, 0x07, 0xd9, + 0x5f, 0x9d, 0xc2, 0x26, 0xc5, 0xc5, 0x03, 0xda, 0xf0, 0x15, 0x2f, 0xce, 0x9d, 0x36, + 0x52, 0xbc, 0xfb, 0xc2, 0x3a, 0x67, 0x6c, 0x18, 0xf4, 0xae, 0x17, 0x81, 0xd4, 0x7f, + 0xda, 0x9a, 0x2f, 0x36, 0xb6, 0x82, 0x4a, 0xfc, 0x95, 0xc3, 0x7a, 0xee, 0x00, 0x1e, + 0xa3, 0x4b, 0x29, 0x1f, 0x66, 0xd6, 0x34, 0xc9, 0x6d, 0x2e, 0x8f, 0xff, 0x6d, 0xe5, + 0xbc, 0xc7, 0x38, 0x11, 0xdd, 0x49, 0x5d, 0x07, 0x9d, 0xcb, 0x53, 0x1a, 0x44, 0xbb, + 0xbe, 0x82, 0x07, 0x3f, 0xb8, 0x09, 0xfb, 0x47, 0x4d, 0x94, 0x7e, 0xd7, 0xe5, 0xa1, + 0xbd, 0x18, 0x3c, 0x3d, 0x62, 0x92, 0x90, 0x3a, 0x9f, 0xf5, 0x2e, 0x4c, 0xb4, 0x13, + 0xf0, 0x81, 0x58, 0x76, 0xa3, 0xfd, 0x1d, 0x57, 0xf2, 0x14, 0xba, 0x35, 0xf0, 0xfa, + 0x70, 0x00, 0xb7, 0xc9, 0x3b, 0xac, 0x70, 0x5f, 0x0a, 0x49, 0xb0, 0xcc, 0x56, 0x8d, + 0xfa, 0x29, 0xe1, 0x2e, 0xea, 0xcc, 0x3c, 0x0a, 0x18, 0x54, 0x8d, 0x2e, 0x7c, 0xbf, + 0xbd, 0x90, 0x32, 0x51, 0x87, 0x0c, 0xd6, 0xe8, 0x45, 0xbf, 0xaf, 0x85, 0x7d, 0x47, + 0x9f, 0xbe, 0xfb, 0x6f, 0xc5, 0xad, 0xc2, 0xf6, 0xd3, 0x2d, 0x4e, 0x8b, 0x3b, 0x0d, + 0x33, 0x55, 0xc4, 0x1b, 0xf9, 0x80, 0x7c, 0xd5, 0xfa, 0xdc, 0x51, 0x84, 0xf5, 0x5c, + 0x04, 0xbc, 0x55, 0xc4, 0x24, 0x34, 0x3e, 0xcf, 0x83, 0x37, 0x4d, 0x63, 0x87, 0xaf, + 0x10, 0xc1, 0x8c, 0x2f, 0xed, 0x0d, 0x45, 0xeb, 0x0d, 0xb6, 0x7b, 0x84, 0x34, 0xef, + 0xcf, 0x48, 0xae, 0x1b, 0x98, 0xbe, 0xe4, 0x3d, 0x63, 0x03, 0xb9, 0x0e, 0x1a, 0x5d, + 0xcb, 0x4e, 0x29, 0x20, 0x6b, 0x69, 0x53, 0xb6, 0x75, 0x7d, 0x07, 0x96, 0x91, 0x64, + 0xb1, 0x4b, 0xaf, 0xda, 0x51, 0xf3, 0x28, 0x5f, 0xb0, 0x70, 0x24, 0xa5, 0x82, 0x9e, + 0x9e, 0xd7, 0x45, 0x1a, 0xd7, 0xa2, 0x8a, 0x57, 0x91, 0xd8, 0xfe, 0x7d, 0xfb, 0xa2, + 0xf6, 0x1a, 0x34, 0x9a, 0x37, 0xa4, 0xcc, 0xb0, 0x49, 0xe0, 0x30, 0xaf, 0xa4, 0x44, + 0xfc, 0x21, 0x0f, 0x0a, 0xd2, 0x46, 0xf0, 0xd1, 0x11, 0x36, 0x10, 0x12, 0x79, 0xbf, + 0x19, 0x0d, 0xe9, 0xca, 0xe8, 0x35, 0x31, 0xfd, 0xc3, 0x4c, 0xa4, 0xf1, 0xde, 0xea, + 0xe5, 0x59, 0x56, 0xd9, 0xac, 0xe0, 0x4e, 0x7e, 0x63, 0x4a, 0x74, 0xae, 0x0e, 0xc3, + 0x75, 0x02, 0x5e, 0xc3, 0x3c, 0xa6, 0xd1, 0x1e, 0xe6, 0xd8, 0xe7, 0xa3, 0xbd, 0x69, + 0x92, 0x34, 0x57, 0x65, 0xc9, 0x9d, 0xe4, 0x71, 0xd2, 0x3f, 0xab, 0xe7, 0xf3, 0x1c, + 0xa0, 0x3a, 0xe7, 0x16, 0x62, 0x76, 0xb3, 0xfd, 0xfe, 0x3e, 0xad, 0x0d, 0x9c, 0x15, + 0x49, 0xc2, 0x6b, 0x18, 0x71, 0x6e, 0x1c, 0xa1, 0x0b, 0x61, 0x9d, 0xfd, 0x7d, 0xda, + 0xec, 0x02, 0xd3, 0xac, 0x82, 0x6d, 0x34, 0xae, 0xca, 0x49, 0x0a, 0xa5, 0x96, 0x91, + 0x0c, 0x0a, 0x62, 0xe0, 0xb7, 0x37, 0x97, 0x24, 0xf1, 0x86, 0x7f, 0xe5, 0xf7, 0x40, + 0x0d, 0xd8, 0x88, 0x65, 0xa0, 0xff, 0xea, 0x71, 0x28, 0xb8, 0xcf, 0x78, 0x65, 0x46, + 0xa3, 0x9d, 0x5d, 0xc5, 0x68, 0xb5, 0xbb, 0x80, 0x09, 0xaf, 0xeb, 0xe5, 0xf9, 0xbe, + 0x8b, 0x7e, 0xc7, 0xa9, 0xc5, 0x94, 0x6f, 0x79, 0x23, 0xa5, 0xce, 0x41, 0x2f, 0xd9, + 0x5c, 0x47, 0x1b, 0xb1, 0xc2, 0xb4, 0xf8, 0x22, 0xa4, 0xd5, 0x44, 0x7d, 0x73, 0x1f, + 0x9e, 0xe2, 0x0a, 0x31, 0x39, 0x10, 0x28, 0xfb, 0xc0, 0xf3, 0xb0, 0x46, 0xc7, 0xfe, + 0xb3, 0x80, 0xd1, 0x94, 0xa1, 0xdd, 0x19, 0xd5, 0x45, 0x5f, 0x62, 0x7e, 0x75, 0xdf, + 0xff, 0xe5, 0x8d, 0xac, 0x1c, 0xa2, 0x55, 0xe1, 0xbd, 0x74, 0x36, 0x1d, 0xdc, 0x22, + 0x2f, 0x1b, 0x7a, 0x8c, 0xed, 0xb2, 0xa2, 0x2c, 0xc1, 0xd9, 0x39, 0xc3, 0x02, 0xd8, + 0xf8, 0x4e, 0xe3, 0xf2, 0x86, 0x66, 0xe8, 0x18, 0xd1, 0x0c, 0x62, 0x1e, 0x9a, 0x4e, + 0xa4, 0x90, 0xb6, 0x51, 0x53, 0xfe, 0x01, 0x5f, 0x72, 0x19, 0x88, 0x20, 0x39, 0xb5, + 0x59, 0xe9, 0x54, 0x1c, 0x34, 0x43, 0x9b, 0xe4, 0x7f, 0x1f, 0x3c, 0x9d, 0x33, 0xaa, + 0x89, 0x5b, 0x87, 0x9e, 0xb2, 0x77, 0xb6, 0xe7, 0x53, 0xa5, 0xe7, 0x60, 0x35, 0x0f, + 0x0d, 0x13, 0x89, 0x9b, 0xcf, 0x98, 0x37, 0x50, 0x12, 0x01, 0xea, 0xee, 0x32, 0x14, + 0xcf, 0x13, 0xea, 0x17, 0x4d, 0x13, 0x4b, 0x27, 0x82, 0x62, 0x12, 0xc6, 0x03, 0xaa, + 0x93, 0x2a, 0xaa, 0x41, 0x81, 0xa8, 0x40, 0x87, 0x95, 0x1a, 0xbf, 0x3e, 0x2c, 0x20, + 0xd0, 0x8e, 0x50, 0x57, 0x43, 0xc0, 0x13, 0xab, 0xa4, 0xf4, 0x0a, 0xd7, 0xe5, 0xee, + 0xee, 0xf6, 0xfd, 0x7f, 0x6f, 0x6c, 0xb5, 0x1a, 0x59, 0xa8, 0x22, 0xab, 0x19, 0x1a, + 0x50, 0xed, 0xf3, 0x69, 0x67, 0xf3, 0x8c, 0xc7, 0x4f, 0x35, 0xd5, 0x7b, 0x0b, 0x70, + 0x80, 0x8b, 0x78, 0x6e, 0x36, 0x09, 0x0d, 0xd1, 0xd3, 0x1c, 0x9c, 0x77, 0x7e, 0x91, + 0x07, 0x2d, 0x1d, 0xbf, 0xc4, 0x80, 0xac, 0xec, 0x81, 0xef, 0x00, 0x9d, 0x36, 0x81, + 0x6f, 0x9d, 0x09, 0x42, 0xb8, 0xd6, 0x00, 0xb7, 0x3d, 0x0e, 0xe2, 0xc8, 0xcb, 0x89, + 0x15, 0x1c, 0xe3, 0x80, 0x73, 0xca, 0x1c, 0xdc, 0x64, 0x22, 0xe7, 0x5f, 0x3b, 0x3c, + 0x01, 0x81, 0xaa, 0x50, 0x41, 0xb8, 0x07, 0xde, 0x89, 0xb5, 0x56, 0x6e, 0xbe, 0xbb, + 0x44, 0x1d, 0xd7, 0x8e, 0xe4, 0x21, 0x2c, 0x87, 0x67, 0xc8, 0x7c, 0x90, 0x73, 0xea, + 0xe7, 0x3d, 0x62, 0x48, 0x32, 0x95, 0xcb, 0x68, 0xcc, 0x75, 0x2f, 0xd5, 0xca, 0x0e, + 0x34, 0x41, 0xa1, 0xe2, 0x7b, 0x77, 0xcf, 0xa9, 0x2b, 0x94, 0x0d, 0xf7, 0xc6, 0xc9, + 0x6c, 0x2c, 0xe5, 0x33, 0xe6, 0xba, 0x69, 0xbb, 0x1c, 0xea, 0x1a, 0x05, 0x78, 0xe4, + 0xe3, 0xbf, 0xf6, 0x1b, 0x9f, 0x3d, 0xc5, 0x7f, 0x85, 0xd9, 0x6c, 0xd9, 0xd1, 0x30, + 0x01, 0xc8, 0xc3, 0x76, 0xf0, 0x11, 0x13, 0x04, 0xc3, 0xfd, 0xaf, 0x8b, 0x0a, 0x86, + 0x6c, 0xb1, 0xfb, 0x1a, 0x7f, 0x8e, 0x5e, 0xee, 0xbc, 0x4d, 0x77, 0x1f, 0xb3, 0xfe, + 0xa3, 0xc2, 0x82, 0x72, 0x68, 0x37, 0xdc, 0xd3, 0x46, 0x14, 0x16, 0x38, 0x45, 0x3e, + 0xef, 0x5f, 0x0e, 0x18, 0x55, 0xe6, 0x51, 0x00, 0x44, 0x2a, 0xd2, 0xb1, 0x06, 0xb6, + 0x59, 0x85, 0x71, 0xfb, 0x66, 0x6f, 0xe6, 0xf5, 0x83, 0xd7, 0x62, 0x34, 0x17, 0x55, + 0x43, 0x67, 0x91, 0x4d, 0x60, 0xa8, 0xf0, 0xb3, 0xb6, 0x13, 0xe1, 0xe2, 0xa9, 0x55, + 0xc5, 0xca, 0xbd, 0x66, 0xe2, 0x10, 0x3f, 0x94, 0x6f, 0x01, 0xb7, 0xb6, 0xfc, 0x8d, + 0x1a, 0xc1, 0x90, 0xe5, 0xb9, 0x59, 0x36, 0xcd, 0x35, 0x40, 0xc2, 0xb8, 0x3f, 0x67, + 0x39, 0xf6, 0xbf, 0xbb, 0xc9, 0x18, 0xb1, 0x98, 0x78, 0x5a, 0x3b, 0xdf, 0x85, 0x50, + 0x26, 0xf9, 0xde, 0x0f, 0xd7, 0xd1, 0x80, 0x87, 0x22, 0xdd, 0x40, 0x1b, 0xe4, 0x8b, + 0xb4, 0xe3, 0xfa, 0x38, 0xcf, 0x82, 0x65, 0x31, 0xdd, 0xc8, 0xce, 0xed, 0x8e, 0xf3, + 0xe0, 0xb2, 0xda, 0x68, 0x4c, 0x89, 0xa9, 0x7b, 0x02, 0x94, 0x4f, 0x8a, 0x48, 0x45, + 0x18, 0x7f, 0xda, 0x8c, 0x52, 0x9d, 0xed, 0x3d, 0xb1, 0xb0, 0x4f, 0x30, 0x6c, 0x28, + 0x95, 0xaa, 0x87, 0xd6, 0xf8, 0x2c, 0x99, 0x8d, 0x02, 0x6e, 0xf1, 0xdf, 0x18, 0x7b, + 0xba, 0x8c, 0xca, 0x00, 0x42, 0xd3, 0x01, 0xe7, 0x54, 0xd7, 0x96, 0x07, 0x08, 0x75, + 0x63, 0xf3, 0x77, 0x66, 0xde, 0x12, 0xdd, 0x42, 0x26, 0xe9, 0x3c, 0xde, 0x7b, 0x9e, + 0x24, 0x8c, 0x66, 0x1a, 0x6c, 0x80, 0x5a, 0x30, 0xfe, 0x0a, 0xa5, 0x26, 0x9e, 0x16, + 0x0c, 0x86, 0x1c, 0xe0, 0x81, 0x2d, 0x2d, 0x22, 0xf0, 0x62, 0xf8, 0xa0, 0xff, 0x36, + 0xe1, 0x57, 0x4c, 0xac, 0xc0, 0xbc, 0xeb, 0xb7, 0xba, 0x0e, 0x17, 0x18, 0xb3, 0xcc, + 0x58, 0x74, 0xa7, 0xbc, 0x8f, 0xd8, 0x23, 0x20, 0xba, 0x5c, 0xc8, 0xc9, 0xb6, 0x52, + 0xba, 0xf0, 0x93, 0xe8, 0x58, 0xe1, 0x48, 0x21, 0xbf, 0x1b, 0x34, 0x78, 0x2d, 0x9b, + 0x85, 0x0c, 0x35, 0x6c, 0x84, 0x9b, 0xf2, 0xdd, 0x7c, 0x5c, 0x03, 0xf4, 0x45, 0x2e, + 0x4f, 0x23, 0x0e, 0x4e, 0xfd, 0x9f, 0xf8, 0xd0, 0x6b, 0x0f, 0xe0, 0x2c, 0xc6, 0x39, + 0x32, 0x80, 0x85, 0x3a, 0x19, 0x79, 0x13, 0x86, 0x52, 0x2c, 0xa6, 0x83, 0xaf, 0x38, + 0xb0, 0xe6, 0x4e, 0xaa, 0x3a, 0x4b, 0xb9, 0x63, 0x32, 0x4a, 0x2b, 0x3f, 0xd1, 0xca, + 0xf7, 0xed, 0x4e, 0x4e, 0x80, 0xa1, 0xa7, 0x78, 0x14, 0xd6, 0x4d, 0x53, 0xb3, 0x49, + 0x41, 0x2a, 0xdb, 0x88, 0x57, 0x9e, 0x3e, 0x15, 0x79, 0x7c, 0x40, 0x3b, 0xdd, 0x07, + 0xcd, 0x96, 0x0c, 0x62, 0x1e, 0x38, 0x50, 0x93, 0xc3, 0x1d, 0x0b, 0x35, 0xf5, 0x54, + 0x26, 0x77, 0xd0, 0x78, 0xf2, 0x94, 0x14, 0xa1, 0x39, 0x0b, 0xb5, 0xf9, 0x39, 0xc9, + 0x4f, 0xd7, 0xe6, 0x6d, 0xcd, 0x0e, 0x65, 0xa3, 0x11, 0x0f, 0xc8, 0x40, 0xf2, 0x62, + 0x17, 0x2b, 0x5c, 0x9f, 0x2f, 0xb0, 0x23, 0x50, 0x72, 0x3b, 0x29, 0xfb, 0x47, 0x80, + 0xca, 0xf1, 0x5e, 0x38, 0x4c, 0x1c, 0x5b, 0x88, 0x62, 0x42, 0x95, 0x1a, 0x1e, 0xad, + 0xe8, 0x0e, 0x4e, 0xba, 0x90, 0x98, 0x5d, 0x98, 0x17, 0xef, 0xf6, 0xbd, 0x5e, 0x25, + 0xdf, 0xe4, 0x47, 0x2e, 0xab, 0xcd, 0xac, 0xa1, 0xdb, 0x9b, 0x80, 0xd4, 0x18, 0x6f, + 0x3f, 0x84, 0x26, 0x16, 0xe6, 0xe8, 0xb9, 0x08, 0x4b, 0x01, 0x25, 0x5d, 0x7c, 0x5d, + 0x4e, 0x11, 0x62, 0x88, 0x90, 0x57, 0xe6, 0x93, 0x1c, 0x06, 0x5a, 0x04, 0xd4, 0xcd, + 0xda, 0x4e, 0xb1, 0x78, 0x06, 0x49, 0xab, 0xe6, 0x1e, 0x2c, 0x36, 0x27, 0x0b, 0x66, + 0x63, 0xf4, 0x76, 0x5d, 0x4a, 0x62, 0xbe, 0x2f, 0xb1, 0xdc, 0x58, 0x08, 0x7d, 0xc3, + 0xc4, 0xe6, 0xef, 0x37, 0xfb, 0x22, 0x81, 0x83, 0x4a, 0x73, 0x5d, 0x03, 0xad, 0x1f, + 0xa1, 0x7f, 0x08, 0xd8, 0xd5, 0xc3, 0x5e, 0x4e, 0xed, 0x56, 0x9a, 0x31, 0xcf, 0x02, + 0xb3, 0xef, 0x44, 0x38, 0x84, 0x12, 0xed, 0x40, 0x82, 0xec, 0xdd, 0xd4, 0xf8, 0x09, + 0xa9, 0xd8, 0xb8, 0x70, 0xd7, 0x12, 0x8c, 0x82, 0x01, 0x88, 0x7e, 0x5c, 0x85, 0x82, + 0xde, 0x6c, 0x9c, 0xd8, 0x9d, 0x69, 0x9c, 0xf5, 0xc1, 0x4b, 0x79, 0x5b, 0x7f, 0x81, + 0xa7, 0xfc, 0xd1, 0x7c, 0xd7, 0x0c, 0x0d, 0x09, 0x18, 0xc3, 0xc4, 0x1f, 0xb5, 0x8e, + 0x4a, 0xd1, 0x47, 0x94, 0x80, 0x8d, 0xfb, 0xc2, 0xe9, 0x21, 0x0a, 0xc6, 0x23, 0x9b, + 0xe9, 0x5e, 0x36, 0x38, 0xc9, 0x1f, 0xae, 0x3b, 0x28, 0xeb, 0xc4, 0x20, 0xd5, 0x94, + 0xbd, 0xa3, 0x46, 0xba, 0xf6, 0x2a, 0x6d, 0x53, 0xda, 0xcb, 0xdd, 0x2d, 0x79, 0xd0, + 0x1c, 0x4a, 0x3c, 0xd5, 0x9c, 0x6d, 0x81, 0xe0, 0x4c, 0x20, 0x39, 0x9f, 0x78, 0xad, + 0x8d, 0x97, 0xfc, 0x9a, 0x8a, 0x4f, 0x0b, 0x8b, 0xcb, 0x13, 0x7b, 0x03, 0x86, 0x5e, + 0x40, 0x90, 0x09, 0x7e, 0xb0, 0x34, 0x06, 0x1b, 0x22, 0x24, 0x80, 0x23, 0x84, 0x2b, + 0x3a, 0xd1, 0xdb, 0x31, 0x10, 0xcb, 0x25, 0xde, 0x7a, 0x3f, 0xc2, 0xc2, 0x74, 0x1b, + 0x1e, 0xd4, 0x6c, 0x55, 0x2b, 0x01, 0x66, 0x8d, 0xde, 0xdb, 0x35, 0x19, 0x4e, 0x2f, + 0x3a, 0xd5, 0xe1, 0x04, 0x80, 0x94, 0x36, 0x85, 0xec, 0xe0, 0xa1, 0x1e, 0x80, 0xa9, + 0x63, 0xdf, 0x6a, 0x63, 0x3c, 0x79, 0xa9, 0x89, 0x2a, 0xe4, 0xb4, 0xdd, 0x36, 0x19, + 0x58, 0x64, 0x9f, 0xe9, 0x83, 0xb4, 0xdf, 0x7b, 0xbd, 0x6f, 0x6b, 0x91, 0xf8, 0x1a, + 0x7d, 0xeb, 0xf2, 0x4e, 0x9d, 0xe5, 0xd5, 0x74, 0x3d, 0xd1, 0xb0, 0x30, 0xda, 0x8b, + 0x7a, 0x82, 0x50, 0x79, 0xc9, 0xc9, 0x80, 0x11, 0xec, 0x3e, 0x2c, 0x85, 0x3d, 0x13, + 0xa3, 0x0b, 0x19, 0xc5, 0x0d, 0x2f, 0x8b, 0xe8, 0x79, 0x73, 0x60, 0x4d, 0xa2, 0xd6, + 0xa4, 0x32, 0x5d, 0xce, 0xc4, 0x65, 0x52, 0x62, 0xfc, 0xe1, 0x11, 0xa8, 0x75, 0xcc, + 0x25, 0xe9, 0xf0, 0x75, 0xfe, 0x89, 0x8a, 0x2f, 0xe0, 0x54, 0x1f, 0x8b, 0x5a, 0xf5, + 0xec, 0xcf, 0xf7, 0xec, 0xea, 0x45, 0xaa, 0x6b, 0x51, 0x1a, 0x0d, 0x82, 0x76, 0xce, + 0x4a, 0x24, 0x44, 0x43, 0xe0, 0x70, 0x4d, 0x1c, 0x3a, 0x33, 0xa4, 0x1b, 0x3f, 0x33, + 0xad, 0x01, 0xd9, 0x77, 0x69, 0x03, 0x7b, 0xd3, 0x9a, 0x2e, 0xcc, 0xb9, 0x76, 0x3c, + 0xc0, 0x6d, 0x71, 0x1f, 0xf9, 0x81, 0x90, 0xf5, 0x56, 0xf1, 0xca, 0x0d, 0x44, 0xa0, + 0xa1, 0xdf, 0xc6, 0xd9, 0x6d, 0x66, 0x00, 0x48, 0xff, 0xa0, 0xaa, 0xb2, 0xe2, 0x56, + 0x70, 0xba, 0xaf, 0x4e, 0x4e, 0x84, 0xe6, 0x95, 0xa3, 0xb7, 0x76, 0xeb, 0x3d, 0x37, + 0xdf, 0x5c, 0xfd, 0x08, 0x29, 0x0c, 0x65, 0xff, 0x5d, 0x0f, 0xc2, 0x35, 0x73, 0x71, + 0xf3, 0x9b, 0x66, 0xbe, 0xca, 0x3c, 0xca, 0xc2, 0xa8, 0xcc, 0xef, 0xb4, 0x4f, 0x28, + 0xff, 0xd7, 0x38, 0x98, 0x92, 0x3a, 0xc2, 0x92, 0x61, 0xcf, 0xa8, 0xb3, 0x2d, 0x35, + 0x0d, 0xf6, 0x94, 0x79, 0x47, 0x55, 0xfd, 0xb5, 0x9f, 0x6d, 0xca, 0x39, 0x97, 0xc0, + 0x75, 0xda, 0x0e, 0xe7, 0x30, 0xd9, 0x60, 0x8e, 0x89, 0x0f, 0xf2, 0x8f, 0xb4, 0xaa, + 0xd5, 0x11, 0x05, 0x8b, 0x52, 0x4b, 0x62, 0x0e, 0x41, 0x63, 0x6a, 0x1a, 0x1a, 0x75, + 0x32, 0xa2, 0xf8, 0x62, 0xf3, 0xd6, 0xc9, 0xe8, 0xc1, 0x16, 0xde, 0xd5, 0xac, 0x76, + 0x34, 0xf9, 0x2a, 0x01, 0xcf, 0x16, 0x61, 0x9b, 0xf2, 0xfb, 0xff, 0x01, 0xb9, 0xf6, + 0x76, 0x1d, 0x53, 0xbf, 0xa8, 0x65, 0xd1, 0x4e, 0x8f, 0xa9, 0x74, 0xf8, 0x63, 0x3e, + 0x73, 0xa8, 0x1d, 0xc0, 0x7c, 0x4c, 0xb8, 0x5e, 0xd7, 0x1c, 0xcb, 0x10, 0x13, 0xcf, + 0x71, 0x73, 0x6d, 0x4c, 0x50, 0x8a, 0xa4, 0x23, 0x88, 0xd7, 0x38, 0xc6, 0xa5, 0xd8, + 0x56, 0x51, 0x5d, 0x58, 0xdc, 0x71, 0xd1, 0x1f, 0x4a, 0x05, 0x48, 0x6b, 0x08, 0x97, + 0x27, 0x18, 0xd6, 0xaf, 0xa0, 0xc1, 0x5f, 0xc6, 0x88, 0x27, 0xb9, 0x28, 0x72, 0xe5, + 0xfa, 0x81, 0x72, 0xad, 0x63, 0xb2, 0xc6, 0x77, 0x7d, 0x60, 0x43, 0x87, 0x59, 0xbe, + 0x89, 0x08, 0xb7, 0xb3, 0x10, 0x95, 0xdf, 0xd9, 0xfb, 0x6e, 0x29, 0x7c, 0x56, 0xac, + 0x65, 0x6f, 0x25, 0x3d, 0x6b, 0x19, 0x3b, 0x3c, 0xd6, 0xee, 0xd8, 0x45, 0xa9, 0xfc, + 0x4a, 0x00, 0xff, 0xb4, 0x61, 0x88, 0x13, 0x65, 0x8a, 0x08, 0xee, 0xfd, 0xd6, 0xa3, + 0x87, 0xa7, 0x7d, 0x9e, 0xae, 0x94, 0x99, 0x98, 0x39, 0x2a, 0xc2, 0x55, 0x9b, 0xc4, + 0x3b, 0x90, 0x15, 0x4f, 0x68, 0xce, 0xfc, 0x57, 0xe6, 0x96, 0x5c, 0x87, 0x10, 0x2c, + 0xa9, 0x7d, 0x9e, 0x8e, 0x7d, 0x7e, 0xf3, 0x23, 0x3b, 0x85, 0x03, 0x1f, 0x7b, 0x17, + 0x6d, 0x0a, 0x75, 0xd6, 0x52, 0x34, 0x66, 0x74, 0xc1, 0xd8, 0xf2, 0x4c, 0x01, 0x2e, + 0x15, 0xc8, 0xa4, 0xd9, 0x45, 0xe5, 0x5e, 0x72, 0x13, 0xda, 0x7b, 0x73, 0xe4, 0x44, + 0xc0, 0xde, 0x25, 0xea, 0x40, 0x23, 0xaa, 0x05, 0xfe, 0x34, 0x2e, 0xf4, 0x82, 0x82, + 0x30, 0xd9, 0x17, 0x2d, 0xc0, 0xd0, 0x54, 0x8c, 0x17, 0x36, 0x8f, 0x5c, 0xa7, 0xad, + 0x27, 0x18, 0x9f, 0x75, 0xa6, 0xd6, 0xa7, 0x7a, 0xc3, 0x3b, 0xab, 0x2a, 0x9d, 0xb6, + 0xa8, 0xd5, 0x77, 0x97, 0x4a, 0x01, 0xbe, 0x6b, 0xe7, 0xc3, 0x1f, 0xb8, 0x8a, 0xd8, + 0xbd, 0xb5, 0xf7, 0x1a, 0xea, 0xe5, 0x18, 0x2a, 0x22, 0xa4, 0x38, 0xcf, 0xc0, 0x0c, + 0x01, 0x9e, 0x43, 0xb4, 0xd2, 0x80, 0xfa, 0x26, 0x5e, 0x32, 0xdb, 0xf9, 0x9b, 0x0b, + 0x87, 0xcd, 0x43, 0xef, 0x94, 0x87, 0x35, 0x0e, 0xa7, 0x32, 0x3c, 0x72, 0xc0, 0x0a, + 0xba, 0xa8, 0x7b, 0xe3, 0x26, 0x42, 0x4d, 0x42, 0x51, 0xed, 0xb9, 0x05, 0x97, 0xc2, + 0x77, 0x9a, 0x0e, 0x55, 0x3a, 0x84, 0xa7, 0x6e, 0xf3, 0xb3, 0xea, 0x0d, 0xef, 0xc7, + 0x07, 0x75, 0x0b, 0xc2, 0x61, 0x4f, 0x7e, 0x4d, 0x21, 0xdf, 0xb8, 0x0a, 0x2c, 0xb3, + 0x46, 0xab, 0x96, 0x0a, 0xc4, 0xcc, 0x11, 0x4c, 0x64, 0xdc, 0xd6, 0xb4, 0x5a, 0xd7, + 0x66, 0x46, 0xd8, 0x2c, 0x0a, 0x6d, 0xc5, 0xe2, 0x10, 0x05, 0x28, 0x55, 0x20, 0xa1, + 0x5b, 0x46, 0x52, 0x55, 0x3c, 0x32, 0x9e, 0x65, 0x8f, 0x33, 0xd0, 0xf0, 0xa5, 0x9f, + 0x03, 0x9a, 0x20, 0x3c, 0x1a, 0x34, 0xa1, 0x9d, 0xb5, 0xf8, 0xab, 0xd4, 0x5b, 0x5c, + 0x81, 0x0e, 0x21, 0x07, 0xff, 0x5d, 0x3d, 0xaf, 0xbe, 0x7a, 0xb6, 0x93, 0xc3, 0xda, + 0xa5, 0x46, 0xbd, 0x4e, 0x2c, 0xe9, 0x1b, 0x09, 0x3a, 0x47, 0x2e, 0x6c, 0x3d, 0x11, + 0x09, 0xac, 0x47, 0xe6, 0xf2, 0x4e, 0xa8, 0x2f, 0x78, 0x5c, 0x4d, 0x4c, 0x2a, 0x1c, + 0xe5, 0x57, 0x0d, 0x93, 0x67, 0xe7, 0x8b, 0x7c, 0xf5, 0x25, 0xb6, 0x47, 0x98, 0x4e, + 0x9a, 0x7e, 0x04, 0xf2, 0x25, 0x92, 0xc7, 0xc7, 0x6a, 0xd5, 0x32, 0xb8, 0x1c, 0x97, + 0x55, 0x0f, 0x57, 0x25, 0x31, 0x9c, 0xd1, 0xb6, 0x8f, 0x5c, 0xd5, 0xb6, 0xd7, 0xf6, + 0x58, 0x19, 0x26, 0x16, 0xc0, 0x79, 0x6b, 0x2e, 0x36, 0xa4, 0x4d, 0xfd, 0x98, 0x34, + 0xfe, 0xee, 0xbc, 0x2f, 0x98, 0x92, 0x77, 0x94, 0x7c, 0x58, 0xcf, 0x3c, 0x9e, 0x33, + 0xbc, 0x65, 0x42, 0xa6, 0xc4, 0xa8, 0xc1, 0x2b, 0x0e, 0x53, 0x51, 0xa5, 0xd1, 0xd3, + 0xc3, 0xf0, 0x24, 0xd6, 0x40, 0xde, 0xe6, 0x63, 0x19, 0x0e, 0x9d, 0xce, 0x71, 0xb6, + 0x7e, 0xb3, 0x44, 0xf0, 0xf1, 0x85, 0x85, 0x86, 0xcd, 0x78, 0xfa, 0x16, 0xa6, 0x21, + 0xd1, 0xc7, 0x2b, 0xd6, 0x77, 0xf7, 0xf5, 0x12, 0xd4, 0x1f, 0x90, 0x14, 0x61, 0xd9, + 0x22, 0xe1, 0x38, 0x07, 0x81, 0x99, 0x06, 0xea, 0x49, 0xb6, 0xca, 0x5b, 0xb6, 0xde, + 0xae, 0x6e, 0x1d, 0x7b, 0x7c, 0x5e, 0x9c, 0xad, 0x7c, 0xd3, 0xab, 0xc2, 0x68, 0xe2, + 0x77, 0xbb, 0xc7, 0x13, 0x48, 0x0c, 0x0e, 0x04, 0xfd, 0x17, 0x0b, 0x16, 0xc7, 0xed, + 0x72, 0x70, 0x50, 0x79, 0x74, 0xb1, 0xcf, 0x6e, 0x12, 0x2b, 0x9c, 0x24, 0x7a, 0x54, + 0xf8, 0xfb, 0xff, 0xaa, 0x78, 0x60, 0x40, 0xe3, 0x2f, 0x0b, 0x6d, 0x10, 0x32, 0x28, + 0x1a, 0x8d, 0x4f, 0x25, 0x59, 0x4a, 0xf9, 0x79, 0x96, 0x5c, 0x07, 0x29, 0xb0, 0x02, + 0x51, 0xf4, 0x64, 0x2e, 0xf0, 0x76, 0xb7, 0x8f, 0xfc, 0xa7, 0x27, 0x78, 0x71, 0xb5, + 0xcd, 0xec, 0x9f, 0xf4, 0x84, 0x09, 0x51, 0x01, 0x70, 0x2e, 0xe6, 0x9d, 0x7f, 0x60, + 0x59, 0xe4, 0x97, 0x82, 0x23, 0x4b, 0xba, 0x43, 0x02, 0x2b, 0xa8, 0xd1, 0x07, 0x74, + 0xf1, 0x37, 0x9b, 0xe0, 0x7c, 0x82, 0x56, 0x8f, 0xd6, 0x18, 0xcb, 0xe9, 0x11, 0xba, + 0x23, 0x8d, 0xaa, 0x76, 0x87, 0x08, 0x69, 0xfa, 0xa3, 0x51, 0x1a, 0x03, 0x75, 0x4d, + 0x17, 0xf5, 0x50, 0x39, 0xd1, 0xf5, 0x28, 0x85, 0x36, 0x8a, 0x0e, 0xcf, 0x93, 0x4e, + 0x3f, 0x0f, 0xe3, 0x00, 0x8c, 0x82, 0x97, 0x2f, 0xde, 0xfb, 0x74, 0x6b, 0x04, 0x93, + 0x98, 0xbd, 0x7d, 0x0d, 0xf2, 0x5a, 0xcd, 0x51, 0xab, 0xce, 0xf5, 0xa1, 0xe4, 0x4d, + 0xcc, 0x0e, 0xde, 0x8b, 0xe3, 0x93, 0xec, 0xd8, 0x0e, 0x8f, 0xb3, 0x69, 0x99, 0x20, + 0xc1, 0x08, 0xec, 0xcf, 0x03, 0xdd, 0x68, 0x8e, 0x4d, 0x95, 0x37, 0x97, 0x6c, 0xc3, + 0x49, 0x48, 0x9a, 0xb1, 0x67, 0xa3, 0x6e, 0x78, 0xf6, 0xee, 0x52, 0x3e, 0x22, 0x09, + 0xc8, 0x86, 0x87, 0x63, 0xe0, 0xf6, 0xf3, 0x53, 0x7c, 0xbe, 0x64, 0x35, 0x5a, 0xec, + 0x12, 0x46, 0xe7, 0x20, 0xb8, 0xf6, 0x08, 0x09, 0x11, 0xf1, 0xfc, 0x6f, 0x5b, 0x30, + 0x18, 0xa3, 0x8d, 0x95, 0xa1, 0xc1, 0xa8, 0xe6, 0x73, 0x7f, 0x41, 0x59, 0xe6, 0x3d, + 0x3f, 0xd8, 0x1b, 0xcb, 0x00, 0xd1, 0x85, 0x9e, 0xcd, 0x79, 0x04, 0xea, 0x06, 0x0c, + 0xa8, 0x3b, 0xd9, 0xe0, 0x6d, 0x1a, 0x02, 0x04, 0x3c, 0x64, 0x8a, 0x49, 0x81, 0x51, + 0x50, 0x2e, 0x53, 0x4e, 0x69, 0x56, 0xde, 0x37, 0x62, 0xad, 0x69, 0x2a, 0xbe, 0xa4, + 0xf7, 0x7a, 0x2a, 0x12, 0xe6, 0xe5, 0x17, 0x02, 0x79, 0x7c, 0xb7, 0x9e, 0x39, 0x12, + 0x26, 0x93, 0x9b, 0x92, 0xb1, 0x0b, 0x58, 0x71, 0xe7, 0x13, 0x1a, 0xc8, 0xdc, 0x33, + 0x83, 0x14, 0xfa, 0xbf, 0xf4, 0x47, 0x79, 0xb6, 0xe1, 0x8b, 0x8d, 0xab, 0x92, 0xb0, + 0x0a, 0x6f, 0xd1, 0x27, 0x5c, 0xfb, 0xb0, 0x94, 0xaa, 0xd5, 0x2a, 0xfd, 0x0c, 0x39, + 0x25, 0x4e, 0x3e, 0x46, 0x5c, 0x42, 0xe1, 0x50, 0xab, 0xca, 0xf6, 0xd5, 0x3a, 0xc3, + 0x65, 0x39, 0xfb, 0xea, 0x55, 0xd7, 0x63, 0x34, 0xcf, 0xb0, 0x80, 0xb2, 0xfc, 0x4a, + 0xf4, 0x43, 0x6b, 0x3d, 0xb0, 0xfc, 0x86, 0x05, 0xa3, 0x66, 0xc5, 0xe8, 0x10, 0x8d, + 0xae, 0xa5, 0xa7, 0xc5, 0xc3, 0xd1, 0x57, 0x1a, 0x6f, 0x68, 0xd0, 0x62, 0x82, 0x99, + 0xcc, 0x24, 0x8b, 0x68, 0x3b, 0x16, 0x57, 0x1c, 0x6d, 0xc2, 0x99, 0xde, 0x8c, 0xa4, + 0x64, 0xdd, 0xba, 0x5b, 0x78, 0x7d, 0xa1, 0x2b, 0x9d, 0x25, 0x06, 0x59, 0xac, 0xd4, + 0x76, 0x5e, 0xa7, 0x1d, 0x2c, 0x9f, 0x12, 0x95, 0xe8, 0x7e, 0x5d, 0x8a, 0xbe, 0xdb, + 0xe4, 0x59, 0x02, 0x55, 0xf0, 0xb2, 0x4b, 0x15, 0xfd, 0xd9, 0xbd, 0x09, 0x1e, 0x96, + 0x60, 0x1d, 0xe8, 0x1e, 0x54, 0x75, 0x58, 0xb0, 0x0d, 0xf9, 0x9a, 0x5a, 0xd7, 0xc3, + 0x96, 0xb3, 0x89, 0x42, 0x2c, 0xc0, 0x36, 0xe9, 0x55, 0x37, 0x60, 0x07, 0x87, 0x6c, + 0x43, 0x8b, 0xf1, 0x58, 0xd8, 0xb5, 0x73, 0x42, 0xcd, 0xc0, 0x63, 0x04, 0xc9, 0x51, + 0xcd, 0xae, 0x1e, 0xaf, 0x75, 0x2e, 0x81, 0xed, 0xea, 0xa8, 0x0f, 0xde, 0x3d, 0x0c, + 0xc6, 0x64, 0xd2, 0xbb, 0xfc, 0xba, 0x06, 0xd4, 0xf4, 0xce, 0xeb, 0xdf, 0xdc, 0x15, + 0x24, 0x20, 0x76, 0xc3, 0x35, 0x8b, 0xad, 0x5c, 0xee, 0x96, 0x98, 0x6a, 0xf7, 0x88, + 0x88, 0x2b, 0xad, 0xf6, 0x1c, 0x79, 0x54, 0x04, 0xac, 0xed, 0xca, 0xc2, 0x7c, 0x2d, + 0x9e, 0x92, 0xf6, 0x47, 0xb1, 0x58, 0x5a, 0xa2, 0xcd, 0x9f, 0x99, 0x0a, 0x00, 0xbf, + 0xf7, 0xc2, 0x08, 0xba, 0x5d, 0x30, 0x73, 0xb8, 0xa0, 0x1b, 0xb4, 0x21, 0x71, 0xde, + 0xb5, 0xe1, 0xb7, 0x7b, 0xac, 0xc3, 0x0f, 0x06, 0xf1, 0x21, 0x37, 0xc2, 0x0e, 0x30, + 0xc3, 0x2e, 0xb7, 0xa3, 0x41, 0x0e, 0xf5, 0xca, 0x23, 0xe5, 0x47, 0xca, 0x05, 0x01, + 0x8d, 0x26, 0xb3, 0xfd, 0xd4, 0xb7, 0xc7, 0xee, 0x66, 0x2b, 0xaf, 0xb0, 0x21, 0x26, + 0x80, 0x31, 0x00, 0xa1, 0x04, 0x00, 0x7b, 0x1a, 0x6b, 0x29, 0x57, 0xbb, 0xaf, 0xa0, + 0xcd, 0x0a, 0xde, 0x84, 0x11, 0x69, 0xcd, 0xe9, 0xfc, 0x90, 0xe0, 0x2c, 0x33, 0x1d, + 0xeb, 0x84, 0x7b, 0xc5, 0xe5, 0x30, 0x92, 0x9c, 0xee, 0x34, 0x44, 0xe6, 0x09, 0xa8, + 0xd9, 0xd6, 0x7e, 0x78, 0x8d, 0xba, 0xf5, 0x82, 0x33, 0x00, 0xe8, 0x66, 0xc4, 0x88, + 0x45, 0x06, 0x1e, 0xbe, 0xe5, 0xb9, 0xcd, 0x06, 0x5e, 0x79, 0xfd, 0x8f, 0xa0, 0x97, + 0x15, 0xab, 0xf8, 0xce, 0xf4, 0x18, 0x9d, 0xf4, 0xbc, 0x49, 0xa3, 0x8f, 0xc2, 0x21, + 0xb8, 0xd6, 0xa1, 0x03, 0x29, 0x03, 0x6c, 0xba, 0x6b, 0xc1, 0x6e, 0xc2, 0x73, 0x14, + 0x2b, 0x24, 0xa0, 0x41, 0x24, 0x29, 0xd1, 0xe9, 0x9f, 0xee, 0xeb, 0x7d, 0x2f, 0x6f, + 0x31, 0x65, 0xb5, 0x09, 0x25, 0x27, 0x64, 0x32, 0xeb, 0xc6, 0xac, 0x1e, 0xbc, 0x0e, + 0x3e, 0x30, 0x09, 0xa6, 0xc1, 0xcf, 0x66, 0x85, 0x97, 0x0d, 0xae, 0x93, 0xed, 0xf8, + 0x00, 0x3c, 0xc3, 0x63, 0x9b, 0x18, 0x40, 0x83, 0x5f, 0x50, 0x42, 0x7a, 0x9e, 0xe4, + 0xaa, 0x15, 0x79, 0xbd, 0xa5, 0x8b, 0x89, 0x44, 0xd0, 0x76, 0x31, 0x64, 0xfa, 0x6f, + 0xd0, 0x67, 0xc2, 0x08, 0x6c, 0x43, 0x3a, 0xf4, 0xa7, 0xea, 0x3d, 0x31, 0x9f, 0x90, + 0xbc, 0xee, 0x45, 0x97, 0x4f, 0x36, 0xe4, 0x78, 0x39, 0x4e, 0x90, 0xea, 0x83, 0x82, + 0xd9, 0xf9, 0x3e, 0xcd, 0x64, 0x93, 0xc8, 0xd6, 0x25, 0x4d, 0x17, 0x02, 0xc3, 0xb9, + 0x32, 0x0d, 0xf3, 0x1a, 0x39, 0x9d, 0x00, 0x7b, 0x9f, 0xf2, 0x07, 0xb6, 0x7a, 0x4e, + 0xa6, 0x9e, 0x31, 0xea, 0x74, 0xe9, 0xfd, 0x26, 0xfa, 0x6c, 0xd7, 0xb2, 0x97, 0x1a, + 0x60, 0x0d, 0x37, 0x4e, 0x92, 0xe1, 0x09, 0x31, 0x37, 0x31, 0xb6, 0xaf, 0x7c, 0x83, + 0x95, 0xae, 0xef, 0xc9, 0x7d, 0x6e, 0xb9, 0x02, 0x1f, 0x9d, 0xdf, 0xc2, 0xc3, 0xe0, + 0x9a, 0x6f, 0x1f, 0x43, 0x06, 0x9a, 0xa3, 0x18, 0x88, 0x48, 0x83, 0x38, 0xbd, 0x70, + 0x04, 0x73, 0x3f, 0x34, 0x9f, 0x04, 0x77, 0x0c, 0x98, 0x23, 0xbc, 0x51, 0xbf, 0x32, + 0x14, 0x13, 0x39, 0xc9, 0x14, 0x2c, 0xb9, 0xc6, 0x21, 0xea, 0x28, 0x13, 0x27, 0xa7, + 0x81, 0x18, 0x99, 0xc9, 0x3f, 0x69, 0xe9, 0x87, 0x7a, 0x6c, 0xe8, 0xe2, 0x8e, 0x8b, + 0xc5, 0xb5, 0x8d, 0x9c, 0x33, 0xf1, 0xe9, 0x88, 0x48, 0xac, 0xbf, 0x45, 0x92, 0xdd, + 0x8b, 0x55, 0xc3, 0xbb, 0xc6, 0x28, 0xa1, 0xb6, 0xea, 0x0c, 0x29, 0x31, 0xce, 0xd3, + 0x2c, 0x5f, 0x07, 0x6d, 0xa9, 0x77, 0xf1, 0x1b, 0xb4, 0x9c, 0x51, 0x04, 0x6a, 0x0f, + 0xb6, 0x85, 0x45, 0x37, 0x5a, 0x77, 0xbf, 0x17, 0x4a, 0x3e, 0x45, 0xe2, 0x48, 0xca, + 0x73, 0xa7, 0x92, 0xfa, 0x14, 0x28, 0xa4, 0xc1, 0x13, 0xfa, 0x91, 0x44, 0x6d, 0x77, + 0x11, 0xd0, 0xc5, 0xcf, 0xf3, 0x39, 0x87, 0x99, 0xc3, 0x67, 0xe7, 0xe5, 0x7c, 0x6d, + 0xd7, 0x4b, 0x84, 0xfc, 0x50, 0x53, 0x6c, 0x26, 0xf2, 0xe4, 0x25, 0x42, 0x02, 0xec, + 0xc8, 0x9c, 0x77, 0x2c, 0x47, 0x6d, 0xba, 0x07, 0xec, 0xce, 0xae, 0xc9, 0xd6, 0xdd, + 0xb3, 0xbc, 0xea, 0x26, 0x52, 0xe8, 0x6e, 0xc5, 0x79, 0xad, 0x91, 0xa8, 0x9a, 0xfe, + 0xb9, 0x1e, 0x39, 0xca, 0x37, 0xa5, 0xa3, 0x74, 0xb0, 0x26, 0xae, 0xce, 0x78, 0xf3, + 0xd1, 0x97, 0x3d, 0x14, 0xb9, 0x6a, 0xd5, 0x90, 0xd0, 0xbf, 0xc5, 0x8b, 0x0b, 0xb2, + 0xc3, 0x27, 0x26, 0x6d, 0x2b, 0x93, 0xa2, 0x6b, 0xad, 0x91, 0x75, 0x90, 0x58, 0x57, + 0x4e, 0x86, 0xa6, 0xac, 0x20, 0x5e, 0xf5, 0xbf, 0xb7, 0xf1, 0x47, 0x99, 0x16, 0xc7, + 0xbb, 0x87, 0x0a, 0x3b, 0x82, 0x24, 0xd7, 0xbc, 0x82, 0x17, 0x40, 0x21, 0xe0, 0xa9, + 0xfa, 0x4f, 0xd0, 0xf5, 0x84, 0xcb, 0x43, 0x24, 0x10, 0xab, 0x3c, 0xb8, 0xfc, 0xbc, + 0xdf, 0x83, 0x25, 0xec, 0xec, 0xe6, 0x5b, 0xc3, 0xb3, 0x40, 0x93, 0x46, 0x75, 0x68, + 0xec, 0x6e, 0xac, 0x5e, 0xdd, 0x46, 0xe2, 0x46, 0x72, 0xd2, 0x56, 0xa6, 0xfc, 0x5b, + 0x88, 0xcf, 0x11, 0x03, 0x5e, 0x32, 0x42, 0x33, 0x24, 0xa1, 0x8f, 0x33, 0x50, 0x0f, + 0x8e, 0x10, 0x4d, 0x33, 0x9d, 0x8e, 0x32, 0x28, 0x42, 0x72, 0x27, 0x57, 0x70, 0x54, + 0xc2, 0x39, 0xcb, 0xb4, 0x85, 0x48, 0xf4, 0x03, 0xf2, 0x44, 0x6c, 0x95, 0x46, 0x02, + 0xf0, 0x0e, 0x2a, 0x31, 0x1e, 0x67, 0x52, 0x3a, 0x55, 0x55, 0x46, 0x75, 0xc7, 0x23, + 0x68, 0x0b, 0x39, 0xb8, 0x6b, 0xa2, 0xb5, 0xed, 0x7b, 0x3b, 0x6f, 0xf5, 0x26, 0xbb, + 0x18, 0x16, 0x45, 0x76, 0xd2, 0x05, 0x35, 0x61, 0x65, 0xfd, 0x4e, 0x72, 0x71, 0xaa, + 0x8e, 0x92, 0x29, 0x77, 0x55, 0x4e, 0x82, 0x67, 0x54, 0xcc, 0xf7, 0x48, 0xe7, 0x78, + 0x49, 0x23, 0x59, 0xf4, 0x9b, 0xe1, 0x80, 0x68, 0x54, 0x94, 0xf8, 0xa0, 0x39, 0x78, + 0x7e, 0xda, 0xcd, 0x9d, 0x57, 0xb0, 0x18, 0xa6, 0xd8, 0x78, 0xcc, 0xe2, 0x24, 0x3a, + 0xcc, 0xa2, 0x6f, 0x2f, 0x7a, 0x27, 0x17, 0x70, 0x0e, 0x99, 0x90, 0x71, 0xf0, 0xb3, + 0x78, 0x9f, 0xfb, 0x1a, 0x1c, 0x98, 0xa0, 0x81, 0x60, 0x58, 0x97, 0x36, 0x5e, 0xf1, + 0xdb, 0x2f, 0x17, 0xa1, 0x28, 0xa7, 0xe3, 0x97, 0xad, 0x0a, 0x53, 0x0d, 0x20, 0x95, + 0x06, 0x43, 0xb2, 0x89, 0x4a, 0xc5, 0x29, 0x91, 0x4c, 0xb6, 0x46, 0xda, 0xc0, 0x41, + 0xc5, 0x52, 0xe6, 0x83, 0xac, 0xdd, 0xef, 0x2b, 0xfb, 0x3f, 0xe6, 0x6c, 0xe0, 0xa7, + 0x8c, 0xbf, 0xa4, 0xaf, 0x54, 0xce, 0x5c, 0xfc, 0x9e, 0x07, 0x2a, 0x71, 0xe6, 0x38, + 0xb2, 0xd0, 0x26, 0x0b, 0xcb, 0x1c, 0xb9, 0x57, 0x4b, 0xeb, 0x71, 0x4a, 0xd6, 0x9c, + 0x6e, 0x86, 0xcd, 0x28, 0x32, 0x9f, 0x40, 0x1f, 0xa3, 0xe4, 0xa6, 0x89, 0x6b, 0x7a, + 0x42, 0xea, 0xff, 0x30, 0x93, 0xdc, 0xbf, 0xfe, 0x1d, 0x4f, 0x89, 0x9a, 0xf1, 0x61, + 0xf5, 0x84, 0xe6, 0xd0, 0xbd, 0x81, 0x32, 0xf8, 0xb8, 0x64, 0xa4, 0x5d, 0x7e, 0x72, + 0x38, 0xe3, 0x02, 0xfd, 0x06, 0x48, 0x6a, 0xe0, 0x32, 0xf7, 0x7d, 0xad, 0xd6, 0x20, + 0x1b, 0x1b, 0x9c, 0xbe, 0x60, 0x38, 0x3a, 0xc8, 0x46, 0x77, 0xe8, 0xbd, 0x29, 0x96, + 0x5c, 0xd9, 0x9e, 0x80, 0x16, 0x4a, 0x9c, 0xa6, 0xad, 0xcf, 0x38, 0xe9, 0x59, 0xb7, + 0xab, 0x11, 0xec, 0xa0, 0x9a, 0x7c, 0x33, 0x44, 0x85, 0x89, 0x42, 0x77, 0x6a, 0x76, + 0xe6, 0x36, 0x83, 0x17, 0x7c, 0xca, 0xb4, 0xd9, 0x37, 0x64, 0x64, 0xd1, 0xda, 0xd6, + 0xb8, 0x10, 0x4d, 0x47, 0x68, 0x3e, 0xd0, 0x10, 0x7e, 0x52, 0x93, 0xf7, 0xed, 0xa4, + 0xa6, 0xf2, 0x53, 0xd1, 0xad, 0x5f, 0xa7, 0xcd, 0x94, 0x04, 0x01, 0x0a, 0x3e, 0x4d, + 0x3d, 0xe1, 0x48, 0x80, 0x96, 0xf5, 0xed, 0x83, 0x3e, 0x66, 0xb0, 0x35, 0xde, 0xac, + 0xab, 0x67, 0x5c, 0x33, 0xcf, 0xba, 0x2c, 0xb7, 0x7a, 0xa9, 0x32, 0xf2, 0x43, 0x4f, + 0x99, 0x1e, 0x7e, 0x30, 0x6c, 0x4f, 0x87, 0xff, 0x9d, 0x92, 0xfe, 0xde, 0x16, 0xa6, + 0xf8, 0x7f, 0x2d, 0x80, 0x87, 0x2d, 0x16, 0x2d, 0x44, 0x37, 0x9c, 0x49, 0x95, 0xf9, + 0xc1, 0x89, 0x18, 0x16, 0x5c, 0x72, 0x2d, 0x90, 0x8a, 0x32, 0x51, 0x72, 0x24, 0x1d, + 0x1a, 0x80, 0xc0, 0x24, 0x2a, 0x95, 0x84, 0xc1, 0x94, 0x53, 0x6a, 0xa1, 0x13, 0x7a, + 0x86, 0xc3, 0x7a, 0x5c, 0x31, 0xed, 0xba, 0xe4, 0x30, 0x35, 0x26, 0x52, 0xf6, 0x33, + 0x21, 0x3b, 0xac, 0xe5, 0x3a, 0x79, 0xb2, 0x0f, 0x3c, 0xed, 0xa1, 0x66, 0xc0, 0x06, + 0x98, 0xaa, 0x26, 0xbc, 0x4c, 0x1a, 0xf1, 0x22, 0xf5, 0x57, 0x57, 0xdb, 0x60, 0x58, + 0x65, 0x5f, 0x06, 0x51, 0xd3, 0x66, 0x05, 0x12, 0x61, 0x7e, 0x09, 0x8d, 0x47, 0x4d, + 0xac, 0xf7, 0x70, 0xfb, 0x18, 0xe6, 0x7e, 0x20, 0xea, 0xd9, 0x9a, 0xfc, 0x83, 0x42, + 0x37, 0x43, 0x85, 0x8d, 0xa1, 0x5b, 0xd5, 0xcb, 0xdc, 0x1d, 0x8c, 0xc6, 0x80, 0x9a, + 0x57, 0x54, 0x86, 0x8d, 0x13, 0x45, 0xea, 0x8c, 0xdf, 0x07, 0xb6, 0x25, 0xec, 0x6e, + 0xba, 0x9f, 0xb1, 0xa1, 0xd4, 0xc7, 0xad, 0x9c, 0x83, 0x68, 0x9d, 0xcf, 0x09, 0x05, + 0x76, 0xab, 0xb5, 0xf2, 0xc3, 0x14, 0x17, 0xe5, 0x2e, 0xea, 0xe1, 0x1a, 0xa4, 0xdc, + 0x7c, 0x70, 0xc5, 0xf2, 0x0a, 0x2e, 0xa5, 0xaf, 0x34, 0xaf, 0x72, 0x3f, 0xbc, 0x9d, + 0x12, 0x80, 0xb1, 0x33, 0x00, 0x6f, 0x35, 0xf4, 0xa6, 0x94, 0x09, 0xa1, 0xd7, 0xdd, + 0x57, 0xd1, 0x8b, 0xc4, 0x35, 0xd5, 0x8c, 0xcd, 0xf4, 0xdd, 0x20, 0x8e, 0xe1, 0xab, + 0xca, 0xc4, 0x95, 0x8b, 0x1e, 0x7b, 0xee, 0x27, 0xf2, 0x90, 0x76, 0x72, 0x12, 0xd9, + 0x75, 0xef, 0x2a, 0x78, 0xc1, 0x53, 0xf7, 0x7d, 0x86, 0x44, 0x3b, 0x17, 0xc1, 0x4c, + 0x45, 0xcd, 0x16, 0xe6, 0x49, 0xbb, 0x8b, 0x23, 0xdd, 0x7f, 0x5d, 0xea, 0x2b, 0xea, + 0x48, 0x0c, 0xac, 0x86, 0xfd, 0x90, 0xe0, 0xcb, 0x36, 0x8b, 0x42, 0x89, 0xa5, 0x95, + 0x11, 0xc9, 0xfc, 0x8f, 0x2f, 0x9e, 0xc4, 0x9e, 0xfe, 0x6f, 0xf0, 0x07, 0x4e, 0x9d, + 0x3d, 0x63, 0xcd, 0x88, 0x08, 0x74, 0xff, 0x1e, 0x1d, 0xc7, 0x33, 0x38, 0x06, 0x6e, + 0x4a, 0xaa, 0x30, 0xf1, 0x8e, 0xd9, 0x9a, 0x58, 0xfd, 0x0e, 0x7a, 0x43, 0xfb, 0x02, + 0xc7, 0xa8, 0x46, 0x03, 0xbe, 0x43, 0x49, 0xea, 0x12, 0xce, 0x73, 0x9a, 0x61, 0x0a, + 0xdd, 0x80, 0x42, 0x3e, 0xe8, 0x31, 0x33, 0x1b, 0x49, 0x2a, 0xd4, 0x94, 0xaf, 0xa1, + 0x75, 0x44, 0xf5, 0xca, 0xe8, 0xfe, 0x5c, 0x89, 0xbb, 0x31, 0xbb, 0x9c, 0x37, 0x70, + 0x03, 0x4b, 0xfc, 0x0e, 0x73, 0xb3, 0xb7, 0xde, 0xff, 0x3c, 0x7a, 0x1b, 0xaa, 0x2e, + 0x03, 0x64, 0x8f, 0x07, 0x93, 0x1a, 0xcd, 0xad, 0xa3, 0xed, 0x32, 0x37, 0x90, 0x4c, + 0x3e, 0xca, 0x2a, 0x15, 0x02, 0xb3, 0xe6, 0x00, 0xb9, 0x05, 0x8f, 0x5c, 0x6f, 0x7a, + 0xf6, 0xf0, 0xe9, 0x49, 0x01, 0xce, 0xee, 0xf1, 0xa0, 0x50, 0x76, 0x89, 0xf4, 0x50, + 0x08, 0xf8, 0x28, 0xd1, 0xa2, 0x82, 0xfa, 0xe9, 0x8f, 0x0e, 0x33, 0x6b, 0x54, 0x84, + 0x0a, 0x67, 0xa7, 0x8c, 0x2f, 0xaf, 0x15, 0x08, 0x56, 0xaa, 0x56, 0x73, 0xc2, 0x44, + 0x24, 0x71, 0x6a, 0x40, 0x4d, 0xca, 0x97, 0x05, 0x31, 0x43, 0x7a, 0x43, 0x5a, 0x20, + 0x4a, 0xae, 0x03, 0x7a, 0xd0, 0xf4, 0x9f, 0xe0, 0xe3, 0x8e, 0x68, 0xc7, 0x10, 0x89, + 0xae, 0xf2, 0x3b, 0x6a, 0x29, 0xe0, 0xfe, 0x20, 0xaf, 0x0b, 0x29, 0xd2, 0xc3, 0x81, + 0xc6, 0x29, 0x29, 0xc6, 0xf2, 0xb6, 0x67, 0xed, 0x86, 0xb8, 0x34, 0x87, 0xc2, 0xbc, + 0x37, 0x32, 0x7d, 0x9f, 0x09, 0xbd, 0xcf, 0xc4, 0x97, 0xd3, 0xfb, 0xbb, 0xe1, 0xa7, + 0xd5, 0x09, 0x1e, 0x73, 0xc5, 0x61, 0xb0, 0xef, 0xd6, 0xa0, 0xf8, 0x31, 0x55, 0x5e, + 0x76, 0xda, 0xa7, 0x22, 0x2e, 0x80, 0x61, 0xeb, 0x29, 0x7d, 0x9a, 0xa3, 0x02, 0x5f, + 0x94, 0x96, 0xa3, 0x52, 0xde, 0xc1, 0xdb, 0x4c, 0x2b, 0xbc, 0x4c, 0xd8, 0x10, 0x41, + 0xb9, 0x46, 0x47, 0xc5, 0x5b, 0xc2, 0x5a, 0xc1, 0x94, 0x0a, 0xb5, 0x99, 0xd9, 0x29, + 0x84, 0xae, 0xc9, 0xd8, 0x96, 0x65, 0x5b, 0xc5, 0xb3, 0xe7, 0x65, 0x35, 0xdc, 0x8e, + 0x35, 0xb3, 0xf6, 0xb8, 0x40, 0xb0, 0xfc, 0x8b, 0xf0, 0x69, 0xe4, 0x06, 0x04, 0x11, + 0xd3, 0xb5, 0x01, 0xe3, 0xbb, 0x92, 0xeb, 0xb8, 0x47, 0x67, 0x6c, 0xb1, 0x42, 0x8b, + 0x76, 0xc4, 0x9e, 0xd9, 0x47, 0x55, 0xdb, 0xa5, 0x00, 0x48, 0x0c, 0x81, 0x0d, 0x6f, + 0xe9, 0xd2, 0x84, 0x58, 0xfd, 0xba, 0x67, 0x44, 0xf6, 0x7c, 0x94, 0x95, 0x2f, 0x35, + 0x59, 0xc0, 0xac, 0x16, 0xd4, 0x37, 0x30, 0x3a, 0x53, 0xdd, 0x65, 0xa0, 0x40, 0x9b, + 0x1b, 0x26, 0xb6, 0xd9, 0x3f, 0x86, 0x3c, 0x31, 0x43, 0x9c, 0x80, 0x56, 0xb1, 0xbf, + 0x71, 0xbc, 0x97, 0x87, 0xc9, 0x02, 0x2c, 0xbf, 0x63, 0x7f, 0x40, 0xde, 0xc6, 0xcc, + 0x4b, 0x1b, 0x49, 0xd8, 0x9f, 0x6b, 0x47, 0xcc, 0x05, 0xd8, 0x4b, 0xc4, 0xba, 0x00, + 0x25, 0x02, 0xac, 0xd6, 0xb6, 0x9c, 0x69, 0x2a, 0x2c, 0x0b, 0xaa, 0xba, 0x73, 0x73, + 0x23, 0xbd, 0xab, 0x64, 0x87, 0x80, 0x96, 0xc4, 0x5a, 0x5a, 0x47, 0x80, 0x10, 0x86, + 0x2b, 0x02, 0x09, 0xdf, 0x7d, 0x07, 0x3c, 0xf2, 0x66, 0x42, 0xef, 0xdb, 0x6e, 0xe2, + 0x63, 0xfa, 0xd4, 0x11, 0x8a, 0x7b, 0xda, 0x8e, 0xd0, 0xb0, 0x7e, 0x61, 0x19, 0x04, + 0x67, 0x4b, 0x09, 0x03, 0xcc, 0x43, 0x85, 0x05, 0x32, 0x70, 0xd4, 0xf2, 0x50, 0xe9, + 0x4b, 0x64, 0xed, 0x0b, 0x98, 0xb0, 0x8b, 0xb5, 0x77, 0x83, 0xe5, 0xd3, 0x0a, 0x99, + 0xf0, 0x31, 0xab, 0x03, 0x79, 0xe4, 0x42, 0xf7, 0x05, 0xe9, 0x3b, 0xc5, 0x93, 0xd9, + 0xa2, 0x60, 0xc6, 0xa4, 0xc9, 0x7e, 0xa4, 0x35, 0x3b, 0x7f, 0xd2, 0xda, 0x2f, 0xf9, + 0x89, 0x9a, 0xeb, 0x7d, 0x0b, 0xfd, 0xee, 0x43, 0xa7, 0x19, 0x28, 0xc4, 0xac, 0xbf, + 0x5f, 0x09, 0x61, 0xff, 0x11, 0x75, 0x83, 0x3d, 0x9a, 0xe0, 0x30, 0xd2, 0x47, 0xd2, + 0xdd, 0xf8, 0x9e, 0x8c, 0x95, 0x2d, 0x0e, 0x48, 0x12, 0xd3, 0xae, 0x8d, 0x41, 0xfb, + 0x9d, 0x36, 0x4a, 0x7a, 0x2f, 0xe3, 0x07, 0xf2, 0x1d, 0x3c, 0x8d, 0x2c, 0x56, 0xf9, + 0x58, 0xdc, 0x51, 0x0d, 0x2d, 0xd0, 0xd7, 0xd2, 0xc3, 0x4a, 0xc8, 0xd3, 0x4f, 0x24, + 0xf1, 0x5b, 0x1f, 0x9a, 0x43, 0xa8, 0xe4, 0xff, 0xbc, 0x2a, 0xf9, 0x8e, 0x86, 0x11, + 0xb4, 0xa9, 0xfd, 0xea, 0x4d, 0xee, 0xe7, 0x75, 0x65, 0x23, 0x7b, 0xe8, 0x62, 0x47, + 0x49, 0x11, 0x49, 0xbc, 0xd2, 0xad, 0xac, 0xad, 0x4a, 0xdc, 0x7f, 0x3d, 0xcb, 0x15, + 0xfa, 0x66, 0xba, 0x96, 0x46, 0x64, 0xc0, 0x42, 0x62, 0x8b, 0xb2, 0x8e, 0xad, 0x10, + 0xc1, 0xc3, 0x85, 0x7e, 0x8e, 0xbd, 0xe6, 0x2f, 0x51, 0xce, 0x14, 0x3c, 0xac, 0xb7, + 0x3e, 0x7f, 0x6a, 0x79, 0x31, 0x0b, 0xc0, 0xa4, 0x98, 0xbf, 0xcd, 0xfc, 0xae, 0x12, + 0x97, 0x46, 0xf0, 0x7f, 0x86, 0x33, 0x38, 0xbe, 0x7a, 0xae, 0xa8, 0x18, 0xe2, 0x0f, + 0x3e, 0x33, 0x25, 0x76, 0xa6, 0xd7, 0x6a, 0xb6, 0x25, 0x60, 0x68, 0x18, 0x9b, 0x45, + 0xfa, 0xf4, 0xfa, 0x53, 0x89, 0x51, 0x0c, 0xc0, 0x5e, 0x48, 0x75, 0xd3, 0x0d, 0x65, + 0xe0, 0x48, 0x10, 0x37, 0x24, 0xec, 0x1f, 0x22, 0xcc, 0x48, 0x97, 0x4e, 0xfc, 0x0f, + 0x2a, 0x46, 0xe1, 0xa4, 0xc0, 0xa8, 0xa9, 0xa9, 0x64, 0xf4, 0xa5, 0x3d, 0x7e, 0xbf, + 0x4e, 0xfc, 0x64, 0x70, 0x37, 0xfe, 0x65, 0xcc, 0x7c, 0xfd, 0xcc, 0x95, 0xa7, 0xfb, + 0xfb, 0x84, 0xc9, 0x03, 0xa4, 0x03, 0x26, 0x55, 0x54, 0x89, 0xbc, 0x43, 0x03, 0x5c, + 0xfe, 0x08, 0xe2, 0x0b, 0x6d, 0xde, 0xdb, 0xec, 0xf9, 0x99, 0x87, 0xf8, 0x6c, 0xdb, + 0xc8, 0xa7, 0xc2, 0xb4, 0x87, 0xd3, 0xac, 0xdd, 0x22, 0xc8, 0x48, 0x76, 0xc7, 0x4d, + 0x2b, 0x45, 0xc6, 0xb1, 0x7a, 0x5b, 0x01, 0xfb, 0x48, 0xe8, 0xce, 0x0c, 0x5e, 0xb6, + 0xb9, 0x0f, 0x87, 0xfd, 0x88, 0x27, 0x17, 0xe5, 0xba, 0x94, 0xc5, 0xf9, 0x86, 0x31, + 0x45, 0xe0, 0x0d, 0x0e, 0xd7, 0x96, 0xc8, 0xc2, 0xf3, 0xd5, 0x5a, 0x63, 0xff, 0xcc, + 0x6a, 0x28, 0xe1, 0xd1, 0x2c, 0x93, 0xce, 0xc3, 0x51, 0xf5, 0x63, 0xce, 0x0b, 0x33, + 0x89, 0x83, 0x59, 0x42, 0xa5, 0xc6, 0x2a, 0xc3, 0xcd, 0x92, 0x6b, 0xdd, 0x28, 0x09, + 0xdd, 0xb5, 0xab, 0x24, 0x34, 0x3a, 0x58, 0x05, 0xf1, 0x25, 0xf7, 0x10, 0xf3, 0xd5, + 0x52, 0x40, 0xe1, 0xb9, 0x17, 0x50, 0x65, 0x52, 0xb2, 0x44, 0xb3, 0x85, 0xa3, 0x75, + 0x21, 0x1e, 0x94, 0x96, 0x24, 0xb7, 0xc8, 0x75, 0xd6, 0x00, 0xc4, 0xf6, 0xdb, 0x4f, + 0x77, 0xd9, 0xd4, 0x2a, 0xe3, 0x52, 0xc2, 0x94, 0xf5, 0xcf, 0xf2, 0x6e, 0x93, 0x77, + 0xec, 0x91, 0x5a, 0x09, 0x9d, 0x75, 0xb6, 0x69, 0xf7, 0x0a, 0x8c, 0x07, 0x11, 0x3b, + 0x85, 0x91, 0xe5, 0xa9, 0x37, 0xe2, 0x4d, 0xa2, 0x45, 0xbc, 0x54, 0x4e, 0x90, 0xe2, + 0xf8, 0xf8, 0x67, 0x58, 0x6e, 0xb8, 0xc6, 0x0e, 0x6f, 0xb5, 0x2f, 0x18, 0xfe, 0x98, + 0x37, 0x83, 0x05, 0xec, 0x4b, 0x9d, 0x52, 0x3b, 0x6d, 0x4b, 0xed, 0x34, 0xff, 0xd7, + 0x71, 0x86, 0x9c, 0xb9, 0x9b, 0x6d, 0x25, 0xff, 0xa6, 0x4c, 0x23, 0x16, 0xab, 0xa0, + 0x86, 0x8a, 0x5e, 0x7f, 0xc7, 0x7d, 0x8e, 0x09, 0xb2, 0xe7, 0xbd, 0x93, 0xfc, 0x73, + 0x57, 0xb1, 0xf5, 0xb7, 0x14, 0xf0, 0x96, 0x94, 0x67, 0x74, 0x8e, 0x74, 0x1c, 0xda, + 0x47, 0x8b, 0x04, 0x24, 0xd3, 0xe1, 0xca, 0xf6, 0xbc, 0xd2, 0x67, 0xd6, 0x42, 0xe0, + 0xf2, 0x74, 0xf1, 0x8d, 0x39, 0xfa, 0x93, 0xbb, 0x6a, 0x39, 0x69, 0xa4, 0x8a, 0xa1, + 0x25, 0x71, 0xb0, 0x83, 0xe0, 0x94, 0x10, 0xe9, 0x4c, 0x3f, 0x77, 0x3b, 0xe3, 0x14, + 0xd0, 0xfd, 0x8e, 0xa2, 0xbe, 0xf4, 0x96, 0xba, 0x1f, 0x7d, 0xf5, 0xb8, 0x7e, 0xa3, + 0x7a, 0xc6, 0x96, 0xde, 0xc7, 0x98, 0xba, 0xbf, 0xf3, 0x67, 0xbd, 0x31, 0xbb, 0x62, + 0x9b, 0xc2, 0x3c, 0xc9, 0x76, 0x6c, 0x74, 0xd9, 0xd5, 0xdb, 0x8d, 0x70, 0x16, 0x01, + 0xe7, 0x04, 0xf7, 0xb1, 0xe8, 0x61, 0xc2, 0x04, 0xaf, 0xc4, 0xb8, 0x40, 0xb6, 0xfa, + 0x44, 0xc0, 0x6e, 0xae, 0xcb, 0xdb, 0x0c, 0x1f, 0x88, 0xc9, 0xed, 0x14, 0xb5, 0x25, + 0x89, 0x1e, 0xe4, 0x87, 0x07, 0xbd, 0xb1, 0xda, 0xed, 0x03, 0xc9, 0xe2, 0x32, 0x2d, + 0x81, 0x53, 0xd8, 0x91, 0x5f, 0x87, 0xdb, 0xdc, 0xc7, 0x4e, 0x27, 0x11, 0x23, 0x7d, + 0xee, 0x2a, 0x20, 0xcc, 0x59, 0x3a, 0x8a, 0x5a, 0x5b, 0x57, 0xe4, 0x71, 0xa5, 0x0d, + 0xf6, 0xbe, 0x6a, 0xa8, 0xfd, 0x11, 0x89, 0xe5, 0xee, 0x84, 0xe9, 0x46, 0x63, 0xda, + 0x3d, 0xd5, 0x60, 0xe4, 0xe9, 0x1f, 0x7c, 0xb7, 0x2f, 0x2c, 0x2b, 0x5f, 0x62, 0x0c, + 0x8a, 0x61, 0x43, 0xe5, 0x6d, 0xdc, 0xe5, 0x8e, 0x02, 0x59, 0x8d, 0x24, 0x14, 0x4d, + 0x47, 0x96, 0x9c, 0x18, 0xf4, 0x4d, 0x3b, 0x9e, 0x42, 0x6d, 0xfd, 0xe2, 0xc3, 0xb4, + 0xfe, 0xf9, 0x71, 0xd4, 0x0c, 0x20, 0xc4, 0x5f, 0x78, 0xed, 0x73, 0xc8, 0x76, 0x4f, + 0x8c, 0x95, 0x59, 0xb6, 0x6b, 0x7b, 0x7f, 0xa1, 0xd4, 0x0a, 0xc1, 0x68, 0xf5, 0x0f, + 0x14, 0xc5, 0xb9, 0xd0, 0xe8, 0xbd, 0xa3, 0xa5, 0x12, 0x91, 0xb8, 0x7b, 0x92, 0x1a, + 0xe0, 0x28, 0x45, 0x5b, 0x62, 0x0c, 0x76, 0x3e, 0x12, 0xe3, 0xca, 0x01, 0x82, 0x6c, + 0xeb, 0x39, 0x67, 0xd9, 0x9d, 0x0b, 0x0f, 0x04, 0xec, 0x11, 0xea, 0xcf, 0xc5, 0x7a, + 0xb1, 0x23, 0x9a, 0xca, 0x54, 0xab, 0xaf, 0xc3, 0xa0, 0x71, 0xcb, 0x2e, 0x33, 0x5d, + 0x3a, 0xe5, 0x97, 0x96, 0x2f, 0x01, 0x0e, 0x42, 0x59, 0xae, 0x25, 0xca, 0x51, 0xe6, + 0xd0, 0xec, 0x5d, 0x06, 0xe3, 0x5d, 0x0b, 0xa1, 0x61, 0x85, 0x0d, 0x44, 0xe2, 0xd2, + 0xd7, 0x32, 0xb5, 0x3c, 0xe8, 0x39, 0xc8, 0x2c, 0x31, 0xf8, 0xd1, 0xc0, 0x2d, 0x2d, + 0x58, 0x49, 0x75, 0xe4, 0x6f, 0xb2, 0xb6, 0xf7, 0x0e, 0x32, 0x80, 0xbb, 0x82, 0xb9, + 0x2d, 0xfb, 0x33, 0xb3, 0x6b, 0x04, 0xe1, 0x1d, 0x48, 0x8b, 0x9e, 0x60, 0xfe, 0x2d, + 0xc0, 0xa9, 0x3b, 0x54, 0x72, 0x01, 0xd2, 0x29, 0x4e, 0xd3, 0xbc, 0x51, 0xab, 0x73, + 0x51, 0x7a, 0xeb, 0x1c, 0x65, 0x5c, 0x07, 0x0f, 0xf8, 0x1e, 0x91, 0xe9, 0xe5, 0x34, + 0xca, 0xaf, 0x9d, 0x14, 0xca, 0x55, 0x43, 0xdd, 0x63, 0xcb, 0x6e, 0xd6, 0x7f, 0x48, + 0x80, 0x62, 0xf8, 0x63, 0x3d, 0x11, 0x92, 0x5b, 0xaf, 0xab, 0x86, 0x99, 0x45, 0x66, + 0x3e, 0x93, 0x6d, 0x03, 0xd3, 0x8a, 0x27, 0x32, 0x1a, 0xc6, 0xcf, 0x6b, 0xc4, 0x92, + 0x6b, 0xb6, 0x3c, 0xc3, 0xd0, 0xe0, 0xba, 0x2f, 0xb9, 0x4e, 0x96, 0xdd, 0xb5, 0x81, + 0xc8, 0x6c, 0x4a, 0x3b, 0x06, 0x41, 0x1f, 0x91, 0x5b, 0x65, 0xf7, 0x36, 0x09, 0xfa, + 0x87, 0x2b, 0x8c, 0x59, 0xdb, 0xce, 0xd5, 0xa1, 0xe8, 0xf8, 0x45, 0xcc, 0x42, 0xec, + 0xb8, 0xca, 0x48, 0x74, 0xc7, 0xd3, 0xfd, 0xba, 0xfc, 0xaf, 0xc3, 0x65, 0x2c, 0x5f, + 0x4e, 0xbc, 0xe1, 0x0f, 0x2e, 0x47, 0x4c, 0x65, 0x89, 0x19, 0xe1, 0xd7, 0x14, 0x57, + 0x2d, 0xb9, 0x84, 0x85, 0xf3, 0x67, 0xe5, 0xa9, 0x29, 0xa3, 0x0c, 0xc3, 0xed, 0x49, + 0x31, 0xd7, 0x08, 0xed, 0x48, 0xe8, 0x5b, 0x1a, 0x2a, 0x4f, 0x92, 0x1f, 0x8c, 0xf8, + 0xb7, 0x1b, 0xcb, 0xfd, 0x1e, 0x89, 0x4f, 0x57, 0x1c, 0x40, 0x9a, 0x79, 0xf7, 0x03, + 0xaa, 0x51, 0xbb, 0x6d, 0x6c, 0x7d, 0xce, 0xf0, 0xd5, 0x75, 0x73, 0x1a, 0x1d, 0xb1, + 0x97, 0x7f, 0x92, 0x49, 0x7e, 0x77, 0xaa, 0x9b, 0x8a, 0xb9, 0x71, 0xd9, 0xf2, 0xa6, + 0x7d, 0x15, 0x4a, 0x57, 0x6d, 0x60, 0x7a, 0xfa, 0xd2, 0xfc, 0x2a, 0xc0, 0xad, 0x04, + 0xa7, 0xe3, 0x17, 0x53, 0x2b, 0x02, 0x30, 0x6a, 0x75, 0x51, 0x92, 0x72, 0xd0, 0x1b, + 0x60, 0xa6, 0x22, 0xd0, 0xa1, 0x27, 0xc5, 0x2d, 0xcc, 0xde, 0xa3, 0x99, 0xbd, 0x30, + 0x88, 0xd8, 0xe6, 0xf6, 0x97, 0x3a, 0xe8, 0xd9, 0x05, 0x0a, 0xb7, 0xba, 0x09, 0x57, + 0xae, 0xcf, 0x39, 0x40, 0xc1, 0xec, 0x8c, 0xb5, 0x81, 0x25, 0xa6, 0xda, 0x26, 0xc6, + 0x48, 0x52, 0x8b, 0x92, 0x31, 0x02, 0xa8, 0xe9, 0x3f, 0x1d, 0x31, 0xf6, 0xb2, 0xe6, + 0x7a, 0x05, 0x0f, 0x91, 0x49, 0x09, 0x69, 0x4d, 0xe9, 0x7d, 0x4f, 0x42, 0x82, 0xf6, + 0x80, 0xb5, 0x86, 0x5c, 0x73, 0xf9, 0xce, 0x1e, 0x83, 0x4f, 0xbe, 0x96, 0xdf, 0xe5, + 0x3a, 0x78, 0xfd, 0xaf, 0x53, 0xca, 0x1c, 0xad, 0x60, 0xcf, 0x69, 0x62, 0x60, 0x2c, + 0x84, 0x1e, 0x93, 0x73, 0x7e, 0xae, 0x33, 0xa1, 0xe3, 0x66, 0xd2, 0x00, 0x0c, 0x75, + 0xa6, 0x2e, 0x90, 0x60, 0x35, 0x33, 0x05, 0x69, 0xed, 0xe0, 0xbf, 0x23, 0x7a, 0x0b, + 0x23, 0x87, 0x57, 0x75, 0xe7, 0xc0, 0xfc, 0x68, 0xf9, 0x1a, 0x9f, 0x99, 0x23, 0x5b, + 0x84, 0x04, 0xe0, 0x94, 0xec, 0x8a, 0x13, 0xe9, 0xbb, 0x9a, 0x85, 0x24, 0x6f, 0x2b, + 0xea, 0x77, 0x9f, 0xbd, 0xbd, 0x0c, 0xb3, 0xc1, 0xb7, 0xcd, 0x59, 0xef, 0xbe, 0x9e, + 0xe4, 0xf7, 0x41, 0x8f, 0x0d, 0x1f, 0xb5, 0xa9, 0xe2, 0x0b, 0x76, 0x27, 0x19, 0xb3, + 0xd8, 0x14, 0x5f, 0x3f, 0xbb, 0x21, 0x7d, 0xf8, 0xe7, 0xc4, 0x34, 0x57, 0x35, 0x08, + 0x89, 0xbf, 0x4a, 0xf8, 0xfd, 0x63, 0xdf, 0x7e, 0x8e, 0x7c, 0x9f, 0x06, 0xdb, 0x3f, + 0xff, 0xc1, 0x9c, 0xda, 0xa1, 0x6e, 0x8c, 0x2d, 0xd8, 0x53, 0x26, 0x59, 0xdf, 0x36, + 0xe2, 0xc8, 0xb3, 0xe5, 0x14, 0xe2, 0x76, 0x41, 0xf4, 0x4f, 0xa4, 0x63, 0x43, 0x36, + 0x9a, 0x98, 0xdd, 0x7b, 0x00, 0xda, 0xc1, 0x2e, 0x61, 0x04, 0x1f, 0xbd, 0x2b, 0xf9, + 0x71, 0xb4, 0x48, 0x9d, 0x55, 0x52, 0x28, 0x10, 0x0d, 0xf2, 0x42, 0x31, 0x10, 0x5c, + 0xac, 0xff, 0x1e, 0xa7, 0x4d, 0x16, 0xf4, 0x46, 0xe4, 0x55, 0xfe, 0x15, 0x65, 0xb4, + 0xff, 0x72, 0x8d, 0x64, 0x8e, 0xa8, 0xa7, 0xab, 0x4b, 0x73, 0x58, 0xa8, 0x5a, 0xdc, + 0x63, 0x3e, 0x12, 0x01, 0xf6, 0x93, 0x1c, 0x68, 0x43, 0xcd, 0x0d, 0x37, 0x27, 0x5c, + 0x06, 0x6b, 0x09, 0x95, 0x67, 0x2f, 0x86, 0x88, 0xe2, 0x7e, 0xaf, 0xe2, 0xaf, 0xf8, + 0xe8, 0x86, 0x23, 0x0d, 0x32, 0x6f, 0x89, 0x23, 0x85, 0x0d, 0xfa, 0x1f, 0x7b, 0xd3, + 0x1d, 0x40, 0x29, 0x31, 0x4e, 0x99, 0x22, 0xc4, 0x78, 0x25, 0x94, 0xfc, 0x39, 0x05, + 0x91, 0xf0, 0x17, 0x29, 0x10, 0x02, 0xdf, 0x24, 0x4a, 0x5d, 0xfc, 0xe7, 0xa9, 0x1f, + 0xa7, 0x2e, 0x1f, 0x7c, 0x9b, 0x60, 0xeb, 0x09, 0xb9, 0x6a, 0x73, 0x31, 0x66, 0x0d, + 0x5e, 0x25, 0xbd, 0xcd, 0x6f, 0xcc, 0x95, 0x6b, 0x93, 0xdc, 0xf0, 0x11, 0x65, 0x70, + 0x9b, 0xcd, 0x1e, 0x28, 0xa4, 0x09, 0xa6, 0xc4, 0x3d, 0x3f, 0x9b, 0x3d, 0x83, 0x68, + 0xb4, 0x7c, 0xd5, 0xf1, 0xcc, 0x70, 0xd3, 0xaa, 0x81, 0x60, 0xb5, 0xac, 0xeb, 0x3d, + 0x75, 0x13, 0x9d, 0xe8, 0x49, 0x18, 0xfb, 0xc6, 0x19, 0x45, 0xdf, 0xc1, 0x2d, 0x46, + 0xf5, 0x58, 0x3b, 0xe6, 0xea, 0x4f, 0xa0, 0x49, 0x4d, 0x4d, 0xdb, 0x46, 0x98, 0x11, + 0x72, 0x42, 0xcc, 0x90, 0x2b, 0xac, 0x87, 0xb7, 0xd9, 0x20, 0xce, 0x8d, 0x1d, 0xd3, + 0x43, 0x7d, 0x84, 0x5b, 0x91, 0x6b, 0x0a, 0x9c, 0xc8, 0xbd, 0xa6, 0x71, 0x15, 0x2f, + 0x0e, 0x1e, 0x82, 0xdd, 0x43, 0x62, 0x09, 0x67, 0xe2, 0x72, 0x67, 0xe7, 0x42, 0xd0, + 0x01, 0x58, 0xde, 0xd7, 0x36, 0xef, 0x3e, 0xa7, 0xaa, 0xf9, 0xf1, 0xb5, 0xea, 0x10, + 0x61, 0x9a, 0x86, 0xfe, 0xd2, 0xdd, 0x2d, 0x44, 0xc0, 0x56, 0xf2, 0xb1, 0x45, 0x9d, + 0x3c, 0x38, 0x76, 0x78, 0x12, 0x74, 0xeb, 0x78, 0x95, 0x6e, 0x5f, 0x57, 0x7a, 0x08, + 0x34, 0xb1, 0xfc, 0x50, 0x99, 0xd7, 0x85, 0x37, 0x71, 0xa4, 0xfb, 0x09, 0xa9, 0x52, + 0xc1, 0xe7, 0xff, 0x65, 0x13, 0x04, 0x4c, 0x27, 0x1f, 0x26, 0xe0, 0x61, 0xfd, 0x29, + 0xcc, 0xb3, 0xc1, 0xd1, 0xe8, 0x09, 0x98, 0x6a, 0x3d, 0xe5, 0x27, 0x96, 0x41, 0x9e, + 0x7d, 0xa5, 0xfd, 0x1c, 0x38, 0x46, 0xdb, 0x7a, 0x63, 0x17, 0xbe, 0x7e, 0xc3, 0x74, + 0x13, 0x0e, 0x63, 0x3b, 0xb4, 0x1e, 0x2f, 0x10, 0x7f, 0xdc, 0xfb, 0x68, 0xef, 0x68, + 0xc9, 0x11, 0xa2, 0x6c, 0x51, 0xe7, 0xe5, 0xad, 0x1a, 0x3f, 0x84, 0xc0, 0xb0, 0xbf, + 0xf7, 0x30, 0x48, 0xc9, 0x34, 0x5a, 0x4e, 0xcc, 0x83, 0x64, 0x3d, 0xc9, 0xa4, 0x2b, + 0x2e, 0x7b, 0xbb, 0xb9, 0x37, 0x43, 0x30, 0x31, 0xe3, 0xfc, 0x3d, 0x4c, 0x96, 0x31, + 0x06, 0x97, 0x91, 0xa1, 0xde, 0xd9, 0x06, 0x86, 0x73, 0x4a, 0x03, 0xe9, 0xbb, 0x07, + 0xa7, 0xfa, 0xab, 0x4c, 0xce, 0x85, 0x1c, 0xb5, 0x51, 0xc3, 0x6d, 0x8c, 0xb4, 0xf1, + 0x0d, 0xad, 0x7d, 0x72, 0x00, 0x97, 0xea, 0x52, 0x00, 0x75, 0x5b, 0xf1, 0x02, 0x6e, + 0x14, 0x56, 0xdd, 0x3c, 0x0e, 0x4a, 0xe2, 0x0d, 0x9b, 0xfd, 0x4b, 0xb4, 0xda, 0x58, + 0x82, 0x7f, 0x45, 0x3c, 0xfb, 0x61, 0x7c, 0x09, 0xca, 0x9a, 0xc1, 0xf7, 0x1e, 0xa6, + 0x13, 0xc3, 0x78, 0xc1, 0xb1, 0xbd, 0x4f, 0xb7, 0xa8, 0x33, 0x48, 0x60, 0x53, 0xca, + 0xf0, 0x15, 0x0f, 0xb1, 0xb2, 0x64, 0x47, 0x8f, 0xdf, 0x44, 0xf0, 0x3e, 0x02, 0x2a, + 0x3d, 0x9a, 0xee, 0xd5, 0x00, 0x7c, 0x8d, 0x8d, 0x92, 0xeb, 0xe0, 0xa7, 0x43, 0xfc, + 0xc4, 0x88, 0x5b, 0x02, 0xc0, 0xff, 0x34, 0xf4, 0x80, 0xb4, 0x99, 0x6b, 0x25, 0x39, + 0x77, 0x07, 0xe3, 0x64, 0xbf, 0x86, 0xe6, 0xf9, 0xef, 0x13, 0x49, 0x4c, 0x2c, 0xee, + 0x9e, 0x83, 0x7f, 0x75, 0x4f, 0xc9, 0x7a, 0x1e, 0x92, 0xc2, 0x45, 0x33, 0xcf, 0x76, + 0x12, 0xc4, 0xbb, 0xf4, 0x43, 0xf5, 0x4c, 0x7e, 0x35, 0x32, 0x1d, 0x06, 0xea, 0xac, + 0x0a, 0xfa, 0x4e, 0x91, 0x19, 0x36, 0x28, 0x63, 0x3b, 0xf0, 0xc5, 0xfe, 0xf4, 0x69, + 0x9d, 0x43, 0x71, 0x39, 0xa2, 0x2e, 0x61, 0xfa, 0x0b, 0xab, 0xd2, 0xc2, 0x5b, 0x7f, + 0xf9, 0x7f, 0x5f, 0x70, 0x97, 0xee, 0x3d, 0xe4, 0x94, 0xe4, 0xa3, 0xd2, 0x4d, 0xbc, + 0x99, 0xdb, 0x87, 0xda, 0x50, 0x43, 0xd4, 0x9a, 0x05, 0x9d, 0xdf, 0x65, 0x0f, 0xec, + 0x2c, 0xd8, 0x8a, 0x30, 0x0e, 0x1d, 0x52, 0x58, 0xa7, 0xeb, 0xbf, 0x49, 0xe2, 0xd8, + 0x4b, 0x24, 0xf5, 0xdb, 0x9c, 0xfc, 0x45, 0x3f, 0xf4, 0x45, 0x37, 0x7f, 0x79, 0x27, + 0xa3, 0xba, 0xbe, 0x3f, 0xfc, 0x42, 0x42, 0x1d, 0x02, 0x49, 0xca, 0xbc, 0xda, 0x44, + 0xd0, 0x16, 0x9f, 0x2f, 0xa7, 0x82, 0xe7, 0xa8, 0x8a, 0x3c, 0x07, 0xcc, 0x77, 0x40, + 0xfa, 0x8c, 0x81, 0x76, 0xd3, 0x6e, 0x65, 0x18, 0xa0, 0xd1, 0x1e, 0x4b, 0xfa, 0x8f, + 0xb9, 0xfa, 0x60, 0x22, 0x14, 0x30, 0xb5, 0x6e, 0x39, 0x29, 0x95, 0x27, 0x10, 0xc4, + 0xbd, 0x67, 0x5a, 0x92, 0x23, 0x3d, 0xc4, 0xd1, 0x23, 0xec, 0x73, 0xc9, 0x4a, 0xc3, + 0xb4, 0xbc, 0x81, 0x12, 0x0b, 0x4f, 0x52, 0x36, 0x36, 0x1c, 0x8c, 0xf4, 0x47, 0xac, + 0xb5, 0x07, 0x24, 0x3a, 0xf1, 0xdc, 0x9d, 0x58, 0x20, 0xc7, 0x86, 0x7d, 0xf7, 0x8f, + 0xfb, 0x53, 0x0e, 0x91, 0xf5, 0x8d, 0xfb, 0x51, 0x43, 0xc2, 0x04, 0x4f, 0xd6, 0x68, + 0xb9, 0x50, 0xf2, 0x80, 0xa0, 0x7a, 0xe8, 0x84, 0xfe, 0xfd, 0x2b, 0x30, 0x1d, 0x10, + 0xce, 0xbf, 0xf8, 0xa3, 0x1a, 0xc7, 0x32, 0x87, 0x4a, 0x5f, 0x8e, 0x84, 0xa9, 0x7d, + 0x46, 0xf6, 0x89, 0xed, 0x3e, 0x39, 0x67, 0xd3, 0x80, 0xce, 0xfd, 0x24, 0xd1, 0x8e, + 0x83, 0x25, 0x45, 0x12, 0xe5, 0xe4, 0xd9, 0xd5, 0xdd, 0x47, 0x32, 0x81, 0x98, 0xde, + 0x3f, 0x1b, 0xaf, 0x83, 0x4c, 0x98, 0x4a, 0x52, 0x1a, 0xd9, 0xa3, 0x8d, 0x67, 0x0d, + 0x1f, 0x1e, 0x44, 0x55, 0x80, 0x6f, 0x59, 0xfb, 0x51, 0x4b, 0xa4, 0x50, 0xca, 0x5c, + 0x99, 0x91, 0xae, 0x41, 0x8a, 0x8f, 0xa1, 0xc7, 0x3e, 0x1b, 0x40, 0xd1, 0xe6, 0x25, + 0x6a, 0x5d, 0x5e, 0xc7, 0x9b, 0xef, 0x07, 0x7d, 0xb1, 0xfe, 0x0f, 0xe0, 0x7b, 0x52, + 0x02, 0x2b, 0x7c, 0x37, 0xd4, 0x4f, 0xd9, 0xce, 0x68, 0x5b, 0xce, 0x24, 0x9f, 0x9a, + 0x28, 0x8f, 0xab, 0x14, 0xed, 0x56, 0x52, 0x8a, 0xac, 0x7d, 0x62, 0x99, 0xe2, 0x7a, + 0x80, 0x3b, 0x6d, 0x14, 0x6a, 0x1e, 0xdf, 0x92, 0xbf, 0x8a, 0x68, 0xe7, 0xff, 0x8b, + 0xec, 0x2f, 0xed, 0x38, 0x26, 0x94, 0x05, 0x5b, 0x1d, 0x5d, 0x49, 0x34, 0x2e, 0x2c, + 0x39, 0x26, 0xfe, 0x14, 0x32, 0x94, 0xa5, 0xdd, 0xe4, 0xdd, 0x28, 0x99, 0x88, 0x9a, + 0x74, 0xf4, 0x84, 0xb2, 0x20, 0x67, 0x1e, 0x36, 0x11, 0x64, 0xb9, 0x0d, 0x35, 0xdf, + 0xa3, 0xe8, 0xf0, 0xe3, 0xec, 0x46, 0x61, 0xe0, 0xb0, 0x3b, 0x14, 0x9f, 0x1e, 0xac, + 0x19, 0x6f, 0x95, 0x74, 0xca, 0xd2, 0x98, 0x04, 0x52, 0xc5, 0x6b, 0x9a, 0x92, 0x48, + 0xe6, 0x7b, 0x77, 0x30, 0xa7, 0xe1, 0xe1, 0x7a, 0x1f, 0xb6, 0x07, 0xbe, 0x37, 0x89, + 0x69, 0x69, 0x0a, 0x4f, 0xea, 0x1c, 0xe5, 0xaf, 0xe1, 0x20, 0x1e, 0x30, 0x28, 0xb1, + 0xca, 0x5f, 0xc9, 0xc8, 0x66, 0xb3, 0x85, 0x2a, 0xd4, 0x0a, 0x91, 0xac, 0x08, 0x31, + 0x1f, 0x62, 0x79, 0x65, 0xfd, 0xcd, 0xbe, 0x38, 0x48, 0x54, 0x37, 0xf1, 0x3a, 0xbf, + 0x39, 0x62, 0xab, 0xc7, 0xe2, 0x58, 0x93, 0x96, 0xef, 0xaf, 0x8b, 0xce, 0x9a, 0x4b, + 0xe1, 0xeb, 0x2f, 0x24, 0xb2, 0x9c, 0x2d, 0x89, 0x35, 0xd2, 0x10, 0xaa, 0xcd, 0x62, + 0x05, 0xe8, 0xd1, 0xb2, 0xca, 0x95, 0x81, 0xb1, 0xb4, 0xa3, 0x6f, 0x7c, 0x47, 0x4d, + 0x5b, 0x53, 0x37, 0xe2, 0x6a, 0x1c, 0xa2, 0x86, 0x83, 0x6d, 0x09, 0x62, 0x9e, 0x31, + 0xf0, 0x31, 0xdf, 0x02, 0x9d, 0xec, 0x40, 0xb9, 0xfe, 0x15, 0xd3, 0xb1, 0xfa, 0x3c, + 0x5b, 0x67, 0xa1, 0xac, 0x9a, 0x3d, 0xc8, 0x0c, 0xdd, 0xa6, 0x23, 0x48, 0x67, 0x6a, + 0xf8, 0xd3, 0x26, 0x90, 0x62, 0x19, 0xfa, 0xf5, 0x21, 0x66, 0x89, 0xae, 0x1d, 0xde, + 0x30, 0x4c, 0x8a, 0xe0, 0xce, 0x7c, 0x3f, 0xfc, 0x6b, 0xca, 0x36, 0x1d, 0xb3, 0x4b, + 0xf0, 0x27, 0x1b, 0xfd, 0x82, 0x6e, 0x2e, 0x3e, 0x12, 0x90, 0xa9, 0xba, 0x34, 0x55, + 0x83, 0xe3, 0x38, 0x30, 0x04, 0x7c, 0x59, 0x0f, 0x25, 0xdc, 0xec, 0xe6, 0xfd, 0x68, + 0xe3, 0xe4, 0xea, 0xf7, 0xa8, 0x6c, 0x88, 0xbb, 0xf6, 0x62, 0x67, 0xf8, 0x8b, 0x36, + 0x6f, 0xd6, 0xcd, 0x25, 0x8a, 0x8f, 0x74, 0x03, 0xf7, 0xb0, 0x53, 0x3e, 0x1f, 0x27, + 0xe7, 0x80, 0x6d, 0xc8, 0x55, 0x2b, 0xbb, 0x10, 0xae, 0xda, 0xc0, 0x31, 0xd1, 0x4f, + 0x15, 0xcc, 0xf4, 0x07, 0xb7, 0x57, 0x14, 0x1f, 0xfe, 0x77, 0x7e, 0xa5, 0x91, 0xe7, + 0x1d, 0xcd, 0xa8, 0xed, 0x96, 0x50, 0xeb, 0xca, 0xb4, 0x30, 0xc5, 0x5a, 0xd7, 0xcb, + 0x98, 0x25, 0x2a, 0x3b, 0x24, 0xca, 0xe3, 0x4b, 0xbf, 0x43, 0x50, 0x86, 0x19, 0x67, + 0xd2, 0xc9, 0x8e, 0x12, 0xb9, 0x6e, 0xd1, 0x0f, 0x27, 0x86, 0xb1, 0xa7, 0x6a, 0xd4, + 0x09, 0xf3, 0xd8, 0x77, 0x13, 0x7f, 0xd1, 0xf3, 0xca, 0x9e, 0x3f, 0x6f, 0xa5, 0xca, + 0x3d, 0xeb, 0xb1, 0x7e, 0x3d, 0xea, 0xcc, 0xf5, 0xb1, 0xe6, 0x10, 0x0f, 0xdf, 0xca, + 0xed, 0xe1, 0xce, 0x8d, 0xad, 0xd0, 0x94, 0x78, 0x20, 0x84, 0x75, 0x4b, 0x83, 0xe6, + 0x3f, 0xbd, 0x62, 0xe0, 0x3a, 0x4d, 0x18, 0x6c, 0xc6, 0xe9, 0xa2, 0x72, 0x34, 0xec, + 0x0e, 0x0c, 0xd0, 0x7c, 0x10, 0x4b, 0x34, 0x78, 0x6e, 0xa1, 0x4a, 0x76, 0x7e, 0x39, + 0x2b, 0x88, 0x38, 0x10, 0x3c, 0xe7, 0x59, 0x53, 0xc4, 0x5d, 0x37, 0x54, 0xe6, 0x67, + 0xe5, 0xdf, 0xd3, 0x79, 0x00, 0x0e, 0xd4, 0xa0, 0xe0, 0x26, 0x7b, 0x84, 0xc5, 0x21, + 0xb1, 0x0f, 0xbb, 0x2d, 0x56, 0x6a, 0x36, 0x65, 0xb1, 0x21, 0x60, 0x3b, 0x79, 0x1d, + 0x4c, 0x13, 0x4f, 0x25, 0x04, 0x5b, 0x59, 0x12, 0x03, 0x0c, 0x0c, 0x02, 0xba, 0x0b, + 0xa3, 0x87, 0x22, 0xbf, 0x42, 0x1e, 0xdb, 0x5f, 0x2c, 0x93, 0x58, 0x10, 0x8d, 0x28, + 0xce, 0x4d, 0x8c, 0xc9, 0x62, 0xfa, 0xe7, 0x8c, 0x1f, 0xed, 0xf9, 0x6d, 0xfb, 0x25, + 0x33, 0x47, 0xaf, 0x7c, 0x3b, 0xb5, 0xa1, 0xf9, 0x70, 0x93, 0xaf, 0x88, 0xca, 0x77, + 0xdb, 0x75, 0x31, 0xa2, 0x8a, 0x4e, 0x8b, 0x89, 0x18, 0xa3, 0x59, 0x44, 0x2c, 0x24, + 0x17, 0xfc, 0xda, 0x6d, 0xfa, 0xf5, 0x5f, 0x8d, 0xdd, 0x84, 0xe3, 0xcf, 0xa4, 0x2e, + 0x93, 0x6d, 0x0a, 0xdd, 0xba, 0xba, 0x0d, 0x96, 0xa7, 0xe1, 0x79, 0x05, 0xcd, 0x01, + 0xfd, 0x30, 0xb6, 0x9b, 0xd5, 0x3d, 0x5a, 0x44, 0x55, 0xcc, 0x73, 0xe8, 0xfa, 0x2e, + 0xef, 0xae, 0x24, 0xc6, 0x23, 0x8f, 0x1b, 0x9a, 0xc5, 0x07, 0xa0, 0x2b, 0x48, 0x7c, + 0x9e, 0x67, 0xbb, 0xce, 0x4d, 0x37, 0x2c, 0xeb, 0xb0, 0x72, 0x79, 0x71, 0xc9, 0x16, + 0xc4, 0x15, 0xa7, 0x08, 0x8b, 0xa9, 0x9b, 0xdf, 0xd8, 0xe2, 0x3c, 0xdb, 0xcd, 0xf6, + 0xf0, 0x25, 0x08, 0x28, 0xc6, 0xad, 0x91, 0x7f, 0x9a, 0xaa, 0xf8, 0x0a, 0x6e, 0x55, + 0x6d, 0xc3, 0x0b, 0xb1, 0x0d, 0x98, 0xc4, 0xd3, 0xad, 0x42, 0xbc, 0x0e, 0xa7, 0xaf, + 0x75, 0xca, 0x06, 0xaf, 0x73, 0x25, 0x02, 0xde, 0x73, 0x19, 0x19, 0xa1, 0x5f, 0x99, + 0x1e, 0x6e, 0x67, 0x31, 0x57, 0x23, 0x7e, 0x9d, 0xac, 0x3e, 0x9e, 0x57, 0x63, 0x3d, + 0xab, 0x6a, 0x68, 0xd5, 0xb1, 0x32, 0x0b, 0x2c, 0x7d, 0x5e, 0x1e, 0xe8, 0x87, 0xa5, + 0x56, 0x16, 0xde, 0xa1, 0xf0, 0xac, 0xb7, 0x15, 0xa3, 0x05, 0x74, 0xa8, 0xc2, 0x00, + 0xf2, 0x24, 0x37, 0x3c, 0x34, 0xab, 0x77, 0xa5, 0x64, 0xa8, 0xf1, 0x12, 0x4a, 0xb7, + 0x6c, 0x8a, 0x34, 0x5c, 0x62, 0x70, 0x0d, 0xe9, 0xf0, 0x32, 0xd4, 0x32, 0x9b, 0x95, + 0x3c, 0xd8, 0x3d, 0x8f, 0xb9, 0x63, 0x44, 0xf8, 0x8d, 0x9e, 0x0f, 0xdc, 0x0b, 0x46, + 0xbd, 0xdb, 0xde, 0xc7, 0xeb, 0xc8, 0x14, 0x11, 0x2a, 0x5e, 0x0e, 0x1f, 0x98, 0x02, + 0x85, 0xdf, 0xd3, 0xec, 0xb7, 0x19, 0x8a, 0x8e, 0x5f, 0x57, 0xee, 0x60, 0x8f, 0x04, + 0xe1, 0x9b, 0x58, 0x6a, 0x3a, 0xb5, 0xc2, 0x62, 0x17, 0x3d, 0x3e, 0xdf, 0x1c, 0x04, + 0x0b, 0x4e, 0x34, 0x97, 0xd9, 0x6b, 0xc1, 0xb5, 0xca, 0xdf, 0x39, 0xb4, 0x82, 0x1a, + 0x4b, 0x3e, 0xfa, 0x85, 0x72, 0x94, 0xf9, 0x87, 0x4b, 0x0c, 0x34, 0x13, 0xab, 0x65, + 0x4f, 0x9b, 0x41, 0x82, 0x8f, 0x21, 0x5f, 0xf8, 0x14, 0x39, 0x22, 0x08, 0xe4, 0x9e, + 0x7b, 0x5d, 0xdb, 0x7f, 0x21, 0xcf, 0xa3, 0x92, 0xc9, 0xe3, 0x6e, 0xcf, 0x5a, 0x39, + 0x1c, 0xf5, 0x40, 0x21, 0x1a, 0x0c, 0xe3, 0xcb, 0xb5, 0x0b, 0x98, 0x2d, 0x44, 0x43, + 0x24, 0x0d, 0x9e, 0xdb, 0x0f, 0x78, 0x84, 0x66, 0xe0, 0x1c, 0x09, 0x5c, 0x37, 0xf7, + 0x3c, 0x53, 0xad, 0x2b, 0x3a, 0xed, 0xed, 0x50, 0x52, 0xab, 0x62, 0x9e, 0xd9, 0x84, + 0xc6, 0x63, 0xa0, 0xe5, 0xbb, 0x0d, 0x6f, 0x8a, 0x39, 0x1e, 0x63, 0xe7, 0xfd, 0x67, + 0x68, 0xde, 0x2a, 0x7b, 0x3e, 0x71, 0xdb, 0xc1, 0xaf, 0x65, 0xda, 0x65, 0xc8, 0x0c, + 0x84, 0xe8, 0x52, 0x9f, 0x93, 0xb7, 0x58, 0x66, 0x91, 0xbf, 0xcd, 0x85, 0x77, 0x22, + 0x23, 0xca, 0x7e, 0x59, 0x80, 0x1a, 0xa9, 0xc4, 0xff, 0x50, 0x90, 0x5b, 0x1b, 0x8d, + 0x64, 0x43, 0xba, 0xce, 0x26, 0x53, 0x0a, 0xcb, 0x24, 0x84, 0x3b, 0x50, 0xba, 0xcf, + 0xa0, 0xc1, 0xbc, 0x27, 0xf1, 0x90, 0xb1, 0x35, 0x50, 0x9e, 0x72, 0xe7, 0xa5, 0x36, + 0x45, 0xc0, 0xfc, 0xa3, 0x3c, 0xb1, 0x0b, 0x50, 0x92, 0x69, 0x11, 0xc7, 0x93, 0xd1, + 0x85, 0x31, 0x50, 0xc7, 0x9a, 0x12, 0x7c, 0x3c, 0x5a, 0xdc, 0xd7, 0xc5, 0xb8, 0xf3, + 0x85, 0x1a, 0x32, 0x32, 0xe4, 0x2d, 0x0e, 0x05, 0x9e, 0x0a, 0x38, 0xb5, 0x48, 0x58, + 0x44, 0x44, 0xe2, 0x9b, 0xb6, 0x39, 0x5b, 0xfc, 0x16, 0xda, 0x17, 0x1b, 0x8e, 0xbf, + 0x83, 0x8a, 0x8f, 0x88, 0xd8, 0x46, 0xc6, 0x13, 0x56, 0x87, 0xae, 0x02, 0xf0, 0xd5, + 0xef, 0x3c, 0xbd, 0xd0, 0x00, 0xe6, 0xde, 0x91, 0xdd, 0x77, 0xde, 0xf7, 0xf9, 0xc8, + 0xd5, 0xff, 0x31, 0x29, 0xbc, 0x32, 0x38, 0x0a, 0xc5, 0xc2, 0x80, 0x71, 0xe2, 0xb0, + 0x86, 0x64, 0xcf, 0x84, 0x20, 0xf0, 0xfb, 0x98, 0x10, 0x02, 0xbe, 0x98, 0x91, 0x67, + 0xca, 0x02, 0xcf, 0x33, 0x07, 0xed, 0x20, 0x2b, 0x64, 0x7b, 0xc4, 0x97, 0x12, 0x1b, + 0xfc, 0xa4, 0x16, 0x78, 0x69, 0x07, 0xbf, 0xc4, 0xfc, 0x97, 0xa4, 0x6b, 0x3e, 0xa6, + 0xef, 0x74, 0x7a, 0xd5, 0x9f, 0x06, 0x56, 0x18, 0x2f, 0x0e, 0x2b, 0x71, 0x47, 0x43, + 0x04, 0x82, 0x7c, 0x2b, 0x3c, 0x15, 0x7c, 0x93, 0x35, 0x69, 0xe6, 0x86, 0x18, 0x24, + 0xcc, 0x60, 0x8e, 0x7f, 0xb7, 0xbf, 0x3a, 0x9b, 0x2e, 0xc0, 0xb2, 0xd6, 0x87, 0x09, + 0xb5, 0x70, 0xf0, 0x81, 0x9f, 0x60, 0x8c, 0x98, 0x46, 0x08, 0xb0, 0xa6, 0x1c, 0x4f, + 0xe9, 0xa2, 0x6e, 0xee, 0x41, 0xaf, 0xc1, 0x75, 0xfd, 0xb2, 0x6b, 0xf2, 0x75, 0xd8, + 0x94, 0xf6, 0x55, 0xa7, 0x09, 0xbf, 0x51, 0x28, 0xc2, 0x3a, 0x09, 0xdf, 0x44, 0x16, + 0x22, 0x06, 0xed, 0x95, 0xa1, 0x09, 0x9b, 0xa2, 0xe8, 0x05, 0xe0, 0x1c, 0xc0, 0xcc, + 0x00, 0x18, 0x9b, 0xb6, 0x88, 0xd6, 0xac, 0xc1, 0xc7, 0xb7, 0x87, 0xc4, 0xd6, 0xe7, + 0x76, 0x73, 0xf4, 0x57, 0xcb, 0xb2, 0x69, 0x13, 0xcb, 0x2a, 0x5a, 0xc9, 0x29, 0x35, + 0x6b, 0x98, 0xdf, 0x59, 0xea, 0x39, 0x82, 0x6a, 0x58, 0x57, 0x6b, 0x81, 0x60, 0xfe, + 0x9f, 0xc8, 0xe3, 0xf0, 0xa8, 0x6d, 0xac, 0xeb, 0xb8, 0x5a, 0xf0, 0x9a, 0xd0, 0x19, + 0x9a, 0x30, 0x82, 0x7c, 0xfa, 0xb0, 0x2f, 0xfb, 0xce, 0xcf, 0x7e, 0x67, 0x71, 0xdc, + 0x95, 0x24, 0x35, 0x8d, 0x0b, 0xec, 0xce, 0x48, 0x0b, 0xc8, 0xb2, 0xa5, 0xf8, 0x3a, + 0x44, 0xd5, 0x9e, 0x39, 0x2b, 0x0f, 0xe3, 0x5a, 0x5e, 0x77, 0x57, 0x95, 0x18, 0x4f, + 0x29, 0xad, 0x61, 0x22, 0x90, 0xa9, 0x9c, 0x23, 0x13, 0x95, 0xd2, 0xec, 0x9b, 0x50, + 0x7c, 0x0f, 0xdc, 0x8c, 0x11, 0xfc, 0xfd, 0xe8, 0x7a, 0x93, 0xd3, 0x54, 0x8d, 0x54, + 0xbf, 0x32, 0x4a, 0x28, 0xd5, 0x72, 0xdd, 0xbd, 0xca, 0xfd, 0x78, 0x15, 0xf9, 0x37, + 0x77, 0x95, 0x02, 0xe5, 0x8e, 0x05, 0x53, 0x0d, 0x72, 0x3a, 0xcb, 0x25, 0x4a, 0x16, + 0xbb, 0x03, 0xea, 0x7d, 0x72, 0x89, 0x5c, 0xf4, 0x87, 0x1b, 0xf8, 0xcb, 0x94, 0xdf, + 0x4a, 0xba, 0xc2, 0xe2, 0x82, 0xd8, 0xda, 0x72, 0xb7, 0x2e, 0x75, 0xf8, 0xd6, 0x4a, + 0x43, 0x68, 0xfe, 0xbf, 0xc5, 0x86, 0x21, 0xfd, 0xdf, 0x97, 0x0b, 0x83, 0xfb, 0xa8, + 0x16, 0x84, 0x5c, 0x3b, 0x18, 0x92, 0xde, 0xa7, 0xfa, 0xa1, 0xc2, 0xf9, 0xe9, 0xd4, + 0x13, 0xa1, 0xfa, 0x16, 0x4b, 0xd6, 0xeb, 0x2b, 0xa2, 0xd7, 0x3c, 0x73, 0x3e, 0xb7, + 0x5c, 0xfb, 0xfc, 0x5b, 0x6d, 0x26, 0x74, 0x77, 0xfb, 0xa6, 0x5b, 0x83, 0x1c, 0x55, + 0x09, 0x59, 0x46, 0x30, 0x87, 0x4d, 0x21, 0x1b, 0xa4, 0xec, 0xba, 0xd6, 0xed, 0x0d, + 0x62, 0x4b, 0x14, 0x4e, 0x71, 0x8d, 0xb4, 0x31, 0x87, 0x07, 0x37, 0x65, 0x19, 0x50, + 0x77, 0x84, 0xe7, 0x74, 0x5c, 0xfe, 0xb0, 0xb8, 0x93, 0xd4, 0x16, 0x32, 0x3a, 0xb3, + 0xfd, 0xf7, 0x65, 0xcd, 0x7d, 0xe1, 0xe3, 0x8c, 0xc8, 0x50, 0x4a, 0x1d, 0x53, 0xf9, + 0xf9, 0x18, 0x3e, 0x5e, 0x0b, 0x26, 0x76, 0xc2, 0xe2, 0x72, 0x42, 0x7d, 0xff, 0x61, + 0xc0, 0x22, 0x47, 0xf8, 0xf2, 0xe3, 0x2d, 0x14, 0xf7, 0xb3, 0xe2, 0xfb, 0xfb, 0xcc, + 0x72, 0xf1, 0x23, 0x02, 0x63, 0x84, 0xbd, 0x00, 0x87, 0xba, 0xcc, 0x2b, 0xe9, 0xc1, + 0x5f, 0x2c, 0xd0, 0x8e, 0x5f, 0x39, 0xcd, 0x3f, 0xe2, 0xc9, 0x72, 0x94, 0x81, 0xdf, + 0x74, 0x0a, 0x63, 0x85, 0xf9, 0x64, 0x0f, 0x74, 0x8c, 0xc3, 0x5e, 0x1a, 0xd8, 0xf5, + 0xd6, 0x0b, 0xb0, 0xc0, 0xbc, 0xd4, 0x22, 0xd3, 0x0b, 0x7b, 0x00, 0x18, 0x73, 0x37, + 0x44, 0x94, 0xb8, 0x63, 0x98, 0x69, 0x77, 0x48, 0xa4, 0x9a, 0x89, 0x56, 0x21, 0xd1, + 0x3a, 0x0e, 0x81, 0x9f, 0x2d, 0xef, 0xcf, 0x07, 0x98, 0x22, 0xc7, 0xa1, 0xba, 0xa5, + 0x14, 0xa8, 0xb3, 0x23, 0x04, 0x5e, 0xe3, 0x08, 0xba, 0xf2, 0x89, 0xcf, 0x54, 0x3b, + 0x6f, 0xe1, 0xa9, 0xc9, 0x17, 0xe5, 0xa8, 0xa4, 0x7f, 0x49, 0xb5, 0x8c, 0x41, 0xda, + 0xe4, 0x65, 0x52, 0xcd, 0x5f, 0x8e, 0xe8, 0x80, 0x40, 0xfc, 0x5f, 0xfd, 0x05, 0x90, + 0xd5, 0xee, 0x23, 0xb7, 0xaf, 0xf4, 0x1f, 0xfa, 0x3e, 0x44, 0xe8, 0x2f, 0x2c, 0xba, + 0xfb, 0x06, 0x00, 0xf0, 0x23, 0x77, 0x2f, 0xdf, 0x84, 0x6b, 0xa8, 0x4b, 0xea, 0x1f, + 0x00, 0x19, 0x01, 0x8b, 0x48, 0x21, 0x64, 0x02, 0x60, 0x0e, 0x58, 0xfc, 0x8f, 0xe0, + 0xa9, 0x84, 0x02, 0x4b, 0x7f, 0x9d, 0xc9, 0x52, 0x43, 0x2a, 0x59, 0x6f, 0xcc, 0x1a, + 0x73, 0xce, 0xaf, 0x0a, 0xda, 0xaf, 0xac, 0x78, 0xde, 0xca, 0x60, 0xe1, 0xce, 0xe8, + 0xb5, 0xba, 0xde, 0x78, 0x9a, 0xc9, 0xd5, 0x08, 0x10, 0xf9, 0x1f, 0xb0, 0x45, 0xb6, + 0x71, 0x9f, 0x45, 0xff, 0xfc, 0x8d, 0x91, 0x42, 0x76, 0x23, 0xd6, 0x04, 0x89, 0xde, + 0xd9, 0x9b, 0xbb, 0x47, 0xd5, 0xcb, 0x4a, 0x29, 0xfa, 0x03, 0xd0, 0x5b, 0x6f, 0xf1, + 0x7b, 0xf3, 0x80, 0x59, 0x7e, 0x18, 0xe5, 0xe0, 0xf3, 0x7d, 0x47, 0x80, 0x57, 0x5e, + 0x58, 0xf8, 0x79, 0xf3, 0x23, 0x7a, 0xb5, 0x61, 0x5d, 0x35, 0xfe, 0x35, 0xa9, 0x3c, + 0x4e, 0x38, 0x70, 0x98, 0x31, 0x72, 0x7d, 0xfc, 0x45, 0x6f, 0x77, 0xe3, 0x27, 0x75, + 0x38, 0x3c, 0xd5, 0xb4, 0xcc, 0xb5, 0x1e, 0x7c, 0x44, 0xba, 0xd4, 0xee, 0xf5, 0x97, + 0xa4, 0xd3, 0x92, 0x15, 0xdd, 0xd9, 0x52, 0xc7, 0xf5, 0x18, 0x15, 0x33, 0x59, 0xa1, + 0xdc, 0x4b, 0x77, 0x97, 0x06, 0x6f, 0x84, 0x91, 0x7b, 0xb8, 0x7a, 0x68, 0xfd, 0xe9, + 0x7d, 0x09, 0x6e, 0x00, 0x89, 0x89, 0xaf, 0x76, 0xcc, 0xa3, 0xd8, 0x98, 0x3c, 0x5b, + 0x1b, 0xce, 0x32, 0xd1, 0x5e, 0x1c, 0xdc, 0xc6, 0x70, 0xd5, 0x93, 0x87, 0x15, 0x53, + 0x4d, 0xe5, 0xfb, 0xaf, 0x0c, 0x5a, 0x6c, 0x9c, 0x99, 0x8a, 0x9d, 0x8f, 0xca, 0x07, + 0xe4, 0xb5, 0xbb, 0xc1, 0x69, 0x86, 0x29, 0x1d, 0xc5, 0x54, 0x4a, 0x17, 0x8f, 0x03, + 0x01, 0x56, 0xfe, 0x70, 0x87, 0xb2, 0xa9, 0xf8, 0x36, 0x89, 0xa1, 0x1c, 0x99, 0x6a, + 0xd0, 0xe8, 0xee, 0x38, 0xd8, 0xb9, 0xbd, 0x4e, 0x0d, 0xfc, 0xd3, 0x4c, 0xd5, 0x15, + 0x61, 0x70, 0x4c, 0xa2, 0x07, 0x68, 0x8d, 0x04, 0x1d, 0x6e, 0x7f, 0xb0, 0x99, 0xbf, + 0xee, 0x63, 0xd6, 0x85, 0xd4, 0x6b, 0x44, 0x01, 0x61, 0x20, 0x91, 0x0b, 0xfb, 0x31, + 0x06, 0x62, 0x52, 0x1f, 0x23, 0x1b, 0x38, 0x2a, 0xeb, 0x2f, 0x06, 0xd7, 0xd7, 0x44, + 0x75, 0x72, 0x15, 0xf2, 0xef, 0xb0, 0x15, 0x02, 0xec, 0x71, 0xed, 0x73, 0x4a, 0x3f, + 0x85, 0x4a, 0xd6, 0x48, 0xd9, 0x00, 0x45, 0x84, 0x64, 0x0f, 0xfa, 0xa1, 0xd3, 0x3e, + 0xa4, 0x2b, 0x6e, 0x2c, 0x21, 0xc9, 0x9f, 0x1d, 0x80, 0xb4, 0xd6, 0x99, 0x13, 0x64, + 0x59, 0x5c, 0x50, 0x25, 0xdf, 0xee, 0x6a, 0x65, 0x3d, 0x86, 0x80, 0x70, 0x93, 0x9f, + 0xb2, 0x71, 0x08, 0x14, 0x25, 0x7f, 0x83, 0xfa, 0xcf, 0xc8, 0x86, 0xa1, 0xe6, 0x8d, + 0xfa, 0x77, 0x5c, 0x9e, 0xe8, 0x7c, 0x80, 0x9e, 0x2b, 0xc8, 0x5c, 0xb8, 0x91, 0x57, + 0x50, 0x15, 0xc5, 0x58, 0x1b, 0x60, 0xd5, 0x96, 0x2b, 0xfb, 0x31, 0x2e, 0x25, 0x49, + 0x00, 0xa4, 0x28, 0x54, 0x66, 0xc7, 0xe0, 0xda, 0xd3, 0x5f, 0x67, 0x0d, 0x55, 0x16, + 0xad, 0x8d, 0x76, 0xce, 0xc1, 0xd3, 0x40, 0x56, 0x32, 0x9d, 0x3d, 0x87, 0xb9, 0x25, + 0x59, 0x96, 0x2c, 0x1f, 0x8b, 0xff, 0xcc, 0x27, 0x72, 0x32, 0x7b, 0xa3, 0x4e, 0x7e, + 0x99, 0x7c, 0x03, 0xa2, 0x25, 0xa7, 0xd6, 0xca, 0x41, 0x11, 0x36, 0xac, 0x11, 0x92, + 0x56, 0x41, 0x4c, 0xe0, 0xab, 0x5d, 0xe1, 0x60, 0x4c, 0x2b, 0xdc, 0x1d, 0xb4, 0x27, + 0xc0, 0x70, 0x2e, 0x3f, 0x44, 0x4f, 0x15, 0x23, 0xd0, 0x5d, 0x60, 0xb8, 0xf9, 0x11, + 0xa7, 0xff, 0x4e, 0x99, 0xbe, 0xd5, 0x3b, 0x7e, 0xbd, 0x18, 0x3c, 0xb3, 0xd2, 0x6b, + 0xbd, 0x1e, 0x61, 0x49, 0xd2, 0xa7, 0x90, 0xe8, 0xa3, 0x3c, 0xc7, 0x61, 0x9d, 0xce, + 0x27, 0x46, 0x39, 0x49, 0xa3, 0xa6, 0x1b, 0x63, 0xb7, 0xaa, 0xa0, 0x21, 0x77, 0x48, + 0x25, 0x72, 0x48, 0x3c, 0xb9, 0x77, 0x80, 0xb5, 0xf9, 0x7d, 0x6b, 0x87, 0x48, 0xa9, + 0xb6, 0x79, 0x7d, 0xa2, 0xe9, 0x3e, 0xe3, 0x7b, 0xd2, 0xb4, 0x94, 0x4c, 0xa7, 0x71, + 0x3b, 0x7d, 0xf2, 0x4c, 0xb7, 0x9d, 0xa7, 0x55, 0xd8, 0x91, 0xe3, 0x2d, 0xd9, 0x1d, + 0x02, 0xcc, 0xec, 0x86, 0x46, 0x1e, 0x3a, 0xad, 0x60, 0x0c, 0x71, 0x50, 0x7b, 0xa2, + 0xc7, 0x11, 0x1c, 0x3d, 0x3f, 0x51, 0x23, 0x6d, 0x0f, 0x54, 0xe3, 0x80, 0xb5, 0xd7, + 0x36, 0xdd, 0x16, 0xbb, 0xcc, 0x52, 0x6f, 0xb8, 0xd2, 0xcc, 0x29, 0x1c, 0x0c, 0x99, + 0x0c, 0x9a, 0xce, 0x69, 0xb5, 0xa2, 0xe4, 0x0b, 0x91, 0xc3, 0x4e, 0xa5, 0x3a, 0x0b, + 0xa4, 0xdf, 0xf0, 0xc4, 0xec, 0x79, 0xab, 0x24, 0x04, 0xb5, 0x82, 0x04, 0x09, 0x86, + 0x20, 0xf8, 0x79, 0x55, 0x58, 0x06, 0xc6, 0xa1, 0xcc, 0x74, 0x91, 0xe3, 0xe7, 0x14, + 0x29, 0x3f, 0x1d, 0xdb, 0x75, 0x88, 0xad, 0x3e, 0xfc, 0x4d, 0x08, 0xfc, 0x8f, 0xe1, + 0x0d, 0x41, 0x8b, 0x36, 0x89, 0x5d, 0x14, 0x8c, 0x2f, 0x7b, 0xe7, 0xc5, 0x68, 0x61, + 0x3d, 0x5b, 0x49, 0x0a, 0x3c, 0x0c, 0x9e, 0xeb, 0x5a, 0xe0, 0xd6, 0xd0, 0x4b, 0x98, + 0xdf, 0x5d, 0xf0, 0x97, 0x04, 0xe4, 0x2e, 0xe5, 0x19, 0x37, 0xe0, 0x0d, 0xd7, 0xfc, + 0x69, 0xda, 0xa1, 0x0a, 0x03, 0x29, 0xf5, 0x96, 0x97, 0x44, 0xbe, 0xcf, 0xe1, 0x25, + 0xd8, 0xd5, 0x2b, 0x50, 0x94, 0x9e, 0x92, 0x9f, 0x67, 0x67, 0x66, 0xe0, 0x12, 0x7b, + 0x6f, 0xdb, 0x19, 0x3b, 0xa6, 0x70, 0x08, 0x1e, 0x66, 0xd1, 0x03, 0x18, 0x21, 0xee, + 0x61, 0x32, 0x69, 0xaa, 0xb3, 0x0a, 0xb3, 0xbd, 0x6c, 0xa0, 0xb9, 0x70, 0x94, 0x38, + 0x41, 0xc1, 0x5a, 0x07, 0x4e, 0xaa, 0xbb, 0xba, 0x60, 0x27, 0x87, 0x4f, 0x1a, 0x78, + 0x3e, 0x41, 0x37, 0x93, 0x01, 0xb3, 0x9b, 0xe3, 0x5a, 0xea, 0x65, 0x47, 0xaf, 0x3d, + 0x93, 0x5b, 0x3b, 0x42, 0x89, 0x5f, 0x0c, 0x54, 0x92, 0xf7, 0x01, 0x34, 0x66, 0x3c, + 0xc9, 0x68, 0x1e, 0xd5, 0x23, 0x33, 0x21, 0x96, 0x53, 0xe8, 0x72, 0xf3, 0xd9, 0xa6, + 0x01, 0x9d, 0xa2, 0x2f, 0x7c, 0x61, 0xe7, 0x50, 0x67, 0xbc, 0xc8, 0x93, 0x70, 0x1b, + 0xd2, 0x68, 0x48, 0xc0, 0xe7, 0x5c, 0xbd, 0x94, 0xd1, 0x6a, 0x1e, 0xf0, 0xf1, 0x93, + 0x64, 0x0d, 0xdf, 0x08, 0x0d, 0x2a, 0x42, 0xcc, 0xba, 0x71, 0x3f, 0xaf, 0x7c, 0xb6, + 0xc5, 0x3d, 0xdc, 0xf9, 0x0e, 0xbe, 0xa9, 0x25, 0x3b, 0xee, 0x57, 0xb8, 0x70, 0x85, + 0x6b, 0xcc, 0x28, 0x20, 0x6c, 0x3b, 0xde, 0x7a, 0x58, 0x1b, 0x40, 0xf3, 0x4b, 0x95, + 0xa5, 0x54, 0xbd, 0x71, 0x33, 0xcb, 0x08, 0x07, 0xcf, 0x44, 0x0c, 0x68, 0x3e, 0x4c, + 0x76, 0x89, 0x5f, 0x6a, 0x52, 0x24, 0x48, 0x8f, 0xae, 0x9f, 0x6b, 0xa0, 0xe1, 0xca, + 0x66, 0x30, 0xc1, 0xeb, 0x37, 0xbc, 0x51, 0xaf, 0xea, 0x2a, 0xbf, 0xf7, 0x09, 0x8e, + 0x15, 0x25, 0xcd, 0x20, 0x8b, 0xf4, 0xd2, 0x36, 0x57, 0x5d, 0xe2, 0x29, 0x93, 0x62, + 0xa5, 0xc2, 0xeb, 0x08, 0xae, 0x1a, 0xc8, 0xc0, 0x7c, 0x6b, 0xa0, 0x2c, 0x17, 0x4a, + 0x1b, 0x11, 0x42, 0xbd, 0x53, 0x62, 0x52, 0x78, 0xdc, 0x16, 0xb9, 0xe6, 0x95, 0xb6, + 0x4b, 0x0b, 0x0f, 0x85, 0x0a, 0x8d, 0x6b, 0x31, 0x5c, 0x5a, 0x61, 0x32, 0x07, 0xb1, + 0x82, 0x6b, 0x7b, 0x5c, 0x93, 0xcf, 0xb8, 0x89, 0xe1, 0x58, 0x33, 0xed, 0xf8, 0xcc, + 0x29, 0x9a, 0x44, 0x84, 0xf1, 0x49, 0x71, 0xc8, 0xb7, 0x53, 0x01, 0xba, 0xf1, 0xa1, + 0xce, 0x7a, 0xbd, 0xd9, 0x4d, 0x02, 0x65, 0x5b, 0xce, 0x1f, 0x7e, 0x3a, 0xcb, 0x56, + 0x1f, 0x29, 0xcd, 0xd7, 0x2a, 0xdc, 0x39, 0xb6, 0x63, 0x16, 0xb0, 0x66, 0xf9, 0x93, + 0xcf, 0xd0, 0x9b, 0x2d, 0x43, 0x83, 0xa9, 0x0f, 0x60, 0x78, 0x23, 0xe4, 0xf4, 0x63, + 0xb2, 0x00, 0xfe, 0xf8, 0x41, 0x99, 0xf1, 0x45, 0x76, 0x4b, 0x2b, 0xf0, 0x36, 0x64, + 0x0e, 0x58, 0x39, 0xd8, 0x3d, 0xc5, 0xff, 0xd5, 0xe0, 0x31, 0xab, 0x06, 0x06, 0xcd, + 0x21, 0x22, 0x1b, 0x8b, 0xf6, 0x60, 0x35, 0x3b, 0x58, 0x22, 0x5a, 0xf1, 0x91, 0x5b, + 0xb0, 0x66, 0x70, 0x25, 0x94, 0x67, 0x81, 0x9c, 0x65, 0x8c, 0xe8, 0x7d, 0x24, 0xce, + 0xba, 0x79, 0xee, 0x24, 0xa3, 0xb4, 0xfb, 0x49, 0xb1, 0xdf, 0x86, 0x02, 0xab, 0x00, + 0x61, 0xd7, 0x01, 0x3f, 0x19, 0xd6, 0x86, 0x4f, 0xc0, 0x84, 0x12, 0xa0, 0x5b, 0x86, + 0x52, 0xca, 0x11, 0x5d, 0xcf, 0xdd, 0x83, 0xc1, 0x51, 0xd3, 0x8b, 0x21, 0xac, 0x3e, + 0xc9, 0x9d, 0x11, 0xb4, 0xd4, 0x51, 0x4a, 0x5e, 0x5a, 0x36, 0x09, 0xfc, 0xa6, 0x5c, + 0x09, 0x39, 0x7a, 0x45, 0xe5, 0x09, 0x0e, 0x23, 0x3d, 0x37, 0x94, 0x0b, 0x62, 0xb7, + 0x5d, 0xdf, 0xd8, 0x74, 0x13, 0xd6, 0x3c, 0xb3, 0xb4, 0xc2, 0x6e, 0xfc, 0x4d, 0x24, + 0x43, 0xa5, 0xa5, 0x81, 0xa3, 0xfa, 0xc9, 0xa9, 0xf4, 0xb6, 0x28, 0x13, 0x58, 0xe0, + 0x6a, 0x0a, 0xb8, 0xee, 0x53, 0xca, 0xb0, 0x8e, 0xa9, 0x87, 0xc3, 0x1c, 0xbc, 0x94, + 0xb2, 0x5a, 0x4e, 0xa7, 0x54, 0x99, 0x17, 0x4b, 0xff, 0xab, 0x81, 0x4b, 0xdf, 0xc6, + 0xc3, 0x97, 0x3f, 0x7d, 0x60, 0xb8, 0xeb, 0xc1, 0xa7, 0xc7, 0xb2, 0x35, 0x29, 0x8f, + 0x96, 0x49, 0x7e, 0xc7, 0x00, 0xc7, 0x81, 0x3f, 0xa7, 0xc5, 0xa4, 0xd9, 0x72, 0x50, + 0x4e, 0x35, 0xb0, 0xa8, 0x91, 0xa8, 0x31, 0xbc, 0x1c, 0xb9, 0x14, 0x55, 0x48, 0xe0, + 0xa3, 0x3a, 0x7a, 0x33, 0xfe, 0x39, 0xf0, 0x06, 0xb8, 0x87, 0xca, 0xfb, 0x31, 0x6e, + 0x06, 0x28, 0xe4, 0x06, 0x58, 0x39, 0x31, 0x81, 0x03, 0x54, 0x7e, 0xbd, 0x3d, 0x71, + 0x9b, 0xde, 0xbb, 0x7d, 0x4d, 0x0b, 0x63, 0x5b, 0x20, 0x36, 0x33, 0xe6, 0x8c, 0xfa, + 0x62, 0x33, 0x0a, 0xb1, 0xba, 0xb4, 0x60, 0x59, 0xd4, 0x77, 0x0f, 0x0c, 0xa2, 0x45, + 0xa6, 0x40, 0x48, 0x4d, 0x33, 0x26, 0x54, 0xf3, 0x25, 0xf2, 0x8c, 0xff, 0xc4, 0x3c, + 0xa9, 0x52, 0x3b, 0xe4, 0x15, 0x37, 0xf7, 0x01, 0x7c, 0xfb, 0xa8, 0xee, 0xa9, 0xda, + 0xe4, 0xe5, 0x79, 0xab, 0x8f, 0xa6, 0x88, 0x08, 0x16, 0xc8, 0x21, 0xff, 0x4d, 0x74, + 0x02, 0x5d, 0x09, 0xab, 0xc6, 0xf4, 0x62, 0x27, 0xde, 0x94, 0x20, 0xc7, 0x20, 0xe3, + 0xb4, 0x2e, 0xb3, 0xab, 0x77, 0x1d, 0xec, 0x4b, 0x3b, 0x91, 0x8d, 0xb8, 0x34, 0x18, + 0x32, 0x86, 0x60, 0x84, 0x77, 0x13, 0xa8, 0xeb, 0x04, 0xf7, 0xe3, 0xd3, 0xfd, 0x5a, + 0x96, 0x92, 0x5d, 0x8d, 0xb9, 0x72, 0x61, 0xc2, 0xa3, 0x6f, 0x95, 0xd6, 0x79, 0x4b, + 0x24, 0x26, 0x46, 0xee, 0x55, 0x4c, 0xc4, 0x30, 0xe9, 0xcf, 0xe4, 0x2c, 0x5e, 0x71, + 0x8c, 0x85, 0xa9, 0x6d, 0xf5, 0x51, 0x88, 0xeb, 0xc1, 0x0c, 0xff, 0x07, 0xbe, 0x17, + 0x61, 0xa6, 0x41, 0x8a, 0xdc, 0x8d, 0xba, 0x9d, 0xcd, 0x2f, 0xa4, 0x05, 0x0e, 0x7e, + 0x33, 0x3b, 0xb3, 0xd9, 0xe1, 0xa8, 0xd0, 0x02, 0xba, 0x7c, 0xbb, 0x5c, 0xfd, 0x0a, + 0xb2, 0xf6, 0x19, 0xcf, 0x49, 0x99, 0xae, 0x07, 0x51, 0x16, 0xd4, 0x52, 0x5d, 0xf0, + 0x29, 0x30, 0x27, 0xc3, 0x89, 0xdf, 0x0f, 0x22, 0x1f, 0xf3, 0x9a, 0x2c, 0x7c, 0x04, + 0x82, 0xa5, 0x89, 0xbd, 0xa7, 0x77, 0xb9, 0xe1, 0x0e, 0x55, 0x7c, 0x41, 0xec, 0x8a, + 0x7b, 0x46, 0x1f, 0xdd, 0x7c, 0x57, 0x00, 0x69, 0xe1, 0x0d, 0xfa, 0xb5, 0xb7, 0x11, + 0x1f, 0x9a, 0x3a, 0xe1, 0xde, 0x07, 0xf9, 0xa5, 0x1e, 0xf8, 0xd4, 0x9f, 0xb7, 0xc6, + 0x9f, 0x8f, 0xc8, 0x7d, 0xbc, 0x0b, 0xbc, 0xf7, 0xd4, 0x3b, 0xa0, 0x0a, 0xf5, 0x6b, + 0x5a, 0xe3, 0x3c, 0xbc, 0x04, 0x10, 0xda, 0x84, 0xec, 0x9a, 0x84, 0x6a, 0x9d, 0x3f, + 0x91, 0x8f, 0x01, 0xdc, 0x38, 0x05, 0xc3, 0x39, 0x64, 0x45, 0x51, 0xde, 0x1b, 0xad, + 0x2a, 0x40, 0xe3, 0x2f, 0xc5, 0x8a, 0x61, 0x77, 0x43, 0x4c, 0x4c, 0x00, 0x40, 0x62, + 0xdf, 0x8f, 0x7c, 0x09, 0x59, 0xaf, 0xf4, 0x5e, 0x7f, 0x88, 0x9b, 0xa2, 0xed, 0xa9, + 0xf3, 0xa6, 0xf0, 0x0c, 0xb0, 0x86, 0x96, 0xd9, 0xca, 0xbc, 0xd5, 0x1a, 0x23, 0xae, + 0xd1, 0xef, 0xa5, 0xe8, 0x9c, 0xe4, 0xf7, 0x44, 0x37, 0x68, 0xe7, 0x83, 0x9c, 0xee, + 0x06, 0x22, 0x24, 0x29, 0x02, 0x4b, 0x4d, 0x96, 0xbb, 0x93, 0x1d, 0x67, 0x0b, 0x4c, + 0x7e, 0x0d, 0xfb, 0xfc, 0xb9, 0x36, 0x2c, 0x3b, 0x3c, 0x08, 0xd3, 0x99, 0xd7, 0x61, + 0x54, 0xa5, 0x7d, 0x82, 0x18, 0xb7, 0x33, 0xb1, 0x32, 0x18, 0x05, 0xfe, 0x07, 0x6a, + 0x4b, 0xa7, 0x93, 0x73, 0x91, 0x0e, 0x7f, 0xf9, 0x0b, 0x34, 0x70, 0x34, 0x3a, 0xec, + 0xd3, 0x0e, 0xca, 0xef, 0xec, 0xa3, 0x39, 0x7d, 0xe8, 0xb1, 0x07, 0x15, 0x57, 0x79, + 0x7d, 0x60, 0x8e, 0xd9, 0x5a, 0x94, 0x8c, 0x98, 0xa1, 0x60, 0x89, 0xcf, 0xc3, 0x43, + 0x6d, 0x35, 0x32, 0x73, 0xd9, 0x77, 0x20, 0x9e, 0x50, 0xd1, 0x7b, 0x06, 0x48, 0x6c, + 0x7f, 0xaf, 0x92, 0xcd, 0x94, 0x0f, 0xf9, 0xb4, 0x9e, 0x9e, 0x2a, 0xe4, 0x6d, 0x0b, + 0x0a, 0x85, 0xab, 0x31, 0x79, 0x58, 0x0e, 0xcd, 0x40, 0xba, 0xcf, 0xad, 0x44, 0x88, + 0x17, 0x82, 0x2a, 0x74, 0xb5, 0xd5, 0x67, 0x4b, 0x21, 0xae, 0x8d, 0x99, 0xc9, 0x0e, + 0x13, 0xcc, 0x26, 0x95, 0x4d, 0x8c, 0xa6, 0x8a, 0xb9, 0x57, 0xe5, 0x05, 0x30, 0xfc, + 0x15, 0x74, 0x21, 0x71, 0x1f, 0x05, 0x37, 0x0f, 0x71, 0xca, 0xa4, 0xb3, 0xe0, 0x1c, + 0x92, 0x38, 0xeb, 0x99, 0xec, 0xb7, 0x83, 0x5b, 0x21, 0xc2, 0xd9, 0x23, 0xda, 0x84, + 0xa3, 0x07, 0x40, 0x3b, 0x22, 0xb2, 0xcd, 0x0b, 0x9d, 0x9f, 0xdd, 0xe1, 0xfe, 0x5b, + 0x3c, 0xca, 0x46, 0xb9, 0x6c, 0x8a, 0x76, 0x12, 0xc2, 0x80, 0x7a, 0x5d, 0x03, 0xb2, + 0x33, 0x21, 0xee, 0xf4, 0x79, 0x4a, 0xa7, 0xeb, 0xaf, 0xff, 0x26, 0x04, 0xa6, 0xe9, + 0x20, 0xb4, 0x31, 0x9a, 0xa4, 0x50, 0x12, 0xd3, 0x48, 0xa6, 0x1a, 0xf2, 0x47, 0xd5, + 0xd4, 0x10, 0x8d, 0x36, 0x47, 0x20, 0x9f, 0xe0, 0x7a, 0xa8, 0x37, 0x3c, 0x77, 0x62, + 0xee, 0x12, 0xc7, 0x73, 0x45, 0x27, 0xbc, 0xc8, 0x7e, 0x9f, 0xac, 0x91, 0x1e, 0x04, + 0x3f, 0x12, 0x74, 0x0c, 0x63, 0x27, 0xae, 0xc7, 0xd5, 0xeb, 0x51, 0xd0, 0x40, 0xda, + 0x3f, 0xd6, 0x89, 0x92, 0x3d, 0x4c, 0xca, 0xeb, 0x00, 0x87, 0x9f, 0x4a, 0x8c, 0xf7, + 0x99, 0x1a, 0x0a, 0xe6, 0x30, 0xfc, 0x90, 0xb5, 0x36, 0x5c, 0x9d, 0x2c, 0x89, 0x7e, + 0xde, 0x51, 0x8c, 0x23, 0x49, 0x32, 0xd0, 0x47, 0x81, 0x0d, 0x4b, 0xda, 0x35, 0xe0, + 0xbd, 0x88, 0x05, 0x62, 0xd0, 0xc6, 0xa7, 0xd7, 0x0e, 0xa0, 0x6d, 0x90, 0x2e, 0xdb, + 0xa4, 0x6e, 0xe9, 0x01, 0x54, 0xa5, 0x95, 0x7b, 0x17, 0x86, 0xe8, 0x28, 0x85, 0x61, + 0x28, 0x4b, 0x2a, 0x1b, 0x25, 0x4c, 0x21, 0x05, 0xb5, 0x77, 0xce, 0x29, 0x68, 0x69, + 0xa2, 0x47, 0x1d, 0x5a, 0xf6, 0x14, 0xc2, 0x31, 0x41, 0x44, 0xa2, 0xc1, 0x16, 0x76, + 0xfb, 0xc0, 0x12, 0xab, 0xd1, 0xb6, 0x9e, 0x3f, 0x21, 0x8a, 0x72, 0x42, 0x16, 0xd6, + 0x86, 0x88, 0x0c, 0x66, 0x84, 0xd4, 0x12, 0xf4, 0x2c, 0x7a, 0x89, 0x3c, 0x95, 0x28, + 0xee, 0x1c, 0xa1, 0x2d, 0xcf, 0x63, 0x15, 0xfb, 0xc5, 0x0f, 0xf0, 0xe3, 0x10, 0x7d, + 0x17, 0x60, 0x38, 0x60, 0x6b, 0x78, 0x6a, 0x1b, 0xbe, 0xee, 0x45, 0xe9, 0x1e, 0x62, + 0x45, 0x5e, 0xdf, 0xbd, 0x99, 0x64, 0xc8, 0xd3, 0xcf, 0xdd, 0xbb, 0xf8, 0xbd, 0xbf, + 0x97, 0x21, 0x4a, 0x1e, 0xd8, 0x3d, 0xa9, 0xdd, 0xdb, 0x5e, 0xac, 0xb1, 0xf3, 0x41, + 0x0f, 0x03, 0x30, 0xe1, 0x88, 0xc8, 0xd9, 0x56, 0xed, 0x5f, 0x18, 0x1e, 0xf2, 0x50, + 0xc6, 0x34, 0x3a, 0x70, 0xc3, 0xea, 0xda, 0x2b, 0x1b, 0xcc, 0x9b, 0x6e, 0x00, 0x94, + 0x63, 0x47, 0xd1, 0x2d, 0x30, 0xff, 0x55, 0xce, 0x17, 0xbe, 0x20, 0x67, 0x2c, 0x8f, + 0x48, 0x9b, 0x15, 0xef, 0x67, 0xa2, 0x30, 0xdd, 0x0f, 0xc0, 0x16, 0xe9, 0xc7, 0x17, + 0x87, 0x2e, 0xcb, 0xc8, 0x85, 0x20, 0x1a, 0x92, 0x64, 0x8e, 0x22, 0x9c, 0xa5, 0xe9, + 0x0b, 0x7e, 0xe9, 0xb0, 0x36, 0xef, 0xb6, 0xe2, 0x1a, 0x74, 0x11, 0x64, 0x8b, 0xae, + 0xb9, 0xe9, 0x54, 0x88, 0xd3, 0xbf, 0x4c, 0x96, 0x84, 0x4c, 0x6a, 0x9a, 0x1f, 0x09, + 0x43, 0x6d, 0x2b, 0x13, 0x28, 0x47, 0x41, 0x34, 0xbf, 0x45, 0xd0, 0x6c, 0x99, 0x51, + 0x4d, 0xb3, 0x4c, 0x58, 0x95, 0xe8, 0x5d, 0x5b, 0x59, 0x08, 0xca, 0x41, 0x12, 0xf0, + 0x9d, 0xc5, 0x9d, 0xb0, 0x65, 0xfa, 0xc8, 0x00, 0x20, 0x09, 0x4f, 0xbf, 0x56, 0xe8, + 0x4f, 0xaa, 0xc8, 0xd1, 0x8d, 0x8a, 0x03, 0x58, 0x53, 0xd0, 0xc0, 0x8d, 0x13, 0xb2, + 0x2c, 0x72, 0x7e, 0x1b, 0xe7, 0xd6, 0x1c, 0x12, 0xb0, 0x1f, 0x53, 0x92, 0x18, 0x8f, + 0xe1, 0x86, 0xde, 0x5a, 0xb9, 0x3d, 0x97, 0x5a, 0xd6, 0xf9, 0x48, 0x8e, 0x9e, 0xa9, + 0xb9, 0x3a, 0xb0, 0xd4, 0xc7, 0x1e, 0xb2, 0xc3, 0xaf, 0xaa, 0xdc, 0xdb, 0xd4, 0x94, + 0x57, 0x65, 0x26, 0xef, 0x81, 0x8f, 0x5c, 0x9a, 0x71, 0x1c, 0xd3, 0xae, 0x42, 0xc2, + 0xe3, 0xcb, 0xd0, 0xbc, 0xdc, 0x45, 0xaa, 0xb3, 0x35, 0x09, 0x59, 0x54, 0x15, 0x40, + 0x87, 0x36, 0xe9, 0x7e, 0xaa, 0x3d, 0xb0, 0xd3, 0x3b, 0xe2, 0xa4, 0x9a, 0xa2, 0x9d, + 0xfd, 0xc3, 0x88, 0xcf, 0x65, 0x18, 0x6e, 0x58, 0xed, 0x10, 0xad, 0xff, 0x44, 0xc0, + 0xe3, 0xc5, 0x2d, 0x83, 0x9b, 0xf7, 0x59, 0xe2, 0x25, 0x68, 0xcd, 0x12, 0x46, 0xc1, + 0x2d, 0xd6, 0x7c, 0xac, 0xac, 0x7b, 0xc9, 0xcd, 0x05, 0xe0, 0xff, 0xe7, 0xe9, 0xa4, + 0x52, 0x1f, 0xfa, 0xdb, 0x60, 0x2a, 0xc7, 0x18, 0xc5, 0x2b, 0x46, 0x1b, 0x9d, 0x1f, + 0x4f, 0xfe, 0x9c, 0x21, 0xe8, 0x58, 0x79, 0x03, 0xde, 0x5d, 0xbd, 0xa1, 0x9e, 0x7c, + 0xac, 0xce, 0xf0, 0x8e, 0xab, 0x4e, 0x98, 0x6f, 0x54, 0xa2, 0x06, 0xa9, 0x72, 0xbf, + 0x84, 0xd6, 0x56, 0x3e, 0xb9, 0xe0, 0xda, 0xb5, 0xac, 0xf5, 0x50, 0xfb, 0xd9, 0xfa, + 0xc6, 0xd2, 0x5f, 0x3e, 0x16, 0x7c, 0xa9, 0xe2, 0xb8, 0xe5, 0xfa, 0x8b, 0xa7, 0x33, + 0x2a, 0x4c, 0x84, 0xad, 0x9a, 0x93, 0xf4, 0xe3, 0x9d, 0xf8, 0x82, 0xf9, 0x3e, 0x0f, + 0xbe, 0x89, 0x69, 0xbe, 0x46, 0xbd, 0x41, 0xae, 0xcb, 0xd3, 0x22, 0x87, 0x22, 0x15, + 0x64, 0xe7, 0xb4, 0x63, 0x00, 0x24, 0xf9, 0x9a, 0x83, 0xf1, 0x30, 0xd1, 0x55, 0x56, + 0xe0, 0x5d, 0x10, 0x2d, 0x19, 0x5b, 0xe4, 0x27, 0x1f, 0xc1, 0x87, 0xbb, 0x4e, 0x09, + 0x79, 0x2d, 0x72, 0x37, 0xa8, 0xcb, 0xe2, 0xb0, 0xee, 0x25, 0x2c, 0x13, 0x0d, 0x5e, + 0x99, 0x6d, 0xb1, 0x2f, 0x73, 0xd7, 0xe6, 0xbb, 0x8f, 0x8e, 0xa1, 0xef, 0xec, 0x04, + 0xc3, 0xe6, 0x34, 0x4a, 0x31, 0x30, 0xed, 0xf7, 0x1e, 0xf0, 0x08, 0x00, 0xa2, 0xea, + 0xe1, 0x36, 0xf2, 0x41, 0x70, 0x07, 0x73, 0x66, 0xf1, 0x64, 0xae, 0xfd, 0x89, 0x5c, + 0x4f, 0xab, 0xeb, 0x4b, 0x44, 0x33, 0x03, 0x6d, 0xb9, 0x8e, 0x06, 0x05, 0xe4, 0xe1, + 0xdc, 0x3a, 0x36, 0xb6, 0xb5, 0xdd, 0x9d, 0xae, 0x22, 0x24, 0x63, 0xf9, 0xbf, 0x1d, + 0x0d, 0x26, 0x23, 0x23, 0xaa, 0xe6, 0xbe, 0xe7, 0x61, 0x53, 0x82, 0xb7, 0x26, 0xc0, + 0xaa, 0x32, 0xe3, 0x4c, 0xeb, 0xcc, 0x8d, 0x6d, 0x13, 0xd3, 0x57, 0x22, 0xe6, 0x11, + 0x4e, 0x35, 0xef, 0xe4, 0x6e, 0x9a, 0x12, 0xf0, 0xdf, 0x39, 0x77, 0x91, 0x59, 0x2f, + 0xf5, 0xa9, 0x63, 0x37, 0xa1, 0x49, 0xdc, 0xda, 0xb9, 0xe5, 0xc9, 0xf6, 0xaf, 0x62, + 0x8f, 0xb8, 0x0d, 0x53, 0x93, 0xfa, 0x0b, 0xe1, 0x41, 0xde, 0xfa, 0xfd, 0xc8, 0xe1, + 0x4f, 0xe4, 0xf2, 0x6c, 0x68, 0x0b, 0x4a, 0x79, 0x6f, 0xfd, 0xba, 0xdf, 0x08, 0xdc, + 0x26, 0x1a, 0x76, 0x48, 0x52, 0x0e, 0xf8, 0x4a, 0x90, 0x94, 0x42, 0xe8, 0x6d, 0xcf, + 0x58, 0xf2, 0x50, 0x3a, 0xc1, 0xaa, 0x66, 0x92, 0x99, 0x12, 0xb7, 0xcd, 0xce, 0xe5, + 0x06, 0x34, 0x76, 0x2e, 0x26, 0x46, 0xb5, 0x24, 0x3b, 0x0d, 0xf0, 0x25, 0x43, 0x19, + 0x0b, 0xca, 0xca, 0x91, 0x94, 0xd7, 0xdf, 0xe4, 0xbf, 0xbb, 0x2e, 0x1b, 0x71, 0xd7, + 0x7d, 0x23, 0x0c, 0xe0, 0x28, 0x2e, 0xa7, 0x1e, 0x5c, 0x78, 0x27, 0x93, 0xfd, 0x81, + 0xd8, 0x00, 0x0e, 0xcd, 0x2a, 0x24, 0x27, 0xc9, 0x39, 0xf6, 0x29, 0x99, 0x2c, 0xaf, + 0x05, 0x85, 0x9a, 0x02, 0xb9, 0x1b, 0x05, 0x70, 0x94, 0x6c, 0x4f, 0xe0, 0x48, 0x8f, + 0xc9, 0xba, 0x8e, 0x79, 0x85, 0x41, 0x18, 0x53, 0xbd, 0x4d, 0x1f, 0x69, 0x6f, 0xe8, + 0xf5, 0xfc, 0x9b, 0x1f, 0xd3, 0x47, 0x1d, 0xfb, 0x14, 0xb8, 0x73, 0x30, 0x7f, 0xa0, + 0x12, 0xce, 0xb2, 0x9c, 0x2d, 0x94, 0x85, 0x4c, 0x60, 0x0b, 0xc6, 0xc6, 0xfd, 0x46, + 0xab, 0xd5, 0x90, 0xcb, 0x58, 0x9d, 0x5b, 0x12, 0x62, 0x1d, 0x54, 0x25, 0x46, 0xb1, + 0x75, 0xcc, 0x02, 0xb8, 0xd6, 0xd6, 0xb7, 0x46, 0x84, 0xf8, 0xad, 0xda, 0xd7, 0x35, + 0xda, 0xcc, 0xb7, 0x8f, 0x96, 0x72, 0x66, 0x55, 0xbf, 0xa8, 0x28, 0x1a, 0x4b, 0xf1, + 0xbe, 0x78, 0x5d, 0x5c, 0x27, 0x60, 0x04, 0x8e, 0x76, 0xb2, 0xbc, 0x0f, 0xb3, 0x39, + 0xb5, 0x5d, 0x5e, 0x97, 0x93, 0xb6, 0xfb, 0xa7, 0x84, 0x07, 0x45, 0x20, 0x4d, 0x3f, + 0x74, 0x39, 0x02, 0x5a, 0x91, 0x1d, 0x7a, 0x9a, 0x4a, 0x75, 0xae, 0x14, 0x09, 0x15, + 0xa7, 0xea, 0x87, 0xf9, 0xae, 0xf4, 0xe1, 0xc2, 0xa7, 0x86, 0x0f, 0x4a, 0x64, 0x6c, + 0x83, 0xbe, 0x01, 0x39, 0xbf, 0xa1, 0x5c, 0xe2, 0x28, 0xe4, 0x4a, 0x31, 0x92, 0xa6, + 0xb2, 0xb2, 0x87, 0xe2, 0x39, 0x49, 0xa1, 0x68, 0xba, 0x3f, 0x2d, 0x13, 0xbd, 0xd9, + 0x6f, 0xd0, 0xdd, 0x26, 0xe2, 0xbd, 0x4b, 0xa5, 0x57, 0xbe, 0x67, 0x0d, 0x64, 0x0f, + 0x5d, 0x7c, 0x12, 0x9b, 0x51, 0x9f, 0x23, 0x56, 0xa6, 0xee, 0xe6, 0x58, 0x94, 0x91, + 0xdf, 0x18, 0xb6, 0xef, 0xc8, 0x43, 0x54, 0x0b, 0xae, 0x1c, 0xe1, 0x2f, 0x1d, 0x36, + 0x73, 0xe7, 0xc6, 0xaa, 0x5d, 0x32, 0x80, 0x03, 0x65, 0x2a, 0x91, 0x02, 0xab, 0xb3, + 0xe5, 0x63, 0x1b, 0x59, 0xe6, 0xc8, 0x21, 0xba, 0xeb, 0xfd, 0x2f, 0x2c, 0x0e, 0x2a, + 0x2e, 0x6c, 0xe8, 0x78, 0x64, 0x6f, 0x32, 0x82, 0x02, 0x04, 0xd7, 0x80, 0xf7, 0xdc, + 0x23, 0x5b, 0xde, 0x48, 0x99, 0x6f, 0xf1, 0xd7, 0xc6, 0x05, 0xad, 0x73, 0xf7, 0xa7, + 0x97, 0x4d, 0x2d, 0x16, 0x98, 0x8a, 0x2b, 0x98, 0x80, 0x30, 0x68, 0x37, 0xa1, 0x8d, + 0x81, 0x86, 0x6d, 0xaa, 0xda, 0x42, 0x0a, 0xd1, 0x21, 0x20, 0xba, 0x21, 0xfd, 0xfa, + 0xe4, 0xa1, 0xa8, 0xf6, 0x59, 0x8c, 0x96, 0x3d, 0xc2, 0xc8, 0xbf, 0xb1, 0xfb, 0xc9, + 0x94, 0x68, 0x6b, 0x43, 0x0a, 0xf9, 0x5e, 0x0e, 0xb4, 0xdd, 0x1c, 0xd9, 0x58, 0x5b, + 0x7a, 0xc2, 0xf9, 0x4f, 0x37, 0x80, 0x1d, 0xea, 0xd6, 0x11, 0xe4, 0x39, 0xe9, 0x2b, + 0xd8, 0xb4, 0x76, 0xd0, 0xd7, 0x64, 0xc2, 0x8c, 0xce, 0xab, 0xd5, 0xe1, 0xb7, 0x77, + 0xf7, 0xe4, 0xee, 0xb3, 0xc0, 0x36, 0x77, 0xc5, 0x97, 0xae, 0xa4, 0x0f, 0x98, 0xcf, + 0xa7, 0xfc, 0x79, 0xc5, 0xb1, 0xd9, 0x7b, 0xa9, 0xb6, 0x69, 0xc0, 0xdf, 0x8f, 0x24, + 0x19, 0x1f, 0xe2, 0x0b, 0xfa, 0x18, 0x9f, 0xe0, 0x1d, 0x7c, 0x7f, 0xc2, 0xd2, 0x3a, + 0x96, 0x79, 0x2f, 0x52, 0x6b, 0x8f, 0xf5, 0xcb, 0xbf, 0x2a, 0x6a, 0x84, 0x07, 0x59, + 0x79, 0x10, 0xe6, 0x67, 0x2d, 0xf0, 0xf3, 0x7a, 0x3c, 0xab, 0xe6, 0x80, 0x70, 0x48, + 0x58, 0x75, 0x10, 0x04, 0xc3, 0x2c, 0x15, 0x39, 0xf2, 0x98, 0xc3, 0x16, 0xd7, 0xa3, + 0x69, 0x6f, 0x82, 0xa1, 0x2f, 0x13, 0x73, 0xa9, 0xcd, 0x02, 0x57, 0xb7, 0x20, 0x69, + 0x9e, 0x6b, 0x9d, 0x9c, 0xae, 0x89, 0x88, 0x95, 0x8d, 0x5b, 0x31, 0x23, 0x9e, 0x9e, + 0x1e, 0x5c, 0xbc, 0x2c, 0xae, 0xb5, 0x67, 0x1c, 0xf9, 0x10, 0x89, 0xef, 0xaa, 0xe7, + 0xa2, 0xca, 0x4e, 0xc1, 0xfc, 0x6d, 0xc5, 0xb3, 0x87, 0xfb, 0x59, 0x74, 0x79, 0x06, + 0x0c, 0x67, 0xe9, 0x30, 0x6a, 0x9d, 0x1d, 0xfa, 0xa9, 0x8b, 0x5f, 0x83, + ], + }, + // This test vector is too long for most no-std environments. + #[cfg(feature = "std")] + TestVector { + normal: &[ + 0xd5, 0x6f, 0x10, 0x99, 0x77, 0x64, 0xd0, 0x87, 0x40, 0x89, 0x86, 0xe7, 0x3d, 0x6e, + 0x28, 0x4f, 0xea, 0x9a, 0x23, 0xc3, 0x93, 0x11, 0x78, 0x2f, 0x86, 0xca, 0xbf, 0xf9, + 0x45, 0x5e, 0x4c, 0xf6, 0x99, 0xe5, 0xf5, 0xd4, 0xbc, 0x0b, 0x39, 0x05, 0xa4, 0xe3, + 0xbd, 0x01, 0xc5, 0x4d, 0xf8, 0x64, 0x34, 0x43, 0xbe, 0x0f, 0x88, 0x90, 0x32, 0xea, + 0x32, 0x5b, 0xf0, 0x71, 0x07, 0xfd, 0x41, 0xd6, 0x73, 0xee, 0xba, 0xe6, 0xfa, 0x63, + 0x7b, 0x70, 0xcc, 0x0e, 0xd3, 0xf0, 0x09, 0x58, 0xdf, 0xb8, 0xdc, 0xf0, 0x0e, 0x85, + 0xa1, 0xd0, 0xa6, 0xa8, 0x90, 0x81, 0x40, 0xc2, 0xf4, 0x34, 0xc2, 0xe2, 0x60, 0xef, + 0xb0, 0xbc, 0xa2, 0x00, 0x35, 0x04, 0xc9, 0x99, 0x93, 0xa9, 0xe1, 0xc0, 0xff, 0x9c, + 0xef, 0xe6, 0xa6, 0x65, 0xd7, 0x91, 0x42, 0x86, 0x90, 0xe4, 0x7e, 0xf8, 0xc1, 0x31, + 0xa8, 0xe9, 0xbf, 0xb4, 0xc3, 0x08, 0x02, 0x35, 0x03, 0x2d, 0x73, 0x1b, 0x0d, 0x38, + 0x41, 0x22, 0x5f, 0x1c, 0x11, 0xe2, 0xc2, 0x8e, 0xe8, 0x4d, 0x35, 0xf9, 0x22, 0x61, + 0x00, 0x56, 0x59, 0x72, 0xeb, 0x26, 0x9d, 0x27, 0x8e, 0xf6, 0x49, 0x79, 0xbf, 0x65, + 0x15, 0xed, 0x4a, 0x68, 0x40, 0xb0, 0x88, 0x3a, 0x9e, 0x6e, 0xf6, 0x4a, 0x0e, 0xfc, + 0xae, 0x1c, 0xf2, 0x1d, 0xfe, 0x74, 0x85, 0x4e, 0x84, 0xc2, 0x74, 0x9f, 0xac, 0x03, + 0x82, 0x52, 0x75, 0xc9, 0xb6, 0x30, 0x21, 0x84, 0xc7, 0x2d, 0xf4, 0xc4, 0xbb, 0x28, + 0x62, 0xe4, 0xe8, 0xa7, 0xd9, 0xa4, 0xa2, 0x82, 0x86, 0x6f, 0x9a, 0x7b, 0x2c, 0xfc, + 0x9a, 0x56, 0x31, 0x3d, 0xa0, 0xc4, 0x7a, 0x34, 0xb7, 0xb9, 0xcd, 0xa3, 0xac, 0xe8, + 0x18, 0x5f, 0x07, 0xdf, 0x36, 0xe4, 0x48, 0xa7, 0x6a, 0xa4, 0x77, 0xf2, 0x24, 0xd8, + 0x7a, 0x07, 0x4f, 0x43, 0xaf, 0x5d, 0x5f, 0x79, 0xb3, 0xab, 0x11, 0x28, 0xf0, 0x81, + 0x91, 0x44, 0x7f, 0xa6, 0x46, 0xbf, 0xdd, 0xe5, 0xb5, 0x1e, 0x23, 0x3c, 0xa6, 0x15, + 0x5d, 0x10, 0x15, 0x85, 0xbc, 0x2c, 0x40, 0x15, 0x8a, 0xc2, 0x10, 0x6e, 0x66, 0xa2, + 0x6e, 0x46, 0x42, 0x33, 0x70, 0x63, 0x68, 0x76, 0xb4, 0x34, 0xa7, 0x4f, 0x8c, 0xe8, + 0x06, 0x00, 0x50, 0xb0, 0x82, 0xa7, 0x9b, 0x61, 0xbb, 0x5d, 0x34, 0x4e, 0xb5, 0xa1, + 0x15, 0x83, 0x26, 0xce, 0xd9, 0xa9, 0xd9, 0xf5, 0x4f, 0xb2, 0xfe, 0x8f, 0x9f, 0x05, + 0xcd, 0x11, 0x1e, 0xe4, 0x6c, 0x47, 0x10, 0xf6, 0xf6, 0x3a, 0x62, 0x69, 0x45, 0x57, + 0xef, 0x1b, 0x12, 0xc8, 0x80, 0x06, 0xb6, 0x78, 0x72, 0x50, 0x5f, 0x4e, 0x88, 0x3b, + 0x58, 0x59, 0x07, 0x92, 0x9a, 0x2f, 0x3f, 0xdb, 0x0d, 0x8f, 0x79, 0x14, 0xc4, 0x2d, + 0xde, 0x2d, 0x20, 0x00, 0xf5, 0xae, 0x02, 0xd4, 0x18, 0x21, 0xc8, 0xe1, 0xee, 0x01, + 0x38, 0xeb, 0xcb, 0x72, 0x8d, 0x7c, 0x6c, 0x3c, 0x80, 0x02, 0x7e, 0x43, 0x75, 0x94, + 0xc6, 0x70, 0xfd, 0x6f, 0x39, 0x08, 0x22, 0x2e, 0xe7, 0xa1, 0xb9, 0x17, 0xf8, 0x27, + 0x1a, 0xbe, 0x66, 0x0e, 0x39, 0xe0, 0x51, 0xaa, 0xa6, 0xfc, 0xa1, 0x86, 0x22, 0x76, + 0xe2, 0xba, 0xa0, 0xfe, 0x0b, 0x16, 0x2a, 0xeb, 0xcf, 0xe3, 0xd9, 0x34, 0x9c, 0x8d, + 0x15, 0x4b, 0xb7, 0xee, 0x28, 0x21, 0x2c, 0x1b, 0xaa, 0x70, 0x5d, 0x82, 0x07, 0x0d, + 0x70, 0x32, 0xf2, 0x69, 0x5d, 0x17, 0x96, 0x80, 0x9f, 0xab, 0x41, 0x24, 0x69, 0x26, + 0xaf, 0x99, 0x2b, 0x6e, 0xee, 0x95, 0xa9, 0xa0, 0x6b, 0xc4, 0x56, 0x2c, 0x5f, 0x2f, + 0x1b, 0x19, 0x54, 0x95, 0x00, 0x37, 0x2e, 0x7a, 0xd5, 0x79, 0xa6, 0xd6, 0xd7, 0x8b, + 0x33, 0x15, 0x31, 0x30, 0xfb, 0x44, 0x8f, 0xb7, 0x9e, 0x8a, 0x66, 0x9d, 0xb8, 0xa0, + 0xf3, 0x5c, 0xdf, 0x9a, 0xe5, 0xd3, 0x2d, 0x73, 0x2f, 0xc7, 0x94, 0x18, 0xe2, 0x3b, + 0x45, 0x1d, 0xdc, 0x95, 0xa2, 0x2a, 0xba, 0xbb, 0x05, 0x6e, 0xc6, 0xb5, 0xe8, 0xba, + 0x4f, 0x52, 0x4d, 0xfa, 0xfe, 0x87, 0x52, 0x62, 0xdd, 0x7b, 0xe4, 0x1c, 0xbb, 0xc6, + 0x24, 0x20, 0xd4, 0xad, 0x6d, 0xf5, 0xc9, 0xb7, 0x13, 0x60, 0x4f, 0x65, 0x60, 0x88, + 0xa4, 0x48, 0x5e, 0x93, 0xbe, 0x19, 0x07, 0xd2, 0x7a, 0xc6, 0xec, 0x3c, 0x57, 0x25, + 0x9b, 0xd6, 0x98, 0x1d, 0x42, 0xc1, 0xb7, 0x8a, 0x29, 0xad, 0x96, 0x85, 0xe6, 0x3c, + 0x49, 0x4d, 0x41, 0x29, 0x62, 0x3e, 0xa1, 0xa7, 0xff, 0xec, 0x85, 0xfa, 0x29, 0x41, + 0x10, 0x73, 0xed, 0xb2, 0x97, 0x8e, 0xf4, 0xe4, 0x69, 0xdd, 0xd5, 0xcd, 0xa9, 0x86, + 0x18, 0x99, 0x95, 0xf8, 0x8d, 0x6a, 0xb3, 0x66, 0xdb, 0x01, 0x90, 0x01, 0xf5, 0xb2, + 0x52, 0x88, 0xcf, 0x86, 0x0f, 0xd9, 0x98, 0xee, 0x57, 0x3c, 0x8c, 0xc4, 0x8a, 0xa9, + 0xef, 0xcf, 0x9b, 0x61, 0x7e, 0x04, 0x3c, 0x32, 0x9c, 0xd1, 0xaa, 0x1a, 0x0e, 0xd3, + 0xa4, 0x02, 0xfb, 0x96, 0xe3, 0x36, 0xc7, 0x19, 0xe6, 0x25, 0x3c, 0xb6, 0x91, 0xaa, + 0x0d, 0xb5, 0x27, 0x36, 0x62, 0x6e, 0xd1, 0x97, 0x88, 0x75, 0x88, 0x8e, 0xc7, 0x6c, + 0x84, 0x6b, 0xc2, 0x27, 0x27, 0x2a, 0x58, 0x53, 0x17, 0xdf, 0xf0, 0xb1, 0x14, 0x8d, + 0x92, 0xd6, 0xf5, 0xfb, 0x7d, 0x95, 0x33, 0x67, 0x70, 0xa7, 0xd1, 0x6f, 0xac, 0x1a, + 0xdd, 0x86, 0x07, 0x76, 0xcb, 0x48, 0x02, 0x21, 0xf8, 0xfb, 0x33, 0xd7, 0xe4, 0xe9, + 0xb0, 0x79, 0x02, 0xd2, 0xff, 0x86, 0xfd, 0xac, 0x72, 0x09, 0x62, 0x34, 0xae, 0xd4, + 0x8d, 0xe8, 0x92, 0xff, 0x73, 0x55, 0x07, 0x3b, 0xbf, 0x06, 0x15, 0xf6, 0x7b, 0x11, + 0x00, 0xcc, 0x2e, 0xa3, 0xba, 0x3d, 0x6c, 0x1a, 0x1a, 0x90, 0x87, 0xb1, 0x19, 0xba, + 0xee, 0xbf, 0xa6, 0x2b, 0xc9, 0xf0, 0xec, 0x47, 0x9d, 0x99, 0xc1, 0xa3, 0xb1, 0x58, + 0xb5, 0x14, 0xd1, 0x62, 0x9d, 0xb3, 0x99, 0x3f, 0x11, 0x67, 0x2a, 0x26, 0x70, 0x8e, + 0x5a, 0xd8, 0x16, 0xb5, 0x47, 0xab, 0x7e, 0x82, 0x7d, 0x07, 0x1b, 0xa7, 0x84, 0x2b, + 0x3e, 0x90, 0x30, 0x53, 0x83, 0x89, 0x6e, 0xc4, 0x90, 0x5f, 0x70, 0xc7, 0x8b, 0x69, + 0x4e, 0x6a, 0x5a, 0x3e, 0x43, 0x12, 0xcd, 0x82, 0x08, 0x13, 0x2b, 0x84, 0x0f, 0x05, + 0xc7, 0x14, 0x52, 0x3c, 0xa8, 0x19, 0x72, 0x0a, 0xe2, 0x27, 0xfd, 0x1a, 0xcb, 0xa7, + 0x14, 0xfa, 0x4f, 0xc4, 0x5f, 0xc5, 0x39, 0x88, 0x57, 0xb4, 0x0d, 0xc1, 0x48, 0x79, + 0x85, 0x6f, 0x35, 0x4b, 0xa4, 0xd2, 0x58, 0x1d, 0x0c, 0xda, 0x54, 0xb6, 0x38, 0xba, + 0x9d, 0x76, 0xf9, 0xb5, 0x2d, 0x17, 0xc8, 0xf8, 0x8e, 0xe6, 0x3f, 0x58, 0x45, 0xb5, + 0xdc, 0xef, 0xa4, 0xc3, 0x47, 0x9b, 0xce, 0x9a, 0xca, 0xd1, 0x8b, 0x4a, 0xea, 0xe0, + 0x3c, 0x0e, 0xae, 0x22, 0x5d, 0x42, 0x84, 0x8b, 0xde, 0xaa, 0x53, 0x6d, 0x7d, 0x8d, + 0xd3, 0xbc, 0x97, 0x9f, 0x06, 0x58, 0x66, 0x73, 0xbc, 0x6f, 0xf1, 0xc5, 0xd3, 0xb3, + 0x20, 0xf3, 0x49, 0xa5, 0xb3, 0xa8, 0xb3, 0x55, 0x59, 0x22, 0x96, 0xaa, 0xf6, 0x1c, + 0x5b, 0x72, 0x52, 0xf7, 0x3e, 0xc0, 0xa9, 0x46, 0x6a, 0x1b, 0x85, 0x76, 0x4f, 0xb0, + 0x83, 0x1b, 0x4a, 0x1a, 0x36, 0x89, 0x0e, 0x22, 0x4c, 0x01, 0xac, 0xfc, 0xe4, 0x8e, + 0xe3, 0xed, 0x93, 0x87, 0x73, 0x98, 0xe0, 0x72, 0x6d, 0x02, 0x93, 0x6d, 0x0d, 0x03, + 0x2e, 0x18, 0xe3, 0x28, 0x8b, 0x26, 0x70, 0xe1, 0x36, 0x2c, 0x32, 0xd6, 0xe4, 0x73, + 0x3b, 0x9d, 0xd2, 0xd5, 0xf2, 0x6e, 0x1f, 0xe3, 0x06, 0xf7, 0x3c, 0x00, 0x7f, 0xdd, + 0xca, 0xe9, 0xd9, 0xc0, 0xaa, 0xf1, 0x87, 0xd7, 0x42, 0x8b, 0x1e, 0x9d, 0x47, 0x9c, + 0x18, 0x23, 0x7b, 0x98, 0x28, 0xbc, 0xa8, 0xb9, 0x8c, 0x9d, 0x9b, 0xec, 0x7d, 0x82, + 0x70, 0xb5, 0xd8, 0xee, 0xc3, 0xcc, 0x4f, 0x43, 0xfa, 0x01, 0x88, 0x52, 0x1b, 0xc6, + 0x1b, 0x21, 0xdd, 0x04, 0xe3, 0x7a, 0x83, 0xec, 0xe6, 0x8c, 0xa7, 0xa2, 0xfa, 0x6c, + 0x8f, 0x9e, 0x34, 0xa6, 0x29, 0x03, 0x35, 0xaa, 0x1f, 0xbd, 0x83, 0xd5, 0x4a, 0xaf, + 0x44, 0x1e, 0x31, 0x9e, 0xa4, 0x7a, 0x86, 0x2a, 0xd0, 0x29, 0x3c, 0xed, 0xf5, 0xdd, + 0x9e, 0xda, 0xde, 0xee, 0x33, 0xcb, 0x52, 0x2c, 0xd0, 0x11, 0x8b, 0xbd, 0x81, 0x1a, + 0xce, 0x9a, 0x23, 0xbd, 0xa3, 0x9a, 0xba, 0x72, 0xf1, 0x56, 0x6f, 0xc1, 0x68, 0x84, + 0x97, 0xd2, 0xa7, 0x92, 0x8c, 0x36, 0x70, 0x15, 0x25, 0x67, 0x8b, 0xc9, 0x72, 0x14, + 0xb3, 0x1b, 0x37, 0xba, 0xb4, 0x6b, 0x88, 0xf2, 0x7f, 0x04, 0x48, 0xde, 0xcb, 0x31, + 0x62, 0x2d, 0x0f, 0x0f, 0x87, 0xa8, 0x55, 0xba, 0x54, 0x00, 0x03, 0x32, 0x03, 0x1f, + 0x73, 0xab, 0xff, 0xd4, 0x65, 0x91, 0xda, 0x0b, 0x88, 0x72, 0x35, 0x04, 0xed, 0xb2, + 0x33, 0x72, 0x30, 0xda, 0xd2, 0xac, 0xc0, 0xd8, 0xbb, 0x68, 0xbc, 0x83, 0x7a, 0x2f, + 0xf9, 0x30, 0xbf, 0xf0, 0x6f, 0xde, 0x74, 0xeb, 0x90, 0xaa, 0xe4, 0xf6, 0x0d, 0xbb, + 0x6e, 0xb8, 0x27, 0xea, 0x99, 0x88, 0x4a, 0xcd, 0x62, 0x85, 0xa9, 0x88, 0x92, 0x80, + 0x2c, 0xf5, 0x9d, 0x5d, 0x60, 0xd0, 0x16, 0x63, 0x38, 0x7b, 0x3e, 0xd2, 0x72, 0x3b, + 0xd6, 0x48, 0x9e, 0x9c, 0x2c, 0x10, 0x6d, 0x4a, 0xa2, 0xde, 0x23, 0xce, 0xd1, 0x6c, + 0x72, 0x04, 0x29, 0xc7, 0x75, 0x3a, 0x77, 0x38, 0xec, 0x7d, 0x9d, 0xb8, 0x62, 0x42, + 0x29, 0xed, 0xd2, 0x17, 0xb8, 0x0d, 0x74, 0x87, 0x5a, 0x14, 0xca, 0xe4, 0x86, 0x3f, + 0x13, 0x9e, 0x9c, 0x0b, 0x13, 0x1b, 0x2a, 0x4c, 0x28, 0x07, 0x1a, 0x38, 0xec, 0x61, + 0xf6, 0x68, 0x01, 0xaa, 0x59, 0x56, 0xfc, 0xb2, 0xa4, 0x6b, 0x95, 0x87, 0x66, 0x5b, + 0x75, 0x71, 0xaa, 0x03, 0x48, 0x1f, 0xd8, 0xd9, 0xd5, 0x69, 0x8f, 0x83, 0x6f, 0xc8, + 0x63, 0x5e, 0x69, 0xe3, 0xbd, 0xe4, 0x2f, 0x4a, 0xc0, 0x71, 0x32, 0x8b, 0x54, 0x09, + 0xf6, 0xe4, 0x2d, 0x79, 0x0a, 0xed, 0xd7, 0x3b, 0xc1, 0xa2, 0x35, 0x47, 0x23, 0xb3, + 0xb8, 0x19, 0xd0, 0x63, 0x7a, 0x6f, 0xa4, 0x66, 0x39, 0x46, 0xa3, 0x0a, 0xc5, 0xaf, + 0xdd, 0x30, 0xce, 0x83, 0x0f, 0x67, 0x91, 0xb4, 0x57, 0x52, 0x70, 0xa1, 0x72, 0x0f, + 0x91, 0x86, 0x6e, 0x2b, 0x86, 0xf4, 0x78, 0x88, 0x94, 0xc8, 0xda, 0x62, 0xd8, 0xb9, + 0x1f, 0xaf, 0x52, 0x0e, 0x3b, 0xed, 0xbc, 0x12, 0x06, 0xa5, 0xa5, 0xe6, 0xef, 0xd3, + 0xdf, 0xde, 0x08, 0x43, 0xc3, 0xb0, 0x67, 0x57, 0x64, 0x3f, 0xc0, 0x06, 0x00, 0x88, + 0x38, 0xca, 0x47, 0x30, 0x87, 0xf8, 0x97, 0x79, 0x18, 0xcc, 0x1b, 0x81, 0xc9, 0xe6, + 0x8e, 0x3b, 0x88, 0x8f, 0xe6, 0xf7, 0xc6, 0x30, 0xf1, 0xbc, 0x7a, 0xe1, 0x88, 0xf5, + 0x12, 0x84, 0x20, 0x41, 0xca, 0xda, 0x1e, 0x05, 0xf8, 0x66, 0xd2, 0x56, 0x2d, 0xbe, + 0x09, 0xc4, 0xb4, 0x30, 0x68, 0xf7, 0x54, 0xda, 0xd3, 0x4d, 0xf0, 0xfc, 0xfc, 0x18, + 0x1f, 0x31, 0x80, 0x1a, 0x79, 0x92, 0xd2, 0xf1, 0x6b, 0xe0, 0x21, 0x1b, 0x4a, 0x22, + 0xf6, 0x2a, 0xab, 0x64, 0x70, 0x1b, 0xf4, 0xa4, 0xe6, 0xd6, 0x66, 0xfc, 0x30, 0x4a, + 0x5c, 0x79, 0xc6, 0x09, 0xac, 0xc4, 0x3b, 0x00, 0xb4, 0x86, 0x48, 0x93, 0xd3, 0x7d, + 0x50, 0x07, 0xf0, 0xc3, 0x29, 0xa4, 0x75, 0x50, 0x52, 0x57, 0x75, 0x70, 0xdd, 0x38, + 0xfa, 0xc0, 0x43, 0xcd, 0x91, 0xc1, 0x2e, 0xe3, 0x4e, 0x9c, 0xfa, 0xe3, 0x92, 0xa7, + 0x8b, 0xda, 0xbd, 0x4e, 0xe3, 0x1d, 0xc0, 0xde, 0xb0, 0x2f, 0xe7, 0xb1, 0xd8, 0xb0, + 0x17, 0x8a, 0xc9, 0x51, 0x31, 0x05, 0xfc, 0xc7, 0xe3, 0x0b, 0xa8, 0xe0, 0x16, 0xaa, + 0x36, 0xa6, 0xb5, 0xdf, 0x5e, 0x5a, 0x19, 0x09, 0xf6, 0x3a, 0xba, 0x09, 0x5d, 0x98, + 0x77, 0xa8, 0xf2, 0xdc, 0x53, 0xf4, 0x6f, 0x6c, 0x9b, 0x07, 0xad, 0xdf, 0x14, 0x6f, + 0x4f, 0xfa, 0x50, 0x1f, 0x9d, 0xd3, 0xcf, 0xf9, 0x24, 0xe3, 0x01, 0x0f, 0xaf, 0x50, + 0x4e, 0x2b, 0x8a, 0xca, 0x73, 0x57, 0xac, 0xbf, 0xfe, 0xc7, 0x3a, 0xc3, 0x4c, 0x1a, + 0x73, 0x16, 0x0f, 0x2c, 0xea, 0x1e, 0x05, 0x10, 0xf8, 0x4d, 0x2f, 0xe2, 0xf7, 0x3b, + 0x6e, 0x92, 0x19, 0x07, 0xa1, 0xb7, 0xb3, 0x75, 0x12, 0x13, 0x24, 0x1b, 0x2c, 0xfa, + 0xa5, 0x5a, 0x5e, 0xa4, 0xdd, 0x51, 0x7e, 0x7b, 0x49, 0xd2, 0xde, 0x8c, 0x09, 0x08, + 0x43, 0x73, 0x0d, 0x24, 0x08, 0xa2, 0xa3, 0x04, 0xaa, 0x1e, 0x2e, 0x13, 0x70, 0xa6, + 0xbf, 0x6c, 0x2b, 0xc7, 0x3f, 0xf0, 0x0d, 0x89, 0x3b, 0xc1, 0x28, 0x5e, 0xfc, 0xa8, + 0x25, 0x99, 0xd1, 0x81, 0xf1, 0x23, 0x51, 0xf9, 0x39, 0xa9, 0x4e, 0xa8, 0xb9, 0x75, + 0xc0, 0x65, 0xa9, 0x1f, 0xf2, 0x57, 0xca, 0xc7, 0xa9, 0x23, 0x85, 0xfc, 0x8f, 0xa9, + 0x21, 0xb1, 0x06, 0xba, 0x86, 0x60, 0xc6, 0x0a, 0xc8, 0xba, 0x5e, 0xce, 0x45, 0x60, + 0x6f, 0x04, 0xf3, 0x6a, 0x3a, 0x90, 0xbb, 0x38, 0x38, 0xc4, 0x2a, 0xbf, 0x62, 0xdd, + 0x2d, 0x84, 0xba, 0xbe, 0xf3, 0xe1, 0x88, 0xe9, 0x17, 0x1a, 0xff, 0x9b, 0xc1, 0x16, + 0x66, 0x90, 0x09, 0xd8, 0x87, 0x13, 0x0a, 0xc9, 0xf7, 0x39, 0x6a, 0x62, 0x7a, 0x84, + 0x74, 0xc1, 0x81, 0x1b, 0x69, 0x6f, 0x99, 0x55, 0x2b, 0x14, 0xc4, 0x84, 0xdf, 0xe4, + 0x2c, 0x24, 0xd5, 0x7c, 0x3a, 0x9c, 0x3f, 0xea, 0x13, 0x76, 0xcd, 0xcb, 0x63, 0x42, + 0x1c, 0x31, 0x4a, 0x62, 0x2a, 0x9a, 0xef, 0x0b, 0xc0, 0x57, 0xcb, 0x11, 0xbc, 0x5e, + 0x30, 0x66, 0xe3, 0x3a, 0x3b, 0x9b, 0x31, 0xdf, 0x25, 0x75, 0xcd, 0x51, 0x85, 0xa4, + 0xf3, 0xfc, 0x4e, 0x4c, 0x3d, 0x40, 0x2e, 0xd4, 0x20, 0x46, 0xf8, 0x1f, 0x97, 0x48, + 0x16, 0xd2, 0x79, 0xb1, 0x51, 0x3a, 0xb8, 0x1d, 0x3f, 0x0a, 0x3c, 0x7f, 0x7f, 0xcf, + 0x2f, 0xbb, 0x4e, 0x26, 0x32, 0x19, 0x93, 0xa5, 0x13, 0xad, 0x3d, 0x7f, 0x4a, 0xfe, + 0x6c, 0x1b, 0xbd, 0xc6, 0x57, 0x58, 0x50, 0x80, 0xbb, 0x5a, 0x0f, 0x25, 0x97, 0x3d, + 0x63, 0xeb, 0x20, 0xad, 0xa0, 0x16, 0x6b, 0xbd, 0x8a, 0x39, 0xff, 0x93, 0x24, 0x6f, + 0x27, 0x89, 0x73, 0x2a, 0xd0, 0x55, 0x87, 0xf8, 0xdb, 0x7b, 0xc8, 0x7c, 0x24, 0x2c, + 0xfd, 0x36, 0xce, 0x68, 0x5a, 0x4b, 0x65, 0x69, 0x86, 0xc3, 0x9f, 0xd7, 0xfc, 0xb2, + 0x3c, 0x91, 0x91, 0x3e, 0x46, 0x11, 0x19, 0x1e, 0xdc, 0xc8, 0x8b, 0x78, 0xf1, 0x45, + 0xea, 0x29, 0xd2, 0x71, 0xb9, 0x40, 0xc6, 0x99, 0x41, 0xe4, 0xc3, 0xfd, 0x2d, 0x71, + 0xf3, 0xb1, 0x90, 0x69, 0x0e, 0xe1, 0x6f, 0x5d, 0x14, 0xac, 0x22, 0x24, 0xe6, 0xfc, + 0x89, 0x59, 0x76, 0x54, 0x52, 0x7d, 0xab, 0xe7, 0x2e, 0x75, 0xd2, 0xd2, 0xa1, 0x3a, + 0x9f, 0xba, 0xa6, 0x37, 0x8e, 0x8a, 0x26, 0x43, 0x21, 0x08, 0x7a, 0x19, 0x00, 0xef, + 0xe3, 0xca, 0xd1, 0x4a, 0x57, 0x96, 0x86, 0xaa, 0x36, 0x36, 0xbd, 0x37, 0x5b, 0xd3, + 0x13, 0x6b, 0xee, 0x0b, 0xda, 0xab, 0xcf, 0xac, 0x88, 0x1b, 0xc7, 0x01, 0x81, 0x27, + 0x21, 0xe6, 0xfb, 0x75, 0xaa, 0x07, 0x2d, 0x2d, 0x18, 0x7e, 0x62, 0x25, 0x8d, 0x65, + 0xa1, 0x92, 0x15, 0x7c, 0xdf, 0x2e, 0xc3, 0x21, 0x40, 0x7f, 0x68, 0x2f, 0x5e, 0xec, + 0x6a, 0x32, 0x97, 0xab, 0x20, 0xb7, 0x06, 0x1c, 0x62, 0x24, 0x57, 0x16, 0xa4, 0x4f, + 0x71, 0xfb, 0xfc, 0x34, 0xc7, 0x9b, 0x44, 0xe0, 0x9e, 0x42, 0x12, 0xac, 0x26, 0x53, + 0xf6, 0xc4, 0x03, 0x64, 0x3e, 0x1c, 0x5b, 0x9a, 0xd1, 0x34, 0xd8, 0x9c, 0x68, 0x0b, + 0x70, 0x72, 0x83, 0xaf, 0x54, 0x32, 0x6f, 0xc4, 0xf8, 0x4d, 0x6a, 0x58, 0x29, 0xa0, + 0xad, 0x48, 0x30, 0x80, 0x6c, 0x05, 0x75, 0x84, 0x92, 0xcd, 0x6a, 0xc4, 0x6b, 0xa0, + 0x1a, 0x2b, 0x37, 0x22, 0xb5, 0xe4, 0xcd, 0xaf, 0xbb, 0x3f, 0x36, 0x78, 0x5f, 0x42, + 0x4a, 0xf0, 0x44, 0xda, 0xc5, 0xdb, 0x5f, 0x7d, 0xf8, 0x39, 0xeb, 0x63, 0xc0, 0xc1, + 0x7d, 0x8b, 0x0c, 0x79, 0xdb, 0x86, 0x30, 0x94, 0x20, 0x15, 0xbe, 0x13, 0xf7, 0x9a, + 0xf6, 0xf4, 0x3e, 0x5a, 0xb0, 0x77, 0x81, 0x14, 0x79, 0x8f, 0x44, 0x22, 0x58, 0xee, + 0xdc, 0x43, 0x6f, 0xcc, 0x38, 0x6b, 0x36, 0xb5, 0x7e, 0x19, 0x17, 0xd7, 0x20, 0x17, + 0x73, 0x66, 0xf4, 0x24, 0xb0, 0xa5, 0x4b, 0x0b, 0x60, 0xf4, 0xfb, 0x13, 0x58, 0xc2, + 0x0a, 0xa4, 0x1d, 0xc5, 0x02, 0xe1, 0xdd, 0x8a, 0x16, 0x33, 0xf3, 0xd8, 0xe3, 0x27, + 0x6b, 0x59, 0xe7, 0xd2, 0xc4, 0xe6, 0x24, 0xa6, 0xf5, 0x36, 0x95, 0xbc, 0xaf, 0x24, + 0x7e, 0x36, 0x48, 0x3f, 0x13, 0xb2, 0x04, 0x42, 0x22, 0x37, 0xfc, 0x6a, 0xb3, 0xeb, + 0xa0, 0x2f, 0xc4, 0x14, 0x2b, 0x42, 0x97, 0xeb, 0xb5, 0x68, 0x3d, 0xb8, 0xd2, 0x43, + 0x19, 0x70, 0x6a, 0xd2, 0x6a, 0xaf, 0xd8, 0x1c, 0x53, 0xb7, 0x40, 0xf3, 0x45, 0x43, + 0xa6, 0xb3, 0xe9, 0xf5, 0xbb, 0x7d, 0x5c, 0x49, 0xe8, 0xc3, 0x7f, 0x61, 0x49, 0x21, + 0x25, 0x4f, 0x32, 0x12, 0x39, 0x4c, 0x79, 0x7d, 0x1c, 0xee, 0x78, 0x99, 0xb7, 0xb4, + 0xb6, 0x5b, 0x59, 0xb7, 0x34, 0x2f, 0x92, 0x53, 0x1c, 0x1d, 0x59, 0xe1, 0x79, 0x70, + 0xb7, 0x31, 0x74, 0x14, 0x43, 0x8c, 0xd8, 0x0b, 0xd0, 0xf9, 0xa6, 0x7c, 0x9b, 0x9e, + 0x55, 0x2f, 0x01, 0x3c, 0x11, 0x5a, 0x95, 0x4f, 0x35, 0xe0, 0x61, 0x6c, 0x68, 0xd4, + 0x31, 0x63, 0xd3, 0x34, 0xda, 0xc3, 0x82, 0x70, 0x33, 0xe5, 0xad, 0x84, 0x88, 0xbf, + 0xd9, 0xc4, 0xbb, 0xbe, 0x8f, 0x59, 0x35, 0xc6, 0xc5, 0xea, 0x04, 0xc3, 0xad, 0x49, + 0xc7, 0x47, 0xa9, 0xe7, 0x23, 0x1b, 0xcd, 0x7d, 0x16, 0x21, 0x5e, 0x6e, 0x80, 0x73, + 0x7d, 0x6b, 0x54, 0xfe, 0xc8, 0xb8, 0x84, 0x02, 0xf0, 0x47, 0x52, 0x45, 0xe1, 0x74, + 0xa7, 0x45, 0xb8, 0x31, 0xf8, 0xfe, 0x03, 0xa7, 0x6f, 0xb9, 0xce, 0xca, 0x4d, 0x22, + 0xb7, 0x83, 0xc3, 0x28, 0xc6, 0x91, 0x5c, 0x43, 0x40, 0x50, 0x64, 0xae, 0x56, 0xbc, + 0x89, 0xe6, 0x4d, 0x15, 0x78, 0xe4, 0xd3, 0xa3, 0x4b, 0xb9, 0x55, 0x91, 0xea, 0xf1, + 0xd3, 0xda, 0x02, 0xa4, 0x54, 0x9f, 0xa8, 0x0d, 0xb0, 0xff, 0x7c, 0xb0, 0x39, 0x93, + 0xb6, 0x8a, 0xe1, 0x5a, 0x30, 0xe8, 0x79, 0x49, 0xaa, 0x08, 0x0e, 0x94, 0xab, 0xde, + 0x68, 0x89, 0x8c, 0x33, 0x92, 0xa2, 0x17, 0xd6, 0x49, 0x61, 0x6b, 0xbe, 0x73, 0x9b, + 0x13, 0xd1, 0x4d, 0xf0, 0x3f, 0xf2, 0x76, 0x71, 0x48, 0x9b, 0xe0, 0xb4, 0xbe, 0xba, + 0xaf, 0xa7, 0xd1, 0xe6, 0x39, 0xd5, 0xb3, 0xe9, 0x94, 0xff, 0xb6, 0xb7, 0xa2, 0x09, + 0xf6, 0xad, 0xfe, 0x8d, 0x1e, 0x5c, 0xcf, 0x01, 0x0c, 0x19, 0x16, 0x8a, 0xeb, 0x18, + 0xaa, 0x9d, 0x68, 0x7e, 0x24, 0xad, 0xc0, 0xb1, 0x13, 0x5c, 0x70, 0xc9, 0x70, 0xe0, + 0x90, 0x3a, 0xf6, 0xe1, 0x70, 0x81, 0xd5, 0x81, 0x8e, 0x88, 0xb1, 0x4e, 0x4f, 0x60, + 0x1b, 0x8c, 0x06, 0x3e, 0x3f, 0x43, 0x87, 0xff, 0xa2, 0x32, 0x2a, 0x51, 0x81, 0x90, + 0x9f, 0x09, 0x80, 0xd6, 0x89, 0xde, 0x7f, 0x8e, 0x6a, 0x5c, 0x62, 0xa7, 0x77, 0xd1, + 0x75, 0x00, 0x2a, 0x13, 0x7d, 0xe8, 0x5b, 0x88, 0x88, 0x92, 0x91, 0x98, 0x11, 0x7a, + 0xa5, 0xd6, 0x19, 0x93, 0xe1, 0xdc, 0xf7, 0x58, 0x76, 0xdc, 0xa6, 0x09, 0xf9, 0xd2, + 0x84, 0x71, 0xf9, 0x97, 0xfa, 0x11, 0xf9, 0x9d, 0x42, 0x3f, 0x9c, 0xf1, 0x73, 0x4b, + 0xe8, 0xa5, 0xff, 0x99, 0x7d, 0x45, 0x1e, 0xb3, 0xcf, 0x4b, 0x3d, 0xfd, 0xd9, 0xd4, + 0x54, 0x5c, 0x35, 0xb2, 0xb5, 0xa7, 0xdc, 0x17, 0xa8, 0x36, 0xb1, 0x2b, 0x43, 0xbe, + 0xfc, 0x0b, 0xe0, 0xa1, 0xbd, 0x36, 0x97, 0x72, 0x33, 0x80, 0x78, 0xb4, 0xff, 0x7d, + 0x8e, 0x2d, 0x97, 0x9a, 0x34, 0x41, 0xe1, 0xc8, 0xf5, 0xaf, 0xe4, 0x7b, 0x1e, 0x7d, + 0xa5, 0x6c, 0xf0, 0x06, 0x02, 0xd0, 0x1b, 0x11, 0x0c, 0x05, 0xcf, 0x48, 0xfd, 0xa3, + 0xe6, 0xcc, 0xe3, 0x2a, 0x04, 0x40, 0x00, 0xf4, 0x5c, 0x6d, 0x1e, 0x69, 0x6d, 0x24, + 0x5c, 0xbd, 0x31, 0x2b, 0xdc, 0x3a, 0x3a, 0x21, 0xc9, 0x92, 0xd0, 0xeb, 0xc8, 0xcc, + 0x8f, 0xa6, 0x30, 0x6d, 0x7e, 0x13, 0x0a, 0x2b, 0xa4, 0x20, 0x18, 0xfe, 0x59, 0x69, + 0x49, 0xfd, 0x82, 0x26, 0x7b, 0xcc, 0x59, 0xdd, 0x46, 0x26, 0xef, 0xc3, 0xea, 0x74, + 0x38, 0xd0, 0x5c, 0x91, 0xb0, 0xf8, 0xe0, 0x92, 0x55, 0x0d, 0x2d, 0x39, 0xa0, 0x1e, + 0xb4, 0x5e, 0xe8, 0xf7, 0xd0, 0x9b, 0x03, 0x8d, 0x83, 0x83, 0xe1, 0x9b, 0xc3, 0x0e, + 0x64, 0x03, 0x82, 0x8c, 0xdb, 0x65, 0x2a, 0x55, 0x6b, 0x12, 0x04, 0x09, 0x31, 0x40, + 0x2a, 0xa6, 0xac, 0x34, 0xfc, 0x19, 0xfd, 0xc0, 0x6e, 0x2e, 0x77, 0x87, 0xf5, 0xb7, + 0x7b, 0x04, 0x5f, 0xd0, 0x98, 0xc0, 0x31, 0xbd, 0xbd, 0x46, 0x27, 0x76, 0x09, 0xd8, + 0x42, 0xf4, 0x84, 0x24, 0xed, 0xa3, 0x1e, 0x3c, 0xf2, 0xcd, 0xd6, 0x43, 0x85, 0xba, + 0xd3, 0x11, 0x88, 0x58, 0xd1, 0x42, 0xd9, 0x06, 0xea, 0xdb, 0x75, 0x90, 0xc9, 0x41, + 0x36, 0xda, 0x6a, 0x06, 0x35, 0x14, 0xd6, 0xa2, 0x5f, 0x7b, 0x37, 0xd7, 0x66, 0x4f, + 0x9b, 0x97, 0x09, 0x43, 0x3e, 0x6e, 0x70, 0x21, 0x18, 0xa4, 0xab, 0x9e, 0x7a, 0x7a, + 0x3e, 0x62, 0x59, 0x12, 0x99, 0x37, 0xd2, 0x9d, 0x0d, 0xb2, 0x60, 0x70, 0x52, 0x3e, + 0x8b, 0x06, 0x43, 0x13, 0x0a, 0xbe, 0xfe, 0x94, 0x3b, 0x40, 0x12, 0x98, 0xae, 0x01, + 0xa3, 0xab, 0x00, 0xab, 0xbc, 0x60, 0xd7, 0xdb, 0x93, 0x3c, 0x7f, 0x07, 0xa8, 0xbf, + 0x0f, 0x7c, 0xe1, 0x66, 0x0b, 0xcc, 0xb4, 0x5e, 0x04, 0x2b, 0x45, 0x1b, 0x93, 0x50, + 0x02, 0xce, 0xce, 0x27, 0xf3, 0x6a, 0xba, 0x56, 0x47, 0xac, 0x28, 0xd8, 0x18, 0x6c, + 0xdd, 0x1f, 0xb9, 0x5d, 0xc1, 0x35, 0xd4, 0x89, 0x92, 0xf6, 0x8d, 0xa1, 0x2a, 0xd6, + 0x1a, 0xc7, 0x56, 0x68, 0x0d, 0xd7, 0xf8, 0xd0, 0x77, 0x4a, 0xbd, 0x6c, 0xfd, 0xa2, + 0xf0, 0x32, 0xaf, 0x3b, 0xe1, 0x39, 0xa6, 0x33, 0xd6, 0x73, 0x3c, 0x75, 0xd1, 0xab, + 0xa8, 0x90, 0x18, 0xc8, 0x57, 0x2b, 0x99, 0xcd, 0x30, 0xc5, 0x37, 0x06, 0x79, 0x41, + 0xdf, 0x1c, 0x4b, 0xc1, 0xfd, 0x57, 0x0f, 0x7b, 0x4d, 0xdc, 0x97, 0x51, 0x86, 0x23, + 0xe3, 0xae, 0x4a, 0x87, 0xbd, 0xb9, 0x66, 0xc9, 0x4d, 0x86, 0x1e, 0x80, 0xde, 0x88, + 0xc2, 0x92, 0xae, 0xe9, 0x38, 0x71, 0x94, 0xe2, 0x56, 0xc6, 0x70, 0x07, 0x52, 0x30, + 0x1c, 0x73, 0xfc, 0x95, 0x65, 0xa4, 0x04, 0x80, 0xd8, 0x12, 0x6e, 0x9d, 0x08, 0x58, + 0x79, 0xe2, 0x4b, 0x16, 0xe9, 0xc4, 0x85, 0xd8, 0xf0, 0xd6, 0x18, 0xca, 0x0d, 0xd1, + 0x21, 0xb5, 0x1a, 0x7c, 0xab, 0x23, 0x0c, 0x5b, 0x45, 0x67, 0x2b, 0xdb, 0x8e, 0xa3, + 0xa0, 0x40, 0xf7, 0xaa, 0xa0, 0x98, 0xba, 0x26, 0x02, 0x5d, 0x2e, 0xab, 0x79, 0x48, + 0x69, 0x3d, 0xd5, 0xf6, 0xd3, 0x09, 0x65, 0x01, 0xe9, 0xe0, 0x71, 0x25, 0xd7, 0xeb, + 0x29, 0x3b, 0x3a, 0xba, 0xd5, 0x7f, 0xd5, 0xf0, 0x11, 0x64, 0x70, 0x2d, 0xae, 0x64, + 0xbd, 0xba, 0x8c, 0x92, 0x4f, 0xb0, 0x79, 0x96, 0x79, 0xd7, 0x7f, 0x98, 0xd3, 0x03, + 0x91, 0x9f, 0xb4, 0xa7, 0xff, 0x26, 0xa9, 0x6f, 0x13, 0x7a, 0x5e, 0x5c, 0xb9, 0x5b, + 0xc4, 0xc6, 0xff, 0x99, 0x93, 0x52, 0x6b, 0xda, 0x15, 0x03, 0x16, 0x8a, 0xb4, 0x8c, + 0xbd, 0x45, 0x15, 0x39, 0x27, 0xd3, 0x04, 0x30, 0x42, 0x3d, 0xbd, 0xf0, 0x66, 0x05, + 0xf5, 0xb5, 0x4b, 0x80, 0x8f, 0xeb, 0x22, 0xb2, 0x08, 0xb0, 0x64, 0x58, 0x18, 0x47, + 0xb2, 0xf6, 0x4c, 0xa6, 0x48, 0x37, 0x00, 0x72, 0x16, 0xde, 0x6e, 0xca, 0xff, 0xeb, + 0x4b, 0x69, 0xe6, 0x33, 0x47, 0xf8, 0x4a, 0xbc, 0xad, 0x8f, 0x2e, 0x75, 0x7d, 0x58, + 0x61, 0xce, 0x77, 0xee, 0x46, 0x51, 0x3d, 0xa7, 0x41, 0x68, 0x37, 0xdc, 0xb2, 0x3d, + 0x33, 0xea, 0x72, 0xaf, 0x23, 0xd0, 0xad, 0x8c, 0x93, 0x07, 0xd0, 0xb5, 0x85, 0x8d, + 0xa9, 0x5b, 0x77, 0xff, 0xf9, 0x02, 0x7b, 0x88, 0x59, 0xe1, 0x1d, 0xcb, 0xd5, 0x98, + 0x35, 0x0e, 0xee, 0x50, 0x93, 0x94, 0x81, 0x70, 0x8e, 0xa7, 0x08, 0xeb, 0x9f, 0x66, + 0x43, 0x88, 0xb9, 0xc6, 0x4d, 0x6a, 0xf0, 0xf9, 0x66, 0x90, 0x34, 0x24, 0x00, 0x34, + 0x8e, 0x92, 0x9e, 0x07, 0x46, 0x02, 0x53, 0xf3, 0x83, 0x90, 0xf8, 0x7b, 0xd6, 0xc0, + 0x53, 0x08, 0xc3, 0xbd, 0xe2, 0x52, 0x28, 0xe0, 0xfa, 0x08, 0x80, 0xb0, 0x8e, 0xf3, + 0x4a, 0x5a, 0x9c, 0xc0, 0xea, 0x0a, 0x67, 0xca, 0x65, 0xb6, 0xff, 0xd0, 0x05, 0x57, + 0x29, 0x09, 0xf1, 0xc4, 0x2d, 0xd7, 0x45, 0xee, 0xee, 0x9d, 0xd6, 0xb4, 0x43, 0x9c, + 0x9f, 0x3f, 0x98, 0xa1, 0x18, 0xfe, 0x16, 0x69, 0x8e, 0x9c, 0xef, 0xf5, 0x58, 0xf1, + 0x60, 0x66, 0x97, 0x5f, 0xe3, 0x95, 0x83, 0xe9, 0xb5, 0x85, 0x3b, 0x13, 0x11, 0x39, + 0x15, 0x80, 0x01, 0x9f, 0xe5, 0x5d, 0x59, 0xd1, 0xc8, 0x28, 0xd3, 0xfe, 0xb6, 0xa3, + 0xb9, 0xce, 0x92, 0xd0, 0x89, 0xae, 0x4b, 0x40, 0x8e, 0x23, 0xd6, 0xa4, 0x37, 0xd4, + 0x98, 0x9b, 0x51, 0x9b, 0x7a, 0x9e, 0xb0, 0x8a, 0xe6, 0xd4, 0x48, 0xa7, 0xa1, 0x6e, + 0x8a, 0xed, 0x26, 0xa2, 0xec, 0xd0, 0xca, 0xd8, 0x08, 0x44, 0xfd, 0x06, 0x50, 0xd8, + 0xc4, 0xe4, 0xd2, 0xaf, 0x90, 0x65, 0x67, 0x48, 0xd8, 0x09, 0x9a, 0x0c, 0x75, 0x6f, + 0xc1, 0x6c, 0xca, 0x06, 0xa3, 0x34, 0x43, 0x07, 0x02, 0xae, 0x19, 0x61, 0x66, 0x5b, + 0x48, 0x45, 0xac, 0xd1, 0xa8, 0xe3, 0x41, 0x01, 0xe6, 0x8b, 0xb6, 0x44, 0xac, 0x03, + 0x4d, 0xc6, 0x3e, 0x6e, 0x34, 0x4c, 0x3d, 0x63, 0x76, 0x2a, 0x7a, 0x5b, 0xf5, 0x9f, + 0x13, 0x09, 0x54, 0x10, 0x98, 0x1d, 0x6b, 0x6b, 0x16, 0xbc, 0xd4, 0xc9, 0xfa, 0x68, + 0xaf, 0x6e, 0x53, 0x65, 0xe9, 0x4e, 0xcb, 0xe7, 0xab, 0x8b, 0x80, 0x43, 0xdf, 0xba, + 0xcb, 0x23, 0xc8, 0x4d, 0x71, 0xa8, 0xfe, 0x5d, 0x9a, 0xc5, 0x50, 0x2c, 0xe9, 0xf7, + 0x3f, 0x40, 0x8e, 0x14, 0x37, 0x6d, 0xb8, 0x6e, 0xf5, 0x7c, 0xc3, 0x7d, 0x09, 0x89, + 0x6f, 0xa9, 0x06, 0x97, 0x2e, 0x55, 0x71, 0x80, 0xa4, 0xab, 0x5a, 0xd0, 0x9d, 0x88, + 0x46, 0xdd, 0x6d, 0xa7, 0x48, 0x76, 0x54, 0x36, 0xe0, 0x16, 0x02, 0x40, 0xf8, 0xd4, + 0x1c, 0x0a, 0xc7, 0x83, 0xf9, 0x39, 0xf2, 0xd0, 0xed, 0x26, 0x2c, 0xe8, 0x59, 0xc1, + 0x31, 0xeb, 0xc9, 0x3f, 0xf2, 0xe6, 0xe4, 0x07, 0xd4, 0xe2, 0x43, 0xe1, 0xe9, 0x31, + 0xd5, 0x3a, 0x45, 0x43, 0xb6, 0xe2, 0x6d, 0x82, 0x59, 0x6f, 0xc5, 0x3b, 0x52, 0x31, + 0x2c, 0x77, 0x6d, 0x12, 0xeb, 0x2b, 0x65, 0x9b, 0x4f, 0xb0, 0x98, 0xdf, 0x87, 0xd6, + 0x83, 0xcf, 0x9e, 0x54, 0x12, 0xee, 0x56, 0xc3, 0xfe, 0x98, 0x41, 0xd7, 0x3f, 0xd0, + 0x70, 0xdf, 0xa5, 0x1f, 0x5b, 0xaf, 0xed, 0xf2, 0x06, 0xf1, 0x3c, 0x52, 0x4e, 0x5c, + 0x50, 0xca, 0xc9, 0x90, 0x6e, 0xfa, 0x39, 0x32, 0x90, 0x04, 0x2e, 0x3b, 0xc5, 0x9f, + 0x96, 0x0b, 0x7d, 0x24, 0x0a, 0xe4, 0x43, 0xfc, 0x49, 0x26, 0x9c, 0xe0, 0x00, 0x61, + 0xe6, 0x5c, 0x6d, 0x74, 0x81, 0x2a, 0x30, 0xdd, 0x5f, 0x5f, 0xe7, 0x4e, 0xff, 0x61, + 0xe0, 0xcb, 0xab, 0x3c, 0xec, 0x75, 0xd0, 0xae, 0xf9, 0x50, 0x83, 0x18, 0x94, 0x52, + 0xdd, 0x3d, 0x9e, 0xdf, 0x44, 0x87, 0xbc, 0x73, 0x4c, 0x8b, 0x24, 0xf2, 0x12, 0x96, + 0xe4, 0xe9, 0xef, 0x11, 0x7d, 0x7f, 0xb9, 0x77, 0xe3, 0xb0, 0xe6, 0x40, 0x6e, 0x63, + 0x08, 0x59, 0x06, 0x33, 0x1a, 0x93, 0x03, 0x3d, 0x1c, 0xb8, 0x36, 0x0f, 0xe6, 0xfe, + 0xa6, 0x1a, 0x68, 0x26, 0xdf, 0x36, 0x25, 0x57, 0x89, 0xf9, 0x2e, 0x40, 0xba, 0xfc, + 0xb2, 0xeb, 0xcb, 0x9e, 0x55, 0x6f, 0x6c, 0x0c, 0xca, 0xdc, 0x6a, 0xf0, 0x8e, 0x31, + 0xec, 0x4a, 0xd5, 0x28, 0x80, 0x34, 0xe1, 0x6d, 0x15, 0x5c, 0xfd, 0xca, 0xda, 0x7b, + 0xab, 0x59, 0x9c, 0x2f, 0xa4, 0xad, 0x2e, 0x62, 0x93, 0xf9, 0xfe, 0x09, 0x71, 0x69, + 0x14, 0x82, 0x76, 0xb6, 0xa9, 0xea, 0xa7, 0x2f, 0x14, 0x8b, 0x0c, 0x95, 0x65, 0xc3, + 0xc2, 0xdd, 0x63, 0x12, 0x5e, 0x0f, 0xa5, 0x30, 0x86, 0x1a, 0x71, 0x0d, 0xf8, 0xe4, + 0x81, 0xf2, 0x71, 0x29, 0x20, 0xf8, 0x78, 0x7e, 0x0a, 0xed, 0xfe, 0x61, 0x8a, 0xff, + 0x50, 0xa3, 0xb5, 0x62, 0x13, 0x88, 0x4d, 0x62, 0x62, 0xc1, 0x1d, 0xeb, 0xf2, 0xba, + 0x7e, 0x8a, 0xd6, 0x69, 0x2c, 0xb1, 0x70, 0x78, 0x33, 0x14, 0x18, 0xda, 0x4b, 0xe0, + 0x64, 0xff, 0x52, 0x70, 0x07, 0x39, 0x34, 0xab, 0xcd, 0x2a, 0xb0, 0x46, 0x9e, 0xca, + 0xf7, 0x27, 0x5b, 0x4b, 0xd7, 0x2b, 0xc6, 0xed, 0x34, 0x47, 0x8e, 0xa4, 0x08, 0x9b, + 0x73, 0x6a, 0x16, 0xdd, 0x90, 0x6d, 0x49, 0xf2, 0x5c, 0x33, 0x82, 0x7c, 0x57, 0x1c, + 0xe0, 0xb5, 0xd7, 0x21, 0x77, 0xaa, 0x35, 0x08, 0x80, 0x4b, 0xc0, 0xf8, 0xfa, 0xa9, + 0x47, 0x12, 0x22, 0x31, 0x40, 0x2d, 0x2f, 0x5c, 0xc9, 0xa0, 0xeb, 0x0e, 0x09, 0xd4, + 0x27, 0xb4, 0x27, 0x28, 0x8d, 0x93, 0x7d, 0x9d, 0x72, 0xb7, 0x74, 0x56, 0xf8, 0x86, + 0x59, 0x4c, 0xd8, 0xc6, 0xa4, 0x62, 0xf7, 0x7f, 0xd8, 0x30, 0x76, 0x46, 0x9c, 0xc0, + 0xec, 0xba, 0x3c, 0xc4, 0x0c, 0xad, 0x69, 0xe5, 0xb5, 0x41, 0x12, 0xea, 0xb3, 0x33, + 0x96, 0xae, 0xcf, 0xbc, 0x21, 0x1f, 0x1f, 0x79, 0xcf, 0x33, 0x10, 0x8e, 0x93, 0xd9, + 0x53, 0x78, 0xba, 0xe6, 0x95, 0x82, 0x74, 0xb3, 0x10, 0x88, 0xfb, 0xd8, 0xb3, 0xa3, + 0xa0, 0xd1, 0x54, 0xa7, 0x89, 0x73, 0x5b, 0x03, 0x49, 0xc4, 0xd5, 0x1c, 0x88, 0x9d, + 0x08, 0x95, 0x2d, 0xdd, 0x54, 0x88, 0xbe, 0x95, 0x56, 0x05, 0x94, 0xe6, 0x73, 0xfa, + 0x05, 0x1b, 0xf9, 0xb6, 0x14, 0xa1, 0x5e, 0x10, 0x0b, 0x60, 0xa0, 0xfe, 0x9a, 0x7e, + 0x12, 0xa9, 0xb2, 0x56, 0xdf, 0x58, 0x9b, 0x3e, 0x48, 0xe5, 0xb8, 0x0f, 0xb8, 0xcf, + 0xf0, 0x3e, 0x86, 0xf6, 0x0c, 0xc0, 0x70, 0xfb, 0x23, 0xc9, 0x7d, 0x4c, 0x14, 0xfa, + 0x3a, 0x73, 0x46, 0xff, 0x55, 0x6b, 0xc6, 0x85, 0x5a, 0x5f, 0x83, 0xe3, 0xdc, 0xd9, + 0xf6, 0xea, 0xb3, 0xda, 0xbc, 0xd4, 0x77, 0x50, 0xe3, 0x4e, 0x7c, 0x09, 0x38, 0xf6, + 0x4d, 0x45, 0x1e, 0x39, 0x50, 0x9e, 0x90, 0x27, 0x47, 0xa7, 0x07, 0x55, 0x12, 0x20, + 0x95, 0x08, 0x2a, 0xb7, 0x98, 0x59, 0x19, 0x07, 0x31, 0x41, 0xb6, 0xd3, 0x70, 0x20, + 0x91, 0xab, 0x71, 0x72, 0x80, 0xbd, 0xc5, 0x5e, 0x79, 0x9c, 0x01, 0xad, 0x86, 0x41, + 0x90, 0x4e, 0x3b, 0x1d, 0xd2, 0x9e, 0x1a, 0x96, 0x4c, 0x73, 0x7d, 0x3c, 0x15, 0x5a, + 0xfb, 0x30, 0x7b, 0x74, 0x8e, 0x41, 0x12, 0xb4, 0x8b, 0x77, 0xd5, 0xed, 0x57, 0x00, + 0xe6, 0x00, 0x2b, 0x18, 0xb0, 0xfe, 0xd2, 0xcf, 0xfd, 0xf6, 0x1f, 0xd9, 0x93, 0x4b, + 0x60, 0x73, 0x2f, 0x4d, 0x37, 0x81, 0x0a, 0x91, 0xac, 0xef, 0x1e, 0x03, 0x8b, 0x81, + 0xd7, 0x36, 0xd9, 0x8e, 0xad, 0xa9, 0xcd, 0x7e, 0x0c, 0x2b, 0xe2, 0x7a, 0xb8, 0x50, + 0x32, 0x06, 0x60, 0x91, 0x22, 0x4e, 0xdf, 0x87, 0x2f, 0x79, 0x63, 0x7d, 0xda, 0x39, + 0x16, 0x79, 0x6a, 0x5c, 0x62, 0xf5, 0x7f, 0x1d, 0xe3, 0x76, 0x78, 0xb6, 0xde, 0xa0, + 0x08, 0x69, 0x93, 0x36, 0x74, 0xf8, 0x8e, 0x41, 0xa9, 0x18, 0x08, 0x07, 0x3b, 0x0f, + 0x43, 0x6e, 0xbe, 0x25, 0xa5, 0xf4, 0x4a, 0x60, 0x10, 0x33, 0xe2, 0x18, 0x4b, 0x88, + 0xdb, 0x79, 0xe9, 0x68, 0xca, 0x6d, 0x89, 0xb7, 0x49, 0x01, 0xbe, 0x6c, 0x6d, 0xb3, + 0x63, 0x65, 0x80, 0x18, 0x2e, 0x65, 0x8d, 0xfc, 0x68, 0x67, 0x67, 0xd6, 0xd8, 0x19, + 0xfa, 0x92, 0x3e, 0x0c, 0xdf, 0x3e, 0xa3, 0x65, 0x76, 0xf8, 0x52, 0xbc, 0xd4, 0xe1, + 0x96, 0xa7, 0x1a, 0x13, 0x29, 0xf6, 0xc3, 0xff, 0x8e, 0x42, 0xe3, 0x09, 0x5a, 0xbd, + 0x8e, 0xc1, 0x97, 0x99, 0x07, 0x13, 0xee, 0x89, 0x39, 0x4c, 0x57, 0x19, 0xb2, 0x76, + 0xde, 0x8f, 0x81, 0x8a, 0x34, 0xa7, 0xbe, 0xc1, 0xf2, 0x68, 0x68, 0x2e, 0x91, 0x42, + 0xc7, 0xd3, 0x87, 0x89, 0xf6, 0x76, 0xcc, 0x12, 0xb7, 0x1a, 0xb6, 0x66, 0x35, 0xc5, + 0x02, 0xe6, 0x9d, 0x05, 0xb9, 0xc7, 0xef, 0x01, 0x52, 0x97, 0x75, 0xc6, 0x23, 0xa4, + 0x8e, 0x4c, 0xc5, 0xc4, 0x15, 0xc9, 0xfd, 0x56, 0x53, 0x65, 0xa4, 0x16, 0x37, 0x68, + 0x78, 0x51, 0x53, 0x88, 0x7f, 0xb5, 0xf9, 0x63, 0xe7, 0xac, 0xc1, 0x62, 0xf2, 0x80, + 0x5f, 0x45, 0xf4, 0x44, 0x87, 0xf8, 0x5e, 0x19, 0x9c, 0x1d, 0xf4, 0xa0, 0xfc, 0xa4, + 0xd4, 0x4b, 0xaa, 0x62, 0xda, 0x7a, 0xf5, 0xed, 0x69, 0x68, 0x41, 0x12, 0xd3, 0x5f, + 0x36, 0x73, 0x73, 0x2f, 0x5a, 0x1a, 0xc3, 0xe4, 0xf0, 0x21, 0xba, 0x5c, 0x2c, 0x32, + 0xf0, 0x6e, 0x6b, 0x90, 0xfa, 0xe2, 0xd2, 0x54, 0xcf, 0x09, 0xe7, 0x69, 0x0c, 0xf4, + 0xe3, 0xaa, 0x70, 0x30, 0x98, 0x74, 0x48, 0xe1, 0x47, 0xf9, 0x43, 0xba, 0xb5, 0xca, + 0xb5, 0x58, 0x02, 0x9a, 0x36, 0x02, 0x4d, 0x2e, 0x79, 0x0f, 0xc6, 0xfd, 0x66, 0x7f, + 0x17, 0x6e, 0x0a, 0xa9, 0x9d, 0xd1, 0xd7, 0x2b, 0x57, 0x36, 0x8f, 0x01, 0xb6, 0x6c, + 0x4a, 0x96, 0xc1, 0x56, 0xf3, 0xf2, 0x85, 0x41, 0xab, 0x4c, 0xa4, 0x96, 0x69, 0x60, + 0x21, 0x82, 0x08, 0x46, 0x69, 0x61, 0x12, 0x94, 0x90, 0xa7, 0xd8, 0xb6, 0x5c, 0x14, + 0x70, 0xba, 0xd8, 0xdb, 0x08, 0x28, 0xef, 0x06, 0xc1, 0xcb, 0x55, 0x70, 0x0e, 0x85, + 0xe2, 0x4f, 0xde, 0xa9, 0x4e, 0xa2, 0xb0, 0x6e, 0x8d, 0x8a, 0x89, 0xfc, 0x91, 0x87, + 0x1f, 0x88, 0xfb, 0x1a, 0xbd, 0xcd, 0x72, 0x1e, 0xff, 0xf1, 0x2e, 0xf9, 0xd4, 0xf5, + 0xb0, 0x45, 0x85, 0x19, 0x7c, 0x3b, 0x3c, 0xc8, 0xe8, 0x57, 0xd8, 0x1f, 0x21, 0xef, + 0x88, 0x1f, 0xed, 0x53, 0x3c, 0x92, 0xcf, 0x4c, 0xb0, 0xe1, 0x8f, 0xe7, 0xd3, 0x4e, + 0x99, 0x7c, 0x64, 0x92, 0x88, 0x4f, 0xe5, 0x6a, 0x8b, 0x91, 0x08, 0x98, 0x0d, 0x45, + 0x3c, 0xb8, 0xa6, 0x6e, 0xa0, 0xa0, 0x15, 0x35, 0x50, 0x06, 0x0a, 0xcb, 0x04, 0x3a, + 0x40, 0xed, 0x6f, 0x92, 0x9d, 0x3e, 0x0d, 0xa1, 0x64, 0xb2, 0x36, 0x19, 0xaf, 0x1d, + 0xe4, 0x56, 0xfd, 0xd0, 0x37, 0xbf, 0x1e, 0xa7, 0xfa, 0xb2, 0x9a, 0x67, 0x61, 0xef, + 0x4d, 0xed, 0xc8, 0x6c, 0x2f, 0x17, 0x62, 0xad, 0x64, 0x48, 0x4c, 0x08, 0xff, 0xea, + 0x77, 0x5a, 0x90, 0x4d, 0xec, 0x82, 0x7f, 0xd8, 0x7a, 0x18, 0x86, 0x0d, 0x6e, 0x8a, + 0x4a, 0x52, 0xb5, 0xcf, 0x44, 0xbe, 0x28, 0xa6, 0x2d, 0x41, 0x59, 0x02, 0x09, 0x3a, + 0x0c, 0x36, 0x5d, 0x29, 0x9e, 0xde, 0xba, 0x53, 0x13, 0x6c, 0x62, 0x6e, 0x16, 0x0a, + 0xcb, 0x00, 0x44, 0xce, 0x6f, 0x2b, 0xb8, 0xdd, 0xe1, 0xfd, 0xda, 0x5b, 0x47, 0x4d, + 0x5b, 0x35, 0x07, 0x47, 0x4e, 0x3d, 0x52, 0x77, 0x24, 0x12, 0x01, 0xb8, 0x26, 0x1a, + 0x49, 0xd4, 0x91, 0xaf, 0x04, 0x9b, 0x39, 0xe2, 0x6d, 0x13, 0x57, 0xc3, 0x06, 0x92, + 0x64, 0x16, 0x77, 0x6d, 0x7d, 0x13, 0xf8, 0x40, 0xbd, 0x82, 0xac, 0xa0, 0x1c, 0x83, + 0x1c, 0x98, 0x3f, 0x19, 0x85, 0xee, 0x0a, 0xda, 0xe8, 0xdb, 0x84, 0x47, 0xc0, 0xe5, + 0x1c, 0x09, 0xdf, 0xe3, 0xde, 0xe3, 0x88, 0x0a, 0x97, 0x13, 0xce, 0xb7, 0x45, 0xab, + 0xfd, 0xd9, 0xf1, 0xc7, 0xea, 0xd7, 0x63, 0x08, 0xcd, 0xee, 0xa2, 0x1c, 0x8b, 0x09, + 0x57, 0x02, 0x7c, 0x5d, 0x00, 0xe5, 0x0a, 0x43, 0x88, 0xc7, 0xaf, 0x2b, 0xd6, 0x43, + 0xcb, 0x5e, 0xae, 0x49, 0x27, 0x4d, 0x12, 0x30, 0xa4, 0xcd, 0x49, 0x23, 0x7a, 0xe3, + 0x7b, 0x38, 0x10, 0xc2, 0xc3, 0x95, 0x8a, 0x7d, 0xee, 0x02, 0x34, 0x30, 0x1b, 0x89, + 0xa2, 0xdf, 0x2a, 0x78, 0xef, 0x0b, 0xfb, 0x4b, 0xf6, 0xb3, 0x87, 0xdf, 0x2c, 0x6c, + 0x86, 0xe6, 0x1c, 0xd1, 0x0c, 0xa1, 0x1f, 0x81, 0x13, 0x01, 0x26, 0x07, 0xf1, 0x5b, + 0x28, 0x56, 0x24, 0x0f, 0xdc, 0x52, 0x06, 0x5a, 0x10, 0x28, 0xc8, 0xa2, 0xdd, 0xfd, + 0xd1, 0x5c, 0xf5, 0x26, 0x5f, 0x87, 0x38, 0x8a, 0xb9, 0xbf, 0x21, 0xc9, 0xa7, 0x8c, + 0x59, 0x03, 0x8a, 0x98, 0xab, 0x64, 0xfd, 0x67, 0x10, 0x77, 0xd4, 0x72, 0xc2, 0x09, + 0xdd, 0x72, 0x9b, 0xd7, 0xf8, 0x48, 0x09, 0x45, 0xfb, 0xa7, 0x52, 0x09, 0x8a, 0x94, + 0xcc, 0xb2, 0x4c, 0xf3, 0xbc, 0x09, 0x2d, 0x42, 0x36, 0x46, 0x11, 0xa2, 0x93, 0xaf, + 0xf3, 0xc5, 0x79, 0x37, 0x2c, 0x12, 0xe1, 0x50, 0x90, 0xaa, 0x27, 0x23, 0x20, 0x57, + 0xf2, 0xed, 0xde, 0x4e, 0x1d, 0xb2, 0x92, 0xf7, 0xb1, 0x86, 0x47, 0x22, 0x67, 0x35, + 0x17, 0x6d, 0x90, 0xf1, 0x26, 0x5b, 0x37, 0x98, 0xcc, 0xab, 0xac, 0x0b, 0x8d, 0x79, + 0xb1, 0x77, 0x20, 0xb2, 0xba, 0x71, 0xd7, 0x85, 0x0c, 0xc2, 0xa0, 0x87, 0x2b, 0xf0, + 0xf4, 0xb8, 0x14, 0x36, 0x78, 0x59, 0xf8, 0x99, 0x48, 0xf0, 0xa1, 0xa3, 0x83, 0x60, + 0x4b, 0x9e, 0xf0, 0x7e, 0xa9, 0x3d, 0xbb, 0x98, 0x71, 0xc0, 0x09, 0xaa, 0x6a, 0x31, + 0xd8, 0xea, 0xf1, 0x43, 0x0b, 0x7b, 0xc0, 0xac, 0x26, 0x4e, 0x2f, 0x97, 0x6a, 0xd3, + 0x97, 0xf2, 0x7f, 0x48, 0x37, 0x8f, 0x8a, 0x4e, 0xd9, 0x02, 0xc6, 0x6e, 0x49, 0x18, + 0xfa, 0xee, 0x8d, 0xc0, 0x06, 0x72, 0x46, 0x96, 0x0d, 0xb1, 0xf8, 0xcd, 0x07, 0xbf, + 0x90, 0xd7, 0x53, 0x7c, 0xc2, 0x7b, 0xbb, 0x8c, 0x9d, 0x5b, 0x29, 0x62, 0xc4, 0x7e, + 0xd1, 0x82, 0xa2, 0xfc, 0xe0, 0x5f, 0x8e, 0x03, 0xc4, 0xe2, 0x5e, 0x49, 0x6d, 0xd5, + 0x7d, 0x6a, 0xb3, 0x45, 0x8f, 0xac, 0xbd, 0x91, 0xea, 0x22, 0x72, 0xff, 0xda, 0x47, + 0xb0, 0x73, 0x59, 0x5e, 0x78, 0xdd, 0x84, 0xb7, 0x1f, 0xf8, 0x8b, 0x74, 0x21, 0x02, + 0x88, 0xf0, 0xea, 0xf8, 0xe7, 0x1a, 0xeb, 0xa4, 0x4c, 0x5e, 0xc3, 0x82, 0xe3, 0x59, + 0x33, 0xe1, 0x7b, 0xa7, 0xef, 0xd6, 0x64, 0x90, 0xf6, 0x72, 0x03, 0x2d, 0x4e, 0xbc, + 0xf7, 0xcd, 0x55, 0x7a, 0xe0, 0xdb, 0xb7, 0x25, 0x00, 0x4e, 0xcb, 0x05, 0x7a, 0x5a, + 0x2b, 0x15, 0x7a, 0x1a, 0xbf, 0xb9, 0x83, 0x87, 0x08, 0xba, 0x28, 0xe7, 0xea, 0xa2, + 0x12, 0xa9, 0x04, 0x22, 0xc1, 0x27, 0x17, 0x53, 0xb9, 0xf3, 0x0f, 0x8f, 0xf8, 0xe5, + 0x33, 0xa9, 0x93, 0xf0, 0x69, 0xbd, 0x82, 0x2b, 0xf7, 0x24, 0xd1, 0xb7, 0x38, 0xc7, + 0x3d, 0x4b, 0x46, 0xe9, 0x90, 0x28, 0xde, 0x1e, 0xaa, 0xdf, 0x9a, 0xb0, 0x89, 0xdd, + 0x46, 0x6c, 0xa1, 0x85, 0xa8, 0x0a, 0xfc, 0xfd, 0x44, 0x68, 0x5c, 0xf8, 0xec, 0xe5, + 0x58, 0xd7, 0xbf, 0xd0, 0x17, 0x39, 0x20, 0xd7, 0x17, 0x51, 0x30, 0xf0, 0xe4, 0xd0, + 0x93, 0x74, 0x41, 0xbc, 0xe9, 0x8c, 0xfa, 0x5b, 0x33, 0x3b, 0x66, 0x19, 0x0f, 0x2b, + 0x44, 0x71, 0x38, 0xe8, 0xc2, 0x6d, 0x84, 0x12, 0xca, 0xc8, 0x20, 0x86, 0xd6, 0x1b, + 0x5d, 0x2c, 0x8c, 0xf0, 0xbb, 0xeb, 0xac, 0x5b, 0x89, 0xbf, 0xe8, 0x2b, 0x58, 0x91, + 0x76, 0x64, 0xba, 0xb9, 0x1c, 0xe2, 0xec, 0xe2, 0x90, 0xb2, 0x7b, 0x60, 0x52, 0xd4, + 0xbf, 0x99, 0x1a, 0x33, 0xf4, 0x58, 0x1a, 0x63, 0x36, 0x25, 0x78, 0x79, 0x58, 0x89, + 0x7f, 0xca, 0x4b, 0x98, 0xb7, 0xe7, 0x27, 0x7c, 0x5e, 0x6a, 0x1d, 0x88, 0x59, 0x48, + 0xc9, 0xd4, 0x84, 0xdd, 0x0c, 0xef, 0xef, 0x85, 0x4e, 0x81, 0x76, 0xc3, 0x97, 0xdc, + 0xfa, 0x77, 0x2e, 0x71, 0x14, 0x72, 0xe7, 0x90, 0xba, 0x8d, 0x39, 0x35, 0xd5, 0x7c, + 0xa3, 0x13, 0x49, 0x37, 0x9e, 0x62, 0x83, 0xa6, 0xaa, 0x8f, 0xc9, 0x91, 0xef, 0xc7, + 0xd3, 0xb7, 0xef, 0x66, 0xb9, 0x2f, 0xe0, 0x9d, 0x35, 0x16, 0x27, 0x0a, 0xe1, 0x9a, + 0x99, 0x92, 0x16, 0xee, 0xae, 0x16, 0x21, 0x44, 0xac, 0xea, 0x56, 0x0d, 0x17, 0x72, + 0x05, 0xf2, 0x6c, 0x97, 0x03, 0xb5, 0x4e, 0x80, 0xaf, 0x1a, 0x87, 0x94, 0xd6, 0xd3, + 0xf1, 0xc5, 0xee, 0xad, 0x22, 0x0b, 0x11, 0x9f, 0x06, 0xb2, 0x00, 0x98, 0x6c, 0x91, + 0x21, 0x32, 0xcb, 0x08, 0xa9, 0x8e, 0x0f, 0xee, 0x35, 0xe7, 0xf7, 0x7f, 0xc8, 0x52, + 0x1d, 0x38, 0x77, 0x3e, 0x61, 0x4e, 0xee, 0xb8, 0xa3, 0xea, 0xd8, 0x6a, 0x02, 0x48, + 0x32, 0xe6, 0x4a, 0x4c, 0x75, 0x72, 0x0c, 0xdc, 0xdd, 0xf9, 0xd0, 0x77, 0x09, 0xa1, + 0x68, 0xd0, 0x10, 0x12, 0xc2, 0xe4, 0xf3, 0x34, 0x30, 0xf2, 0x99, 0x70, 0xc6, 0x0b, + 0xe8, 0xc5, 0xe2, 0xc8, 0xcc, 0x8a, 0x86, 0xed, 0xcd, 0x51, 0x2d, 0xa7, 0x0d, 0xd7, + 0xbb, 0x40, 0xe2, 0x7b, 0x32, 0xdf, 0x3d, 0x77, 0x6a, 0x4a, 0x7b, 0x00, 0xe3, 0xbd, + 0x8f, 0x69, 0x7f, 0x1f, 0x4e, 0x5c, 0x9f, 0xbe, 0xbe, 0xb4, 0x46, 0xb0, 0x25, 0xfd, + 0x80, 0x65, 0xb1, 0x86, 0xae, 0xdc, 0x75, 0xf5, 0x68, 0x87, 0x2c, 0x16, 0xfa, 0xf5, + 0xe5, 0xa3, 0x47, 0x4d, 0x8a, 0x9d, 0x45, 0x54, 0x8f, 0xac, 0xb7, 0x46, 0x9a, 0xcb, + 0x2d, 0xa1, 0x0b, 0x70, 0x78, 0x25, 0x9c, 0x50, 0x7c, 0x4d, 0xeb, 0xe4, 0x50, 0x8e, + 0x0c, 0xee, 0x4f, 0xbc, 0xb0, 0xd1, 0x3b, 0xf6, 0x24, 0x37, 0xdc, 0xf0, 0x5a, 0x63, + 0x13, 0x45, 0xef, 0xbe, 0x0d, 0x7b, 0xb9, 0x01, 0x61, 0x66, 0x55, 0x4f, 0xf3, 0x8a, + 0x1d, 0x77, 0xf2, 0xfd, 0xa4, 0xe7, 0xeb, 0xa7, 0xa7, 0x8a, 0xb3, 0x1f, 0x38, 0x29, + 0x42, 0x52, 0xa2, 0xb1, 0x0f, 0xd2, 0x86, 0x5b, 0x57, 0x05, 0x05, 0x5d, 0xfe, 0x9b, + 0x3e, 0x9e, 0x8f, 0x7a, 0xd5, 0xf4, 0x00, 0x7d, 0xbe, 0x42, 0x2b, 0x3a, 0xa0, 0xbe, + 0xb9, 0xd1, 0xc8, 0x9d, 0x37, 0x46, 0x08, 0x54, 0xff, 0x6e, 0x5f, 0x03, 0xe5, 0xff, + 0x3d, 0x4f, 0x18, 0x48, 0xf4, 0xcc, 0x64, 0x21, 0x8a, 0x01, 0xf2, 0x47, 0x2b, 0xb0, + 0x55, 0x80, 0x2f, 0x97, 0xf3, 0x20, 0x41, 0xa7, 0x92, 0x79, 0x0b, 0x7c, 0x22, 0x6b, + 0x04, 0xa6, 0xea, 0xe8, 0x5f, 0x1b, 0x71, 0xca, 0x19, 0xa1, 0x71, 0x89, 0x02, 0xb4, + 0xc3, 0xa3, 0xb5, 0x06, 0xd8, 0xc1, 0xb7, 0xae, 0x72, 0x8c, 0x9b, 0x6c, 0xc3, 0x17, + 0xe5, 0xe0, 0xde, 0xe5, 0x33, 0xe2, 0xe9, 0x99, 0x73, 0xd8, 0x83, 0xa4, 0x0c, 0x6e, + 0x68, 0xf2, 0x31, 0xd2, 0xcb, 0x01, 0x2f, 0x60, 0xc1, 0x43, 0xcc, 0xab, 0xdd, 0x40, + 0x45, 0x59, 0x0d, 0x9e, 0x43, 0xfb, 0xa3, 0x6f, 0xe4, 0xcf, 0xd9, 0x7b, 0x4b, 0xdd, + 0x0c, 0x4d, 0x2c, 0x93, 0xc5, 0x72, 0x8b, 0x12, 0x87, 0xfd, 0x25, 0x41, 0x72, 0x2c, + 0x69, 0x9b, 0xc1, 0xa0, 0x05, 0x83, 0xdb, 0xc9, 0x48, 0xd5, 0x32, 0x4a, 0xc5, 0xbd, + 0x7a, 0x68, 0x09, 0x64, 0x67, 0x3e, 0xdf, 0x2c, 0x6d, 0xeb, 0xb1, 0xc8, 0xe1, 0xd0, + 0x24, 0x16, 0xe6, 0xbd, 0xb2, 0xa7, 0x68, 0x1b, 0xf4, 0x29, 0x92, 0x25, 0xc2, 0x1b, + 0x5d, 0xb6, 0xa8, 0x45, 0xad, 0x10, 0x4d, 0x34, 0x29, 0xcd, 0xc5, 0x9e, 0x3b, 0xca, + 0xcf, 0x6d, 0xbc, 0x88, 0xaf, 0x0f, 0x67, 0xdc, 0xbd, 0xf3, 0xa0, 0x72, 0x3e, 0x4d, + 0x4b, 0xce, 0x32, 0x85, 0x1b, 0xb5, 0x19, 0x7a, 0x8f, 0x43, 0x30, 0xb2, 0x72, 0x27, + 0xf0, 0xb7, 0x71, 0xd0, 0xaf, 0x17, 0x5e, 0x9c, 0x3f, 0x6e, 0x1f, 0x68, 0x46, 0x2e, + 0xe7, 0xfe, 0x17, 0x97, 0xd9, 0x28, 0x40, 0x6f, 0x92, 0x38, 0xa3, 0xf3, 0xfd, 0x83, + 0x6a, 0x27, 0x56, 0xdd, 0x0a, 0x11, 0xe1, 0xab, 0x94, 0x9d, 0x5e, 0x30, 0x89, 0x4f, + 0x56, 0x29, 0x95, 0x25, 0xe6, 0x5d, 0x95, 0x0f, 0x2e, 0xb5, 0x0b, 0x3a, 0x8e, 0xa7, + 0xac, 0xad, 0xbc, 0x3c, 0x77, 0xeb, 0x53, 0xe7, 0xde, 0x9b, 0xa8, 0x2f, 0x7d, 0xd5, + 0xf6, 0x13, 0xcd, 0xa6, 0x29, 0xfc, 0xd2, 0xf6, 0x36, 0x6b, 0x2e, 0x1e, 0xc2, 0x40, + 0xd4, 0x82, 0xc3, 0xa6, 0xf9, 0xd9, 0x8d, 0xab, 0x1c, 0x86, 0x4c, 0x00, 0xb8, 0xfd, + 0x36, 0x46, 0xf0, 0xd5, 0x96, 0xfe, 0x18, 0x0f, 0x70, 0xb1, 0x94, 0x84, 0x25, 0x63, + 0xe9, 0xf3, 0xf4, 0xdc, 0xf5, 0x2b, 0x89, 0x3a, 0x70, 0x9e, 0x1d, 0xd4, 0xa7, 0xca, + 0x1c, 0x49, 0xec, 0x81, 0x4e, 0x8f, 0xe6, 0xe0, 0xe0, 0xde, 0x54, 0x6a, 0x4f, 0xbe, + 0x7d, 0x25, 0x67, 0x0b, 0x2f, 0xc6, 0x8a, 0x8f, 0xb2, 0xc4, 0xa6, 0x3d, 0xef, 0xec, + 0x6f, 0xe0, 0x1d, 0x8c, 0xe0, 0xf5, 0x1d, 0x3c, 0x65, 0xa4, 0x28, 0x90, 0x97, 0x5f, + 0xa1, 0xed, 0xed, 0x70, 0x56, 0x20, 0xdf, 0xcd, 0x1d, 0x0c, 0xde, 0xad, 0x2a, 0xbf, + 0xa6, 0xdf, 0xe2, 0x6d, 0x79, 0xc9, 0x0c, 0x63, 0xff, 0x96, 0xe5, 0x40, 0xb7, 0x61, + 0x5d, 0x43, 0xa6, 0x26, 0x1d, 0x57, 0x73, 0x03, 0x06, 0xb6, 0x63, 0x2c, 0x8e, 0xe6, + 0x1b, 0xaa, 0x4a, 0xb4, 0xd3, 0x08, 0x4d, 0x65, 0x9c, 0xab, 0xcf, 0xc4, 0x06, 0x4c, + 0x09, 0xd2, 0x42, 0x69, 0xb3, 0x03, 0x17, 0x10, 0xb6, 0x7d, 0x3b, 0x0b, 0x73, 0x6f, + 0xac, 0xbc, 0x18, 0x1e, 0xb1, 0xdc, 0x8c, 0x49, 0x3f, 0x10, 0xdb, 0xe6, 0xfe, 0x45, + 0xfd, 0xd4, 0xab, 0x60, 0x22, 0xfa, 0xbd, 0xd3, 0x4c, 0x09, 0xf7, 0x51, 0x04, 0xc3, + 0x85, 0xc9, 0x26, 0x83, 0x41, 0xc1, 0x6e, 0xbe, 0x80, 0xf8, 0xc8, 0x0e, 0x8e, 0x06, + 0x23, 0x06, 0x03, 0x99, 0x5a, 0xde, 0x55, 0x61, 0xfe, 0xd4, 0x5c, 0xf8, 0xd1, 0x14, + 0xd4, 0xcf, 0x02, 0x42, 0x0c, 0x4b, 0x96, 0x2d, 0xc2, 0x02, 0xf8, 0xa5, 0x07, 0xf3, + 0xd8, 0xe8, 0xa3, 0x44, 0xfb, 0xa1, 0x0a, 0x32, 0x7f, 0xf2, 0x22, 0x54, 0xf6, 0xc3, + 0xac, 0x8f, 0x3c, 0xf9, 0x70, 0x0b, 0x1f, 0xd2, 0xec, 0xbe, 0x9f, 0x4e, 0x91, 0xe4, + 0x3a, 0x65, 0x4f, 0xff, 0x02, 0x7c, 0xd9, 0x17, 0x4b, 0x63, 0x8e, 0x6e, 0xfe, 0xc4, + 0xab, 0xfb, 0xa1, 0x87, 0xf8, 0xf3, 0xdb, 0xa0, 0x45, 0x9d, 0xa6, 0xc3, 0xf8, 0x00, + 0xcb, 0x6b, 0x61, 0x33, 0xa8, 0xb4, 0xac, 0x1e, 0xf6, 0x58, 0xd1, 0x11, 0xc0, 0x3f, + 0x07, 0x22, 0x08, 0xdc, 0xc2, 0x07, 0xa2, 0x22, 0x3a, 0x70, 0x22, 0x92, 0x43, 0x2e, + 0x83, 0x06, 0xfc, 0x03, 0x04, 0x63, 0xe7, 0x54, 0xff, 0x0f, 0x15, 0x3d, 0x97, 0xbc, + 0x9c, 0xe9, 0x6d, 0xff, 0x4b, 0xed, 0x2f, 0x1e, 0xa5, 0xb8, 0xea, 0x87, 0x6d, 0x2e, + 0xe4, 0xe4, 0xf6, 0xe4, 0x9a, 0x4a, 0x85, 0xa9, 0xcf, 0x4a, 0x33, 0xdc, 0xd9, 0x36, + 0x60, 0xa4, 0x25, 0x43, 0xe5, 0x34, 0x22, 0x39, 0x0d, 0x66, 0x5b, 0xdd, 0x30, 0x24, + 0x78, 0xb3, 0x3c, 0x8d, 0x57, 0x47, 0x92, 0x41, 0x4c, 0x5f, 0xe5, 0xb7, 0x4f, 0xe1, + 0xd1, 0x69, 0x52, 0x5c, 0x99, 0x30, 0x1a, 0x3a, 0x68, 0xa0, 0xc8, 0x5f, 0x99, 0x08, + 0xed, 0x24, 0x25, 0x51, 0x5d, 0x45, 0xca, 0xe5, 0xca, 0xe7, 0xce, 0x0e, 0x98, 0xb5, + 0x82, 0x9e, 0xd6, 0x96, 0xbe, 0x2c, 0x3d, 0xb4, 0x59, 0xe0, 0xad, 0x5b, 0x5d, 0xf7, + 0x4a, 0xa1, 0x7b, 0x43, 0x44, 0x65, 0x42, 0xaf, 0x17, 0x84, 0x40, 0x1e, 0xfe, 0xc9, + 0xf1, 0x25, 0x6d, 0xaf, 0x71, 0x91, 0x59, 0xd8, 0xa1, 0x83, 0x3f, 0xc0, 0x5c, 0xdb, + 0x01, 0xf6, 0x88, 0xef, 0x49, 0x81, 0xc7, 0x4a, 0x7f, 0xf4, 0x3d, 0xe3, 0x55, 0xc3, + 0xc4, 0x66, 0x1c, 0x36, 0xfa, 0x24, 0xec, 0x10, 0x99, 0xa8, 0xad, 0xf4, 0xe3, 0x11, + 0x48, 0x78, 0x20, 0xb5, 0xa7, 0x76, 0xea, 0x06, 0x42, 0xef, 0x8e, 0xf1, 0xe2, 0x87, + 0x82, 0x76, 0x7d, 0x9d, 0xe5, 0x7d, 0xea, 0xde, 0xad, 0xcb, 0x4a, 0xf5, 0x19, 0x3e, + 0x09, 0xc9, 0xbb, 0x74, 0x73, 0x77, 0x3a, 0x8c, 0xa5, 0x6d, 0x76, 0x51, 0x1d, 0x65, + 0x99, 0x20, 0xdb, 0x99, 0x64, 0xd3, 0x2b, 0xad, 0xb6, 0x1f, 0x4c, 0xf6, 0xb0, 0x22, + 0xd7, 0xc1, 0x53, 0x93, 0x18, 0x49, 0x64, 0x3e, 0x8b, 0x99, 0xea, 0xe0, 0x28, 0x4f, + 0x8b, 0x01, 0x15, 0xb4, 0x23, 0x7a, 0x7c, 0x5d, 0x81, 0x97, 0x0f, 0xe8, 0x7c, 0x6f, + 0x84, 0xb6, 0x68, 0x6c, 0x46, 0x25, 0xdb, 0xdd, 0x9d, 0x79, 0xd2, 0xc5, 0x55, 0xdd, + 0x4f, 0xce, 0xed, 0x2c, 0x5e, 0x5e, 0x89, 0x6f, 0x63, 0x1a, 0xe4, 0x59, 0x7e, 0x9c, + 0xc0, 0xbe, 0xe7, 0xb3, 0x02, 0x5f, 0x95, 0x56, 0x10, 0x6a, 0x84, 0x3a, 0x18, 0x22, + 0x7f, 0x5a, 0xb9, 0x61, 0x7d, 0x7b, 0xcb, 0x1a, 0xf5, 0x28, 0xfa, 0xa7, 0xa0, 0x52, + 0xea, 0x4f, 0x52, 0xca, 0x59, 0x45, 0x57, 0xfd, 0xad, 0x33, 0x05, 0x2b, 0xc8, 0x2b, + 0x39, 0xc6, 0xa6, 0x09, 0xa0, 0x70, 0x75, 0x3d, 0x78, 0x8b, 0x2c, 0x4a, 0x2c, 0xae, + 0xbb, 0xe7, 0x9f, 0xf0, 0x12, 0x07, 0x1c, 0x07, 0x08, 0x10, 0x94, 0xad, 0x60, 0x59, + 0xc2, 0x8f, 0x48, 0xe5, 0x56, 0xc4, 0xe8, 0xd8, 0xc5, 0x37, 0x8b, 0xc2, 0x93, 0x07, + 0x6b, 0xb4, 0x97, 0x07, 0x5f, 0x9c, 0xa0, 0xba, 0x13, 0x11, 0x55, 0x0f, 0xa2, 0x17, + 0x3d, 0x0e, 0xb1, 0xf0, 0xbd, 0xdd, 0xf3, 0xb3, 0xd5, 0xc2, 0x43, 0xff, 0xea, 0xbe, + 0xe8, 0x23, 0xcd, 0x63, 0xb4, 0x39, 0x39, 0xce, 0x95, 0x46, 0xed, 0x4c, 0x41, 0xe6, + 0x0c, 0xcc, 0x7e, 0x1c, 0x54, 0x3c, 0xb3, 0xe2, 0xd3, 0x50, 0xe2, 0xe2, 0xe9, 0x74, + 0x21, 0x5c, 0xf7, 0xaa, 0x96, 0x9b, 0x66, 0x81, 0x14, 0xac, 0xdb, 0x29, 0xf4, 0xcd, + 0xcf, 0xdc, 0xec, 0x2a, 0x8c, 0xe4, 0xf5, 0x95, 0xf4, 0xff, 0x5f, 0x70, 0x7e, 0x7f, + 0xa4, 0xde, 0xe8, 0xbf, 0x8f, 0x39, 0x52, 0xae, 0x32, 0xe7, 0x7f, 0x34, 0xf8, 0xb3, + 0xab, 0xaa, 0xe9, 0x69, 0x28, 0xba, 0x4a, 0x6c, 0x0f, 0xbf, 0x5b, 0x29, 0x19, 0x2d, + 0xae, 0x80, 0x0d, 0xfa, 0x79, 0x57, 0x0c, 0xaf, 0x0b, 0xb8, 0x33, 0xbd, 0x37, 0xa3, + 0xd4, 0xbe, 0xaf, 0x09, 0x1f, 0x6b, 0x3e, 0x55, 0xaa, 0xe5, 0x25, 0xf4, 0x13, 0xac, + 0x80, 0x4c, 0x34, 0x7d, 0x54, 0x1d, 0x2c, 0x09, 0xec, 0x6e, 0x54, 0x03, 0x5d, 0xf1, + 0xd8, 0x30, 0x28, 0x4d, 0x9b, 0x46, 0xff, 0xd2, 0xb2, 0xeb, 0x04, 0x0b, 0x61, 0x77, + 0xd0, 0xa0, 0x9c, 0x16, 0x60, 0x34, 0xa9, 0x57, 0xb1, 0x8f, 0xf6, 0x2e, 0x43, 0x4a, + 0x3e, 0xc7, 0x32, 0x62, 0xe4, 0xb2, 0x3f, 0xec, 0x9d, 0x29, 0x0a, 0x81, 0xc5, 0xb1, + 0xf7, 0x3c, 0xb4, 0xcd, 0x1c, 0x47, 0x2b, 0x86, 0xe5, 0x34, 0xab, 0x9e, 0x65, 0x53, + 0x29, 0x5d, 0xb0, 0xcf, 0x34, 0xe1, 0x39, 0x2a, 0xad, 0x5a, 0xbc, 0xf3, 0x98, 0x64, + 0x16, 0xa7, 0x0a, 0x9d, 0xbe, 0x59, 0xbb, 0x95, 0x8e, 0xbc, 0x71, 0x1c, 0x3a, 0xe0, + 0x8c, 0xaf, 0x52, 0xec, 0xa9, 0xcb, 0x54, 0xc4, 0x58, 0xbe, 0x7f, 0x5e, 0x62, 0x14, + 0xec, 0xa0, 0xf0, 0xa3, 0x81, 0x52, 0x62, 0x20, 0x01, 0x32, 0xe6, 0x14, 0x54, 0x37, + 0xec, 0xd2, 0x1f, 0xc8, 0x03, 0x6c, 0xb0, 0x0a, 0x49, 0x13, 0x84, 0xc3, 0x41, 0xd8, + 0x72, 0xdc, 0xda, 0x31, 0xb1, 0x42, 0x96, 0x73, 0xd9, 0xc4, 0xf5, 0x7b, 0x81, 0xa0, + 0x23, 0x6d, 0xa5, 0xec, 0x55, 0x02, 0xee, 0x29, 0x63, 0x15, 0x0a, 0x00, 0x26, 0xbd, + 0x63, 0xef, 0x67, 0x9e, 0x8c, 0x25, 0xb8, 0xec, 0xee, 0x06, 0x56, 0x4a, 0xf3, 0xb0, + 0x2d, 0xea, 0xb1, 0x06, 0x97, 0xa2, 0x4d, 0xe6, 0x7d, 0x4f, 0x65, 0x04, 0xae, 0x27, + 0x37, 0xb8, 0xe1, 0x73, 0x25, 0xc2, 0xff, 0x15, 0x0c, 0x62, 0xe3, 0x79, 0x83, 0x44, + 0xa1, 0xad, 0x3c, 0xbb, 0x75, 0xb7, 0xf2, 0xa1, 0x57, 0x38, 0xf6, 0x01, 0xcf, 0x00, + 0xf7, 0xe8, 0xbc, 0x08, 0xb6, 0x89, 0x56, 0x7e, 0x4c, 0x7c, 0x01, 0x05, 0x8b, 0xee, + 0xc2, 0x90, 0x3c, 0x5c, 0xa6, 0xb4, 0xc4, 0xa5, 0x71, 0xf4, 0x60, 0xd6, 0x05, 0x87, + 0x36, 0x29, 0x96, 0xc6, 0xe1, 0x25, 0x54, 0xe8, 0xe3, 0x4e, 0x68, 0x3a, 0x27, 0xf8, + 0xa5, 0xff, 0x97, 0x1d, 0x5a, 0x0d, 0xc2, 0xf3, 0xef, 0xd3, 0x88, 0x99, 0x87, 0xc1, + 0xcc, 0x39, 0xce, 0x5d, 0x4b, 0x6b, 0x54, 0x4c, 0xe0, 0x4c, 0x71, 0xee, 0x4b, 0xfa, + 0xe5, 0x04, 0x0d, 0x61, 0xf0, 0x57, 0xe4, 0xf7, 0x70, 0x17, 0x28, 0xf1, 0x20, 0x04, + 0xa7, 0xf7, 0xed, 0xeb, 0x3a, 0xb2, 0x26, 0x09, 0xed, 0x33, 0xb0, 0xab, 0x5d, 0x69, + 0xb1, 0x2d, 0x45, 0x76, 0x57, 0x77, 0x14, 0xdf, 0xc6, 0xdd, 0xa7, 0x1f, 0xf6, 0x01, + 0x7b, 0x55, 0xb3, 0x35, 0x4d, 0x11, 0xe9, 0x21, 0x67, 0x92, 0xe5, 0x60, 0x9f, 0xc0, + 0x67, 0x88, 0xec, 0x66, 0x8e, 0xef, 0x64, 0x5e, 0x63, 0xb3, 0x7e, 0x2d, 0x0c, 0xd2, + 0x63, 0x04, 0x08, 0x00, 0xbc, 0x8a, 0xa2, 0x80, 0x15, 0x6a, 0x79, 0x4f, 0x62, 0xa5, + 0xf6, 0x93, 0xeb, 0xd9, 0x07, 0x4b, 0x5d, 0x35, 0x4a, 0x71, 0xc8, 0xe3, 0x36, 0xde, + 0x04, 0x08, 0xac, 0x70, 0x80, 0xa2, 0xae, 0xee, 0x36, 0x6c, 0x58, 0x14, 0x6f, 0x32, + 0xe3, 0x49, 0xa9, 0xbc, 0x65, 0x7e, 0xc9, 0xe5, 0x7a, 0x89, 0xa0, 0x4c, 0xce, 0xee, + 0x21, 0xbd, 0xf3, 0x79, 0x3e, 0x49, 0xa5, 0xcf, 0x71, 0x3a, 0x42, 0xd0, 0x29, 0xdd, + 0xdb, 0x3d, 0xb4, 0x95, 0x09, 0x2c, 0x37, 0xce, 0x81, 0x4b, 0xe7, 0x3e, 0xf4, 0xec, + 0x8d, 0x70, 0xe8, 0x69, 0xbd, 0x2b, 0x78, 0x8f, 0x15, 0x00, 0xfe, 0x5e, 0xe5, 0x6c, + 0x0c, 0xe7, 0x04, 0xeb, 0xa2, 0xc1, 0xa3, 0xa3, 0x29, 0x0d, 0xe6, 0xec, 0x68, 0xcc, + 0xb5, 0xef, 0x7c, 0xd0, 0x21, 0x2a, 0x3f, 0x09, 0x96, 0x92, 0xcf, 0x00, 0x04, 0x8d, + 0xe5, 0x01, 0x26, 0x19, 0xe7, 0x41, 0x69, 0x2b, 0xfc, 0x74, 0x05, 0xba, 0x3e, 0x87, + 0x5e, 0x98, 0xb7, 0xca, 0x31, 0xe9, 0x65, 0xa1, 0x6f, 0xdd, 0xb5, 0xb0, 0xb7, 0x72, + 0xa3, 0xf5, 0xd0, 0x50, 0xd8, 0xad, 0x7f, 0x60, 0x7f, 0x71, 0xc5, 0x36, 0x3f, 0x7b, + 0x7d, 0x2c, 0x34, 0x38, 0xab, 0xe6, 0xb8, 0xcd, 0x3b, 0xb4, 0x21, 0x8b, 0x4d, 0x7f, + 0x55, 0x65, 0x0b, 0x80, 0x13, 0x80, 0xc7, 0xb5, 0xc6, 0x10, 0x07, 0x9e, 0x51, 0x37, + 0x16, 0xc4, 0x6f, 0xaf, 0xcf, 0x3c, 0x8c, 0x27, 0x15, 0x38, 0x27, 0x83, 0xae, 0xe6, + 0x69, 0xa9, 0xdf, 0x47, 0x17, 0x70, 0x71, 0xb5, 0x43, 0x98, 0xce, 0xcf, 0xd6, 0x86, + 0xa0, 0xbc, 0x9a, 0xd3, 0x7f, 0x44, 0xb5, 0x38, 0x87, 0x75, 0x87, 0x51, 0x66, 0x00, + 0x6d, 0x25, 0xdf, 0x4b, 0x5e, 0xd1, 0xc4, 0x1f, 0x12, 0x1b, 0x9e, 0x16, 0xfc, 0xa6, + 0xe0, 0x15, 0xa9, 0x01, 0xe1, 0xe7, 0xe2, 0xc0, 0x99, 0x4e, 0x42, 0x7b, 0xeb, 0xd3, + 0x56, 0xe4, 0x17, 0x6d, 0xec, 0x83, 0xe6, 0xfe, 0x80, 0x02, 0x9c, 0xfc, 0x47, 0x8b, + 0x88, 0xb6, 0xfd, 0x38, 0xc0, 0x39, 0xe0, 0x8b, 0x6f, 0xd9, 0x5d, 0xab, 0xcf, 0xb2, + 0x5f, 0x23, 0x8b, 0x26, 0x62, 0x06, 0xb0, 0xa2, 0xf9, 0xa2, 0xee, 0xa1, 0xc0, 0x83, + 0xfa, 0xc8, 0x08, 0xaa, 0xfa, 0x03, 0x65, 0x66, 0xcc, 0xd2, 0x02, 0xbc, 0xfa, 0x41, + 0x4e, 0x71, 0xc8, 0xb4, 0x89, 0x33, 0xc8, 0xed, 0x45, 0x28, 0x7e, 0x1b, 0x43, 0x9b, + 0x61, 0x06, 0xa5, 0x50, 0x94, 0x73, 0xf5, 0x7b, 0x87, 0x88, 0xaf, 0x52, 0x7c, 0xf9, + 0xa7, 0xab, 0xa5, 0x93, 0xdc, 0x9f, 0x5e, 0x5a, 0xca, 0x1a, 0x64, 0x8e, 0xe4, 0x88, + 0xf3, 0x6d, 0xeb, 0x4a, 0x3f, 0xdb, 0x0f, 0xf6, 0xf5, 0xa3, 0x04, 0x4a, 0x63, 0xe1, + 0x7f, 0x70, 0xa4, 0x30, 0x38, 0x24, 0x60, 0x3a, 0xb5, 0x0e, 0x9b, 0xf7, 0x5b, 0xae, + 0xb5, 0x7b, 0xfd, 0xc8, 0x9b, 0xfd, 0xbc, 0x27, 0x27, 0x9d, 0x10, 0x73, 0xbf, 0x7f, + 0x95, 0x05, 0xfb, 0x31, 0x68, 0xd2, 0x06, 0xe2, 0xbf, 0x41, 0x02, 0xbf, 0x15, 0x9c, + 0xff, 0x61, 0xe6, 0xd6, 0x6c, 0x80, 0x37, 0x50, 0xda, 0x25, 0x4c, 0xd6, 0xb8, 0x1a, + 0xed, 0x42, 0x09, 0x97, 0x94, 0xb8, 0x4e, 0xce, 0x90, 0x42, 0x18, 0xe6, 0xf6, 0x6e, + 0xc6, 0x34, 0xe9, 0x2e, 0xef, 0xf4, 0x5f, 0x52, 0xe0, 0x4b, 0x4b, 0x79, 0x5a, 0x15, + 0x25, 0xaa, 0xf9, 0xc5, 0x1d, 0x62, 0x60, 0xfb, 0xd6, 0x4e, 0x8d, 0x8a, 0xc2, 0x66, + 0xdc, 0x6e, 0x7d, 0xf6, 0x15, 0x3a, 0xd9, 0x73, 0x55, 0x83, 0x79, 0x28, 0x40, 0x4c, + 0xd5, 0x81, 0xbc, 0x9c, 0xf9, 0xdc, 0xd6, 0x67, 0x47, 0xdc, 0x97, 0x0a, 0x9f, 0x00, + 0xde, 0xb4, 0x4b, 0xd6, 0x34, 0xab, 0x04, 0x2e, 0x01, 0x04, 0xc1, 0xce, 0x74, 0x7f, + 0x53, 0x75, 0x1b, 0xc3, 0x3e, 0x38, 0x4c, 0x6b, 0x55, 0x76, 0x39, 0x9e, 0x16, 0xf8, + 0xf0, 0xcb, 0x08, 0xde, 0x35, 0x08, 0x37, 0x33, 0x95, 0x45, 0x87, 0xc1, 0xc2, 0x4d, + 0xf2, 0xae, 0x66, 0x30, 0xff, 0xfe, 0x99, 0x62, 0x15, 0xef, 0xe4, 0xd2, 0x62, 0x6d, + 0xeb, 0x20, 0x56, 0x6a, 0x8f, 0x5e, 0xad, 0x2f, 0x04, 0xdb, 0x5d, 0x08, 0x77, 0x9c, + 0x9c, 0x65, 0x9e, 0xa3, 0x43, 0xcd, 0x78, 0x46, 0x34, 0xc9, 0x9d, 0x8c, 0x8b, 0xad, + 0xa9, 0x3b, 0xe8, 0xe6, 0xda, 0x84, 0x15, 0x94, 0xba, 0xcf, 0x7c, 0xb3, 0xe6, 0x92, + 0xc7, 0x4b, 0x5f, 0xfe, 0x95, 0x78, 0x73, 0x11, 0x3a, 0x1a, 0xb0, 0x64, 0x02, 0x6f, + 0x6d, 0xee, 0x8b, 0x48, 0xa3, 0x84, 0xa1, 0x33, 0x83, 0x18, 0x36, 0x07, 0x86, 0x50, + 0x27, 0x84, 0xd1, 0x7d, 0x40, 0x0c, 0xe3, 0xd7, 0x21, 0x78, 0x7e, 0xdc, 0x4c, 0x6b, + 0x39, 0x35, 0x66, 0x25, 0x10, 0x77, 0x10, 0x00, 0x68, 0x0d, 0x78, 0xbb, 0x49, 0xc5, + 0x66, 0xef, 0x27, 0xdf, 0x61, 0xc9, 0xfe, 0xb9, 0x2c, 0x08, 0x97, 0x59, 0x44, 0x87, + 0x27, 0xa9, 0x34, 0xe3, 0x57, 0x95, 0x3d, 0xe1, 0xe9, 0xe9, 0x0f, 0xd8, 0xdf, 0xfe, + 0x40, 0xb8, 0x73, 0xbc, 0xd5, 0xb9, 0x82, 0x08, 0xdf, 0x4b, 0x2c, 0xa2, 0x89, 0x7a, + 0xf9, 0x0d, 0x8c, 0x8a, 0x23, 0x62, 0x30, 0x02, 0xa9, 0xd8, 0xbc, 0x02, 0xe8, 0x06, + 0x25, 0x4f, 0x41, 0x0e, 0x3b, 0x02, 0x40, 0x9c, 0xbe, 0xbf, 0xce, 0x8a, 0xcf, 0x65, + 0xcf, 0x39, 0x42, 0x6b, 0x64, 0xa6, 0xba, 0x93, 0x74, 0xa1, 0x3d, 0x72, 0x59, 0x62, + 0x3f, 0x65, 0xe9, 0x3e, 0x10, 0xbf, 0x1f, 0x16, 0xba, 0x7a, 0xe0, 0x7d, 0xa9, 0x20, + 0x58, 0x1c, 0x70, 0x40, 0x9e, 0xdc, 0x7b, 0x9e, 0x21, 0x4e, 0x95, 0x91, 0x92, 0x82, + 0x4c, 0x1d, 0xa6, 0x5d, 0x33, 0x7b, 0x73, 0x75, 0xf5, 0x03, 0x2f, 0xea, 0xd3, 0xb4, + 0xf3, 0x28, 0x48, 0x11, 0x95, 0x0c, 0x7a, 0x90, 0xae, 0xc9, 0x75, 0xd4, 0xe3, 0x62, + 0x9f, 0x52, 0xd1, 0x9a, 0x16, 0x4e, 0x51, 0x16, 0xef, 0x3a, 0xd0, 0x22, 0x44, 0x2d, + 0x1e, 0xec, 0x76, 0xb8, 0x88, 0x73, 0x8b, 0x53, 0xe5, 0x05, 0x58, 0xa7, 0x0f, 0x20, + 0xc8, 0xac, 0xb5, 0x8d, 0xee, 0x63, 0x27, 0x15, 0xe4, 0x78, 0xe2, 0xbc, 0x21, 0xbc, + 0xfb, 0xe3, 0x15, 0x59, 0x96, 0xca, 0xe7, 0xbd, 0x97, 0xf0, 0x2b, 0x51, 0x6d, 0x32, + 0x00, 0xfb, 0x3c, 0x17, 0x39, 0x7c, 0xc1, 0x2b, 0xb7, 0xa1, 0x9f, 0xd4, 0x36, 0xe6, + 0x7a, 0xbc, 0xe6, 0x6d, 0x30, 0xfe, 0xc0, 0x47, 0xfb, 0x27, 0x70, 0x82, 0x0e, 0x47, + 0x6f, 0x3e, 0x32, 0xbc, 0x48, 0x3b, 0xf5, 0x31, 0x64, 0xae, 0x49, 0x70, 0xf1, 0x1b, + 0x9c, 0xae, 0xe4, 0xed, 0x6c, 0xb8, 0xd2, 0xd7, 0x0f, 0x69, 0x13, 0xd8, 0xe0, 0x2a, + 0xf8, 0xfb, 0xb1, 0xe4, 0x09, 0xb4, 0xef, 0x08, 0x04, 0x48, 0xe5, 0x3b, 0xe6, 0xe5, + 0xe6, 0x05, 0x75, 0xdf, 0xde, 0x94, 0x28, 0xb0, 0x06, 0x96, 0x61, 0x1a, 0x2f, 0x72, + 0x33, 0x2a, 0xe2, 0x90, 0x23, 0xdd, 0x88, 0xae, 0x77, 0xf1, 0x5b, 0x8a, 0xe2, 0xc2, + 0x4b, 0x86, 0xcf, 0x3d, 0x57, 0x43, 0x9c, 0xaf, 0x17, 0xf2, 0x8e, 0xda, 0x94, 0x93, + 0x2e, 0xef, 0x28, 0x53, 0x4e, 0x16, 0x49, 0xce, 0xf8, 0x85, 0x40, 0xfc, 0xb1, 0xa6, + 0x3e, 0x11, 0x5c, 0x58, 0x22, 0xaf, 0xa4, 0x40, 0xc8, 0xd7, 0x9d, 0x66, 0xf9, 0xbb, + 0x1f, 0x48, 0xe1, 0x14, 0x0b, 0x06, 0xec, 0x87, 0x18, 0x3c, 0xbc, 0x6e, 0x95, 0xf6, + 0xcd, 0x5f, 0x7e, 0xbc, 0xad, 0xb8, 0x97, 0xc7, 0x7b, 0x4a, 0xfb, 0x36, 0x7b, 0x95, + 0x2d, 0xbb, 0x71, 0x7f, 0x75, 0x18, 0x90, 0xc8, 0xac, 0x30, 0x36, 0xda, 0xcd, 0xbd, + 0x78, 0x4a, 0x0d, 0x83, 0xab, 0xb8, 0x44, 0x6b, 0x3f, 0x93, 0x96, 0x33, 0x5f, 0xbf, + 0x0b, 0x44, 0xed, 0xc9, 0x9e, 0x1c, 0x67, 0xc5, 0xc3, 0x81, 0x6a, 0xce, 0x76, 0x29, + 0xe6, 0xe7, 0xb0, 0x28, 0xd6, 0xc8, 0x62, 0x74, 0x9e, 0x86, 0xeb, 0xc5, 0x11, 0x7e, + 0x21, 0xf4, 0x23, 0xe1, 0x8d, 0x09, 0x76, 0xa1, 0xf5, 0x1d, 0x45, 0x47, 0x6d, 0xa5, + 0x60, 0xff, 0x23, 0x15, 0x42, 0xbb, 0x21, 0xc3, 0xde, 0xd2, 0xf2, 0x3b, 0x2a, 0x50, + 0xe0, 0xb8, 0x22, 0x56, 0x90, 0x67, 0x5d, 0x1d, 0x11, 0x65, 0xd7, 0x60, 0x70, 0x2e, + 0xf1, 0x03, 0xd2, 0x23, 0x67, 0x26, 0x90, 0x23, 0x59, 0xbe, 0x8d, 0x79, 0x73, 0x52, + 0xf9, 0x6d, 0x22, 0x46, 0xa2, 0xee, 0x0a, 0xf8, 0x0a, 0x2a, 0x2d, 0x89, 0xa5, 0x85, + 0x30, 0xd6, 0xe3, 0x6b, 0xd3, 0x3a, 0x00, 0xc1, 0xb8, 0x93, 0xd6, 0xff, 0x8f, 0x90, + 0x01, 0x44, 0x15, 0x1b, 0xee, 0x34, 0xc7, 0x94, 0x4b, 0x99, 0xed, 0x6e, 0x79, 0x45, + 0xe7, 0xf0, 0xde, 0x87, 0x26, 0x3d, 0x0b, 0xba, 0x6e, 0x55, 0xac, 0x96, 0xa9, 0x6d, + 0x49, 0x95, 0x12, 0x9b, 0xcf, 0xa9, 0xd9, 0xda, 0x6d, 0xe6, 0xdd, 0x48, 0x26, 0x39, + 0x15, 0x3a, 0x81, 0x69, 0xa4, 0xab, 0x46, 0x4e, 0x39, 0x0b, 0x7f, 0x0a, 0x96, 0xd1, + 0x4a, 0x73, 0xf7, 0x69, 0x7f, 0x7e, 0xce, 0x3c, 0xd7, 0x81, 0xd3, 0x5d, 0xd2, 0x2a, + 0xdd, 0xdd, 0x2f, 0x5d, 0x34, 0x52, 0x04, 0xe4, 0xbb, 0x55, 0x7e, 0x88, 0x45, 0x3f, + 0x18, 0x8c, 0xac, 0xbe, 0x92, 0x29, 0x87, 0xbb, 0xe3, 0xb3, 0xd9, 0x76, 0x82, 0x61, + 0x35, 0xc1, 0x03, 0xb6, 0xca, 0x18, 0x2b, 0x63, 0xe9, 0xe6, 0x7f, 0x83, 0xdc, 0x9f, + 0x48, 0x93, 0x33, 0xd5, 0x2a, 0x7f, 0xd7, 0x68, 0x8a, 0x58, 0xd6, 0x62, 0x0b, 0x67, + 0xe9, 0xc7, 0xb0, 0x91, 0x6f, 0xef, 0x90, 0xf1, 0x5d, 0x8e, 0x4e, 0xb8, 0x0c, 0xf5, + 0x99, 0x68, 0x2f, 0x95, 0x4f, 0xf4, 0xe0, 0xb3, 0x71, 0x83, 0x13, 0x0c, 0xa2, 0xee, + 0xd0, 0x91, 0x3f, 0x46, 0xa4, 0xdb, 0x99, 0x2a, 0x1c, 0x3b, 0xf3, 0x19, 0xdc, 0x86, + 0x75, 0x94, 0x01, 0x01, 0x53, 0x7c, 0xff, 0xc4, 0xa8, 0x2d, 0x59, 0x9b, 0xbe, 0xa0, + 0xd4, 0x7e, 0x7a, 0xbf, 0xa9, 0x92, 0xb4, 0x99, 0x8c, 0xb2, 0x50, 0x09, 0x55, 0xe6, + 0x1c, 0x0d, 0x46, 0xb3, 0x21, 0x17, 0xfb, 0xb9, 0x7f, 0x7a, 0x76, 0x32, 0xd8, 0x72, + 0x4b, 0x5d, 0xff, 0x67, 0xf7, 0x5e, 0x2d, 0x31, 0x74, 0x06, 0xa0, 0xce, 0xc2, 0x89, + 0xed, 0x08, 0x3b, 0x7c, 0x58, 0x19, 0x81, 0x8c, 0x50, 0x47, 0x93, 0xde, 0x53, 0xb6, + 0xbf, 0xdb, 0x51, 0x0e, 0x7c, 0xa7, 0x29, 0xba, 0x74, 0x3d, 0x10, 0xb3, 0xe9, 0x95, + 0x7e, 0xfa, 0x84, 0x20, 0x13, 0x39, 0x47, 0x7c, 0xf3, 0x5f, 0xbb, 0x6a, 0x27, 0x9b, + 0xad, 0x9e, 0x8f, 0x42, 0xb9, 0xb3, 0xfd, 0x6f, 0x3b, 0xc7, 0x70, 0x67, 0x1d, 0x9c, + 0x19, 0x12, 0x2f, 0xa3, 0x25, 0x6d, 0x09, 0x07, 0x36, 0xb6, 0xd6, 0x4e, 0xb9, 0xcc, + 0x03, 0x20, 0xf1, 0xea, 0xaa, 0x27, 0x1b, 0xa2, 0x86, 0x1e, 0xc4, 0xb3, 0xf3, 0xf6, + 0xc8, 0x40, 0xb6, 0x19, 0xff, 0x38, 0x8d, 0x81, 0xfc, 0x40, 0x44, 0xa0, 0xd5, 0x31, + 0xa4, 0xbb, 0x44, 0xc9, 0x3d, 0x09, 0x9d, 0xb0, 0x8a, 0x9b, 0xc3, 0x46, 0xa0, 0xb6, + 0x2f, 0x16, 0x8f, 0xfb, 0xdb, 0x73, 0x93, 0x66, 0xbb, 0x53, 0x5d, 0xde, 0x66, 0xc2, + 0xc1, 0x28, 0x7b, 0x3b, 0x27, 0x85, 0xae, 0xd6, 0x4c, 0xc4, 0x0c, 0xbc, 0x7d, 0x33, + 0xcb, 0xa4, 0xa9, 0xf3, 0xfc, 0xf5, 0xf8, 0x31, 0x36, 0xa4, 0x39, 0x2d, 0x21, 0xa7, + 0xf9, 0xeb, 0x1c, 0xe4, 0xb6, 0xe1, 0x7e, 0x6f, 0x4a, 0x85, 0xa5, 0x79, 0x66, 0x9e, + 0xfd, 0x0f, 0xb0, 0x98, 0x78, 0xe0, 0x88, 0xe3, 0x22, 0xe9, 0x06, 0xe8, 0x0d, 0x27, + 0xf8, 0xd0, 0xca, 0x7e, 0x79, 0x15, 0xab, 0x40, 0x96, 0x59, 0xa6, 0xd8, 0x0f, 0xde, + 0xd1, 0x0a, 0xff, 0x9f, 0xb7, 0x73, 0x74, 0x9d, 0x79, 0x28, 0x57, 0xf6, 0x8c, 0x7e, + 0x8c, 0xf5, 0x18, 0x26, 0x0a, 0x61, 0x08, 0x6d, 0xe3, 0x2f, 0xff, 0x82, 0x39, 0xf4, + 0x53, 0x61, 0x7a, 0x19, 0xf6, 0xfe, 0xc2, 0x20, 0x67, 0x60, 0x65, 0xeb, 0xe2, 0x75, + 0x7e, 0xfc, 0xac, 0xcb, 0x77, 0xfc, 0x61, 0xe5, 0x9b, 0x97, 0x63, 0x7e, 0x92, 0x0d, + 0xee, 0x5e, 0x7e, 0x7a, 0x12, 0xe9, 0xd6, 0xd2, 0x28, 0xb2, 0x6b, 0x2f, 0xa8, 0x36, + 0xf4, 0x72, 0x83, 0x69, 0xad, 0xcd, 0xfc, 0xd0, 0x04, 0xdc, 0xf1, 0x9e, 0x27, 0xc0, + 0xc0, 0x84, 0x44, 0xd2, 0x9a, 0x12, 0x2b, 0x23, 0x09, 0xf7, 0x16, 0x3c, 0x99, 0x0e, + 0xb9, 0x26, 0x1f, 0xd4, 0x15, 0xc0, 0x45, 0x4a, 0x56, 0xaa, 0x3e, 0xaf, 0x9c, 0x1f, + 0x9b, 0xff, 0xf6, 0x04, 0x77, 0x6a, 0x4d, 0x25, 0xe7, 0xd3, 0xcd, 0xc5, 0xc5, 0xf1, + 0x9c, 0xd2, 0xa8, 0x79, 0x4a, 0x4f, 0x57, 0x16, 0x7f, 0xbc, 0x7e, 0xaa, 0x06, 0x16, + 0x4d, 0x51, 0xc4, 0x53, 0x06, 0x14, 0xbc, 0xf5, 0x20, 0xb2, 0x63, 0x82, 0x0a, 0xa1, + 0x7b, 0x20, 0xb4, 0x8c, 0xbf, 0x59, 0xd8, 0xe3, 0x09, 0x32, 0x2e, 0xbe, 0x56, 0x6f, + 0xbe, 0x46, 0xe0, 0xaa, 0x29, 0x76, 0x6a, 0xdf, 0xdf, 0x01, 0x7a, 0x71, 0x05, 0x10, + 0x3c, 0x7f, 0xca, 0xb7, 0xb0, 0x76, 0x48, 0xc7, 0xc1, 0x16, 0x04, 0x84, 0xf7, 0x7a, + 0x6c, 0x70, 0xa5, 0x38, 0x1b, 0x82, 0x56, 0x40, 0xa1, 0xbe, 0x48, 0xe4, 0x15, 0xa1, + 0xe6, 0xa2, 0x7d, 0x78, 0x02, 0x2a, 0x8a, 0x2f, 0xf0, 0x70, 0xab, 0xf1, 0x23, 0x94, + 0xe3, 0xae, 0x5a, 0x8c, 0x23, 0xe3, 0x73, 0x3e, 0xa4, 0x7a, 0x44, 0xcb, 0x2c, 0x96, + 0x8b, 0xca, 0x24, 0x98, 0x37, 0xde, 0x1d, 0x39, 0xa5, 0xa1, 0xdc, 0xae, 0x71, 0x0c, + 0xe0, 0x43, 0x01, 0x69, 0xbd, 0x6e, 0x9f, 0x64, 0xab, 0xf1, 0xe6, 0x4e, 0xc4, 0x9e, + 0xd0, 0x80, 0x4e, 0xb6, 0x47, 0x74, 0x3a, 0xce, 0xa9, 0x29, 0xed, 0x0f, 0x7c, 0x90, + 0x15, 0xb0, 0xe8, 0x1e, 0x21, 0x29, 0xdb, 0x05, 0x0d, 0x5e, 0x78, 0xe6, 0x82, 0xc8, + 0x19, 0x93, 0xea, 0x87, 0x53, 0xc9, 0x91, 0xb0, 0x2e, 0x61, 0x81, 0x0e, 0x74, 0x61, + 0xed, 0x87, 0xb3, 0x80, 0xdb, 0x96, 0xab, 0xe3, 0xbe, 0xad, 0x0f, 0x4b, 0x22, 0x12, + 0xdb, 0x65, 0x8c, 0x11, 0xb8, 0x3f, 0x53, 0x11, 0x47, 0x85, 0x27, 0x65, 0x98, 0xb0, + 0x19, 0x7a, 0x7f, 0x1c, 0x25, 0x62, 0x7d, 0x79, 0x62, 0x4d, 0xac, 0xee, 0x97, 0x7d, + 0x9f, 0x4e, 0x1a, 0x35, 0xed, 0x2e, 0xaa, 0xd3, 0xcb, 0x68, 0x25, 0x0a, 0xa9, 0xb3, + 0xab, 0x1a, 0x83, 0x45, 0x72, 0x8e, 0x7d, 0x1a, 0x78, 0xbe, 0x1f, 0xe4, 0x62, 0xce, + 0x8e, 0xad, 0x52, 0x8f, 0x7c, 0x05, 0x0f, 0x1f, 0x6e, 0x02, 0x2b, 0xa8, 0xb0, 0xce, + 0xdf, 0x6e, 0x29, 0x7a, 0xb5, 0x64, 0xca, 0x1a, 0x1f, 0xaa, 0xf4, 0xcf, 0xf1, 0xe4, + 0x20, 0x32, 0xfb, 0xbb, 0x38, 0x9d, 0x3f, 0x66, 0xd5, 0x75, 0x55, 0xef, 0x3f, 0x3e, + 0x9e, 0x49, 0xc2, 0xac, 0x4e, 0x85, 0xbb, 0x75, 0x1d, 0x62, 0x66, 0xc9, 0x03, 0x5b, + 0x77, 0x9d, 0x76, 0x9d, 0x49, 0x5c, 0x91, 0x8a, 0x05, 0x5e, 0x77, 0x67, 0xfb, 0xb4, + 0xbb, 0xac, 0x3f, 0x96, 0x3d, 0xe9, 0x97, 0x46, 0xec, 0x4d, 0xfb, 0x64, 0x2d, 0x9c, + 0x2b, 0x86, 0x38, 0xe1, 0x6c, 0x16, 0xe7, 0x27, 0x70, 0x79, 0x3b, 0x7e, 0xa1, 0xd0, + 0x70, 0xc4, 0xe1, 0x1c, 0xbc, 0x20, 0xd8, 0xff, 0x3b, 0xea, 0xd1, 0x0d, 0xb9, 0xc9, + 0x4a, 0xe0, 0x48, 0x27, 0x21, 0xe1, 0xf2, 0x2c, 0xef, 0xe0, 0xdf, 0x7c, 0x57, 0x7a, + 0xa3, 0x8e, 0xc0, 0xe6, 0xc7, 0x8c, 0x9b, 0xa1, 0x64, 0xe9, 0xdd, 0x00, 0x55, 0xdd, + 0xe8, 0x3e, 0x8a, 0xd2, 0x40, 0xe6, 0xdf, 0xdb, 0xfb, 0xe1, 0x76, 0xe4, 0x55, 0x1f, + 0xdd, 0xe9, 0x2d, 0xb1, 0x67, 0x27, 0x42, 0x04, 0x41, 0x70, 0x06, 0x58, 0xb5, 0x0e, + 0xbb, 0x5a, 0x16, 0x13, 0x26, 0x7e, 0xac, 0x51, 0xc8, 0x0b, 0x19, 0xec, 0xb7, 0x86, + 0xab, 0x3b, 0xb9, 0x37, 0xf0, 0xd9, 0x8e, 0x08, 0xb9, 0xc9, 0xcd, 0x4d, 0xf1, 0x53, + 0x4e, 0xfe, 0xe3, 0x8a, 0x8f, 0x87, 0x8c, 0x9f, 0x3b, 0xdc, 0x7e, 0xfb, 0x2d, 0x53, + 0xff, 0x84, 0xfb, 0x83, 0xea, 0xe7, 0xc9, 0x9e, 0xff, 0xa6, 0x3c, 0x96, 0x49, 0xa1, + 0xf1, 0x70, 0xd2, 0x9a, 0xf0, 0x3a, 0x3b, 0x45, 0x58, 0x9f, 0xae, 0x81, 0xeb, 0x0b, + 0x5d, 0x8e, 0x0d, 0x38, 0x02, 0x1d, 0x3b, 0x5f, 0x07, 0xe8, 0x8c, 0x99, 0x04, 0x37, + 0x6d, 0x27, 0xf1, 0x3e, 0x44, 0x41, 0xd5, 0x38, 0x74, 0x42, 0xc5, 0xea, 0x0a, 0xf5, + 0xa2, 0x0a, 0x38, 0x32, 0xbc, 0x3b, 0x9c, 0x59, 0xb8, 0x4b, 0xca, 0x39, 0xb5, 0x2c, + 0xd6, 0xb1, 0xfa, 0x29, 0x32, 0xba, 0x9d, 0x66, 0xc4, 0x12, 0xf5, 0xcd, 0x39, 0x35, + 0x1e, 0x13, 0x33, 0xef, 0x85, 0xd0, 0xee, 0xe5, 0x45, 0xa7, 0xe4, 0x06, 0xf6, 0xeb, + 0x3b, 0xf8, 0x93, 0xf3, 0xed, 0xac, 0x94, 0x64, 0x33, 0x92, 0xa2, 0x8b, 0x0e, 0x49, + 0x0c, 0x51, 0xe4, 0xb7, 0x16, 0x3c, 0x1c, 0xf7, 0x57, 0xd2, 0x24, 0x18, 0xdd, 0x63, + 0x38, 0x1b, 0xa2, 0xf2, 0x98, 0x28, 0x83, 0x6f, 0xe9, 0x78, 0xda, 0xb5, 0x20, 0x1b, + 0x2d, 0xb0, 0x8c, 0x3b, 0x38, 0x9b, 0xa4, 0xb6, 0xac, 0xf7, 0x78, 0xc2, 0xbf, 0x91, + 0x02, 0xbe, 0x0c, 0x3e, 0x12, 0xd7, 0x7a, 0xea, 0x6d, 0xf7, 0x53, 0x8e, 0x8c, 0xf3, + 0x62, 0xba, 0xaa, 0xad, 0x1d, 0xc5, 0x60, 0x42, 0xc6, 0xf2, 0x4c, 0xaf, 0x46, 0xbe, + 0xd6, 0x6a, 0xbf, 0x4c, 0x40, 0x2a, 0x74, 0x92, 0x4e, 0xcf, 0xd0, 0xa0, 0x8d, 0xed, + 0xee, 0xa0, 0xef, 0xce, 0xcd, 0x35, 0x2c, 0x27, 0x5f, 0x13, 0xed, 0x20, 0x76, 0x03, + 0x82, 0x2b, 0x1e, 0xf9, 0x97, 0xb7, 0xed, 0x42, 0xf4, 0xa5, 0x76, 0xb9, 0xe4, 0xc0, + 0x07, 0x38, 0x56, 0x3f, 0x82, 0xa7, 0x62, 0x85, 0x46, 0x7d, 0xa2, 0x95, 0xc2, 0x3b, + 0xa1, 0xc5, 0x87, 0xeb, 0xef, 0xaf, 0x13, 0xcd, 0x4d, 0x50, 0xf2, 0x3c, 0xa5, 0x74, + 0x3c, 0x22, 0x5c, 0x38, 0x6d, 0x46, 0xd4, 0xac, 0x70, 0x83, 0x79, 0xef, 0x99, 0x96, + 0x74, 0x4b, 0x39, 0x12, 0x04, 0x4b, 0x35, 0x5f, 0x92, 0x7a, 0x67, 0xaf, 0x1e, 0xf2, + 0x6a, 0x71, 0x7f, 0xb5, 0xa8, 0x46, 0xac, 0x9d, 0xa1, 0x5e, 0xa3, 0xf1, 0x8f, 0x8c, + 0x36, 0x18, 0x3f, 0x87, 0x9b, 0xb9, 0xa3, 0xb2, 0x98, 0xff, 0xf9, 0xa4, 0x89, 0x64, + 0x6e, 0x77, 0x8e, 0x6d, 0x67, 0x01, 0xf9, 0xad, 0xac, 0x7a, 0xe8, 0x82, 0x09, 0xa8, + 0x43, 0xba, 0x8a, 0x55, 0xd1, 0x19, 0x2b, 0xbe, 0xef, 0x31, 0xd0, 0x71, 0x45, 0x37, + 0xf7, 0xa0, 0x35, 0xb0, 0x79, 0xc6, 0xad, 0xd4, 0xab, 0x50, 0x61, 0x2d, 0x35, 0x89, + 0x7a, 0x93, 0x3d, 0x49, 0xe8, 0xef, 0x08, 0x6c, 0xdf, 0x96, 0xc8, 0x0d, 0x28, 0x56, + 0xcc, 0xc7, 0xe4, 0x5f, 0xc4, 0xef, 0xd4, 0xbf, 0x1b, 0x98, 0xab, 0x28, 0x89, 0x1b, + 0x4a, 0xea, 0x7e, 0xf8, 0x4c, 0xf7, 0x36, 0x93, 0x5c, 0x46, 0x6b, 0x24, 0x97, 0x4d, + 0xf8, 0xf5, 0x35, 0x5b, 0x8b, 0xa3, 0x20, 0xac, 0x5f, 0xbc, 0x47, 0x5a, 0xa2, 0xcf, + 0x5a, 0xd3, 0x77, 0x80, 0xbd, 0x9f, 0x9d, 0x46, 0x42, 0xcf, 0x6c, 0x2d, 0xc6, 0xb8, + 0x2f, 0x91, 0x7d, 0x09, 0xc4, 0xf7, 0x28, 0x88, 0xf9, 0x15, 0x53, 0x44, 0x7f, 0xc5, + 0x70, 0x26, 0x6d, 0xaa, 0xfd, 0x4b, 0x96, 0xcf, 0xe2, 0xa0, 0xb0, 0x67, 0x92, 0x46, + 0x9a, 0x72, 0x7d, 0xbe, 0xd0, 0x55, 0x91, 0xea, 0x60, 0x57, 0x32, 0x20, 0x5e, 0x26, + 0x05, 0x97, 0x8a, 0x3a, 0x90, 0x2c, 0x3c, 0xd6, 0x5f, 0x94, 0x83, 0x00, 0xf7, 0x37, + 0x51, 0x88, 0x15, 0xf4, 0x63, 0xd3, 0xc6, 0x1a, 0x18, 0x9b, 0xc3, 0xbc, 0x84, 0xb0, + 0x22, 0xf6, 0x3d, 0x65, 0x4f, 0x52, 0x0e, 0x3a, 0x7a, 0xd8, 0x8e, 0x5d, 0x8d, 0xa1, + 0x50, 0x14, 0xbe, 0x4b, 0xb9, 0x67, 0x99, 0x27, 0xdc, 0x7e, 0x0f, 0xba, 0xf0, 0x58, + 0xd9, 0x3f, 0x37, 0xc7, 0x2b, 0x28, 0x6b, 0x02, 0xb7, 0x5f, 0x3c, 0xdb, 0xfb, 0x85, + 0x0e, 0xed, 0x90, 0xcb, 0x23, 0x39, 0x24, 0x32, 0xeb, 0xc3, 0x6b, 0xd2, 0x47, 0x54, + 0x46, 0x9c, 0x03, 0x73, 0x1a, 0x7e, 0xbb, 0xed, 0x28, 0x57, 0x78, 0x49, 0x81, 0xa0, + 0x71, 0x67, 0x05, 0xd9, 0xcb, 0x47, 0xd9, 0x87, 0xf8, 0x3d, 0x34, 0x21, 0xb1, 0x07, + 0xd1, 0x55, 0xdb, 0xb6, 0x61, 0xed, 0x08, 0xf2, 0xfc, 0x2e, 0x6b, 0x4a, 0x5b, 0x09, + 0x77, 0x64, 0x51, 0xd8, 0x73, 0xb2, 0xfc, 0x63, 0x68, 0x1c, 0xe3, 0x08, 0xc8, 0x08, + 0xf5, 0x38, 0x8c, 0xb1, 0xaa, 0x55, 0x89, 0xa1, 0x87, 0x73, 0xdb, 0x39, 0x07, 0xa0, + 0x6b, 0xef, 0x62, 0xd1, 0x29, 0x60, 0xaa, 0xe7, 0x2a, 0x2b, 0x89, 0x7e, 0x26, 0xb5, + 0x75, 0xfd, 0x04, 0x8a, 0x57, 0x22, 0x2c, 0x7c, 0x68, 0x0d, 0x54, 0xdc, 0x73, 0x28, + 0xd0, 0xf0, 0xf2, 0xd7, 0x0b, 0x43, 0x10, 0x8c, 0xb2, 0x0c, 0x5c, 0x31, 0x16, 0x46, + 0x31, 0xb0, 0xe5, 0xb3, 0xbd, 0x31, 0xb7, 0xdf, 0x8f, 0x4c, 0x1f, 0xe1, 0x43, 0x4f, + 0xa7, 0x47, 0x56, 0x70, 0x6f, 0x83, 0x10, 0x60, 0xa5, 0xb7, 0x03, 0xdf, 0x9c, 0xd4, + 0x2e, 0x24, 0x96, 0x0e, 0x50, 0x8a, 0x04, 0x36, 0x11, 0x8d, 0x4a, 0x92, 0x07, 0xb6, + 0xd8, 0x50, 0x59, 0x6d, 0xde, 0xbe, 0x30, 0xf9, 0x28, 0xee, 0xea, 0xe7, 0x35, 0x98, + 0xfb, 0x3d, 0x86, 0x9d, 0x2d, 0x18, 0x15, 0xa9, 0xe1, 0x4d, 0x12, 0x79, 0xf7, 0xb4, + 0xb6, 0x3f, 0x4b, 0xca, 0x0f, 0x56, 0x68, 0x9b, 0xf8, 0x73, 0x3b, 0x03, 0x06, 0x49, + 0x64, 0xa4, 0xb0, 0x20, 0xb0, 0x60, 0xdc, 0xf4, 0x54, 0x71, 0xfa, 0x1d, 0x41, 0xe5, + 0xee, 0x03, 0xf9, 0xbd, 0x90, 0x65, 0x2b, 0x53, 0x72, 0x30, 0x3a, 0x3a, 0xb9, 0xbb, + 0x2e, 0xe3, 0x79, 0xb9, 0xaf, 0xcd, 0x1f, 0x6a, 0x3c, 0xb9, 0x00, 0x0b, 0xb1, 0x4e, + 0xfc, 0x33, 0x3d, 0x3d, 0x64, 0x75, 0x4a, 0x2b, 0xfc, 0x0c, 0x08, 0xe1, 0x9f, 0x5a, + 0xb8, 0x29, 0x59, 0xb5, 0xcb, 0x96, 0x49, 0x97, 0x9e, 0x3c, 0xcf, 0x75, 0xa8, 0xda, + 0xd0, 0x54, 0x60, 0x26, 0x1f, 0xcd, 0xcb, 0x00, 0x7a, 0xeb, 0xc1, 0x5e, 0x11, 0x67, + 0x5c, 0x2d, 0xb4, 0xa6, 0xcb, 0x79, 0x38, 0xe1, 0xfe, 0xb5, 0xcd, 0xdc, 0x27, 0xd6, + 0xd0, 0x75, 0x44, 0x1e, 0x16, 0xc7, 0x07, 0xf0, 0x97, 0x14, 0x47, 0x4c, 0x96, 0x16, + 0x0a, 0xa6, 0x8e, 0xaa, 0x12, 0x31, 0x79, 0x06, 0x9c, 0xd2, 0x20, 0x44, 0x06, 0x26, + 0xcd, 0xfe, 0xed, 0x65, 0xf9, 0xfa, 0xbd, 0xaa, 0x6d, 0xb1, 0x76, 0x0d, 0xa5, 0xd8, + 0x4c, 0xfd, 0x60, 0x03, 0xcf, 0xfe, 0x52, 0xfd, 0xd0, 0xd2, 0xa9, 0x80, 0x34, 0x8f, + 0x26, 0x9f, 0x5a, 0x07, 0x64, 0x2e, 0x89, 0xce, 0x26, 0x27, 0xba, 0x0e, 0x87, 0x13, + 0x9e, 0xc2, 0xdb, 0x57, 0x2d, 0x1c, 0xec, 0x82, 0x76, 0xd1, 0xa6, 0x2a, 0x47, 0x2f, + 0x61, 0x2a, 0xc9, 0xda, 0x09, 0x3a, 0x9c, 0x5f, 0xcc, 0x78, 0x11, 0x9c, 0x82, 0xbe, + 0xfd, 0x7b, 0x30, 0xff, 0x2c, 0x00, 0x59, 0x41, 0x0b, 0xfd, 0x5b, 0x32, 0x2c, 0xa5, + 0xdb, 0x69, 0x39, 0x39, 0xfa, 0x89, 0x76, 0x6f, 0xf0, 0x98, 0xad, 0x4b, 0xc6, 0x40, + 0x37, 0xa3, 0x4a, 0x73, 0x12, 0x86, 0x05, 0x72, 0x3a, 0x24, 0x1f, 0x0e, 0xb1, 0x54, + 0x0f, 0x5f, 0x5b, 0x55, 0x5b, 0x75, 0x79, 0x98, 0x0f, 0x97, 0x50, 0x46, 0x9b, 0x58, + 0xcb, 0x10, 0x70, 0x0b, 0xdf, 0xcf, 0xc6, 0x28, 0xac, 0x85, 0xc0, 0x7f, 0xb3, 0xc0, + 0x42, 0x00, 0x32, 0x70, 0x9c, 0x0e, 0xb6, 0xef, 0x2c, 0x14, 0xb4, 0x37, 0x2b, 0x58, + 0xa0, 0xde, 0x19, 0x78, 0x9c, 0x91, 0xfc, 0x99, 0x31, 0xec, 0xbc, 0xac, 0x64, 0x19, + 0xca, 0x0e, 0x5d, 0x97, 0xa3, 0xb4, 0x1c, 0x76, 0xc8, 0xa1, 0x96, 0xc7, 0xa3, 0xad, + 0xf5, 0x5b, 0xdb, 0xe6, 0x0e, 0x85, 0x59, 0x26, 0x4b, 0x6d, 0x8e, 0xf7, 0x5d, 0x26, + 0xdc, 0x72, 0x0f, 0xe5, 0xec, 0x1f, 0x59, 0x66, 0x2d, 0x95, 0xd0, 0x8e, 0x78, 0x9e, + 0x3a, 0xd1, 0x82, 0x9e, 0x40, 0x11, 0x9a, 0xa7, 0x89, 0x7d, 0x89, 0x40, 0x4d, 0xc4, + 0x96, 0x60, 0x46, 0x68, 0xf5, 0x59, 0xca, 0x67, 0x43, 0x7d, 0x2b, 0xfb, 0xb7, 0xf5, + 0x1f, 0x36, 0xe0, 0xa5, 0xb7, 0x22, 0x8f, 0x05, 0xb6, 0xec, 0x57, 0x89, 0xc1, 0x3f, + 0xc2, 0x71, 0x95, 0x56, 0x15, 0x52, 0x63, 0x96, 0x6e, 0x81, 0xf5, 0x21, 0x51, 0xe2, + 0xf6, 0xe3, 0x68, 0x69, 0xd8, 0xa3, 0xc4, 0xc4, 0x96, 0xa5, 0x13, 0x63, 0x2c, 0xaa, + 0x8a, 0xbe, 0x1f, 0x27, 0x35, 0xeb, 0x60, 0xfc, 0x12, 0x85, 0x82, 0x8e, 0xad, 0xdc, + 0x54, 0x41, 0xa4, 0x02, 0xa3, 0xbf, 0x5b, 0xcd, 0x22, 0x7c, 0xd8, 0x04, 0xe3, 0xc8, + 0xca, 0x21, 0x24, 0x3c, 0xdf, 0xcd, 0x53, 0xd8, 0x66, 0x05, 0xf3, 0xf8, 0xaf, 0x1a, + 0x9c, 0xc5, 0x69, 0x33, 0x15, 0x53, 0x28, 0x28, 0x01, 0x43, 0xfa, 0xdb, 0x3a, 0x1f, + 0xc3, 0x3d, 0x76, 0x9f, 0x07, 0xff, 0xc0, 0x1e, 0x35, 0x79, 0xe1, 0x18, 0x1f, 0x19, + 0x15, 0xdb, 0x89, 0xd8, 0x2e, 0x50, 0xbd, 0x74, 0x24, 0x08, 0x7c, 0x79, 0x7d, 0x9b, + 0x7b, 0x3b, 0x7d, 0x2a, 0x53, 0xb8, 0xff, 0xf9, 0xf2, 0xd9, 0x28, 0xab, 0x99, 0x6d, + 0xce, 0x5e, 0xd2, 0x71, 0x58, 0x98, 0xe4, 0x85, 0x8e, 0xec, 0x60, 0x78, 0xa9, 0x48, + 0x8d, 0x2d, 0xa6, 0xd1, 0x73, 0x05, 0xd0, 0xa3, 0x47, 0x18, 0x62, 0xa2, 0x22, 0x38, + 0xb9, 0xbe, 0xc2, 0x3e, 0xf2, 0xe2, 0x04, 0x1d, 0x50, 0x08, 0x73, 0x3e, 0x9e, 0xa5, + 0x66, 0x2c, 0x9f, 0xea, 0x0e, 0x4a, 0xfd, 0xf3, 0x27, 0x0c, 0x11, 0x32, 0x3b, 0xa4, + 0x8b, 0x35, 0x50, 0x85, 0x74, 0x40, 0x97, 0xf3, 0xf6, 0xc5, 0x2e, 0xe4, 0x04, 0x31, + 0x73, 0x9c, 0x5c, 0xa8, 0xdb, 0x2b, 0xda, 0x13, 0xda, 0x9b, 0x33, 0x0b, 0x62, 0x00, + 0x0b, 0x79, 0xfd, 0x35, 0x44, 0xb1, 0x31, 0x83, 0x15, 0x9d, 0x17, 0x4f, 0xfe, 0xd2, + 0x54, 0x85, 0x40, 0xa5, 0x2e, 0xe4, 0xb6, 0x2d, 0x35, 0xaa, 0x5a, 0x58, 0x63, 0xf2, + 0xba, 0xa4, 0x47, 0x5f, 0x3e, 0xb6, 0xc7, 0x35, 0x9d, 0xc8, 0x39, 0xdb, 0xc8, 0x68, + 0x90, 0xd1, 0x99, 0xd8, 0xea, 0x6c, 0x9d, 0x97, 0xf1, 0x9e, 0x79, 0x2c, 0x7b, 0xcb, + 0x66, 0x25, 0xff, 0x32, 0xb7, 0x31, 0x57, 0x5f, 0x62, 0xd9, 0x44, 0xc8, 0x06, 0xb3, + 0xf9, 0x3c, 0x04, 0xb7, 0x3a, 0x98, 0xb2, 0x73, 0x43, 0xeb, 0x25, 0xa0, 0x6c, 0x87, + 0x53, 0x60, 0xde, 0x1a, 0x14, 0x38, 0x84, 0x0a, 0xd0, 0x66, 0x1d, 0xeb, 0xdc, 0x9b, + 0x82, 0x8a, 0xd0, 0xcb, 0xc0, 0x01, 0x1b, 0x32, 0x35, 0xb2, 0xc7, 0x53, 0x77, 0x78, + 0xf4, 0x58, 0x82, 0x1b, 0x83, 0xaa, 0x4c, 0xb3, 0xe5, 0x4e, 0xd0, 0x61, 0x3e, 0x32, + 0xe6, 0x3e, 0xf9, 0x85, 0xf9, 0x35, 0xbd, 0x7f, 0xf8, 0xc7, 0x70, 0x5c, 0x89, 0xc0, + 0xbb, 0xcc, 0xda, 0x9e, 0x66, 0x5e, 0x3b, 0x06, 0xba, 0x87, 0x9f, 0xdd, 0xf3, 0x5e, + 0x0b, 0x2f, 0x60, 0xc2, 0xa7, 0x0c, 0xb8, 0xeb, 0x9d, 0xe2, 0xf5, 0xd7, 0x38, 0xc0, + 0x5e, 0x34, 0xe5, 0x0f, 0x1f, 0x26, 0x19, 0x25, 0x8b, 0x89, 0xe5, 0x73, 0xda, 0x55, + 0x75, 0x46, 0x3d, 0x2e, 0x3b, 0xce, 0x39, 0xf7, 0x0e, 0xb4, 0x55, 0x26, 0xcd, 0x99, + 0xfa, 0xd9, 0x0f, 0x97, 0x92, 0xd0, 0xcd, 0x59, 0x3b, 0xa8, 0x6a, 0xa1, 0xae, 0xa5, + 0x03, 0xdd, 0xca, 0x5e, 0x3e, 0x57, 0x37, 0xe6, 0xfc, 0x7b, 0xab, 0x27, 0x85, 0x12, + 0x69, 0x20, 0xc4, 0x47, 0xd5, 0xe5, 0x6a, 0x75, 0xdb, 0xe8, 0x9d, 0x68, 0x8b, 0xc0, + 0xda, 0xa7, 0x9a, 0xa6, 0x2d, 0xe9, 0xea, 0x29, 0x55, 0xf7, 0x1e, 0x1a, 0x61, 0x68, + 0x2a, 0x61, 0x78, 0xf8, 0x0b, 0xca, 0xda, 0x3b, 0x97, 0xae, 0xec, 0x77, 0xd9, 0xc8, + 0x56, 0x3b, 0x06, 0x9e, 0xa0, 0x13, 0x2f, 0x72, 0x3f, 0xbe, 0x75, 0x60, 0x2d, 0xd6, + 0x29, 0xac, 0x48, 0x09, 0x93, 0xd3, 0x71, 0x4f, 0xf0, 0x2c, 0x97, 0x0e, 0xbd, 0x83, + 0xe6, 0xd6, 0xcb, 0xbe, 0x39, 0x08, 0x6b, 0x03, 0x54, 0x20, 0xe0, 0xc2, 0x75, 0x62, + 0x86, 0x58, 0xa3, 0xba, 0x92, 0x30, 0x5c, 0xc0, 0x76, 0x98, 0xf1, 0x2e, 0xe1, 0xe4, + 0x17, 0x13, 0x70, 0xac, 0x39, 0xdf, 0x0e, 0x46, 0x6d, 0xc8, 0xec, 0xc3, 0x9d, 0xa5, + 0xee, 0x47, 0xb6, 0x82, 0x9d, 0xbb, 0xa9, 0x97, 0x0f, 0x03, 0x58, 0xed, 0x68, 0x26, + 0x49, 0x60, 0x5c, 0x7b, 0xfe, 0xe6, 0x93, 0x1a, 0x29, 0x5b, 0x14, 0xa3, 0x40, 0x76, + 0x00, 0x07, 0x4e, 0xdc, 0x79, 0xfa, 0x61, 0xe6, 0x80, 0x6f, 0x11, 0x08, 0xd3, 0x34, + 0xb4, 0xa5, 0x90, 0xf7, 0xa0, 0x26, 0xb0, 0xeb, 0x02, 0x80, 0x4d, 0x39, 0x17, 0x46, + 0x6e, 0x99, 0x91, 0x20, 0x64, 0x1c, 0xe0, 0x7e, 0xbc, 0xdc, 0x99, 0x42, 0x60, 0x82, + 0xe0, 0x77, 0x1f, 0x15, 0x9c, 0x82, 0x6a, 0x9b, 0xe6, 0xce, 0xd7, 0x2d, 0x0e, 0x9c, + 0xfa, 0x5b, 0x4b, 0x8a, 0x86, 0x40, 0xca, 0x34, 0x88, 0xa1, 0xeb, 0x2b, 0x6e, 0x37, + 0x4e, 0x8c, 0x2e, 0x00, 0x3c, 0xdf, 0xa2, 0x32, 0x10, 0x37, 0x48, 0xb5, 0xc9, 0xdc, + 0x11, 0xbb, 0x30, 0xf6, 0x46, 0xb9, 0x73, 0xd7, 0x83, 0xf5, 0x99, 0x14, 0x17, 0x4e, + 0x48, 0xbd, 0x6a, 0x84, 0xfa, 0xd8, 0x9d, 0xbc, 0xa5, 0xc7, 0x6d, 0x0a, 0xb4, 0x14, + 0x5a, 0xbd, 0x08, 0xe4, 0xd0, 0xf2, 0xc7, 0x60, 0x25, 0xfc, 0x85, 0xfc, 0x11, 0x6c, + 0xca, 0x8d, 0x30, 0x2c, 0x8a, 0x3b, 0xeb, 0x26, 0x60, 0x3a, 0x1a, 0xf1, 0xb5, 0x93, + 0x91, 0xea, 0xf4, 0x71, 0x75, 0x9a, 0xdf, 0x19, 0x4c, 0x40, 0xc2, 0x09, 0x29, 0x8c, + 0xc0, 0x51, 0xfc, 0x79, 0x03, 0xfe, 0x40, 0x90, 0x2c, 0x35, 0x6f, 0x28, 0x27, 0x9f, + 0x27, 0x94, 0xbb, 0xb9, 0xe0, 0x0b, 0x1e, 0x22, 0x0e, 0x55, 0xb6, 0x76, 0xa1, 0x8a, + 0x9c, 0xad, 0xb8, 0x8b, 0x5b, 0x14, 0x8d, 0x38, 0xf3, 0x80, 0x90, 0xed, 0xc4, 0xf2, + 0x6f, 0x14, 0x90, 0xb6, 0xa1, 0x7c, 0xf9, 0x9f, 0x9a, 0x7c, 0x45, 0x8c, 0x3b, 0x31, + 0x82, 0x3f, 0xdf, 0x69, 0x57, 0x8c, 0x47, 0xdb, 0x5b, 0x3d, 0xda, 0x86, 0xaa, 0xb1, + 0xec, 0x9f, 0x58, 0xd9, 0x62, 0x26, 0xc6, 0xb9, 0x1d, 0xc0, 0xf0, 0x3f, 0xe8, 0xd7, + 0xdf, 0x23, 0x0f, 0x07, 0xb2, 0xfb, 0x94, 0x87, 0x76, 0x60, 0x1e, 0x9c, 0x83, 0xf6, + 0xc1, 0xcf, 0x87, 0x6f, 0xc8, 0xed, 0x44, 0xad, 0xa0, 0xe1, 0x60, 0x8f, 0x48, 0x5c, + 0x6d, 0x75, 0x67, 0x8b, 0x3c, 0x00, 0xe9, 0x67, 0xd3, 0x4a, 0x9c, 0xf1, 0x02, 0x8c, + 0x17, 0x05, 0xfa, 0x37, 0x67, 0xf4, 0x6d, 0x4b, 0xab, 0x70, 0x28, 0xb0, 0x9b, 0x20, + 0x38, 0xfc, 0x1b, 0x72, 0x7f, 0x61, 0x9e, 0x61, 0xc4, 0xfc, 0x16, 0xbf, 0xfe, 0x65, + 0x7e, 0x99, 0x12, 0x6a, 0xc5, 0x18, 0x4f, 0xc8, 0x7f, 0x5e, 0x53, 0x01, 0x88, 0x64, + 0x23, 0xb3, 0x56, 0x87, 0x59, 0x09, 0xec, 0x92, 0xb3, 0x2d, 0x33, 0x08, 0x42, 0x53, + 0xa1, 0xb9, 0x7c, 0x5d, 0x2e, 0xd6, 0x6c, 0x7e, 0x22, 0xd1, 0x85, 0x58, 0xfe, 0x82, + 0xb5, 0xec, 0x88, 0xc6, 0x07, 0x05, 0x82, 0xfa, 0xcf, 0x75, 0x6d, 0x70, 0x32, 0x38, + 0xd9, 0xaf, 0x94, 0x19, 0x96, 0x6b, 0xe4, 0x62, 0xdf, 0xbd, 0x31, 0x5c, 0x5b, 0xfa, + 0xf0, 0x44, 0xaa, 0x69, 0x5a, 0x05, 0xe6, 0x9d, 0x3d, 0x41, 0xe7, 0x73, 0x78, 0x75, + 0x1d, 0x4e, 0x02, 0xc2, 0x66, 0xdf, 0xb5, 0xcb, 0x6a, 0x7c, 0x40, 0x08, 0xf9, 0x44, + 0x88, 0x83, 0x11, 0xe6, 0xde, 0x37, 0xdc, 0x7b, 0xdf, 0x65, 0xd7, 0x0c, 0xab, 0x3e, + 0x07, 0x8a, 0xb4, 0x4e, 0x23, 0x2b, 0x41, 0x1c, 0xaf, 0xb2, 0x88, 0x4e, 0x26, 0x45, + 0x95, 0xbe, 0xed, 0xf9, 0xd4, 0x9a, 0x79, 0x36, 0xbb, 0x28, 0x7f, 0xe2, 0x8e, 0x1c, + 0x29, 0x63, 0x5e, 0xae, 0xca, 0x74, 0x7d, 0x06, 0x87, 0xcf, 0x46, 0x59, 0x02, 0xd2, + 0x5f, 0x5e, 0x51, 0x58, 0x48, 0x1d, 0xaa, 0xcd, 0xd3, 0x00, 0xb4, 0x77, 0x40, 0xbc, + 0x0c, 0x62, 0x77, 0xb4, 0x47, 0xcc, 0x26, 0x64, 0x04, 0x42, 0x43, 0xdd, 0x48, 0x11, + 0x40, 0x4e, 0xcb, 0xd7, 0xc7, 0xa6, 0x3c, 0x9f, 0xb7, 0xd9, 0x37, 0xbc, 0xd8, 0x12, + 0xc2, 0x34, 0x59, 0x23, 0xb5, 0x90, 0x26, 0x83, 0xbd, 0x2e, 0xd5, 0x4c, 0x01, 0xae, + 0x04, 0x19, 0xa7, 0xf5, 0x4e, 0x8a, 0x3a, 0x59, 0xc6, 0xa6, 0xda, 0xcf, 0x89, 0xc7, + 0x37, 0x0e, 0x79, 0xb5, 0x60, 0x13, 0x6a, 0x2b, 0x00, 0xdd, 0xb6, 0x07, 0x4d, 0x74, + 0xff, 0xc5, 0xc5, 0xdf, 0xd0, 0x6b, 0x6c, 0x51, 0x9a, 0xbe, 0xc3, 0x59, 0x6a, 0x47, + 0x61, 0x13, 0xbe, 0x41, 0x38, 0xee, 0xad, 0x5f, 0xfd, 0xe8, 0x6b, 0x1e, 0x32, 0x40, + 0x1f, 0xa3, 0x84, 0x62, 0x32, 0xd0, 0xb3, 0xc9, 0xbd, 0x56, 0x88, 0xb6, 0x4a, 0x33, + 0x09, 0x38, 0x16, 0x2a, 0x8b, 0x89, 0x29, 0xd7, 0x0c, 0x1b, 0x67, 0x53, 0x62, 0xf4, + 0xc2, 0xa9, 0xbb, 0x6b, 0x7f, 0x91, 0xeb, 0xd4, 0x7d, 0x26, 0x3c, 0xf0, 0xa4, 0x05, + 0xa2, 0x8b, 0xa7, 0x41, 0x56, 0x44, 0xf9, 0x3b, 0x6c, 0xdf, 0xa3, 0xec, 0xeb, 0xb7, + 0xb8, 0xd4, 0xee, 0x8b, 0x94, 0xb2, 0x7b, 0x61, 0xe4, 0x03, 0x5e, 0xd6, 0xa4, 0x77, + 0x46, 0x7f, 0x4a, 0x32, 0x0b, 0x8a, 0x4e, 0xba, 0x0a, 0xb5, 0x6c, 0x26, 0x3e, 0x4b, + 0xfb, 0xe2, 0x6a, 0x41, 0x8e, 0xd1, 0xcd, 0xe6, 0x18, 0x4b, 0x89, 0x50, 0xfe, 0x7a, + 0xac, 0x7f, 0x20, 0xa4, 0x7b, 0xa1, 0xbf, 0xf9, 0x80, 0x4f, 0x53, 0xf6, 0x93, 0x23, + 0xdb, 0x84, 0x75, 0x20, 0xa6, 0x58, 0x47, 0xb3, 0x03, 0x4c, 0x4e, 0x08, 0x1b, 0xb4, + 0xb8, 0x69, 0x26, 0x3b, 0x5f, 0x9b, 0x3a, 0x7a, 0x83, 0x3b, 0x6e, 0x4c, 0xa7, 0x90, + 0xcc, 0xf9, 0xfd, 0xae, 0x80, 0x79, 0xe5, 0x56, 0x09, 0x27, 0x2c, 0x63, 0xb5, 0x49, + 0xb0, 0xc8, 0x5f, 0x11, 0x0c, 0xc9, 0xc9, 0x58, 0x68, 0x01, 0x14, 0xb3, 0x11, 0x74, + 0x80, 0xaf, 0x57, 0xcb, 0x15, 0x9e, 0xdf, 0xbe, 0x5c, 0xb9, 0xc6, 0x2b, 0xce, 0x2c, + 0xf2, 0xab, 0x29, 0xb6, 0x67, 0x11, 0xac, 0x7a, 0xa5, 0x3a, 0x74, 0x9f, 0xfa, 0x83, + 0x90, 0x7e, 0xcb, 0x69, 0x12, 0xaa, 0x56, 0x96, 0x38, 0xde, 0xa1, 0x9e, 0x54, 0x41, + 0x61, 0x1e, 0xfc, 0xa3, 0x20, 0x99, 0x65, 0x3e, 0x8a, 0x5c, 0xa1, 0xfb, 0xbd, 0xba, + 0xb1, 0xd6, 0x44, 0x71, 0xec, 0x32, 0x0e, 0xc3, 0x8e, 0xa4, 0x88, 0x40, 0x0c, 0x9b, + 0x1f, 0x4e, 0x8c, 0xb5, 0x48, 0x0c, 0x0e, 0x92, 0x42, 0xb0, 0x86, 0xa8, 0x0e, 0xee, + 0xd4, 0x90, 0xae, 0x32, 0x00, 0x0c, 0x80, 0x09, 0xec, 0xb7, 0x1f, 0xfa, 0x39, 0xf4, + 0xf3, 0xb5, 0x74, 0x9c, 0xfd, 0x1b, 0xef, 0xe0, 0xd9, 0x66, 0x7a, 0xb3, 0x02, 0x20, + 0xc2, 0xdc, 0x04, 0x39, 0x36, 0x98, 0xb2, 0xcf, 0xa2, 0x04, 0x92, 0xf2, 0x50, 0xce, + 0x14, 0x32, 0x35, 0x81, 0x58, 0x70, 0x3d, 0xf7, 0xb1, 0x39, 0xd7, 0x45, 0xce, 0x1f, + 0xc3, 0x40, 0x78, 0x77, 0x01, 0xfb, 0x51, 0xdd, 0x5e, 0x48, 0xb8, 0x95, 0x09, 0x41, + 0x7d, 0x88, 0x89, 0x00, 0x80, 0x63, 0xf9, 0xba, 0x01, 0x5a, 0x07, 0xd8, 0xd3, 0x9b, + 0xbd, 0x00, 0x76, 0x2f, 0x59, 0x5a, 0xfa, 0xd8, 0xd8, 0x59, 0xea, 0xab, 0xf0, 0xd8, + 0x2d, 0x46, 0x33, 0xcf, 0x82, 0x98, 0xb0, 0x9b, 0xea, 0x3f, 0x22, 0x28, 0x55, 0xa9, + 0x2a, 0x08, 0x43, 0xf5, 0x2f, 0xa5, 0x8d, 0xb3, 0xa1, 0x75, 0xc3, 0x0d, 0x2a, 0xbe, + 0x64, 0x82, 0x64, 0x90, 0xcb, 0xe6, 0xca, 0x14, 0x88, 0xfe, 0x3a, 0x01, 0x5a, 0x94, + 0x6d, 0xc9, 0xc4, 0x5a, 0xc3, 0x09, 0x25, 0x72, 0x7a, 0x13, 0xe0, 0x89, 0x78, 0xf7, + 0x24, 0x03, 0x47, 0x20, 0x8a, 0x4d, 0x25, 0x38, 0xc2, 0xd5, 0x61, 0x24, 0x37, 0x8c, + 0x22, 0xc0, 0x4e, 0x23, 0xdc, 0x28, 0xb1, 0x50, 0x19, 0xbe, 0x77, 0x6d, 0x70, 0xbf, + 0xc1, 0xd2, 0x64, 0x5b, 0x5e, 0x80, 0xd1, 0xfd, 0x84, 0x19, 0xdf, 0x72, 0x90, 0x43, + 0x80, 0xe2, 0xe1, 0xfc, 0x4d, 0xd1, 0xdf, 0x1b, 0xa3, 0xdf, 0xe4, 0x80, 0xcc, 0x84, + 0x6d, 0x51, 0x51, 0x4a, 0x06, 0x5e, 0xd7, 0x62, 0x78, 0x7a, 0xfd, 0x6e, 0xb9, 0x0b, + 0xdf, 0x8f, 0xbb, 0xad, 0x5e, 0xb3, 0xd2, 0x3f, 0xdc, 0x8c, 0x54, 0xcc, 0xa1, 0x0f, + 0xa1, 0xfe, 0x54, 0x64, 0x82, 0xf5, 0xe1, 0x42, 0x4b, 0xfd, 0xa8, 0x7a, 0xa7, 0xfb, + 0x78, 0x6e, 0x26, 0x0f, 0x26, 0x14, 0xbe, 0x08, 0x11, 0xee, 0x16, 0xb8, 0xd2, 0x9d, + 0xf9, 0xa0, 0xf3, 0x30, 0xe9, 0x70, 0x9f, 0x63, 0xc9, 0x50, 0xfb, 0xd9, 0x03, 0xff, + 0x7d, 0x5b, 0x0c, 0xa2, 0x9f, 0xd6, 0x3b, 0x0f, 0x97, 0x51, 0x77, 0x69, 0x02, 0x5c, + 0xc3, 0x6a, 0x52, 0xe0, 0xc1, 0x15, 0x93, 0x4a, 0x3c, 0xa2, 0x58, 0xb8, 0xba, 0xb9, + 0x00, 0x16, 0xa4, 0x01, 0xd5, 0xd8, 0xd7, 0xc3, 0xb9, 0x44, 0x92, 0x5b, 0x35, 0xa9, + 0x34, 0x9a, 0x1a, 0xc7, 0xd9, 0x85, 0x21, 0x61, 0x0c, 0x2f, 0xad, 0x8b, 0x5c, 0x8b, + 0x31, 0x9c, 0xd6, 0xe0, 0x5f, 0x9b, 0xbe, 0xd3, 0x53, 0xf1, 0xd0, 0xc8, 0x65, 0xa9, + 0x4a, 0xa4, 0x56, 0xdc, 0xd1, 0x8a, 0x39, 0xe2, 0xf5, 0x85, 0xd9, 0xbe, 0xa8, 0x4e, + 0xb5, 0xf0, 0xaf, 0x8b, 0x45, 0x77, 0x94, 0x98, 0xc9, 0xae, 0x1f, 0x75, 0x5d, 0x9f, + 0x90, 0xa2, 0xc3, 0x27, 0x3e, 0x52, 0xaa, 0xd3, 0xca, 0x34, 0xb4, 0x43, 0x79, 0x1b, + 0x02, 0x99, 0x94, 0xb1, 0xee, 0x4c, 0x40, 0xfc, 0xa0, 0x05, 0x35, 0x2b, 0x8d, 0x6d, + 0x28, 0x69, 0x83, 0x17, 0x7d, 0x65, 0x5b, 0x6f, 0x34, 0xc4, 0x99, 0x32, 0x2b, 0x65, + 0xda, 0x6e, 0xb6, 0xb9, 0xe1, 0xf4, 0xd5, 0x90, 0x21, 0x25, 0xb6, 0x4c, 0x93, 0xda, + 0x74, 0xcc, 0x1a, 0x35, 0x60, 0x18, 0xb0, 0x09, 0x3b, 0xb5, 0xcc, 0x82, 0x05, 0xb2, + 0x69, 0x2f, 0x6d, 0x3e, 0x9c, 0x1c, 0xc8, 0x85, 0x41, 0xb4, 0xd9, 0x83, 0x84, 0x54, + 0x85, 0xb4, 0x50, 0xcd, 0x4b, 0x98, 0x2a, 0xba, 0x8d, 0x2e, 0x91, 0xf4, 0x1f, 0x22, + 0xee, 0xe7, 0xf3, 0x6d, 0x79, 0xcc, 0xa9, 0xc0, 0xe0, 0x1b, 0x26, 0xc4, 0x65, 0x11, + 0x18, 0xea, 0x77, 0x15, 0x14, 0xc7, 0x7e, 0xd6, 0x0c, 0xd5, 0x24, 0x51, 0x94, 0x2d, + 0xc8, 0x5b, 0x3f, 0xba, 0x44, 0x8b, 0x2d, 0x63, 0x10, 0xf2, 0x77, 0x79, 0x42, 0x83, + 0x2e, 0x21, 0xcf, 0x3d, 0x44, 0x87, 0x4f, 0x8d, 0x04, 0xa8, 0x05, 0x26, 0xc6, 0x9f, + 0xd3, 0xb5, 0x10, 0x49, 0xe6, 0x92, 0xba, 0x45, 0xa7, 0x02, 0xee, 0x12, 0x51, 0x4a, + 0xc2, 0xe1, 0x89, 0x4f, 0x9b, 0x83, 0xd7, 0x56, 0xd0, 0x93, 0x96, 0x97, 0xca, 0x98, + 0x2b, 0x68, 0x7c, 0x9e, 0xd7, 0xe0, 0xb2, 0x32, 0x77, 0x07, 0x3c, 0x19, 0x30, 0xa4, + 0x73, 0xd1, 0x66, 0x8e, 0xf2, 0xe9, 0xae, 0x96, 0x63, 0xcf, 0xf0, 0x58, 0x16, 0x62, + 0x6c, 0xd3, 0xc5, 0xbf, 0x77, 0x16, 0x53, 0xd7, 0x78, 0x51, 0x81, 0x35, 0x5c, 0x05, + 0xae, 0xd2, 0x4a, 0x99, 0xc4, 0xb6, 0x74, 0xd2, 0x4a, 0x0f, 0x08, 0xf4, 0xb0, 0xcf, + 0xbe, 0x90, 0xf2, 0xfd, 0xba, 0xb4, 0x24, 0x82, 0xe9, 0x8f, 0x13, 0xff, 0xfc, 0xd1, + 0xad, 0x33, 0xf4, 0xf4, 0xc0, 0x4d, 0xeb, 0xc8, 0x9f, 0x40, 0xb5, 0xdb, 0xf6, 0x45, + 0x46, 0xc5, 0x20, 0xdc, 0xa5, 0xd0, 0xec, 0xf3, 0xf6, 0x5d, 0x3a, 0x77, 0xd0, 0x12, + 0x9f, 0x60, 0x03, 0x71, 0x10, 0x8a, 0xac, 0x30, 0xa9, 0xec, 0xa8, 0xbe, 0xe5, 0x52, + 0x4f, 0xab, 0x67, 0x1f, 0xc0, 0x86, 0x58, 0x76, 0x2c, 0x87, 0x38, 0xab, 0xc9, 0xfa, + 0x76, 0x93, 0xe3, 0x9d, 0x39, 0xd7, 0x03, 0xd5, 0xcd, 0x94, 0x2b, 0x5a, 0x55, 0xfe, + 0xda, 0xfe, 0xcc, 0xae, 0xf7, 0x02, 0x17, 0x69, 0xe9, 0x2c, 0xc9, 0xd3, 0xac, 0x7b, + 0x4c, 0x23, 0xb3, 0x3f, 0xc2, 0x23, 0x21, 0x85, 0x4b, 0xa3, 0x3f, 0x49, 0xee, 0xba, + 0xdd, 0xca, 0x29, 0xb3, 0x56, 0x40, 0xe4, 0xf0, 0xc2, 0xfd, 0x8c, 0x12, 0xb9, 0x84, + 0x52, 0x97, 0x60, 0xe0, 0x65, 0xfe, 0xcb, 0xa1, 0x21, 0x86, 0xd2, 0x0a, 0xee, 0xc3, + 0xda, 0x58, 0xfc, 0x35, 0x9b, 0xa8, 0x25, 0xe5, 0xb8, 0xe2, 0xe1, 0x8f, 0x12, 0xcf, + 0x29, 0x49, 0xc3, 0x12, 0xf6, 0x3c, 0x4d, 0xd7, 0xa7, 0x9b, 0x0e, 0x66, 0xb9, 0xc8, + 0xb6, 0x6f, 0xe8, 0x9a, 0xd7, 0xed, 0xc6, 0x2a, 0xc4, 0xd2, 0x07, 0xe2, 0x77, 0xb9, + 0x33, 0xb0, 0xc2, 0x06, 0xdd, 0x7c, 0x22, 0xd2, 0xdb, 0x26, 0x33, 0xfc, 0x01, 0xa8, + 0x3c, 0x24, 0xfc, 0xad, 0x40, 0x9c, 0xee, 0xd5, 0x36, 0xa6, 0xd3, 0xe8, 0xe0, 0x8d, + 0x42, 0xb5, 0x13, 0x48, 0x97, 0xb4, 0x36, 0xbf, 0xf3, 0xa1, 0xbc, 0xef, 0xc5, 0x3a, + 0xec, 0x30, 0xed, 0x89, 0x11, 0x0f, 0x90, 0x10, 0x97, 0x8d, 0xf7, 0x0c, 0xe4, 0xac, + 0x6f, 0x1d, 0x60, 0x25, 0x50, 0xcf, 0x20, 0xe4, 0x44, 0x36, 0x06, 0x3e, 0x3a, 0x15, + 0xb5, 0x1e, 0xcb, 0xaa, 0x4a, 0x59, 0xdf, 0x2f, 0xe0, 0x15, 0xcb, 0x36, 0x37, 0xf3, + 0x72, 0x83, 0x04, 0xec, 0x3a, 0x72, 0x4f, 0x31, 0x49, 0x27, 0x5e, 0x7b, 0x63, 0x4b, + 0xd8, 0x82, 0x78, 0xd9, 0x3f, 0xab, 0x6b, 0x94, 0x16, 0x68, 0xd9, 0x13, 0xdb, 0xcd, + 0x89, 0x21, 0x3f, 0x3b, 0xac, 0xfc, 0xfd, 0x20, 0x02, 0xea, 0x86, 0x6f, 0x3f, 0x17, + 0x07, 0x35, 0x12, 0x64, 0xb6, 0x67, 0x88, 0xf4, 0xeb, 0x7f, 0x68, 0xc5, 0xa5, 0x36, + 0xfa, 0x9c, 0x13, 0x0d, 0x8f, 0x6d, 0xa1, 0xbb, 0x03, 0x1d, 0xf9, 0xe2, 0x20, 0xd8, + 0xca, 0x8b, 0xab, 0x46, 0xdd, 0xcf, 0x9c, 0x35, 0xfa, 0x63, 0x48, 0x09, 0xa7, 0x3d, + 0xcd, 0x91, 0xb7, 0x9f, 0x5b, 0xcb, 0x98, 0x7b, 0x20, 0x54, 0x4b, 0xb5, 0x2a, 0xaf, + 0x0d, 0x9e, 0x3a, 0xea, 0x91, 0x18, 0x3b, 0x8c, 0x48, 0x12, 0x78, 0x6c, 0x8d, 0xc9, + 0xb9, 0x30, 0x73, 0xa3, 0x05, 0x26, 0x71, 0xb3, 0x71, 0x50, 0x52, 0x5d, 0x59, 0x24, + 0xaa, 0x6e, 0xe5, 0xe0, 0x36, 0xc1, 0xbe, 0xb9, 0xda, 0xf6, 0xf9, 0x4d, 0x05, 0x10, + 0x0b, 0x2d, 0xdd, 0x36, 0xb1, 0x3c, 0x4d, 0xf9, 0xd4, 0x56, 0xf6, 0x48, 0x0b, 0xb1, + 0xaf, 0xa6, 0x20, 0x26, 0xea, 0x80, 0x97, 0x94, 0xd3, 0xb7, 0x4d, 0x78, 0x01, 0x7e, + 0xe0, 0xfb, 0xca, 0x83, 0xcc, 0x7e, 0x5c, 0xbd, 0x52, 0x7a, 0xcd, 0xe7, 0x46, 0x53, + 0x73, 0x51, 0x2c, 0x07, 0x64, 0x6a, 0x62, 0xc6, 0x0f, 0x5c, 0x16, 0xc2, 0xef, 0x9f, + 0x41, 0x8d, 0x8c, 0x7d, 0x18, 0x8f, 0x7b, 0x13, 0xdd, 0x45, 0x38, 0xa5, 0x5d, 0x18, + 0x6a, 0xd6, 0x36, 0x2a, 0x58, 0x9a, 0x9f, 0x52, 0xb2, 0x5e, 0x61, 0x6f, 0xb2, 0xa3, + 0x57, 0xac, 0xca, 0xde, 0x63, 0x57, 0xfa, 0x5a, 0x42, 0xa7, 0x98, 0xe4, 0x17, 0x13, + 0x11, 0xad, 0xe9, 0xcc, 0xfd, 0x15, 0xf2, 0x7c, 0x8c, 0x19, 0x72, 0x17, 0x9d, 0x26, + 0x1f, 0xb9, 0xb0, 0x9b, 0xc7, 0xa0, 0x36, 0xc1, 0x05, 0x55, 0x9b, 0x04, 0x38, 0x9d, + 0xfd, 0x8a, 0x7b, 0xe2, 0xa3, 0xae, 0x2b, 0xba, 0x2a, 0xfb, 0xd1, 0xe9, 0xbf, 0x90, + 0x05, 0xc8, 0xb3, 0x66, 0x35, 0x4f, 0x90, 0x9b, 0xe7, 0x1e, 0x52, 0xc0, 0x90, 0x80, + 0xfb, 0xa7, 0x45, 0x23, 0x77, 0xe8, 0xf1, 0x2c, 0x18, 0x4f, 0xe7, 0xed, 0x46, 0x5b, + 0x32, 0xc9, 0xf9, 0xb2, 0x81, 0x9e, 0xa1, 0xd1, 0x19, 0xfc, 0x26, 0x7c, 0x8a, 0x75, + 0x33, 0x81, 0xeb, 0x51, 0xac, 0xf8, 0x54, 0xc1, 0x9e, 0x8d, 0x58, 0xff, 0x42, 0x74, + 0xeb, 0xa8, 0xc6, 0x3f, 0x0f, 0xa1, 0x70, 0xa6, 0x3c, 0xbf, 0xce, 0x2c, 0xf8, 0x7b, + 0xdc, 0xdf, 0x32, 0xb7, 0xe1, 0x98, 0x04, 0x54, 0x1c, 0x2c, 0x58, 0x97, 0x24, 0xef, + 0xc6, 0x9b, 0xc4, 0x65, 0xd0, 0x90, 0x8e, 0x09, 0xb8, 0x4d, 0x1f, 0x50, 0x41, 0x2b, + 0xb0, 0x7f, 0x47, 0xfb, 0x9f, 0x0d, 0x47, 0x29, 0x28, 0x16, 0x14, 0xca, 0xca, 0xb6, + 0x14, 0xef, 0x65, 0xce, 0xba, 0x13, 0x96, 0xb5, 0x24, 0x9d, 0x2c, 0x61, 0x70, 0x4f, + 0xb6, 0xf3, 0x48, 0x44, 0x71, 0x83, 0xf9, 0x88, 0x2a, 0x98, 0xae, 0x9c, 0x71, 0xa7, + 0x66, 0x33, 0xe0, 0x5b, 0x33, 0x3a, 0x1b, 0xce, 0xee, 0xc9, 0xbd, 0x44, 0xb8, 0x87, + 0x6f, 0xab, 0x6c, 0xd7, 0x2a, 0x5e, 0x33, 0x5c, 0x97, 0x7a, 0x8c, 0x56, 0xca, 0x16, + 0x7b, 0x1a, 0x19, 0x8e, 0x93, 0x1b, 0xf2, 0x85, 0xf6, 0x86, 0x81, 0xfc, 0x5a, 0xca, + 0x84, 0x66, 0x76, 0xe8, 0x9b, 0x17, 0xee, 0x76, 0x9a, 0x08, 0xf9, 0xb4, 0x60, 0xfe, + 0x4e, 0x48, 0x81, 0xf9, 0xb2, 0x0f, 0xed, 0xb3, 0x9d, 0x1f, 0xc6, 0x66, 0x5d, 0x10, + 0x6b, 0xaa, 0x5a, 0x93, 0x14, 0x0d, 0x1d, 0xda, 0xca, 0xe4, 0xa7, 0x59, 0x0f, 0x5a, + 0xb0, 0x78, 0x52, 0xc1, 0x81, 0x1f, 0x1a, 0x03, 0x5c, 0x3f, 0x1a, 0x60, 0xb1, 0x54, + 0x22, 0x6c, 0x9d, 0xb0, 0x8f, 0xfd, 0xd0, 0xb6, 0xde, 0xee, 0x72, 0x2a, 0x90, 0x07, + 0x6c, 0xa7, 0xc6, 0xd6, 0x04, 0xfe, 0x83, 0x32, 0x86, 0x8e, 0x1d, 0x59, 0x32, 0x2f, + 0x26, 0x2b, 0xbf, 0xbe, 0x95, 0xcc, 0x5b, 0x9b, 0x1e, 0x20, 0x31, 0x0b, 0x76, 0x35, + 0x0b, 0x4d, 0x60, 0x4c, 0xd1, 0xa4, 0x58, 0x66, 0x1d, 0xc4, 0x74, 0xfe, 0x4c, 0x58, + 0x79, 0x04, 0xc0, 0x53, 0x47, 0x5e, 0x17, 0x61, 0xb8, 0x0a, 0x60, 0xcc, 0x48, 0xed, + 0xd9, 0x54, 0x34, 0xdf, 0x02, 0x3b, 0x94, 0xa5, 0x8a, 0x99, 0xd6, 0x25, 0x66, 0xe0, + 0x0f, 0x67, 0x77, 0x90, 0xdc, 0xa0, 0x76, 0xa4, 0xf1, 0x67, 0x47, 0x0c, 0x43, 0xa8, + 0x1e, 0x6c, 0x32, 0xf0, 0xd0, 0x0d, 0x23, 0x65, 0x6b, 0xa7, 0x48, 0x28, 0xb8, 0xe4, + 0xd4, 0x75, 0x38, 0xe5, 0x0c, 0x0e, 0xce, 0xe2, 0xcd, 0xfe, 0x0d, 0x59, 0x43, 0xe2, + 0x3e, 0x3f, 0x17, 0x33, 0x82, 0x9d, 0x3e, 0x1b, 0x80, 0x53, 0x93, 0x30, 0xe0, 0x6c, + 0x6a, 0xe3, 0xd0, 0xec, 0xe7, 0x38, 0xc0, 0xdd, 0x74, 0x2a, 0xa5, 0x86, 0x0f, 0x43, + 0xb5, 0x30, 0xf0, 0x3d, 0xc5, 0x5d, 0xeb, 0xf7, 0x20, 0x12, 0x3f, 0x8f, 0xba, 0xf2, + 0xe5, 0x68, 0x59, 0xa5, 0x34, 0x3d, 0x46, 0x12, 0xee, 0x21, 0x46, 0x4d, 0xb2, 0x50, + 0x1d, 0x4f, 0x35, 0x31, 0x47, 0xf3, 0xe1, 0xa5, 0xab, 0xb8, 0x93, 0x85, 0x08, 0x16, + 0xc8, 0x0a, 0xf2, 0x9d, 0x88, 0x92, 0x48, 0xc9, 0x2a, 0x72, 0x9a, 0x0e, 0x2b, 0xe2, + 0xb6, 0x6c, 0xc1, 0x3a, 0xc5, 0xd9, 0x96, 0xb2, 0x50, 0x14, 0x66, 0x6d, 0xdc, 0x63, + 0x8a, 0x1f, 0xd2, 0xa0, 0xaf, 0xee, 0x93, 0xd9, 0x8e, 0x31, 0xdc, 0x1e, 0xa8, 0x58, + 0xd7, 0x2b, 0x84, 0xbb, 0xd3, 0x2f, 0xc0, 0xc6, 0x16, 0xe7, 0xd4, 0xab, 0xda, 0xf3, + 0xc1, 0x8f, 0xf9, 0x60, 0x13, 0x24, 0x5d, 0x83, 0xb3, 0xbd, 0xf9, 0x21, 0xf4, 0x03, + 0xf1, 0xae, 0xcf, 0xdd, 0xd8, 0x85, 0xfd, 0xcf, 0xc7, 0x33, 0x87, 0x0f, 0x76, 0x0c, + 0xb8, 0x7e, 0xd4, 0xfc, 0xd9, 0xcc, 0xa9, 0x33, 0x2e, 0x8e, 0x1c, 0x85, 0x62, 0x3b, + 0x20, 0x66, 0x09, 0xf8, 0x87, 0xeb, 0xdb, 0xcf, 0x9d, 0xa1, 0x0f, 0x38, 0x14, 0x19, + 0x7a, 0x9f, 0x82, 0x07, 0x05, 0xea, 0xa1, 0x28, 0x3a, 0xc7, 0x93, 0x16, 0x83, 0x08, + 0x3f, 0x22, 0xfc, 0x4d, 0xc7, 0xff, 0x68, 0x1a, 0xb8, 0x46, 0x18, 0x6f, 0x22, 0xd5, + 0x73, 0x08, 0x43, 0xde, 0x71, 0x00, 0xf0, 0x31, 0x17, 0xa3, 0xbb, 0xa0, 0x64, 0xca, + 0x3c, 0xea, 0x93, 0xf3, 0xab, 0xd3, 0x0b, 0xe6, 0xdb, 0x09, 0x35, 0x52, 0x9d, 0xed, + 0x0b, 0x50, 0xec, 0xef, 0x9f, 0x59, 0x6d, 0xb0, 0x1a, 0x87, 0xa8, 0xda, 0xdb, 0x82, + 0x7a, 0x1b, 0xe8, 0xb5, 0x79, 0x9b, 0x33, 0xc9, 0x9a, 0x82, 0x2b, 0x73, 0xf7, 0xe6, + 0x62, 0xed, 0x6f, 0x86, 0x03, 0x45, 0xa2, 0x62, 0x83, 0xc1, 0xb4, 0x08, 0x0e, 0xcd, + 0xf5, 0x79, 0xd7, 0x0e, 0x7b, 0x0c, 0x0a, 0xb7, 0x1e, 0x11, 0x6e, 0xe2, 0xd9, 0xda, + 0x27, 0x46, 0x1e, 0x28, 0x12, 0x2a, 0x09, 0xca, 0x04, 0xde, 0x38, 0x76, 0x50, 0x2f, + 0xd2, 0x4d, 0xff, 0x92, 0x09, 0x55, 0x2f, 0x91, 0x13, 0x87, 0x70, 0x78, 0xa0, 0x94, + 0xe0, 0xe5, 0xf8, 0xce, 0xbb, 0x41, 0x54, 0xe0, 0x3a, 0x6b, 0x56, 0xf6, 0x04, 0xdf, + 0x98, 0x4b, 0xd2, 0x9e, 0xfd, 0x4f, 0x88, 0xc3, 0xf6, 0x29, 0xea, 0x2b, 0xba, 0x91, + 0x27, 0xea, 0x5a, 0x6c, 0xc5, 0xa3, 0x9d, 0x74, 0x1e, 0xdd, 0x71, 0x1a, 0x24, 0x44, + 0x7f, 0xe0, 0x6c, 0xf8, 0x45, 0x5a, 0x44, 0x06, 0x5e, 0x24, 0x52, 0x76, 0x3b, 0x0d, + 0x93, 0xf8, 0x6a, 0x31, 0x47, 0xbd, 0x08, 0x75, 0x7a, 0x4f, 0x7a, 0xa7, 0x79, 0x3c, + 0x97, 0x82, 0x1c, 0x2b, 0x57, 0x22, 0xc9, 0xdb, 0xad, 0x20, 0xf6, 0xa1, 0xe7, 0xad, + 0xf6, 0x8b, 0xf2, 0x22, 0x7b, 0xe5, 0x12, 0x04, 0xe9, 0xde, 0xca, 0x8d, 0x9e, 0xb6, + 0x26, 0x6f, 0x65, 0x9b, 0x33, 0x55, 0xc8, 0x97, 0x7e, 0xae, 0x7e, 0x9e, 0xd5, 0x39, + 0xd1, 0x79, 0x39, 0xf0, 0xc6, 0x16, 0x6b, 0x01, 0x13, 0x2d, 0xb0, 0x01, 0x66, 0x25, + 0x0e, 0xa9, 0x64, 0xe3, 0x9d, 0x9d, 0x55, 0xab, 0x43, 0x9a, 0x29, 0xbb, 0x0b, 0xcf, + 0xd3, 0xa9, 0x99, 0xb3, 0x1f, 0xe7, 0xa9, 0x51, 0x00, 0x2e, 0xe5, 0xdc, 0x01, 0x27, + 0x03, 0x24, 0xb1, 0x10, 0x10, 0x37, 0x89, 0x29, 0x42, 0x90, 0x7c, 0x6e, 0x19, 0x50, + 0x9a, 0x6c, 0x5f, 0x66, 0x59, 0xba, 0xf7, 0xf4, 0x36, 0x3c, 0x49, 0x15, 0xe6, 0x1b, + 0xda, 0x34, 0x06, 0x9b, 0xd9, 0x86, 0xb6, 0x37, 0x7f, 0xf6, 0x04, 0xed, 0xe5, 0xa7, + 0x42, 0x5d, 0xb2, 0x88, 0x86, 0xb1, 0xa2, 0x61, 0x36, 0x6d, 0xa8, 0xa1, 0x39, 0x86, + 0x65, 0xbe, 0xed, 0x3b, 0xe9, 0xbc, 0x2e, 0x05, 0x5e, 0x71, 0x1b, 0x7d, 0x36, 0xdd, + 0xbd, 0xd3, 0x65, 0xcc, 0xdc, 0xd7, 0xfc, 0xba, 0xfe, 0x71, 0x29, 0x66, 0x95, 0x08, + 0xda, 0xc0, 0xad, 0x2d, 0x55, 0xee, 0x7f, 0xc6, 0x0b, 0xce, 0x22, 0x88, 0x50, 0xba, + 0x7b, 0x94, 0x3a, 0x8d, 0x50, 0xff, 0xcb, 0x2a, 0x67, 0x06, 0x51, 0xd3, 0x15, 0xd8, + 0x71, 0x9c, 0x7b, 0x57, 0xf6, 0x37, 0xa3, 0x7e, 0xdd, 0x32, 0x6a, 0xbc, 0x76, 0xf0, + 0xa7, 0x69, 0x0c, 0x23, 0x68, 0x80, 0x16, 0x01, 0x07, 0xc2, 0xb4, 0xc8, 0x5e, 0xcf, + 0x2a, 0xd9, 0xf5, 0xdd, 0x26, 0x45, 0x62, 0x6e, 0x40, 0x90, 0xf1, 0x00, 0x47, 0xcc, + 0x13, 0x15, 0x40, 0xca, 0x58, 0x03, 0x04, 0x5a, 0x6a, 0xee, 0x91, 0xea, 0x0b, 0x3f, + 0x9b, 0x77, 0xc4, 0x43, 0x40, 0x69, 0xc5, 0x32, 0x0c, 0xf5, 0xb7, 0x01, 0x82, 0xd9, + 0xfb, 0xbf, 0x30, 0x98, 0x30, 0x60, 0x11, 0x75, 0x9d, 0x0d, 0x64, 0xa8, 0x84, 0x14, + 0x1e, 0xa0, 0x21, 0xcd, 0xd9, 0x5e, 0xfa, 0x32, 0x63, 0xa5, 0x05, 0xb8, 0x52, 0x29, + 0xd1, 0x54, 0xec, 0xaa, 0x23, 0x5e, 0x8f, 0xa1, 0x07, 0x95, 0xc9, 0xda, 0x27, 0x41, + 0xcd, 0x98, 0x71, 0x90, 0x16, 0xa9, 0x01, 0x17, 0xa7, 0x6f, 0x84, 0xf0, 0x0b, 0x5c, + 0x3d, 0x4b, 0xce, 0xd7, 0x9a, 0x73, 0xbf, 0xb3, 0xa1, 0xc7, 0x8a, 0xd1, 0xad, 0xea, + 0x50, 0x78, 0xf2, 0xf1, 0xb0, 0x0f, 0x81, 0x5b, 0xc7, 0xa3, 0x0e, 0xf8, 0x58, 0x40, + 0x07, 0x77, 0x32, 0xdc, 0xb1, 0xa6, 0x1e, 0x9f, 0x31, 0x76, 0x3d, 0x52, 0x2d, 0x04, + 0xc4, 0x90, 0x37, 0x1a, 0xea, 0xbc, 0xa9, 0x49, 0x9b, 0x05, 0x13, 0x17, 0x8d, 0x54, + 0x31, 0x14, 0x8a, 0x72, 0x80, 0x5d, 0x09, 0x32, 0x9e, 0xa5, 0xd9, 0x41, 0xf3, 0x32, + 0xd5, 0xc6, 0xd3, 0x2b, 0xa2, 0xef, 0x9f, 0x87, 0x23, 0xb6, 0xae, 0xa4, 0x5f, 0x94, + 0xb6, 0xb2, 0x1a, 0xab, 0x7d, 0x16, 0x06, 0x46, 0xc3, 0x76, 0x0e, 0x7a, 0xcd, 0xa1, + 0xff, 0xdd, 0x8f, 0x54, 0xf4, 0xa2, 0xc3, 0x1a, 0xfe, 0x9b, 0x48, 0x19, 0x23, 0x3b, + 0xfe, 0x8e, 0xf8, 0x91, 0x64, 0xfa, 0x0e, 0xcb, 0xf1, 0xcc, 0xe8, 0x66, 0x62, 0xe7, + 0x47, 0x34, 0x44, 0x65, 0x9f, 0xc8, 0xcb, 0xc9, 0xf3, 0x61, 0x7e, 0xe8, 0x19, 0x5f, + 0xe1, 0xbc, 0xf5, 0xbb, 0x1b, 0x63, 0x4c, 0xd4, 0x3f, 0x62, 0xea, 0x93, 0xa4, 0x6d, + 0x88, 0xf2, 0xfc, 0xbc, 0x3e, 0x28, 0x40, 0x84, 0xe7, 0x04, 0xfb, 0x1d, 0x7d, 0x0d, + 0x9a, 0xcb, 0x91, 0x96, 0x1e, 0x2e, 0xeb, 0xe2, 0xdc, 0x9e, 0xbe, 0x36, 0x5b, 0x25, + 0xb5, 0x66, 0x75, 0x97, 0x3d, 0x0c, 0x38, 0xf4, 0x76, 0x30, 0x57, 0x47, 0x23, 0xcd, + 0x3e, 0xc6, 0x6c, 0x8f, 0x3b, 0x12, 0x82, 0x21, 0xa7, 0x90, 0xd9, 0x2c, 0x89, 0x5b, + 0x94, 0x27, 0x0f, 0xe9, 0x40, 0x51, 0xa1, 0x70, 0xe9, 0x5b, 0x8b, 0xe7, 0x16, 0x34, + 0x86, 0xec, 0x8c, 0x0b, 0xee, 0xbe, 0xf6, 0x5e, 0x16, 0x26, 0xb0, 0x46, 0xd7, 0xe7, + 0xf8, 0x26, 0x37, 0x2b, 0x6a, 0xa1, 0x0b, 0xae, 0xfb, 0x84, 0x8f, 0xa1, 0xdf, 0x6b, + 0xb1, 0xdc, 0x43, 0x95, 0x40, 0xf6, 0x3c, 0x9c, 0x7a, 0x9d, 0x5f, 0x88, 0x13, 0x40, + 0x29, 0x62, 0x65, 0x1e, 0xe9, 0x84, 0x39, 0x02, 0xb6, 0xc3, 0x98, 0x2d, 0xce, 0x50, + 0xa6, 0x17, 0x8a, 0x55, 0xa1, 0xad, 0xc0, 0x1c, 0xe7, 0xdc, 0x6c, 0x83, 0x38, 0xe1, + 0xa9, 0xce, 0xef, 0xc1, 0x78, 0xdc, 0x43, 0x14, 0xf6, 0x74, 0x9a, 0x81, 0xa7, 0x31, + 0xee, 0x3c, 0x7f, 0xc0, 0xc3, 0x5d, 0x1c, 0xe3, 0x63, 0xce, 0xf1, 0x13, 0x28, 0xf3, + 0x87, 0xc4, 0x01, 0xfe, 0xf2, 0x7a, 0x67, 0xa6, 0x29, 0x2f, 0x6f, 0x72, 0xb0, 0xa1, + 0xd6, 0xc3, 0x89, 0x16, 0x2d, 0x16, 0x2e, 0xf0, 0x50, 0xae, 0x5f, 0x3d, 0xdb, 0xb5, + 0x5c, 0xaa, 0xbc, 0xa9, 0xa1, 0xbe, 0x89, 0xb4, 0x63, 0x49, 0x4d, 0x74, 0x39, 0xfb, + 0x56, 0x47, 0xa9, 0x18, 0x12, 0x8b, 0x96, 0x25, 0xd3, 0x3e, 0xac, 0xa6, 0x19, 0xd5, + 0x2f, 0x03, 0x5f, 0xe6, 0x08, 0x9c, 0xe8, 0xd8, 0xb9, 0x0f, 0xe3, 0x67, 0x0d, 0x8c, + 0x5a, 0x2e, 0x3e, 0x05, 0x49, 0x69, 0xa3, 0xd9, 0x7e, 0x61, 0xb5, 0xe6, 0x30, 0x67, + 0x4f, 0xc7, 0x08, 0x57, 0xf1, 0xbb, 0xf1, 0x0f, 0xdc, 0x40, 0x49, 0xef, 0xf5, 0x60, + 0xeb, 0xa5, 0xf2, 0x2a, 0xcc, 0x8d, 0x77, 0xdb, 0xee, 0x0b, 0x20, 0x55, 0x7f, 0xa4, + 0xd0, 0x33, 0x31, 0x72, 0xcb, 0xb5, 0xcb, 0xcc, 0x2b, 0x13, 0x5f, 0x2c, 0xcd, 0xe0, + 0x14, 0xe6, 0x3e, 0xbe, 0x4e, 0xdf, 0x92, 0x5e, 0x61, 0xba, 0x2a, 0x32, 0x0c, 0xd3, + 0x99, 0x91, 0x5a, 0xdd, 0xfc, 0xeb, 0x1a, 0xd0, 0x69, 0xa9, 0xfd, 0x5b, 0x62, 0x10, + 0xa4, 0xb6, 0xe5, 0x04, 0x52, 0xb1, 0xf9, 0x06, 0xdd, 0x16, 0xf0, 0x16, 0x68, 0xf0, + 0xaf, 0x56, 0x6a, 0x28, 0x7c, 0xce, 0xfc, 0xd8, 0x94, 0x73, 0x41, 0x85, 0x9a, 0xe7, + 0xdc, 0x3a, 0x06, 0xf6, 0xbf, 0x15, 0x74, 0xfe, 0xb9, 0x31, 0xf9, 0x27, 0xe2, 0xd5, + 0x05, 0xf6, 0x08, 0x59, 0x9e, 0x23, 0xb0, 0x5a, 0xf7, 0xc3, 0x23, 0x69, 0x83, 0x97, + 0xa8, 0x01, 0xdc, 0x7f, 0x78, 0x82, 0x5c, 0xc7, 0xeb, 0x9f, 0xcc, 0xe6, 0xc6, 0xc4, + 0xf8, 0xf6, 0x88, 0x39, 0xd3, 0x0a, 0xc5, 0x67, 0x14, 0x8e, 0x70, 0x84, 0xdb, 0x2b, + 0x37, 0x58, 0x30, 0xa0, 0x7b, 0x30, 0x5f, 0xed, 0xd6, 0x07, 0xa3, 0x47, 0xfa, 0x65, + 0xde, 0xf0, 0x1d, 0x4e, 0x1f, 0xd6, 0xc1, 0x6b, 0x4b, 0x47, 0xf5, 0xb0, 0x1b, 0x43, + 0x65, 0xb7, 0x72, 0x26, 0xe6, 0x0f, 0xdd, 0x40, 0xf2, 0x2a, 0x39, 0x5a, 0xa2, 0x35, + 0xf0, 0xdf, 0xda, 0x8f, 0xb4, 0xd3, 0xde, 0x65, 0xb0, 0xcf, 0x4f, 0x4c, 0x22, 0x0b, + 0x3b, 0x4a, 0x9e, 0x32, 0xbc, 0x0d, 0xb6, 0x4f, 0x16, 0x2c, 0x07, 0xdf, 0x42, 0xa1, + 0x01, 0x99, 0x03, 0xa6, 0x7c, 0xda, 0x69, 0x3d, 0xde, 0xb5, 0xca, 0x39, 0xa0, 0xfe, + 0x50, 0x08, 0x50, 0xec, 0x7c, 0x06, 0xbe, 0xe7, 0x18, 0x66, 0xb3, 0x55, 0xcc, 0xbc, + 0x07, 0x8c, 0xd4, 0xdc, 0x03, 0x6f, 0xda, 0xa8, 0x1c, 0xb2, 0xde, 0x99, 0xcc, 0x88, + 0xf6, 0x0a, 0x49, 0x46, 0x42, 0x87, 0xf5, 0x9f, 0xc7, 0x14, 0x8b, 0x1a, 0xfb, 0x4a, + 0x2f, 0x9b, 0xb8, 0x97, 0x14, 0xe1, 0xeb, 0x8c, 0x03, 0x61, 0xe5, 0x99, 0x2a, 0x5b, + 0x79, 0xcd, 0xbb, 0x91, 0xd9, 0xbf, 0x29, 0xeb, 0x59, 0x8c, 0xbb, 0x4b, 0xda, 0x92, + 0x3d, 0x26, 0x7f, 0xea, 0xcb, 0x91, 0xce, 0x72, 0xd6, 0x1a, 0xb1, 0xea, 0x00, 0xf5, + 0x6a, 0xa6, 0x76, 0x6e, 0xab, 0xc4, 0x7d, 0xca, 0xa6, 0x9a, 0x02, 0x4b, 0xbf, 0xf2, + 0xf2, 0x96, 0x91, 0x7f, 0x17, 0xa3, 0xf8, 0xc9, 0x3e, 0x1b, 0xf2, 0x9c, 0x3c, 0xfc, + 0x99, 0x1a, 0x2b, 0xe8, 0xcf, 0xa7, 0x0e, 0x5d, 0xe3, 0xf2, 0xdd, 0x52, 0xa7, 0x55, + 0x01, 0x38, 0x68, 0x7a, 0xec, 0x28, 0x92, 0x6f, 0xa1, 0x68, 0xb1, 0x81, 0xdb, 0x72, + 0x82, 0xbd, 0x60, 0xda, 0xd3, 0x31, 0x0d, 0xfe, 0x54, 0x2c, 0xeb, 0xe6, 0x94, 0x74, + 0x00, 0x25, 0xc7, 0xec, 0x2a, 0x20, 0x43, 0xfe, 0xbb, 0x77, 0x9f, 0x7f, 0x37, 0x89, + 0xa5, 0xe2, 0x42, 0xdb, 0x48, 0x03, 0xee, 0x36, 0x72, 0x52, 0xc4, 0x63, 0xc9, 0xa8, + 0x8b, 0x41, 0x7b, 0x70, 0x86, 0x6d, 0x9a, 0xfb, 0x7a, 0x08, 0x27, 0x68, 0x01, 0xf9, + 0x22, 0x7c, 0x63, 0x81, 0xf1, 0x5c, 0xc0, 0x94, 0xac, 0x7b, 0xd1, 0x54, 0xa4, 0xce, + 0xf9, 0x0b, 0x48, 0x47, 0xdc, 0x16, 0x8a, 0x01, 0xf1, 0xe3, 0x1e, 0xec, 0x74, 0xa7, + 0xef, 0xce, 0xba, 0x11, 0xf5, 0x07, 0x69, 0xf5, 0xd8, 0xf5, 0x4d, 0x36, 0x20, 0xc2, + 0x3e, 0xc8, 0x99, 0x3f, 0x7a, 0xef, 0x27, 0xc1, 0xd3, 0x51, 0x96, 0xb1, 0x02, 0xb3, + 0xcf, 0x3f, 0xed, 0x8b, 0xf8, 0x5d, 0x8a, 0x45, 0xf6, 0x96, 0x83, 0xec, 0xdd, 0x1a, + 0x23, 0x44, 0xef, 0xb8, 0x48, 0x07, 0xd9, 0x0f, 0x18, 0x35, 0xb4, 0xf2, 0xf2, 0x4d, + 0x8f, 0xf8, 0x12, 0x30, 0x47, 0xeb, 0x9f, 0x7d, 0x30, 0x62, 0x3e, 0x14, 0x29, 0x0d, + 0x56, 0x17, 0x96, 0x3b, 0x42, 0x21, 0x40, 0x4a, 0xe7, 0x61, 0xc8, 0x6b, 0xec, 0x7a, + 0x07, 0xbf, 0x81, 0xa0, 0xb9, 0xa7, 0xf7, 0xd0, 0x87, 0xac, 0x26, 0xce, 0x3d, 0xfa, + 0x9c, 0x93, 0xfe, 0xea, 0xeb, 0xd1, 0x0d, 0xc1, 0x88, 0xc6, 0x27, 0xd4, 0xb9, 0x1d, + 0x2a, 0x79, 0x01, 0xdc, 0x39, 0x4e, 0x52, 0x39, 0x05, 0x0a, 0x17, 0xec, 0xd5, 0x33, + 0x20, 0xa5, 0xd7, 0x72, 0x4c, 0xd4, 0xf9, 0x82, 0xc9, 0x3d, 0x6b, 0xbd, 0x01, 0xce, + 0xc3, 0xe1, 0xf7, 0x1a, 0x0f, 0x12, 0xde, 0xa3, 0xd1, 0x42, 0xff, 0x0f, 0xff, 0xd7, + 0xa1, 0xb8, 0xf9, 0xeb, 0x82, 0xcc, 0x72, 0x10, 0x3c, 0x71, 0x97, 0x55, 0x3d, 0x07, + 0x2a, 0xe1, 0xad, 0xf7, 0x0c, 0xa4, 0x00, 0x7a, 0x3d, 0x07, 0xff, 0xf5, 0xec, 0x82, + 0xe6, 0x64, 0x71, 0x01, 0x0c, 0xf9, 0x8a, 0x3a, 0x2a, 0x5b, 0xe1, 0x6b, 0x86, 0x2d, + 0x29, 0xc7, 0x70, 0x12, 0x72, 0x47, 0x61, 0xe9, 0xcb, 0xbe, 0x42, 0x62, 0xcc, 0xa5, + 0xb0, 0xb9, 0x31, 0xe8, 0xbb, 0x72, 0x67, 0x1f, 0xe4, 0xb4, 0xb5, 0x88, 0xc9, 0x0a, + 0xd5, 0xc0, 0x0b, 0x55, 0xdc, 0x8c, 0x8a, 0xf9, 0xb0, 0xf6, 0xa3, 0xca, 0x1e, 0x07, + 0xef, 0xf1, 0x58, 0x11, 0x39, 0x1c, 0x53, 0xf7, 0xe4, 0x3b, 0x1b, 0x81, 0x16, 0xda, + 0xdc, 0x01, 0x6d, 0x19, 0x26, 0xc8, 0x48, 0x0d, 0x4e, 0xe3, 0x4e, 0x76, 0x19, 0x1b, + 0x79, 0xbe, 0xd0, 0xce, 0x95, 0x97, 0x3a, 0x4c, 0x7c, 0xf2, 0xf0, 0x57, 0xc7, 0x14, + 0x7e, 0xdb, 0x01, 0x3d, 0x20, 0x5d, 0x81, 0xe2, 0x36, 0x08, 0x88, 0xa2, 0xab, 0xdd, + 0xcc, 0xf0, 0xf6, 0xf3, 0xd8, 0xf8, 0xba, 0x11, 0x1d, 0x64, 0x2c, 0x52, 0xd0, 0x4e, + 0xbd, 0x3c, 0xe1, 0x7c, 0x60, 0xd9, 0x22, 0x57, 0xea, 0x58, 0x69, 0x09, 0x45, 0x01, + 0xbb, 0x67, 0x12, 0x68, 0xb2, 0x24, 0x47, 0x7a, 0x8e, 0x01, 0x41, 0xd6, 0xff, 0x37, + 0xe2, 0x4f, 0xf1, 0xc7, 0x65, 0xe8, 0x4d, 0x26, 0x4d, 0xb8, 0x8f, 0x00, 0x92, 0x8e, + 0x64, 0xc4, 0x12, 0xbd, 0x59, 0x15, 0x1a, 0x65, 0x71, 0xc6, 0x67, 0x09, 0x16, 0xb0, + 0x70, 0x6b, 0x04, 0x4f, 0xc5, 0xc2, 0xbd, 0x93, 0xad, 0xe3, 0x96, 0x79, 0x57, 0xcd, + 0xb9, 0x41, 0x27, 0x4c, 0xc6, 0xbd, 0xb4, 0xe0, 0x36, 0xb7, 0x67, 0xb9, 0x50, 0xc0, + 0x9e, 0x46, 0x26, 0xa1, 0xd0, 0x05, 0xbc, 0xf4, 0x83, 0x6e, 0xf6, 0xa1, 0xde, 0x48, + 0x09, 0x5d, 0xcb, 0x46, 0x12, 0x78, 0xb1, 0x6c, 0x45, 0x68, 0x90, 0xb2, 0x3d, 0x40, + 0xbd, 0x36, 0x04, 0x10, 0xf0, 0x01, 0x0a, 0x55, 0xf5, 0x05, 0xfe, 0x5e, 0x2d, 0xb2, + 0x01, 0xc7, 0x52, 0xe9, 0xb5, 0xb1, 0x5b, 0xf8, 0xaa, 0x9e, 0x82, 0xd6, 0x49, 0xab, + 0x11, 0x73, 0xba, 0x2a, 0x51, 0x32, 0xe0, 0xcc, 0x50, 0x51, 0xcc, 0xf7, 0x4c, 0x7a, + 0x6a, 0x37, 0x07, 0xab, 0x59, 0x83, 0xf7, 0xcc, 0x27, 0x5c, 0x99, 0x1a, 0xbe, 0x4d, + 0x7c, 0xee, 0x5f, 0x28, 0x9e, 0xfe, 0x72, 0x7e, 0xb3, 0xda, 0x86, 0xfa, 0x21, 0xa2, + 0x8d, 0x6b, 0x8a, 0x2a, 0xff, 0xd4, 0x2d, 0xb9, 0x8b, 0xb2, 0xa4, 0x6c, 0xd8, 0xa3, + 0x29, 0x31, 0x2f, 0xa9, 0x45, 0x39, 0xd9, 0xcb, 0x35, 0xdc, 0xb6, 0x04, 0x67, 0x8b, + 0x63, 0x90, 0x64, 0xd9, 0x20, 0x05, 0xdf, 0x2d, 0x10, 0x68, 0x1c, 0x64, 0xb9, 0xed, + 0x8c, 0xe4, 0x7d, 0x7e, 0xba, 0x0f, 0x2b, 0x50, 0x2b, 0x20, 0x6a, 0xd4, 0xb2, 0xe9, + 0x2b, 0xbe, 0x45, 0x86, 0xf6, 0xd7, 0x50, 0x9e, 0x57, 0xa6, 0x37, 0x7f, 0xea, 0xbe, + 0x38, 0xb3, 0xcc, 0x6c, 0x95, 0x5d, 0x5e, 0x7b, 0xdf, 0x7e, 0xb1, 0x32, 0xd8, 0x6b, + 0xc0, 0x7a, 0x30, 0x98, 0xb4, 0x13, 0xe4, 0x40, 0x5d, 0xaa, 0xa2, 0x55, 0x29, 0x1d, + 0x55, 0x2b, 0x2c, 0x80, 0x07, 0xbe, 0xd4, 0x1e, 0x22, 0xf1, 0xcf, 0x79, 0x11, 0x82, + 0x12, 0x00, 0x55, 0x5e, 0x9c, 0x4f, 0xfb, 0x09, 0xef, 0xc1, 0x22, 0x38, 0x11, 0x75, + 0x03, 0x1c, 0x38, 0x28, 0x0b, 0x53, 0x26, 0xeb, 0xbe, 0xaf, 0x33, 0x4f, 0xdc, 0xf0, + 0xdc, 0x44, 0x4e, 0x62, 0x9f, 0x93, 0x95, 0x51, 0x54, 0x0b, 0xcb, 0xbb, 0xb1, 0xab, + 0x9c, 0x23, 0x1a, 0x86, 0x6b, 0x32, 0x9e, 0x85, 0x24, 0xab, 0x25, 0xf9, 0x3e, 0x5e, + 0x33, 0x4a, 0x05, 0x27, 0x2a, 0x3f, 0x82, 0x6f, 0x9d, 0x05, 0xa4, 0x50, 0x58, 0xdf, + 0xcd, 0xf6, 0x88, 0x43, 0xa8, 0xb9, 0x36, 0xa0, 0xcf, 0x5e, 0x6a, 0xa8, 0xae, 0x1b, + 0x80, 0xf6, 0x01, 0x61, 0xbf, 0x41, 0x4f, 0x28, 0x02, 0x11, 0x11, 0x09, 0x21, 0xa9, + 0xc8, 0x5f, 0x51, 0x04, 0xa0, 0x16, 0x8e, 0x8e, 0x72, 0xde, 0x4f, 0x8a, 0xa0, 0x41, + 0x32, 0xeb, 0x25, 0x88, 0x76, 0xf1, 0x9d, 0x7b, 0xe5, 0xf2, 0xdd, 0x2b, 0x0b, 0x30, + 0x4b, 0x92, 0x3b, 0x29, 0x52, 0xd9, 0x1f, 0xde, 0xe7, 0xe5, 0x52, 0x05, 0xdb, 0xb1, + 0x94, 0xeb, 0xba, 0x32, 0x2f, 0xdc, 0x67, 0xb2, 0x52, 0x2c, 0x92, 0x61, 0x21, 0xc7, + 0xfa, 0x1a, 0xf1, 0x7e, 0xd0, 0x6c, 0x47, 0x27, 0x8f, 0x96, 0x08, 0x92, 0x96, 0x08, + 0x7a, 0x70, 0x4b, 0x7d, 0x0f, 0x84, 0x7d, 0x51, 0xd6, 0xcc, 0x68, 0xac, 0xc5, 0x22, + 0x07, 0x74, 0x73, 0x41, 0xf6, 0xb9, 0x8c, 0xb1, 0xcd, 0x4f, 0xaf, 0xcd, 0x2b, 0xb0, + 0xd0, 0x5b, 0xc7, 0x9b, 0xb8, 0x0d, 0x7c, 0x4b, 0x8a, 0x1a, 0x11, 0xbc, 0x0a, 0x3b, + 0xde, 0xca, 0x45, 0x41, 0x86, 0x9b, 0x4d, 0xc9, 0xd6, 0xb4, 0x8c, 0xd7, 0x86, 0x9b, + 0xf7, 0x63, 0xb9, 0xdc, 0x42, 0x45, 0x27, 0x3c, 0x70, 0x4b, 0x0d, 0x8d, 0xec, 0x4b, + 0x85, 0xd1, 0x6d, 0xd4, 0x38, 0xce, 0xd6, 0x22, 0x0f, 0xa6, 0x69, 0x26, 0x66, 0x3f, + 0xcc, 0x22, 0x8f, 0xc6, 0xc4, 0xd2, 0x7e, 0x17, 0xe3, 0x27, 0x83, 0x4b, 0x67, 0x57, + 0x91, 0x4d, 0x1b, 0xcb, 0xf3, 0x4b, 0x65, 0xd8, 0x58, 0xab, 0x8b, 0x5c, 0x12, 0x0c, + 0xb0, 0x85, 0x05, 0x22, 0xf5, 0x42, 0x89, 0x3f, 0xdd, 0xb1, 0x79, 0xe8, 0x7f, 0x83, + 0x2d, 0xaa, 0xa1, 0x52, 0xc8, 0x31, 0xf1, 0x35, 0x64, 0x00, 0x9c, 0x41, 0x81, 0x23, + 0x53, 0x3d, 0xe2, 0xc6, 0x79, 0x49, 0xe3, 0xaf, 0x2d, 0xcb, 0x60, 0xd6, 0xbd, 0xbd, + 0xda, 0xda, 0x63, 0xa3, 0x0b, 0x4b, 0x54, 0xcd, 0x1c, 0xe5, 0xa5, 0xa0, 0x0f, 0x8e, + 0x85, 0x57, 0xeb, 0xa9, 0x23, 0x4e, 0x81, 0x17, 0x8d, 0x0f, 0xca, 0xb5, 0x61, 0x0f, + 0xba, 0x96, 0x69, 0xcf, 0xeb, 0x1b, 0xd0, 0x8c, 0xd9, 0x65, 0x33, 0x49, 0x8b, 0x27, + 0x2c, 0x57, 0x79, 0xa9, 0xf9, 0x39, 0x69, 0x1d, 0xe1, 0xad, 0x88, 0x1c, 0x80, 0x87, + 0x8d, 0x6c, 0x29, 0x42, 0x15, 0x23, 0x0b, 0xbb, 0x61, 0x90, 0x69, 0xb4, 0xdc, 0x17, + 0xb3, 0xe5, 0x9d, 0xbd, 0x24, 0x2c, 0xd8, 0x8e, 0xcc, 0x3b, 0xe3, 0xa2, 0x69, 0x6b, + 0xf7, 0xf2, 0xd9, 0xe5, 0xb8, 0xc1, 0x52, 0xcc, 0x0d, 0x99, 0xa0, 0xa5, 0xe9, 0xa3, + 0x8b, 0x1b, 0x8e, 0xb1, 0xa0, 0x13, 0xeb, 0x76, 0x51, 0x33, 0x37, 0xa7, 0xb0, 0xda, + 0xdb, 0x4e, 0x81, 0x7b, 0x6f, 0x49, 0x78, 0x02, 0xbd, 0x47, 0xe9, 0x3a, 0x82, 0x0c, + 0x4f, 0xad, 0x6c, 0x65, 0x09, 0x74, 0x42, 0xb9, 0xca, 0xc1, 0x61, 0xb6, 0x4d, 0x77, + 0xb3, 0x05, 0x7d, 0x13, 0x36, 0xb5, 0x5a, 0x48, 0xa7, 0xb7, 0xf6, 0x89, 0xec, 0x9f, + 0x6c, 0x6a, 0x81, 0xf3, 0xee, 0xe6, 0xdd, 0xc5, 0xcc, 0xc9, 0x6f, 0xef, 0x4c, 0x4c, + 0x89, 0x07, 0xbb, 0xff, 0xe3, 0x2c, 0xb6, 0x12, 0x92, 0x05, 0x3d, 0xb8, 0x6d, 0x36, + 0x6b, 0x7e, 0x6b, 0x30, 0x13, 0xd1, 0x4b, 0x20, 0x5f, 0xb4, 0x5d, 0x06, 0x7e, 0x37, + 0x50, 0x2e, 0x37, 0x9c, 0x4a, 0xa1, 0x38, 0x2f, 0xf9, 0x22, 0x0c, 0x4f, 0x38, 0x9c, + 0xa2, 0xc4, 0x14, 0x99, 0x55, 0x60, 0x52, 0x3e, 0x6d, 0xde, 0x86, 0xa3, 0x7f, 0x3f, + 0x86, 0xda, 0x8e, 0x7c, 0x03, 0x4f, 0x4b, 0x6d, 0x79, 0x43, 0xce, 0xf1, 0x20, 0x30, + 0xc4, 0x00, 0x99, 0xd8, 0x77, 0xca, 0xbe, 0x81, 0xb0, 0x87, 0x50, 0xe3, 0xfb, 0xfe, + 0x63, 0x12, 0xf6, 0x38, 0x0b, 0x98, 0xfb, 0x85, 0x0a, 0x2a, 0x14, 0x2b, 0x91, 0x4a, + 0xdc, 0x71, 0x54, 0x47, 0xc5, 0x79, 0x1a, 0x1b, 0x67, 0xae, 0x65, 0x6c, 0xad, 0xdd, + 0x21, 0xe1, 0xb4, 0x6d, 0xc9, 0xa7, 0x64, 0x12, 0x7b, 0xc0, 0xa3, 0x01, 0xb4, 0x80, + 0x04, 0xa9, 0xc5, 0x27, 0x6b, 0xcf, 0x08, 0xe7, 0xfe, 0x4a, 0xe5, 0x2d, 0x76, 0xe4, + 0x31, 0x48, 0x8a, 0x5b, 0x9d, 0x43, 0x1f, 0xa1, 0x36, 0x34, 0x6e, 0x5a, 0x53, 0xab, + 0x3f, 0x68, 0x12, 0xf2, 0xd9, 0x70, 0xf7, 0xb3, 0x98, 0x98, 0xcf, 0x8b, 0x62, 0xf2, + 0xdb, 0xf6, 0x1e, 0x99, 0xa2, 0x91, 0x5d, 0xfb, 0x75, 0xae, 0x22, 0xb7, 0x9f, 0x84, + 0xcf, 0x25, 0x97, 0xeb, 0x34, 0xec, 0x3d, 0x29, 0x2e, 0x6b, 0x5d, 0x84, 0xeb, 0xac, + 0x4d, 0x92, 0xde, 0x52, 0xe1, 0xf8, 0xbf, 0x6b, 0xfd, 0xba, 0xda, 0x63, 0x44, 0x09, + 0xf2, 0x0e, 0xf2, 0xcc, 0x6e, 0x3c, 0x39, 0x0e, 0x43, 0x5f, 0x47, 0xe3, 0x47, 0x23, + 0x8d, 0xb4, 0x86, 0x90, 0x84, 0x04, 0x73, 0xb0, 0xa0, 0x83, 0x1a, 0x5a, 0x8a, 0x58, + 0xc4, 0xdc, 0xfc, 0x4e, 0xab, 0x7b, 0x41, 0x8c, 0xba, 0x2a, 0x41, 0x4f, 0x95, 0x57, + 0x71, 0x90, 0xff, 0x88, 0xd7, 0x27, 0xf7, 0x3e, 0x2f, 0xff, 0x97, 0xaa, 0xbd, 0x11, + 0x14, 0xb7, 0x64, 0xe3, 0xed, 0xbc, 0x18, 0x3e, 0x60, 0x3a, 0xcf, 0xb7, 0xc0, 0x9b, + 0xf1, 0x32, 0xbb, 0x01, 0xef, 0xc7, 0x17, 0x8d, 0x4f, 0x9a, 0x2d, 0xba, 0xf4, 0x92, + 0x4f, 0xd8, 0x0f, 0xbe, 0x0e, 0x60, 0x4f, 0x60, 0x39, 0x08, 0x32, 0xeb, 0x98, 0x04, + 0x79, 0xe0, 0x4e, 0x9c, 0x9a, 0x2b, 0xb2, 0xfb, 0x36, 0x84, 0xd8, 0xf8, 0x06, 0x48, + 0xd5, 0x80, 0x78, 0x38, 0x54, 0x58, 0x4f, 0x62, 0xbe, 0x0c, 0xc9, 0x21, 0x88, 0x32, + 0x38, 0x56, 0x10, 0xd9, 0x62, 0x36, 0x5f, 0x50, 0x71, 0xfa, 0x3d, 0x36, 0x8f, 0xfb, + 0x67, 0x1b, 0xa2, 0xc2, 0xf9, 0xa0, 0xfc, 0x68, 0xd8, 0x07, 0x22, 0x19, 0xa7, 0x7b, + 0xef, 0x2d, 0x6b, 0x4a, 0x19, 0xf1, 0x6d, 0xd5, 0x30, 0x74, 0x22, 0x47, 0x46, + ], + jumbled: &[ + 0x9c, 0x7d, 0x38, 0x5a, 0xd1, 0xc4, 0x69, 0x67, 0x88, 0x0b, 0xa4, 0x31, 0x47, 0xb3, + 0x14, 0xc3, 0xe5, 0xb0, 0x02, 0x75, 0x12, 0x82, 0x11, 0x60, 0xa0, 0x99, 0xf3, 0xa2, + 0x5a, 0xdf, 0x38, 0x27, 0x47, 0x03, 0xd0, 0x7b, 0x15, 0xc8, 0xc2, 0x6d, 0x75, 0x55, + 0xb6, 0x15, 0xca, 0x77, 0x61, 0xc2, 0x9a, 0x60, 0x42, 0x04, 0x80, 0x2b, 0xd4, 0x14, + 0x2f, 0x2b, 0xe4, 0xbd, 0x38, 0x15, 0x86, 0x18, 0xe3, 0xed, 0xda, 0x02, 0xd7, 0x87, + 0xd9, 0xea, 0x1b, 0xd9, 0xe6, 0xab, 0x5a, 0x60, 0xc3, 0xbb, 0x36, 0xdf, 0xbc, 0x35, + 0x47, 0x37, 0x47, 0x7a, 0x3a, 0xe0, 0x9b, 0xe5, 0x83, 0xe1, 0x5b, 0x56, 0x7a, 0x97, + 0xcb, 0xae, 0xf7, 0x8c, 0x1e, 0x34, 0x4a, 0xbe, 0x1c, 0x00, 0x9a, 0xdc, 0x71, 0xfa, + 0x3d, 0x13, 0x6e, 0x96, 0xf7, 0x5c, 0xb5, 0xd0, 0x82, 0xfa, 0x20, 0xfe, 0xf4, 0x7e, + 0x66, 0x36, 0x48, 0x2d, 0x97, 0x7d, 0x20, 0x7c, 0x10, 0x9a, 0xe3, 0xba, 0x3d, 0xb7, + 0xac, 0xfb, 0xaf, 0xb1, 0x71, 0x2a, 0x75, 0xdf, 0x1f, 0xd6, 0x17, 0x81, 0x4d, 0xb4, + 0xe9, 0x02, 0xb0, 0x7b, 0x1d, 0x76, 0x25, 0xbc, 0xdd, 0xc9, 0xa8, 0xd8, 0xa3, 0xc0, + 0x2d, 0x22, 0x48, 0xa9, 0x8b, 0xa9, 0xda, 0xa1, 0xd5, 0x65, 0xe8, 0x80, 0x83, 0x52, + 0xae, 0xcb, 0xcc, 0x12, 0x04, 0x8b, 0x39, 0x0f, 0x6b, 0x4b, 0x8b, 0xe8, 0x71, 0xdf, + 0xc2, 0xa7, 0xea, 0x03, 0x72, 0x0a, 0x1b, 0x2c, 0x2a, 0xc1, 0x2b, 0x7b, 0x75, 0xeb, + 0xf4, 0x69, 0xe3, 0x1c, 0x36, 0x1d, 0x41, 0x56, 0x90, 0xca, 0x4b, 0xfe, 0x42, 0x46, + 0x25, 0x49, 0xfd, 0xf9, 0xde, 0x7f, 0x38, 0x1b, 0xad, 0xcc, 0x3d, 0xb1, 0xb8, 0xc1, + 0x37, 0x02, 0xc3, 0xfd, 0xa1, 0x92, 0xf5, 0x80, 0x63, 0x06, 0xa9, 0xd6, 0xf0, 0x62, + 0x93, 0xef, 0x88, 0x41, 0x3b, 0x88, 0x3b, 0x19, 0xd4, 0x85, 0xe2, 0x53, 0x7e, 0x89, + 0xbd, 0x53, 0xc1, 0xc0, 0x04, 0xf2, 0x62, 0x26, 0xc7, 0x2f, 0xb4, 0x66, 0x31, 0x4e, + 0x67, 0xa9, 0x32, 0x8c, 0x6d, 0x64, 0xe3, 0x81, 0x0d, 0x85, 0x4f, 0xb3, 0x31, 0x30, + 0xe3, 0x4c, 0x85, 0x94, 0x0a, 0x8b, 0xc8, 0x35, 0xba, 0x67, 0x71, 0xf6, 0x30, 0x01, + 0xca, 0x8c, 0x8f, 0xb3, 0xbc, 0x7d, 0x9e, 0x61, 0x25, 0xfd, 0x3d, 0x25, 0x1c, 0xc4, + 0x53, 0xda, 0xfd, 0x6b, 0x02, 0x8b, 0xc7, 0xe1, 0xf7, 0xba, 0xb8, 0x93, 0xd7, 0x0b, + 0x7e, 0xc3, 0x6b, 0x80, 0xc6, 0x45, 0xf9, 0xb9, 0x5d, 0x4d, 0x83, 0x8a, 0x4d, 0x14, + 0x38, 0x40, 0xc9, 0x61, 0xd5, 0x95, 0xff, 0x43, 0x0e, 0xed, 0xa4, 0x7f, 0xe9, 0xb6, + 0x82, 0x6f, 0x08, 0xfe, 0x6c, 0x54, 0x17, 0xea, 0x78, 0xf8, 0x08, 0x09, 0x9c, 0x10, + 0x02, 0x25, 0xd6, 0x69, 0x59, 0x35, 0x8b, 0xe9, 0xb3, 0x9a, 0x38, 0xc2, 0xf9, 0x23, + 0x43, 0xe1, 0x52, 0xad, 0x97, 0x6c, 0x7a, 0x37, 0xa7, 0xf3, 0xc3, 0xe1, 0x64, 0x07, + 0x52, 0xf4, 0xd4, 0xd0, 0x4e, 0x93, 0xfa, 0x1b, 0x9d, 0x99, 0xe7, 0xe5, 0x47, 0xa1, + 0x96, 0x10, 0x09, 0x2d, 0x4a, 0x1d, 0x33, 0xbd, 0xa0, 0xce, 0x10, 0x08, 0xca, 0xfa, + 0xa0, 0x1d, 0xcc, 0x14, 0xc1, 0xab, 0x1d, 0xe1, 0x3e, 0x37, 0xbf, 0xb4, 0xaf, 0xda, + 0x05, 0x4e, 0x64, 0x40, 0x52, 0xd3, 0x6e, 0xa2, 0x28, 0xd8, 0xee, 0x62, 0x68, 0x72, + 0x33, 0x95, 0x3f, 0xcf, 0x4f, 0xa5, 0x4c, 0xa4, 0x89, 0xca, 0xff, 0xaa, 0x52, 0xeb, + 0xb9, 0xac, 0xc5, 0xb4, 0x47, 0x9a, 0x74, 0xdd, 0xf7, 0x60, 0x12, 0x7e, 0xcb, 0x80, + 0xfa, 0xa3, 0xee, 0x50, 0x38, 0xaa, 0xd5, 0x47, 0x7c, 0x76, 0x14, 0x1a, 0xc7, 0x31, + 0x26, 0x4b, 0x89, 0x0f, 0x0d, 0x91, 0x8e, 0x1f, 0xe7, 0xe0, 0xb9, 0x0c, 0x2d, 0x3e, + 0xcb, 0xbc, 0x7c, 0x40, 0x71, 0x8e, 0x2f, 0xf4, 0x7a, 0x51, 0x9f, 0x82, 0x61, 0xf2, + 0xeb, 0x60, 0x9e, 0x00, 0xea, 0x34, 0xbc, 0xb9, 0xe6, 0x87, 0xde, 0x43, 0xc5, 0xa2, + 0xd7, 0x31, 0x14, 0x17, 0x5d, 0xcc, 0xb1, 0x1a, 0xb4, 0xb0, 0xed, 0x9c, 0xc4, 0x06, + 0x5b, 0x00, 0xed, 0x0b, 0xac, 0xe4, 0x15, 0x2c, 0x49, 0xe6, 0x02, 0xdc, 0x7e, 0x4b, + 0xbb, 0xb5, 0xca, 0xdd, 0xe7, 0xd2, 0x2f, 0x8c, 0x97, 0x56, 0xf1, 0x6f, 0xe4, 0x94, + 0xc6, 0xc5, 0x53, 0xc5, 0x89, 0x6a, 0xc0, 0xd4, 0x23, 0x42, 0xa7, 0x95, 0xf0, 0xf4, + 0xfe, 0xec, 0xdc, 0x3b, 0xee, 0xb5, 0xcf, 0x6e, 0x8a, 0x28, 0x63, 0x7d, 0x7c, 0x7b, + 0x79, 0x64, 0xa6, 0x5a, 0xef, 0x9a, 0x5d, 0x5c, 0x7e, 0x94, 0x84, 0xd3, 0xb9, 0x7f, + 0xdb, 0x43, 0x97, 0xc5, 0xab, 0xf1, 0x63, 0xb2, 0x87, 0x2b, 0x6b, 0x78, 0x0d, 0x7f, + 0x9d, 0xf2, 0x10, 0x10, 0xdc, 0x86, 0x56, 0x89, 0x8d, 0xba, 0x35, 0x54, 0x84, 0x41, + 0x24, 0xad, 0x9a, 0x5f, 0x17, 0x3e, 0x6b, 0xde, 0xed, 0x45, 0xae, 0xe7, 0xce, 0xe0, + 0x60, 0x69, 0xc4, 0xe9, 0x3d, 0x07, 0xe3, 0xcd, 0x7a, 0x2a, 0xb6, 0xa8, 0x44, 0x5e, + 0x80, 0x56, 0xf5, 0x86, 0x5a, 0xbb, 0x83, 0x08, 0x4f, 0xfa, 0x51, 0xa5, 0xd0, 0x38, + 0xb2, 0x62, 0xf1, 0xbc, 0x46, 0x8b, 0x70, 0x80, 0xe5, 0x3f, 0x60, 0x58, 0x28, 0x6d, + 0x45, 0xeb, 0xb5, 0x32, 0xe0, 0x28, 0x2e, 0xc8, 0xe2, 0xde, 0xd3, 0x4b, 0x19, 0xf7, + 0x30, 0xe8, 0xfb, 0xd0, 0x4e, 0x02, 0x80, 0x55, 0x80, 0x06, 0x93, 0x66, 0x70, 0x47, + 0xec, 0x95, 0xbc, 0xd6, 0x97, 0xc8, 0x25, 0x74, 0x3a, 0x56, 0xb6, 0x73, 0x52, 0x9d, + 0xf8, 0x01, 0x8e, 0x8d, 0xff, 0x34, 0x5d, 0x7d, 0x5b, 0x7f, 0x1b, 0xff, 0x68, 0xd1, + 0x7c, 0xfa, 0x97, 0xe1, 0x1a, 0x30, 0x54, 0x6f, 0xdb, 0xad, 0x60, 0xa4, 0x83, 0xf4, + 0x35, 0xe9, 0xf8, 0xc8, 0x5d, 0x81, 0x91, 0xa0, 0x7e, 0xb9, 0xd8, 0x8e, 0x0f, 0xaf, + 0xd5, 0x9d, 0x21, 0x09, 0x42, 0xd9, 0x3b, 0x4d, 0x14, 0x07, 0x8e, 0xf4, 0x46, 0xb2, + 0x8e, 0x6b, 0x16, 0x86, 0x9f, 0xab, 0x07, 0xde, 0x46, 0x9b, 0x41, 0xde, 0x98, 0x33, + 0x7d, 0xed, 0x2a, 0x48, 0x00, 0x63, 0x19, 0x62, 0x39, 0x0c, 0x8e, 0x18, 0x3c, 0x01, + 0x08, 0x49, 0xb0, 0xe0, 0xe8, 0xd8, 0x4b, 0x4d, 0xd9, 0xef, 0xd4, 0xc5, 0xaf, 0x8b, + 0x9a, 0x7c, 0xa0, 0x49, 0x7b, 0xc4, 0x45, 0xae, 0xdf, 0x4a, 0x28, 0x65, 0xfd, 0x89, + 0xdc, 0xcf, 0x8e, 0xfb, 0x77, 0x4b, 0x8a, 0x98, 0x6a, 0xc0, 0x13, 0x84, 0x8e, 0xfd, + 0x91, 0x8a, 0x70, 0xe2, 0xb6, 0xc1, 0x99, 0x7b, 0x3d, 0x69, 0x82, 0xbd, 0x84, 0xc9, + 0xbc, 0x73, 0x48, 0x69, 0x50, 0x3f, 0xa1, 0x19, 0x73, 0x00, 0xfd, 0x5e, 0x3e, 0xf2, + 0x2f, 0x8a, 0x04, 0x81, 0x53, 0xc1, 0xe0, 0x14, 0xf8, 0x7e, 0xd6, 0x64, 0x1c, 0x79, + 0xc3, 0xca, 0xb1, 0x85, 0xfb, 0x66, 0xb8, 0x30, 0xb4, 0x92, 0x5a, 0x39, 0x91, 0xb3, + 0x5b, 0xe4, 0xc7, 0x26, 0xd6, 0x12, 0x0a, 0xe0, 0x06, 0x44, 0xfa, 0x37, 0x80, 0xc8, + 0x56, 0x2e, 0x8b, 0xbf, 0xa8, 0xd2, 0x73, 0x5b, 0x6d, 0xd2, 0x74, 0x93, 0xb8, 0x65, + 0xdc, 0xe2, 0xb0, 0x75, 0xfa, 0x13, 0x02, 0x38, 0x54, 0x70, 0x34, 0xe5, 0x08, 0x34, + 0x02, 0x02, 0x13, 0x82, 0x85, 0x8a, 0xd7, 0x13, 0xa6, 0x1e, 0xfd, 0x67, 0xd1, 0x2c, + 0xad, 0x27, 0xbc, 0x3c, 0xaf, 0x15, 0x0d, 0x93, 0xed, 0xca, 0xd7, 0x2f, 0xde, 0x04, + 0xfc, 0x02, 0xe6, 0x11, 0x60, 0x69, 0xd6, 0x98, 0xc3, 0x4c, 0xd1, 0xc3, 0x2b, 0xd2, + 0x8e, 0xb9, 0x0e, 0xfc, 0x86, 0x42, 0xd6, 0xa8, 0x2f, 0x93, 0xe9, 0x26, 0xfd, 0xac, + 0xf5, 0xd6, 0x5d, 0x2a, 0x86, 0x6b, 0x8f, 0xe5, 0xa2, 0x8a, 0x01, 0x82, 0xd5, 0x6c, + 0xbd, 0xb2, 0xee, 0x0a, 0xcd, 0xcb, 0xcf, 0x61, 0x14, 0x6f, 0x56, 0x76, 0x81, 0xcb, + 0x8f, 0x72, 0xdc, 0x4a, 0x80, 0x85, 0x74, 0x51, 0x09, 0xf3, 0x41, 0x1b, 0xd2, 0x50, + 0xa8, 0x8f, 0x4c, 0x75, 0x3b, 0xce, 0xb0, 0x88, 0x43, 0xff, 0xa4, 0xad, 0xc8, 0x1a, + 0xaf, 0xee, 0xb2, 0xf3, 0xdc, 0x01, 0x94, 0x49, 0x74, 0x5f, 0xd3, 0xc8, 0xec, 0x1b, + 0x82, 0x80, 0xe0, 0xdd, 0x71, 0xc2, 0x4e, 0xfa, 0x28, 0xb0, 0x76, 0x6d, 0xc0, 0x9b, + 0x87, 0x78, 0x63, 0x17, 0x90, 0xe2, 0x51, 0x57, 0x38, 0xd2, 0x32, 0x2e, 0x41, 0x56, + 0xfc, 0x86, 0x3d, 0xd0, 0xf5, 0x8d, 0x84, 0xf2, 0x4f, 0x87, 0xc3, 0xeb, 0x9f, 0x5e, + 0x54, 0x4c, 0x26, 0x0f, 0x7c, 0xd9, 0x4d, 0x04, 0xf4, 0xf7, 0x72, 0xc7, 0x33, 0x72, + 0xd4, 0x50, 0x51, 0x93, 0xf0, 0x1e, 0xcb, 0xb7, 0xe6, 0xe0, 0x64, 0x81, 0x51, 0x3a, + 0xb7, 0xd4, 0xbb, 0xbe, 0xc3, 0xd2, 0x08, 0x46, 0x1e, 0xb2, 0x73, 0x0c, 0xc0, 0xb8, + 0x1f, 0x6d, 0xc8, 0x8f, 0xf4, 0x85, 0x40, 0x72, 0x45, 0xd4, 0x7c, 0x3b, 0xb3, 0xd8, + 0x4f, 0x70, 0x81, 0x03, 0x2e, 0x68, 0x6b, 0x11, 0x74, 0x34, 0x2a, 0x84, 0x91, 0xe8, + 0x66, 0xa6, 0xa0, 0xa6, 0x93, 0x9c, 0xaf, 0xea, 0xe4, 0x18, 0x4b, 0x17, 0x82, 0x3f, + 0x8e, 0xf7, 0x6a, 0xc5, 0xce, 0x0d, 0xdd, 0x18, 0x5d, 0xdf, 0x8e, 0xa9, 0xda, 0xa9, + 0x58, 0x85, 0x0d, 0x67, 0x4c, 0x9c, 0x86, 0x1f, 0xb4, 0x86, 0xdd, 0x90, 0x29, 0x4b, + 0xd9, 0x05, 0x85, 0x8a, 0xb7, 0x6f, 0x2d, 0xbe, 0xa7, 0x4f, 0x43, 0xd0, 0x32, 0xd4, + 0x3b, 0x2c, 0x5d, 0xa8, 0x22, 0xe1, 0x35, 0x1f, 0xd6, 0x2c, 0x45, 0x98, 0xed, 0x71, + 0xf7, 0x65, 0x36, 0x91, 0x5f, 0xed, 0xe4, 0xf6, 0xf9, 0xbf, 0x61, 0x08, 0xdc, 0xa0, + 0x40, 0xa2, 0x0e, 0xd0, 0xe1, 0x74, 0x71, 0x63, 0x87, 0x7b, 0xfa, 0xbd, 0x58, 0x9a, + 0x18, 0xab, 0x23, 0x2c, 0x61, 0x66, 0xe9, 0xbc, 0x66, 0xdd, 0xab, 0xc7, 0x24, 0xc4, + 0xa4, 0xe2, 0x3a, 0xf0, 0xd9, 0x53, 0xab, 0x02, 0x00, 0x3d, 0x6d, 0xe7, 0x0f, 0xac, + 0x31, 0xcf, 0x51, 0x7f, 0x1c, 0x2e, 0xe6, 0xf5, 0x02, 0x90, 0x5d, 0x11, 0xcf, 0xfb, + 0x62, 0xc7, 0xa7, 0x03, 0x61, 0x2f, 0xa0, 0x8e, 0x33, 0xd5, 0xeb, 0x66, 0xfa, 0x54, + 0x0b, 0x4f, 0xf8, 0xe4, 0x79, 0xde, 0xde, 0xbf, 0x08, 0xdb, 0x5e, 0x6b, 0x2b, 0xb4, + 0x84, 0xfa, 0x71, 0xc7, 0x72, 0xf0, 0xc4, 0x78, 0x6d, 0xe4, 0xe1, 0x38, 0xf4, 0x92, + 0xdb, 0x8a, 0x1f, 0x2b, 0xfa, 0x93, 0x40, 0x83, 0xb9, 0xdf, 0xcc, 0x04, 0xa9, 0x52, + 0x87, 0xbd, 0xf0, 0xa6, 0x75, 0x8d, 0x7c, 0x2c, 0x2d, 0x0a, 0x5f, 0x6b, 0xa4, 0x2a, + 0xe7, 0x67, 0x91, 0x56, 0x9d, 0x7c, 0xfd, 0x03, 0x90, 0x10, 0xe4, 0x26, 0xc6, 0x4d, + 0x36, 0x46, 0x9b, 0xa8, 0x1e, 0x81, 0x09, 0xec, 0xcb, 0x03, 0xe3, 0x9a, 0xff, 0x4e, + 0xe9, 0xde, 0xd0, 0xa7, 0x2d, 0xdf, 0xb7, 0x41, 0xb0, 0xd5, 0xd4, 0x75, 0xee, 0x7f, + 0x26, 0x50, 0xbd, 0xaf, 0x4f, 0xc4, 0x4b, 0x21, 0x3f, 0x7c, 0xc2, 0xec, 0xa0, 0x43, + 0xf8, 0x6c, 0x87, 0x9e, 0x37, 0x19, 0xd3, 0x1c, 0x2f, 0x42, 0x96, 0x6f, 0x8c, 0x2c, + 0xa0, 0x07, 0xa4, 0xde, 0xb7, 0xe6, 0x0a, 0x37, 0xcd, 0xd7, 0x13, 0xaf, 0xa8, 0xac, + 0xda, 0x26, 0x2f, 0x53, 0x65, 0x93, 0x13, 0x40, 0x40, 0xdc, 0xeb, 0x5d, 0x29, 0x25, + 0x48, 0xbe, 0xac, 0x4b, 0xa8, 0x11, 0xf2, 0x64, 0x03, 0xa4, 0x2c, 0xff, 0x39, 0x9a, + 0x2e, 0x07, 0x26, 0xb7, 0x58, 0xe2, 0xa1, 0x6d, 0x01, 0xce, 0x1c, 0x7b, 0xe0, 0x47, + 0xc0, 0xcb, 0x3a, 0x66, 0xa4, 0x8e, 0x8b, 0xa8, 0x9f, 0x2d, 0x48, 0x0b, 0x0e, 0x75, + 0x5b, 0x82, 0x8d, 0x3b, 0x94, 0xed, 0x40, 0x60, 0xbc, 0x8a, 0x30, 0x68, 0xf2, 0x0a, + 0x2d, 0x36, 0x5c, 0xb2, 0x84, 0x0d, 0x28, 0xe3, 0x23, 0x10, 0x04, 0x3f, 0xe5, 0x35, + 0xd8, 0x92, 0xd0, 0x20, 0x27, 0x65, 0xa3, 0xf6, 0x8c, 0x8f, 0xec, 0x96, 0x30, 0x1a, + 0xde, 0x85, 0x8f, 0x9a, 0x93, 0x22, 0xf0, 0xaa, 0x8d, 0xaf, 0xef, 0x15, 0x84, 0xe6, + 0x2e, 0x41, 0x7c, 0x31, 0x0b, 0x2b, 0xe5, 0x8e, 0x5b, 0x08, 0x44, 0x2a, 0x62, 0xbc, + 0x5b, 0x06, 0x9f, 0xdf, 0x60, 0x91, 0xfa, 0x59, 0x64, 0xaf, 0xa1, 0x89, 0x1b, 0xdb, + 0x77, 0xef, 0x7e, 0xcb, 0x01, 0x94, 0x16, 0x69, 0xb9, 0x13, 0x40, 0x85, 0xff, 0xc6, + 0xd4, 0x4c, 0xcd, 0xc8, 0xe8, 0x9f, 0x43, 0x14, 0x09, 0xb3, 0xae, 0xa6, 0xca, 0x6c, + 0x18, 0xe7, 0x54, 0xae, 0x75, 0xca, 0xbc, 0x10, 0xf1, 0xb2, 0xad, 0x81, 0x98, 0x9d, + 0x99, 0x92, 0xa9, 0x94, 0xf3, 0xe5, 0xf6, 0xc0, 0xe7, 0xef, 0xe5, 0x6d, 0x80, 0x61, + 0x4e, 0x67, 0x42, 0x22, 0xa4, 0xeb, 0xe0, 0x29, 0x6f, 0x91, 0x12, 0x40, 0x07, 0xf0, + 0x89, 0xe7, 0x9a, 0xfc, 0x7b, 0xcb, 0xe6, 0x7a, 0xaa, 0x36, 0x76, 0x37, 0xa8, 0xf0, + 0x95, 0xac, 0x9c, 0x70, 0xe8, 0x1d, 0x10, 0xdd, 0x36, 0x51, 0xb6, 0x77, 0xb5, 0xee, + 0x6a, 0x48, 0x4f, 0x0a, 0x81, 0x50, 0x17, 0xf1, 0x6b, 0xf4, 0x3c, 0x8e, 0x19, 0x3a, + 0x80, 0xce, 0x32, 0x74, 0x60, 0xaf, 0x1b, 0xae, 0x73, 0xb5, 0xdf, 0xce, 0xce, 0xf2, + 0x8e, 0x0b, 0x9f, 0xb8, 0x3e, 0x6e, 0x49, 0xf9, 0x95, 0xa6, 0x88, 0x62, 0x70, 0x01, + 0xa3, 0xef, 0x08, 0xdd, 0x59, 0x33, 0xe0, 0x05, 0x60, 0x3f, 0x22, 0x61, 0x69, 0x96, + 0x1a, 0xf0, 0xcc, 0xa1, 0x41, 0x78, 0x62, 0x1e, 0x55, 0x09, 0x97, 0x5b, 0x10, 0xf2, + 0x51, 0x85, 0x3f, 0x1e, 0x07, 0xf9, 0x35, 0xe4, 0x30, 0xf4, 0xd3, 0x39, 0xdd, 0x2e, + 0x65, 0x2a, 0xe1, 0x7c, 0x8c, 0x72, 0x62, 0x7f, 0x19, 0x24, 0x45, 0xcf, 0xeb, 0x23, + 0x7c, 0xfb, 0xa3, 0x36, 0x7c, 0x15, 0x6c, 0xcc, 0xbd, 0x45, 0xea, 0xfa, 0x2a, 0xb0, + 0x6c, 0xbd, 0xad, 0xce, 0x3a, 0x5f, 0x9a, 0xc4, 0x6b, 0x36, 0xfd, 0x93, 0x9d, 0x0b, + 0x29, 0x68, 0x7c, 0xe8, 0xdf, 0xdf, 0x4b, 0xab, 0x92, 0xa6, 0x71, 0xd4, 0xe4, 0x56, + 0xd2, 0x00, 0xc1, 0xf9, 0xc0, 0x33, 0x49, 0x5c, 0x66, 0xc7, 0xd0, 0xfa, 0x32, 0x2f, + 0x1f, 0x7b, 0x05, 0x1f, 0x37, 0x11, 0x19, 0x51, 0x1c, 0x2f, 0x36, 0x0f, 0x28, 0x25, + 0x30, 0xa6, 0xd7, 0x21, 0xc5, 0x4d, 0xf2, 0x56, 0x6b, 0xc8, 0xad, 0xbf, 0x3c, 0x54, + 0x0e, 0x79, 0xf5, 0x07, 0xb1, 0x07, 0xb5, 0xff, 0xe0, 0xa5, 0x8a, 0x17, 0xc1, 0xda, + 0xa6, 0x02, 0xfb, 0x07, 0xc0, 0xb2, 0xda, 0xeb, 0x55, 0xa1, 0x2e, 0x80, 0x1d, 0x7c, + 0x28, 0x39, 0xd2, 0x90, 0xa6, 0x4b, 0x5e, 0x28, 0x17, 0x8f, 0x59, 0x72, 0xc4, 0xa2, + 0xa6, 0x22, 0xd5, 0xe2, 0x12, 0xf3, 0x03, 0x90, 0x1c, 0xc3, 0xe5, 0xff, 0x85, 0x38, + 0x68, 0x83, 0xac, 0x13, 0x04, 0x60, 0xfb, 0x45, 0x10, 0xd2, 0x2a, 0xe8, 0x8c, 0x6e, + 0x53, 0x6d, 0xfb, 0x0e, 0x3d, 0x41, 0x9e, 0x17, 0x2a, 0x00, 0x05, 0xb0, 0xad, 0x25, + 0xbc, 0x76, 0x43, 0x11, 0x75, 0x7f, 0xab, 0x47, 0x0b, 0x55, 0x04, 0x8c, 0x54, 0x0f, + 0x80, 0x37, 0x5e, 0x79, 0x78, 0x2e, 0x87, 0xb6, 0x23, 0xd7, 0x4b, 0x87, 0xc3, 0x59, + 0x4f, 0xc2, 0x41, 0xa0, 0x01, 0x77, 0x29, 0x80, 0xfe, 0x68, 0xca, 0x52, 0xf6, 0xcc, + 0xe1, 0xde, 0xe5, 0x69, 0xf5, 0x93, 0xf5, 0x48, 0xbc, 0x84, 0x68, 0xad, 0x43, 0xf8, + 0x35, 0x6e, 0x3b, 0x53, 0x95, 0xa1, 0x1a, 0xe3, 0xb1, 0x0a, 0x52, 0xad, 0x16, 0xc0, + 0x58, 0x8f, 0x13, 0x5d, 0xd8, 0x73, 0xb4, 0x8b, 0xe6, 0x95, 0x07, 0x67, 0x65, 0x1f, + 0x8a, 0x56, 0xf8, 0x2a, 0xc9, 0x01, 0x20, 0x20, 0xab, 0x9e, 0xea, 0x34, 0x10, 0xb4, + 0x0a, 0xb2, 0xb1, 0x2b, 0xbb, 0x69, 0x4c, 0x51, 0x71, 0xcb, 0x5e, 0x3a, 0x26, 0x35, + 0x81, 0x46, 0x7a, 0xe4, 0xe2, 0xe3, 0x5b, 0x48, 0x65, 0xf9, 0xe6, 0x98, 0x37, 0x2f, + 0x84, 0xbb, 0x3f, 0xb6, 0xa5, 0x4f, 0x60, 0xe0, 0x21, 0x46, 0x08, 0x98, 0x5c, 0x32, + 0xb0, 0x32, 0x89, 0x34, 0xdf, 0xfe, 0x80, 0xf6, 0x30, 0x15, 0x4b, 0xa6, 0x1b, 0xf0, + 0x09, 0x49, 0x3e, 0x3a, 0xf6, 0x95, 0x53, 0x8c, 0x52, 0x3f, 0x7f, 0x31, 0xb8, 0xa4, + 0xec, 0x81, 0xc8, 0x4b, 0xd7, 0x75, 0x4e, 0xe7, 0xfb, 0x09, 0x0c, 0xb4, 0x8d, 0x87, + 0xad, 0x6b, 0xf0, 0x8e, 0xf7, 0x93, 0xe4, 0x36, 0x7f, 0xb8, 0x47, 0x64, 0x92, 0xf8, + 0xad, 0x92, 0xde, 0x03, 0x5a, 0x4d, 0xd5, 0xc4, 0xf7, 0x56, 0x87, 0x33, 0x3f, 0xe8, + 0x30, 0x04, 0x8a, 0xcf, 0xfd, 0xd3, 0xb7, 0xc0, 0x28, 0xed, 0xd1, 0xf1, 0xfd, 0x68, + 0xae, 0xb8, 0x52, 0xbf, 0xe9, 0x27, 0x99, 0xd4, 0x95, 0xf3, 0xf3, 0xac, 0xf8, 0x3a, + 0x99, 0x36, 0x52, 0xbe, 0x83, 0xc7, 0x48, 0xd4, 0x2e, 0x7c, 0x42, 0xd8, 0xc8, 0xf2, + 0x68, 0x14, 0xf3, 0xea, 0xe6, 0xbf, 0x0c, 0x1d, 0x39, 0x5b, 0x8f, 0xf2, 0x65, 0xd1, + 0x3c, 0xac, 0x8b, 0x31, 0x9b, 0xf6, 0x36, 0xcb, 0x06, 0xa7, 0x4c, 0x58, 0xaa, 0x5b, + 0x96, 0x52, 0x22, 0xfd, 0x18, 0x90, 0x83, 0xaa, 0xa4, 0xa6, 0xe9, 0xa4, 0x6c, 0x06, + 0x49, 0x63, 0xcb, 0x0a, 0x76, 0x31, 0xe7, 0x9c, 0xfc, 0xd8, 0xbe, 0x8b, 0x0b, 0x7a, + 0xdd, 0x02, 0x64, 0xd5, 0xe5, 0x40, 0xd6, 0x95, 0xf8, 0x71, 0x02, 0x62, 0xe4, 0xb4, + 0x8d, 0x76, 0xe3, 0x6a, 0x61, 0x3c, 0x63, 0x2f, 0xb9, 0x2f, 0x1c, 0x69, 0xb4, 0xc9, + 0xc6, 0xf2, 0xa8, 0xe2, 0x49, 0x95, 0x04, 0x90, 0x88, 0xd4, 0x4a, 0x79, 0xc6, 0x16, + 0xf2, 0x80, 0x9b, 0xe8, 0xfe, 0x91, 0xba, 0x39, 0x3d, 0x17, 0x9c, 0xbb, 0x48, 0xb0, + 0x3f, 0x24, 0x57, 0xdd, 0x14, 0x7c, 0x17, 0x5c, 0x54, 0xb3, 0xc2, 0x27, 0xa2, 0x3b, + 0x33, 0xa2, 0x96, 0xf6, 0xd1, 0xc9, 0x98, 0xf0, 0x1a, 0x3d, 0x4d, 0x8c, 0x07, 0x65, + 0x21, 0x6e, 0x7e, 0x1a, 0x25, 0xbc, 0x10, 0xa9, 0x79, 0x31, 0xd9, 0xc7, 0xdc, 0x2b, + 0x76, 0xe0, 0x7e, 0x28, 0x4e, 0x0a, 0x14, 0x42, 0xcc, 0x9c, 0x9f, 0x6b, 0x8d, 0x69, + 0xb3, 0x88, 0x79, 0xc9, 0xf9, 0x5b, 0x93, 0x90, 0x7c, 0x56, 0x57, 0x2f, 0xd2, 0xa5, + 0xca, 0xa8, 0x62, 0x5b, 0xd9, 0x0e, 0x40, 0x55, 0x19, 0x6e, 0x02, 0x3a, 0xf4, 0x02, + 0x67, 0x40, 0x91, 0x8b, 0x26, 0x09, 0xd1, 0xdf, 0x5d, 0xfa, 0x36, 0xaf, 0xa3, 0x2f, + 0x30, 0xb7, 0xf5, 0x1b, 0xa9, 0xda, 0xfe, 0x93, 0xe5, 0x06, 0x23, 0x8b, 0xf3, 0x90, + 0xbd, 0xa7, 0xbe, 0xe5, 0xae, 0xaf, 0x95, 0x38, 0x1f, 0x55, 0xaa, 0x19, 0x31, 0x7d, + 0x52, 0x85, 0x2c, 0x60, 0x4e, 0x68, 0x81, 0x06, 0xf2, 0xa3, 0xce, 0x0f, 0xc2, 0xf6, + 0x72, 0xfd, 0xff, 0xcd, 0x82, 0xd7, 0xfb, 0x27, 0x1b, 0xef, 0x46, 0x24, 0x97, 0xbc, + 0x15, 0x2f, 0xb1, 0x5d, 0x94, 0xab, 0x8a, 0x1f, 0xd2, 0xf9, 0xf4, 0xe5, 0x49, 0x30, + 0x74, 0xfc, 0xb6, 0x3e, 0x63, 0x14, 0x21, 0xfc, 0xc4, 0xb8, 0xef, 0x0a, 0x8b, 0xa4, + 0x89, 0x3e, 0x7e, 0x43, 0x98, 0x11, 0x40, 0xed, 0x74, 0xf6, 0x66, 0xde, 0x8a, 0xb2, + 0xf3, 0xc0, 0x1b, 0xef, 0xf0, 0x72, 0x79, 0xe4, 0xfe, 0x89, 0x93, 0x56, 0x82, 0xf6, + 0xac, 0x52, 0x40, 0x69, 0x18, 0x91, 0x75, 0xe8, 0xe8, 0xea, 0x04, 0x59, 0xb2, 0xfb, + 0x51, 0x93, 0x94, 0xbc, 0x50, 0x65, 0x57, 0x36, 0x52, 0xf3, 0x20, 0x37, 0x75, 0x88, + 0x7f, 0x9f, 0x76, 0x6b, 0x26, 0x63, 0xc8, 0x17, 0x3b, 0x21, 0x13, 0x4a, 0xc2, 0xc5, + 0x41, 0x8c, 0xd6, 0xec, 0x54, 0xa1, 0xe6, 0x83, 0xf1, 0xb5, 0x6b, 0xfb, 0x2c, 0x87, + 0x8b, 0xa0, 0x7c, 0x67, 0xee, 0xb1, 0xa0, 0x75, 0x90, 0x45, 0x72, 0xb9, 0x41, 0xae, + 0x23, 0x7b, 0x05, 0x07, 0x44, 0x2a, 0x14, 0x87, 0x4e, 0x1e, 0x56, 0x08, 0xd1, 0xa7, + 0x57, 0xd2, 0x6c, 0x92, 0x3a, 0x6d, 0x46, 0x4d, 0x57, 0x1a, 0xd2, 0x5c, 0xf8, 0xb8, + 0xf1, 0xd5, 0x19, 0x91, 0xa3, 0x52, 0x94, 0xcb, 0x60, 0xcd, 0xef, 0x3c, 0x11, 0x38, + 0xb5, 0xa4, 0x65, 0x49, 0x71, 0x9a, 0xee, 0xa6, 0x06, 0x7d, 0xd2, 0xc5, 0x23, 0xa2, + 0x02, 0xcb, 0xf4, 0x43, 0xaa, 0x15, 0x26, 0x14, 0xa8, 0x1d, 0x06, 0xd2, 0x70, 0x03, + 0xea, 0xf4, 0x8e, 0x3f, 0xdd, 0x96, 0x7b, 0x60, 0x7b, 0x5d, 0x49, 0x30, 0x56, 0xde, + 0xd7, 0xba, 0xc0, 0x93, 0x5c, 0x3f, 0x0c, 0x4e, 0x86, 0x01, 0xd4, 0xd9, 0xbb, 0xf7, + 0x85, 0x22, 0x4a, 0x70, 0x7d, 0x15, 0xc2, 0x92, 0x84, 0x67, 0xff, 0x4c, 0xae, 0x55, + 0x2c, 0x3e, 0xde, 0xb5, 0x3b, 0x0e, 0xb4, 0xcb, 0xb3, 0x98, 0x74, 0xe3, 0x39, 0xd5, + 0x4c, 0x21, 0x8c, 0x82, 0xe3, 0xa5, 0x3e, 0xf8, 0x0f, 0xc8, 0x7e, 0x52, 0x37, 0x06, + 0x73, 0x61, 0x2c, 0x78, 0x45, 0x21, 0xea, 0x7b, 0xe0, 0x18, 0x53, 0x4b, 0xb2, 0xd1, + 0x74, 0x9c, 0x30, 0x1a, 0x6e, 0x0b, 0x9c, 0xd3, 0x7d, 0xb1, 0x2e, 0x9a, 0x7c, 0xbb, + 0xd2, 0xc1, 0xaf, 0xfe, 0x1b, 0x7d, 0xf7, 0x00, 0x27, 0x93, 0xa4, 0x8d, 0xe7, 0x91, + 0x80, 0xb3, 0xa0, 0x9d, 0xa9, 0x2c, 0xbb, 0xf6, 0x58, 0x7a, 0x2b, 0xe3, 0x02, 0xe2, + 0x6a, 0x1e, 0x5f, 0xa0, 0x4b, 0xc0, 0x37, 0x8d, 0x8f, 0x40, 0xd4, 0x91, 0x80, 0xb0, + 0x16, 0x74, 0xf8, 0x00, 0x5b, 0xf7, 0x67, 0xe9, 0xf0, 0xf4, 0xcb, 0x21, 0x43, 0x93, + 0x98, 0xf1, 0xac, 0x85, 0xe1, 0x50, 0x8c, 0x5f, 0xa4, 0x0d, 0x20, 0x1b, 0x24, 0x7c, + 0x3c, 0x1b, 0xc3, 0xcb, 0xc0, 0x58, 0x7a, 0x45, 0x05, 0x3d, 0x78, 0xd4, 0xd0, 0x71, + 0x92, 0x05, 0x66, 0xdc, 0x2a, 0x7c, 0xff, 0x44, 0xea, 0xa1, 0x70, 0xec, 0x68, 0x19, + 0xf4, 0x3c, 0x75, 0x0d, 0xd0, 0x6c, 0x92, 0x8a, 0x98, 0xab, 0x59, 0xf6, 0x9b, 0x8c, + 0xda, 0x4e, 0x31, 0x10, 0x80, 0xfc, 0x96, 0xe8, 0xab, 0x03, 0x68, 0xb9, 0xdd, 0x07, + 0x48, 0xbc, 0xc3, 0xa8, 0xe5, 0x6d, 0x5a, 0x15, 0x09, 0x66, 0x87, 0x4d, 0x35, 0xca, + 0x3b, 0xe0, 0x70, 0xb4, 0x3d, 0x35, 0x7f, 0x72, 0xfd, 0x98, 0xb8, 0x8f, 0xf8, 0x39, + 0x24, 0xeb, 0x39, 0x8a, 0x3a, 0x2c, 0x2e, 0x78, 0x9a, 0xf8, 0x64, 0xc6, 0xc3, 0x9e, + 0x3d, 0xc4, 0xbc, 0xbd, 0x51, 0x73, 0x1e, 0x0f, 0x76, 0x46, 0x4b, 0x4a, 0x66, 0xb6, + 0x6f, 0x48, 0x0d, 0xd3, 0x54, 0xf1, 0xd6, 0xad, 0x21, 0xb2, 0xb3, 0x28, 0xa6, 0x29, + 0x82, 0xa8, 0x9a, 0x57, 0x0c, 0x24, 0x8a, 0xfc, 0x9f, 0x70, 0x43, 0x3b, 0x3f, 0xd9, + 0x4f, 0xf3, 0xac, 0x95, 0x6f, 0xaf, 0xfa, 0x02, 0xfa, 0xae, 0x44, 0x2d, 0xc1, 0x8f, + 0x76, 0xfc, 0xd3, 0x14, 0x22, 0x95, 0x04, 0x26, 0xe9, 0x39, 0x93, 0x92, 0x62, 0x91, + 0x13, 0x36, 0x70, 0xcb, 0x1a, 0xdd, 0x69, 0x59, 0xe9, 0x30, 0xb8, 0x75, 0x08, 0x1a, + 0x15, 0x5f, 0x96, 0xfa, 0x8d, 0x70, 0xa6, 0xaa, 0x64, 0x5f, 0x59, 0x37, 0x04, 0x16, + 0xd8, 0xd3, 0x02, 0x0e, 0x69, 0xf4, 0xb2, 0xf2, 0xa1, 0xa8, 0x68, 0xfb, 0xeb, 0xda, + 0xb5, 0xb3, 0xa0, 0x0b, 0xe9, 0x9c, 0x43, 0xb4, 0x82, 0x05, 0x58, 0x1c, 0xe3, 0x8b, + 0x8a, 0x53, 0x95, 0x1e, 0x5b, 0xbb, 0x23, 0x51, 0xbb, 0x9e, 0x13, 0xa0, 0xc2, 0x28, + 0x9b, 0xe0, 0x7b, 0xba, 0x56, 0x87, 0x84, 0xbb, 0x34, 0x9e, 0x1e, 0x0d, 0x89, 0x16, + 0x59, 0xd9, 0xe4, 0xe0, 0x29, 0x08, 0xd0, 0x31, 0x68, 0x4f, 0xe2, 0xd1, 0x0f, 0x75, + 0x9f, 0x08, 0x2b, 0x6d, 0xd1, 0xe7, 0x61, 0x0d, 0xaa, 0x69, 0xfb, 0xee, 0x9e, 0xb2, + 0x05, 0xdb, 0x6e, 0xe8, 0x39, 0x3d, 0xd5, 0x65, 0x54, 0x5a, 0x5a, 0xe8, 0x4e, 0x16, + 0x60, 0x69, 0x00, 0xc3, 0x59, 0xd5, 0xf7, 0x5e, 0xd4, 0x2c, 0xeb, 0xfd, 0x9e, 0x97, + 0xf6, 0xac, 0xaa, 0x5e, 0xb9, 0xb8, 0x5b, 0xfd, 0x0e, 0x22, 0xbf, 0xa7, 0xa2, 0x82, + 0x47, 0x09, 0x2c, 0x20, 0xf8, 0x82, 0xa3, 0x79, 0x71, 0xed, 0xfa, 0xa4, 0x49, 0x9c, + 0x5b, 0x2f, 0xda, 0x48, 0xb2, 0x11, 0x59, 0x76, 0xcf, 0xa5, 0xb1, 0x31, 0x58, 0x22, + 0x20, 0x41, 0xd3, 0x19, 0xaf, 0xf3, 0xbd, 0x40, 0x75, 0x51, 0xbf, 0xf2, 0xac, 0x5c, + 0x9a, 0x43, 0x2e, 0x3d, 0x76, 0x13, 0xf5, 0x8a, 0x88, 0x02, 0x17, 0xbb, 0xbb, 0x6f, + 0x86, 0x5d, 0x9a, 0x6a, 0x7e, 0xd6, 0xe5, 0x71, 0x9e, 0xb9, 0xa1, 0xe7, 0xd0, 0xd8, + 0x17, 0xd8, 0xc1, 0xce, 0xd4, 0xdf, 0xd5, 0x73, 0x03, 0xe0, 0x40, 0xcf, 0x12, 0xe6, + 0x2b, 0xb3, 0xaa, 0xee, 0x75, 0x3a, 0x44, 0x3c, 0xe4, 0xaf, 0xe4, 0x67, 0xe8, 0x9c, + 0x30, 0x74, 0x64, 0x52, 0xd5, 0x75, 0x63, 0xe1, 0xba, 0xc2, 0x18, 0x4b, 0x97, 0xec, + 0x6a, 0xec, 0xb7, 0xa6, 0xb7, 0x8f, 0x57, 0xdb, 0x49, 0x56, 0x91, 0x4f, 0x86, 0xc6, + 0xe2, 0x66, 0x60, 0xd1, 0x2d, 0x4f, 0xb7, 0x90, 0x81, 0x98, 0xf0, 0x50, 0xfa, 0x5e, + 0x18, 0xac, 0x99, 0x76, 0xfe, 0x8b, 0x97, 0x39, 0xe9, 0xa9, 0x9a, 0x29, 0xfb, 0x46, + 0xce, 0x15, 0x38, 0x24, 0xad, 0xe6, 0x1c, 0x69, 0xb7, 0x73, 0x09, 0x04, 0x56, 0x08, + 0x90, 0x16, 0x63, 0x77, 0x70, 0x29, 0xef, 0x82, 0x2e, 0x65, 0x3f, 0x3d, 0x57, 0xca, + 0x35, 0x8a, 0x4b, 0x66, 0x8a, 0x95, 0x2b, 0xe9, 0x8e, 0x4b, 0xbd, 0x1d, 0xea, 0xe5, + 0x22, 0x2e, 0x98, 0xd4, 0x7f, 0xc4, 0xf5, 0x53, 0xb3, 0x07, 0x5d, 0x47, 0x8d, 0x68, + 0x92, 0xdc, 0x8d, 0xa8, 0xac, 0x8e, 0xcf, 0x0f, 0xd4, 0x63, 0xac, 0x85, 0x80, 0x65, + 0xe9, 0x68, 0x78, 0x3f, 0xc2, 0xc3, 0x7c, 0x8f, 0x94, 0xf2, 0x11, 0x2f, 0xd0, 0x26, + 0x75, 0xdb, 0x4b, 0x3a, 0x06, 0x45, 0x9f, 0x6a, 0x16, 0x06, 0x88, 0x39, 0x4c, 0xea, + 0x21, 0x01, 0x66, 0xfa, 0x11, 0x3a, 0x7e, 0xf5, 0x65, 0xaa, 0xa9, 0x6b, 0xfe, 0x17, + 0x8d, 0x82, 0x0e, 0x86, 0x98, 0x20, 0xec, 0x82, 0xf8, 0xb9, 0x2b, 0x2a, 0x79, 0x33, + 0x25, 0xe1, 0xb7, 0x16, 0xfa, 0xa8, 0xcc, 0xf6, 0x9c, 0x99, 0x38, 0xb1, 0xdc, 0x3f, + 0xf7, 0xc2, 0x2f, 0x27, 0x97, 0x47, 0xa2, 0x65, 0xab, 0xc7, 0xa3, 0x06, 0x93, 0x14, + 0x14, 0x1c, 0x25, 0x36, 0xf9, 0x3e, 0xf1, 0x4d, 0x23, 0x25, 0xa9, 0x94, 0x3c, 0x9b, + 0x93, 0x29, 0xd8, 0xb8, 0xa5, 0x6c, 0xd7, 0xc3, 0xb2, 0x91, 0x9c, 0x63, 0x6a, 0x62, + 0x5e, 0x5e, 0xec, 0xf2, 0xd5, 0x4b, 0xcf, 0x4b, 0x9d, 0x65, 0x7b, 0xbb, 0xdb, 0x88, + 0x67, 0xa3, 0x9b, 0x71, 0xc7, 0xe0, 0xee, 0x38, 0x70, 0x70, 0x2e, 0x23, 0x23, 0x02, + 0x3b, 0x67, 0x71, 0xdc, 0x55, 0x7c, 0x23, 0x37, 0xda, 0x37, 0xe0, 0xf6, 0xa0, 0x81, + 0x33, 0x54, 0x3e, 0xaa, 0x2a, 0xbf, 0xc0, 0xe9, 0x2c, 0xfc, 0x32, 0xbd, 0xe4, 0x1f, + 0xf0, 0x27, 0x82, 0x16, 0xff, 0x02, 0x4b, 0xe9, 0x94, 0xb1, 0xe4, 0x03, 0x28, 0x5d, + 0x7e, 0x22, 0x3a, 0xd5, 0xf2, 0xca, 0x15, 0x99, 0x72, 0xf8, 0xa3, 0x32, 0x01, 0x95, + 0x7d, 0x3e, 0xf3, 0xf1, 0xd7, 0x67, 0x10, 0x24, 0x3d, 0x80, 0x69, 0x10, 0x22, 0x0e, + 0x35, 0x72, 0xc6, 0x67, 0xa8, 0x72, 0x2b, 0x0f, 0x51, 0xe4, 0xcf, 0xc5, 0xf0, 0x91, + 0xc1, 0x3a, 0x3d, 0x60, 0xf7, 0xa5, 0x48, 0xab, 0xd3, 0x69, 0x16, 0xf0, 0xa7, 0xd7, + 0x67, 0x06, 0xa4, 0x5a, 0x99, 0x36, 0xa7, 0x51, 0x30, 0x65, 0x1d, 0x3d, 0x48, 0x7b, + 0xbf, 0x95, 0x6f, 0x0b, 0x45, 0xb0, 0xe9, 0xeb, 0x2b, 0x9f, 0x49, 0x8f, 0xab, 0xae, + 0x86, 0xa1, 0x98, 0xb4, 0x37, 0xb5, 0x05, 0x25, 0xa7, 0xbe, 0x8e, 0x73, 0x64, 0x8a, + 0x60, 0xe6, 0xd6, 0x83, 0x7d, 0xf4, 0xcd, 0xbf, 0x76, 0x2f, 0xcd, 0xee, 0xc2, 0xdb, + 0xd0, 0xd7, 0x60, 0x20, 0x44, 0xf5, 0x82, 0xd9, 0x66, 0x07, 0xad, 0x0c, 0xf8, 0xf6, + 0x43, 0xa9, 0xec, 0x48, 0xb4, 0x42, 0xcb, 0x1b, 0x42, 0x22, 0x8e, 0x05, 0x11, 0x19, + 0x9b, 0xb3, 0xb2, 0x8c, 0xc6, 0x4b, 0x38, 0x20, 0x87, 0xd4, 0x41, 0x26, 0x9f, 0xcc, + 0x6f, 0x87, 0xa9, 0x2d, 0x07, 0x7a, 0x14, 0xed, 0xa1, 0xf0, 0x09, 0x87, 0x47, 0xd4, + 0xc3, 0x74, 0xca, 0x2f, 0x9b, 0x00, 0x91, 0xe6, 0xe6, 0xf3, 0x40, 0x5d, 0xcd, 0xbc, + 0xf8, 0xa2, 0x6a, 0x0d, 0xa3, 0xdd, 0xa6, 0xcb, 0xb0, 0x15, 0x0a, 0xc0, 0x82, 0x7e, + 0x1b, 0x4f, 0x62, 0xe7, 0x92, 0x06, 0xea, 0x88, 0xe1, 0xd6, 0x7a, 0x0a, 0x3b, 0x4f, + 0x00, 0xcd, 0xf2, 0xd9, 0xe0, 0xed, 0xba, 0xb3, 0x3b, 0x62, 0xa2, 0x94, 0xa7, 0x99, + 0x99, 0xc9, 0x44, 0x91, 0x06, 0x9e, 0xb0, 0x41, 0x52, 0xfe, 0x64, 0x21, 0x81, 0x98, + 0xdc, 0x97, 0xaf, 0xc6, 0x2c, 0x8e, 0x7c, 0x2b, 0xfc, 0x5b, 0xcf, 0x8a, 0xdb, 0x60, + 0x1d, 0x33, 0x5d, 0x6d, 0x4e, 0x4c, 0xd3, 0x9d, 0xd9, 0x40, 0xbc, 0x42, 0x7a, 0x8b, + 0x4b, 0xa7, 0x49, 0xe5, 0x9e, 0xed, 0x2a, 0x47, 0xc8, 0xa0, 0x31, 0xee, 0x92, 0x6a, + 0xda, 0x76, 0x63, 0x41, 0x80, 0x73, 0x5c, 0xb1, 0xd7, 0xa7, 0xf8, 0x44, 0x08, 0x57, + 0xe2, 0x99, 0x01, 0xb0, 0xf2, 0x3e, 0x5d, 0xde, 0x04, 0xab, 0xbb, 0xa9, 0xf5, 0x7e, + 0xe8, 0x6d, 0x68, 0xf3, 0x60, 0x70, 0xa1, 0xe2, 0xf1, 0xac, 0x95, 0x1e, 0xd4, 0x8a, + 0x89, 0x90, 0x6f, 0xa9, 0x0d, 0x43, 0x91, 0x50, 0xf8, 0xb1, 0xf2, 0x72, 0x9e, 0x85, + 0xe7, 0x76, 0xdb, 0x76, 0xe9, 0xe3, 0x0a, 0xf9, 0xa8, 0xfe, 0xcc, 0xd4, 0x44, 0xdc, + 0x07, 0xea, 0x98, 0x34, 0x0f, 0xf6, 0x81, 0x89, 0xa3, 0xb2, 0x3a, 0x51, 0xe9, 0x18, + 0x11, 0xf3, 0x52, 0x91, 0x59, 0xca, 0x34, 0xbf, 0x8c, 0x82, 0x34, 0xf1, 0x4a, 0xe0, + 0xbf, 0xff, 0x19, 0xdd, 0x74, 0x03, 0x90, 0x88, 0x99, 0xe2, 0x5f, 0x81, 0x14, 0x68, + 0x10, 0x45, 0x5c, 0xaf, 0x23, 0xe8, 0x2b, 0x44, 0xfe, 0xa6, 0xf0, 0x36, 0x1f, 0x4b, + 0xbc, 0x74, 0xeb, 0xbe, 0xd7, 0x66, 0x3e, 0x16, 0xf2, 0xcd, 0x9b, 0xb6, 0xa2, 0x85, + 0xad, 0x4b, 0x5c, 0xaa, 0x75, 0x26, 0xfe, 0x92, 0xa9, 0xe5, 0xc4, 0x75, 0xda, 0x9a, + 0x28, 0x33, 0x47, 0xf8, 0xf1, 0xab, 0xef, 0xe3, 0xf4, 0x79, 0x9d, 0x50, 0xf2, 0x51, + 0xa0, 0x2c, 0x1a, 0x33, 0x9e, 0xf7, 0x80, 0x4e, 0xa6, 0xcf, 0x11, 0x04, 0x67, 0x69, + 0x9b, 0xbe, 0x60, 0x18, 0x05, 0x43, 0x7b, 0xff, 0x63, 0x17, 0xf6, 0x0e, 0x36, 0xe0, + 0xf6, 0x7b, 0x96, 0x48, 0xcd, 0x43, 0x93, 0x1c, 0x9b, 0xec, 0xa1, 0x32, 0xa5, 0xf9, + 0x80, 0x40, 0x6b, 0xa7, 0x98, 0xc9, 0x41, 0x45, 0xfe, 0x87, 0x00, 0x94, 0x91, 0xad, + 0x60, 0x76, 0x52, 0xe6, 0x14, 0xae, 0x6a, 0xcd, 0x59, 0x94, 0x66, 0xdf, 0x69, 0x66, + 0x8f, 0x5c, 0xd6, 0x3e, 0xfc, 0xdf, 0xb5, 0x78, 0x20, 0xd5, 0xe9, 0xa2, 0x2b, 0xf3, + 0xe5, 0x42, 0x3e, 0xcd, 0x88, 0x27, 0x9f, 0x05, 0xc8, 0xa1, 0x48, 0x17, 0x34, 0x37, + 0x3a, 0xbc, 0xf7, 0xee, 0x3b, 0x27, 0xd2, 0x76, 0xc3, 0xfc, 0xce, 0xbe, 0xe1, 0x9f, + 0xe6, 0x4a, 0xb8, 0x8d, 0x72, 0x2a, 0x9d, 0x20, 0x96, 0x85, 0x74, 0x06, 0x8f, 0xe0, + 0xb4, 0x38, 0x92, 0xbc, 0x20, 0xec, 0xb5, 0x13, 0x0a, 0xa2, 0x8e, 0xef, 0x98, 0x17, + 0xa2, 0xb9, 0x07, 0x7c, 0xeb, 0xb7, 0x7c, 0xba, 0x8b, 0x00, 0xb5, 0xef, 0x3e, 0x79, + 0x79, 0xf7, 0x2b, 0xd6, 0xf4, 0x4d, 0x70, 0xbe, 0x0c, 0x8c, 0x5d, 0xc8, 0xe2, 0x33, + 0xf0, 0x7b, 0x0d, 0x19, 0x48, 0xde, 0x06, 0x9d, 0xd1, 0x33, 0xc1, 0x07, 0x9b, 0x5b, + 0xc3, 0xa4, 0xc7, 0x94, 0x72, 0x86, 0xbb, 0x2a, 0x0f, 0xe6, 0x20, 0xb4, 0x28, 0x32, + 0xf4, 0x11, 0x78, 0x97, 0xdd, 0xa7, 0x83, 0x64, 0x48, 0x06, 0xca, 0x29, 0x29, 0xf8, + 0xda, 0xd3, 0xf0, 0x35, 0x25, 0xfe, 0xbc, 0xd4, 0x58, 0x3f, 0xf9, 0x54, 0xfa, 0x0d, + 0x7d, 0xf1, 0x4a, 0x6e, 0x91, 0x0e, 0x75, 0x53, 0x70, 0xe1, 0x0c, 0x27, 0xae, 0x21, + 0xd9, 0xaa, 0x5e, 0x4f, 0xc4, 0xef, 0xc5, 0xec, 0x7d, 0x44, 0xe7, 0xb9, 0x5b, 0x9b, + 0x8a, 0x59, 0x72, 0xf0, 0x31, 0x32, 0x58, 0xab, 0xad, 0x58, 0x32, 0x93, 0x93, 0x08, + 0x59, 0xbb, 0x08, 0xcc, 0x85, 0x84, 0x1c, 0x43, 0x2a, 0x43, 0xd5, 0x4d, 0xe0, 0xea, + 0x41, 0xbd, 0xa2, 0x84, 0xf8, 0x91, 0x3c, 0x33, 0xd9, 0xd0, 0xb3, 0x16, 0x77, 0x92, + 0xb3, 0x68, 0xd3, 0x20, 0x1d, 0x1e, 0x9c, 0x82, 0xd7, 0xed, 0x11, 0xfd, 0xee, 0x60, + 0xb9, 0x6d, 0xbc, 0x3a, 0x39, 0x82, 0x6f, 0xd9, 0xf2, 0x64, 0x03, 0x4f, 0xe0, 0x72, + 0x50, 0xad, 0xe7, 0xcf, 0xf8, 0x77, 0xb9, 0xd0, 0x0c, 0xd9, 0xda, 0x55, 0x43, 0x39, + 0xd6, 0x95, 0x7b, 0xf2, 0x9a, 0x14, 0x48, 0x4a, 0x6a, 0x4a, 0x11, 0x67, 0x97, 0x78, + 0x87, 0xff, 0xdf, 0x2f, 0x64, 0x4c, 0x6a, 0xca, 0x56, 0xb3, 0x6c, 0x15, 0xe7, 0xb6, + 0x22, 0x20, 0x1e, 0xe5, 0x1b, 0xa0, 0x3d, 0xf3, 0xc6, 0xf5, 0xe7, 0x51, 0x23, 0x48, + 0x98, 0xeb, 0xf9, 0x1c, 0x31, 0x3f, 0x8d, 0x28, 0xd6, 0xbb, 0x5a, 0x00, 0x72, 0xe4, + 0xb6, 0x1e, 0x48, 0xd9, 0x1c, 0xc3, 0xe8, 0x80, 0x52, 0x61, 0x72, 0xf1, 0x02, 0x19, + 0x81, 0x77, 0xe8, 0xdb, 0x68, 0xd4, 0x4c, 0xa5, 0x17, 0x2d, 0xc9, 0xbd, 0x79, 0xaf, + 0xa7, 0x59, 0x4d, 0xd5, 0x69, 0xbb, 0x6a, 0xb1, 0x60, 0xb6, 0x09, 0xb5, 0x0b, 0xe5, + 0x17, 0x8a, 0x2d, 0x25, 0x63, 0xc6, 0xc1, 0xd0, 0x6f, 0x1a, 0xa1, 0xb4, 0xab, 0x37, + 0x83, 0x18, 0x7c, 0xac, 0x66, 0x4c, 0x3a, 0xcc, 0x57, 0xb7, 0x81, 0x25, 0xf3, 0x6a, + 0xdc, 0x3a, 0x1c, 0x63, 0x91, 0xcc, 0xfe, 0xb5, 0xb5, 0x59, 0xca, 0xd3, 0x88, 0x2b, + 0x8a, 0x74, 0xd9, 0x4a, 0xc2, 0xdd, 0x10, 0x2b, 0x8b, 0x0b, 0x11, 0x0b, 0x22, 0xc6, + 0x4a, 0x88, 0x48, 0x12, 0x13, 0xdf, 0x70, 0x7b, 0x8a, 0xf2, 0x4c, 0xbb, 0x91, 0x0c, + 0x9d, 0x1e, 0x88, 0x6d, 0x8b, 0xab, 0xaa, 0xb6, 0x9b, 0xd6, 0x83, 0x2b, 0x02, 0xa4, + 0x00, 0xbb, 0x85, 0x82, 0xa5, 0x5a, 0x2b, 0xca, 0x1e, 0xcd, 0x6e, 0xa2, 0xdd, 0xc5, + 0x52, 0xc6, 0x3f, 0xf3, 0x91, 0x05, 0xc5, 0x5a, 0xb9, 0x3d, 0x81, 0x93, 0x62, 0x35, + 0x1e, 0xd1, 0xfe, 0x79, 0xf5, 0x39, 0x7d, 0xf5, 0x10, 0x26, 0xe5, 0x41, 0xc9, 0x20, + 0xb1, 0x41, 0x63, 0xc9, 0xf0, 0xc1, 0xa0, 0x92, 0x62, 0x60, 0x0f, 0x9a, 0x9a, 0x97, + 0x65, 0x9e, 0xee, 0xd2, 0xb7, 0x52, 0xbb, 0x1d, 0xa1, 0xf7, 0xb0, 0xfa, 0xbf, 0x46, + 0x25, 0x0e, 0x84, 0x1c, 0x19, 0x42, 0xf4, 0x88, 0xb1, 0x4f, 0x38, 0xa0, 0x48, 0x2c, + 0xc2, 0x8b, 0x42, 0x16, 0xd9, 0xfd, 0xd4, 0x16, 0x41, 0x5f, 0x7e, 0x30, 0xe8, 0xa2, + 0x73, 0x55, 0x04, 0xaf, 0x87, 0xa1, 0x8c, 0x8e, 0xa8, 0x62, 0xb0, 0xd9, 0x75, 0xe7, + 0x58, 0xe6, 0xea, 0x81, 0x00, 0x7d, 0x51, 0x44, 0xc8, 0xf3, 0x35, 0x78, 0x6f, 0x03, + 0xc0, 0x51, 0xde, 0x99, 0xb3, 0x38, 0xdc, 0x0f, 0x79, 0xce, 0x34, 0xa7, 0x53, 0xbc, + 0x8f, 0x14, 0x0a, 0x7e, 0x67, 0xc3, 0x6c, 0x86, 0x08, 0xf5, 0x6e, 0xe7, 0x13, 0xc0, + 0x5e, 0x76, 0xf0, 0xfb, 0x6a, 0xd6, 0xa1, 0x90, 0x21, 0x0f, 0xb2, 0x78, 0xb5, 0x36, + 0x5e, 0x2e, 0x65, 0x3c, 0xbe, 0xbe, 0x64, 0x4b, 0xb8, 0x98, 0xfc, 0x5f, 0x0b, 0x70, + 0x56, 0x51, 0x7f, 0x03, 0xaa, 0xe2, 0x33, 0x88, 0x32, 0x8b, 0xf2, 0x36, 0x3a, 0xfa, + 0xa1, 0x3f, 0x7f, 0xef, 0x9f, 0x6a, 0x8c, 0xea, 0x2b, 0xf4, 0x86, 0x24, 0x68, 0xc5, + 0xb2, 0x22, 0xee, 0x06, 0xaa, 0x95, 0x98, 0x67, 0x4f, 0x2a, 0x41, 0x79, 0xf6, 0x9c, + 0x2c, 0xef, 0x6b, 0x16, 0xba, 0x96, 0x78, 0x80, 0x8f, 0xc5, 0x69, 0x18, 0x01, 0x43, + 0x14, 0x70, 0x80, 0x23, 0x9c, 0xfa, 0x65, 0x33, 0x7e, 0x28, 0x9d, 0x21, 0x4b, 0x48, + 0xa2, 0xae, 0xac, 0xf5, 0x8e, 0xf6, 0x8a, 0x32, 0x46, 0x52, 0xe9, 0xb5, 0xcb, 0x21, + 0xb7, 0xec, 0xfb, 0x21, 0x11, 0xf7, 0xda, 0x20, 0xff, 0x08, 0x9b, 0x8b, 0xe0, 0xb1, + 0xdb, 0xeb, 0x48, 0x8c, 0x88, 0x66, 0xda, 0x19, 0x27, 0xd7, 0x3f, 0x93, 0xef, 0x3e, + 0xfd, 0x04, 0x76, 0x69, 0x08, 0x12, 0x30, 0x72, 0x0f, 0xc8, 0x33, 0x23, 0xd0, 0x33, + 0x9d, 0x64, 0xdc, 0xfa, 0xc9, 0x26, 0x69, 0xea, 0xba, 0x5b, 0xc7, 0x83, 0xaf, 0x54, + 0xef, 0x14, 0xd7, 0xb3, 0x74, 0x30, 0xca, 0xe6, 0x30, 0xff, 0x45, 0x45, 0xbb, 0x55, + 0x7d, 0xf6, 0xf3, 0xa2, 0x3c, 0xf0, 0x93, 0x44, 0x81, 0xd0, 0x98, 0x70, 0xc7, 0x3d, + 0x1e, 0x1b, 0x4f, 0xca, 0xf7, 0x3b, 0xe1, 0x79, 0x4c, 0x4b, 0xdb, 0x21, 0xfe, 0xc5, + 0x05, 0x2b, 0x71, 0x28, 0xa2, 0x43, 0x8c, 0x4b, 0x82, 0x80, 0xd5, 0xf3, 0x21, 0xea, + 0x66, 0x30, 0x93, 0xfc, 0xb5, 0x0b, 0xc9, 0x9b, 0xab, 0x12, 0xfe, 0xbe, 0xfd, 0x0f, + 0x61, 0x0c, 0x92, 0x55, 0x23, 0x3b, 0xe0, 0x5d, 0x5c, 0x30, 0x06, 0x47, 0x38, 0x9b, + 0x4e, 0xf3, 0x1c, 0xf7, 0x43, 0x38, 0xda, 0x32, 0x65, 0xd6, 0x89, 0x3d, 0x21, 0x67, + 0x8e, 0x7f, 0x0e, 0xa6, 0xaa, 0x06, 0x12, 0x80, 0x52, 0x62, 0xd5, 0x61, 0x2c, 0x05, + 0xaa, 0x68, 0xf4, 0x82, 0x49, 0xbf, 0x59, 0x92, 0xc2, 0xd0, 0x6d, 0xb0, 0x7a, 0x7d, + 0x8f, 0xc2, 0x05, 0xb0, 0x61, 0x48, 0x3b, 0x3c, 0x4b, 0xa7, 0x8b, 0x50, 0xdb, 0x0d, + 0x88, 0x10, 0x87, 0x92, 0xdf, 0xca, 0xd0, 0xe0, 0x26, 0xa6, 0xc1, 0x5b, 0x67, 0xe4, + 0x7c, 0x46, 0xc3, 0x0d, 0xa2, 0x64, 0x19, 0xb9, 0x5b, 0xd9, 0xb7, 0x23, 0x10, 0xb0, + 0x45, 0x56, 0xfe, 0xc3, 0xb8, 0x6f, 0x6d, 0xe6, 0x73, 0x78, 0x4d, 0x38, 0x89, 0xf8, + 0x2a, 0x0d, 0x81, 0x44, 0xab, 0xaf, 0xd8, 0x08, 0x15, 0xd6, 0x5b, 0x1c, 0x7a, 0x35, + 0x44, 0xc0, 0xdb, 0x0f, 0xe6, 0x31, 0x60, 0x49, 0xf2, 0x0d, 0x90, 0x7b, 0xfb, 0x56, + 0x73, 0xf1, 0xc5, 0x87, 0x84, 0x2d, 0xd1, 0x85, 0x93, 0xbb, 0x0c, 0x1f, 0x86, 0xff, + 0xb6, 0xe2, 0x2d, 0x20, 0x56, 0xf8, 0xa1, 0x85, 0xe2, 0xd3, 0xb6, 0xdd, 0x0c, 0x4c, + 0xe5, 0xc4, 0x61, 0x5c, 0xc4, 0x9f, 0x30, 0x00, 0x67, 0x0c, 0x65, 0xe2, 0xb1, 0x28, + 0xf8, 0x06, 0xa4, 0x53, 0xa4, 0x08, 0xec, 0x2f, 0x1b, 0xb5, 0xd4, 0xe5, 0xd2, 0x97, + 0x11, 0x44, 0xd6, 0x5d, 0xdb, 0xd2, 0x84, 0x05, 0x3b, 0x0e, 0x6e, 0xf9, 0x38, 0x3d, + 0x19, 0xf3, 0x73, 0xc5, 0x6c, 0x5a, 0xae, 0xda, 0x6d, 0x2e, 0x5c, 0x58, 0x8a, 0x9e, + 0xcc, 0x3c, 0x19, 0x5f, 0xa3, 0x79, 0x04, 0x21, 0x5f, 0x79, 0x9b, 0x77, 0x96, 0x3f, + 0xf4, 0x04, 0x69, 0xee, 0x34, 0x37, 0x2d, 0x6c, 0x6d, 0x6e, 0xeb, 0x66, 0xd5, 0x66, + 0xb8, 0xaa, 0xbd, 0xcd, 0xa7, 0xb4, 0x76, 0xce, 0x31, 0x96, 0x45, 0xb6, 0x06, 0x33, + 0x24, 0xb5, 0x26, 0x3f, 0xe1, 0xd2, 0x7a, 0x7e, 0xd0, 0xf1, 0x7b, 0x63, 0x3d, 0xd7, + 0xc1, 0x05, 0x00, 0x09, 0xfe, 0xaf, 0x77, 0x15, 0x8d, 0x6d, 0x6a, 0x09, 0xe3, 0x34, + 0xfd, 0x8a, 0x1d, 0x9c, 0x92, 0xd9, 0x94, 0x0a, 0x52, 0x51, 0xac, 0xfc, 0x9e, 0xe6, + 0x52, 0x8c, 0xf4, 0x8d, 0xda, 0x78, 0x24, 0x18, 0x2f, 0x7b, 0x4b, 0x12, 0x67, 0x0a, + 0x37, 0xd1, 0xd5, 0x32, 0x90, 0xa6, 0xa9, 0x7a, 0xfb, 0x02, 0x9a, 0x4c, 0xb2, 0xb4, + 0x97, 0x03, 0x1e, 0x2a, 0x46, 0xf7, 0x12, 0xc1, 0xd4, 0xc2, 0x89, 0x38, 0x6c, 0xbd, + 0xb8, 0xc9, 0x97, 0xa8, 0x85, 0x17, 0x39, 0x98, 0xdd, 0x8d, 0xc2, 0xee, 0x22, 0xad, + 0x02, 0x01, 0x0d, 0x2a, 0x80, 0x2b, 0x99, 0xba, 0x71, 0xc2, 0x58, 0x7b, 0xb3, 0xff, + 0x4d, 0x17, 0x87, 0xb7, 0xc1, 0x8b, 0xf6, 0xde, 0xc4, 0x9a, 0x60, 0x57, 0xa4, 0x73, + 0xfe, 0xc2, 0xc1, 0xe6, 0x50, 0xdd, 0xcb, 0x23, 0x0d, 0xd8, 0x56, 0xdc, 0x33, 0x16, + 0xf5, 0x80, 0xe3, 0xce, 0xe3, 0x7c, 0x4f, 0x1f, 0x9b, 0x2d, 0x22, 0x28, 0x76, 0x42, + 0xfe, 0x9a, 0x65, 0xdb, 0x59, 0x01, 0xc5, 0x9b, 0xca, 0x76, 0xe9, 0xf1, 0x13, 0xa7, + 0x00, 0x5c, 0x4b, 0x10, 0x67, 0xc8, 0x0b, 0x75, 0xb5, 0x97, 0x20, 0x2a, 0xef, 0x21, + 0x4b, 0x36, 0x93, 0xd6, 0x26, 0xd0, 0x93, 0xf9, 0xb3, 0x9b, 0xec, 0xf6, 0x03, 0x72, + 0xab, 0x5a, 0xb8, 0x9d, 0x95, 0x68, 0x5d, 0x98, 0xc1, 0x72, 0xf8, 0x91, 0x18, 0xac, + 0x7f, 0x94, 0xb6, 0x4a, 0xc7, 0xe6, 0xd2, 0xab, 0x7c, 0x58, 0x37, 0x94, 0xd0, 0x39, + 0xc7, 0x55, 0x86, 0xfb, 0xc0, 0xdd, 0x6a, 0xfa, 0x39, 0x2e, 0xb3, 0x7e, 0x3c, 0x81, + 0x7d, 0x19, 0x58, 0xca, 0xe0, 0x54, 0x47, 0xa9, 0xe7, 0x21, 0x8d, 0xc2, 0xd8, 0x14, + 0xd4, 0xe2, 0xcc, 0xa2, 0x0e, 0x4e, 0x9f, 0x65, 0x2a, 0xb4, 0x53, 0xff, 0x72, 0xf2, + 0xd0, 0x76, 0xfb, 0x9b, 0xd6, 0x41, 0xad, 0x11, 0x13, 0x6c, 0xf3, 0x75, 0x8f, 0x62, + 0x93, 0x81, 0x67, 0x04, 0x5d, 0x71, 0x2f, 0x52, 0xb8, 0x74, 0x66, 0x3c, 0x9d, 0x6c, + 0xfc, 0x61, 0x32, 0x15, 0xf6, 0x71, 0xfb, 0xf8, 0x0e, 0xfa, 0xe8, 0xba, 0x82, 0x71, + 0xad, 0xd4, 0xb6, 0xa9, 0x0a, 0xf6, 0x13, 0xcf, 0x24, 0xd9, 0x68, 0xad, 0x2e, 0x6b, + 0xaa, 0x57, 0xaa, 0x3b, 0x87, 0xe7, 0x1f, 0xdd, 0x5f, 0x60, 0xb3, 0x3d, 0x1c, 0x2f, + 0xd7, 0x02, 0xac, 0xd9, 0xdd, 0xf0, 0xdb, 0x2d, 0xd0, 0x23, 0xa4, 0x5d, 0xf9, 0xf9, + 0x3d, 0x93, 0xd0, 0x30, 0x73, 0x64, 0xe3, 0xaf, 0x81, 0xf1, 0x41, 0x57, 0x9f, 0x5f, + 0xb3, 0xfa, 0x2d, 0x1d, 0x62, 0xa2, 0x0c, 0xb7, 0xe9, 0x79, 0x1c, 0x98, 0x7a, 0xbd, + 0x85, 0x84, 0xf9, 0xdc, 0x78, 0x96, 0x4e, 0x9f, 0xa8, 0x40, 0x1f, 0x7c, 0xcf, 0xc2, + 0x71, 0x64, 0xc1, 0x6c, 0x7c, 0x3b, 0xb9, 0x15, 0xef, 0x6c, 0xca, 0x1d, 0xae, 0x46, + 0x71, 0xdd, 0xd7, 0x79, 0xbd, 0xc9, 0xbb, 0x34, 0x07, 0x5c, 0x05, 0xd2, 0x7b, 0x7b, + 0x01, 0xdf, 0xef, 0x9b, 0xee, 0x8b, 0x28, 0x59, 0x1f, 0xe0, 0x22, 0x2e, 0x6c, 0x26, + 0x03, 0xd8, 0xfe, 0x28, 0x42, 0x29, 0xe0, 0x4f, 0xd3, 0xce, 0x88, 0x95, 0xc8, 0x1f, + 0x0e, 0xf3, 0x28, 0xa4, 0x93, 0x37, 0xe2, 0xb0, 0x40, 0xd5, 0x90, 0xc6, 0x3f, 0x14, + 0x92, 0xb7, 0x9b, 0xab, 0xba, 0x22, 0x63, 0x47, 0x30, 0x80, 0x03, 0xac, 0xeb, 0x39, + 0xb3, 0x9f, 0x23, 0x31, 0xf8, 0x7d, 0x2e, 0x2e, 0x68, 0x22, 0xa7, 0xe2, 0xc4, 0xed, + 0x2b, 0xcc, 0xc0, 0xec, 0x24, 0x5e, 0x4b, 0x63, 0xc9, 0xa9, 0x8e, 0x49, 0x76, 0xed, + 0x5a, 0x7a, 0xa6, 0xff, 0x86, 0xaa, 0x12, 0xb7, 0x20, 0x27, 0x59, 0x0b, 0x49, 0x91, + 0x76, 0x21, 0x80, 0xf6, 0x9b, 0x4e, 0x5d, 0x7f, 0x66, 0x1f, 0x06, 0x3c, 0xb3, 0x0d, + 0xb9, 0xbf, 0x98, 0x90, 0x44, 0x09, 0xe4, 0x93, 0x86, 0x1f, 0x32, 0xad, 0x63, 0x70, + 0x51, 0x7f, 0xe5, 0x6e, 0x24, 0x5d, 0x9f, 0x1c, 0x6e, 0xfa, 0xfd, 0xcd, 0x23, 0x3a, + 0x77, 0xf7, 0x39, 0x88, 0xdd, 0x66, 0xa3, 0xf1, 0x2f, 0x79, 0xdc, 0xc3, 0x32, 0xf7, + 0x35, 0xd4, 0x2b, 0x36, 0x81, 0xd0, 0x2b, 0x23, 0xcf, 0x03, 0xee, 0xd3, 0x28, 0x09, + 0x58, 0x87, 0x1e, 0x1b, 0x21, 0xa4, 0x1a, 0x35, 0x5e, 0xa4, 0x83, 0x8c, 0xc6, 0xc1, + 0x78, 0x6e, 0xf8, 0x15, 0x63, 0x22, 0x08, 0x66, 0x22, 0x3e, 0x10, 0x59, 0x1a, 0x17, + 0x52, 0xe7, 0x96, 0x31, 0x39, 0xed, 0x45, 0x80, 0x25, 0xeb, 0xf0, 0x4d, 0x82, 0xdd, + 0x95, 0xe4, 0x54, 0xb8, 0x26, 0x36, 0x9f, 0xeb, 0x5f, 0x0f, 0xae, 0xbf, 0x86, 0xf0, + 0xc9, 0x4a, 0x45, 0xa8, 0xd4, 0x17, 0xcb, 0xdf, 0x39, 0x51, 0x6f, 0x19, 0x5b, 0x62, + 0x12, 0xff, 0x85, 0xd0, 0x6a, 0xb3, 0x70, 0x46, 0x44, 0x39, 0xfd, 0xe1, 0x64, 0x6c, + 0x6f, 0x01, 0xbf, 0xda, 0x28, 0xb1, 0xdc, 0xb6, 0xbb, 0xfe, 0x44, 0x37, 0x9a, 0xe8, + 0x5b, 0xa3, 0x28, 0x1e, 0x75, 0x63, 0xe3, 0xa7, 0x6c, 0x80, 0x4c, 0x3e, 0x9f, 0xe7, + 0x41, 0xb6, 0x81, 0x20, 0x2a, 0x07, 0x82, 0x09, 0x5c, 0x16, 0xb6, 0x5e, 0xa3, 0x22, + 0x1e, 0x43, 0x7b, 0xec, 0x84, 0x28, 0xf1, 0xde, 0xa8, 0x04, 0xbc, 0x05, 0xc5, 0x8d, + 0xec, 0x5d, 0x79, 0x72, 0x22, 0x25, 0x2e, 0xcf, 0x46, 0xc9, 0x21, 0xaf, 0xeb, 0x51, + 0x75, 0xe9, 0x3c, 0xc7, 0x34, 0xe8, 0xb3, 0x92, 0x74, 0x39, 0x4c, 0x5b, 0x27, 0xfd, + 0x0f, 0x5e, 0x21, 0xd4, 0xa9, 0x36, 0x5e, 0xa4, 0x0e, 0x5c, 0xd5, 0x3c, 0x78, 0x05, + 0x78, 0x14, 0x1c, 0x6b, 0xf1, 0x99, 0xc1, 0x2c, 0xb5, 0xeb, 0x5b, 0xdb, 0xe2, 0xb4, + 0x19, 0x01, 0x35, 0x3f, 0x88, 0x04, 0x5d, 0x8c, 0x69, 0x60, 0x5a, 0x66, 0xcb, 0xd7, + 0xe4, 0xdc, 0xcf, 0x65, 0xe7, 0x6c, 0xe2, 0x71, 0x4e, 0x35, 0x50, 0x79, 0x07, 0x35, + 0xbf, 0xaf, 0x10, 0xc7, 0x7d, 0x5e, 0x0b, 0xd1, 0x4f, 0xa6, 0x41, 0x05, 0xb5, 0x8a, + 0x51, 0xb8, 0x50, 0x65, 0x34, 0x7e, 0x99, 0x55, 0xf1, 0x3d, 0xc7, 0xba, 0xc2, 0x67, + 0x89, 0x6e, 0xb1, 0xcc, 0xb2, 0x15, 0x15, 0xd7, 0x97, 0x70, 0x89, 0xcf, 0x9f, 0xea, + 0x41, 0x7f, 0x84, 0x4b, 0x87, 0x46, 0xca, 0x0e, 0xaf, 0x03, 0xa3, 0x2a, 0xb5, 0x75, + 0xba, 0xbd, 0x98, 0xc6, 0xb5, 0x3a, 0x7e, 0xcc, 0xe2, 0x7d, 0x8e, 0x4b, 0x96, 0x71, + 0x90, 0x24, 0xb1, 0x02, 0xb1, 0x6b, 0x58, 0x6c, 0x31, 0x31, 0x8f, 0xbb, 0xee, 0x95, + 0x9b, 0x91, 0x6c, 0xd3, 0xde, 0x73, 0xfd, 0xc1, 0xf9, 0xf8, 0x4e, 0x4f, 0x3e, 0x9d, + 0xd2, 0x09, 0x44, 0x6f, 0x82, 0x46, 0xf8, 0xac, 0xf0, 0xb6, 0x1e, 0x32, 0xc7, 0x93, + 0x17, 0xca, 0xc4, 0x55, 0x57, 0x30, 0x16, 0xf5, 0x80, 0x71, 0xf9, 0x7e, 0x63, 0xa4, + 0xaa, 0x50, 0x94, 0xcc, 0xa5, 0x5b, 0xab, 0x34, 0xcb, 0xd4, 0x51, 0x21, 0xff, 0xf3, + 0x51, 0xd3, 0x28, 0x16, 0xda, 0xcb, 0x05, 0xc1, 0x32, 0xa0, 0xfe, 0x1d, 0x87, 0x3b, + 0x33, 0x14, 0x95, 0x8f, 0xa2, 0xf7, 0x2b, 0x9d, 0x4d, 0xca, 0x43, 0x79, 0x26, 0x74, + 0x13, 0xc6, 0x98, 0xdf, 0x0b, 0x07, 0xaa, 0xe8, 0x8d, 0xaa, 0x8a, 0xa1, 0x43, 0x42, + 0x7b, 0x26, 0x03, 0xdd, 0x0e, 0x75, 0xf6, 0x5c, 0x39, 0x62, 0x3c, 0x36, 0x5a, 0x00, + 0x49, 0x44, 0x78, 0x3c, 0xe4, 0x4b, 0x81, 0xda, 0x3f, 0xd6, 0x94, 0xc7, 0x1a, 0x2f, + 0x3d, 0x12, 0x77, 0xe5, 0xd5, 0xa8, 0x14, 0x3d, 0xe0, 0x6e, 0x8c, 0x42, 0xc1, 0x83, + 0x81, 0x71, 0x1c, 0xac, 0x99, 0x06, 0xc5, 0xfa, 0xa2, 0x92, 0x21, 0xdf, 0xb8, 0xa4, + 0x44, 0xf0, 0x7d, 0x43, 0xf4, 0x09, 0x31, 0x12, 0x27, 0x10, 0x51, 0x38, 0x75, 0x65, + 0x4b, 0x8b, 0x01, 0x5e, 0x43, 0xf5, 0x70, 0x8f, 0x89, 0x17, 0x9b, 0xe9, 0x19, 0xd1, + 0xe6, 0x55, 0x86, 0xb6, 0xcc, 0x86, 0x10, 0xc7, 0xc6, 0x66, 0x99, 0x18, 0x09, 0x44, + 0xee, 0xc6, 0x74, 0x18, 0xdd, 0xf8, 0xf8, 0x11, 0x77, 0xf0, 0xe3, 0x8d, 0x2c, 0xa6, + 0x04, 0x2b, 0x71, 0x39, 0x33, 0x69, 0xba, 0x67, 0x64, 0xf1, 0x98, 0xb3, 0x23, 0x69, + 0x52, 0xa6, 0x2f, 0x68, 0x6d, 0x24, 0xfc, 0xa5, 0x60, 0x83, 0xf3, 0x87, 0xdf, 0x93, + 0x14, 0xfb, 0xf5, 0x8a, 0x2a, 0x89, 0x22, 0x9b, 0xa5, 0xff, 0x0f, 0x10, 0xe4, 0x3a, + 0x4b, 0xf1, 0xf0, 0x22, 0x12, 0x4c, 0x19, 0xf3, 0xb2, 0x74, 0x56, 0x41, 0xa0, 0x35, + 0x87, 0xac, 0x73, 0xff, 0x8a, 0x30, 0x40, 0xb5, 0x8e, 0xfe, 0x8f, 0xba, 0x6d, 0xc2, + 0x14, 0x09, 0x42, 0x1f, 0xa9, 0xe0, 0xec, 0xbb, 0xe2, 0x52, 0x3c, 0xb4, 0xe7, 0xff, + 0x98, 0xe4, 0x19, 0x27, 0x96, 0x02, 0xfc, 0x11, 0x77, 0xed, 0xda, 0x59, 0xeb, 0xc9, + 0x35, 0x0a, 0xe3, 0x75, 0x4e, 0xa7, 0x73, 0xd4, 0x1e, 0x54, 0x60, 0x3c, 0xf0, 0x24, + 0x86, 0x62, 0x20, 0x46, 0xbb, 0x0e, 0x4a, 0x4f, 0x2a, 0x52, 0x76, 0x9b, 0x9f, 0x6b, + 0xf7, 0x28, 0x8b, 0x13, 0x3f, 0xf1, 0x95, 0x98, 0x64, 0xc8, 0x7a, 0xbf, 0xc3, 0x73, + 0xaf, 0x6a, 0xc4, 0x9f, 0x79, 0x58, 0xc1, 0x08, 0x61, 0xe9, 0x52, 0x72, 0x84, 0x58, + 0xef, 0x8e, 0xe6, 0x29, 0x92, 0xcd, 0x3a, 0x08, 0x98, 0xff, 0x31, 0xe7, 0xec, 0x86, + 0xcf, 0xa1, 0xc3, 0x48, 0x48, 0x72, 0xec, 0x66, 0x0c, 0xb9, 0xfc, 0x32, 0x02, 0xb8, + 0x46, 0x99, 0xfe, 0xb2, 0xb6, 0x98, 0x28, 0xc1, 0xcd, 0xcb, 0x34, 0x67, 0x9f, 0x76, + 0x7f, 0x92, 0x63, 0xa8, 0x7b, 0xf1, 0xe2, 0x23, 0xfc, 0xb1, 0xa8, 0xcd, 0xe2, 0x73, + 0x9e, 0xf3, 0x3a, 0x43, 0x1e, 0xa6, 0xd6, 0x49, 0x97, 0x8e, 0x4e, 0x6a, 0x11, 0x80, + 0x5b, 0x2a, 0xf3, 0x44, 0x68, 0x61, 0x66, 0x08, 0x61, 0x0a, 0x59, 0x50, 0xaa, 0x5f, + 0xa5, 0x59, 0x2d, 0x71, 0x7f, 0x10, 0x28, 0x84, 0xb8, 0x56, 0x7d, 0x52, 0xf8, 0x9d, + 0x37, 0x83, 0x9b, 0x49, 0x2c, 0x3a, 0xef, 0xb8, 0x67, 0x18, 0x00, 0x75, 0x2a, 0x3d, + 0x61, 0xd2, 0x49, 0x37, 0x02, 0x5c, 0x86, 0x0e, 0x16, 0x96, 0x2e, 0x86, 0xc6, 0xaa, + 0x13, 0xe6, 0x11, 0xb5, 0x55, 0xf1, 0xda, 0xef, 0x69, 0x70, 0xf8, 0x54, 0x41, 0x8b, + 0x36, 0x05, 0xc1, 0x85, 0x6c, 0x5b, 0xcd, 0x53, 0x79, 0xcc, 0x29, 0xe6, 0xa5, 0x7b, + 0xe4, 0xfc, 0x3b, 0xcf, 0x90, 0xf8, 0x75, 0x73, 0x07, 0x68, 0x92, 0xe7, 0x28, 0x75, + 0xbc, 0x3b, 0x9f, 0xa3, 0x1b, 0xae, 0x59, 0x3d, 0x14, 0xa8, 0xc5, 0x68, 0x1b, 0x00, + 0xcb, 0xf0, 0x83, 0x69, 0x49, 0x3e, 0x09, 0x31, 0xf2, 0xab, 0x4b, 0xfc, 0x9f, 0x60, + 0xa1, 0xe9, 0x4c, 0x8c, 0x19, 0x62, 0x13, 0x81, 0x7c, 0x07, 0x70, 0x08, 0x44, 0xed, + 0x87, 0x08, 0x5d, 0xed, 0x45, 0x80, 0x35, 0xfe, 0xc6, 0x00, 0x09, 0xd1, 0x4c, 0x06, + 0x93, 0xf3, 0x5e, 0x26, 0x3d, 0x02, 0x33, 0xe5, 0x44, 0x0f, 0x28, 0xc0, 0xf5, 0x7b, + 0xf8, 0x18, 0xbe, 0x54, 0x92, 0xd7, 0x8c, 0xc6, 0x04, 0x0b, 0xc0, 0x7b, 0xf1, 0x21, + 0x68, 0x98, 0x60, 0x4d, 0xc7, 0x56, 0x03, 0xa2, 0x81, 0xdf, 0xd7, 0xc6, 0x1b, 0x14, + 0xc9, 0xc8, 0xd5, 0xa7, 0xcb, 0x07, 0xdb, 0x43, 0x19, 0x0a, 0xfe, 0x4b, 0xc8, 0x05, + 0x7a, 0x38, 0xa0, 0x19, 0xb8, 0x2f, 0xdc, 0x7f, 0x3c, 0x52, 0x88, 0xdc, 0x58, 0x97, + 0x73, 0x85, 0x18, 0xbd, 0x90, 0x6c, 0x7e, 0x2d, 0x6e, 0x2d, 0xa4, 0xf4, 0x45, 0x8f, + 0x40, 0x17, 0x7e, 0x86, 0x86, 0x51, 0x2c, 0xee, 0x16, 0x84, 0x46, 0x28, 0x89, 0xa7, + 0x64, 0x54, 0x3f, 0x61, 0x23, 0x62, 0x4f, 0x77, 0xf0, 0x28, 0xb5, 0xa4, 0xe1, 0x2d, + 0x5b, 0x16, 0x28, 0x35, 0x12, 0x49, 0x54, 0x64, 0x42, 0x46, 0xc3, 0x39, 0xb8, 0x9c, + 0xeb, 0xa7, 0x0a, 0xc8, 0xdc, 0xc7, 0x67, 0x11, 0x0f, 0xda, 0xd9, 0x9e, 0x5d, 0xbb, + 0x2b, 0xbb, 0x36, 0x62, 0x8e, 0xf4, 0x26, 0xd9, 0xcc, 0x30, 0x37, 0x2a, 0x25, 0xb4, + 0xf1, 0x14, 0xb5, 0xd0, 0xa4, 0xa2, 0x0d, 0xef, 0x74, 0xce, 0x34, 0x4d, 0xdc, 0xa2, + 0xcc, 0xf4, 0x71, 0xa2, 0x9e, 0x38, 0x72, 0x98, 0x5e, 0xb4, 0xf3, 0x7c, 0x24, 0xa9, + 0xa3, 0x9b, 0x18, 0x4b, 0xa6, 0x54, 0xff, 0x24, 0x88, 0xe4, 0xbf, 0xfc, 0x06, 0x63, + 0xb0, 0xce, 0xed, 0x39, 0xa5, 0xba, 0x98, 0x17, 0x4f, 0x3d, 0xb9, 0xc0, 0x69, 0x18, + 0x9f, 0x51, 0xb0, 0x43, 0x79, 0x68, 0xb5, 0x73, 0x5b, 0x18, 0x4e, 0xf9, 0x08, 0x84, + 0x25, 0x1c, 0x41, 0xc7, 0x08, 0xc2, 0xb3, 0x58, 0x94, 0xe2, 0x62, 0xf6, 0x3a, 0xc3, + 0x7e, 0x47, 0x77, 0xa0, 0xe7, 0x90, 0xbc, 0xf5, 0x67, 0x43, 0x98, 0xc1, 0xc0, 0xfd, + 0x48, 0x02, 0x30, 0xa1, 0xec, 0xb5, 0x33, 0x23, 0xb1, 0x13, 0xea, 0xd6, 0x0e, 0x7d, + 0x74, 0xb9, 0x5e, 0x96, 0x50, 0x98, 0xb3, 0xf1, 0xf7, 0xeb, 0x19, 0xb9, 0x58, 0x02, + 0x94, 0x1f, 0xcd, 0x68, 0x0c, 0x97, 0x99, 0x75, 0x9c, 0xa0, 0x0b, 0x69, 0x40, 0x8e, + 0x0e, 0x8a, 0x0d, 0xfc, 0xde, 0x3f, 0xa7, 0x07, 0x80, 0x6a, 0x94, 0x27, 0xc6, 0x72, + 0x62, 0x22, 0x00, 0x2d, 0x1a, 0x10, 0xbf, 0xf4, 0x31, 0xc4, 0x24, 0x97, 0x5a, 0x56, + 0x8b, 0xce, 0xe0, 0x9f, 0xb8, 0x9e, 0xda, 0xa7, 0xbe, 0xab, 0x87, 0xb0, 0xba, 0x66, + 0x9e, 0xba, 0xf1, 0x61, 0xd3, 0xb3, 0x8f, 0x65, 0x9b, 0x22, 0x7e, 0x10, 0x1d, 0x5b, + 0xc6, 0x47, 0xdc, 0x0d, 0x7a, 0x2a, 0x9f, 0x3b, 0x76, 0x8c, 0xbd, 0x76, 0x5b, 0x36, + 0xe2, 0xc0, 0x03, 0x13, 0x62, 0xcd, 0xeb, 0xbc, 0xbc, 0xc1, 0xad, 0x3d, 0x54, 0xf4, + 0x0a, 0x1a, 0x4c, 0x07, 0x19, 0x47, 0x9a, 0xa2, 0x11, 0x60, 0xb0, 0xb2, 0x11, 0x5c, + 0x8f, 0xe8, 0x6b, 0x29, 0xb3, 0xa0, 0xcb, 0x34, 0x65, 0xcd, 0x3b, 0x77, 0x91, 0xf6, + 0x5c, 0x9d, 0x0c, 0x79, 0x15, 0x18, 0xed, 0xa6, 0x3f, 0x33, 0x39, 0x5f, 0xf7, 0x37, + 0x1b, 0xb5, 0x14, 0x17, 0xfb, 0xf9, 0xb8, 0x35, 0x50, 0x33, 0xb3, 0x07, 0x22, 0x31, + 0x18, 0xbf, 0x53, 0x21, 0x1b, 0x27, 0x36, 0xe3, 0x2d, 0xf7, 0x70, 0x44, 0x5c, 0xa0, + 0x1f, 0xd9, 0x7a, 0x5e, 0x7b, 0x6a, 0xa5, 0x4a, 0x8e, 0xdd, 0x15, 0x8e, 0x69, 0x0e, + 0x9b, 0xdd, 0x90, 0x18, 0xc6, 0x03, 0x4e, 0x09, 0x68, 0xda, 0x9e, 0x65, 0x9e, 0xa1, + 0x1c, 0xf6, 0x7e, 0x12, 0xee, 0x34, 0xe5, 0xf6, 0xfc, 0x14, 0x40, 0x6f, 0x05, 0x92, + 0x73, 0xf3, 0x21, 0xb4, 0xb7, 0x4a, 0x7d, 0xa2, 0x7f, 0xf3, 0xf5, 0xca, 0xb9, 0xe5, + 0x52, 0x8d, 0x13, 0xbf, 0x2b, 0x0c, 0xbb, 0x72, 0x30, 0xd2, 0xe8, 0x2a, 0x9a, 0xc4, + 0x1e, 0x0b, 0xfe, 0xe2, 0x3d, 0x22, 0x6d, 0x0b, 0xf1, 0xfd, 0x81, 0xdf, 0x92, 0x58, + 0xfc, 0x93, 0x7c, 0x8e, 0xb8, 0xec, 0xc2, 0x78, 0x88, 0x47, 0xf9, 0xd7, 0x0d, 0xad, + 0x8f, 0xe8, 0x41, 0x5c, 0x4c, 0x12, 0xf7, 0xaa, 0x1a, 0xbf, 0xc9, 0x52, 0xcd, 0x70, + 0x32, 0x6c, 0xfe, 0xc4, 0xb8, 0x52, 0x73, 0x4b, 0xc4, 0xc4, 0x50, 0xcd, 0x2d, 0x36, + 0xb8, 0x59, 0x32, 0x95, 0x0a, 0x4c, 0x84, 0x3c, 0x20, 0x65, 0x96, 0xe6, 0x9f, 0xc0, + 0x0a, 0xf5, 0xe8, 0x5f, 0x93, 0xf7, 0x69, 0xf1, 0xb7, 0xa9, 0x7c, 0xbb, 0xcc, 0x54, + 0xac, 0x37, 0xfd, 0x49, 0x2e, 0x61, 0x2a, 0xd5, 0xa3, 0x2f, 0xfb, 0xd5, 0xe8, 0xa2, + 0xe9, 0x1f, 0x4a, 0x08, 0x14, 0xd2, 0x3b, 0x74, 0xf1, 0x4e, 0x6b, 0x87, 0xa1, 0xd5, + 0x17, 0xf9, 0x68, 0x2a, 0xd4, 0xba, 0x67, 0x2b, 0x4f, 0xc5, 0xc3, 0x78, 0xcc, 0x88, + 0x96, 0x90, 0x13, 0x4c, 0x81, 0xbf, 0xf6, 0xe5, 0x97, 0x1b, 0x53, 0xac, 0x81, 0xa4, + 0x4e, 0x82, 0xe7, 0x92, 0x49, 0x9a, 0xf6, 0x53, 0xc1, 0xc3, 0x15, 0x55, 0xe2, 0x4a, + 0x67, 0x41, 0x39, 0x1e, 0xfd, 0x94, 0xea, 0x12, 0xdf, 0x20, 0x2c, 0x43, 0x2e, 0x69, + 0x76, 0x3a, 0x31, 0xb5, 0x25, 0xa9, 0x1d, 0xee, 0x81, 0x37, 0x65, 0x1d, 0x5c, 0xeb, + 0x64, 0x8a, 0x93, 0x2c, 0xab, 0xf8, 0xa6, 0xd8, 0x06, 0xd5, 0x57, 0x3c, 0xd1, 0x61, + 0x9e, 0x4e, 0xf7, 0xe0, 0x3b, 0xc4, 0x6a, 0x18, 0xee, 0xd3, 0x67, 0x46, 0x36, 0x8c, + 0x44, 0xb2, 0xad, 0x56, 0xe1, 0x80, 0x29, 0xdb, 0xf5, 0x4c, 0x3d, 0xbc, 0x96, 0x2b, + 0x8d, 0xd7, 0xb8, 0xfd, 0x99, 0xb6, 0x45, 0x31, 0xac, 0x18, 0xa5, 0x47, 0x70, 0x7e, + 0x2b, 0x00, 0x41, 0x77, 0x9d, 0xce, 0x63, 0x59, 0x7d, 0x65, 0x0e, 0x1e, 0x0d, 0xc3, + 0x02, 0x44, 0x33, 0x2e, 0xa5, 0xec, 0x1e, 0xec, 0x51, 0x07, 0xfa, 0x7f, 0xf5, 0xef, + 0xb6, 0x20, 0xfc, 0x0b, 0x1d, 0x6d, 0x07, 0xd7, 0x9c, 0xb4, 0xa2, 0x22, 0x7d, 0xba, + 0x2f, 0x4b, 0x1f, 0x48, 0x26, 0x76, 0xfc, 0x72, 0x35, 0x28, 0x03, 0x90, 0x85, 0xf3, + 0x3b, 0xd9, 0x6c, 0x19, 0x4d, 0x81, 0xd0, 0x79, 0x73, 0x63, 0x0c, 0xb3, 0x91, 0x55, + 0x46, 0xe3, 0xce, 0xc1, 0xf0, 0x30, 0xee, 0xf8, 0x46, 0x3d, 0x83, 0x5e, 0xfe, 0x9f, + 0x25, 0xee, 0xf3, 0x3b, 0x86, 0x5e, 0xd5, 0x39, 0x90, 0x4a, 0x9c, 0xd8, 0xdf, 0x9b, + 0xa9, 0xcc, 0x0f, 0x10, 0xe8, 0x11, 0x6d, 0x47, 0x7a, 0x45, 0x70, 0x25, 0x66, 0xf0, + 0x67, 0x61, 0x86, 0x9e, 0x39, 0xfa, 0xb9, 0xb7, 0x5b, 0x65, 0x1a, 0x6e, 0x00, 0xef, + 0x59, 0xe8, 0x38, 0xc2, 0x8c, 0x7d, 0xfd, 0x71, 0xe5, 0x2e, 0x57, 0x63, 0x83, 0x9f, + 0xa9, 0x0a, 0xdb, 0x8d, 0xde, 0x11, 0xac, 0x46, 0x83, 0x9a, 0xb1, 0xca, 0x98, 0x8e, + 0xa3, 0x0a, 0x6b, 0xa2, 0x0f, 0x12, 0x46, 0x55, 0x5d, 0xa7, 0x00, 0x45, 0x02, 0xf8, + 0xae, 0x70, 0x67, 0x5e, 0xaf, 0x9c, 0x74, 0x04, 0x01, 0xdc, 0x5c, 0xc0, 0xae, 0xdb, + 0x9b, 0xa0, 0x52, 0x41, 0x51, 0x67, 0xed, 0x7f, 0xee, 0xe8, 0x76, 0xa9, 0x4c, 0x3c, + 0xdf, 0x85, 0x64, 0xbc, 0x05, 0x12, 0x78, 0x37, 0x46, 0x6b, 0x61, 0x6e, 0xad, 0x59, + 0xf5, 0x78, 0x9b, 0x9f, 0x9a, 0x9d, 0xd2, 0xb1, 0x66, 0x0a, 0x9d, 0x9b, 0x62, 0xd2, + 0x20, 0xdb, 0xbe, 0x53, 0xfa, 0xc0, 0xdd, 0x36, 0x74, 0x55, 0xd7, 0xe0, 0x7f, 0xe3, + 0xe8, 0x28, 0x06, 0x3e, 0x6e, 0x9d, 0x10, 0xf9, 0x3d, 0xa1, 0x35, 0x0f, 0xf0, 0x4f, + 0x6e, 0x50, 0xa2, 0xff, 0x39, 0x15, 0xd4, 0xe4, 0x07, 0x45, 0x26, 0xb2, 0x0f, 0xfb, + 0x30, 0x51, 0xa3, 0x64, 0x88, 0x6c, 0xa9, 0x98, 0x48, 0x8b, 0x6d, 0x5f, 0xd6, 0x3f, + 0x3f, 0xbc, 0x97, 0x3d, 0x23, 0x2a, 0xbe, 0x52, 0xa2, 0xf5, 0x4b, 0x0d, 0xfb, 0x8e, + 0x04, 0x07, 0xbf, 0x88, 0x05, 0xbe, 0x28, 0x43, 0xcc, 0x19, 0x7a, 0x67, 0xf1, 0xf7, + 0x9a, 0x4f, 0x9c, 0x64, 0xdd, 0x77, 0xc5, 0x9b, 0x6c, 0x46, 0x0e, 0xfb, 0x06, 0xa1, + 0x71, 0x69, 0x3a, 0x86, 0xf0, 0x50, 0xa2, 0x2c, 0x6d, 0x0c, 0xcd, 0x4e, 0x88, 0x39, + 0x4b, 0x83, 0x5b, 0x64, 0x27, 0x31, 0x74, 0x5c, 0x58, 0x01, 0xd5, 0x45, 0x1b, 0x0e, + 0x5e, 0x1a, 0x99, 0xb8, 0x72, 0x8b, 0x6d, 0x89, 0x99, 0xbe, 0xd1, 0xe1, 0x9e, 0x27, + 0x40, 0xab, 0xc6, 0x4f, 0xc4, 0xdc, 0x71, 0x06, 0xc7, 0x70, 0x0c, 0xc6, 0xbc, 0xe5, + 0xad, 0x93, 0x4a, 0xd9, 0x3c, 0x62, 0xbe, 0x1b, 0x72, 0x14, 0xfb, 0x5f, 0x0e, 0x0d, + 0xe0, 0x3f, 0x61, 0x42, 0x2b, 0xc5, 0x3d, 0x5a, 0x8f, 0x2a, 0x9c, 0x74, 0x57, 0xf4, + 0xd9, 0xff, 0xdb, 0x8b, 0x15, 0xf9, 0x81, 0x69, 0x26, 0xd3, 0xa3, 0x0b, 0x98, 0x91, + 0xf0, 0x69, 0xfa, 0x7d, 0x92, 0xbf, 0x8e, 0xf0, 0xf9, 0x0b, 0x51, 0xef, 0x08, 0x6c, + 0xc6, 0x0f, 0x84, 0x51, 0xbd, 0xc1, 0x93, 0xc2, 0xdb, 0x65, 0xaf, 0x31, 0xa5, 0x48, + 0x18, 0x71, 0xd7, 0x39, 0xe2, 0x0e, 0x92, 0x8f, 0x99, 0xb5, 0x2d, 0x6f, 0x68, 0xbd, + 0x53, 0x6f, 0x1e, 0x97, 0x61, 0x90, 0x30, 0xbd, 0x11, 0x40, 0x36, 0x17, 0x67, 0x2e, + 0xa5, 0x97, 0x50, 0x7e, 0x58, 0x20, 0x1e, 0xca, 0x40, 0x9e, 0x3d, 0x7d, 0x28, 0xef, + 0xbf, 0x18, 0x1a, 0xd8, 0x9f, 0x4e, 0x9b, 0x9d, 0x92, 0x2f, 0xb9, 0xba, 0x63, 0x04, + 0x7f, 0x6c, 0x3c, 0x7c, 0x5f, 0xab, 0xa0, 0xe7, 0x9a, 0x23, 0x10, 0x60, 0x19, 0x30, + 0x2e, 0xae, 0x05, 0x49, 0x0f, 0xd8, 0x28, 0xa6, 0xe7, 0xdc, 0x25, 0x85, 0x59, 0x58, + 0xb2, 0x85, 0x92, 0x1f, 0x91, 0x55, 0x01, 0x60, 0x59, 0x97, 0x85, 0xc4, 0x90, 0xdf, + 0xbf, 0x4d, 0xbd, 0x5f, 0x53, 0xa6, 0x69, 0x84, 0xb8, 0xe3, 0x74, 0xcd, 0x7f, 0x73, + 0x9b, 0x94, 0x0f, 0x0a, 0x8d, 0xfd, 0x9c, 0x95, 0x44, 0x69, 0x9b, 0xab, 0x69, 0x87, + 0x6a, 0x37, 0x7c, 0x76, 0xac, 0x68, 0x52, 0x1f, 0x80, 0xe9, 0xd0, 0xb3, 0xf1, 0x1a, + 0x50, 0x5c, 0xbb, 0x04, 0x8b, 0xc0, 0x6d, 0x06, 0x10, 0x14, 0x27, 0x85, 0x8b, 0xdb, + 0x01, 0x6d, 0x6f, 0x76, 0x44, 0xb5, 0x8e, 0x3b, 0x29, 0xbb, 0xde, 0xe4, 0x6e, 0xa7, + 0xbd, 0x3d, 0x1e, 0x52, 0x72, 0xd1, 0xa5, 0x2c, 0xaa, 0x9c, 0x81, 0x5b, 0x16, 0x4f, + 0xbf, 0xab, 0x81, 0x10, 0xaa, 0xc4, 0x88, 0x46, 0x6a, 0x12, 0x8e, 0xb5, 0x5a, 0x8b, + 0x77, 0x56, 0xe9, 0x15, 0xc6, 0x75, 0x72, 0x7f, 0x5b, 0x96, 0x89, 0x16, 0xa4, 0xb1, + 0x1c, 0x35, 0x3a, 0xe2, 0x78, 0xd2, 0x4c, 0x64, 0x14, 0x19, 0x07, 0x85, 0x4a, 0xf0, + 0x39, 0xcd, 0x8c, 0x67, 0xb3, 0x79, 0x63, 0x96, 0xaf, 0xe0, 0x15, 0x95, 0xb3, 0xf6, + 0xe1, 0xac, 0x1e, 0xbc, 0x14, 0xad, 0xcf, 0xa7, 0x9d, 0x52, 0x1b, 0x75, 0x3a, 0x29, + 0x6f, 0x10, 0x5e, 0xf5, 0xee, 0x97, 0x61, 0xe9, 0x32, 0xd2, 0xf3, 0xb0, 0x32, 0xc3, + 0xd2, 0xaa, 0x06, 0x2f, 0x57, 0xd4, 0x83, 0x93, 0x07, 0x89, 0xf6, 0x1b, 0xeb, 0x9b, + 0x4f, 0x5f, 0x4a, 0xd4, 0x90, 0x38, 0x07, 0xc6, 0x19, 0x28, 0xac, 0x70, 0xb5, 0x74, + 0x9b, 0x40, 0xf8, 0xad, 0x52, 0x2b, 0xf7, 0x3e, 0x26, 0xd8, 0xf5, 0x87, 0x0d, 0x80, + 0x92, 0xa4, 0xe1, 0xff, 0x26, 0x1c, 0x8f, 0xb4, 0xe0, 0x0f, 0x4f, 0x30, 0x15, 0x73, + 0xa7, 0xf1, 0x4e, 0x0d, 0xc8, 0x42, 0x63, 0x4c, 0xb9, 0x6a, 0x83, 0x4b, 0x56, 0x8b, + 0xec, 0x18, 0x14, 0x8c, 0xf0, 0x06, 0x9d, 0xdf, 0xb3, 0x51, 0x24, 0xad, 0x12, 0xd4, + 0xfc, 0xb4, 0x3b, 0x82, 0xc7, 0x0e, 0x45, 0x94, 0x50, 0x9d, 0x97, 0x0e, 0x90, 0xa7, + 0x64, 0x13, 0x04, 0xbe, 0x52, 0x80, 0xfc, 0xbc, 0xce, 0x28, 0x73, 0x5e, 0xbd, 0x87, + 0x7d, 0xee, 0xf8, 0xcb, 0xb0, 0x47, 0x75, 0x79, 0x51, 0xc3, 0x05, 0x8a, 0x9b, 0xc3, + 0xd2, 0xdc, 0xc6, 0x97, 0xe8, 0x9e, 0xca, 0xae, 0xa3, 0xc9, 0x6e, 0x60, 0xc5, 0x8c, + 0x4a, 0x17, 0xd0, 0xdf, 0xb7, 0x29, 0x74, 0x39, 0xf5, 0x51, 0x3a, 0xbd, 0xa7, 0x01, + 0x00, 0x60, 0xff, 0x1d, 0x0c, 0x7c, 0x7f, 0xe4, 0x15, 0x4c, 0x3c, 0x99, 0x67, 0xc0, + 0xaf, 0x55, 0x1d, 0x53, 0xa4, 0x61, 0x9a, 0x6c, 0xa0, 0x8d, 0xe9, 0xec, 0x5b, 0xcf, + 0x86, 0x9e, 0x5d, 0x54, 0x1c, 0x3d, 0xa0, 0xa5, 0x60, 0xeb, 0x2c, 0x35, 0xbc, 0x23, + 0x9c, 0x60, 0x70, 0x7b, 0xec, 0x60, 0xea, 0xde, 0x92, 0x62, 0x4b, 0x97, 0x1d, 0xdb, + 0x26, 0x5a, 0xc1, 0x40, 0x47, 0x06, 0x07, 0xce, 0xaa, 0xd7, 0x9a, 0xd6, 0x80, 0xf6, + 0x07, 0xc2, 0xf7, 0xc1, 0x79, 0x1e, 0xe0, 0xec, 0x82, 0xa0, 0x76, 0x3e, 0x4e, 0x5d, + 0x22, 0x0d, 0x3d, 0xa2, 0x11, 0x3c, 0x98, 0xff, 0xc9, 0x21, 0x06, 0x13, 0x25, 0x82, + 0xae, 0xa8, 0x41, 0x6e, 0xf7, 0xaa, 0x58, 0xbd, 0x95, 0x59, 0x10, 0x30, 0x8a, 0x9a, + 0x2a, 0xb7, 0xa9, 0xf8, 0x6e, 0x66, 0x69, 0x1e, 0xf6, 0x69, 0x79, 0xae, 0xcb, 0xa3, + 0x78, 0xff, 0x55, 0x80, 0x57, 0x84, 0x40, 0x55, 0x35, 0xbc, 0xf6, 0x03, 0x4a, 0xf6, + 0x09, 0x45, 0x1f, 0x59, 0x39, 0x46, 0x9d, 0xb2, 0xd9, 0x8e, 0x0d, 0xc1, 0x90, 0x60, + 0x49, 0xcd, 0x80, 0xa0, 0x60, 0x34, 0x35, 0x6c, 0x15, 0x2d, 0xcb, 0x16, 0xa6, 0xda, + 0xa9, 0xe7, 0xa5, 0x37, 0xd9, 0xe4, 0xa6, 0x6b, 0x97, 0x05, 0xf7, 0xed, 0x2b, 0xc0, + 0xec, 0xa6, 0xdf, 0x37, 0x6b, 0xa7, 0x72, 0x77, 0x10, 0xa9, 0x4c, 0xf4, 0x6e, 0x19, + 0x42, 0xd5, 0xff, 0xff, 0x5c, 0xee, 0x61, 0xd4, 0xa5, 0x3a, 0x4a, 0x1b, 0x4d, 0x88, + 0x61, 0xf0, 0x6c, 0x67, 0x20, 0x6d, 0x42, 0xed, 0x38, 0xa7, 0xc9, 0x0c, 0xae, 0x6f, + 0x84, 0x6c, 0x82, 0x6a, 0xee, 0x4a, 0x72, 0xbf, 0x91, 0xc8, 0xc9, 0xca, 0x84, 0x17, + 0x51, 0x88, 0xac, 0x7a, 0xf5, 0x6d, 0x2a, 0x22, 0xd2, 0xcd, 0x98, 0x2a, 0x1b, 0x10, + 0xc3, 0xff, 0x43, 0x69, 0xea, 0xd3, 0x0a, 0x17, 0x01, 0x08, 0x53, 0x3f, 0xe3, 0x00, + 0x9e, 0x6a, 0xed, 0xb7, 0x6a, 0x48, 0x7d, 0xb7, 0x8b, 0xd2, 0x7b, 0xbc, 0x43, 0xb1, + 0x8d, 0xaf, 0xa1, 0x55, 0xed, 0x54, 0x93, 0xa0, 0x25, 0xdb, 0x7b, 0xbf, 0x8e, 0xea, + 0x48, 0xc0, 0xdf, 0xed, 0xec, 0xaf, 0x19, 0x01, 0xae, 0x11, 0x96, 0xdf, 0x7b, 0xf9, + 0x1d, 0x5b, 0x7d, 0xff, 0x95, 0xf6, 0x6d, 0x11, 0x22, 0xfc, 0x69, 0xf6, 0xf8, 0xd5, + 0x31, 0x6c, 0xca, 0xf6, 0xab, 0xa6, 0x09, 0x86, 0x35, 0xa1, 0x25, 0xe3, 0x34, 0x8f, + 0x69, 0x79, 0x3e, 0x11, 0x1c, 0xe6, 0xbe, 0xcd, 0xb6, 0x68, 0xee, 0x50, 0x52, 0x40, + 0xd0, 0x16, 0x03, 0x7c, 0x97, 0x79, 0x92, 0xbc, 0x9b, 0x0d, 0x06, 0x08, 0xba, 0x99, + 0xb7, 0xf5, 0x9e, 0x50, 0x81, 0xc3, 0x43, 0x10, 0xd4, 0x79, 0x52, 0x69, 0xfd, 0x32, + 0x49, 0x8a, 0x35, 0xc0, 0x4f, 0x15, 0x8d, 0x69, 0xdb, 0xb3, 0x1d, 0x95, 0xb2, 0xff, + 0x29, 0x38, 0xaa, 0x73, 0x39, 0x0c, 0xda, 0x04, 0x90, 0x58, 0xe8, 0xfe, 0xf9, 0x25, + 0x7c, 0x79, 0x46, 0xd9, 0xaa, 0x99, 0x8a, 0x3e, 0x0e, 0x33, 0x1c, 0xc1, 0x3e, 0x2f, + 0x2b, 0x36, 0x88, 0xfd, 0x42, 0xdc, 0xe9, 0x56, 0x19, 0xb6, 0x92, 0x43, 0x12, 0xa1, + 0xcd, 0x31, 0x79, 0xa6, 0x7d, 0x70, 0x34, 0xd6, 0x7b, 0x87, 0xdd, 0x60, 0xc0, 0x62, + 0x02, 0xbd, 0xb9, 0x87, 0x55, 0x02, 0x9e, 0x71, 0x66, 0x63, 0x6c, 0x2b, 0x64, 0x0f, + 0xda, 0x80, 0x8d, 0xe2, 0xf7, 0x19, 0x15, 0x77, 0xe4, 0x65, 0x8b, 0xa2, 0xa8, 0xfd, + 0x25, 0xca, 0xdc, 0xf8, 0xde, 0x8d, 0xb2, 0x66, 0xbd, 0xef, 0x42, 0x8e, 0x59, 0x88, + 0x71, 0xcc, 0x74, 0x43, 0x92, 0xeb, 0xb5, 0x98, 0x84, 0x87, 0x80, 0x29, 0x91, 0xb0, + 0xbd, 0x6a, 0xd5, 0x13, 0x6d, 0x4b, 0x4c, 0xaf, 0x9a, 0xef, 0x81, 0x2e, 0xf7, 0x67, + 0x7a, 0x75, 0x65, 0x0e, 0xc1, 0x12, 0xb0, 0xbb, 0xf7, 0x35, 0xa3, 0x09, 0x67, 0x26, + 0x56, 0xcb, 0x6e, 0x92, 0x61, 0xcc, 0x98, 0x25, 0x51, 0xad, 0x7e, 0x04, 0x83, 0x70, + 0x7c, 0xdf, 0x28, 0x62, 0x9c, 0x6a, 0xd0, 0x9e, 0xee, 0x5e, 0x90, 0x5c, 0x13, 0x01, + 0xa1, 0xdf, 0x60, 0x08, 0x4b, 0xe7, 0x05, 0x6d, 0xa1, 0xf1, 0x31, 0xe0, 0x85, 0x15, + 0x69, 0x81, 0x6b, 0xa7, 0x44, 0xec, 0x90, 0xf1, 0x0e, 0x4f, 0x79, 0xb5, 0x5c, 0x54, + 0x5f, 0xda, 0x70, 0xfa, 0x25, 0x1c, 0x48, 0x0d, 0x22, 0xe8, 0x7b, 0x5a, 0xbe, 0x4b, + 0xda, 0x91, 0x6e, 0x19, 0xc2, 0xcc, 0xcd, 0x0e, 0x33, 0xee, 0x70, 0xa0, 0xaa, 0x09, + 0x48, 0x14, 0x3c, 0x1a, 0x6f, 0x98, 0xab, 0xe2, 0x86, 0x71, 0xd6, 0x4e, 0xbd, 0x15, + 0xa6, 0xee, 0xf4, 0xcc, 0x81, 0xaa, 0x85, 0x7c, 0xa3, 0x0e, 0xda, 0xdb, 0xa7, 0x7f, + 0x0d, 0xca, 0x93, 0xce, 0x7e, 0x41, 0xf5, 0x2c, 0x51, 0x01, 0xc6, 0x07, 0x47, 0x16, + 0xc2, 0xc3, 0x38, 0x80, 0x82, 0x65, 0x75, 0x20, 0xcb, 0xb3, 0x7e, 0x09, 0x8a, 0x86, + 0x6d, 0x19, 0x75, 0x2a, 0x81, 0x2d, 0xa8, 0x0f, 0xc5, 0x5d, 0xec, 0xf9, 0x78, 0x38, + 0x9d, 0x57, 0x43, 0x54, 0x4e, 0x9f, 0xd9, 0xb7, 0x1f, 0x2b, 0x6f, 0x8b, 0x97, 0x32, + 0xce, 0xe3, 0x20, 0x5e, 0x26, 0x4f, 0x16, 0xe1, 0x29, 0x03, 0x3d, 0xf0, 0x7c, 0x99, + 0xd2, 0x9c, 0x9d, 0xb0, 0x62, 0x56, 0xd1, 0x32, 0x74, 0x8a, 0x44, 0x07, 0xd5, 0xf9, + 0x9b, 0x17, 0x15, 0x1c, 0x23, 0xb8, 0x1f, 0x4d, 0xce, 0xa3, 0x02, 0xe3, 0xda, 0x73, + 0x40, 0xc5, 0xab, 0xa6, 0x77, 0xd4, 0x2f, 0x76, 0x4e, 0xd9, 0x3b, 0x92, 0x36, 0x07, + 0xd2, 0xeb, 0xd6, 0xdd, 0xb0, 0xcf, 0x65, 0xaa, 0x29, 0x86, 0x14, 0xbc, 0x7e, 0x6d, + 0xb5, 0x99, 0xde, 0x06, 0x33, 0x96, 0xe1, 0xec, 0x7d, 0xeb, 0x2d, 0x34, 0xd9, 0x43, + 0x90, 0xf8, 0x72, 0xf5, 0x49, 0xaa, 0x38, 0x01, 0xb9, 0x58, 0x99, 0x17, 0x2e, 0x83, + 0xbc, 0x29, 0x23, 0x39, 0x7e, 0xac, 0x54, 0xd6, 0x7e, 0x7a, 0xc9, 0x45, 0x3f, 0x42, + 0x26, 0xb5, 0x3b, 0x04, 0x8d, 0x38, 0x2d, 0xf0, 0x24, 0x1c, 0xcf, 0x33, 0xd9, 0x25, + 0xad, 0xfe, 0x21, 0xb2, 0x81, 0x5b, 0x3c, 0xde, 0x36, 0xac, 0x0a, 0xe3, 0xec, 0x48, + 0xc0, 0xd1, 0x3f, 0x10, 0xdb, 0x32, 0x71, 0x2d, 0x39, 0x23, 0x31, 0x9e, 0x2d, 0x89, + 0x9b, 0xdb, 0xe6, 0xcb, 0xc8, 0xa4, 0x66, 0xe2, 0x0f, 0x34, 0x70, 0x21, 0xb6, 0x09, + 0xe7, 0x95, 0x58, 0xa4, 0xcd, 0x38, 0x17, 0xf9, 0x2f, 0xf0, 0x23, 0x8b, 0xe3, 0xc7, + 0x8b, 0x3f, 0xce, 0x58, 0x5c, 0x6f, 0xc0, 0x10, 0x52, 0xca, 0xb9, 0x08, 0x10, 0x56, + 0x54, 0xad, 0x4f, 0x95, 0xf4, 0x26, 0xf7, 0xb4, 0xf1, 0x20, 0xed, 0x0a, 0x09, 0xdc, + 0x3e, 0x8a, 0x46, 0x4e, 0x33, 0xef, 0x33, 0xb5, 0x25, 0xeb, 0xe4, 0xd7, 0x0f, 0xbd, + 0xf3, 0x40, 0xdf, 0x3d, 0x4f, 0x76, 0x7c, 0x1b, 0x9b, 0xc0, 0x6d, 0x36, 0xe7, 0x0d, + 0x88, 0xf5, 0x33, 0xdc, 0x94, 0xd9, 0x7b, 0xde, 0x79, 0x96, 0xab, 0x9a, 0x2c, 0xa7, + 0x18, 0xf8, 0xe2, 0x28, 0xb8, 0xd6, 0x92, 0xdb, 0x45, 0xff, 0xa6, 0xa9, 0x8f, 0x96, + 0x25, 0x99, 0x7a, 0x46, 0xf7, 0xb0, 0x91, 0x36, 0x05, 0xce, 0xad, 0xd9, 0x00, 0x80, + 0x0e, 0xa4, 0xe1, 0xe5, 0x67, 0x61, 0x78, 0xae, 0x9a, 0x35, 0xc7, 0x57, 0x02, 0xfc, + 0xd2, 0x7d, 0x04, 0xe7, 0xe8, 0x25, 0xa6, 0x7c, 0x5b, 0x6b, 0x73, 0x86, 0x3b, 0xa5, + 0xa1, 0x01, 0x0b, 0x66, 0x4c, 0x61, 0xa3, 0x22, 0x55, 0xca, 0xab, 0xa5, 0xf8, 0xbf, + 0x68, 0x72, 0x73, 0xe8, 0xda, 0xc7, 0x16, 0x92, 0x06, 0x5c, 0xae, 0x57, 0xba, 0xb4, + 0xdd, 0x15, 0xd6, 0x81, 0x8a, 0xe0, 0x29, 0xe6, 0x56, 0xa0, 0xf6, 0xba, 0xd2, 0xee, + 0x07, 0xbf, 0x55, 0x86, 0xc3, 0xbe, 0x8e, 0xed, 0x9f, 0x3c, 0xc6, 0x1d, 0xad, 0x55, + 0x18, 0x04, 0x23, 0xe3, 0x42, 0xa0, 0x9c, 0xcd, 0x82, 0xc8, 0x6b, 0x6c, 0xe1, 0x5f, + 0x58, 0x61, 0xd2, 0x41, 0x8d, 0x37, 0x51, 0x0a, 0xd7, 0xb5, 0xbc, 0xe1, 0x9e, 0xcf, + 0x78, 0x97, 0x8a, 0x88, 0xd6, 0x5d, 0x4f, 0xf9, 0x12, 0xe9, 0xcf, 0xa1, 0x7e, 0x38, + 0x6f, 0x2c, 0x52, 0x0a, 0x61, 0x4a, 0xb7, 0x7c, 0xd7, 0x84, 0x0b, 0xa5, 0x2e, 0x15, + 0x77, 0xe6, 0x3b, 0xfc, 0x1b, 0xba, 0x78, 0x21, 0x3e, 0x8d, 0x34, 0xd7, 0x84, 0x28, + 0x5d, 0x3e, 0x0e, 0x2f, 0x4f, 0x04, 0x6a, 0x6c, 0xc6, 0x23, 0x79, 0x98, 0xc8, 0x9f, + 0x2a, 0xe9, 0x51, 0x80, 0x3b, 0x02, 0xf2, 0xee, 0x6f, 0x53, 0x50, 0x01, 0x11, 0x01, + 0x54, 0xc2, 0x31, 0x0c, 0x44, 0xd9, 0xa4, 0xc9, 0xe5, 0xda, 0xe6, 0xbc, 0xe6, 0xe0, + 0xb3, 0x48, 0xeb, 0xbb, 0x78, 0x55, 0x7b, 0xa2, 0x4e, 0x7d, 0xaf, 0x80, 0x53, 0x75, + 0xe0, 0xfa, 0xfe, 0x37, 0xb7, 0x2f, 0xeb, 0xab, 0x8d, 0x0b, 0x4a, 0x79, 0xee, 0x09, + 0x73, 0xd4, 0xf8, 0x79, 0x49, 0x93, 0x62, 0x34, 0x04, 0x00, 0x09, 0x9e, 0x3a, 0x02, + 0x18, 0x72, 0x2a, 0x1e, 0x5a, 0xb8, 0x37, 0xbf, 0xe2, 0xfe, 0xc0, 0x24, 0xd0, 0x64, + 0x8f, 0xb7, 0x2f, 0x31, 0x54, 0x44, 0x62, 0xf3, 0xaa, 0xab, 0x65, 0x07, 0x69, 0x91, + 0x50, 0xc4, 0x75, 0x75, 0x6f, 0x59, 0x88, 0xbd, 0x09, 0xd0, 0xc5, 0x8c, 0x0f, 0xf3, + 0x0a, 0xd6, 0xc8, 0x52, 0x7a, 0x31, 0xff, 0x1c, 0xdf, 0x49, 0x35, 0x89, 0x72, 0xb4, + 0x72, 0x1a, 0x94, 0x9a, 0xe8, 0xcc, 0xfd, 0xf2, 0x34, 0x2a, 0x88, 0xf8, 0xed, 0x81, + 0xd1, 0xa0, 0xd2, 0xbe, 0xe7, 0x6a, 0x74, 0x33, 0x46, 0x5d, 0x98, 0x8c, 0xeb, 0xda, + 0x56, 0x27, 0x4d, 0xee, 0x65, 0x80, 0x68, 0x14, 0x2f, 0x63, 0xff, 0xd9, 0x18, 0x7a, + 0x00, 0x83, 0xf7, 0xc4, 0xa3, 0x74, 0x5b, 0x09, 0xa1, 0x23, 0x5f, 0x3e, 0xc7, 0xf1, + 0x9e, 0x98, 0xb8, 0x70, 0xe7, 0x82, 0xd6, 0xb0, 0x69, 0xe0, 0x88, 0x44, 0x78, 0xe7, + 0x72, 0x28, 0x9c, 0x1b, 0x67, 0xa3, 0xe8, 0x55, 0xbe, 0xa6, 0x16, 0x0d, 0x25, 0x04, + 0x1a, 0xdc, 0x57, 0x1c, 0x5b, 0xc3, 0xbb, 0x9e, 0x64, 0xbe, 0x50, 0x6e, 0xeb, 0x8c, + 0x60, 0x7c, 0x2a, 0x68, 0x93, 0x64, 0xac, 0xd1, 0xca, 0xc4, 0xf7, 0x2e, 0x7e, 0x76, + 0x0d, 0xba, 0x39, 0x1b, 0x25, 0x6c, 0x5c, 0x16, 0xb7, 0x06, 0xa5, 0x80, 0x16, 0xa3, + 0x1f, 0x08, 0xaf, 0x16, 0x6d, 0x2d, 0x06, 0x26, 0x45, 0x09, 0x1c, 0xcb, 0x7e, 0xa3, + 0xf6, 0x12, 0x5d, 0x03, 0x77, 0x6c, 0x42, 0x77, 0xf0, 0xe1, 0x7b, 0x09, 0x07, 0x2e, + 0xf0, 0x2a, 0x03, 0xa1, 0xff, 0x02, 0x30, 0xbf, 0xf5, 0x76, 0x4b, 0x18, 0xef, 0x50, + 0x1f, 0x44, 0x08, 0x96, 0x4c, 0xec, 0xaf, 0x73, 0x18, 0xd4, 0xa8, 0x98, 0x3b, 0xed, + 0x6f, 0xdb, 0xdd, 0xdc, 0x66, 0xdb, 0x3b, 0xaa, 0xe6, 0xaa, 0xc5, 0x32, 0x1c, 0x19, + 0xc3, 0x2b, 0x08, 0x35, 0xe8, 0xc1, 0xd8, 0xe5, 0x21, 0x92, 0xe0, 0x09, 0x6f, 0x6d, + 0x57, 0x11, 0xe0, 0x55, 0x4f, 0x38, 0x09, 0xb5, 0x7f, 0x1c, 0x02, 0x47, 0x9e, 0xdd, + 0xe1, 0x22, 0x68, 0xd7, 0x39, 0x13, 0xbf, 0x75, 0x19, 0x53, 0x90, 0x7e, 0xb0, 0x42, + 0xb1, 0x4b, 0xad, 0x03, 0x00, 0xa3, 0x46, 0x01, 0x5d, 0x07, 0x1a, 0x81, 0x57, 0x54, + 0x6f, 0x10, 0x81, 0xc8, 0xef, 0x54, 0x94, 0x9b, 0x4a, 0xfb, 0x0b, 0x83, 0x2a, 0x0e, + 0x67, 0x54, 0x19, 0x3e, 0x00, 0x91, 0x67, 0xd3, 0xc5, 0xf1, 0x91, 0x9f, 0x46, 0x3c, + 0x85, 0xe8, 0x8b, 0x89, 0x75, 0xca, 0x0c, 0x83, 0x41, 0xeb, 0x99, 0x8c, 0x20, 0xe2, + 0xec, 0x85, 0x98, 0x89, 0x09, 0x8b, 0xdb, 0xcc, 0xf7, 0x18, 0x95, 0xb4, 0x68, 0xb2, + 0x6f, 0xdf, 0x69, 0xcc, 0x1f, 0x81, 0xeb, 0x1c, 0xd4, 0x18, 0xae, 0xbe, 0x50, 0x4f, + 0x37, 0x6c, 0xcc, 0xf9, 0xc9, 0x59, 0x65, 0xe8, 0x25, 0xf0, 0x18, 0x6b, 0x23, 0xfa, + 0x06, 0x73, 0x2b, 0xe4, 0xf7, 0x52, 0x27, 0xeb, 0x69, 0x57, 0x2a, 0x52, 0x93, 0x15, + 0x40, 0xa1, 0x3a, 0x76, 0x9f, 0x9b, 0xbd, 0x1a, 0xb1, 0x2b, 0x55, 0xea, 0x68, 0x90, + 0x5d, 0x55, 0x0f, 0xc2, 0xd2, 0xb1, 0x26, 0x98, 0x6d, 0x34, 0xcc, 0x06, 0xc9, 0x3a, + 0x77, 0x2d, 0xc4, 0xa7, 0xb9, 0x30, 0x84, 0x64, 0x5b, 0x1f, 0x97, 0xb3, 0x4a, 0xa4, + 0xcd, 0x4f, 0xad, 0x91, 0x69, 0xbf, 0xe0, 0xbf, 0xd0, 0x60, 0x42, 0xf9, 0x13, 0xa6, + 0xc0, 0xf7, 0x11, 0xa7, 0x7a, 0xa5, 0x6c, 0xa3, 0x29, 0x65, 0x70, 0x1b, 0x00, 0xe4, + 0x12, 0x21, 0x97, 0x9f, 0xec, 0xe9, 0xe7, 0x5c, 0x0f, 0xf9, 0x04, 0x89, 0xcf, 0x2e, + 0xd4, 0x54, 0x6a, 0x98, 0xd6, 0x63, 0x87, 0xcf, 0x62, 0x42, 0xeb, 0xbe, 0xd4, 0xe6, + 0x58, 0x2d, 0xa1, 0xce, 0xa4, 0x1c, 0x97, 0xd7, 0x68, 0x48, 0xef, 0xc0, 0xb2, 0x03, + 0xa5, 0xcd, 0x0a, 0x8a, 0xf7, 0x4d, 0x41, 0x4d, 0xde, 0x76, 0x4c, 0x26, 0x9e, 0xfa, + 0x9f, 0xa9, 0x97, 0xc3, 0x96, 0x15, 0xc4, 0x00, 0xe9, 0x24, 0x17, 0xf6, 0x9a, 0x23, + 0xa8, 0xcb, 0x3e, 0x17, 0x5d, 0x13, 0x08, 0xe9, 0xf3, 0x7b, 0x23, 0x62, 0xdf, 0x1a, + 0xd7, 0x1a, 0xb7, 0x7e, 0x87, 0x3c, 0xbf, 0xea, 0xdd, 0xee, 0x3b, 0x0d, 0xa4, 0x74, + 0x47, 0xff, 0xae, 0x4a, 0x18, 0x8b, 0x0a, 0xb1, 0x74, 0x7d, 0xb3, 0x3d, 0x7e, 0xe5, + 0x6d, 0x68, 0x82, 0xf4, 0x21, 0x2c, 0x0b, 0xa0, 0xf7, 0x3a, 0xfd, 0xb1, 0x9f, 0x4c, + 0xac, 0x28, 0x74, 0xbe, 0x4e, 0xcb, 0x84, 0xcd, 0xef, 0x29, 0x9e, 0xb5, 0x40, 0x0c, + 0xf1, 0x64, 0xb8, 0x3d, 0x44, 0x15, 0xc1, 0xff, 0x34, 0x66, 0x92, 0x90, 0xd9, 0x3d, + 0x82, 0xcb, 0xde, 0x07, 0xdb, 0xb5, 0x55, 0x55, 0x16, 0x62, 0x67, 0xdc, 0x39, 0xdc, + 0xd8, 0xea, 0xf9, 0xe8, 0xea, 0x97, 0x56, 0x07, 0xf7, 0x36, 0x8f, 0x5e, 0xee, 0x99, + 0x96, 0x75, 0x26, 0x91, 0xbc, 0x82, 0x7c, 0xca, 0x7e, 0x32, 0x5c, 0x69, 0x86, 0x34, + 0x7a, 0x7d, 0x1d, 0x81, 0x8f, 0x9b, 0xe0, 0x09, 0xa1, 0x8d, 0xec, 0x32, 0xfa, 0x18, + 0x33, 0x91, 0x3c, 0x27, 0x02, 0x2e, 0x6e, 0xbb, 0xf5, 0x14, 0x8e, 0x70, 0xc2, 0x89, + 0xf0, 0x94, 0x1b, 0x45, 0xc0, 0xa7, 0x74, 0x53, 0x74, 0x31, 0x3d, 0x3b, 0x95, 0x71, + 0x02, 0xc6, 0x70, 0x0b, 0x1e, 0xd8, 0x71, 0x75, 0xba, 0xbe, 0x0f, 0x2a, 0xff, 0xdb, + 0xa2, 0xa4, 0x69, 0x34, 0xbf, 0x1a, 0xa6, 0x32, 0x40, 0x27, 0x0a, 0x47, 0x57, 0xfb, + 0x95, 0xa5, 0x0d, 0x63, 0x8e, 0x4b, 0xc5, 0x43, 0x45, 0x13, 0xd4, 0xc9, 0x8d, 0x97, + 0x04, 0xe9, 0x00, 0xe2, 0xec, 0xa9, 0x97, 0xd1, 0x6c, 0x2f, 0xf4, 0x5f, 0x19, 0x24, + 0x08, 0xe2, 0x26, 0x35, 0x4a, 0xac, 0xcb, 0x7a, 0xb4, 0x37, 0xb1, 0xbc, 0xa4, 0x5b, + 0x2b, 0x91, 0xeb, 0x8d, 0x8c, 0x5f, 0x68, 0xcf, 0x2f, 0x24, 0xd7, 0x18, 0xea, 0x5e, + 0x51, 0xa3, 0x1f, 0xa2, 0xfd, 0x11, 0x4d, 0xf8, 0x8a, 0x06, 0x55, 0xdb, 0x98, 0xd6, + 0x0b, 0x57, 0xbf, 0xc8, 0x3b, 0xb4, 0xbd, 0x05, 0x80, 0xe4, 0xc5, 0x49, 0x20, 0x2c, + 0x8a, 0xf4, 0xe9, 0xfa, 0x72, 0x4f, 0x73, 0xa9, 0x63, 0x32, 0x3f, 0xf0, 0x13, 0x8a, + 0x7a, 0x92, 0x08, 0x97, 0xe9, 0x80, 0x2b, 0x9d, 0x37, 0x21, 0x88, 0x9e, 0xda, 0xe0, + 0xe1, 0x2b, 0xf7, 0x5d, 0xe7, 0x1b, 0x84, 0xf1, 0x75, 0xc1, 0xf2, 0xac, 0xb5, 0xac, + 0x37, 0x07, 0x2a, 0x00, 0xbf, 0x56, 0xa6, 0xb3, 0x59, 0x22, 0x90, 0x96, 0x49, 0xf8, + 0x1c, 0x31, 0x31, 0x71, 0x15, 0xad, 0xca, 0xbc, 0x60, 0xf0, 0x26, 0x06, 0x2c, 0x61, + 0xea, 0x41, 0xb8, 0x98, 0x01, 0xd0, 0x75, 0x35, 0x36, 0x2a, 0xdb, 0x56, 0x5a, 0x96, + 0x08, 0xf6, 0xe2, 0x82, 0xcc, 0xe4, 0xae, 0x3b, 0xa9, 0xfa, 0x5d, 0x45, 0xae, 0xa1, + 0xae, 0xac, 0x8a, 0xf5, 0x40, 0xf1, 0x4a, 0x38, 0x65, 0x2b, 0x28, 0xbd, 0x61, 0xf8, + 0x9f, 0x1d, 0x33, 0xf1, 0x12, 0xcc, 0x05, 0x35, 0xb2, 0xc6, 0x71, 0x75, 0x8f, 0x29, + 0x99, 0xe0, 0x6f, 0x72, 0x79, 0x42, 0x74, 0xa4, 0xc3, 0x0c, 0x9f, 0x2b, 0x87, 0xc7, + 0x6f, 0xbf, 0x3d, 0x6e, 0x6a, 0xd5, 0xcf, 0xaa, 0xb3, 0x76, 0x53, 0x9f, 0x82, 0x91, + 0x00, 0xe8, 0x6c, 0x76, 0x81, 0x7e, 0x35, 0x9d, 0xd6, 0xfe, 0x18, 0xf7, 0x4e, 0xce, + 0x4a, 0x08, 0x98, 0x9a, 0x5d, 0x1c, 0xc3, 0xfa, 0x19, 0xc2, 0x85, 0x30, 0x63, 0x12, + 0x61, 0x50, 0x59, 0xe6, 0xf2, 0xaa, 0x9e, 0xb1, 0x1f, 0x16, 0xad, 0xa3, 0xd7, 0x3b, + 0xe2, 0x68, 0xfe, 0x6d, 0x14, 0x4a, 0x53, 0xe2, 0x47, 0x79, 0x93, 0x10, 0x9c, 0x6c, + 0x4a, 0x35, 0x2a, 0x57, 0xf2, 0x45, 0xa0, 0x04, 0xd5, 0x7e, 0x0c, 0x4c, 0x73, 0x55, + 0x50, 0x1e, 0x88, 0x47, 0xc5, 0x49, 0xad, 0x3d, 0xed, 0xda, 0xb4, 0xfa, 0x3a, 0x94, + 0x6b, 0x4f, 0xd2, 0x81, 0xd6, 0x3e, 0x9a, 0x06, 0x09, 0x28, 0x97, 0x55, 0x86, 0x6a, + 0xaf, 0xf9, 0xa3, 0xdf, 0x4a, 0x06, 0xf9, 0x1a, 0x9a, 0x68, 0x94, 0xa9, 0x6e, 0x21, + 0x3b, 0xc2, 0x5d, 0xfd, 0x2d, 0xb6, 0xa2, 0x93, 0x6e, 0x0a, 0x72, 0x46, 0xfe, 0xfe, + 0xe9, 0x4e, 0x57, 0x53, 0x41, 0x34, 0x62, 0xd5, 0x6e, 0x85, 0xb0, 0x21, 0x04, 0x13, + 0x25, 0xb3, 0xd8, 0x19, 0xa6, 0x21, 0xbc, 0x83, 0x30, 0x26, 0x7c, 0x25, 0xc0, 0x73, + 0x34, 0x30, 0x9e, 0xdb, 0xc0, 0x7b, 0x4e, 0xab, 0xc8, 0x0e, 0x29, 0x16, 0xd2, 0xaf, + 0x90, 0x0e, 0xb4, 0x9e, 0xe1, 0xd1, 0xe4, 0x18, 0x8a, 0x68, 0x3f, 0x78, 0xcc, 0xa6, + 0x4a, 0xb9, 0x8c, 0x18, 0x2c, 0xe3, 0x55, 0xa4, 0x49, 0x09, 0x55, 0xf3, 0x4d, 0x8a, + 0x7b, 0xed, 0x97, 0x58, 0x4e, 0xe4, 0xe1, 0x97, 0x55, 0x53, 0x3d, 0xf5, 0x86, 0xde, + 0x23, 0x17, 0x79, 0x0c, 0x68, 0x53, 0xe7, 0x4e, 0xca, 0xca, 0x1d, 0x3c, 0x94, 0x8b, + 0x3a, 0x93, 0x78, 0xf0, 0xf8, 0x91, 0xc8, 0x40, 0x8a, 0x3b, 0xd8, 0x2a, 0x2c, 0x64, + 0xf8, 0x11, 0x46, 0xec, 0x67, 0x3d, 0x09, 0x49, 0x1d, 0xbe, 0xce, 0x3b, 0xfd, 0xd8, + 0x24, 0x93, 0xfd, 0x16, 0x22, 0xa4, 0x5a, 0x54, 0x30, 0xf6, 0x99, 0x02, 0x85, 0x03, + 0xd8, 0xfd, 0x03, 0x4c, 0x48, 0x1a, 0x31, 0x74, 0xe5, 0x3d, 0xf0, 0x09, 0x80, 0x00, + 0x84, 0x90, 0x4b, 0x3c, 0x76, 0x0b, 0xf6, 0x87, 0x79, 0x19, 0xc4, 0x38, 0x1e, 0x12, + 0x20, 0x0a, 0x83, 0xc4, 0x8c, 0xb4, 0x66, 0x51, 0xba, 0x52, 0x86, 0xac, 0x84, 0xa8, + 0x0a, 0x77, 0xb3, 0x1b, 0x1c, 0x82, 0x67, 0xc1, 0x9f, 0x76, 0xa8, 0x3c, 0x13, 0xe8, + 0x35, 0xe1, 0x37, 0x19, 0x52, 0x79, 0xf8, 0x6e, 0xed, 0xeb, 0x8e, 0xca, 0x64, 0xd9, + 0xcb, 0x8b, 0xf1, 0x3d, 0x7e, 0x0e, 0x4a, 0xc6, 0x8c, 0xf8, 0x1b, 0xae, 0x97, 0x48, + 0x46, 0x93, 0x2d, 0xb6, 0x88, 0xa2, 0x34, 0x1e, 0xba, 0x45, 0xf7, 0x94, 0x92, 0x9c, + 0x9f, 0x2e, 0x29, 0x5b, 0x5e, 0x2e, 0x31, 0x0b, 0x7f, 0x25, 0x90, 0x06, 0xbd, 0xdb, + 0x88, 0x06, 0xcc, 0x18, 0x6a, 0x35, 0x23, 0x6c, 0xf1, 0x14, 0x19, 0xd6, 0xdd, 0x0d, + 0xb6, 0x0b, 0x4f, 0x43, 0x8a, 0xc7, 0x0b, 0x92, 0x5b, 0xdf, 0x89, 0xa7, 0x76, 0xbe, + 0x8e, 0xfc, 0xb2, 0x90, 0xc1, 0x79, 0x16, 0xb4, 0xad, 0x64, 0x9b, 0xd5, 0xdf, 0x1c, + 0xb9, 0x68, 0x2a, 0x03, 0xff, 0x88, 0xbf, 0x03, 0xad, 0x57, 0xd2, 0x06, 0xe3, 0x12, + 0x7d, 0xa4, 0x70, 0x6a, 0x4f, 0x1b, 0xde, 0xc1, 0x48, 0x3f, 0x8f, 0x35, 0x55, 0x87, + 0x99, 0xe2, 0xbf, 0x75, 0xc8, 0xd2, 0xd5, 0x9b, 0x3c, 0xce, 0xc1, 0x0c, 0x88, 0xf3, + 0xb0, 0x21, 0xc9, 0xe8, 0x74, 0x93, 0xe1, 0x2b, 0x43, 0xa5, 0x3f, 0xf9, 0xe6, 0xc3, + 0x8a, 0x09, 0x4c, 0xb2, 0x8a, 0x84, 0xd9, 0xd5, 0x9d, 0xd9, 0xdf, 0x46, 0x1f, 0x59, + 0x47, 0x0a, 0xe3, 0xd5, 0x4c, 0x52, 0x58, 0xeb, 0x0c, 0xe3, 0x49, 0x86, 0x9d, 0xef, + 0xb3, 0xc0, 0xce, 0xe5, 0xb6, 0x38, 0x06, 0x2e, 0x6a, 0xb5, 0x41, 0x76, 0xc9, 0x27, + 0x95, 0xf3, 0x7b, 0x51, 0x9d, 0x93, 0x5d, 0x47, 0x70, 0x26, 0x9e, 0x87, 0x87, 0x59, + 0x4a, 0xd4, 0xba, 0xf3, 0x11, 0xf5, 0x1b, 0xed, 0x1a, 0x23, 0xa5, 0x7a, 0xd5, 0xd6, + 0xeb, 0x19, 0x2e, 0x41, 0x0b, 0xb2, 0xb5, 0x90, 0x43, 0x56, 0xa2, 0xac, 0xde, 0x05, + 0xd6, 0x3a, 0xa1, 0xbf, 0x4a, 0x67, 0xe0, 0xd2, 0x59, 0xc8, 0xc4, 0x10, 0xa7, 0xdb, + 0xac, 0xfe, 0xb6, 0xc2, 0x70, 0x49, 0xe7, 0xa5, 0xb5, 0x80, 0x13, 0xc1, 0xf4, 0xdb, + 0x51, 0x45, 0x28, 0xc6, 0x9d, 0xd8, 0x18, 0x91, 0xf6, 0x7f, 0xd4, 0xf1, 0x0e, 0x19, + 0x8f, 0xb4, 0x5f, 0x49, 0x00, 0x60, 0x21, 0xcb, 0x4b, 0xe9, 0x73, 0x27, 0x49, 0xf6, + 0x81, 0xb7, 0x42, 0xec, 0x92, 0x1b, 0xca, 0xdc, 0x65, 0x95, 0x16, 0x08, 0x68, 0x0f, + 0x2a, 0x59, 0xa2, 0x8d, 0x92, 0xa5, 0x93, 0x1b, 0xd4, 0x9e, 0x55, 0x1d, 0xa0, 0xb7, + 0xc0, 0xe5, 0xfc, 0xa1, 0xdb, 0xa1, 0x87, 0xdc, 0x17, 0x63, 0x11, 0xd5, 0x3e, 0x5c, + 0x7d, 0x32, 0x86, 0x7b, 0x4a, 0x7c, 0xf6, 0x67, 0x0a, 0xc1, 0x33, 0x32, 0xc5, 0x65, + 0x9e, 0x12, 0x12, 0x21, 0x90, 0xfe, 0x46, 0xcf, 0x65, 0x59, 0xee, 0x0d, 0xd2, 0x92, + 0x0f, 0xe4, 0x00, 0xa9, 0x94, 0x26, 0x05, 0x55, 0x1a, 0xef, 0xf2, 0xf9, 0xe2, 0x2f, + 0xa8, 0x75, 0x7f, 0x83, 0x3a, 0x97, 0xbb, 0x49, 0x1b, 0x2b, 0xd5, 0x84, 0x16, 0x11, + 0x5a, 0x29, 0xef, 0xd6, 0x5f, 0x40, 0x15, 0xcb, 0x13, 0xfe, 0x7f, 0xf5, 0x87, 0x38, + 0xcd, 0x56, 0xa9, 0xe5, 0x13, 0x0b, 0xfc, 0x66, 0xa1, 0x7f, 0x4e, 0xe2, 0xbc, 0x92, + 0xf1, 0x7b, 0x10, 0x72, 0x27, 0x97, 0x89, 0x35, 0xab, 0x4f, 0x80, 0xd3, 0xf6, 0x4f, + 0x69, 0x4c, 0x0b, 0xd5, 0x85, 0xb9, 0xc4, 0xd0, 0xd3, 0x0b, 0x6c, 0xdb, 0x27, 0xa5, + 0x27, 0xb8, 0xbf, 0xac, 0x31, 0x99, 0xc4, 0x61, 0xd8, 0x00, 0x39, 0x54, 0xcd, 0xd3, + 0x0c, 0x0c, 0xf1, 0xdb, 0x97, 0x75, 0xf8, 0x0a, 0xf9, 0x32, 0xec, 0x6b, 0x45, 0x7f, + 0x1f, 0xf0, 0xe0, 0xb9, 0xf7, 0xb2, 0xaf, 0x7b, 0xdc, 0x34, 0x94, 0x9f, 0x96, 0x6c, + 0x30, 0x56, 0x22, 0x97, 0xc0, 0x80, 0xd9, 0x71, 0x77, 0x3b, 0x40, 0x6e, 0x89, 0xd7, + 0xf1, 0xe2, 0xbb, 0xf6, 0x54, 0x60, 0x08, 0x93, 0xab, 0x11, 0xfa, 0xae, 0xce, 0xe7, + 0x75, 0x28, 0x71, 0x02, 0x42, 0x43, 0x10, 0xee, 0xff, 0xd4, 0xc9, 0xc0, 0x18, 0xb4, + 0xe4, 0xc1, 0x41, 0x0e, 0x2f, 0x2d, 0x25, 0x2a, 0x24, 0x22, 0xaf, 0xe2, 0x77, 0x6c, + 0x30, 0x46, 0xef, 0xd1, 0x41, 0x12, 0x88, 0x85, 0x99, 0xcd, 0xce, 0xb1, 0x1c, 0x87, + 0xe7, 0x23, 0x96, 0xfa, 0x2a, 0xa5, 0x6c, 0xf6, 0x89, 0xa2, 0x4e, 0x65, 0x1d, 0x12, + 0xfa, 0x91, 0x21, 0x69, 0x70, 0x35, 0xf9, 0x2d, 0x8a, 0x72, 0x51, 0xcf, 0x2c, 0x19, + 0x2c, 0xbf, 0x21, 0xf2, 0x75, 0xa6, 0x14, 0x1f, 0x9b, 0xf3, 0x40, 0x4d, 0x70, 0x89, + 0x42, 0x05, 0xa6, 0x59, 0xe1, 0xc5, 0x2a, 0x71, 0x39, 0xb2, 0x1b, 0x6a, 0x4c, 0x1b, + 0x3e, 0x9f, 0x7a, 0x9d, 0x0d, 0x4b, 0x15, 0x6c, 0xe2, 0xf3, 0x76, 0xfe, 0x36, 0x23, + 0xec, 0x3a, 0x35, 0xcc, 0xba, 0xbf, 0x6c, 0x48, 0x67, 0xef, 0x1c, 0xdc, 0x02, 0x75, + 0x13, 0x50, 0xdd, 0x70, 0x6f, 0xbe, 0x1c, 0x2b, 0x71, 0xd8, 0x23, 0x31, 0x0a, 0x20, + 0x0c, 0x09, 0x74, 0xda, 0x3f, 0xf4, 0x8c, 0xaa, 0x4c, 0x59, 0x17, 0x3d, 0xbb, 0x62, + 0x78, 0x01, 0x57, 0x5c, 0x28, 0x22, 0x2a, 0x64, 0xaa, 0x14, 0x8a, 0xe6, 0x3b, 0x0c, + 0x09, 0xda, 0x44, 0x54, 0xc1, 0x25, 0xeb, 0x2d, 0xab, 0xab, 0xa4, 0xbb, 0x54, 0x38, + 0x8e, 0xd9, 0xa2, 0xdc, 0xb0, 0xf0, 0xe7, 0x58, 0x64, 0x29, 0x36, 0xad, 0x21, 0x8e, + 0xbc, 0xfd, 0x84, 0x10, 0xb4, 0x00, 0x97, 0xd9, 0x29, 0x70, 0x8d, 0x93, 0x3d, 0x97, + 0x60, 0x81, 0x93, 0x72, 0x7b, 0x03, 0x94, 0xae, 0xbb, 0xc3, 0x5c, 0xb8, 0x59, 0x24, + 0xe8, 0x37, 0x11, 0x74, 0x05, 0x98, 0x4a, 0xe7, 0xae, 0xa2, 0xbb, 0x53, 0x05, 0x98, + 0x18, 0x54, 0x48, 0x09, 0xed, 0xe3, 0xd1, 0xd7, 0x8a, 0xbc, 0x82, 0x6c, 0xb5, 0x9a, + 0x71, 0x3d, 0x9c, 0x6f, 0x52, 0xa9, 0xf7, 0x30, 0x65, 0x75, 0xfe, 0x40, 0xdb, 0x6d, + 0xfc, 0x8e, 0xa8, 0x9d, 0x72, 0x5c, 0x53, 0x74, 0x35, 0xc5, 0x35, 0xdc, 0x93, 0x4a, + 0x12, 0xeb, 0x93, 0x50, 0x0c, 0x4d, 0x01, 0xbf, 0x1c, 0x92, 0x5c, 0xf1, 0x67, 0xa2, + 0xbe, 0x55, 0x64, 0xea, 0x2e, 0x2f, 0xf7, 0xe6, 0xc9, 0xdc, 0xbc, 0xa5, 0x6a, 0x6f, + 0x46, 0xef, 0x1e, 0x68, 0x80, 0x06, 0xb8, 0xbd, 0x9e, 0x95, 0x7c, 0x51, 0x4a, 0xd0, + 0x64, 0x7e, 0x7e, 0x76, 0xa2, 0xd0, 0x81, 0x2f, 0x91, 0xdb, 0x4f, 0x21, 0x03, 0xd5, + 0x5c, 0xcc, 0xe4, 0x3c, 0x7e, 0xa5, 0xec, 0x8d, 0xb1, 0xb2, 0xdc, 0x7a, 0x80, 0xfd, + 0x1d, 0x94, 0xbd, 0x63, 0x03, 0x42, 0x07, 0x2a, 0xc4, 0xd7, 0x28, 0xfe, 0x4b, 0x22, + 0x04, 0xc7, 0x09, 0x14, 0x4f, 0x89, 0x86, 0xe7, 0x8b, 0x9e, 0xf4, 0x33, 0x88, 0xe1, + 0x45, 0x24, 0x83, 0x74, 0x6d, 0xd9, 0xc8, 0xe9, 0x66, 0x9e, 0xeb, 0xd9, 0x97, 0xc2, + 0x72, 0x45, 0xf5, 0xa5, 0x29, 0x99, 0x89, 0xd5, 0xe2, 0x27, 0x90, 0x13, 0xe9, 0x64, + 0x6a, 0x29, 0xa9, 0xac, 0x9f, 0x79, 0xea, 0x9a, 0x31, 0x98, 0xa0, 0xbe, 0xe1, 0x04, + 0xb3, 0x24, 0x14, 0xf5, 0xa9, 0x3b, 0xf6, 0xc4, 0xe0, 0x0a, 0x80, 0x1d, 0x07, 0x5d, + 0xfa, 0x01, 0xed, 0x58, 0xa4, 0x6b, 0xb8, 0xfe, 0xd5, 0xfd, 0xbd, 0xc2, 0x69, 0x87, + 0x34, 0x99, 0x80, 0xd3, 0x37, 0xd7, 0x3e, 0xdc, 0x8a, 0xe8, 0xcf, 0xc8, 0xe2, 0x49, + 0xfd, 0x87, 0xe0, 0x97, 0x81, 0xd0, 0x27, 0x21, 0x06, 0x15, 0xc6, 0x44, 0xbd, 0x73, + 0xac, 0x0e, 0x3e, 0x97, 0x61, 0xcf, 0x84, 0x21, 0xc5, 0x43, 0x89, 0x12, 0x01, 0xf8, + 0x72, 0x29, 0x74, 0xf8, 0x28, 0x58, 0x89, 0x6d, 0x6a, 0x23, 0xc4, 0x27, 0x2d, 0x30, + 0x84, 0x2f, 0xdd, 0xc2, 0xfd, 0x37, 0x88, 0x02, 0xea, 0x06, 0x8c, 0xae, 0x01, 0xf5, + 0xc3, 0xf2, 0x8b, 0xa8, 0xe7, 0xa9, 0x08, 0x08, 0xe5, 0x74, 0x0e, 0xee, 0x6e, 0xf3, + 0x73, 0x3c, 0x57, 0xda, 0x82, 0x35, 0x9e, 0xc7, 0xa9, 0x0f, 0x16, 0x7d, 0xf8, 0x4f, + 0x25, 0x33, 0xeb, 0x98, 0xaa, 0xd8, 0xc9, 0xdc, 0x2b, 0xa3, 0x97, 0x6e, 0xfc, 0x8a, + 0xe8, 0x62, 0x28, 0x66, 0x68, 0x3e, 0x27, 0x51, 0x51, 0xbf, 0x64, 0x6f, 0xfc, 0x37, + 0xa6, 0xf8, 0x13, 0x8a, 0xcc, 0x9e, 0xd4, 0x63, 0x6f, 0xb7, 0x86, 0x02, 0xcb, 0x1c, + 0x18, 0x0c, 0x5f, 0x4d, 0x91, 0xa7, 0xf4, 0x00, 0xba, 0x61, 0x9a, 0x49, 0x03, 0xb2, + 0x75, 0x65, 0x70, 0xe0, 0x94, 0xa7, 0x03, 0x04, 0xd5, 0x95, 0x2a, 0xd5, 0xca, 0xc2, + 0x7a, 0xc4, 0xd2, 0xbf, 0x38, 0x25, 0x3e, 0xb0, 0x8a, 0xc1, 0xa1, 0x68, 0x8c, 0xec, + 0xc4, 0xa5, 0x31, 0x09, 0xdb, 0x5d, 0x6b, 0x90, 0xfd, 0x57, 0x93, 0x29, 0xb0, 0xbf, + 0xa8, 0x85, 0x69, 0x53, 0xd1, 0x18, 0x32, 0x1c, 0x36, 0x7b, 0x7d, 0xff, 0x60, 0x89, + 0xbf, 0x4d, 0x68, 0x72, 0x5c, 0x0b, 0xa1, 0xe5, 0x26, 0x23, 0x37, 0xc4, 0x5a, 0x68, + 0x89, 0x0d, 0xa2, 0xbf, 0xf6, 0xe2, 0xe9, 0xa4, 0x98, 0x57, 0xc2, 0x00, 0x12, 0xdf, + 0x0a, 0x8c, 0xa8, 0xdf, 0x5b, 0xf5, 0x58, 0x8f, 0x7a, 0x6c, 0xc0, 0x6d, 0xd3, 0x16, + 0xbf, 0x5a, 0x01, 0x78, 0xa9, 0x80, 0x6c, 0x65, 0xf6, 0xaf, 0xae, 0x12, 0x58, 0x75, + 0x34, 0xf8, 0x6e, 0xd1, 0x0a, 0xf3, 0x70, 0x70, 0x0f, 0x4a, 0x57, 0xaa, 0xed, 0x30, + 0xda, 0x9d, 0x83, 0x49, 0x37, 0x00, 0x26, 0x12, 0x17, 0x7f, 0xeb, 0x45, 0x37, 0x23, + 0x2b, 0x21, 0xcf, 0x2a, 0x36, 0x85, 0xaa, 0x7c, 0xc6, 0xb1, 0x55, 0x43, 0x73, 0xb1, + 0xd0, 0x9d, 0xc6, 0xef, 0xbc, 0x07, 0x59, 0x37, 0x90, 0x07, 0x2e, 0x5e, 0xad, 0x32, + 0x88, 0xd0, 0x83, 0x60, 0x57, 0x6b, 0x5b, 0x99, 0xe2, 0xe9, 0xad, 0xaa, 0xb3, 0x39, + 0xe0, 0x98, 0xc4, 0x47, 0x33, 0x2e, 0xeb, 0x10, 0xb9, 0x42, 0x73, 0xcc, 0xa6, 0x38, + 0xd3, 0xf9, 0x22, 0xe1, 0xdb, 0x1e, 0x1a, 0x5b, 0x24, 0xbd, 0xee, 0x00, 0x71, 0x2a, + 0x98, 0x12, 0x2a, 0x15, 0x66, 0x46, 0xc7, 0x54, 0x52, 0xf0, 0xba, 0x50, 0x55, 0x39, + 0xc5, 0xd1, 0xdc, 0xd9, 0xc0, 0x72, 0x4c, 0x0f, 0x05, 0xfa, 0xb4, 0x4b, 0x4d, 0xb0, + 0xba, 0x6b, 0x90, 0x11, 0xc0, 0x4d, 0x63, 0xc6, 0x5f, 0x67, 0x10, 0x47, 0x84, 0x4f, + 0xb3, 0x93, 0xff, 0xbb, 0x49, 0x14, 0xe4, 0x2e, 0x40, 0xf0, 0x3d, 0x31, 0x32, 0x4b, + 0xe4, 0x37, 0xa7, 0x1a, 0xd0, 0x62, 0x6e, 0x80, 0xa6, 0xfe, 0x86, 0x01, 0x4e, 0xc4, + 0x31, 0x2d, 0xee, 0x5d, 0x02, 0xf5, 0xb3, 0x2a, 0xe0, 0xe3, 0x22, 0x7e, 0x21, 0x38, + 0x58, 0xca, 0x1f, 0x59, 0x29, 0xac, 0x5d, 0x90, 0x0e, 0x1c, 0x3a, 0x49, 0xdd, 0x14, + 0xc2, 0x70, 0x5a, 0x65, 0x4f, 0xf2, 0x73, 0xf5, 0xf4, 0x82, 0x9f, 0x52, 0x0f, 0x9a, + 0xf7, 0xc9, 0x4f, 0xcd, 0x75, 0x02, 0x21, 0xed, 0x4f, 0xf9, 0x9a, 0x7a, 0x67, 0x1e, + 0xd5, 0x40, 0x54, 0xb4, 0xb9, 0xb8, 0xf6, 0x58, 0xb1, 0x20, 0xa3, 0x87, 0xb5, 0x33, + 0xb8, 0xd0, 0xe2, 0xcc, 0x6e, 0xde, 0xcb, 0x3a, 0x2a, 0x39, 0xa9, 0x64, 0x8c, 0x02, + 0xdb, 0xc8, 0xd5, 0xaf, 0x20, 0x89, 0x82, 0x69, 0x53, 0xd7, 0x05, 0xe3, 0xc2, 0xfe, + 0x2f, 0xee, 0x34, 0xc0, 0x95, 0x77, 0x2e, 0x8a, 0x88, 0x2f, 0x6d, 0xb3, 0x55, 0xdd, + 0x9b, 0x5d, 0xb4, 0xe8, 0x30, 0x0d, 0x34, 0xd5, 0x99, 0x8c, 0xfd, 0x5d, 0x71, 0x92, + 0xff, 0x96, 0xbb, 0xd1, 0xcd, 0xef, 0xe7, 0x8b, 0x04, 0xf6, 0x66, 0x89, 0x6c, 0x48, + 0x07, 0x24, 0xa3, 0x08, 0xa5, 0x16, 0x84, 0x71, 0x4a, 0x56, 0x8d, 0xcc, 0xbd, 0xe0, + 0xbb, 0x22, 0x72, 0x7f, 0xde, 0xdf, 0x54, 0xf2, 0x19, 0x03, 0x64, 0xe0, 0x6b, 0xb6, + 0x98, 0x25, 0x87, 0xfe, 0xba, 0x97, 0x78, 0x49, 0x5e, 0x28, 0xc6, 0xa7, 0xfb, 0x93, + 0x3f, 0xfb, 0x05, 0xaa, 0x67, 0x74, 0x80, 0xe3, 0x22, 0x5e, 0x04, 0x42, 0xb4, 0xd8, + 0xa2, 0x87, 0xe2, 0x82, 0x7a, 0xd7, 0x4e, 0xaa, 0xfb, 0x7d, 0xa0, 0x30, 0x83, 0xa8, + 0x73, 0x62, 0x76, 0x28, 0x2f, 0xc4, 0x1d, 0xec, 0x2b, 0xa2, 0xbf, 0xc3, 0xfb, 0x04, + 0xdd, 0xed, 0x84, 0xf1, 0xa9, 0x71, 0x26, 0x3b, 0x1d, 0x20, 0x6a, 0x34, 0x53, 0xf8, + 0x4e, 0xe0, 0xaa, 0x72, 0xd3, 0xc3, 0x02, 0x8a, 0xea, 0xd6, 0xc4, 0xa6, 0xf3, 0xc1, + 0x2a, 0xf9, 0x3b, 0xee, 0x7e, 0xb6, 0xc0, 0x87, 0x2a, 0x9a, 0xe2, 0x36, 0x01, 0x26, + 0xa2, 0x2f, 0x1f, 0xc1, 0x06, 0xc3, 0x3a, 0x5b, 0x33, 0x6a, 0x64, 0x32, 0x2f, 0xb1, + 0xf7, 0x6f, 0x0f, 0x21, 0xfc, 0xcc, 0x23, 0x66, 0x17, 0xa4, 0xb3, 0xbe, 0xad, 0x0a, + 0x6c, 0xd2, 0x61, 0x27, 0x71, 0x55, 0x58, 0x4c, 0x5d, 0xa7, 0x66, 0xcb, 0xfd, 0xa2, + 0x1d, 0xea, 0x42, 0x1b, 0x22, 0x7e, 0x57, 0xa9, 0xfd, 0xef, 0x92, 0xa4, 0x7c, 0x3d, + 0x62, 0xfd, 0xf9, 0x34, 0x32, 0x9d, 0xd9, 0x00, 0x30, 0x56, 0x57, 0x03, 0x2c, 0x04, + 0xb1, 0x2e, 0xcd, 0xe3, 0x65, 0x41, 0x92, 0x87, 0x0f, 0x25, 0x5c, 0x76, 0x49, 0xd5, + 0x85, 0x2d, 0x22, 0x9f, 0xd1, 0xfb, 0x03, 0x99, 0x7e, 0xca, 0xb2, 0x8d, 0xb8, 0xd7, + 0xc0, 0x28, 0x06, 0x35, 0x01, 0xbe, 0xcf, 0xf4, 0x63, 0x79, 0x64, 0xf5, 0xe8, 0x12, + 0xcf, 0x2b, 0xe5, 0x1d, 0x8e, 0xcc, 0x20, 0x57, 0x22, 0xe1, 0x09, 0xf7, 0x48, 0x51, + 0x30, 0x61, 0x5c, 0x47, 0x7c, 0xc3, 0x65, 0x22, 0xca, 0x6f, 0xc1, 0x83, 0xe3, 0x35, + 0x98, 0xfc, 0x21, 0xd4, 0x61, 0xf5, 0x04, 0x3b, 0x2c, 0x66, 0x71, 0xf4, 0x39, 0x45, + 0x8b, 0x7f, 0xfe, 0x75, 0xb4, 0x3f, 0xae, 0x19, 0x82, 0x3f, 0xaf, 0x68, 0x0e, 0x14, + 0x27, 0x64, 0x38, 0x11, 0xcb, 0x4a, 0x1b, 0x5d, 0x58, 0x2a, 0x29, 0x71, 0xcd, 0x20, + 0x2a, 0xb7, 0x85, 0x02, 0x93, 0x54, 0x31, 0x88, 0xbb, 0x69, 0x9f, 0x8b, 0x6e, 0xf5, + 0x57, 0x8c, 0x4e, 0x38, 0x88, 0x3c, 0xd6, 0x1e, 0x12, 0x46, 0x20, 0xb9, 0xb3, 0xa2, + 0xc0, 0x37, 0x0c, 0xd1, 0x3f, 0x4e, 0xcc, 0x75, 0xf0, 0x70, 0x22, 0x22, 0x18, 0xfb, + 0x3f, 0xa4, 0xa2, 0xcb, 0x47, 0xb4, 0x88, 0xae, 0xc3, 0xa7, 0x21, 0x58, 0x96, 0xe1, + 0x10, 0xaf, 0x2b, 0x6d, 0x26, 0x73, 0x90, 0x02, 0x7f, 0x18, 0x9c, 0xbd, 0xae, 0xab, + 0xb6, 0xce, 0xa3, 0xd6, 0x53, 0x39, 0xe7, 0x2b, 0xb4, 0x81, 0xda, 0x56, 0x73, 0x5f, + 0xb2, 0x72, 0xb9, 0xbf, 0x8e, 0x62, 0x5b, 0xb5, 0x92, 0xb2, 0x47, 0x6e, 0x51, 0x52, + 0x0d, 0x23, 0x8a, 0xae, 0x36, 0x78, 0xf4, 0x14, 0x0b, 0xcc, 0xdf, 0x6f, 0xee, 0xda, + 0xd3, 0x86, 0xcf, 0x9c, 0x50, 0xa5, 0xc2, 0x6b, 0xcf, 0x62, 0xfe, 0x67, 0x27, 0xc3, + 0x0f, 0xde, 0x37, 0x04, 0x27, 0xf1, 0xee, 0x33, 0x8f, 0xe9, 0x4f, 0x45, 0x66, 0x9a, + 0x3e, 0x92, 0x03, 0x16, 0xc7, 0xd7, 0xab, 0x41, 0xfd, 0x9a, 0x66, 0x3a, 0x9d, 0xb0, + 0x3a, 0x43, 0x25, 0x3a, 0xb8, 0xca, 0xd1, 0xdf, 0x1c, 0xe2, 0xb3, 0x20, 0x72, 0xa6, + 0xc1, 0xa1, 0xdb, 0xdc, 0xde, 0x21, 0xd8, 0x62, 0x5c, 0xf3, 0x9d, 0x43, 0xb3, 0x8b, + 0x4c, 0x80, 0xb4, 0x86, 0x77, 0x87, 0x39, 0xf2, 0xe1, 0x1d, 0x52, 0x13, 0xdf, 0x36, + 0xf4, 0xb4, 0x84, 0x99, 0xbe, 0x44, 0x48, 0x1f, 0x27, 0x69, 0xa3, 0x1b, 0x22, 0x39, + 0x70, 0x8e, 0xd2, 0x3a, 0x0e, 0xfe, 0x2c, 0x72, 0x54, 0xea, 0x18, 0xef, 0x78, 0x48, + 0x65, 0x45, 0x6c, 0xca, 0x1b, 0x4c, 0x7a, 0x8a, 0xb0, 0xba, 0x0f, 0x39, 0x04, 0x1c, + 0x6a, 0x7a, 0x95, 0x2c, 0xbb, 0xb1, 0xf2, 0xed, 0x46, 0x66, 0xcf, 0xa6, 0xe9, 0x7f, + 0xef, 0x15, 0x87, 0x51, 0xef, 0xce, 0xf7, 0x03, 0xce, 0x1e, 0xce, 0x16, 0x88, 0xd3, + 0x3b, 0x58, 0x2c, 0x13, 0xff, 0xd7, 0xd2, 0xc3, 0xe9, 0x9a, 0xca, 0x41, 0xef, 0x1b, + 0x24, 0x41, 0x3c, 0xa3, 0xe2, 0xcc, 0x34, 0x4d, 0x77, 0xdb, 0xbe, 0x1a, 0x01, 0x1b, + 0xab, 0xa2, 0xce, 0xb3, 0x24, 0x28, 0xdc, 0xea, 0xbe, 0x7f, 0xe6, 0xa6, 0xfb, 0x11, + 0x9a, 0xce, 0x1c, 0xb9, 0xf1, 0xd0, 0x91, 0x57, 0xbe, 0xd1, 0xed, 0x91, 0x38, 0x0d, + 0x1d, 0x91, 0x04, 0x06, 0x13, 0x80, 0x5f, 0xc2, 0xfc, 0x8f, 0xc2, 0x87, 0x2e, 0xb9, + 0xac, 0x5f, 0x56, 0x18, 0x75, 0x72, 0x1f, 0x1e, 0xdb, 0x1f, 0x50, 0x84, 0x5d, 0x52, + 0xaa, 0x07, 0x82, 0xce, 0x56, 0x0f, 0x7d, 0x3f, 0x0e, 0x24, 0x7c, 0xc7, 0xf5, 0x3d, + 0xa2, 0x52, 0xdf, 0xe6, 0x7e, 0xca, 0x1c, 0x50, 0xb5, 0xec, 0x58, 0x77, 0x92, 0xd6, + 0x6d, 0x7b, 0xc3, 0xe0, 0x4d, 0xfc, 0xb8, 0x5a, 0x27, 0x00, 0x0e, 0x7f, 0x0e, 0xd0, + 0x6d, 0xae, 0x68, 0x7f, 0x6a, 0x24, 0xff, 0x42, 0x67, 0xfe, 0x67, 0x9f, 0x68, 0x09, + 0x48, 0xed, 0xc8, 0xe5, 0x16, 0x98, 0xbe, 0x3f, 0xe5, 0xe5, 0x69, 0xc2, 0xe6, 0xd3, + 0x11, 0xa8, 0x43, 0x90, 0x10, 0x49, 0x74, 0x31, 0x6e, 0x4f, 0x93, 0xc9, 0x6a, 0x9c, + 0x66, 0xde, 0x1a, 0xe2, 0x8d, 0x56, 0xe6, 0xab, 0x31, 0x61, 0x52, 0x8a, 0x79, 0xc4, + 0xf1, 0x42, 0xc5, 0x30, 0x2b, 0xe1, 0x2b, 0x49, 0x86, 0xc1, 0x5a, 0x88, 0x9b, 0xcb, + 0x06, 0x63, 0xea, 0x3f, 0xd6, 0x0c, 0xef, 0x84, 0xb5, 0x62, 0x20, 0x5a, 0x03, 0xc2, + 0x94, 0xee, 0x9c, 0xe1, 0x09, 0x8c, 0xc8, 0x3b, 0x0f, 0x25, 0x05, 0x7a, 0x90, 0x5b, + 0x4c, 0xc8, 0xdb, 0x8f, 0x1e, 0x24, 0x1a, 0x51, 0x85, 0xa3, 0x13, 0x70, 0x55, 0xd9, + 0x49, 0x46, 0x3b, 0xb6, 0x38, 0xd5, 0x04, 0xae, 0xfa, 0x04, 0x07, 0xfe, 0xfa, 0x6a, + 0x65, 0xe2, 0xce, 0x4d, 0x7c, 0x41, 0x0b, 0xc5, 0xb2, 0x50, 0xf1, 0x40, 0x5f, 0x69, + 0x6b, 0x91, 0x3e, 0x5a, 0x13, 0xf9, 0x00, 0x8e, 0x6c, 0xfd, 0x13, 0xf7, 0x4d, 0x2c, + 0x25, 0xdf, 0xa6, 0xdd, 0xba, 0xd3, 0xc9, 0x3e, 0x93, 0xdc, 0x0c, 0x52, 0xeb, 0xfc, + 0x6d, 0x60, 0x9b, 0xc1, 0xbe, 0x2a, 0xce, 0x0f, 0x7f, 0x04, 0x37, 0x97, 0x9a, 0x1f, + 0x61, 0xcd, 0x3b, 0x96, 0xad, 0x75, 0x9a, 0x83, 0xfb, 0x62, 0x5d, 0x51, 0x1b, 0x3d, + 0xca, 0xf1, 0xb6, 0x5c, 0x02, 0xa0, 0xf3, 0xc1, 0x49, 0xf7, 0xed, 0x60, 0x79, 0xa8, + 0xe4, 0x68, 0x08, 0x8d, 0x44, 0x44, 0x05, 0x1e, 0xc9, 0xe9, 0xe4, 0xf7, 0x8e, 0x5d, + 0x12, 0x80, 0x08, 0x6b, 0xac, 0x3d, 0xb8, 0x1c, 0x0c, 0xbf, 0x24, 0x77, 0x1b, 0x5f, + 0x10, 0xba, 0x0b, 0x61, 0xb3, 0x4b, 0x39, 0x6e, 0xc7, 0x2c, 0x91, 0x2c, 0x52, 0x52, + 0xce, 0x95, 0x78, 0xc4, 0xd2, 0x33, 0xa8, 0x6d, 0x38, 0xe7, 0x32, 0x4b, 0x48, 0x90, + 0xc6, 0x59, 0x69, 0x9e, 0xc2, 0xe8, 0x1f, 0x10, 0x9b, 0x65, 0xb5, 0x79, 0x3e, 0xf1, + 0x5b, 0xf7, 0x69, 0x9c, 0x73, 0xa4, 0x46, 0x83, 0xe7, 0x88, 0x8c, 0x69, 0xbd, 0xe4, + 0x83, 0xc2, 0x75, 0xf6, 0xf3, 0x53, 0x1a, 0x7a, 0xec, 0x9f, 0xc4, 0x01, 0x68, 0x36, + 0x7d, 0x1e, 0x2f, 0xf9, 0x51, 0x3a, 0x64, 0xa7, 0x76, 0x49, 0xb3, 0xfd, 0x01, 0xfe, + 0xaf, 0xc5, 0x23, 0xe2, 0x97, 0x58, 0x92, 0xaf, 0x73, 0xef, 0x2d, 0x77, 0xb4, 0x77, + 0x89, 0x72, 0xc3, 0xcd, 0xb7, 0x39, 0x08, 0xa5, 0x75, 0x2d, 0xa4, 0x01, 0x69, 0xba, + 0x6f, 0xf3, 0xa8, 0x2a, 0x5f, 0x67, 0x64, 0x02, 0x6a, 0x20, 0x67, 0xa2, 0xaa, 0x6e, + 0x75, 0x1d, 0xd4, 0x32, 0x84, 0x89, 0x45, 0x78, 0xc6, 0x1d, 0xcf, 0x56, 0x68, 0x40, + 0x71, 0x43, 0xaf, 0x19, 0x91, 0xd1, 0x91, 0xab, 0x00, 0x10, 0xee, 0xbd, 0xc1, 0xbc, + 0xff, 0x59, 0xbc, 0x7c, 0xe8, 0xa8, 0x6a, 0xc9, 0x7c, 0xa4, 0x87, 0x3d, 0xc7, 0x9b, + 0x7b, 0xb0, 0x9a, 0xd1, 0x5f, 0x7f, 0xa3, 0x90, 0xd9, 0x8d, 0xb2, 0x9e, 0xd0, 0x4e, + 0x48, 0x15, 0x24, 0x85, 0xc4, 0xd1, 0x87, 0xb9, 0xc3, 0xa4, 0xf8, 0x34, 0xc4, 0xe8, + 0xac, 0xba, 0x57, 0x97, 0x40, 0xfd, 0xf3, 0x3b, 0xe8, 0x16, 0x6c, 0xed, 0x11, 0xec, + 0x0c, 0xd0, 0xac, 0xfd, 0x8e, 0x33, 0x31, 0x80, 0x86, 0x83, 0x14, 0x04, 0x22, 0x33, + 0x26, 0x0e, 0x81, 0x77, 0x18, 0x9c, 0xf3, 0x39, 0x88, 0x4a, 0xa3, 0xc7, 0x41, 0x62, + 0x0e, 0x3d, 0x62, 0xd9, 0x17, 0x62, 0xd3, 0x4c, 0xaf, 0x8d, 0x56, 0x13, 0x52, 0x08, + 0x44, 0x78, 0xfb, 0xcd, 0x6b, 0xdb, 0x58, 0x89, 0x5f, 0xe6, 0xee, 0xfa, 0xae, 0x18, + 0x92, 0x93, 0x66, 0xc7, 0x74, 0xec, 0x59, 0xfe, 0x14, 0x4c, 0x9f, 0xe5, 0x24, 0xa3, + 0xc1, 0xda, 0x0e, 0x28, 0x01, 0x0c, 0xf9, 0xe3, 0x33, 0x3f, 0xff, 0xdc, 0x3e, 0x0a, + 0x6a, 0xdc, 0xd3, 0x34, 0xae, 0x4a, 0x2c, 0xa6, 0x53, 0x3f, 0xa8, 0x8a, 0xeb, 0xf6, + 0x5d, 0x61, 0x8b, 0x2a, 0x71, 0x20, 0x75, 0x66, 0x1d, 0xe2, 0x37, 0x5a, 0xc9, 0x9f, + 0x78, 0x49, 0x86, 0x2d, 0x1d, 0xe9, 0xa8, 0xdf, 0x71, 0x9d, 0xb9, 0x4b, 0xd2, 0x2a, + 0xaa, 0x3f, 0x83, 0xfe, 0x97, 0xd7, 0x8e, 0xda, 0x64, 0x78, 0x41, 0xc4, 0x53, 0x33, + 0xe0, 0xf8, 0x76, 0xa0, 0xe0, 0x13, 0xaf, 0x23, 0x49, 0xe9, 0x5f, 0x32, 0xe0, 0x69, + 0x2d, 0x8c, 0x61, 0xf0, 0xdd, 0x0e, 0x1d, 0x1e, 0x29, 0x98, 0xd7, 0xe0, 0x0d, 0x9d, + 0x6d, 0x99, 0xcb, 0x4b, 0xcb, 0xb8, 0x2a, 0xb2, 0x6f, 0xba, 0x52, 0xb6, 0xb2, 0xb6, + 0x37, 0xa9, 0x25, 0xc4, 0xaa, 0x31, 0xb5, 0x22, 0xc0, 0x7d, 0x52, 0xc8, 0xa0, 0xb4, + 0x62, 0x18, 0xb9, 0x6f, 0x07, 0xeb, 0x2e, 0x5f, 0xee, 0xfe, 0xb8, 0x8e, 0xf8, 0x9e, + 0xba, 0x3d, 0xfa, 0x0e, 0xf9, 0x74, 0x27, 0xbc, 0x6f, 0x00, 0xd8, 0xec, 0x50, 0x97, + 0x38, 0x4b, 0x45, 0x8c, 0x44, 0xdd, 0x22, 0x4c, 0x1c, 0x78, 0xac, 0x47, 0x03, 0xba, + 0x05, 0x27, 0xad, 0xf1, 0xce, 0x62, 0xc9, 0x49, 0x52, 0xa0, 0xfc, 0xa0, 0x5d, 0xf9, + 0x59, 0x7b, 0x62, 0xb8, 0x44, 0xf7, 0x0d, 0xd4, 0xfc, 0x4f, 0x07, 0xbd, 0x07, 0x8d, + 0x40, 0x9b, 0x69, 0x6f, 0x84, 0xe3, 0x4b, 0x05, 0xeb, 0xc3, 0x28, 0x5c, 0xd8, 0x4c, + 0xf2, 0x76, 0x72, 0xe9, 0xef, 0x8f, 0x07, 0x0e, 0x2f, 0x12, 0x22, 0xa0, 0x34, 0x1c, + 0xd6, 0x74, 0xfa, 0x6f, 0x18, 0x34, 0x87, 0x39, 0xf4, 0x05, 0x7c, 0x20, 0x50, 0x12, + 0xab, 0x08, 0x33, 0xe8, 0xf4, 0xd2, 0x8c, 0xa2, 0xa1, 0x1e, 0x98, 0x3b, 0xea, 0x19, + 0x2e, 0x2e, 0x31, 0x85, 0x8f, 0x65, 0xe9, 0x13, 0xf0, 0xaf, 0x89, 0x30, 0xb4, 0xba, + 0x55, 0xc5, 0xd4, 0xac, 0xc1, 0xc3, 0x5d, 0x77, 0x29, 0xc1, 0x58, 0xbc, 0x41, 0x9e, + 0x89, 0x83, 0xb6, 0x47, 0x80, 0x49, 0xed, 0x27, 0x1d, 0x53, 0x67, 0xcd, 0xa0, 0x67, + 0x4f, 0x7f, 0x3d, 0x56, 0x77, 0xf3, 0xb7, 0x1f, 0xcf, 0xff, 0x74, 0xee, 0x06, 0x68, + 0x30, 0xc8, 0xe6, 0x38, 0x18, 0xe5, 0x5d, 0x01, 0x23, 0x77, 0xd3, 0x6b, 0x34, 0xa5, + 0x8c, 0x35, 0x3c, 0x34, 0x84, 0x15, 0x30, 0x4b, 0x4e, 0x0f, 0xed, 0xc6, 0xab, 0xc4, + 0xd5, 0x60, 0xf0, 0x7d, 0x37, 0x9f, 0xfb, 0x94, 0x73, 0x5c, 0x8e, 0x7a, 0x25, 0xab, + 0x1b, 0x14, 0x0b, 0xb6, 0xf9, 0xac, 0x62, 0x64, 0x2c, 0x1e, 0xc4, 0x26, 0x5e, 0x5f, + 0xe3, 0xb0, 0x1b, 0xe7, 0x42, 0x9a, 0x73, 0x03, 0x53, 0xe0, 0xaa, 0x06, 0xbc, 0x2d, + 0x0f, 0xd9, 0xf2, 0xf5, 0x28, 0xc2, 0x72, 0x86, 0xb3, 0x61, 0x49, 0x8b, 0x4b, 0x49, + 0xcc, 0x24, 0xc3, 0x6c, 0x72, 0xf4, 0xbe, 0x3f, 0xaf, 0xa5, 0x53, 0x14, 0xe2, 0x67, + 0xa6, 0x94, 0x7a, 0x02, 0xc7, 0x95, 0xfb, 0x3b, 0x92, 0x90, 0xa9, 0x9a, 0x8c, 0xfc, + 0x54, 0x1f, 0x46, 0xcb, 0x89, 0x90, 0x58, 0x2b, 0x77, 0x6a, 0x82, 0x70, 0xa9, 0x74, + 0xb4, 0x76, 0xc9, 0x12, 0x4c, 0x71, 0x39, 0x51, 0x9e, 0xff, 0x3d, 0x8d, 0x72, 0x56, + 0x0f, 0x76, 0x05, 0x4c, 0x9e, 0x49, 0x7c, 0xcb, 0xd1, 0x4b, 0x0a, 0x1e, 0x90, 0xf5, + 0x3b, 0x47, 0x70, 0xfb, 0xf9, 0x5d, 0xb4, 0x9e, 0xcc, 0xe7, 0x3c, 0x82, 0x12, 0xd4, + 0xd7, 0x06, 0x29, 0x28, 0xac, 0xe9, 0x20, 0x2a, 0x16, 0x94, 0xa6, 0xc2, 0x74, 0x9e, + 0x5e, 0x35, 0x8d, 0x87, 0xe4, 0x65, 0xda, 0xfc, 0x03, 0xf0, 0xfd, 0xa3, 0x57, 0x2c, + 0xc9, 0x2f, 0x87, 0xb9, 0x9f, 0x71, 0x21, 0x21, 0x17, 0xb0, 0x55, 0x87, 0xbb, 0x52, + 0x1a, 0x8b, 0x58, 0xf8, 0xd8, 0x6e, 0xf9, 0x77, 0xad, 0x43, 0x92, 0x80, 0x55, 0x57, + 0xe2, 0x11, 0xc5, 0xea, 0x38, 0x8c, 0x4d, 0x0e, 0x8e, 0x7b, 0xc5, 0xc8, 0xdb, 0xcc, + 0xd8, 0x97, 0x0e, 0x87, 0x9a, 0x98, 0xb7, 0x39, 0xfc, 0x20, 0xd0, 0x7a, 0x2b, 0x6a, + 0xed, 0x97, 0xdd, 0xba, 0x0c, 0xa2, 0xc0, 0xd3, 0xbf, 0x66, 0x28, 0x70, 0xab, 0xfa, + 0x7e, 0x74, 0x9b, 0x85, 0x63, 0xdb, 0xf3, 0x01, 0x0f, 0x9d, 0x22, 0x10, 0x56, 0x6e, + 0x6a, 0xa0, 0x7b, 0xba, 0x79, 0x70, 0xb9, 0x10, 0xd7, 0x46, 0xa2, 0x4e, 0x14, 0xc9, + 0x0f, 0x39, 0xbe, 0xd9, 0xa4, 0xda, 0x03, 0x13, 0x8e, 0x9b, 0x57, 0x5c, 0x21, 0xbc, + 0xb9, 0xd1, 0x32, 0xbb, 0xf3, 0x2b, 0x8c, 0x0a, 0x53, 0x91, 0xc5, 0x55, 0xb2, 0x3e, + 0xe0, 0xd5, 0x14, 0x6c, 0x5f, 0x42, 0x60, 0x86, 0xa0, 0xb9, 0x40, 0xdc, 0x26, 0xcf, + 0xf8, 0xc7, 0x74, 0x75, 0x57, 0x01, 0xd9, 0xa3, 0x47, 0x60, 0x19, 0x7c, 0x86, 0xc7, + 0x45, 0x94, 0x0e, 0xc3, 0x4c, 0x46, 0xaa, 0x60, 0x27, 0x2a, 0xe2, 0x18, 0xac, 0x76, + 0xae, 0x02, 0x05, 0x77, 0xa3, 0x6d, 0x11, 0x8d, 0xec, 0x8d, 0xe8, 0x8c, 0xe1, 0x1d, + 0x92, 0x31, 0x4c, 0xa1, 0xed, 0x88, 0x64, 0x5a, 0x09, 0xff, 0x2e, 0x3d, 0x3b, 0x8c, + 0x48, 0x5a, 0x9c, 0x44, 0xbe, 0xfb, 0x1f, 0x75, 0x05, 0xe1, 0xfb, 0x64, 0xc4, 0xd5, + 0x46, 0xe0, 0x5b, 0xb3, 0xbb, 0x87, 0x68, 0x38, 0x4f, 0xb2, 0xf2, 0xc1, 0xcb, 0xaf, + 0x67, 0x5d, 0x26, 0xd1, 0x6f, 0xf0, 0x18, 0x83, 0xfd, 0x96, 0x41, 0x4c, 0x92, 0x8c, + 0xe7, 0xe8, 0x85, 0x33, 0xde, 0x78, 0xbd, 0x49, 0x35, 0x7a, 0x79, 0x04, 0x74, 0xff, + 0xbb, 0xd7, 0x02, 0x42, 0xb7, 0x04, 0xe5, 0x07, 0x35, 0xb4, 0xc3, 0xa0, 0xb4, 0xe0, + 0xe9, 0x4d, 0x8d, 0x76, 0x12, 0x93, 0x72, 0xf8, 0xda, 0x10, 0xf8, 0xd4, 0x37, 0x39, + 0x0e, 0x34, 0x98, 0x21, 0xb6, 0x6c, 0x08, 0x70, 0xe9, 0x2d, 0x86, 0xc8, 0xe1, 0x95, + 0x9b, 0xd8, 0x96, 0x4a, 0x6b, 0x59, 0x8c, 0x3f, 0x78, 0x58, 0xf9, 0xb2, 0xe1, 0xf0, + 0xa6, 0xa8, 0xac, 0x3e, 0xb6, 0x77, 0xe9, 0x06, 0x72, 0xb7, 0x1f, 0x2e, 0x57, 0x24, + 0xe9, 0x42, 0x67, 0x99, 0x72, 0x5a, 0x21, 0x6b, 0x82, 0xcb, 0xc7, 0x22, 0x58, 0xb6, + 0x4a, 0x17, 0xb8, 0x23, 0x2b, 0x38, 0x47, 0x1b, 0xb9, 0xad, 0xdc, 0xf4, 0xe1, 0x52, + 0x03, 0x7a, 0xac, 0xa0, 0x1c, 0xfc, 0x79, 0xe1, 0x6e, 0x4c, 0x8d, 0x82, 0x43, 0xd4, + 0x80, 0x6b, 0x2b, 0xe4, 0x7f, 0x5c, 0xee, 0xbf, 0x07, 0x01, 0xdd, 0x28, 0x5b, 0x2b, + 0x32, 0x52, 0x4c, 0x7b, 0x1d, 0x21, 0x2a, 0x3e, 0x81, 0x2b, 0x06, 0xbd, 0x56, 0x6c, + 0xe4, 0xc7, 0x0d, 0x7a, 0x21, 0x92, 0x9f, 0xbe, 0x43, 0x8e, 0xb1, 0xcd, 0x1c, 0x9b, + 0x98, 0x40, 0xd2, 0xba, 0x63, 0x29, 0xa5, 0x71, 0x3a, 0x16, 0xf5, 0x4d, 0xdb, 0x9a, + 0xe3, 0xa2, 0x98, 0x2d, 0x1c, 0x3e, 0x92, 0x70, 0xd1, 0x5e, 0x93, 0xcb, 0xe3, 0xbb, + 0x80, 0x38, 0x79, 0x07, 0x12, 0x95, 0xda, 0x99, 0x5b, 0x36, 0x71, 0xae, 0x25, 0xa1, + 0x74, 0x67, 0x15, 0x29, 0x7a, 0x77, 0x9f, 0x11, 0x5c, 0xb0, 0x52, 0xaa, 0x5a, 0x53, + 0x3b, 0xe4, 0xea, 0x77, 0x98, 0xb6, 0x49, 0x96, 0x2f, 0x4f, 0x6a, 0xcd, 0x81, 0xb5, + 0x39, 0x3f, 0xfb, 0xc7, 0xf0, 0x4f, 0x2b, 0x09, 0x59, 0x6d, 0x39, 0x5c, 0x7e, 0x4b, + 0x26, 0x20, 0x76, 0xff, 0xf1, 0x44, 0x59, 0xe9, 0x4b, 0xc2, 0x9c, 0x84, 0x17, 0xfc, + 0xb5, 0x54, 0x2e, 0x19, 0xe5, 0xf2, 0x33, 0x13, 0x84, 0x63, 0x31, 0x35, 0x55, 0x1f, + 0x4a, 0xc0, 0xd2, 0x39, 0x1a, 0xfa, 0xa8, 0x32, 0x0f, 0x24, 0x64, 0x28, 0x8a, 0xea, + 0x9f, 0x51, 0x5a, 0x72, 0x85, 0x36, 0xed, 0x73, 0x94, 0x03, 0xbe, 0x89, 0x65, 0x8b, + 0xbf, 0xf9, 0x16, 0xb3, 0xfe, 0x2a, 0x0b, 0x46, 0x81, 0x56, 0x9c, 0x6f, 0xbf, 0x47, + 0x49, 0xdf, 0x24, 0xa0, 0xdc, 0x27, 0x65, 0xcf, 0xa2, 0xdc, 0x7d, 0x13, 0x68, 0x8e, + 0x53, 0x79, 0x4c, 0x5b, 0x24, 0x4b, 0x83, 0x4a, 0xb9, 0xee, 0xa1, 0x66, 0x8a, 0xdc, + 0x1b, 0xec, 0xeb, 0x5f, 0x73, 0xe0, 0xa8, 0x35, 0x88, 0x9d, 0x2b, 0xc1, 0xd8, 0x37, + 0x75, 0x97, 0xaf, 0x77, 0x6a, 0x7a, 0x62, 0xbe, 0x3b, 0x32, 0x94, 0x73, 0xec, 0x85, + 0x93, 0x29, 0x00, 0xbe, 0xa4, 0xa9, 0xe5, 0x68, 0x4b, 0x05, 0x42, 0x56, 0xce, 0xae, + 0xb5, 0xe3, 0x1f, 0xf5, 0x74, 0x5b, 0x3d, 0x02, 0x1e, 0xe3, 0xc3, 0x07, 0xea, 0x97, + 0x70, 0x16, 0x6c, 0x80, 0x0d, 0xb3, 0xce, 0x79, 0xb2, 0xf2, 0xf7, 0xb2, 0xe3, 0x65, + 0xcd, 0x2c, 0x0b, 0xbb, 0x59, 0x8d, 0xd2, 0x5e, 0x16, 0xb2, 0x6c, 0xeb, 0xbf, 0x15, + 0xf0, 0xc5, 0x9f, 0xa3, 0x83, 0x91, 0xda, 0x08, 0xa4, 0x01, 0x7b, 0x91, 0xc5, 0x11, + 0xd4, 0x14, 0x57, 0x6c, 0xe4, 0xd3, 0x18, 0x13, 0xee, 0x07, 0xfd, 0xc6, 0x17, 0x9b, + 0xff, 0x35, 0xf7, 0x3f, 0x6b, 0x58, 0xe4, 0xb8, 0xb6, 0xb6, 0xd1, 0x20, 0xff, 0x6b, + 0x94, 0x94, 0x42, 0x04, 0x14, 0xc5, 0x67, 0x9f, 0x39, 0xea, 0x99, 0x91, 0xa1, 0xcb, + 0x65, 0xc4, 0x1a, 0xd8, 0xed, 0xbe, 0xe2, 0x8f, 0x98, 0x1c, 0x83, 0x3d, 0x98, 0xff, + 0x26, 0x89, 0x3b, 0xe7, 0xb1, 0xaa, 0x7e, 0x97, 0x28, 0xe7, 0x46, 0xbc, 0x68, 0x80, + 0x73, 0x93, 0xe3, 0x03, 0xcc, 0xc4, 0xc8, 0x55, 0x67, 0x75, 0x16, 0x73, 0xbb, 0x2f, + 0x89, 0x61, 0x58, 0xaf, 0xa5, 0x45, 0x19, 0x57, 0x96, 0x9a, 0x0b, 0xbd, 0xed, 0xa6, + 0x3c, 0x4c, 0x69, 0x25, 0x1f, 0xa6, 0x5e, 0x75, 0x62, 0x99, 0xfa, 0x9b, 0x9f, 0x6b, + 0x87, 0x5a, 0x30, 0x3a, 0x66, 0x68, 0x20, 0x43, 0xf9, 0xb6, 0x24, 0x11, 0xba, 0x4d, + 0xe6, 0x4b, 0x21, 0xa6, 0x3c, 0xeb, 0xc3, 0x6f, 0x8c, 0xaf, 0xe4, 0x44, 0x9b, 0xa5, + 0x3f, 0x0e, 0xde, 0x93, 0x15, 0x8b, 0x79, 0xdd, 0xfe, 0x45, 0x27, 0xf5, 0xf0, 0xeb, + 0x71, 0xd3, 0x44, 0x9e, 0x6b, 0x83, 0x57, 0x61, 0x5a, 0x7c, 0xb9, 0xd6, 0xa5, 0x7b, + 0xc2, 0x2a, 0x23, 0x22, 0x42, 0x66, 0x08, 0xc4, 0x0b, 0x59, 0xb7, 0x9c, 0x0b, 0x76, + 0x83, 0xa5, 0x56, 0x21, 0x9b, 0x52, 0x2f, 0x79, 0x5c, 0x18, 0x04, 0x04, 0x35, 0x61, + 0x85, 0xcc, 0xfb, 0x7a, 0x39, 0xb7, 0x36, 0xee, 0x12, 0x5b, 0xb4, 0x64, 0x1d, 0x0a, + 0xb9, 0x3c, 0xbe, 0x9c, 0x73, 0xb3, 0x1b, 0x49, 0x08, 0xbe, 0x08, 0xdf, 0xbd, 0x6f, + 0x9e, 0x41, 0x39, 0x3d, 0x8f, 0x9a, 0xa4, 0x6b, 0xe6, 0xa0, 0xf6, 0x35, 0x99, 0x5c, + 0x7f, 0xa8, 0x19, 0x71, 0xe8, 0x6d, 0xe9, 0x99, 0x02, 0x9b, 0x87, 0xd6, 0x0d, 0xef, + 0x1e, 0xfe, 0x41, 0xdb, 0xde, 0xc6, 0x0d, 0x1f, 0x35, 0x48, 0xae, 0x19, 0xe6, 0xf1, + 0x06, 0x7b, 0xfc, 0xde, 0xbd, 0xfc, 0x0d, 0x15, 0x02, 0xd7, 0xe2, 0x27, 0x5c, 0xd1, + 0xa3, 0x32, 0xa8, 0x75, 0x6f, 0x0f, 0xf1, 0x92, 0xfb, 0xb9, 0x28, 0xdb, 0x2e, 0xea, + 0xc0, 0xe5, 0xca, 0x84, 0x86, 0x0d, 0xec, 0xc4, 0x6d, 0x17, 0x16, 0x56, 0x34, 0xa8, + 0xd2, 0xc3, 0x57, 0x47, 0xac, 0xee, 0xbf, 0xa7, 0xe3, 0xdb, 0x9a, 0x16, 0x5c, 0x81, + 0x0e, 0x40, 0x68, 0x88, 0x3b, 0xa9, 0xec, 0xd8, 0x6f, 0xba, 0xad, 0x4d, 0xe8, 0xb4, + 0x8d, 0x7d, 0x5f, 0x00, 0xe3, 0xdd, 0xe1, 0x56, 0x3c, 0xf9, 0x24, 0x60, 0x89, 0x41, + 0x77, 0xfc, 0x2c, 0xfc, 0x87, 0x49, 0xfd, 0xb8, 0x25, 0xca, 0x02, 0x98, 0x80, 0x04, + 0x52, 0x56, 0x88, 0x17, 0x28, 0xee, 0xc6, 0x6d, 0x30, 0x97, 0x6a, 0xf8, 0x88, 0x87, + 0x62, 0x0a, 0x9c, 0xb6, 0x47, 0x79, 0xb7, 0xd4, 0x09, 0x2a, 0x90, 0xeb, 0x20, 0xe9, + 0x3f, 0x09, 0x6b, 0x22, 0x10, 0x6a, 0xbf, 0x3f, 0xe0, 0x19, 0x54, 0x9f, 0x8e, 0x25, + 0xde, 0x9c, 0x7c, 0x11, 0x05, 0x45, 0xc9, 0x1f, 0x82, 0x2d, 0x1a, 0xc6, 0x8d, 0x69, + 0x4c, 0x3d, 0xbc, 0xc3, 0x88, 0x67, 0xa1, 0x44, 0x88, 0x1c, 0x8c, 0x23, 0xbd, 0x04, + 0x9f, 0xa8, 0xc7, 0x9b, 0x0a, 0xdc, 0x38, 0x4d, 0x28, 0xf3, 0x69, 0x09, 0xa3, 0x59, + 0x75, 0x6b, 0xbd, 0x11, 0xec, 0x8d, 0x73, 0x03, 0x41, 0x9c, 0xed, 0x91, 0x04, 0x9a, + 0xdd, 0x13, 0x7b, 0x6d, 0x90, 0xa2, 0xf6, 0x74, 0xd2, 0xaa, 0x68, 0x10, 0x9f, 0xb8, + 0x0d, 0xa7, 0xc6, 0x49, 0x88, 0xc1, 0xd5, 0x46, 0xdc, 0xfd, 0xd0, 0x15, 0x4a, 0xd0, + 0xa2, 0xa6, 0x28, 0xf7, 0x47, 0xcc, 0x84, 0x0e, 0xa3, 0x62, 0x24, 0x9d, 0xb6, 0xcb, + 0xbd, 0x36, 0xdf, 0x37, 0x01, 0xf3, 0xc9, 0x2d, 0x2d, 0x6a, 0x75, 0xf0, 0xa2, 0xb2, + 0x3b, 0xf9, 0x6a, 0x37, 0xd6, 0x72, 0x2a, 0x6a, 0xe5, 0xcd, 0x30, 0x46, 0x61, 0x7b, + 0x6b, 0x94, 0xee, 0xfc, 0x32, 0x6e, 0xbf, 0xea, 0x83, 0xf3, 0x03, 0x20, 0x40, 0xd4, + 0xc4, 0x0f, 0x73, 0x70, 0x5e, 0x95, 0x56, 0xbc, 0xe1, 0xef, 0xaf, 0x40, 0xd7, 0xd7, + 0xa3, 0x41, 0x54, 0xc6, 0x4f, 0x1f, 0xad, 0x9b, 0x9b, 0x59, 0xa9, 0x78, 0x88, 0x26, + 0x01, 0x35, 0x06, 0x31, 0xfa, 0x5b, 0x80, 0x7f, 0x0b, 0xe2, 0x05, 0x6d, 0xfc, 0xa0, + 0x84, 0xe0, 0xd4, 0xcf, 0x7a, 0x52, 0x1d, 0x6c, 0x3d, 0x71, 0x93, 0xad, 0xcd, 0x5f, + 0x6b, 0x43, 0xd0, 0x60, 0x19, 0xc1, 0x57, 0x6b, 0x20, 0x6c, 0x59, 0x68, 0xc6, 0x1a, + 0x8f, 0xed, 0xd1, 0xef, 0x5a, 0xec, 0x24, 0x1c, 0x25, 0xbd, 0x26, 0x40, 0xd8, 0x48, + 0x3b, 0xfc, 0x32, 0x03, 0x0d, 0x92, 0x13, 0x6b, 0x9e, 0xa6, 0x94, 0xd6, 0x9d, 0x7b, + 0x86, 0x6a, 0x37, 0xae, 0xb5, 0x59, 0x76, 0xa3, 0x58, 0x04, 0xcf, 0xae, 0x3e, 0x36, + 0x84, 0x64, 0xcc, 0xb9, 0x9c, 0xb3, 0x37, 0xec, 0x32, 0x85, 0xb6, 0xc1, 0xeb, 0xe5, + 0x1b, 0x42, 0x5e, 0x85, 0x02, 0x86, 0xed, 0x6c, 0xc5, 0xcf, 0x0c, 0x5d, 0xa5, 0x8e, + 0x00, 0x9f, 0xe1, 0x38, 0xa2, 0x86, 0x98, 0x25, 0x31, 0x8f, 0x75, 0x9b, 0x51, 0x79, + 0xb3, 0xaf, 0xaa, 0xfe, 0xab, 0x18, 0x68, 0x62, 0x09, 0xfd, 0x35, 0x5f, 0x00, 0x7c, + 0xa0, 0x38, 0xc8, 0x79, 0x20, 0xa1, 0xe4, 0x72, 0x24, 0x84, 0x68, 0x12, 0x9b, 0x16, + 0xaf, 0x5f, 0xfe, 0xee, 0x0c, 0xbb, 0xd1, 0xf8, 0x51, 0xbe, 0xab, 0x01, 0x3f, 0x40, + 0xd3, 0x3e, 0x0f, 0xe1, 0x31, 0xc5, 0x1d, 0x5f, 0x11, 0x10, 0xf5, 0x29, 0x0b, 0x5c, + 0xa4, 0xdf, 0xd2, 0x68, 0xa2, 0x2b, 0x86, 0xaf, 0x72, 0xcb, 0xd1, 0x9e, 0x26, 0x76, + 0xc6, 0x82, 0xdc, 0x09, 0x73, 0x19, 0x2b, 0x6b, 0xdb, 0x8f, 0xb9, 0xed, 0xb5, 0xeb, + 0x44, 0xdd, 0xd4, 0x6c, 0x6c, 0x26, 0x91, 0xce, 0xeb, 0x30, 0x81, 0xef, 0x74, 0xa4, + 0x9b, 0xc6, 0xa2, 0xdb, 0x44, 0xa3, 0x9f, 0xe9, 0xc8, 0xdb, 0x85, 0x51, 0xab, 0x0b, + 0x91, 0x72, 0x03, 0xb3, 0x3d, 0xfc, 0xa3, 0x45, 0xb2, 0x42, 0x62, 0x8b, 0xa9, 0x0d, + 0xc5, 0x67, 0x39, 0x3a, 0x5d, 0xa4, 0xc2, 0xa8, 0xc2, 0x47, 0x3f, 0xfa, 0xc5, 0x3a, + 0x4a, 0x3c, 0xc1, 0xe5, 0x4e, 0xf2, 0xd4, 0x1b, 0xfb, 0xa3, 0x81, 0x00, 0xd9, 0xcd, + 0xf2, 0x49, 0xe7, 0x1f, 0x31, 0x70, 0xae, 0x92, 0x90, 0x09, 0x08, 0x52, 0x93, 0x06, + 0xfa, 0x1c, 0xde, 0xf3, 0x9f, 0x21, 0xbc, 0x54, 0xed, 0xe6, 0x76, 0xb2, 0x8c, 0x80, + 0xb6, 0xcf, 0x1a, 0x77, 0x20, 0x2e, 0x20, 0x85, 0x60, 0x5c, 0x44, 0x5f, 0xbc, 0x73, + 0x62, 0xa2, 0x5f, 0xf1, 0x06, 0x9d, 0xa0, 0x72, 0x86, 0x50, 0xe8, 0xc1, 0xeb, 0xf4, + 0x71, 0xd0, 0x14, 0xc9, 0x88, 0x73, 0xb4, 0xd2, 0xc0, 0x82, 0x3e, 0xe5, 0x43, 0xcc, + 0x4b, 0x4b, 0x3b, 0xf7, 0xe0, 0x5e, 0x81, 0x75, 0xe0, 0xd9, 0x55, 0xe4, 0x31, 0x81, + 0x7e, 0x1b, 0x7d, 0x71, 0x16, 0xf0, 0xd1, 0xcf, 0xd9, 0xaf, 0x7a, 0xfb, 0xf3, 0x23, + 0x24, 0x5d, 0x87, 0x16, 0xf1, 0xf5, 0xa4, 0xad, 0x84, 0xfb, 0xb4, 0x23, 0xb4, 0x89, + 0xa9, 0x35, 0x1e, 0xc4, 0x0e, 0x34, 0x36, 0x83, 0xaa, 0x94, 0x46, 0xad, 0xee, 0x98, + 0xdf, 0xa3, 0xb6, 0x88, 0xf2, 0x7b, 0x9b, 0xcd, 0x07, 0xba, 0x9b, 0xe3, 0x9b, 0x9c, + 0x4a, 0x59, 0xac, 0x5d, 0xf6, 0x07, 0x5b, 0x33, 0xca, 0xd5, 0x03, 0xb7, 0x07, 0x24, + 0xad, 0xe9, 0xd6, 0x6c, 0x49, 0x7b, 0x2c, 0xf0, 0x34, 0x12, 0xc4, 0x8e, 0x4f, 0xc6, + 0x83, 0x20, 0xe0, 0x2a, 0x0c, 0x34, 0x29, 0xd4, 0x11, 0x6c, 0x29, 0x64, 0xa9, 0x22, + 0xd7, 0x4c, 0xec, 0xab, 0x43, 0x66, 0x9b, 0x00, 0x91, 0x3b, 0x2e, 0x3e, 0xef, 0x4d, + 0x27, 0x16, 0x0f, 0x89, 0x63, 0x63, 0x66, 0x60, 0x8d, 0x18, 0x29, 0xc2, 0x72, 0x76, + 0x37, 0x8e, 0xdc, 0x5a, 0x06, 0x05, 0x3a, 0xf2, 0xd9, 0x95, 0xb8, 0xf7, 0x94, 0x84, + 0x4a, 0xda, 0x67, 0x80, 0xc0, 0xa5, 0x55, 0x1e, 0x80, 0x83, 0xad, 0x01, 0x30, 0xd5, + 0xf2, 0x64, 0xb1, 0xe9, 0xd6, 0x90, 0x94, 0x45, 0x6a, 0x90, 0x44, 0xd3, 0x70, 0xe5, + 0xa6, 0xf6, 0x64, 0xcb, 0xc9, 0xad, 0x3d, 0x85, 0x16, 0x1e, 0x84, 0x32, 0x98, 0xba, + 0xd5, 0x11, 0xf0, 0x81, 0x17, 0x4a, 0x2a, 0x16, 0x71, 0x10, 0x06, 0xe7, 0x0e, 0xc4, + 0x0e, 0xc7, 0x02, 0xbf, 0x17, 0x7f, 0x7c, 0x32, 0x3a, 0x2f, 0x24, 0xde, 0x82, 0xa9, + 0xc4, 0xfa, 0x0a, 0x93, 0xad, 0x02, 0xd5, 0xb2, 0x7f, 0xb2, 0x14, 0x45, 0xcc, 0x9d, + 0x48, 0x31, 0x6d, 0x0f, 0xef, 0x88, 0xd7, 0xf5, 0x49, 0xa5, 0x58, 0x8b, 0x4c, 0xdf, + 0x0d, 0xe0, 0x4d, 0x96, 0xdb, 0x7a, 0xc6, 0xae, 0xe1, 0x8c, 0x3a, 0xa5, 0xb3, 0xb9, + 0x44, 0xde, 0x36, 0xd0, 0xe5, 0xfb, 0x13, 0x3c, 0xe7, 0x02, 0xff, 0x69, 0x9d, 0xfe, + 0xf0, 0x25, 0x42, 0xdc, 0x97, 0xbb, 0x9c, 0xf9, 0x9a, 0x34, 0xce, 0x98, 0x89, 0x3b, + 0x84, 0xcd, 0xbb, 0x04, 0x05, 0xed, 0x15, 0x32, 0x4f, 0xb6, 0x49, 0x8a, 0xa4, 0xa9, + 0x64, 0x4e, 0x7c, 0xd7, 0xad, 0xae, 0xbc, 0xa7, 0x2b, 0x4e, 0x64, 0xe7, 0x0f, 0x90, + 0x29, 0x97, 0xf9, 0x6c, 0x0e, 0x4e, 0x00, 0xd6, 0xb1, 0x4f, 0x77, 0x15, 0x43, 0xaf, + 0x75, 0xc8, 0x00, 0xdd, 0x4d, 0x3d, 0xd5, 0xf2, 0xcc, 0xb3, 0xd6, 0x85, 0x80, 0x00, + 0x38, 0xa8, 0x6c, 0x87, 0xe4, 0x36, 0xe5, 0xfb, 0x4c, 0x23, 0x73, 0x0b, 0x08, 0x47, + 0x9b, 0xbf, 0x0b, 0x45, 0xcc, 0x10, 0xc7, 0x79, 0x67, 0xf7, 0x1b, 0xd3, 0x81, 0x13, + 0x29, 0xb3, 0x59, 0xcd, 0x2d, 0x49, 0x78, 0x18, 0x95, 0x5c, 0x9b, 0x32, 0x51, 0x89, + 0xe2, 0x7c, 0xf6, 0xa9, 0x59, 0x26, 0xa8, 0x1c, 0x3c, 0x22, 0x1e, 0xec, 0x2a, 0x48, + 0x33, 0x08, 0x90, 0x52, 0xb4, 0x1a, 0xbc, 0xb9, 0xf9, 0x78, 0x6a, 0x33, 0x1f, 0xd0, + 0x11, 0xe5, 0x9f, 0x57, 0x1f, 0x07, 0x1a, 0x19, 0x53, 0x30, 0x60, 0xa1, 0x84, 0xa1, + 0x9c, 0xb0, 0x29, 0xe6, 0xa1, 0xdb, 0x1a, 0x90, 0x21, 0x21, 0x89, 0xc9, 0xce, 0xb8, + 0x9c, 0x2b, 0xfd, 0x42, 0x2c, 0xd4, 0x7b, 0x7a, 0x2d, 0x55, 0x16, 0x69, 0x2d, 0x50, + 0x0a, 0xb4, 0x12, 0x49, 0x56, 0xc1, 0x6d, 0x15, 0xc1, 0xc8, 0xa7, 0x78, 0x7e, 0xe2, + 0x6e, 0xa5, 0xc4, 0x13, 0x7a, 0xec, 0x74, 0x9f, 0x5a, 0xe9, 0xd1, 0xac, 0x79, 0x1f, + 0x44, 0x5d, 0xb3, 0x3b, 0xe9, 0x59, 0xe7, 0xfb, 0x81, 0xaa, 0x09, 0x20, 0x10, 0xff, + 0xe1, 0x00, 0xc4, 0xa9, 0xb2, 0x14, 0x2f, 0x22, 0x7c, 0xc6, 0xc0, 0x26, 0x83, 0x48, + 0x3d, 0xd1, 0x7f, 0x32, 0x4d, 0x5b, 0x80, 0x60, 0xdf, 0x0b, 0xa1, 0x70, 0x52, 0x47, + 0xbc, 0x7f, 0x6e, 0xbd, 0xdc, 0x9f, 0xcb, 0xfe, 0xc1, 0xd4, 0x25, 0xf4, 0x4a, 0xb3, + 0x39, 0xe0, 0x50, 0x79, 0xd6, 0x07, 0xd9, 0x4c, 0x93, 0x6f, 0x3f, 0xaf, 0xaf, 0xfa, + 0x3a, 0xad, 0xac, 0x2e, 0x04, 0x9e, 0x9a, 0xac, 0x4a, 0xa3, 0xef, 0xa3, 0x70, 0x6a, + 0xd3, 0x38, 0x8f, 0x42, 0x29, 0x1c, 0x9b, 0x34, 0xb5, 0x78, 0x27, 0xeb, 0x80, 0x57, + 0x2d, 0x73, 0xc9, 0xd5, 0x0e, 0xeb, 0xde, 0xc7, 0xd4, 0x9d, 0x84, 0xf7, 0x15, 0x49, + 0x55, 0x5c, 0xcf, 0xa0, 0xcc, 0x8f, 0x45, 0xcb, 0x06, 0xb8, 0x12, 0xa7, 0x17, 0x09, + 0x6e, 0x36, 0xa4, 0x46, 0x2a, 0xcf, 0x21, 0xbc, 0x77, 0xa2, 0xf8, 0x8c, 0x24, 0xae, + 0xad, 0x11, 0x60, 0x2a, 0x31, 0x54, 0xe8, 0xbb, 0x9f, 0xbb, 0xfe, 0x89, 0x70, 0x02, + 0xf9, 0xb7, 0x3e, 0xdb, 0xf6, 0x45, 0x72, 0xb1, 0xea, 0x3e, 0x11, 0xf1, 0x63, 0x54, + 0xb8, 0x60, 0x5d, 0xff, 0xce, 0xe2, 0x57, 0xfe, 0x69, 0x4f, 0x85, 0x08, 0xbe, 0x1b, + 0xb3, 0x31, 0x4e, 0xf3, 0xa3, 0xcc, 0x60, 0x30, 0xb0, 0x83, 0x61, 0xd9, 0x17, 0xad, + 0x0a, 0x51, 0x84, 0xfa, 0x9f, 0x15, 0x32, 0x7e, 0x80, 0xcd, 0x03, 0x3b, 0xb7, + ], + }, +]; diff --git a/components/f4jumble/src/test_vectors_long.rs b/components/f4jumble/src/test_vectors_long.rs new file mode 100644 index 0000000000..ccc39139dd --- /dev/null +++ b/components/f4jumble/src/test_vectors_long.rs @@ -0,0 +1,28 @@ +pub(crate) struct TestVector { + pub(crate) length: usize, + pub(crate) jumbled_hash: &'static [u8], +} + +// From https://github.com/zcash-hackworks/zcash-test-vectors/blob/master/f4jumble_long.py +pub(crate) const TEST_VECTORS: &[TestVector] = &[ + TestVector { + length: 3246395, + jumbled_hash: &[ + 0x3f, 0xc2, 0xec, 0xdf, 0xb6, 0x86, 0x96, 0x57, 0x1d, 0x89, 0xe8, 0xbe, 0xdd, 0xb6, + 0x47, 0xe6, 0x99, 0x0b, 0x63, 0xa0, 0x17, 0x1c, 0x36, 0x44, 0x22, 0x73, 0xd6, 0x87, + 0xbd, 0x99, 0x25, 0x7e, 0xc5, 0x00, 0x2e, 0xc8, 0x19, 0x78, 0x01, 0xb6, 0x21, 0x73, + 0x2d, 0x6b, 0x05, 0xb8, 0xd7, 0x0f, 0x68, 0x86, 0x20, 0xa4, 0xc0, 0x88, 0x73, 0xc1, + 0x2e, 0x44, 0x39, 0xa0, 0x12, 0x7d, 0xc9, 0x45, + ], + }, + TestVector { + length: 4194368, + jumbled_hash: &[ + 0xa5, 0xf1, 0x8f, 0x16, 0x3e, 0x59, 0x8d, 0x4a, 0xdb, 0x6e, 0xa7, 0x24, 0x80, 0x57, + 0xe2, 0x4c, 0x1b, 0x61, 0xf2, 0x9b, 0x33, 0xb7, 0xab, 0xcd, 0xab, 0xd4, 0x20, 0xa0, + 0xf2, 0xee, 0x6c, 0x3e, 0xd3, 0x13, 0x94, 0x65, 0x2f, 0x28, 0xb5, 0x9c, 0x44, 0xd3, + 0xea, 0x9e, 0xcf, 0x85, 0xf4, 0xd5, 0x01, 0xe6, 0xaa, 0xc1, 0x4d, 0xf2, 0x88, 0xef, + 0xd6, 0x2c, 0xf8, 0x0d, 0x18, 0x29, 0xd0, 0x25, + ], + }, +]; diff --git a/components/zcash_address/CHANGELOG.md b/components/zcash_address/CHANGELOG.md new file mode 100644 index 0000000000..fd60620bd1 --- /dev/null +++ b/components/zcash_address/CHANGELOG.md @@ -0,0 +1,75 @@ +# Changelog +All notable changes to this library will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this library adheres to Rust's notion of +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Deprecated +- `zcash_address::Network` (use `zcash_protocol::consensus::NetworkType` instead). + +## [0.6.2] - 2024-12-13 +### Fixed +- Migrated to `f4jumble 0.1.1` to fix `no-std` support. + +## [0.6.1] - 2024-12-13 +### Added +- `no-std` support, via a default-enabled `std` feature flag. + +## [0.6.0] - 2024-10-02 +### Changed +- Migrated to `zcash_protocol 0.4`. + +## [0.5.0] - 2024-08-26 +### Changed +- Updated `zcash_protocol` dependency to version `0.3` + +## [0.4.0] - 2024-08-19 +### Added +- `zcash_address::ZcashAddress::{can_receive_memo, can_receive_as, matches_receiver}` +- `zcash_address::unified::Address::{can_receive_memo, has_receiver_of_type, contains_receiver}` +- Module `zcash_address::testing` under the `test-dependencies` feature. +- Module `zcash_address::unified::address::testing` under the + `test-dependencies` feature. + +### Changed +- Updated `zcash_protocol` dependency to version `0.2` + +## [0.3.2] - 2024-03-06 +### Added +- `zcash_address::convert`: + - `TryFromRawAddress::try_from_raw_tex` + - `TryFromAddress::try_from_tex` + - `ToAddress::from_tex` + +## [0.3.1] - 2024-01-12 +### Fixed +- Stubs for `zcash_address::convert` traits that are created by `rust-analyzer` + and similar LSPs no longer reference crate-private type aliases. + +## [0.3.0] - 2023-06-06 +### Changed +- Bumped bs58 dependency to `0.5`. + +## [0.2.1] - 2023-04-15 +### Changed +- Bumped internal dependency to `bech32 0.9`. + +## [0.2.0] - 2022-10-19 +### Added +- `zcash_address::ConversionError` +- `zcash_address::TryFromAddress` +- `zcash_address::TryFromRawAddress` +- `zcash_address::ZcashAddress::convert_if_network` +- A `TryFrom` implementation for `usize`. + +### Changed +- MSRV is now 1.52 + +### Removed +- `zcash_address::FromAddress` (use `TryFromAddress` instead). + +## [0.1.0] - 2022-05-11 +Initial release. diff --git a/components/zcash_address/Cargo.toml b/components/zcash_address/Cargo.toml new file mode 100644 index 0000000000..e05fc17395 --- /dev/null +++ b/components/zcash_address/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "zcash_address" +description = "Zcash address parsing and serialization" +version = "0.6.2" +authors = [ + "Jack Grigg ", +] +homepage = "https://github.com/zcash/librustzcash" +repository = "https://github.com/zcash/librustzcash" +readme = "README.md" +license = "MIT OR Apache-2.0" +edition = "2018" +rust-version = "1.52" +categories = ["cryptography::cryptocurrencies", "encoding"] +keywords = ["zcash", "address", "sapling", "unified"] + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +bech32.workspace = true +bs58.workspace = true +core2.workspace = true +f4jumble = { version = "0.1.1", path = "../f4jumble", default-features = false, features = ["alloc"] } +zcash_protocol.workspace = true +zcash_encoding.workspace = true +proptest = { workspace = true, optional = true } + +[dev-dependencies] +assert_matches.workspace = true +proptest.workspace = true + +[features] +default = ["std"] +std = [ + "core2/std", + "f4jumble/std", + "zcash_encoding/std", + "zcash_protocol/std", +] +test-dependencies = ["dep:proptest"] + +[lib] +bench = false + +[lints] +workspace = true diff --git a/components/zcash_address/LICENSE-APACHE b/components/zcash_address/LICENSE-APACHE new file mode 100644 index 0000000000..1e5006dc14 --- /dev/null +++ b/components/zcash_address/LICENSE-APACHE @@ -0,0 +1,202 @@ + 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 [yyyy] [name of copyright owner] + +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/components/zcash_address/LICENSE-MIT b/components/zcash_address/LICENSE-MIT new file mode 100644 index 0000000000..9500c140cc --- /dev/null +++ b/components/zcash_address/LICENSE-MIT @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2021 Electric Coin Company + +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/components/zcash_address/README.md b/components/zcash_address/README.md new file mode 100644 index 0000000000..ead489de4d --- /dev/null +++ b/components/zcash_address/README.md @@ -0,0 +1,21 @@ +# zcash_address + +Zcash address parsing and serialization. This library allows its users to easily +recognize and give good error messages for new Zcash address types. + +## License + +Licensed under either of + + * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or + http://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you, as defined in the Apache-2.0 +license, shall be dual licensed as above, without any additional terms or +conditions. diff --git a/components/zcash_address/proptest-regressions/kind/unified.txt b/components/zcash_address/proptest-regressions/kind/unified.txt new file mode 100644 index 0000000000..f70dff62c0 --- /dev/null +++ b/components/zcash_address/proptest-regressions/kind/unified.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc e08469bc301313ef868b97a5c37d9a9746d9720c915a9127c89db25c3be778fd # shrinks to ua = Address([Sapling([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), P2pkh([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])]) diff --git a/components/zcash_address/proptest-regressions/kind/unified/address.txt b/components/zcash_address/proptest-regressions/kind/unified/address.txt new file mode 100644 index 0000000000..cda1c138ad --- /dev/null +++ b/components/zcash_address/proptest-regressions/kind/unified/address.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc fd6f22032b9add1319ad27b183de7d522bf9dfa0d6ef56354812bce5a803c11c # shrinks to network = Main, ua = Address([Orchard([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 101, 7, 48, 33, 241, 53, 105]), P2pkh([104, 47, 211, 146, 155, 136, 129, 215, 137, 152, 117, 157, 55, 4, 199, 123, 69, 12, 133, 89]), Sapling([164, 56, 210, 240, 243, 207, 59, 213, 35, 184, 250, 69, 206, 249, 184, 252, 184, 103, 227, 207, 249, 127, 133, 218, 97, 241, 242, 12, 155, 162, 137, 100, 200, 50, 96, 79, 33, 137, 242, 172, 43, 6, 255])]) diff --git a/components/zcash_address/proptest-regressions/kind/unified/fvk.txt b/components/zcash_address/proptest-regressions/kind/unified/fvk.txt new file mode 100644 index 0000000000..cb9eadae1c --- /dev/null +++ b/components/zcash_address/proptest-regressions/kind/unified/fvk.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc e104d5971b8fa530680706dab1f954d27650407285c4d78f3c8428fe20c8f008 # shrinks to network = Main, ufvk = Ufvk([Sapling([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), Orchard([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 199, 56, 71, 87, 43, 196, 81, 100, 9, 151, 208, 145, 131, 104, 86, 105, 222, 242, 35, 138, 199, 195, 23, 165, 218, 165, 79, 239, 183, 228, 111, 72, 26, 242, 158, 79, 109, 240, 47, 52, 59, 46, 164, 181, 240, 159, 234, 120, 160, 214, 6, 235, 69, 147, 88, 78, 48, 20, 53, 243, 221, 39, 208, 139, 21, 211, 238, 118, 101, 5, 77, 77, 29, 176, 157, 151, 6, 72])]) diff --git a/components/zcash_address/src/convert.rs b/components/zcash_address/src/convert.rs new file mode 100644 index 0000000000..5e0277b2bd --- /dev/null +++ b/components/zcash_address/src/convert.rs @@ -0,0 +1,415 @@ +use core::fmt; + +#[cfg(feature = "std")] +use std::error::Error; + +use zcash_protocol::consensus::NetworkType; + +use crate::{kind::*, AddressKind, ZcashAddress}; + +/// An error indicating that an address type is not supported for conversion. +#[derive(Debug)] +pub struct UnsupportedAddress(&'static str); + +impl fmt::Display for UnsupportedAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Zcash {} addresses are not supported", self.0) + } +} + +/// An error encountered while converting a parsed [`ZcashAddress`] into another type. +#[derive(Debug)] +pub enum ConversionError { + /// The address is for the wrong network. + IncorrectNetwork { + expected: NetworkType, + actual: NetworkType, + }, + /// The address type is not supported by the target type. + Unsupported(UnsupportedAddress), + /// A conversion error returned by the target type. + User(E), +} + +impl From for ConversionError { + fn from(e: E) -> Self { + ConversionError::User(e) + } +} + +impl fmt::Display for ConversionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::IncorrectNetwork { expected, actual } => write!( + f, + "Address is for {:?} but we expected {:?}", + actual, expected, + ), + Self::Unsupported(e) => e.fmt(f), + Self::User(e) => e.fmt(f), + } + } +} + +#[cfg(feature = "std")] +impl Error for UnsupportedAddress {} +#[cfg(feature = "std")] +impl Error for ConversionError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + ConversionError::IncorrectNetwork { .. } | ConversionError::Unsupported(_) => None, + ConversionError::User(e) => Some(e), + } + } +} + +/// A helper trait for converting a [`ZcashAddress`] into a network-agnostic type. +/// +/// A blanket implementation of [`TryFromAddress`] is provided for `(NetworkType, T)` where +/// `T: TryFromRawAddress`. +/// +/// [`ZcashAddress`]: crate::ZcashAddress +/// +/// # Examples +/// +/// ``` +/// use zcash_address::{ConversionError, TryFromRawAddress, UnsupportedAddress, ZcashAddress}; +/// use zcash_protocol::consensus::NetworkType; +/// +/// #[derive(Debug, PartialEq)] +/// struct MySapling([u8; 43]); +/// +/// // Implement the TryFromRawAddress trait, overriding whichever conversion methods match +/// // your requirements for the resulting type. +/// impl TryFromRawAddress for MySapling { +/// // In this example we aren't checking the validity of the inner Sapling address, +/// // but your code should do so! +/// type Error = &'static str; +/// +/// fn try_from_raw_sapling(data: [u8; 43]) -> Result> { +/// Ok(MySapling(data)) +/// } +/// } +/// +/// // For a supported address type, the conversion works. +/// let addr_string = "zs1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpq6d8g"; +/// +/// // You can use `ZcashAddress::convert_if_network` to get your type directly. +/// let addr: ZcashAddress = addr_string.parse().unwrap(); +/// let converted = addr.convert_if_network::(NetworkType::Main); +/// assert!(converted.is_ok()); +/// assert_eq!(converted.unwrap(), MySapling([0; 43])); +/// +/// // Using `ZcashAddress::convert` gives us the tuple `(network, converted_addr)`. +/// let addr: ZcashAddress = addr_string.parse().unwrap(); +/// let converted = addr.convert::<(_, MySapling)>(); +/// assert!(converted.is_ok()); +/// assert_eq!(converted.unwrap(), (NetworkType::Main, MySapling([0; 43]))); +/// +/// // For an unsupported address type, we get an error. +/// let addr: ZcashAddress = "t1Hsc1LR8yKnbbe3twRp88p6vFfC5t7DLbs".parse().unwrap(); +/// assert_eq!( +/// addr.convert::<(_, MySapling)>().unwrap_err().to_string(), +/// "Zcash transparent P2PKH addresses are not supported", +/// ); +/// ``` +pub trait TryFromRawAddress: Sized { + /// Conversion errors for the user type (e.g. failing to parse the data passed to + /// [`Self::try_from_raw_sapling`] as a valid Sapling address). + type Error; + + fn try_from_raw_sprout(data: [u8; 64]) -> Result> { + let _ = data; + Err(ConversionError::Unsupported(UnsupportedAddress("Sprout"))) + } + + fn try_from_raw_sapling(data: [u8; 43]) -> Result> { + let _ = data; + Err(ConversionError::Unsupported(UnsupportedAddress("Sapling"))) + } + + fn try_from_raw_unified(data: unified::Address) -> Result> { + let _ = data; + Err(ConversionError::Unsupported(UnsupportedAddress("Unified"))) + } + + fn try_from_raw_transparent_p2pkh( + data: [u8; 20], + ) -> Result> { + let _ = data; + Err(ConversionError::Unsupported(UnsupportedAddress( + "transparent P2PKH", + ))) + } + + fn try_from_raw_transparent_p2sh(data: [u8; 20]) -> Result> { + let _ = data; + Err(ConversionError::Unsupported(UnsupportedAddress( + "transparent P2SH", + ))) + } + + fn try_from_raw_tex(data: [u8; 20]) -> Result> { + let _ = data; + Err(ConversionError::Unsupported(UnsupportedAddress( + "transparent-source restricted P2PKH", + ))) + } +} + +/// A helper trait for converting a [`ZcashAddress`] into another type. +/// +/// [`ZcashAddress`]: crate::ZcashAddress +/// +/// # Examples +/// +/// ``` +/// use zcash_address::{ConversionError, TryFromAddress, UnsupportedAddress, ZcashAddress}; +/// use zcash_protocol::consensus::NetworkType; +/// +/// #[derive(Debug)] +/// struct MySapling([u8; 43]); +/// +/// // Implement the TryFromAddress trait, overriding whichever conversion methods match your +/// // requirements for the resulting type. +/// impl TryFromAddress for MySapling { +/// // In this example we aren't checking the validity of the inner Sapling address, +/// // but your code should do so! +/// type Error = &'static str; +/// +/// fn try_from_sapling( +/// net: NetworkType, +/// data: [u8; 43], +/// ) -> Result> { +/// Ok(MySapling(data)) +/// } +/// } +/// +/// // For a supported address type, the conversion works. +/// let addr: ZcashAddress = +/// "zs1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpq6d8g" +/// .parse() +/// .unwrap(); +/// assert!(addr.convert::().is_ok()); +/// +/// // For an unsupported address type, we get an error. +/// let addr: ZcashAddress = "t1Hsc1LR8yKnbbe3twRp88p6vFfC5t7DLbs".parse().unwrap(); +/// assert_eq!( +/// addr.convert::().unwrap_err().to_string(), +/// "Zcash transparent P2PKH addresses are not supported", +/// ); +/// ``` +pub trait TryFromAddress: Sized { + /// Conversion errors for the user type (e.g. failing to parse the data passed to + /// [`Self::try_from_sapling`] as a valid Sapling address). + type Error; + + fn try_from_sprout( + net: NetworkType, + data: [u8; 64], + ) -> Result> { + let _ = (net, data); + Err(ConversionError::Unsupported(UnsupportedAddress("Sprout"))) + } + + fn try_from_sapling( + net: NetworkType, + data: [u8; 43], + ) -> Result> { + let _ = (net, data); + Err(ConversionError::Unsupported(UnsupportedAddress("Sapling"))) + } + + fn try_from_unified( + net: NetworkType, + data: unified::Address, + ) -> Result> { + let _ = (net, data); + Err(ConversionError::Unsupported(UnsupportedAddress("Unified"))) + } + + fn try_from_transparent_p2pkh( + net: NetworkType, + data: [u8; 20], + ) -> Result> { + let _ = (net, data); + Err(ConversionError::Unsupported(UnsupportedAddress( + "transparent P2PKH", + ))) + } + + fn try_from_transparent_p2sh( + net: NetworkType, + data: [u8; 20], + ) -> Result> { + let _ = (net, data); + Err(ConversionError::Unsupported(UnsupportedAddress( + "transparent P2SH", + ))) + } + + fn try_from_tex( + net: NetworkType, + data: [u8; 20], + ) -> Result> { + let _ = (net, data); + Err(ConversionError::Unsupported(UnsupportedAddress( + "transparent-source restricted P2PKH", + ))) + } +} + +impl TryFromAddress for (NetworkType, T) { + type Error = T::Error; + + fn try_from_sprout( + net: NetworkType, + data: [u8; 64], + ) -> Result> { + T::try_from_raw_sprout(data).map(|addr| (net, addr)) + } + + fn try_from_sapling( + net: NetworkType, + data: [u8; 43], + ) -> Result> { + T::try_from_raw_sapling(data).map(|addr| (net, addr)) + } + + fn try_from_unified( + net: NetworkType, + data: unified::Address, + ) -> Result> { + T::try_from_raw_unified(data).map(|addr| (net, addr)) + } + + fn try_from_transparent_p2pkh( + net: NetworkType, + data: [u8; 20], + ) -> Result> { + T::try_from_raw_transparent_p2pkh(data).map(|addr| (net, addr)) + } + + fn try_from_transparent_p2sh( + net: NetworkType, + data: [u8; 20], + ) -> Result> { + T::try_from_raw_transparent_p2sh(data).map(|addr| (net, addr)) + } + + fn try_from_tex( + net: NetworkType, + data: [u8; 20], + ) -> Result> { + T::try_from_raw_tex(data).map(|addr| (net, addr)) + } +} + +/// A helper trait for converting another type into a [`ZcashAddress`]. +/// +/// This trait is sealed and cannot be implemented for types outside this crate. Its +/// purpose is to move these conversion functions out of the main `ZcashAddress` API +/// documentation, as they are only required when creating addresses (rather than when +/// parsing addresses, which is a more common occurrence). +/// +/// [`ZcashAddress`]: crate::ZcashAddress +/// +/// # Examples +/// +/// ``` +/// use zcash_address::{ToAddress, ZcashAddress}; +/// use zcash_protocol::consensus::NetworkType; +/// +/// #[derive(Debug)] +/// struct MySapling([u8; 43]); +/// +/// impl MySapling { +/// /// Encodes this Sapling address for the given network. +/// fn encode(&self, net: NetworkType) -> ZcashAddress { +/// ZcashAddress::from_sapling(net, self.0) +/// } +/// } +/// +/// let addr = MySapling([0; 43]); +/// let encoded = addr.encode(NetworkType::Main); +/// assert_eq!( +/// encoded.to_string(), +/// "zs1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpq6d8g", +/// ); +/// ``` +pub trait ToAddress: private::Sealed { + fn from_sprout(net: NetworkType, data: [u8; 64]) -> Self; + + fn from_sapling(net: NetworkType, data: [u8; 43]) -> Self; + + fn from_unified(net: NetworkType, data: unified::Address) -> Self; + + fn from_transparent_p2pkh(net: NetworkType, data: [u8; 20]) -> Self; + + fn from_transparent_p2sh(net: NetworkType, data: [u8; 20]) -> Self; + + fn from_tex(net: NetworkType, data: [u8; 20]) -> Self; +} + +impl ToAddress for ZcashAddress { + fn from_sprout(net: NetworkType, data: [u8; 64]) -> Self { + ZcashAddress { + net: if let NetworkType::Regtest = net { + NetworkType::Test + } else { + net + }, + kind: AddressKind::Sprout(data), + } + } + + fn from_sapling(net: NetworkType, data: [u8; 43]) -> Self { + ZcashAddress { + net, + kind: AddressKind::Sapling(data), + } + } + + fn from_unified(net: NetworkType, data: unified::Address) -> Self { + ZcashAddress { + net, + kind: AddressKind::Unified(data), + } + } + + fn from_transparent_p2pkh(net: NetworkType, data: [u8; 20]) -> Self { + ZcashAddress { + net: if let NetworkType::Regtest = net { + NetworkType::Test + } else { + net + }, + kind: AddressKind::P2pkh(data), + } + } + + fn from_transparent_p2sh(net: NetworkType, data: [u8; 20]) -> Self { + ZcashAddress { + net: if let NetworkType::Regtest = net { + NetworkType::Test + } else { + net + }, + kind: AddressKind::P2sh(data), + } + } + + fn from_tex(net: NetworkType, data: [u8; 20]) -> Self { + ZcashAddress { + net, + kind: AddressKind::Tex(data), + } + } +} + +mod private { + use crate::ZcashAddress; + + pub trait Sealed {} + impl Sealed for ZcashAddress {} +} diff --git a/components/zcash_address/src/encoding.rs b/components/zcash_address/src/encoding.rs new file mode 100644 index 0000000000..b514c21e32 --- /dev/null +++ b/components/zcash_address/src/encoding.rs @@ -0,0 +1,386 @@ +use alloc::string::String; +use alloc::vec::Vec; +use core::convert::TryInto; +use core::fmt; +use core::str::FromStr; + +#[cfg(feature = "std")] +use std::error::Error; + +use bech32::{primitives::decode::CheckedHrpstring, Bech32, Bech32m, Checksum, Hrp}; +use zcash_protocol::consensus::{NetworkConstants, NetworkType}; +use zcash_protocol::constants::{mainnet, regtest, testnet}; + +use crate::kind::unified::Encoding; +use crate::{kind::*, AddressKind, ZcashAddress}; + +/// An error while attempting to parse a string as a Zcash address. +#[derive(Debug, PartialEq, Eq)] +pub enum ParseError { + /// The string is an invalid encoding. + InvalidEncoding, + /// The string is not a Zcash address. + NotZcash, + /// Errors specific to unified addresses. + Unified(unified::ParseError), +} + +impl From for ParseError { + fn from(e: unified::ParseError) -> Self { + match e { + unified::ParseError::InvalidEncoding(_) => Self::InvalidEncoding, + unified::ParseError::UnknownPrefix(_) => Self::NotZcash, + _ => Self::Unified(e), + } + } +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ParseError::InvalidEncoding => write!(f, "Invalid encoding"), + ParseError::NotZcash => write!(f, "Not a Zcash address"), + ParseError::Unified(e) => e.fmt(f), + } + } +} + +#[cfg(feature = "std")] +impl Error for ParseError {} + +impl FromStr for ZcashAddress { + type Err = ParseError; + + /// Attempts to parse the given string as a Zcash address. + fn from_str(s: &str) -> Result { + // Remove leading and trailing whitespace, to handle copy-paste errors. + let s = s.trim(); + + // Try decoding as a unified address + match unified::Address::decode(s) { + Ok((net, data)) => { + return Ok(ZcashAddress { + net, + kind: AddressKind::Unified(data), + }); + } + Err(unified::ParseError::NotUnified | unified::ParseError::UnknownPrefix(_)) => { + // allow decoding to fall through to Sapling/TEX/Transparent + } + Err(e) => { + return Err(ParseError::from(e)); + } + } + + // Try decoding as a Sapling address (Bech32) + if let Ok(parsed) = CheckedHrpstring::new::(s) { + // If we reached this point, the encoding is found to be valid Bech32. + let net = match parsed.hrp().as_str() { + mainnet::HRP_SAPLING_PAYMENT_ADDRESS => NetworkType::Main, + testnet::HRP_SAPLING_PAYMENT_ADDRESS => NetworkType::Test, + regtest::HRP_SAPLING_PAYMENT_ADDRESS => NetworkType::Regtest, + // We will not define new Bech32 address encodings. + _ => { + return Err(ParseError::NotZcash); + } + }; + + let data = parsed.byte_iter().collect::>(); + + return data + .try_into() + .map(AddressKind::Sapling) + .map_err(|_| ParseError::InvalidEncoding) + .map(|kind| ZcashAddress { net, kind }); + } + + // Try decoding as a TEX address (Bech32m) + if let Ok(parsed) = CheckedHrpstring::new::(s) { + // If we reached this point, the encoding is found to be valid Bech32m. + let net = match parsed.hrp().as_str() { + mainnet::HRP_TEX_ADDRESS => NetworkType::Main, + testnet::HRP_TEX_ADDRESS => NetworkType::Test, + regtest::HRP_TEX_ADDRESS => NetworkType::Regtest, + // Not recognized as a Zcash address type + _ => { + return Err(ParseError::NotZcash); + } + }; + + let data = parsed.byte_iter().collect::>(); + + return data + .try_into() + .map(AddressKind::Tex) + .map_err(|_| ParseError::InvalidEncoding) + .map(|kind| ZcashAddress { net, kind }); + } + + // The rest use Base58Check. + if let Ok(decoded) = bs58::decode(s).with_check(None).into_vec() { + if decoded.len() >= 2 { + let (prefix, net) = match decoded[..2].try_into().unwrap() { + prefix @ (mainnet::B58_PUBKEY_ADDRESS_PREFIX + | mainnet::B58_SCRIPT_ADDRESS_PREFIX + | mainnet::B58_SPROUT_ADDRESS_PREFIX) => (prefix, NetworkType::Main), + prefix @ (testnet::B58_PUBKEY_ADDRESS_PREFIX + | testnet::B58_SCRIPT_ADDRESS_PREFIX + | testnet::B58_SPROUT_ADDRESS_PREFIX) => (prefix, NetworkType::Test), + // We will not define new Base58Check address encodings. + _ => return Err(ParseError::NotZcash), + }; + + return match prefix { + mainnet::B58_SPROUT_ADDRESS_PREFIX | testnet::B58_SPROUT_ADDRESS_PREFIX => { + decoded[2..].try_into().map(AddressKind::Sprout) + } + mainnet::B58_PUBKEY_ADDRESS_PREFIX | testnet::B58_PUBKEY_ADDRESS_PREFIX => { + decoded[2..].try_into().map(AddressKind::P2pkh) + } + mainnet::B58_SCRIPT_ADDRESS_PREFIX | testnet::B58_SCRIPT_ADDRESS_PREFIX => { + decoded[2..].try_into().map(AddressKind::P2sh) + } + _ => unreachable!(), + } + .map_err(|_| ParseError::InvalidEncoding) + .map(|kind| ZcashAddress { kind, net }); + } + }; + + // If it's not valid Bech32, Bech32m, or Base58Check, it's not a Zcash address. + Err(ParseError::NotZcash) + } +} + +fn encode_bech32(hrp: &str, data: &[u8]) -> String { + bech32::encode::(Hrp::parse_unchecked(hrp), data).expect("encoding is short enough") +} + +fn encode_b58(prefix: [u8; 2], data: &[u8]) -> String { + let mut bytes = Vec::with_capacity(2 + data.len()); + bytes.extend_from_slice(&prefix); + bytes.extend_from_slice(data); + bs58::encode(bytes).with_check().into_string() +} + +impl fmt::Display for ZcashAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let encoded = match &self.kind { + AddressKind::Sprout(data) => encode_b58(self.net.b58_sprout_address_prefix(), data), + AddressKind::Sapling(data) => { + encode_bech32::(self.net.hrp_sapling_payment_address(), data) + } + AddressKind::Unified(addr) => addr.encode(&self.net), + AddressKind::P2pkh(data) => encode_b58(self.net.b58_pubkey_address_prefix(), data), + AddressKind::P2sh(data) => encode_b58(self.net.b58_script_address_prefix(), data), + AddressKind::Tex(data) => encode_bech32::(self.net.hrp_tex_address(), data), + }; + write!(f, "{}", encoded) + } +} + +#[cfg(test)] +mod tests { + use alloc::string::ToString; + + use assert_matches::assert_matches; + + use super::*; + use crate::kind::unified; + use zcash_protocol::consensus::NetworkType; + + fn encoding(encoded: &str, decoded: ZcashAddress) { + assert_eq!(decoded.to_string(), encoded); + assert_eq!(encoded.parse(), Ok(decoded)); + } + + #[test] + fn sprout() { + encoding( + "zc8E5gYid86n4bo2Usdq1cpr7PpfoJGzttwBHEEgGhGkLUg7SPPVFNB2AkRFXZ7usfphup5426dt1buMmY3fkYeRrQGLa8y", + ZcashAddress { net: NetworkType::Main, kind: AddressKind::Sprout([0; 64]) }, + ); + encoding( + "ztJ1EWLKcGwF2S4NA17pAJVdco8Sdkz4AQPxt1cLTEfNuyNswJJc2BbBqYrsRZsp31xbVZwhF7c7a2L9jsF3p3ZwRWpqqyS", + ZcashAddress { net: NetworkType::Test, kind: AddressKind::Sprout([0; 64]) }, + ); + } + + #[test] + fn sapling() { + encoding( + "zs1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpq6d8g", + ZcashAddress { + net: NetworkType::Main, + kind: AddressKind::Sapling([0; 43]), + }, + ); + encoding( + "ztestsapling1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqfhgwqu", + ZcashAddress { + net: NetworkType::Test, + kind: AddressKind::Sapling([0; 43]), + }, + ); + encoding( + "zregtestsapling1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqknpr3m", + ZcashAddress { + net: NetworkType::Regtest, + kind: AddressKind::Sapling([0; 43]), + }, + ); + } + + #[test] + fn unified() { + encoding( + "u1qpatys4zruk99pg59gcscrt7y6akvl9vrhcfyhm9yxvxz7h87q6n8cgrzzpe9zru68uq39uhmlpp5uefxu0su5uqyqfe5zp3tycn0ecl", + ZcashAddress { + net: NetworkType::Main, + kind: AddressKind::Unified(unified::Address(vec![unified::address::Receiver::Sapling([0; 43])])), + }, + ); + encoding( + "utest10c5kutapazdnf8ztl3pu43nkfsjx89fy3uuff8tsmxm6s86j37pe7uz94z5jhkl49pqe8yz75rlsaygexk6jpaxwx0esjr8wm5ut7d5s", + ZcashAddress { + net: NetworkType::Test, + kind: AddressKind::Unified(unified::Address(vec![unified::address::Receiver::Sapling([0; 43])])), + }, + ); + encoding( + "uregtest15xk7vj4grjkay6mnfl93dhsflc2yeunhxwdh38rul0rq3dfhzzxgm5szjuvtqdha4t4p2q02ks0jgzrhjkrav70z9xlvq0plpcjkd5z3", + ZcashAddress { + net: NetworkType::Regtest, + kind: AddressKind::Unified(unified::Address(vec![unified::address::Receiver::Sapling([0; 43])])), + }, + ); + + let badencoded = "uinvalid1ck5navqwcng43gvsxwrxsplc22p7uzlcag6qfa0zh09e87efq6rq8wsnv25umqjjravw70rl994n5ueuhza2fghge5gl7zrl2qp6cwmp"; + assert_eq!( + badencoded.parse::(), + Err(ParseError::NotZcash) + ); + } + + #[test] + fn transparent() { + encoding( + "t1Hsc1LR8yKnbbe3twRp88p6vFfC5t7DLbs", + ZcashAddress { + net: NetworkType::Main, + kind: AddressKind::P2pkh([0; 20]), + }, + ); + encoding( + "tm9iMLAuYMzJ6jtFLcA7rzUmfreGuKvr7Ma", + ZcashAddress { + net: NetworkType::Test, + kind: AddressKind::P2pkh([0; 20]), + }, + ); + encoding( + "t3JZcvsuaXE6ygokL4XUiZSTrQBUoPYFnXJ", + ZcashAddress { + net: NetworkType::Main, + kind: AddressKind::P2sh([0; 20]), + }, + ); + encoding( + "t26YoyZ1iPgiMEWL4zGUm74eVWfhyDMXzY2", + ZcashAddress { + net: NetworkType::Test, + kind: AddressKind::P2sh([0; 20]), + }, + ); + } + + #[test] + fn tex() { + let p2pkh_str = "t1VmmGiyjVNeCjxDZzg7vZmd99WyzVby9yC"; + let tex_str = "tex1s2rt77ggv6q989lr49rkgzmh5slsksa9khdgte"; + + // Transcode P2PKH to TEX + let p2pkh_zaddr: ZcashAddress = p2pkh_str.parse().unwrap(); + assert_matches!(p2pkh_zaddr.net, NetworkType::Main); + if let AddressKind::P2pkh(zaddr_data) = p2pkh_zaddr.kind { + let tex_zaddr = ZcashAddress { + net: p2pkh_zaddr.net, + kind: AddressKind::Tex(zaddr_data), + }; + + assert_eq!(tex_zaddr.to_string(), tex_str); + } else { + panic!("Decoded address should have been a P2PKH address."); + } + + // Transcode TEX to P2PKH + let tex_zaddr: ZcashAddress = tex_str.parse().unwrap(); + assert_matches!(tex_zaddr.net, NetworkType::Main); + if let AddressKind::Tex(zaddr_data) = tex_zaddr.kind { + let p2pkh_zaddr = ZcashAddress { + net: tex_zaddr.net, + kind: AddressKind::P2pkh(zaddr_data), + }; + + assert_eq!(p2pkh_zaddr.to_string(), p2pkh_str); + } else { + panic!("Decoded address should have been a TEX address."); + } + } + + #[test] + fn tex_testnet() { + let p2pkh_str = "tm9ofD7kHR7AF8MsJomEzLqGcrLCBkD9gDj"; + let tex_str = "textest1qyqszqgpqyqszqgpqyqszqgpqyqszqgpfcjgfy"; + + // Transcode P2PKH to TEX + let p2pkh_zaddr: ZcashAddress = p2pkh_str.parse().unwrap(); + assert_matches!(p2pkh_zaddr.net, NetworkType::Test); + if let AddressKind::P2pkh(zaddr_data) = p2pkh_zaddr.kind { + let tex_zaddr = ZcashAddress { + net: p2pkh_zaddr.net, + kind: AddressKind::Tex(zaddr_data), + }; + + assert_eq!(tex_zaddr.to_string(), tex_str); + } else { + panic!("Decoded address should have been a P2PKH address."); + } + + // Transcode TEX to P2PKH + let tex_zaddr: ZcashAddress = tex_str.parse().unwrap(); + assert_matches!(tex_zaddr.net, NetworkType::Test); + if let AddressKind::Tex(zaddr_data) = tex_zaddr.kind { + let p2pkh_zaddr = ZcashAddress { + net: tex_zaddr.net, + kind: AddressKind::P2pkh(zaddr_data), + }; + + assert_eq!(p2pkh_zaddr.to_string(), p2pkh_str); + } else { + panic!("Decoded address should have been a TEX address."); + } + } + + #[test] + fn whitespace() { + assert_eq!( + " t1Hsc1LR8yKnbbe3twRp88p6vFfC5t7DLbs".parse(), + Ok(ZcashAddress { + net: NetworkType::Main, + kind: AddressKind::P2pkh([0; 20]) + }), + ); + assert_eq!( + "t1Hsc1LR8yKnbbe3twRp88p6vFfC5t7DLbs ".parse(), + Ok(ZcashAddress { + net: NetworkType::Main, + kind: AddressKind::P2pkh([0; 20]) + }), + ); + assert_eq!( + "something t1Hsc1LR8yKnbbe3twRp88p6vFfC5t7DLbs".parse::(), + Err(ParseError::NotZcash), + ); + } +} diff --git a/components/zcash_address/src/kind.rs b/components/zcash_address/src/kind.rs new file mode 100644 index 0000000000..38b4557a6e --- /dev/null +++ b/components/zcash_address/src/kind.rs @@ -0,0 +1 @@ +pub mod unified; diff --git a/components/zcash_address/src/kind/unified.rs b/components/zcash_address/src/kind/unified.rs new file mode 100644 index 0000000000..ec5c79e160 --- /dev/null +++ b/components/zcash_address/src/kind/unified.rs @@ -0,0 +1,448 @@ +//! Implementation of [ZIP 316](https://zips.z.cash/zip-0316) Unified Addresses and Viewing Keys. + +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +use core::cmp; +use core::convert::{TryFrom, TryInto}; +use core::fmt; +use core::num::TryFromIntError; + +#[cfg(feature = "std")] +use std::error::Error; + +use bech32::{primitives::decode::CheckedHrpstring, Bech32m, Checksum, Hrp}; + +use zcash_protocol::consensus::NetworkType; + +pub(crate) mod address; +pub(crate) mod fvk; +pub(crate) mod ivk; + +pub use address::{Address, Receiver}; +pub use fvk::{Fvk, Ufvk}; +pub use ivk::{Ivk, Uivk}; + +const PADDING_LEN: usize = 16; + +/// The known Receiver and Viewing Key types. +/// +/// The typecodes `0xFFFA..=0xFFFF` reserved for experiments are currently not +/// distinguished from unknown values, and will be parsed as [`Typecode::Unknown`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Typecode { + /// A transparent P2PKH address, FVK, or IVK encoding as specified in [ZIP 316](https://zips.z.cash/zip-0316). + P2pkh, + /// A transparent P2SH address. + /// + /// This typecode cannot occur in a [`Ufvk`] or [`Uivk`]. + P2sh, + /// A Sapling raw address, FVK, or IVK encoding as specified in [ZIP 316](https://zips.z.cash/zip-0316). + Sapling, + /// An Orchard raw address, FVK, or IVK encoding as specified in [ZIP 316](https://zips.z.cash/zip-0316). + Orchard, + /// An unknown or experimental typecode. + Unknown(u32), +} + +impl Typecode { + pub fn preference_order(a: &Self, b: &Self) -> cmp::Ordering { + match (a, b) { + // Trivial equality checks. + (Self::Orchard, Self::Orchard) + | (Self::Sapling, Self::Sapling) + | (Self::P2sh, Self::P2sh) + | (Self::P2pkh, Self::P2pkh) => cmp::Ordering::Equal, + + // We don't know for certain the preference order of unknown items, but it + // is likely that the higher typecode has higher preference. The exact order + // doesn't really matter, as unknown items have lower preference than + // known items. + (Self::Unknown(a), Self::Unknown(b)) => b.cmp(a), + + // For the remaining cases, we rely on `match` always choosing the first arm + // with a matching pattern. Patterns below are listed in priority order: + (Self::Orchard, _) => cmp::Ordering::Less, + (_, Self::Orchard) => cmp::Ordering::Greater, + + (Self::Sapling, _) => cmp::Ordering::Less, + (_, Self::Sapling) => cmp::Ordering::Greater, + + (Self::P2sh, _) => cmp::Ordering::Less, + (_, Self::P2sh) => cmp::Ordering::Greater, + + (Self::P2pkh, _) => cmp::Ordering::Less, + (_, Self::P2pkh) => cmp::Ordering::Greater, + } + } + + pub fn encoding_order(a: &Self, b: &Self) -> cmp::Ordering { + u32::from(*a).cmp(&u32::from(*b)) + } +} + +impl TryFrom for Typecode { + type Error = ParseError; + + fn try_from(typecode: u32) -> Result { + match typecode { + 0x00 => Ok(Typecode::P2pkh), + 0x01 => Ok(Typecode::P2sh), + 0x02 => Ok(Typecode::Sapling), + 0x03 => Ok(Typecode::Orchard), + 0x04..=0x02000000 => Ok(Typecode::Unknown(typecode)), + 0x02000001..=u32::MAX => Err(ParseError::InvalidTypecodeValue(typecode as u64)), + } + } +} + +impl From for u32 { + fn from(t: Typecode) -> Self { + match t { + Typecode::P2pkh => 0x00, + Typecode::P2sh => 0x01, + Typecode::Sapling => 0x02, + Typecode::Orchard => 0x03, + Typecode::Unknown(typecode) => typecode, + } + } +} + +impl TryFrom for usize { + type Error = TryFromIntError; + fn try_from(t: Typecode) -> Result { + u32::from(t).try_into() + } +} + +impl Typecode { + fn is_transparent(&self) -> bool { + // Unknown typecodes are treated as not transparent for the purpose of disallowing + // only-transparent UAs, which can be represented with existing address encodings. + matches!(self, Typecode::P2pkh | Typecode::P2sh) + } +} + +/// An error while attempting to parse a string as a Zcash address. +#[derive(Debug, PartialEq, Eq)] +pub enum ParseError { + /// The unified container contains both P2PKH and P2SH items. + BothP2phkAndP2sh, + /// The unified container contains a duplicated typecode. + DuplicateTypecode(Typecode), + /// The parsed typecode exceeds the maximum allowed CompactSize value. + InvalidTypecodeValue(u64), + /// The string is an invalid encoding. + InvalidEncoding(String), + /// The items in the unified container are not in typecode order. + InvalidTypecodeOrder, + /// The unified container only contains transparent items. + OnlyTransparent, + /// The string is not Bech32m encoded, and so cannot be a unified address. + NotUnified, + /// The Bech32m string has an unrecognized human-readable prefix. + UnknownPrefix(String), +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ParseError::BothP2phkAndP2sh => write!(f, "UA contains both P2PKH and P2SH items"), + ParseError::DuplicateTypecode(c) => write!(f, "Duplicate typecode {}", u32::from(*c)), + ParseError::InvalidTypecodeValue(v) => write!(f, "Typecode value out of range {}", v), + ParseError::InvalidEncoding(msg) => write!(f, "Invalid encoding: {}", msg), + ParseError::InvalidTypecodeOrder => write!(f, "Items are out of order."), + ParseError::OnlyTransparent => write!(f, "UA only contains transparent items"), + ParseError::NotUnified => write!(f, "Address is not Bech32m encoded"), + ParseError::UnknownPrefix(s) => { + write!(f, "Unrecognized Bech32m human-readable prefix: {}", s) + } + } + } +} + +#[cfg(feature = "std")] +impl Error for ParseError {} + +pub(crate) mod private { + use alloc::borrow::ToOwned; + use alloc::vec::Vec; + use core::cmp; + use core::convert::{TryFrom, TryInto}; + use core2::io::Write; + + use super::{ParseError, Typecode, PADDING_LEN}; + use zcash_encoding::CompactSize; + use zcash_protocol::consensus::NetworkType; + + /// A raw address or viewing key. + pub trait SealedItem: for<'a> TryFrom<(u32, &'a [u8]), Error = ParseError> + Clone { + fn typecode(&self) -> Typecode; + fn data(&self) -> &[u8]; + + fn preference_order(a: &Self, b: &Self) -> cmp::Ordering { + match Typecode::preference_order(&a.typecode(), &b.typecode()) { + cmp::Ordering::Equal => a.data().cmp(b.data()), + res => res, + } + } + + fn encoding_order(a: &Self, b: &Self) -> cmp::Ordering { + match Typecode::encoding_order(&a.typecode(), &b.typecode()) { + cmp::Ordering::Equal => a.data().cmp(b.data()), + res => res, + } + } + } + + /// A Unified Container containing addresses or viewing keys. + pub trait SealedContainer: super::Container + core::marker::Sized { + const MAINNET: &'static str; + const TESTNET: &'static str; + const REGTEST: &'static str; + + /// Implementations of this method should act as unchecked constructors + /// of the container type; the caller is guaranteed to check the + /// general invariants that apply to all unified containers. + fn from_inner(items: Vec) -> Self; + + fn network_hrp(network: &NetworkType) -> &'static str { + match network { + NetworkType::Main => Self::MAINNET, + NetworkType::Test => Self::TESTNET, + NetworkType::Regtest => Self::REGTEST, + } + } + + fn hrp_network(hrp: &str) -> Option { + if hrp == Self::MAINNET { + Some(NetworkType::Main) + } else if hrp == Self::TESTNET { + Some(NetworkType::Test) + } else if hrp == Self::REGTEST { + Some(NetworkType::Regtest) + } else { + None + } + } + + fn write_raw_encoding(&self, mut writer: W) { + for item in self.items_as_parsed() { + let data = item.data(); + CompactSize::write( + &mut writer, + ::from(item.typecode()).try_into().unwrap(), + ) + .unwrap(); + CompactSize::write(&mut writer, data.len()).unwrap(); + writer.write_all(data).unwrap(); + } + } + + /// Returns the jumbled padded raw encoding of this Unified Address or viewing key. + fn to_jumbled_bytes(&self, hrp: &str) -> Vec { + assert!(hrp.len() <= PADDING_LEN); + + let mut padded = Vec::new(); + self.write_raw_encoding(&mut padded); + + let mut padding = [0u8; PADDING_LEN]; + padding[0..hrp.len()].copy_from_slice(hrp.as_bytes()); + padded.write_all(&padding).unwrap(); + + f4jumble::f4jumble(&padded) + .unwrap_or_else(|e| panic!("f4jumble failed on {:?}: {}", padded, e)) + } + + /// Parse the items of the unified container. + fn parse_items>>(hrp: &str, buf: T) -> Result, ParseError> { + fn read_receiver( + mut cursor: &mut core2::io::Cursor<&[u8]>, + ) -> Result { + let typecode = CompactSize::read(&mut cursor) + .map(|v| u32::try_from(v).expect("CompactSize::read enforces MAX_SIZE limit")) + .map_err(|e| { + ParseError::InvalidEncoding(format!( + "Failed to deserialize CompactSize-encoded typecode {}", + e + )) + })?; + let length = CompactSize::read(&mut cursor).map_err(|e| { + ParseError::InvalidEncoding(format!( + "Failed to deserialize CompactSize-encoded length {}", + e + )) + })?; + let addr_end = cursor.position().checked_add(length).ok_or_else(|| { + ParseError::InvalidEncoding(format!( + "Length value {} caused an overflow error", + length + )) + })?; + let buf = cursor.get_ref(); + if (buf.len() as u64) < addr_end { + return Err(ParseError::InvalidEncoding(format!( + "Truncated: unable to read {} bytes of item data", + length + ))); + } + let result = R::try_from(( + typecode, + &buf[cursor.position() as usize..addr_end as usize], + )); + cursor.set_position(addr_end); + result + } + + // Here we allocate if necessary to get a mutable Vec to unjumble. + let mut encoded = buf.into(); + f4jumble::f4jumble_inv_mut(&mut encoded[..]).map_err(|e| { + ParseError::InvalidEncoding(format!("F4Jumble decoding failed: {}", e)) + })?; + + // Validate and strip trailing padding bytes. + if hrp.len() > 16 { + return Err(ParseError::InvalidEncoding( + "Invalid human-readable part".to_owned(), + )); + } + let mut expected_padding = [0; PADDING_LEN]; + expected_padding[0..hrp.len()].copy_from_slice(hrp.as_bytes()); + let encoded = match encoded.split_at(encoded.len() - PADDING_LEN) { + (encoded, tail) if tail == expected_padding => Ok(encoded), + _ => Err(ParseError::InvalidEncoding( + "Invalid padding bytes".to_owned(), + )), + }?; + + let mut cursor = core2::io::Cursor::new(encoded); + let mut result = vec![]; + while cursor.position() < encoded.len().try_into().unwrap() { + result.push(read_receiver(&mut cursor)?); + } + assert_eq!(cursor.position(), encoded.len().try_into().unwrap()); + + Ok(result) + } + + /// A private function that constructs a unified container with the + /// specified items, which must be in ascending typecode order. + fn try_from_items_internal(items: Vec) -> Result { + assert!(u32::from(Typecode::P2sh) == u32::from(Typecode::P2pkh) + 1); + + let mut only_transparent = true; + let mut prev_code = None; // less than any Some + for item in &items { + let t = item.typecode(); + let t_code = Some(u32::from(t)); + if t_code < prev_code { + return Err(ParseError::InvalidTypecodeOrder); + } else if t_code == prev_code { + return Err(ParseError::DuplicateTypecode(t)); + } else if t == Typecode::P2sh && prev_code == Some(u32::from(Typecode::P2pkh)) { + // P2pkh and P2sh can only be in that order and next to each other, + // otherwise we would detect an out-of-order or duplicate typecode. + return Err(ParseError::BothP2phkAndP2sh); + } else { + prev_code = t_code; + only_transparent = only_transparent && t.is_transparent(); + } + } + + if only_transparent { + Err(ParseError::OnlyTransparent) + } else { + // All checks pass! + Ok(Self::from_inner(items)) + } + } + + fn parse_internal>>(hrp: &str, buf: T) -> Result { + Self::parse_items(hrp, buf).and_then(Self::try_from_items_internal) + } + } +} + +use private::SealedItem; + +/// The bech32m checksum algorithm, defined in [BIP-350], extended to allow all lengths +/// supported by [ZIP 316]. +/// +/// [BIP-350]: https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki +/// [ZIP 316]: https://zips.z.cash/zip-0316#solution +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Bech32mZip316 {} +impl Checksum for Bech32mZip316 { + type MidstateRepr = ::MidstateRepr; + // l^MAX from ZIP 316. + const CODE_LENGTH: usize = 4194368; + const CHECKSUM_LENGTH: usize = Bech32m::CHECKSUM_LENGTH; + const GENERATOR_SH: [u32; 5] = Bech32m::GENERATOR_SH; + const TARGET_RESIDUE: u32 = Bech32m::TARGET_RESIDUE; +} + +/// Trait providing common encoding and decoding logic for Unified containers. +pub trait Encoding: private::SealedContainer { + /// Constructs a value of a unified container type from a vector + /// of container items, sorted according to typecode as specified + /// in ZIP 316. + /// + /// This function will return an error in the case that the following ZIP 316 + /// invariants concerning the composition of a unified container are + /// violated: + /// * the item list may not contain two items having the same typecode + /// * the item list may not contain only transparent items (or no items) + /// * the item list may not contain both P2PKH and P2SH items. + fn try_from_items(mut items: Vec) -> Result { + items.sort_unstable_by(Self::Item::encoding_order); + Self::try_from_items_internal(items) + } + + /// Decodes a unified container from its string representation, preserving + /// the order of its components so that it correctly obeys round-trip + /// serialization invariants. + fn decode(s: &str) -> Result<(NetworkType, Self), ParseError> { + if let Ok(parsed) = CheckedHrpstring::new::(s) { + let hrp = parsed.hrp(); + let hrp = hrp.as_str(); + // validate that the HRP corresponds to a known network. + let net = + Self::hrp_network(hrp).ok_or_else(|| ParseError::UnknownPrefix(hrp.to_string()))?; + + let data = parsed.byte_iter().collect::>(); + + Self::parse_internal(hrp, data).map(|value| (net, value)) + } else { + Err(ParseError::NotUnified) + } + } + + /// Encodes the contents of the unified container to its string representation + /// using the correct constants for the specified network, preserving the + /// ordering of the contained items such that it correctly obeys round-trip + /// serialization invariants. + fn encode(&self, network: &NetworkType) -> String { + let hrp = Self::network_hrp(network); + bech32::encode::(Hrp::parse_unchecked(hrp), &self.to_jumbled_bytes(hrp)) + .expect("F4Jumble ensures length is short enough by construction") + } +} + +/// Trait for Unified containers, that exposes the items within them. +pub trait Container { + /// The type of item in this unified container. + type Item: SealedItem; + + /// Returns the items contained within this container, sorted in preference order. + fn items(&self) -> Vec { + let mut items = self.items_as_parsed().to_vec(); + // Unstable sorting is fine, because all items are guaranteed by construction + // to have distinct typecodes. + items.sort_unstable_by(Self::Item::preference_order); + items + } + + /// Returns the items in the order they were parsed from the string encoding. + /// + /// This API is for advanced usage; in most cases you should use `Self::items`. + fn items_as_parsed(&self) -> &[Self::Item]; +} diff --git a/components/zcash_address/src/kind/unified/address.rs b/components/zcash_address/src/kind/unified/address.rs new file mode 100644 index 0000000000..41be7ab992 --- /dev/null +++ b/components/zcash_address/src/kind/unified/address.rs @@ -0,0 +1,437 @@ +use zcash_protocol::{constants, PoolType}; + +use super::{private::SealedItem, ParseError, Typecode}; + +use alloc::vec::Vec; +use core::convert::{TryFrom, TryInto}; + +/// The set of known Receivers for Unified Addresses. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum Receiver { + Orchard([u8; 43]), + Sapling([u8; 43]), + P2pkh([u8; 20]), + P2sh([u8; 20]), + Unknown { typecode: u32, data: Vec }, +} + +impl TryFrom<(u32, &[u8])> for Receiver { + type Error = ParseError; + + fn try_from((typecode, addr): (u32, &[u8])) -> Result { + match typecode.try_into()? { + Typecode::P2pkh => addr.try_into().map(Receiver::P2pkh), + Typecode::P2sh => addr.try_into().map(Receiver::P2sh), + Typecode::Sapling => addr.try_into().map(Receiver::Sapling), + Typecode::Orchard => addr.try_into().map(Receiver::Orchard), + Typecode::Unknown(_) => Ok(Receiver::Unknown { + typecode, + data: addr.to_vec(), + }), + } + .map_err(|e| { + ParseError::InvalidEncoding(format!("Invalid address for typecode {}: {}", typecode, e)) + }) + } +} + +impl SealedItem for Receiver { + fn typecode(&self) -> Typecode { + match self { + Receiver::P2pkh(_) => Typecode::P2pkh, + Receiver::P2sh(_) => Typecode::P2sh, + Receiver::Sapling(_) => Typecode::Sapling, + Receiver::Orchard(_) => Typecode::Orchard, + Receiver::Unknown { typecode, .. } => Typecode::Unknown(*typecode), + } + } + + fn data(&self) -> &[u8] { + match self { + Receiver::P2pkh(data) => data, + Receiver::P2sh(data) => data, + Receiver::Sapling(data) => data, + Receiver::Orchard(data) => data, + Receiver::Unknown { data, .. } => data, + } + } +} + +/// A Unified Address. +/// +/// # Examples +/// +/// ``` +/// # use core::convert::Infallible; +/// use zcash_address::{ +/// unified::{self, Container, Encoding}, +/// ConversionError, TryFromRawAddress, ZcashAddress, +/// }; +/// +/// # #[cfg(not(feature = "std"))] +/// # fn main() {} +/// # #[cfg(feature = "std")] +/// # fn main() -> Result<(), Box> { +/// # let address_from_user = || "u1pg2aaph7jp8rpf6yhsza25722sg5fcn3vaca6ze27hqjw7jvvhhuxkpcg0ge9xh6drsgdkda8qjq5chpehkcpxf87rnjryjqwymdheptpvnljqqrjqzjwkc2ma6hcq666kgwfytxwac8eyex6ndgr6ezte66706e3vaqrd25dzvzkc69kw0jgywtd0cmq52q5lkw6uh7hyvzjse8ksx"; +/// let example_ua: &str = address_from_user(); +/// +/// // We can parse this directly as a `unified::Address`: +/// let (network, ua) = unified::Address::decode(example_ua)?; +/// +/// // Or we can parse via `ZcashAddress` (which you should do): +/// struct MyUnifiedAddress(unified::Address); +/// impl TryFromRawAddress for MyUnifiedAddress { +/// // In this example we aren't checking the validity of the +/// // inner Unified Address, but your code should do so! +/// type Error = Infallible; +/// +/// fn try_from_raw_unified(ua: unified::Address) -> Result> { +/// Ok(MyUnifiedAddress(ua)) +/// } +/// } +/// let addr: ZcashAddress = example_ua.parse()?; +/// let parsed = addr.convert_if_network::(network)?; +/// assert_eq!(parsed.0, ua); +/// +/// // We can obtain the receivers for the UA in preference order +/// // (the order in which wallets should prefer to use them): +/// let receivers: Vec = ua.items(); +/// +/// // And we can create the UA from a list of receivers: +/// let new_ua = unified::Address::try_from_items(receivers)?; +/// assert_eq!(new_ua, ua); +/// # Ok(()) +/// # } +/// ``` +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Address(pub(crate) Vec); + +impl Address { + /// Returns whether this address has the ability to receive transfers of the given pool type. + pub fn has_receiver_of_type(&self, pool_type: PoolType) -> bool { + self.0.iter().any(|r| match r { + Receiver::Orchard(_) => pool_type == PoolType::ORCHARD, + Receiver::Sapling(_) => pool_type == PoolType::SAPLING, + Receiver::P2pkh(_) | Receiver::P2sh(_) => pool_type == PoolType::TRANSPARENT, + Receiver::Unknown { .. } => false, + }) + } + + /// Returns whether this address contains the given receiver. + pub fn contains_receiver(&self, receiver: &Receiver) -> bool { + self.0.contains(receiver) + } + + /// Returns whether this address can receive a memo. + pub fn can_receive_memo(&self) -> bool { + self.0 + .iter() + .any(|r| matches!(r, Receiver::Sapling(_) | Receiver::Orchard(_))) + } +} + +impl super::private::SealedContainer for Address { + /// The HRP for a Bech32m-encoded mainnet Unified Address. + /// + /// Defined in [ZIP 316][zip-0316]. + /// + /// [zip-0316]: https://zips.z.cash/zip-0316 + const MAINNET: &'static str = constants::mainnet::HRP_UNIFIED_ADDRESS; + + /// The HRP for a Bech32m-encoded testnet Unified Address. + /// + /// Defined in [ZIP 316][zip-0316]. + /// + /// [zip-0316]: https://zips.z.cash/zip-0316 + const TESTNET: &'static str = constants::testnet::HRP_UNIFIED_ADDRESS; + + /// The HRP for a Bech32m-encoded regtest Unified Address. + const REGTEST: &'static str = constants::regtest::HRP_UNIFIED_ADDRESS; + + fn from_inner(receivers: Vec) -> Self { + Self(receivers) + } +} + +impl super::Encoding for Address {} +impl super::Container for Address { + type Item = Receiver; + + fn items_as_parsed(&self) -> &[Receiver] { + &self.0 + } +} + +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing { + use alloc::vec::Vec; + + use proptest::{ + array::{uniform11, uniform20, uniform32}, + collection::vec, + prelude::*, + sample::select, + strategy::Strategy, + }; + use zcash_encoding::MAX_COMPACT_SIZE; + + use super::{Address, Receiver}; + use crate::unified::Typecode; + + prop_compose! { + fn uniform43()(a in uniform11(0u8..), b in uniform32(0u8..)) -> [u8; 43] { + let mut c = [0; 43]; + c[..11].copy_from_slice(&a); + c[11..].copy_from_slice(&b); + c + } + } + + /// A strategy to generate an arbitrary transparent typecode. + pub fn arb_transparent_typecode() -> impl Strategy { + select(vec![Typecode::P2pkh, Typecode::P2sh]) + } + + /// A strategy to generate an arbitrary shielded (Sapling, Orchard, or unknown) typecode. + pub fn arb_shielded_typecode() -> impl Strategy { + prop_oneof![ + Just(Typecode::Sapling), + Just(Typecode::Orchard), + ((::from(Typecode::Orchard) + 1)..MAX_COMPACT_SIZE).prop_map(Typecode::Unknown) + ] + } + + /// A strategy to generate an arbitrary valid set of typecodes without + /// duplication and containing only one of P2sh and P2pkh transparent + /// typecodes. The resulting vector will be sorted in encoding order. + pub fn arb_typecodes() -> impl Strategy> { + prop::option::of(arb_transparent_typecode()).prop_flat_map(|transparent| { + prop::collection::hash_set(arb_shielded_typecode(), 1..4).prop_map(move |xs| { + let mut typecodes: Vec<_> = xs.into_iter().chain(transparent).collect(); + typecodes.sort_unstable_by(Typecode::encoding_order); + typecodes + }) + }) + } + + /// Generates an arbitrary Unified address containing receivers corresponding to the provided + /// set of typecodes. The receivers of this address are likely to not represent valid protocol + /// receivers, and should only be used for testing parsing and/or encoding functions that do + /// not concern themselves with the validity of the underlying receivers. + pub fn arb_unified_address_for_typecodes( + typecodes: Vec, + ) -> impl Strategy> { + typecodes + .into_iter() + .map(|tc| match tc { + Typecode::P2pkh => uniform20(0u8..).prop_map(Receiver::P2pkh).boxed(), + Typecode::P2sh => uniform20(0u8..).prop_map(Receiver::P2sh).boxed(), + Typecode::Sapling => uniform43().prop_map(Receiver::Sapling).boxed(), + Typecode::Orchard => uniform43().prop_map(Receiver::Orchard).boxed(), + Typecode::Unknown(typecode) => vec(any::(), 32..256) + .prop_map(move |data| Receiver::Unknown { typecode, data }) + .boxed(), + }) + .collect::>() + } + + /// Generates an arbitrary Unified address. The receivers of this address are likely to not + /// represent valid protocol receivers, and should only be used for testing parsing and/or + /// encoding functions that do not concern themselves with the validity of the underlying + /// receivers. + pub fn arb_unified_address() -> impl Strategy { + arb_typecodes() + .prop_flat_map(arb_unified_address_for_typecodes) + .prop_map(Address) + } +} + +#[cfg(any(test, feature = "test-dependencies"))] +pub mod test_vectors; + +#[cfg(test)] +mod tests { + use alloc::borrow::ToOwned; + + use assert_matches::assert_matches; + use zcash_protocol::consensus::NetworkType; + + use crate::{ + kind::unified::{private::SealedContainer, Container, Encoding}, + unified::address::testing::arb_unified_address, + }; + + use proptest::{prelude::*, sample::select}; + + use super::{Address, ParseError, Receiver, Typecode}; + + proptest! { + #[test] + fn ua_roundtrip( + network in select(vec![NetworkType::Main, NetworkType::Test, NetworkType::Regtest]), + ua in arb_unified_address(), + ) { + let encoded = ua.encode(&network); + let decoded = Address::decode(&encoded); + prop_assert_eq!(&decoded, &Ok((network, ua))); + let reencoded = decoded.unwrap().1.encode(&network); + prop_assert_eq!(reencoded, encoded); + } + } + + #[test] + fn padding() { + // The test cases below use `Address(vec![Receiver::Orchard([1; 43])])` as base. + + // Invalid padding ([0xff; 16] instead of [0x75, 0x00, 0x00, 0x00...]) + let invalid_padding = [ + 0xe6, 0x59, 0xd1, 0xed, 0xf7, 0x4b, 0xe3, 0x5e, 0x5a, 0x54, 0x0e, 0x41, 0x5d, 0x2f, + 0x0c, 0x0d, 0x33, 0x42, 0xbd, 0xbe, 0x9f, 0x82, 0x62, 0x01, 0xc1, 0x1b, 0xd4, 0x1e, + 0x42, 0x47, 0x86, 0x23, 0x05, 0x4b, 0x98, 0xd7, 0x76, 0x86, 0xa5, 0xe3, 0x1b, 0xd3, + 0x03, 0xca, 0x24, 0x44, 0x8e, 0x72, 0xc1, 0x4a, 0xc6, 0xbf, 0x3f, 0x2b, 0xce, 0xa7, + 0x7b, 0x28, 0x69, 0xc9, 0x84, + ]; + assert_eq!( + Address::parse_internal(Address::MAINNET, &invalid_padding[..]), + Err(ParseError::InvalidEncoding( + "Invalid padding bytes".to_owned() + )) + ); + + // Short padding (padded to 15 bytes instead of 16) + let truncated_padding = [ + 0x9a, 0x56, 0x12, 0xa3, 0x43, 0x45, 0xe0, 0x82, 0x6c, 0xac, 0x24, 0x8b, 0x3b, 0x45, + 0x72, 0x9a, 0x53, 0xd5, 0xf8, 0xda, 0xec, 0x07, 0x7c, 0xba, 0x9f, 0xa8, 0xd2, 0x97, + 0x5b, 0xda, 0x73, 0x1b, 0xd2, 0xd1, 0x32, 0x6b, 0x7b, 0x36, 0xdd, 0x57, 0x84, 0x2a, + 0xa0, 0x21, 0x23, 0x89, 0x73, 0x85, 0xe1, 0x4b, 0x3e, 0x95, 0xb7, 0xd4, 0x67, 0xbc, + 0x4b, 0x31, 0xee, 0x5a, + ]; + assert_eq!( + Address::parse_internal(Address::MAINNET, &truncated_padding[..]), + Err(ParseError::InvalidEncoding( + "Invalid padding bytes".to_owned() + )) + ); + } + + #[test] + fn truncated() { + // The test cases below start from an encoding of + // `Address(vec![Receiver::Orchard([1; 43]), Receiver::Sapling([2; 43])])` + // with the receiver data truncated, but valid padding. + + // - Missing the last data byte of the Sapling receiver. + let truncated_sapling_data = [ + 0xaa, 0xb0, 0x6e, 0x7b, 0x26, 0x7a, 0x22, 0x17, 0x39, 0xfa, 0x07, 0x69, 0xe9, 0x32, + 0x2b, 0xac, 0x8c, 0x9e, 0x5e, 0x8a, 0xd9, 0x24, 0x06, 0x5a, 0x13, 0x79, 0x3a, 0x8d, + 0xb4, 0x52, 0xfa, 0x18, 0x4e, 0x33, 0x4d, 0x8c, 0x17, 0x77, 0x4d, 0x63, 0x69, 0x34, + 0x22, 0x70, 0x3a, 0xea, 0x30, 0x82, 0x5a, 0x6b, 0x37, 0xd1, 0x0d, 0xbe, 0x20, 0xab, + 0x82, 0x86, 0x98, 0x34, 0x6a, 0xd8, 0x45, 0x40, 0xd0, 0x25, 0x60, 0xbf, 0x1e, 0xb6, + 0xeb, 0x06, 0x85, 0x70, 0x4c, 0x42, 0xbc, 0x19, 0x14, 0xef, 0x7a, 0x05, 0xa0, 0x71, + 0xb2, 0x63, 0x80, 0xbb, 0xdc, 0x12, 0x08, 0x48, 0x28, 0x8f, 0x1c, 0x9e, 0xc3, 0x42, + 0xc6, 0x5e, 0x68, 0xa2, 0x78, 0x6c, 0x9e, + ]; + assert_matches!( + Address::parse_internal(Address::MAINNET, &truncated_sapling_data[..]), + Err(ParseError::InvalidEncoding(_)) + ); + + // - Truncated after the typecode of the Sapling receiver. + let truncated_after_sapling_typecode = [ + 0x87, 0x7a, 0xdf, 0x79, 0x6b, 0xe3, 0xb3, 0x40, 0xef, 0xe4, 0x5d, 0xc2, 0x91, 0xa2, + 0x81, 0xfc, 0x7d, 0x76, 0xbb, 0xb0, 0x58, 0x98, 0x53, 0x59, 0xd3, 0x3f, 0xbc, 0x4b, + 0x86, 0x59, 0x66, 0x62, 0x75, 0x92, 0xba, 0xcc, 0x31, 0x1e, 0x60, 0x02, 0x3b, 0xd8, + 0x4c, 0xdf, 0x36, 0xa1, 0xac, 0x82, 0x57, 0xed, 0x0c, 0x98, 0x49, 0x8f, 0x49, 0x7e, + 0xe6, 0x70, 0x36, 0x5b, 0x7b, 0x9e, + ]; + assert_matches!( + Address::parse_internal(Address::MAINNET, &truncated_after_sapling_typecode[..]), + Err(ParseError::InvalidEncoding(_)) + ); + } + + #[test] + fn duplicate_typecode() { + // Construct and serialize an invalid UA. This must be done using private + // methods, as the public API does not permit construction of such invalid values. + let ua = Address(vec![Receiver::Sapling([1; 43]), Receiver::Sapling([2; 43])]); + let encoded = ua.to_jumbled_bytes(Address::MAINNET); + assert_eq!( + Address::parse_internal(Address::MAINNET, &encoded[..]), + Err(ParseError::DuplicateTypecode(Typecode::Sapling)) + ); + } + + #[test] + fn p2pkh_and_p2sh() { + // Construct and serialize an invalid UA. This must be done using private + // methods, as the public API does not permit construction of such invalid values. + let ua = Address(vec![Receiver::P2pkh([0; 20]), Receiver::P2sh([0; 20])]); + let encoded = ua.to_jumbled_bytes(Address::MAINNET); + // ensure that decoding catches the error + assert_eq!( + Address::parse_internal(Address::MAINNET, &encoded[..]), + Err(ParseError::BothP2phkAndP2sh) + ); + } + + #[test] + fn addresses_out_of_order() { + // Construct and serialize an invalid UA. This must be done using private + // methods, as the public API does not permit construction of such invalid values. + let ua = Address(vec![Receiver::Sapling([0; 43]), Receiver::P2pkh([0; 20])]); + let encoded = ua.to_jumbled_bytes(Address::MAINNET); + // ensure that decoding catches the error + assert_eq!( + Address::parse_internal(Address::MAINNET, &encoded[..]), + Err(ParseError::InvalidTypecodeOrder) + ); + } + + #[test] + fn only_transparent() { + // Encoding of `Address(vec![Receiver::P2pkh([0; 20])])`. + let encoded = [ + 0xf0, 0x9e, 0x9d, 0x6e, 0xf5, 0xa6, 0xac, 0x16, 0x50, 0xf0, 0xdb, 0xe1, 0x2c, 0xa5, + 0x36, 0x22, 0xa2, 0x04, 0x89, 0x86, 0xe9, 0x6a, 0x9b, 0xf3, 0xff, 0x6d, 0x2f, 0xe6, + 0xea, 0xdb, 0xc5, 0x20, 0x62, 0xf9, 0x6f, 0xa9, 0x86, 0xcc, + ]; + + // We can't actually exercise this error, because at present the only transparent + // receivers we can use are P2PKH and P2SH (which cannot be used together), and + // with only one of them we don't have sufficient data for F4Jumble (so we hit a + // different error). + assert_matches!( + Address::parse_internal(Address::MAINNET, &encoded[..]), + Err(ParseError::InvalidEncoding(_)) + ); + } + + #[test] + fn receivers_are_sorted() { + // Construct a UA with receivers in an unsorted order. + let ua = Address(vec![ + Receiver::P2pkh([0; 20]), + Receiver::Orchard([0; 43]), + Receiver::Unknown { + typecode: 0xff, + data: vec![], + }, + Receiver::Sapling([0; 43]), + ]); + + // `Address::receivers` sorts the receivers in priority order. + assert_eq!( + ua.items(), + vec![ + Receiver::Orchard([0; 43]), + Receiver::Sapling([0; 43]), + Receiver::P2pkh([0; 20]), + Receiver::Unknown { + typecode: 0xff, + data: vec![], + }, + ] + ) + } +} diff --git a/components/zcash_address/src/kind/unified/address/test_vectors.rs b/components/zcash_address/src/kind/unified/address/test_vectors.rs new file mode 100644 index 0000000000..973bc09dcf --- /dev/null +++ b/components/zcash_address/src/kind/unified/address/test_vectors.rs @@ -0,0 +1,1109 @@ +pub struct TestVector { + pub p2pkh_bytes: Option<[u8; 20]>, + pub p2sh_bytes: Option<[u8; 20]>, + pub sapling_raw_addr: Option<[u8; 43]>, + pub orchard_raw_addr: Option<[u8; 43]>, + pub unknown_typecode: Option, + pub unknown_bytes: Option<&'static [u8]>, + pub unified_addr: &'static str, + pub root_seed: [u8; 32], + pub account: u32, + pub diversifier_index: u32, +} + +// From https://github.com/zcash-hackworks/zcash-test-vectors/blob/master/unified_address.py +#[cfg(any(test, feature = "test-dependencies"))] +pub const TEST_VECTORS: &[TestVector] = &[ + TestVector { + p2pkh_bytes: Some([ + 0x7b, 0xb8, 0x35, 0x70, 0xb8, 0xfa, 0xe1, 0x46, 0xe0, 0x3c, 0x53, 0x31, 0xa0, 0x20, 0xb1, 0xe0, 0x89, 0x2f, 0x63, 0x1d + ]), + p2sh_bytes: None, + sapling_raw_addr: Some([ + 0xd8, 0xef, 0x82, 0x93, 0xd2, 0x6d, 0xe8, 0x32, 0xe7, 0x19, 0x3f, 0x29, 0x6b, 0xa1, 0x92, 0x2d, 0x90, 0xf1, 0x22, 0xc6, 0x13, 0x5b, 0xc2, 0x31, 0xee, 0xbd, 0x91, 0xef, 0xdb, 0x03, 0xb1, 0xa8, 0x60, 0x67, 0x71, 0xcd, 0x4f, 0xd6, 0x48, 0x05, 0x74, 0xd4, 0x3e + ]), + orchard_raw_addr: None, + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1l8xunezsvhq8fgzfl7404m450nwnd76zshscn6nfys7vyz2ywyh4cc5daaq0c7q2su5lqfh23sp7fkf3kt27ve5948mzpfdvckzaect2jtte308mkwlycj2u0eac077wu70vqcetkxf", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 0, + diversifier_index: 0, + }, + TestVector { + p2pkh_bytes: Some([ + 0xa7, 0x24, 0x4a, 0x36, 0x2f, 0x49, 0xf2, 0x96, 0x44, 0xa9, 0x55, 0xcf, 0x00, 0x39, 0xb8, 0x8a, 0x61, 0x65, 0x78, 0x61 + ]), + p2sh_bytes: None, + sapling_raw_addr: Some([ + 0x43, 0x5b, 0x0b, 0xbc, 0x95, 0xb5, 0xb7, 0xd5, 0x25, 0x31, 0xa3, 0x94, 0x4f, 0x2b, 0x85, 0x60, 0x3e, 0xe2, 0x2a, 0xaf, 0x85, 0x09, 0x63, 0xbc, 0x15, 0x6e, 0xb5, 0x61, 0xed, 0xf2, 0xcb, 0xe7, 0xcf, 0x0e, 0x77, 0x0e, 0x39, 0x3a, 0xe5, 0xd7, 0x04, 0x90, 0x26 + ]), + orchard_raw_addr: None, + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1fl5mprj0t9p4jg92hjjy8q5myvwc60c9wv0xachauqpn3c3k4xwzlaueafq27dcg7tzzzaz5jl8tyj93wgs983y0jq0qfhzu6n4r8rakpv5f4gg2lrw4z6pyqqcrcqx04d38yunc6je", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 0, + diversifier_index: 3, + }, + TestVector { + p2pkh_bytes: Some([ + 0xe2, 0x56, 0xdc, 0xb0, 0x3e, 0x05, 0xdd, 0xe7, 0xc9, 0x12, 0x12, 0xb4, 0x7a, 0x74, 0x61, 0x31, 0x1c, 0x41, 0x50, 0x59 + ]), + p2sh_bytes: None, + sapling_raw_addr: Some([ + 0x69, 0xa2, 0x5a, 0x38, 0x69, 0x97, 0x08, 0xe5, 0xf6, 0xe7, 0x6e, 0x54, 0xe6, 0xa7, 0xa2, 0xab, 0x84, 0xdc, 0xf2, 0x88, 0xdf, 0x0d, 0x1f, 0x25, 0x63, 0x67, 0x01, 0x68, 0xd6, 0xc4, 0x4a, 0xce, 0x0e, 0xf1, 0x11, 0x55, 0xc6, 0x0d, 0x5c, 0x22, 0x5e, 0x9d, 0xec + ]), + orchard_raw_addr: None, + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1qxqf8ctkxlsdh7xdcgkdtyw4mku7dxma8tsz45xd6ttgs322gdk7kazg3sdn52z7na3tzcrzf7lt3xrdtfp9d4pccderalchvvxk8hghduxrky5guzqlw65fmgp6x7aj4k8v5jkgwuw", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 0, + diversifier_index: 4, + }, + TestVector { + p2pkh_bytes: Some([ + 0xca, 0xd2, 0x68, 0x75, 0x8c, 0x5e, 0x71, 0x49, 0x30, 0x66, 0x44, 0x6b, 0x98, 0xe7, 0x1d, 0xf9, 0xd1, 0xd6, 0xa5, 0xca + ]), + p2sh_bytes: None, + sapling_raw_addr: Some([ + 0x9f, 0x6e, 0x0b, 0xf9, 0x0a, 0x18, 0xfc, 0x0b, 0x9b, 0x83, 0xae, 0x9f, 0x23, 0xad, 0x43, 0x58, 0x64, 0x86, 0x38, 0x48, 0x2b, 0x5d, 0xef, 0x89, 0x75, 0x63, 0x5b, 0x66, 0xfd, 0x8a, 0x70, 0x83, 0x35, 0xf9, 0x23, 0x5a, 0x31, 0x86, 0xec, 0x0f, 0x03, 0x3f, 0x84 + ]), + orchard_raw_addr: Some([ + 0xce, 0xcb, 0xe5, 0xe6, 0x89, 0xa4, 0x53, 0xa3, 0xfe, 0x10, 0xcc, 0xf7, 0x61, 0x7e, 0x6c, 0x1f, 0xb3, 0x82, 0x81, 0x9d, 0x7f, 0xc9, 0x20, 0x0a, 0x1f, 0x42, 0x09, 0x2a, 0xc8, 0x4a, 0x30, 0x37, 0x8f, 0x8c, 0x1f, 0xb9, 0x0d, 0xff, 0x71, 0xa6, 0xd5, 0x04, 0x2d + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1pg2aaph7jp8rpf6yhsza25722sg5fcn3vaca6ze27hqjw7jvvhhuxkpcg0ge9xh6drsgdkda8qjq5chpehkcpxf87rnjryjqwymdheptpvnljqqrjqzjwkc2ma6hcq666kgwfytxwac8eyex6ndgr6ezte66706e3vaqrd25dzvzkc69kw0jgywtd0cmq52q5lkw6uh7hyvzjse8ksx", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 1, + diversifier_index: 3, + }, + TestVector { + p2pkh_bytes: Some([ + 0x8d, 0x65, 0x33, 0x47, 0xa0, 0xfd, 0x3c, 0xd0, 0x84, 0x2a, 0x79, 0x0a, 0x5e, 0xaf, 0x89, 0xd8, 0xe3, 0x85, 0x46, 0x59 + ]), + p2sh_bytes: None, + sapling_raw_addr: Some([ + 0xe1, 0xad, 0xf1, 0x56, 0xa0, 0x7d, 0x56, 0xbc, 0xac, 0x91, 0xbd, 0xb2, 0xf7, 0xbb, 0x3e, 0xa7, 0xc4, 0x45, 0x69, 0xdc, 0xfe, 0xe5, 0x42, 0x73, 0xc0, 0x9e, 0x80, 0x65, 0x80, 0x7b, 0x68, 0x23, 0xfa, 0xa9, 0x4a, 0x77, 0x21, 0x95, 0x54, 0xd0, 0xf6, 0xe0, 0x17 + ]), + orchard_raw_addr: Some([ + 0x24, 0xf8, 0xa6, 0x0c, 0xbd, 0x97, 0xe0, 0x12, 0x61, 0x8d, 0x56, 0x05, 0x4a, 0xd3, 0x92, 0x41, 0x41, 0x1a, 0x28, 0xfd, 0xd5, 0x0e, 0xe3, 0x5e, 0xfa, 0x91, 0x15, 0x2f, 0x60, 0xd5, 0xfa, 0x21, 0x17, 0x2e, 0x5d, 0x45, 0x8d, 0xdb, 0xcb, 0x6b, 0x70, 0x98, 0x96 + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u19mzuf4l37ny393m59v4mxx4t3uyxkh7qpqjdfvlfk9f504cv9w4fpl7cql0kqvssz8jay8mgl8lnrtvg6yzh9pranjj963acc3h2z2qt7007du0lsmdf862dyy40c3wmt0kq35k5z836tfljgzsqtdsccchayfjpygqzkx24l77ga3ngfgskqddyepz8we7ny4ggmt7q48cgvgu57mz", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 1, + diversifier_index: 7, + }, + TestVector { + p2pkh_bytes: Some([ + 0xe5, 0x11, 0xf4, 0x39, 0xb5, 0xf9, 0x6c, 0xf8, 0x24, 0xcd, 0x5e, 0x0e, 0x6b, 0x2e, 0xb8, 0xee, 0x1b, 0xc8, 0x3c, 0xb7 + ]), + p2sh_bytes: None, + sapling_raw_addr: Some([ + 0x60, 0xba, 0x57, 0x2f, 0x8e, 0x37, 0x93, 0x12, 0xd8, 0x68, 0x97, 0x02, 0x5d, 0xec, 0xdd, 0x64, 0xb4, 0xb9, 0x5e, 0x2c, 0x4a, 0xfa, 0x9d, 0x13, 0x72, 0x6b, 0x8c, 0xc3, 0x93, 0xed, 0xb4, 0x98, 0x8c, 0x51, 0xb9, 0x76, 0x02, 0x8f, 0x89, 0x0f, 0x10, 0x8b, 0xd2 + ]), + orchard_raw_addr: Some([ + 0x1f, 0x24, 0x29, 0x4e, 0xd1, 0xb4, 0x05, 0xc7, 0xb3, 0xb1, 0xc3, 0xf1, 0x3d, 0xb5, 0xb9, 0xb2, 0x7b, 0x5d, 0x0f, 0x2a, 0xca, 0x9d, 0x58, 0x9a, 0x69, 0xe5, 0xbe, 0x00, 0xeb, 0x97, 0x86, 0x21, 0xe6, 0x77, 0x6e, 0x87, 0xea, 0x32, 0x6d, 0x47, 0xa3, 0x4c, 0x1a + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1mtxw5nras5glkxz093282sv3n2h8qs7cpxcmmaxj96vtzjzl6rmdaxs4e9es7mxwmd0h3k5wz3ce4ll5g4jz2pn9su4pufq74pxhp4t235n6j7aed3hh8ss7pf3sekf7apsf6vtg84ue5zcq2k9q3xv5yth3q50fu4czdm8sn8q4de3m5k76g2vwwyjsf50hqfxgmwxqxu0rsy22ktw", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 1, + diversifier_index: 8, + }, + TestVector { + p2pkh_bytes: None, + p2sh_bytes: None, + sapling_raw_addr: Some([ + 0x88, 0x53, 0x3c, 0x39, 0x8a, 0x49, 0xc2, 0x51, 0x3d, 0xc8, 0x51, 0x62, 0xbf, 0x22, 0x0a, 0xba, 0xf4, 0x7d, 0xc9, 0x83, 0xf1, 0x4e, 0x90, 0x8d, 0xda, 0xaa, 0x73, 0x22, 0xdb, 0xa1, 0x65, 0x31, 0xbc, 0x62, 0xef, 0xe7, 0x50, 0xfe, 0x57, 0x5c, 0x8d, 0x14, 0x9b + ]), + orchard_raw_addr: Some([ + 0x95, 0x3f, 0x3c, 0x78, 0xd1, 0x03, 0xc3, 0x2b, 0x60, 0x55, 0x92, 0x99, 0x46, 0x2e, 0xbb, 0x27, 0x34, 0x89, 0x64, 0xb8, 0x92, 0xac, 0xad, 0x10, 0x48, 0x2f, 0xe5, 0x02, 0xc9, 0x9f, 0x0d, 0x52, 0x49, 0x59, 0xba, 0x7b, 0xe4, 0xf1, 0x88, 0xe3, 0xa2, 0x71, 0x38 + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1ay3aawlldjrmxqnjf5medr5ma6p3acnet464ht8lmwplq5cd3ugytcmlf96rrmtgwldc75x94qn4n8pgen36y8tywlq6yjk7lkf3fa8wzjrav8z2xpxqnrnmjxh8tmz6jhfh425t7f3vy6p4pd3zmqayq49efl2c4xydc0gszg660q9p", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 2, + diversifier_index: 0, + }, + TestVector { + p2pkh_bytes: None, + p2sh_bytes: None, + sapling_raw_addr: Some([ + 0x61, 0x6f, 0xe1, 0xa9, 0xd8, 0x87, 0x14, 0x8d, 0x6c, 0xa1, 0x0f, 0x48, 0xcc, 0xd9, 0x2d, 0x0d, 0xca, 0xd2, 0x4f, 0x7c, 0x4c, 0x9d, 0x73, 0xee, 0x81, 0x22, 0xb1, 0x76, 0x64, 0x59, 0xb0, 0x4d, 0xac, 0x4d, 0xc0, 0x7e, 0x80, 0xed, 0xb9, 0xd2, 0x29, 0xbb, 0xbc + ]), + orchard_raw_addr: Some([ + 0xcc, 0x80, 0x26, 0x99, 0x33, 0x0b, 0xc4, 0x74, 0x8e, 0x34, 0xdd, 0x59, 0x8c, 0x71, 0x24, 0xe7, 0x22, 0x99, 0xe6, 0xa6, 0xd5, 0xbc, 0xc3, 0x2e, 0x90, 0x40, 0x9c, 0x80, 0x24, 0x86, 0x8b, 0x27, 0x05, 0xaa, 0xdf, 0xab, 0x60, 0x68, 0xd4, 0x58, 0xf6, 0x9b, 0x0c + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u19a4vmx7ysmtavmnaz4d2dgl9pyshexw35rl5ezg5dkkxktg08p42lng7kf9hqtn2fhr63qzyhe8gtnvgtfl9yvne46x6zfzwgedx7c0chnrxty0k5r5qqph8k02zs8e3keul9vj8myju7rvqgjaysa9kt0fucxpzuky6kf0pjgy0a6hx", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 2, + diversifier_index: 5, + }, + TestVector { + p2pkh_bytes: None, + p2sh_bytes: None, + sapling_raw_addr: Some([ + 0x93, 0x04, 0xf6, 0xe3, 0xc8, 0x89, 0x82, 0x9a, 0x0a, 0x48, 0xf2, 0xeb, 0xdc, 0x08, 0x03, 0xbb, 0xbd, 0x39, 0x3e, 0xbf, 0x42, 0x64, 0xe4, 0x5c, 0xb7, 0xdb, 0x79, 0x3e, 0x93, 0x76, 0xfa, 0x85, 0xdd, 0xf3, 0x1f, 0x50, 0x24, 0xe0, 0xbf, 0x79, 0x66, 0x72, 0xbe + ]), + orchard_raw_addr: Some([ + 0x3e, 0xd5, 0x01, 0xc9, 0xc6, 0x3a, 0xba, 0xf4, 0xd0, 0x13, 0x68, 0x21, 0xf9, 0x64, 0x7e, 0x76, 0x45, 0x55, 0xa4, 0x70, 0x33, 0xad, 0x91, 0xd7, 0x34, 0xdf, 0x12, 0xd0, 0x46, 0xc9, 0x69, 0x75, 0x13, 0x30, 0xbb, 0xf4, 0x93, 0xa2, 0x41, 0xec, 0x4b, 0x88, 0xbc + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u13p2teem3xlvy4kwlke24hng5el2z6mn4ftj8xarwn8fy7dqt0flgcfpaxe6sk5cwawwh4tynzu7z2uschaf8tfa3tp2xgt8g4kx5lahhglcjm26jnvw7am6ld33708g0kv35pq83eg6gj82a0aau80enrhywpgr4v4m4vve7tg8vd4hz", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 2, + diversifier_index: 6, + }, + TestVector { + p2pkh_bytes: Some([ + 0x87, 0x1a, 0x08, 0x9d, 0x44, 0x62, 0x68, 0xaa, 0x7a, 0xc0, 0x3d, 0x2a, 0x6f, 0x60, 0xae, 0x70, 0x80, 0x8f, 0x39, 0x74 + ]), + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0x31, 0x84, 0x46, 0x83, 0xa0, 0x7b, 0xf8, 0xe3, 0x00, 0x57, 0x90, 0x2b, 0x0d, 0x23, 0xe2, 0xb2, 0xce, 0x9c, 0xad, 0x0b, 0x22, 0x19, 0x02, 0x38, 0xca, 0x4f, 0x32, 0x9d, 0xa9, 0x2c, 0x79, 0x79, 0x05, 0x2b, 0x00, 0xf7, 0x35, 0xcb, 0x21, 0x06, 0x71, 0xbd, 0xb0 + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1snf9yr883aj2hm8pksp9aymnqdwzy42rpzuffevj35hhxeckays5pcpeq7vy2mtgzlcuc4mnh9443qnuyje0yx6h59angywka4v2ap6kchh2j96ezf9w0c0auyz3wwts2lx5gmk2sk9", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 3, + diversifier_index: 0, + }, + TestVector { + p2pkh_bytes: Some([ + 0x7c, 0xb0, 0x7c, 0x31, 0xb5, 0x80, 0x40, 0xac, 0x7c, 0xc1, 0x2b, 0xfa, 0xaa, 0x13, 0x8c, 0xfb, 0xef, 0xb3, 0x84, 0x57 + ]), + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0x05, 0x68, 0x3c, 0x03, 0x03, 0x85, 0x83, 0x88, 0xa7, 0x85, 0xb4, 0xcf, 0x15, 0xd4, 0x1a, 0xc6, 0x9e, 0x1d, 0x43, 0x5b, 0x0a, 0xd2, 0x38, 0x38, 0xe1, 0x8d, 0x62, 0xf7, 0xec, 0x41, 0xc3, 0x7f, 0xc8, 0x6a, 0xf7, 0x1d, 0xff, 0xd9, 0x4d, 0xff, 0xf6, 0xb2, 0x07 + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1szwcx2zdxalyp7cfqwrptv95rnpyajejs6jmwacz4cgm2g3vzdxl5perhpg3nyhnuplvptdr4g63gupdfj5zal9v35s3e6adqsckv68hyrclan3gxaj6mz8aejzsnhqjyn32jcpnpra", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 3, + diversifier_index: 1, + }, + TestVector { + p2pkh_bytes: Some([ + 0x3e, 0x02, 0xe0, 0x8b, 0x59, 0x65, 0xfc, 0xe9, 0xc2, 0x0c, 0xe6, 0xde, 0x6f, 0x94, 0x07, 0x67, 0x4d, 0x01, 0xba, 0x02 + ]), + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0x55, 0x1a, 0x16, 0xfb, 0x00, 0xd5, 0x48, 0x2a, 0x2a, 0xb2, 0x51, 0x82, 0x56, 0x06, 0x61, 0xcf, 0xd7, 0x4a, 0x60, 0xfe, 0x77, 0xa0, 0xf1, 0xc9, 0x34, 0x7f, 0x16, 0xba, 0x52, 0x49, 0x88, 0x9f, 0x3a, 0xe3, 0x46, 0xed, 0x69, 0x38, 0xc3, 0x0a, 0xbf, 0xaf, 0x80 + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1glq6lzrxc7n7r4c922qht20zmpxyl0asfuldrjcaddagfspxpc3040fdfwdf5crw4j6j6wkx4r038s0w24w7enpyfmmdfu9t9p2amxazgvasms8l03l3j5yhrrfqy6xzue5uggef4p8", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 3, + diversifier_index: 2, + }, + TestVector { + p2pkh_bytes: None, + p2sh_bytes: None, + sapling_raw_addr: Some([ + 0x64, 0x93, 0x34, 0x8e, 0x8a, 0xee, 0x11, 0x2a, 0x87, 0xf5, 0xfa, 0x65, 0xe1, 0xc5, 0x70, 0x65, 0xaa, 0xd3, 0x69, 0x40, 0x1e, 0x05, 0xd0, 0xda, 0xa9, 0x6e, 0x0b, 0xcd, 0x89, 0xe6, 0x7b, 0xf1, 0x9b, 0xeb, 0x3a, 0xc7, 0x4d, 0x59, 0x9d, 0x94, 0x58, 0x5a, 0x68 + ]), + orchard_raw_addr: Some([ + 0x16, 0x50, 0x82, 0xde, 0x84, 0xf2, 0xad, 0x72, 0x04, 0x42, 0x6f, 0xfa, 0xfd, 0x6b, 0x6c, 0x7d, 0xe9, 0xca, 0xb6, 0xd2, 0x5c, 0x13, 0x84, 0x6a, 0x17, 0x86, 0x71, 0x52, 0x68, 0xc4, 0x15, 0x94, 0x8d, 0xb7, 0x88, 0xf4, 0xa5, 0xe0, 0xda, 0xa0, 0x3d, 0x69, 0x9e + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1tqhg04ppjt6vlf2uvkygt07sqzgpclxdpn7j7ydkcr0e8ym68wn592z7uqudktrwn4u3q57flp8hw3d0wd9t0rm0e6m8eys27evfawh6zhha6eulzj86uz89swu7gtk0vcknd3dauhc96twhx20xxsp93dxahqlt7z5p04ldgy2y2lp0", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 4, + diversifier_index: 0, + }, + TestVector { + p2pkh_bytes: None, + p2sh_bytes: None, + sapling_raw_addr: Some([ + 0x65, 0xb6, 0xb0, 0x3f, 0x7b, 0x27, 0x18, 0x9c, 0xc0, 0xed, 0x54, 0xbc, 0xf6, 0xbd, 0x93, 0x8e, 0x39, 0xbf, 0xd1, 0xbf, 0x66, 0xb8, 0xa0, 0x38, 0xc0, 0xa9, 0x67, 0xfb, 0xc5, 0x0e, 0x48, 0xc1, 0x8d, 0xa3, 0xde, 0x20, 0xd6, 0x71, 0x85, 0x8b, 0x8f, 0x7f, 0xbf + ]), + orchard_raw_addr: Some([ + 0xc9, 0x06, 0x10, 0x9b, 0x51, 0xe2, 0xb3, 0x7b, 0xf8, 0xb6, 0x77, 0x61, 0xbf, 0xa9, 0x17, 0xdc, 0x50, 0x59, 0xc3, 0x57, 0xb7, 0xdc, 0x81, 0x07, 0x67, 0x2b, 0x66, 0x18, 0x9a, 0x0d, 0x15, 0xbc, 0x49, 0x6d, 0x84, 0xef, 0x91, 0x14, 0xc6, 0x8c, 0x99, 0xc9, 0x11 + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1zm98xj3ncc79sx8jxhcscptxav0p4wam8mlkf4lp69rhramz7v6fsndwxcd4qtmzkefwcwn5rgd8uztvdrvfqv32jk3xx6wlt7gae9fhs7xh48d3kn9fe92xtcff8hu0zgegmgr95qtxayjylfdct96eg2f2r06drf6sj800mcsns3n0", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 4, + diversifier_index: 1, + }, + TestVector { + p2pkh_bytes: None, + p2sh_bytes: None, + sapling_raw_addr: Some([ + 0xe9, 0x87, 0xa4, 0xf5, 0x0c, 0x94, 0xba, 0x88, 0xe0, 0x48, 0x63, 0x8e, 0xce, 0xc7, 0x06, 0xef, 0x8a, 0x16, 0x26, 0x74, 0xc9, 0xbe, 0xf8, 0xca, 0xed, 0xfd, 0xf4, 0xb2, 0x13, 0x1b, 0x45, 0x15, 0x59, 0x09, 0x04, 0x88, 0xff, 0xe2, 0x9e, 0xc0, 0x2a, 0xba, 0xc1 + ]), + orchard_raw_addr: Some([ + 0x7c, 0xd0, 0x65, 0xb0, 0xab, 0x29, 0x7f, 0xb7, 0xfd, 0x70, 0x12, 0x91, 0xd0, 0x35, 0x89, 0x03, 0x1f, 0xe3, 0xaa, 0xdf, 0x11, 0x77, 0x90, 0x2e, 0x5b, 0xcb, 0x65, 0xb5, 0xba, 0x0a, 0xa2, 0xa0, 0xb7, 0x3f, 0x09, 0x73, 0x4f, 0x0b, 0x86, 0x7b, 0x29, 0x76, 0x3d + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1hfgf2s4pghqteculnmq2rcnvyesml74zqfp5yfhxhwewx62q75qhgmwreg5qht7c5vu3fxefunjrarrfhmcuw2z4ndx0qx7u74gkw2n7v0ypvd4mxgzlenvs7lkurdj09zuhz6pmtuzs4m42sx92axuuru4dmgu46a920x5kuye6gxvs", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 4, + diversifier_index: 2, + }, + TestVector { + p2pkh_bytes: Some([ + 0x40, 0xc4, 0x40, 0x30, 0xe4, 0x68, 0xb7, 0x09, 0x1e, 0x9b, 0xb3, 0x3b, 0xa0, 0xab, 0xdc, 0x63, 0x98, 0x6f, 0x3c, 0x36 + ]), + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0xea, 0x9d, 0xf8, 0x3f, 0xbe, 0xe0, 0x7d, 0x6f, 0x78, 0x95, 0xeb, 0xb2, 0xea, 0x41, 0xec, 0x7c, 0x4b, 0xa6, 0x82, 0xb8, 0x63, 0xe0, 0x69, 0xb4, 0xa4, 0x38, 0xe3, 0x1c, 0x95, 0x71, 0xc8, 0x31, 0x26, 0xc3, 0x05, 0xd7, 0x54, 0x56, 0x41, 0x2a, 0xea, 0xef, 0x1b + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u17cfcut587e3kszg8vud0z5a8lj9gyypyvtt5xn4hfc4p3kv4e0jfr2pzzxhywlkhsjldtmkvupwr7mkjvruz8gnxk7a64x777p4l3u7vpm6zsdsx88ef90x5q5sqx57fq8vtj5vk3hx", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 5, + diversifier_index: 0, + }, + TestVector { + p2pkh_bytes: Some([ + 0x93, 0x7e, 0x71, 0xf9, 0xb2, 0xb6, 0x44, 0x0a, 0x05, 0xee, 0x14, 0x75, 0xbc, 0xc4, 0x87, 0xe0, 0x8a, 0x4f, 0x58, 0x01 + ]), + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0xfd, 0x3e, 0x7e, 0xcc, 0xdb, 0x1a, 0x91, 0xf2, 0xc4, 0x49, 0x8b, 0xb7, 0xeb, 0x61, 0xcb, 0xa8, 0x3e, 0xca, 0x49, 0x9c, 0xfd, 0xe9, 0xc5, 0xce, 0x3e, 0x32, 0x41, 0x87, 0x3b, 0xad, 0x2e, 0x42, 0x3a, 0xbe, 0x91, 0xde, 0xce, 0x0a, 0x69, 0x30, 0xe8, 0x90, 0x1d + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1z6qgxh0wyw0ptgwwgsr5uv05n3xm3z8yrdr06k7q6fj9ypyjcj2hxwfmktv4a7ejaqphcgkddhsvrs93skzl3frm8e48at6huayg7k67e3c50ykpdnhva2jfh5dfcvy6nvttqwgz5a7", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 5, + diversifier_index: 1, + }, + TestVector { + p2pkh_bytes: Some([ + 0xb3, 0x48, 0x66, 0x81, 0x90, 0x53, 0x98, 0x32, 0x31, 0xc4, 0x8f, 0xd8, 0xa2, 0x70, 0x6c, 0xec, 0xff, 0x29, 0xba, 0x99 + ]), + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0x5e, 0xf3, 0xc8, 0xb2, 0xbf, 0x2a, 0x8b, 0x0e, 0x60, 0xa6, 0x25, 0x4f, 0x31, 0x22, 0x29, 0xb4, 0x12, 0x4d, 0x47, 0x87, 0xe7, 0xda, 0xda, 0x5d, 0x81, 0xe1, 0x6b, 0x51, 0x21, 0x17, 0x07, 0x87, 0x1b, 0xed, 0xe3, 0x28, 0x11, 0xa3, 0x5f, 0x40, 0x94, 0xae, 0x8b + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1g6jcyfwqd9yx8pdg4yvf0nsr5j7k5gmx83shh8v0v3w256umheen026x66f4608w2vydyasphgp80j9avq9h56dx73gg2559l5lj707v4458a0ucyhfxcjcccfx9z9upmcf3c6hg9k8", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 5, + diversifier_index: 2, + }, + TestVector { + p2pkh_bytes: Some([ + 0x06, 0x97, 0x4d, 0x8b, 0xcd, 0x8b, 0xa8, 0xef, 0x89, 0xce, 0x36, 0xa6, 0x53, 0xd9, 0x38, 0x68, 0x25, 0x1c, 0x2e, 0x3d + ]), + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0x3c, 0x40, 0x24, 0x69, 0x12, 0xb6, 0xef, 0xef, 0xab, 0x9a, 0x55, 0x24, 0x4a, 0xc2, 0xc1, 0x74, 0xe1, 0xa9, 0xf8, 0xc0, 0xbc, 0x0f, 0xd5, 0x26, 0x93, 0x39, 0x63, 0xc6, 0xec, 0xb9, 0xb8, 0x4e, 0xc8, 0xb0, 0xf6, 0xb4, 0x0d, 0xc8, 0x58, 0xfa, 0x23, 0xc7, 0x2b + ]), + unknown_typecode: Some(65532), + unknown_bytes: Some(&[ + 0xd5, 0x6a, 0x1d, 0x62, 0xf5, 0xa8, 0xd7, 0x55, 0x1d, 0xb5, 0xfd, 0x93, 0x13, 0xe8, 0xc7, 0x20, 0x3d, 0x99, 0x6a, 0xf7, 0xd4, 0x77, 0x08, 0x37, 0x56, 0xd5, 0x9a, 0xf8, 0x0d, 0x06, 0xa7, 0x45, 0xf4, 0x4a, 0xb0, 0x23, 0x75, 0x2c, 0xb5, 0xb4, 0x06, 0xed, 0x89, 0x85, 0xe1, 0x81, 0x30, 0xab, 0x33, 0x36, 0x26, 0x97, 0xb0, 0xe4, 0xe4, 0xc7, 0x63, 0xcc, 0xb8, 0xf6, 0x76, 0x49, 0x5c, 0x22, 0x2f, 0x7f, 0xba, 0x1e, 0x31, 0xde, 0xfa, 0x3d, 0x5a, 0x57, 0xef, 0xc2, 0xe1, 0xe9, 0xb0, 0x1a, 0x03, 0x55, 0x87, 0xd5, 0xfb, 0x1a, 0x38, 0xe0, 0x1d, 0x94, 0x90, 0x3d, 0x3c, 0x3e, 0x0a, 0xd3, 0x36, 0x0c, 0x1d, 0x37, 0x10, 0xac, 0xd2, 0x0b, 0x18, 0x3e, 0x31, 0xd4, 0x9f, 0x25, 0xc9, 0xa1, 0x38, 0xf4, 0x9b, 0x1a, 0x53, 0x7e, 0xdc, 0xf0, 0x4b, 0xe3, 0x4a, 0x98, 0x51, 0xa7, 0xaf, 0x9d, 0xb6, 0x99, 0x0e, 0xd8, 0x3d, 0xd6, 0x4a, 0xf3, 0x59, 0x7c, 0x04, 0x32, 0x3e, 0xa5, 0x1b, 0x00, 0x52, 0xad, 0x80, 0x84, 0xa8, 0xb9, 0xda, 0x94, 0x8d, 0x32, 0x0d, 0xad, 0xd6, 0x4f, 0x54, 0x31, 0xe6, 0x1d, 0xdf, 0x65, 0x8d, 0x24, 0xae, 0x67, 0xc2, 0x2c, 0x8d, 0x13 + ]), + unified_addr: "u1en8ysypun4gdkdnu8zqqg6k73ankr9ffwfzg08wtzg9z939w0wupewemfrc8a630e8gc4uqucym0l4v44fszy3et4veyypt3jsyp0whfpfsn2lw30kj8nepe6wvvasf00wklh85u9v8glqndupmamk9z2ja9sanf70pp4yxvkt3dmyzxa0kkhv2c9pxmkghrxqk0590azvya3nzrtevj449nu3laskrhf7c7nj9cyw7ty38mccg4znrr876guu6pzndx7ngwzhmlsn8d89saf5araaacrhr9958xr6z23mj4qtzzn98whdpu8u7n8fhf5d2vypljda62q73du44sf0e0kxmq3gvgkta0qqgq9w6r403gc5jz2any02etmwlttkv84hgh95czhdf2jugk3u36ke0kchcthg240", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 6, + diversifier_index: 0, + }, + TestVector { + p2pkh_bytes: Some([ + 0xcd, 0xd4, 0xb2, 0xbe, 0x1b, 0x57, 0xf2, 0x4c, 0x85, 0xfc, 0x1e, 0x43, 0xc7, 0x7b, 0xb2, 0xda, 0x2d, 0x26, 0x46, 0xf1 + ]), + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0xfc, 0x23, 0x51, 0x22, 0x89, 0x2d, 0x61, 0x1e, 0x52, 0xee, 0x5b, 0x44, 0x7a, 0x77, 0xec, 0x5a, 0x29, 0x62, 0x13, 0x94, 0x8f, 0xb5, 0x6d, 0x72, 0x1f, 0x66, 0xf2, 0x64, 0xe3, 0x2e, 0x7d, 0x0c, 0xe5, 0x47, 0x30, 0x05, 0xfc, 0x4c, 0x0b, 0xcf, 0x42, 0x1e, 0x8f + ]), + unknown_typecode: Some(65532), + unknown_bytes: Some(&[ + 0x09, 0x13, 0x1f, 0xc0, 0x0f, 0xe7, 0xf2, 0x35, 0x73, 0x42, 0x76, 0xd3, 0x8d, 0x47, 0xf1, 0xe1, 0x91, 0xe0, 0x0c, 0x7a, 0x1d, 0x48, 0xaf, 0x04, 0x68, 0x27, 0x59, 0x1e, 0x97, 0x33, 0xa9, 0x7f, 0xa6, 0xb6, 0x79, 0xf3, 0xdc, 0x60, 0x1d, 0x00, 0x82, 0x85, 0xed, 0xcb, 0xda, 0xe6, 0x9c, 0xe8, 0xfc, 0x1b, 0xe4, 0xaa, 0xc0, 0x0f, 0xf2, 0x71, 0x1e, 0xbd, 0x93, 0x1d, 0xe5, 0x18, 0x85, 0x68, 0x78, 0xf7, 0x34, 0x76, 0xf2, 0x1a, 0x48, 0x2e, 0xc9, 0x37, 0x83, 0x65, 0xc8, 0xf7, 0x39, 0x3c, 0x94, 0xe2, 0x88, 0x53, 0x15, 0xeb, 0x46, 0x71, 0x09, 0x8b, 0x79, 0x53, 0x5e, 0x79, 0x0f, 0xe5, 0x3e, 0x29, 0xfe, 0xf2, 0xb3, 0x76, 0x66, 0x97, 0xac, 0x32, 0xb4, 0xf4, 0x73, 0xf4, 0x68, 0xa0, 0x08, 0xe7, 0x23, 0x89, 0xfc, 0x03, 0x88, 0x0d, 0x78, 0x0c, 0xb0, 0x7f, 0xcf, 0xaa, 0xbe, 0x3f, 0x1a, 0x84, 0xb2, 0x7d, 0xb5, 0x9a, 0x4a, 0x15, 0x3d, 0x88, 0x2d, 0x2b, 0x21, 0x03, 0x59, 0x65, 0x55, 0xed, 0x94, 0x94, 0xc6, 0xac, 0x89, 0x3c, 0x49, 0x72, 0x38, 0x33, 0xec, 0x89, 0x26, 0xc1, 0x03, 0x95, 0x86, 0xa7, 0xaf, 0xcf, 0x4a, 0x0d, 0x9c, 0x73, 0x1e, 0x98 + ]), + unified_addr: "u1a7gz63aey4tnj4klwauth00vnkmltwafwzk9nld2ys7yz3yjzjcdp47crc37zc4g9aq4athg9zh8r792e44kd6g2f4drhsl5ph4ja8pe4gcc9yjyf3rn7pej808hcy6xh0x6y8khmzljehjlwqq4h2czp35vu3l7aa7rpw5vcng9gswwlaqn5ptes592wejx7f49rxsvmzeqjekjtyfevehanvyksa8gtkpk75yrqnam26hzuxrtm6agaluy4hv0ha4sg6h22394m0x5th6r8uj7svzlklaja852vv9ud5gznu2sqyrsqveqjmfk9rcs59sprjj8nrt2nke862xlhvjq9y9zswen27eqj5slg52q2zch59uzwaeat8jw6z6092uu8yqqnnj7h0yguhypgd8y2wu9ftgg38ym3", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 6, + diversifier_index: 1, + }, + TestVector { + p2pkh_bytes: Some([ + 0x9f, 0x98, 0xc3, 0x11, 0x6c, 0xb2, 0xf4, 0xe6, 0xf4, 0xc8, 0x14, 0x14, 0x8c, 0x81, 0xe3, 0x79, 0xa5, 0x38, 0xce, 0xd3 + ]), + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0x25, 0x26, 0xec, 0x65, 0x52, 0xf3, 0xe0, 0x17, 0x5c, 0x92, 0x2f, 0x01, 0x90, 0x77, 0x14, 0x6b, 0x51, 0x93, 0xe8, 0x80, 0x46, 0x1c, 0x3e, 0x1d, 0xac, 0xa4, 0x77, 0x8c, 0xde, 0x01, 0x0e, 0xd5, 0x87, 0x5f, 0x16, 0xb7, 0x43, 0xef, 0x86, 0xac, 0x64, 0x8b, 0x3d + ]), + unknown_typecode: Some(65532), + unknown_bytes: Some(&[ + 0x5d, 0x99, 0x58, 0x9c, 0x8b, 0xb8, 0x38, 0xe8, 0xaa, 0xf7, 0x45, 0x53, 0x3e, 0xd9, 0xe8, 0xae, 0x3a, 0x1c, 0xd0, 0x74, 0xa5, 0x1a, 0x20, 0xda, 0x8a, 0xba, 0x18, 0xd1, 0xdb, 0xeb, 0xbc, 0x86, 0x2d, 0xed, 0x42, 0x43, 0x5e, 0x92, 0x47, 0x69, 0x30, 0xd0, 0x69, 0x89, 0x6c, 0xff, 0x30, 0xeb, 0x41, 0x4f, 0x72, 0x7b, 0x89, 0xe0, 0x01, 0xaf, 0xa2, 0xfb, 0x8d, 0xc3, 0x43, 0x6d, 0x75, 0xa4, 0xa6, 0xf2, 0x65, 0x72, 0x50, 0x4b, 0x19, 0x22, 0x32, 0xec, 0xb9, 0xf0, 0xc0, 0x24, 0x11, 0xe5, 0x25, 0x96, 0xbc, 0x5e, 0x90, 0x45, 0x7e, 0x74, 0x59, 0x39, 0xff, 0xed, 0xbd, 0x12, 0x86, 0x3c, 0xe7, 0x1a, 0x02, 0xaf, 0x11, 0x7d, 0x41, 0x7a, 0xdb, 0x3d, 0x15, 0xcc, 0x54, 0xdc, 0xb1, 0xfc, 0xe4, 0x67, 0x50, 0x0c, 0x6b, 0x8f, 0xb8, 0x6b, 0x12, 0xb5, 0x6d, 0xa9, 0xc3, 0x82, 0x85, 0x7d, 0xee, 0xcc, 0x40, 0xa9, 0x8d, 0x5f, 0x29, 0x35, 0x39, 0x5e, 0xe4, 0x76, 0x2d, 0xd2, 0x1a, 0xfd, 0xbb, 0x5d, 0x47, 0xfa, 0x9a, 0x6d, 0xd9, 0x84, 0xd5, 0x67, 0xdb, 0x28, 0x57, 0xb9, 0x27, 0xb7, 0xfa, 0xe2, 0xdb, 0x58, 0x71, 0x05, 0x41, 0x5d, 0x46, 0x42, 0x78, 0x9d + ]), + unified_addr: "u1ln90fvpdtyjapnsqpa2xjsarmhu3k2qvdr6uc6upurnuvzh382jzmfyw40yu8avd2lj7arvq57n0qmryy0flp7tm0fw05h366587mzzwwrls85da6l2sr7tuazmv5s02avxaxrl4j7pau0u9xyp470y9hkca5m9g4735208w6957p82lxajzq4l2pqkam86y6jfx8cd8ecw2e05qnh0qq95dr09sgz9hqmflzac7hsxj47yvjd69ej06ewdg97wsu2x9wg3ahfh6s4nvk65elwcu5wl092ta38028p4lc2d6l7ea63s6uh4ek0ry9lg50acxuw2sdv02jh90tzh783d59gneu8ue3wqefjmtndyquwq9kkxaedhtqh2yyjew93ua38vp8uchug0q7kg7qvp4l65t9yqaz2w2p", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 6, + diversifier_index: 2, + }, + TestVector { + p2pkh_bytes: None, + p2sh_bytes: None, + sapling_raw_addr: Some([ + 0xda, 0x26, 0x72, 0xc0, 0x10, 0xf7, 0x36, 0x4d, 0xf6, 0xfa, 0xd4, 0x9d, 0xd3, 0x9b, 0xe0, 0xe4, 0xd4, 0xbe, 0x73, 0xc4, 0x5e, 0x23, 0x94, 0x48, 0xfc, 0xc3, 0x85, 0xcc, 0x68, 0x09, 0x4b, 0xf3, 0x6d, 0xdb, 0xc4, 0xec, 0x02, 0x19, 0xb5, 0x67, 0x95, 0x55, 0x56 + ]), + orchard_raw_addr: None, + unknown_typecode: Some(65533), + unknown_bytes: Some(&[ + 0xd1, 0x7d, 0x19, 0xf3, 0x35, 0x5b, 0xcf, 0x73, 0xce, 0xcb, 0x8c, 0xb8, 0xa5, 0xda, 0x01, 0x30, 0x71, 0x52, 0xf1, 0x39, 0x36, 0xa2, 0x70, 0x57, 0x26, 0x70, 0xdc, 0x82, 0xd3, 0x90, 0x26, 0xc6, 0xcb, 0x4c, 0xd4, 0xb0, 0xf7, 0xf5, 0xaa, 0x2a, 0x4f, 0x5a, 0x53, 0x41, 0xec, 0x5d, 0xd7, 0x15, 0x40, 0x6f, 0x2f, 0xdd, 0x2a, 0xfa, 0x73, 0x3f, 0x5f, 0x64, 0x1c, 0x8c, 0x21, 0x86, 0x2a, 0x1b, 0xaf, 0xce, 0x26, 0x09, 0xd9, 0xee, 0xcf, 0xa1, 0x58, 0xcf, 0xb5, 0xcd, 0x79, 0xf8, 0x80, 0x08, 0xe3, 0x15, 0xdc, 0x7d, 0x83, 0x88, 0xe7, 0x6c, 0x17, 0x82, 0xfd, 0x27, 0x95, 0xd1, 0x8a, 0x76, 0x36, 0x24, 0xc2, 0x5f, 0xa9, 0x59, 0xcc, 0x97, 0x48, 0x9c, 0xe7, 0x57, 0x45, 0x82, 0x4b, 0x77, 0x86, 0x8c, 0x53, 0x23, 0x9c, 0xfb, 0xdf, 0x73, 0xca + ]), + unified_addr: "u1sem2gcey0emntrvxyjv8hyhq0w5fr4sxaj3cppgrfqgg6laydh8m78gy2cw2p54zzak3alnnsx4xjuhazpkrfcd90wl0c7ldj6y095hh5j6j2evry9vg5jqp4dyqpwqeryu7pes4sxyyyqwn6egs5daxk4473v9xpgzrwv5n0tvs93nlj4xpphq4vs2w8um9ph7zkte08t7fa509mnrt9apuhr22xq34mp2svjnq6rvfn0hg6lkehxtlj39vgjxjlkjfhx8rw2f02ckq8k5szcxsnhkgr2cqlmf2udl2gqdqr5t6", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 7, + diversifier_index: 1, + }, + TestVector { + p2pkh_bytes: None, + p2sh_bytes: None, + sapling_raw_addr: Some([ + 0x9b, 0x72, 0x8a, 0xd6, 0xf5, 0x03, 0x71, 0xe9, 0x61, 0x23, 0x66, 0x30, 0xb3, 0xc8, 0xcd, 0xd8, 0x14, 0x9c, 0xa2, 0x2c, 0xdb, 0x87, 0xa6, 0x2c, 0xc0, 0xba, 0x3e, 0x3c, 0xfd, 0x2b, 0x0a, 0xdc, 0xc8, 0x29, 0x30, 0xe4, 0x47, 0xf8, 0xdc, 0xf5, 0x4b, 0x45, 0x0b + ]), + orchard_raw_addr: None, + unknown_typecode: Some(65533), + unknown_bytes: Some(&[ + 0xec, 0x65, 0x60, 0x40, 0x37, 0x31, 0x4f, 0xaa, 0xce, 0xb5, 0x62, 0x18, 0xc6, 0xbd, 0x30, 0xf8, 0x37, 0x4a, 0xc1, 0x33, 0x86, 0x79, 0x3f, 0x21, 0xa9, 0xfb, 0x80, 0xad, 0x03, 0xbc, 0x0c, 0xda, 0x4a, 0x44, 0x94, 0x6c, 0x00, 0xe1, 0xb1, 0xa1, 0xdf, 0x0e, 0x5b, 0x87, 0xb5, 0xbe, 0xce, 0x47, 0x7a, 0x70, 0x96, 0x49, 0xe9, 0x50, 0x06, 0x05, 0x91, 0x39, 0x48, 0x12, 0x95, 0x1e, 0x1f, 0xe3, 0x89, 0x5b, 0x8c, 0xc3, 0xd1, 0x4d, 0x2c, 0xf6, 0x55, 0x6d, 0xf6, 0xed, 0x4b, 0x4d, 0xdd, 0x3d, 0x9a, 0x69, 0xf5, 0x33, 0x57, 0xd7, 0x76, 0x7f, 0x4f, 0x5c, 0xcb, 0xdb, 0xc5, 0x96, 0x63, 0x12, 0x77, 0xf8, 0xfe, 0xcd, 0x08, 0xcb, 0x05, 0x6b, 0x95, 0xe3, 0x02, 0x5b, 0x97, 0x92, 0xff, 0xf7, 0xf2, 0x44, 0xfc, 0x71, 0x62, 0x69, 0xb9, 0x26, 0xd6 + ]), + unified_addr: "u10j2s9sy4dmuakf57z58jc5t8yuswega82jpd2hk3q62l6fsphwyjxvmvfwy8skvvvea6dnkl8l9zpjf3m27qsav9y9nlj59hagmjf5xh0xxyqr8lymnmtjn6gzgrn04dr5s0k9k9wuxc2udzjh4llv47zm6jn6ff0j65s54h3m6p0n9ajswrqzpvy8eh4d5pvypyc6rp5m07uwmjp4sr0upca5hl7gr4pxg45m7vlnx5r7va4n6mfyr98twvjrhcyalwhddelnnjrkhcj0wcp5eyas2c2kcadrxyzw28vvv47q74", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 7, + diversifier_index: 5, + }, + TestVector { + p2pkh_bytes: None, + p2sh_bytes: None, + sapling_raw_addr: Some([ + 0x9d, 0xd7, 0x7f, 0xf5, 0xaf, 0x4c, 0x80, 0xc2, 0x51, 0x14, 0xe8, 0x37, 0x58, 0xcb, 0xe1, 0xb5, 0x35, 0xcf, 0xe9, 0x41, 0x30, 0x17, 0x99, 0x41, 0x63, 0xa1, 0x2b, 0x0d, 0xe5, 0x22, 0xcd, 0xd1, 0xb5, 0xd4, 0xbe, 0x29, 0x9c, 0x07, 0x88, 0xcc, 0xb1, 0x54, 0x1e + ]), + orchard_raw_addr: None, + unknown_typecode: Some(65533), + unknown_bytes: Some(&[ + 0x2e, 0x95, 0x96, 0xfa, 0x82, 0x5c, 0x6b, 0xf2, 0x1a, 0xff, 0x9e, 0x68, 0x62, 0x5a, 0x19, 0x24, 0x40, 0xea, 0x06, 0x82, 0x81, 0x23, 0xd9, 0x78, 0x84, 0x80, 0x6f, 0x15, 0xfa, 0x08, 0xda, 0x52, 0x75, 0x4a, 0x10, 0x95, 0xe3, 0xff, 0x1a, 0xbd, 0x5c, 0xe4, 0xfd, 0xdf, 0xcc, 0xfc, 0x3a, 0x61, 0x28, 0xae, 0xf7, 0x84, 0xa6, 0x46, 0x10, 0xa8, 0x9d, 0x1a, 0x70, 0x99, 0x21, 0x6d, 0x08, 0x14, 0xd3, 0xa2, 0xd4, 0x52, 0x43, 0x1c, 0x32, 0xd4, 0x11, 0xac, 0x1c, 0xce, 0x82, 0xad, 0x02, 0x29, 0x40, 0x7b, 0xbc, 0x48, 0x98, 0x56, 0x75, 0xe3, 0xf8, 0x74, 0xa4, 0x53, 0x3f, 0x1d, 0x63, 0xa8, 0x4d, 0xfa, 0x3e, 0x0f, 0x46, 0x0f, 0xe2, 0xf5, 0x7e, 0x34, 0xfb, 0xc7, 0x54, 0x23, 0xc3, 0x73, 0x7f, 0x5b, 0x2a, 0x06, 0x15, 0xf5, 0x72, 0x2d, 0xb0 + ]), + unified_addr: "u1mtnedjgkz5ln6zzs7nrcyt8mertjundexqdxx52n2x4ww3v52s0akf3qy6sqlze3nexcjsxtcajglxcdwg47dsrrva6g5t4nf8u3sjchhkmsqghelysrn0cl52c2m8uuv3nyfdv258jjqnvd4lgqtugc8aqvpmt05c49qv2yqlhxvnq9phdamm4xv89cc7tzvzgmwltxxdsvme44dgzt8prkcwcsma8cdr76m8n0xwj02tpr9086a237xakkdf8fumsj8u4r6qlf0d59x0mw83ar36vrcr94zsherapa0566vd22", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 7, + diversifier_index: 10, + }, + TestVector { + p2pkh_bytes: Some([ + 0x65, 0x70, 0x4e, 0x3a, 0xb7, 0x67, 0xca, 0x57, 0x8e, 0x5b, 0x09, 0x2f, 0xb4, 0x76, 0x04, 0xf6, 0x59, 0x47, 0x5b, 0xae + ]), + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0x5f, 0x09, 0xa9, 0x80, 0x7a, 0x56, 0x32, 0x3b, 0x26, 0x3b, 0x05, 0xdf, 0x36, 0x8d, 0xc2, 0x83, 0x91, 0xb2, 0x1a, 0x64, 0xa0, 0xe1, 0xb4, 0x0f, 0x9a, 0x68, 0x03, 0xb7, 0xe6, 0x8f, 0x39, 0x05, 0x92, 0x3f, 0x35, 0xcb, 0x01, 0xf1, 0x19, 0xb2, 0x23, 0xf4, 0x93 + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1n9znrl4zyuvds24rcapzglzapqdlax4r8rgkvek0y0xlzfjfvn7zexelrafkchea24w030cr9jqsel7t8lvveaq7m7w4z0khmrlzc6748w9ldlccy02scd5xngtcv2yy4ctnyu9zn5m", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 8, + diversifier_index: 0, + }, + TestVector { + p2pkh_bytes: Some([ + 0xef, 0x85, 0xa6, 0x55, 0x3d, 0x89, 0xf1, 0x53, 0xb3, 0x7a, 0xfc, 0xab, 0x92, 0x8e, 0xb2, 0xbb, 0x5f, 0xb3, 0x37, 0xdb + ]), + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0x21, 0x00, 0x6c, 0xfb, 0xb3, 0xdb, 0x4f, 0x4b, 0xb6, 0x31, 0x11, 0xef, 0x63, 0xf7, 0xf8, 0x00, 0x56, 0xf3, 0x1b, 0x34, 0x4d, 0x06, 0xac, 0xa5, 0xb7, 0xfa, 0x07, 0x40, 0xc6, 0x60, 0xc8, 0xb2, 0xdc, 0x3b, 0xd2, 0x34, 0xf4, 0xc1, 0x8a, 0xe9, 0xea, 0xf8, 0x11 + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u19f2knszheph2dt8lrnwqeeq9krnw39pgz8syqv028ghtg7kjz6xvu23suv5hmdmj7e6fjuu6060y34fdw8ccjlp8gsqp0usyhrgw3reqfveet7hh2pqcjafysqqv2l3felj7sl7a7ym", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 8, + diversifier_index: 1, + }, + TestVector { + p2pkh_bytes: Some([ + 0xf9, 0x6a, 0x00, 0xef, 0x8b, 0x22, 0x33, 0x23, 0x69, 0x67, 0xa6, 0xa4, 0x3f, 0x07, 0xec, 0x60, 0x74, 0xf7, 0xfd, 0xc5 + ]), + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0x04, 0x91, 0x5d, 0x2b, 0xeb, 0xce, 0x11, 0x11, 0x1c, 0xe1, 0x95, 0x22, 0x6c, 0xde, 0x84, 0x40, 0x26, 0x3c, 0x50, 0x20, 0x4b, 0x22, 0x72, 0xac, 0x8a, 0x96, 0xb3, 0x8d, 0xbd, 0x70, 0xdb, 0x89, 0x69, 0xec, 0x9b, 0x6c, 0x87, 0xcd, 0x15, 0xd9, 0xd7, 0x65, 0x12 + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u160suxvjkgt22zcp7f9xw5f0axdu7rxdt5ktyexpn4cq70w4at2f74390mns7uksfenrdmcjjzqalyfky6tq05jv8mnamrkyxn9dcxe35z4x6m35cczmjcj55g0fc6a2thz03sjfywxa", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 8, + diversifier_index: 2, + }, + TestVector { + p2pkh_bytes: None, + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0xe3, 0x40, 0x63, 0x65, 0x42, 0xec, 0xe1, 0xc8, 0x12, 0x85, 0xed, 0x4e, 0xab, 0x44, 0x8a, 0xdb, 0xb5, 0xa8, 0xc0, 0xf4, 0xd3, 0x86, 0xee, 0xff, 0x33, 0x7e, 0x88, 0xe6, 0x91, 0x5f, 0x6c, 0x3e, 0xc1, 0xb6, 0xea, 0x83, 0x5a, 0x88, 0xd5, 0x66, 0x12, 0xd2, 0xbd + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1ddnjsdcpm36r6aq79n3s68shjweksnmwtdltrh046s8m6xcws9ygyawalxx8n6hg6vegk0wh8zjnafxgh6msppjsljvyt0ynece3lvm0", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 9, + diversifier_index: 0, + }, + TestVector { + p2pkh_bytes: None, + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0x3f, 0xad, 0xf8, 0xed, 0xb2, 0x0a, 0x33, 0x01, 0xe8, 0x26, 0x0a, 0xa3, 0x11, 0xf4, 0xcb, 0xd5, 0x4d, 0x7d, 0x6a, 0x76, 0xba, 0xac, 0x88, 0xc2, 0x44, 0xb0, 0xb1, 0x21, 0xc6, 0xdc, 0x22, 0xa8, 0xbc, 0xce, 0x15, 0x89, 0x8e, 0x26, 0x78, 0x29, 0xfc, 0x1e, 0x01 + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1nztelxna9h7w0vtpd2xjhxt4lpu8s9cmdl8n8vcr7actf2ny45nd07cy8cyuhuvw3axcp545y0ktq9cezuzx84jyhex8dk4tdvwhu4dl", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 9, + diversifier_index: 1, + }, + TestVector { + p2pkh_bytes: None, + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0x98, 0x7f, 0xd7, 0x4a, 0x22, 0x56, 0xc5, 0x96, 0xa6, 0x6f, 0x83, 0xea, 0xff, 0x7b, 0xb0, 0x26, 0x28, 0x6e, 0x97, 0x2b, 0xe5, 0x6d, 0x3b, 0x50, 0xe3, 0x45, 0x97, 0x47, 0xdf, 0xba, 0x53, 0xff, 0xa0, 0xf2, 0x47, 0x32, 0xb4, 0xaa, 0x6c, 0xd4, 0x37, 0xa3, 0x17 + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1trxzh330wl8wkh92uwv508z0qfx270ruuar8fxeng7arry5d73q9ve6gfud36s9nc4qj3uvn082l9srrfayjhnf20mmunywtvqzgc90c", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 9, + diversifier_index: 2, + }, + TestVector { + p2pkh_bytes: None, + p2sh_bytes: None, + sapling_raw_addr: Some([ + 0x99, 0xae, 0x33, 0x3d, 0xb1, 0x07, 0x4f, 0xca, 0x1a, 0x6a, 0x94, 0xbe, 0xd3, 0xea, 0x54, 0x8c, 0x1d, 0xb2, 0x51, 0x2d, 0xfb, 0xe7, 0x5a, 0xf9, 0xe8, 0x4c, 0x16, 0x22, 0x60, 0xb4, 0x81, 0x3b, 0xb6, 0xbd, 0xb4, 0x44, 0x39, 0x69, 0xda, 0xa5, 0x71, 0x3f, 0xf1 + ]), + orchard_raw_addr: Some([ + 0xcd, 0xf7, 0xfe, 0xd0, 0xd0, 0x82, 0x2f, 0xd8, 0x49, 0xcf, 0xfb, 0x20, 0xa4, 0xd5, 0xee, 0x70, 0x1a, 0xd8, 0x14, 0x1e, 0x66, 0xd8, 0x1d, 0xdf, 0xab, 0xf8, 0x78, 0x75, 0x11, 0x7c, 0x05, 0x09, 0x22, 0x40, 0x60, 0x3c, 0x54, 0x6b, 0x8d, 0xc1, 0x87, 0xcd, 0x8c + ]), + unknown_typecode: Some(65532), + unknown_bytes: Some(&[ + 0x65, 0x7b, 0x43, 0xee, 0x8d, 0xa6, 0x45, 0x44, 0x38, 0x14, 0xcc, 0x73, 0x29, 0xf3, 0xe9, 0xb4, 0xe5, 0x4c, 0x23, 0x6c, 0x29, 0xaf, 0x39, 0x23, 0x10, 0x17, 0x56, 0xd9, 0xfa, 0x4b, 0xd0, 0xf7, 0xd2, 0xdd, 0xaa, 0xcb, 0x6b, 0x0f, 0x86, 0xa2, 0x65, 0x8e, 0x0a, 0x07, 0xa0, 0x5a, 0xc5, 0xb9, 0x50, 0x05, 0x1c, 0xd2, 0x4c, 0x47, 0xa8, 0x8d, 0x13, 0xd6, 0x59, 0xba, 0x2a, 0x46, 0xca, 0x18, 0x30, 0x81, 0x6d, 0x09, 0xcd, 0x76, 0x46, 0xf7, 0x6f, 0x71, 0x6a, 0xbe, 0xc5, 0xde + ]), + unified_addr: "u1xdrenc94696j8clxa2xnkdg8xd5t3y8s24urctyxu87vggv0u46qr4lkpnh7gqqdev9wwugt6xkv8c8du8ufhfl8nfjnzusf6cw20wpm85hlshmnmj2lkyhka9rua7qw7kr0xeajk7y2rlsuwl6z6l5l3wq3v6rrqt9e8zy7sc7pww45jznrj4xy6h9rp4kjy5xtl5upr30u4cyk58kv3t80k3p8w97k3e345h7avmjylxakx6sgyk5ss8th5kqay50ewav62eeep7tghzejaflsdstpwz55haex398jqpq27007me2", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 10, + diversifier_index: 0, + }, + TestVector { + p2pkh_bytes: None, + p2sh_bytes: None, + sapling_raw_addr: Some([ + 0x52, 0xd5, 0x8f, 0x91, 0x37, 0x6a, 0xa9, 0x80, 0xf2, 0xb9, 0xa6, 0x28, 0x3f, 0xf3, 0x57, 0xe8, 0x42, 0x46, 0xd6, 0x94, 0x23, 0x52, 0x18, 0x48, 0x86, 0x44, 0x9f, 0xfe, 0xa8, 0xfa, 0xd7, 0xe7, 0xca, 0x5b, 0x49, 0x0d, 0x09, 0x0a, 0x96, 0xe0, 0x32, 0x33, 0x92 + ]), + orchard_raw_addr: Some([ + 0xe4, 0xe0, 0x10, 0x51, 0xb9, 0x9c, 0x08, 0x50, 0x68, 0x34, 0x97, 0x1f, 0x80, 0xda, 0xde, 0xc4, 0x4a, 0x4d, 0xa1, 0x3e, 0xcd, 0xcb, 0xa6, 0x17, 0xf7, 0x7f, 0xc4, 0x8d, 0x25, 0x32, 0x4f, 0x57, 0xcb, 0x1d, 0x4d, 0x74, 0x24, 0x70, 0x5d, 0x57, 0x3c, 0xd6, 0x82 + ]), + unknown_typecode: Some(65532), + unknown_bytes: Some(&[ + 0x07, 0xfe, 0x9b, 0x52, 0x34, 0x10, 0x80, 0x6e, 0xa6, 0xf2, 0x88, 0xf8, 0x73, 0x6c, 0x23, 0x35, 0x7c, 0x85, 0xf4, 0x57, 0x91, 0xe1, 0x70, 0x80, 0x29, 0xd9, 0x82, 0x4d, 0x90, 0x70, 0x46, 0x07, 0xf3, 0x87, 0xa0, 0x3e, 0x49, 0xbf, 0x98, 0x36, 0x57, 0x44, 0x31, 0x34, 0x5a, 0x78, 0x77, 0xef, 0xaa, 0x8a, 0x08, 0xe7, 0x30, 0x81, 0xef, 0x8d, 0x62, 0xcb, 0x78, 0x0a, 0xb6, 0x88, 0x3a, 0x50, 0xa0, 0xd4, 0x70, 0x19, 0x0d, 0xfb, 0xa1, 0x0a, 0x85, 0x7f, 0x82, 0x84, 0x2d, 0x38 + ]), + unified_addr: "u1y647tzm2ms4stj8skfswfljmvatmhwqzjzl2uq5v3a78ys2mls2g9thdap4yfmr9tw6y5h9gnehzhpddyl43enmhd6xv2udcttqmas35l62jt2yar33jwr5eulchzxg3d8upf2raqcx3jup8s3dep6an5n5xh9ngdjfp4hjv8fwfhh34kvglsug57zf0duypq6ugmysw0mnhdg5fz9sndputdc7pdssg6k3ks76wrrnuu5najqxj8xchp5xv5ahfh3f2szrfl5cm6mslq2f69ja9r54plen209xwpdpwsvm6zep4gwl", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 10, + diversifier_index: 2, + }, + TestVector { + p2pkh_bytes: None, + p2sh_bytes: None, + sapling_raw_addr: Some([ + 0x7a, 0x53, 0xf5, 0x81, 0xd1, 0x59, 0x85, 0xd0, 0xaa, 0xfe, 0x13, 0x4a, 0xdb, 0x95, 0x40, 0xff, 0xd1, 0x9d, 0x96, 0x5a, 0x43, 0x97, 0x7e, 0xd4, 0xcb, 0x55, 0x2c, 0xac, 0x57, 0x40, 0xa9, 0x07, 0xea, 0x24, 0xa9, 0x15, 0x2b, 0x52, 0x26, 0x8f, 0xe8, 0xcc, 0x3f + ]), + orchard_raw_addr: Some([ + 0xb5, 0xa0, 0x53, 0xec, 0x1a, 0xb0, 0x62, 0x3c, 0xe0, 0x4f, 0x35, 0x0c, 0xbb, 0x26, 0x03, 0x13, 0x38, 0xde, 0xa9, 0x07, 0x45, 0x51, 0x43, 0x3a, 0xde, 0xb1, 0xbf, 0x3c, 0xb6, 0x7c, 0x1e, 0x93, 0x98, 0x2f, 0x42, 0xde, 0x82, 0x2e, 0xbe, 0x42, 0x99, 0x69, 0x2a + ]), + unknown_typecode: Some(65532), + unknown_bytes: Some(&[ + 0x25, 0xb3, 0xd6, 0xda, 0x05, 0x73, 0xd3, 0x16, 0xeb, 0x16, 0x0d, 0xc0, 0xb7, 0x16, 0xc4, 0x8f, 0xbd, 0x46, 0x7f, 0x75, 0xb7, 0x80, 0x14, 0x9a, 0xe8, 0x80, 0x8f, 0x4e, 0x68, 0xf5, 0x0c, 0x05, 0x36, 0xac, 0xdd, 0xf6, 0xf1, 0xae, 0xab, 0x01, 0x6b, 0x6b, 0xc1, 0xec, 0x14, 0x4b, 0x4e, 0x55, 0x3a, 0xcf, 0xd6, 0x70, 0xf7, 0x7e, 0x75, 0x5f, 0xc8, 0x8e, 0x06, 0x77, 0xe3, 0x1b, 0xa4, 0x59, 0xb4, 0x4e, 0x30, 0x77, 0x68, 0x95, 0x8f, 0xe3, 0x78, 0x9d, 0x41, 0xc2, 0xb1, 0xff + ]), + unified_addr: "u1m5jvynaxyrtk27mt23q0j4r8uf5dzzhlwf6qd4s7pfdclqnmgkaf82kqrch0p44kd97f9pmwnk6q3rnjnzvlwv2ll289ahzlee4zcnual03ntelg2q2wxlqc6ueav935j4j2rzv2gxcdh6lk67quzxnxt5ay9xh0qjc9575dptfs9luhhr0m9wms2taq2vnrryjdj3ht5cktwathcerl9kw25y89f3hffyr65rnfw0jk2ka7703m8wym0c04u6r0xgagpn7xzfaxttrwgftmztzln6y2qcdglk3u28dgrswywqne28g", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 10, + diversifier_index: 4, + }, + TestVector { + p2pkh_bytes: Some([ + 0x29, 0x4d, 0xbb, 0x37, 0xed, 0xd9, 0x2c, 0xe0, 0x46, 0xe2, 0x66, 0xe2, 0x2b, 0x0e, 0xd5, 0x30, 0xa4, 0x4b, 0x79, 0xc7 + ]), + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0x24, 0xfd, 0x59, 0xf3, 0x2b, 0x2d, 0x39, 0xdd, 0xe6, 0x6e, 0x46, 0xc3, 0x92, 0x06, 0xa3, 0x1b, 0xc0, 0x4f, 0xa5, 0xc6, 0x84, 0x79, 0x76, 0xea, 0x6b, 0xbd, 0x31, 0x63, 0xee, 0x14, 0xf5, 0x8f, 0x58, 0x4a, 0xcc, 0x13, 0x14, 0x79, 0xea, 0x55, 0x8d, 0x3f, 0x84 + ]), + unknown_typecode: Some(65531), + unknown_bytes: Some(&[ + 0x2f, 0xe8, 0x06, 0xb9, 0x45, 0x69, 0xcd, 0x40, 0x59, 0xf3, 0x96, 0xbf, 0x29, 0xb9, 0x9d, 0x0a, 0x40, 0xe5, 0xe1, 0x71, 0x1c, 0xa9, 0x44, 0xf7, 0x2d, 0x43, 0x6a, 0x10, 0x2f, 0xca, 0x4b, 0x97, 0x69, 0x3d, 0xa0, 0xb0, 0x86, 0xfe, 0x9d, 0x2e, 0x71, 0x62, 0x47, 0x0d, 0x02, 0xe0, 0xf0, 0x5d, 0x4b, 0xec, 0x95, 0x12, 0xbf, 0xb3, 0xf3, 0x83, 0x27, 0x29, 0x6e, 0xfa, 0xa7, 0x43, 0x28, 0xb1, 0x18, 0xc2, 0x74, 0x02, 0xc7, 0x0c, 0x3a, 0x90, 0xb4, 0x9a, 0xd4, 0xbb, 0xc6, 0x8e, 0x37, 0xc0, 0xaa, 0x7d, 0x9b, 0x3f, 0xe1, 0x77, 0x99, 0xd7, 0x3b, 0x84, 0x1e, 0x75, 0x17, 0x13, 0xa0, 0x29, 0x43, 0x90, 0x5a, 0xae, 0x08, 0x03, 0xfd, 0x69, 0x44, 0x2e, 0xb7, 0x68, 0x1e, 0xc2, 0xa0, 0x56, 0x00, 0x05, 0x4e, 0x92, 0xee, 0xd5, 0x55, 0x02, 0x8f, 0x21, 0xb6, 0xa1, 0x55, 0x26, 0x8a, 0x2d, 0xd6, 0x64, 0x0a, 0x69, 0x30, 0x1a, 0x52, 0xa3, 0x8d, 0x4d, 0x9f, 0x9f, 0x95, 0x7a, 0xe3, 0x5a, 0xf7, 0x16, 0x71, 0x18, 0x14, 0x1c, 0xe4, 0xc9, 0xbe, 0x0a, 0x6a, 0x49, 0x2f, 0xe7, 0x9f, 0x15, 0x81, 0xa1, 0x55, 0xfa, 0x3a, 0x2b, 0x9d, 0xaf, 0xd8, 0x2e, 0x65, 0x0b, 0x38, 0x6a, 0xd3, 0xa0, 0x8c, 0xb6, 0xb8, 0x31, 0x31, 0xac, 0x30, 0x0b, 0x08, 0x46, 0x35, 0x4a, 0x7e, 0xef, 0x9c, 0x41, 0x0e, 0x4b, 0x62, 0xc4, 0x7c, 0x54, 0x26, 0x90, 0x7d, 0xfc, 0x66, 0x85, 0xc5, 0xc9, 0x9b, 0x71, 0x41, 0xac, 0x62, 0x6a, 0xb4, 0x76, 0x1f, 0xd3, 0xf4, 0x1e, 0x72, 0x8e, 0x1a, 0x28, 0xf8, 0x9d, 0xb8, 0x9f + ]), + unified_addr: "u1tqx832p4wsfe9pd67ggm3qsmfuvdhqvw2259y7uwug7y0lpeu87fmgpqh3zmamex3fzs0d4ct4hhsg2csj5z0q5f3f7n656ap8e4nlng9c4440rz9s7ekxanfw6g84f7vu82fumtmlz3vstl2a9ufa0970k4knsz2wpsjt2xycqeay76pt4fx3ak9y7mps2q6qe2n2h7wkakxr7xu6vd36zhhzgln7ttmrzc0f9ye3jmyu2pp8l8rect87lfxj2fgckcwz3svdx70a947fz04kgu7e907enzrk676zdkdmuyw2kyrclkmj62kmyy2rjetpus7knmxfuu7z0m63uwfhdynhuu3yrjqu5y089v8zwnh60mw5ngc0kszdjmc339fk9mjn396m5ekv7h7td7fa0u9097xph3y5vth9af4sw6ykxdms84wr544mxxqtmgj027d9e8rnlrazge0kwyydyhder3chwhmaqjk9skuxgxzternw4xx962qed", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 11, + diversifier_index: 0, + }, + TestVector { + p2pkh_bytes: Some([ + 0x64, 0x62, 0xc9, 0xa3, 0x00, 0x3e, 0x4d, 0x0e, 0x0a, 0xb7, 0x64, 0x86, 0x0d, 0x8b, 0x71, 0xf8, 0xa3, 0x6a, 0x23, 0xff + ]), + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0x93, 0x3b, 0xf1, 0xeb, 0x8f, 0xc9, 0x9c, 0x38, 0x25, 0x1b, 0xd4, 0x2b, 0xb2, 0xe7, 0xe4, 0xaf, 0xe5, 0x26, 0x35, 0x2a, 0x9b, 0x02, 0x4f, 0x8d, 0x67, 0x1b, 0x4d, 0x33, 0x72, 0x77, 0x19, 0x4b, 0x52, 0x33, 0x8a, 0x91, 0xce, 0x47, 0x25, 0x03, 0xa4, 0x8a, 0x00 + ]), + unknown_typecode: Some(65531), + unknown_bytes: Some(&[ + 0xfd, 0xec, 0xa3, 0x64, 0xdd, 0x2f, 0x0f, 0x07, 0x39, 0xf0, 0x53, 0x45, 0x56, 0x48, 0x31, 0x99, 0xc7, 0x1f, 0x18, 0x93, 0x41, 0xac, 0x9b, 0x78, 0xa2, 0x69, 0x16, 0x42, 0x06, 0xa0, 0xea, 0x1c, 0xe7, 0x3b, 0xfb, 0x2a, 0x94, 0x2e, 0x73, 0x70, 0xb2, 0x47, 0xc0, 0x46, 0xf8, 0xe7, 0x5e, 0xf8, 0xe3, 0xf8, 0xbd, 0x82, 0x1c, 0xf5, 0x77, 0x49, 0x18, 0x64, 0xe2, 0x0e, 0x6d, 0x08, 0xfd, 0x2e, 0x32, 0xb5, 0x55, 0xc9, 0x2c, 0x66, 0x1f, 0x19, 0x58, 0x8b, 0x72, 0xa8, 0x95, 0x99, 0x71, 0x0a, 0x88, 0x06, 0x12, 0x53, 0xca, 0x28, 0x5b, 0x63, 0x04, 0xb3, 0x7d, 0xa2, 0xb5, 0x29, 0x4f, 0x5c, 0xb3, 0x54, 0xa8, 0x94, 0x32, 0x28, 0x48, 0xcc, 0xbd, 0xc7, 0xc2, 0x54, 0x5b, 0x7d, 0xa5, 0x68, 0xaf, 0xac, 0x87, 0xff, 0xa0, 0x05, 0xc3, 0x12, 0x24, 0x1c, 0x2d, 0x57, 0xf4, 0xb4, 0x5d, 0x64, 0x19, 0xf0, 0xd2, 0xe2, 0xc5, 0xaf, 0x33, 0xae, 0x24, 0x37, 0x85, 0xb3, 0x25, 0xcd, 0xab, 0x95, 0x40, 0x4f, 0xc7, 0xae, 0xd7, 0x05, 0x25, 0xcd, 0xdb, 0x41, 0x87, 0x2c, 0xfc, 0xc2, 0x14, 0xb1, 0x32, 0x32, 0xed, 0xc7, 0x86, 0x09, 0x75, 0x3d, 0xbf, 0xf9, 0x30, 0xeb, 0x0d, 0xc1, 0x56, 0x61, 0x2b, 0x9c, 0xb4, 0x34, 0xbc, 0x4b, 0x69, 0x33, 0x92, 0xde, 0xb8, 0x7c, 0x53, 0x04, 0x35, 0x31, 0x2e, 0xdc, 0xed, 0xc6, 0xa9, 0x61, 0x13, 0x33, 0x38, 0xd7, 0x86, 0xc4, 0xa3, 0xe1, 0x03, 0xf6, 0x01, 0x10, 0xa1, 0x6b, 0x13, 0x37, 0x12, 0x97, 0x04, 0xbf, 0x47, 0x54, 0xff, 0x6b, 0xa9, 0xfb, 0xe6, 0x59 + ]), + unified_addr: "u1adph5ua2pv8ghr7utshst0fm0ad7tj32y09t2nhxn2ccwm6hengck3w2vy34tvhqay7rlw8vcfh63f85lh7lz63l0c5vja49tu8vcxvx30re085n8jt5hcqh4g4ec77czl4c8nspqps2ac2g5kxhl4j5g6mz3vsvxrg74e8p9s8hhqu8u3gldhxvrxg2htykqc7ceh930f3edxsg49nctv2e36cne6qpkvxzfymh2el2eguw6kg7zvdu620rgk4cwyvt9hz7zpjk9wskjdpk6p3cpyx3yuf5lk46nx2fyqjca3vtz8d9df3tpmg74d90uv7pp09apfa5ep374clznmh2ne5suxtzk22cp7mvu9gtswpvx9wfst63s73yjwqu9cjenwntdsep0uqz2hgnh4xpq0rlllwgv8z70ke6z5zkwnjmlrzt6nsvhac4zz245rp3rkj9lmj8tvpfmd0zawy08dv3hqxxf8cr06x90amtgkh2ura0yyfwucu", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 11, + diversifier_index: 1, + }, + TestVector { + p2pkh_bytes: Some([ + 0xd5, 0xbc, 0xa9, 0xe4, 0xe5, 0x0b, 0xe0, 0xc1, 0x6b, 0xdf, 0xed, 0x7e, 0x5a, 0xca, 0x20, 0xad, 0x43, 0xa2, 0x3f, 0x20 + ]), + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0x5e, 0xf2, 0x81, 0x73, 0x81, 0x57, 0x1b, 0x0e, 0x85, 0x21, 0x59, 0x59, 0xf1, 0xfa, 0xd8, 0x7b, 0xf9, 0x9b, 0xfb, 0x07, 0x99, 0xd8, 0x1b, 0x28, 0x24, 0xfe, 0x4c, 0xc1, 0x0f, 0x07, 0x77, 0x6b, 0xbe, 0x73, 0x05, 0xe7, 0xc4, 0xc3, 0x93, 0x3b, 0xe4, 0x27, 0x0f + ]), + unknown_typecode: Some(65531), + unknown_bytes: Some(&[ + 0x51, 0xe6, 0x10, 0x62, 0x0f, 0x71, 0xcd, 0xa8, 0xfc, 0x87, 0x76, 0x25, 0xf2, 0xc5, 0xbb, 0x04, 0xcb, 0xe1, 0x22, 0x8b, 0x1e, 0x88, 0x6f, 0x40, 0x50, 0xaf, 0xd8, 0xfe, 0x94, 0xe9, 0x7d, 0x2e, 0x9e, 0x85, 0xc6, 0xbb, 0x74, 0x8c, 0x00, 0x42, 0xd3, 0x24, 0x9a, 0xbb, 0x13, 0x42, 0xbb, 0x0e, 0xeb, 0xf6, 0x20, 0x58, 0xbf, 0x3d, 0xe0, 0x80, 0xd9, 0x46, 0x11, 0xa3, 0x75, 0x09, 0x15, 0xb5, 0xdc, 0x6c, 0x0b, 0x38, 0x99, 0xd4, 0x12, 0x22, 0xba, 0xce, 0x76, 0x0e, 0xe9, 0xc8, 0x81, 0x8d, 0xed, 0x59, 0x9e, 0x34, 0xc5, 0x6d, 0x73, 0x72, 0xaf, 0x1e, 0xb8, 0x68, 0x52, 0xf2, 0xa7, 0x32, 0x10, 0x4b, 0xdb, 0x75, 0x07, 0x39, 0xde, 0x6c, 0x2c, 0x6e, 0x0f, 0x9e, 0xb7, 0xcb, 0x17, 0xf1, 0x94, 0x2b, 0xfc, 0x9f, 0x4f, 0xd6, 0xeb, 0xb6, 0xb4, 0xcd, 0xd4, 0xda, 0x2b, 0xca, 0x26, 0xfa, 0xc4, 0x57, 0x8e, 0x9f, 0x54, 0x34, 0x05, 0xac, 0xc7, 0xd8, 0x6f, 0xf5, 0x91, 0x58, 0xbd, 0x0c, 0xba, 0x3a, 0xef, 0x6f, 0x4a, 0x84, 0x72, 0xd1, 0x44, 0xd9, 0x9f, 0x8b, 0x8d, 0x1d, 0xed, 0xaa, 0x90, 0x77, 0xd4, 0xf0, 0x1d, 0x4b, 0xb2, 0x7b, 0xbe, 0x31, 0xd8, 0x8f, 0xbe, 0xfa, 0xc3, 0xdc, 0xd4, 0x79, 0x75, 0x63, 0xa2, 0x6b, 0x1d, 0x61, 0xfc, 0xd9, 0xa4, 0x64, 0xab, 0x21, 0xed, 0x55, 0x0f, 0xe6, 0xfa, 0x09, 0x69, 0x5b, 0xa0, 0xb2, 0xf1, 0x0e, 0xea, 0x64, 0x68, 0xcc, 0x6e, 0x20, 0xa6, 0x6f, 0x82, 0x6e, 0x3d, 0x14, 0xc5, 0x00, 0x6f, 0x05, 0x63, 0x88, 0x7f, 0x5e, 0x12, 0x89, 0xbe, 0x1b + ]), + unified_addr: "u12acx92vw49jek4lwwnjtzm0cssn2wxfneu7ryj4amd8kvnhahdrq0htsnrwhqvl92yg92yut5jvgygk0rqfs4lgthtycsewc4t57jyjn9p2g6ffxek9rdg48xe5kr37hxxh86zxh2ef0u2lu22n25xaf3a45as6mtxxlqe37r75mndzu9z2fe4h77m35c5mrzf4uqru3fjs39ednvw9ay8nf9r8g9jx8rgj50mj098exdyq803hmqsek3dwlnz4g5whc88mkvvjnfmjldjs9hm8rx89ctn5wxcc2e05rcz7m955zc7trfm07gr7ankf96jxwwfcqppmdefj8gc6508gep8ndrml34rdpk9tpvwzgdcv7lk2d70uh5jqacrpk6zsety33qcc554r3cls4ajktg03d9fye6exk8gnve562yadzsfmfh9d7v6ctl5ufm9ewpr6se25c47huk4fh2hakkwerkdd2yy3093snsgree5lt6smejfvse8v", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 11, + diversifier_index: 2, + }, + TestVector { + p2pkh_bytes: None, + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0x6e, 0xd9, 0x6d, 0x65, 0x37, 0x9d, 0x5e, 0xce, 0x65, 0x69, 0x01, 0xf5, 0xcb, 0x20, 0xcf, 0x55, 0x4c, 0xe1, 0x86, 0x00, 0xd4, 0xa1, 0xed, 0xcf, 0x68, 0x12, 0xf4, 0x45, 0x9d, 0x7f, 0xf7, 0x3c, 0xf2, 0xb8, 0x8c, 0xd8, 0x47, 0x6b, 0x75, 0xe8, 0xc0, 0x8d, 0x28 + ]), + unknown_typecode: Some(65535), + unknown_bytes: Some(&[ + 0x34, 0xd6, 0xe8, 0x4b, 0xf5, 0x9c, 0x1e, 0x04, 0x61, 0x9a, 0x7c, 0x23, 0xa9, 0x96, 0x94, 0x1d, 0x88, 0x9e, 0x46, 0x22, 0xa9, 0xb9, 0xb1, 0xd5, 0x9d, 0x5e, 0x31, 0x90, 0x94, 0x31, 0x8c, 0xd4, 0x05, 0xba, 0x27, 0xb7, 0xe2, 0xc0, 0x84, 0x76, 0x2d, 0x31, 0x45, 0x3e, 0xc4, 0x54, 0x9a, 0x4d, 0x97, 0x72, 0x9d, 0x03, 0x34, 0x60, 0xfc, 0xf8, 0x9d, 0x64, 0x94, 0xf2, 0xff, 0xd7, 0x89, 0xe9, 0x80, 0x82, 0xea, 0x5c, 0xe9, 0x53, 0x4b, 0x3a, 0xcd, 0x60, 0xfe, 0x49, 0xe3, 0x7e, 0x4f, 0x66, 0x69, 0x31, 0x67, 0x73, 0x19, 0xed, 0x89, 0xf8, 0x55, 0x88, 0x74, 0x1b, 0x31, 0x28, 0x90, 0x1a, 0x93, 0xbd, 0x78, 0xe4, 0xbe, 0x02, 0x25, 0xa9, 0xe2, 0x69, 0x2c, 0x77, 0xc9, 0x69, 0xed, 0x01, 0x76, 0xbd, 0xf9, 0x55, 0x59, 0x48, 0xcb, 0xd5, 0xa3, 0x32, 0xd0, 0x45, 0xde, 0x6b, 0xa6, 0xbf, 0x44, 0x90, 0xad, 0xfe, 0x74, 0x44, 0xcd, 0x46, 0x7a, 0x09, 0x07, 0x54, 0x17, 0xfc, 0xc0, 0x06, 0x2e, 0x49, 0xf0, 0x08, 0xc5, 0x1a, 0xd4, 0x22, 0x74, 0x39, 0xc1, 0xb4, 0x47, 0x6c, 0xcd, 0x8e, 0x97, 0x86, 0x2d, 0xab, 0x7b, 0xe1, 0xe8, 0xd3, 0x99, 0xc0, 0x5e, 0xf2, 0x7c, 0x6e, 0x22, 0xee, 0x27, 0x3e, 0x15, 0x78, 0x6e, 0x39, 0x4c, 0x8f, 0x1b, 0xe3, 0x16, 0x82, 0xa3, 0x01, 0x47, 0x96, 0x3a, 0xc8, 0xda, 0x8d, 0x41, 0xd8, 0x04, 0x25, 0x84, 0x26, 0xa3, 0xf7, 0x02, 0x89, 0xb8, 0xad, 0x19, 0xd8, 0xde, 0x13, 0xbe, 0x4e, 0xeb, 0xe3, 0xbd, 0x4c, 0x8a, 0x6f, 0x55, 0xd6, 0xe0, 0xc3, 0x73 + ]), + unified_addr: "u1uehkuaq6rpfgt4ed5zpvhczg9apgpmyk5eq9qg23j8w7jxkhdnqzacte6gu8zgzfzgxy48ryzus3wnkhfxrxmlhs34xde3f34uxcnv3y6dsgj288vu56xs9f6ghvqsgkhuwtz4kkfxj8pa27v5p3ttlst340zvwx9nj6s0zw8p3wwk3zh37dwc7znqz52gj2fpaapzxzyagah0aeyxwa9fxxvyyj6w989v96ymsgf7s8s6ej9346p60fcjzzynvf9rmxevumdvt8l9mvhdfz4u5j4h7e0zjr2sde7fu7z9s02447qg6qzllm22egnx6ej6qczkkk2ygvpy08un9ggp853sddp6vskrlar6sygxec5f6c2t2eu9zmc728esy4sj9z853gxuplr6hw7lpcwzk20d85vuflnhlfv8nr3020r0v9z83ryudsyjv66rttxq2cscqlrdxakrmpjptzcf", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 12, + diversifier_index: 0, + }, + TestVector { + p2pkh_bytes: None, + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0xb6, 0xf4, 0x81, 0x04, 0x2a, 0x78, 0x04, 0x62, 0xff, 0xa9, 0x6f, 0x81, 0xe1, 0x28, 0x89, 0x78, 0xe5, 0xf0, 0x5c, 0x79, 0x15, 0x87, 0xde, 0x7e, 0x95, 0x77, 0x29, 0xbc, 0xac, 0x6e, 0xb9, 0x58, 0x92, 0x53, 0x2b, 0x0f, 0xe1, 0x3e, 0x9c, 0x7e, 0xef, 0x6a, 0x24 + ]), + unknown_typecode: Some(65535), + unknown_bytes: Some(&[ + 0xd4, 0x56, 0x85, 0x18, 0x79, 0xf5, 0xfb, 0xc2, 0x82, 0xdb, 0x9e, 0x13, 0x48, 0x06, 0xbf, 0xf7, 0x1e, 0x11, 0xbc, 0x33, 0xab, 0x75, 0xdd, 0x6c, 0xa0, 0x67, 0xfb, 0x73, 0xa0, 0x43, 0xb6, 0x46, 0xa7, 0xcf, 0x39, 0xca, 0xb4, 0x92, 0x83, 0x86, 0x78, 0x6d, 0x2f, 0x24, 0x14, 0x1e, 0xe1, 0x20, 0xfd, 0xc3, 0x4d, 0x67, 0x64, 0xea, 0xfc, 0x66, 0x88, 0x0e, 0xe0, 0x20, 0x4f, 0x53, 0xcc, 0x11, 0x67, 0xed, 0x20, 0xb4, 0x3a, 0x52, 0xde, 0xa3, 0xca, 0x7c, 0xff, 0x8e, 0xf3, 0x5c, 0xd8, 0xe6, 0xd7, 0xc1, 0x11, 0xa6, 0x8e, 0xf4, 0x4b, 0xcd, 0x0c, 0x15, 0x13, 0xad, 0x47, 0xca, 0x61, 0xc6, 0x59, 0xcc, 0x5d, 0x32, 0x5b, 0x44, 0x0f, 0x6b, 0x9f, 0x59, 0xaf, 0xf6, 0x68, 0x79, 0xbb, 0x66, 0x88, 0xfd, 0x28, 0x59, 0x36, 0x2b, 0x18, 0x2f, 0x20, 0x7b, 0x31, 0x75, 0x96, 0x1f, 0x64, 0x11, 0xa4, 0x93, 0xbf, 0xfd, 0x04, 0x8e, 0x7d, 0x0d, 0x87, 0xd8, 0x2f, 0xe6, 0xf9, 0x90, 0xa2, 0xb0, 0xa2, 0x5f, 0x5a, 0xa0, 0x11, 0x1a, 0x6e, 0x68, 0xf3, 0x7b, 0xf6, 0xf3, 0xac, 0x2d, 0x26, 0xb8, 0x46, 0x86, 0xe5, 0x69, 0xd5, 0x8d, 0x99, 0xc1, 0x38, 0x35, 0x97, 0xfa, 0xd8, 0x11, 0x93, 0xc4, 0xc1, 0xb1, 0x6e, 0x6a, 0x90, 0xe2, 0xd5, 0x07, 0xcd, 0xfe, 0x6f, 0xbd, 0xaa, 0x86, 0x16, 0x3e, 0x9c, 0xf5, 0xde, 0x31, 0x00, 0xfb, 0xca, 0x7e, 0x8d, 0xa0, 0x47, 0xb0, 0x90, 0xdb, 0x9f, 0x37, 0x95, 0x2f, 0xbf, 0xee, 0x76, 0xaf, 0x61, 0x66, 0x81, 0x90, 0xbd, 0x52, 0xed, 0x49, 0x0e, 0x67, 0x7b + ]), + unified_addr: "u1m76hh3wch9vwctg92h0jjt8zu6dry4zl97q9q94huutng5sxyhlzgfj64jqnvla2vqrqe0ndt67td2kejv6zlcw9zeurexxs67l7y67p7mww2j2uvfsp6uynct2apcr0m9xrmswtktmgs3x2glvndrqazy0gyrp30j328h4m5gkju9rl3pfrtjn9tm8v0rzr6t8gkklqfxgwk976dvv4kh7hl5utp9gjryu8wwu80h733ss5cjwpeewdgd3l8h46c0c7hxz4c6daws3vurq2fj9h0hpjnycup9tu8nfahvqjxewyhyuzynnjxa7jrvw2ekdytqs7sn02gqx4vxtkjzfrcy67lkmr6p5kalj0g8apazeyzqw3ywppy9482wj8k4tm06573nr3h78ecq9n260g7c0hm5jm3ffa4g2vk0edpdsnemksdegxgt9s7h8v8pjmcp23rnahmzf8pxdtdt", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 12, + diversifier_index: 1, + }, + TestVector { + p2pkh_bytes: None, + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0xa8, 0xe5, 0x57, 0xa5, 0x8a, 0x19, 0x08, 0xeb, 0x8a, 0x1b, 0xb0, 0x78, 0xb7, 0x7a, 0x95, 0xc0, 0x32, 0xfe, 0x0a, 0x00, 0x69, 0xce, 0x8c, 0x89, 0xd3, 0xe7, 0x70, 0x5a, 0x48, 0xd2, 0xc0, 0x8f, 0x7b, 0x60, 0x4e, 0x5a, 0xf0, 0x21, 0x8d, 0x8c, 0xc9, 0xc8, 0xb8 + ]), + unknown_typecode: Some(65535), + unknown_bytes: Some(&[ + 0x51, 0x5d, 0x01, 0x43, 0x84, 0xaf, 0x07, 0x21, 0x9c, 0x7c, 0x0e, 0xe7, 0xfc, 0x7b, 0xfc, 0x79, 0xf3, 0x25, 0x64, 0x4e, 0x4d, 0xf4, 0xc0, 0xd7, 0xdb, 0x08, 0xe9, 0xf0, 0xbd, 0x02, 0x49, 0x43, 0xc7, 0x05, 0xab, 0xff, 0x89, 0x94, 0xbf, 0xa6, 0x05, 0xcf, 0xbc, 0x7e, 0xd7, 0x46, 0xa7, 0xd3, 0xf7, 0xc3, 0x7d, 0x9e, 0x8b, 0xdc, 0x43, 0x3b, 0x7d, 0x79, 0xe0, 0x8a, 0x12, 0xf7, 0x38, 0xa8, 0xf0, 0xdb, 0xdd, 0xfe, 0xf2, 0xf2, 0x65, 0x7e, 0xf3, 0xe4, 0x7d, 0x1b, 0x0f, 0xd1, 0x1e, 0x6a, 0x13, 0x31, 0x1f, 0xb7, 0x99, 0xc7, 0x9c, 0x64, 0x1d, 0x9d, 0xa4, 0x3b, 0x33, 0xe7, 0xad, 0x01, 0x2e, 0x28, 0x25, 0x53, 0x98, 0x78, 0x92, 0x62, 0x27, 0x5f, 0x11, 0x75, 0xbe, 0x84, 0x62, 0xc0, 0x14, 0x91, 0xc4, 0xd8, 0x42, 0x40, 0x6d, 0x0e, 0xc4, 0x28, 0x2c, 0x95, 0x26, 0x17, 0x4a, 0x09, 0x87, 0x8f, 0xe8, 0xfd, 0xde, 0x33, 0xa2, 0x96, 0x04, 0xe5, 0xe5, 0xe7, 0xb2, 0xa0, 0x25, 0xd6, 0x65, 0x0b, 0x97, 0xdb, 0xb5, 0x2b, 0xef, 0xb5, 0x9b, 0x1d, 0x30, 0xa5, 0x74, 0x33, 0xb0, 0xa3, 0x51, 0x47, 0x44, 0x44, 0x09, 0x9d, 0xaa, 0x37, 0x10, 0x46, 0x61, 0x32, 0x60, 0xcf, 0x33, 0x54, 0xcf, 0xcd, 0xad, 0xa6, 0x63, 0xec, 0xe8, 0x24, 0xff, 0xd7, 0xe4, 0x43, 0x93, 0x88, 0x6a, 0x86, 0x16, 0x5d, 0xdd, 0xdf, 0x2b, 0x4c, 0x41, 0x77, 0x35, 0x54, 0xc8, 0x69, 0x95, 0x26, 0x94, 0x08, 0xb1, 0x1e, 0x67, 0x37, 0xa4, 0xc4, 0x47, 0x58, 0x6f, 0x69, 0x17, 0x34, 0x46, 0xd8, 0xe4, 0x8b, 0xf8 + ]), + unified_addr: "u1c2tpmmdl49pdcfntc2e2gjaxmj2a0ackydlj9aeuqlet4erjdn2edwvtx6vd8nrkxjnvgckn4j3nx48p2gep5x23akrl2cv7u2un4vmjed9hav39taqgzyp602m3tpcv3uzdsjdyl8wxrjycx5aus8ypq2xja8yw0cf045n0zvwt3ajtgs2xyzjl6cq2245avkm26qjv72ta65h04etlp4ntdq87eu9efjx5v6gjsfvwrdt99m4lpu9j52t0h8yvpnzukuzdt89e3pg9cmderzh7tnahmw0rfyc37aqmd6dh24fnxmxagsj4mtz8jv3c3ch20xu4k6whwfsaf2sra4ktgdej9p6kqz05ae3vl3f93xsfx05xpaf884h56epcetx627jttgx2499vc0uzxl83hcdt92z4hy5la40ervrpha4kn3kxxwrngdj76u6mrfcmt4737czn08vd60k5gj", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 12, + diversifier_index: 2, + }, + TestVector { + p2pkh_bytes: None, + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0x51, 0x78, 0x92, 0x4f, 0x70, 0x67, 0xea, 0xc2, 0x61, 0x04, 0x4c, 0xa2, 0x7b, 0xa3, 0xcf, 0x52, 0xf7, 0x98, 0x48, 0x69, 0x73, 0xaf, 0x07, 0x95, 0xe6, 0x15, 0x87, 0xaa, 0x1b, 0x1e, 0xca, 0xd3, 0x33, 0xdc, 0x52, 0x04, 0x97, 0xed, 0xc6, 0x1d, 0xf8, 0x89, 0x80 + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1dqavtnjvu42hlsjw6sc2mxajqlyt03zg8l4luykz9fnchunq74nqxhfp58h5n5xfpyqhheax8thta8lfkjgp8wqwsavc0g4mgu4du02c", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 13, + diversifier_index: 0, + }, + TestVector { + p2pkh_bytes: None, + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0x90, 0x76, 0x39, 0x19, 0x33, 0x11, 0xa8, 0x47, 0x36, 0x6c, 0x1a, 0x43, 0xeb, 0xaa, 0xdd, 0x93, 0x5a, 0x53, 0x18, 0x0f, 0xd3, 0xe1, 0x21, 0x9c, 0x07, 0xc8, 0x20, 0x5f, 0x45, 0x07, 0x7b, 0xc1, 0x76, 0x8a, 0xbd, 0xcf, 0x24, 0x25, 0xa4, 0xa1, 0x3c, 0x4a, 0xba + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1q8g29qhrktunc24lud3fgk007u7ya8q5g8vy9awadxtl7wu5vjllr4mmdfwk0zdh8zqxgl93sthzumeanzzkdqmqdft6ryhwtvqyqt3e", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 13, + diversifier_index: 1, + }, + TestVector { + p2pkh_bytes: None, + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0x28, 0x09, 0xdd, 0xfc, 0x7d, 0xb7, 0x0c, 0x66, 0x0a, 0x6c, 0x3f, 0xc7, 0x56, 0x0c, 0x7a, 0xdd, 0x1c, 0x78, 0x89, 0xd9, 0xb2, 0x77, 0xcb, 0x92, 0xd1, 0x4c, 0xb4, 0x0d, 0x2d, 0xe0, 0x0a, 0xae, 0x31, 0x67, 0x0b, 0x75, 0x3a, 0x42, 0xbd, 0xcd, 0xc3, 0xc2, 0x20 + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u13j3q8q8f9hx2nx0w9l52dqksy4png7fgm0lqjh8ahn9enyvz5z9xnwzdcdjmpf756s2y88rnyr9px4f4k9w03sl6fr4vwsqcvg8ggfjx", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 13, + diversifier_index: 2, + }, + TestVector { + p2pkh_bytes: Some([ + 0xe8, 0x22, 0x5b, 0x81, 0x7c, 0xdc, 0xfd, 0x01, 0x30, 0x7c, 0x66, 0xca, 0x35, 0x18, 0x8e, 0x9b, 0x1a, 0xc2, 0x38, 0xca + ]), + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0xb2, 0x08, 0xc9, 0x23, 0x5c, 0x8d, 0x40, 0xe4, 0x9b, 0x76, 0x10, 0x0b, 0x2d, 0x01, 0x0f, 0x37, 0x83, 0xf1, 0x2c, 0x66, 0xe7, 0xd3, 0xbe, 0xb1, 0x17, 0xb2, 0xc9, 0x63, 0x21, 0xb7, 0xf6, 0x56, 0x2a, 0xdb, 0x4e, 0xfc, 0x14, 0x4e, 0x39, 0xd9, 0x09, 0xe7, 0x28 + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1ukslldhknrzmvpdmn03u03edgfy976w3muurfs9asvh3n9uh9h6sgle6m7yjgf3wafxtvke08u735v4nd3kjqnyulw7cvxh6ke357knyjudgqtes6kcw7y28e6kewr03pjah5mh26na", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 14, + diversifier_index: 0, + }, + TestVector { + p2pkh_bytes: Some([ + 0x38, 0x69, 0x04, 0x8b, 0xd2, 0x2a, 0x3c, 0x3e, 0x6b, 0xc8, 0x84, 0x33, 0x3b, 0x0a, 0x71, 0xb0, 0x5f, 0x7f, 0x41, 0x25 + ]), + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0x33, 0x2f, 0x45, 0x1d, 0xc6, 0xf7, 0xda, 0x17, 0xfe, 0x5f, 0xf4, 0x07, 0x7d, 0x3d, 0x5d, 0xb7, 0x9a, 0x03, 0x6e, 0x71, 0x2d, 0xf5, 0x58, 0x85, 0x3d, 0x4a, 0x85, 0x4a, 0xc4, 0xf6, 0xe5, 0x14, 0x74, 0xcf, 0x75, 0xf3, 0x8f, 0xa9, 0x7c, 0x22, 0xb4, 0xcf, 0x09 + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1a0dnfvgdp4khm5yk79ltkkvp8jjmjykjy38cdue8ktl8askwenl4lzfyu0p7end0guyu6up57wylzns0tpr99wz5z8edh5u0m4yzuusysr3d2xczwkp82atq3vfw45u2yvtau852lnw", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 14, + diversifier_index: 1, + }, + TestVector { + p2pkh_bytes: Some([ + 0x59, 0xe9, 0x19, 0xce, 0x60, 0x11, 0x0f, 0x97, 0x70, 0x7c, 0x5c, 0x23, 0x2b, 0x7d, 0x4d, 0xb1, 0x9e, 0x32, 0xc3, 0xed + ]), + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0x3b, 0x68, 0xc2, 0x9b, 0x4a, 0x13, 0x8b, 0x28, 0x9f, 0xea, 0x8b, 0x67, 0x95, 0xe6, 0x47, 0x59, 0xa7, 0xcd, 0x7c, 0x0a, 0xaf, 0x4b, 0xb9, 0x8e, 0xd3, 0x07, 0x99, 0x59, 0xb0, 0xbb, 0xa9, 0xb7, 0x61, 0x70, 0x4b, 0x6c, 0xfc, 0x14, 0x65, 0xad, 0x74, 0xbb, 0x05 + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1a84vn0qes8q3jhk7zxs2whd2p922far8kztqdapergs5ej8rarn53v5ddnd6t7e3l5efhaefrhkptatnzq565nrpvf7kn2787gdvervmk08azp4qgehaew2zplkxkkyu36l3v7drg2v", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 14, + diversifier_index: 2, + }, + TestVector { + p2pkh_bytes: None, + p2sh_bytes: None, + sapling_raw_addr: Some([ + 0xee, 0xe1, 0x96, 0x41, 0xbc, 0x6b, 0x80, 0x2f, 0x35, 0x3e, 0xb7, 0x93, 0xf7, 0x28, 0xb1, 0x7a, 0x27, 0x7e, 0xf0, 0x35, 0x86, 0x96, 0xa2, 0x4a, 0x71, 0x22, 0xbc, 0x56, 0x53, 0x7b, 0x22, 0x96, 0x47, 0xf3, 0x81, 0x0d, 0x27, 0xce, 0x45, 0x22, 0x7c, 0x6f, 0x39 + ]), + orchard_raw_addr: None, + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u187vrwl4ampyxd5m6aj38n4ndkmj8v6gs97hkt23aps3sn5k89a0gk2smluexgdprcrtm56ezc5c7tjwlrnnl79tjtrxmqd42c5mpyz7g", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 15, + diversifier_index: 0, + }, + TestVector { + p2pkh_bytes: None, + p2sh_bytes: None, + sapling_raw_addr: Some([ + 0x50, 0xca, 0x46, 0xf8, 0x25, 0xf7, 0xf4, 0x23, 0x00, 0x7a, 0xa4, 0x14, 0x71, 0x69, 0xb5, 0x29, 0xf0, 0x7f, 0x1c, 0x8e, 0xd6, 0x34, 0xfa, 0xfc, 0x81, 0x45, 0xa4, 0x81, 0x31, 0x77, 0xdd, 0x12, 0x57, 0xee, 0x8d, 0x8f, 0xc5, 0xf4, 0x4e, 0x9b, 0x56, 0x4f, 0x6a + ]), + orchard_raw_addr: None, + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1xd83nhheggwe78x3lvcygdl8cmwz3gfxnr02sytkxvfpwdep9dzl7vte48zhkx39s705yqp20rw4l835fhg3ylkde44l7glt3cyps5wk", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 15, + diversifier_index: 2, + }, + TestVector { + p2pkh_bytes: None, + p2sh_bytes: None, + sapling_raw_addr: Some([ + 0xc4, 0x12, 0xc8, 0xff, 0x78, 0xf2, 0x8d, 0x9b, 0x33, 0x91, 0xf4, 0xab, 0x15, 0xd0, 0x6a, 0xcf, 0x46, 0xac, 0x05, 0x28, 0x21, 0xee, 0x09, 0x6a, 0x51, 0x52, 0x48, 0x13, 0xf2, 0xad, 0xf9, 0xa4, 0x06, 0x5c, 0xc6, 0xc4, 0x5f, 0xeb, 0xa2, 0xc0, 0x52, 0xdf, 0x9e + ]), + orchard_raw_addr: None, + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1w7x9ttwvk30grems6ae3rhgs6xytrrueaklyc5t509fpux7043fzla70jehhxyn4mg9d3ym095s3wghl9trvvdmu56yn74ajqy38ufjg", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 15, + diversifier_index: 3, + }, + TestVector { + p2pkh_bytes: Some([ + 0xf4, 0x41, 0x22, 0x8e, 0xe2, 0x6a, 0x3a, 0x7d, 0x0d, 0x00, 0xe4, 0xd6, 0x5b, 0xa4, 0x9e, 0x3a, 0xa4, 0x87, 0x7e, 0xb8 + ]), + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0x25, 0x98, 0xd8, 0x4d, 0xff, 0xb3, 0x4f, 0x59, 0x08, 0xb9, 0x07, 0x32, 0x49, 0x0f, 0x38, 0x81, 0x39, 0x91, 0x50, 0xd4, 0xc6, 0x94, 0xfc, 0xe9, 0xbf, 0x30, 0xd1, 0x56, 0x0b, 0x2c, 0x56, 0xf0, 0x98, 0x29, 0xfe, 0x12, 0x3b, 0x9a, 0xdd, 0x20, 0xe5, 0xd7, 0x1c + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1smpx6drvevct3dyrer7esjlct99lf4nxdeltdetyxjdrmtqag7q7mkrd8rxlvj9e5vy0qy24fhvvvrj7agfdgxapefxe72xl8vuu9ds5yfq0p86r3y0jw4suurzjz5s6lzrxkfft4am", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 16, + diversifier_index: 0, + }, + TestVector { + p2pkh_bytes: Some([ + 0x9f, 0x1f, 0x85, 0x26, 0x79, 0x2b, 0x04, 0xef, 0xdd, 0xa3, 0xb3, 0x89, 0x81, 0x86, 0x73, 0x97, 0xac, 0x11, 0xe3, 0xc0 + ]), + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0xc1, 0x15, 0x0a, 0xe8, 0x52, 0x9e, 0x66, 0x70, 0x15, 0xc4, 0x62, 0xf9, 0x1f, 0xb2, 0x6e, 0x91, 0x24, 0x09, 0x5a, 0xeb, 0xd6, 0xe7, 0x2f, 0xca, 0x95, 0xa2, 0xfe, 0x17, 0xae, 0x53, 0xe8, 0xcb, 0x10, 0x1e, 0xda, 0x84, 0xd9, 0xfb, 0x4d, 0x33, 0x6e, 0xe1, 0x03 + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1ymxkv9nks7tuzjt265fg8vctdq5nxqw4l0q2xj2ya5dkt660rrzkg032v5duhgeqae6cnh9tzxry4dspv8yvtq5lem9gujysaz64034mavd8p0ejqhnvp2jg34nt24y2c2whclxxk94", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 16, + diversifier_index: 1, + }, + TestVector { + p2pkh_bytes: Some([ + 0x61, 0x49, 0xd0, 0x37, 0x3c, 0x63, 0xfd, 0xdd, 0x4f, 0xca, 0x3b, 0x9f, 0x54, 0x07, 0xad, 0x22, 0xab, 0xda, 0x0d, 0xf2 + ]), + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0xe9, 0x61, 0x94, 0x4a, 0x70, 0x8a, 0x15, 0xc9, 0xc6, 0x27, 0x34, 0xc3, 0x45, 0x10, 0xbb, 0x5e, 0x2c, 0xd7, 0x40, 0xab, 0xde, 0xb4, 0x88, 0xe4, 0x14, 0x2b, 0x5d, 0x40, 0x2b, 0x02, 0x95, 0xbe, 0xc6, 0x79, 0x22, 0xf1, 0xe7, 0x1a, 0xb7, 0xfb, 0xd0, 0xa2, 0xae + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u14j8rtl62a70skh0nhzv7tasxsa69axm0vlac37ye3mcgfpjk6k9ury7hlmet0grhvhedtfj27xmsygp06pcm932f8sc33u5uwps57d89667kyhwmj8pucp5r8cel2lhuaxmx5ftm2nt", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 16, + diversifier_index: 2, + }, + TestVector { + p2pkh_bytes: Some([ + 0x0e, 0xb9, 0x65, 0x1c, 0x00, 0x37, 0x76, 0xab, 0x5d, 0x1e, 0x93, 0xc2, 0x77, 0x9d, 0x10, 0xa0, 0xbd, 0xc3, 0xbb, 0x77 + ]), + p2sh_bytes: None, + sapling_raw_addr: Some([ + 0xd3, 0xa8, 0x03, 0x80, 0x3f, 0xee, 0xe7, 0xa0, 0x32, 0xa2, 0x4a, 0xdf, 0xaa, 0x8f, 0x6a, 0x94, 0xce, 0xcb, 0x96, 0x71, 0xc1, 0x33, 0x3d, 0x0d, 0x5d, 0x1a, 0x3d, 0x79, 0xd8, 0x2b, 0xc3, 0x10, 0x72, 0x7c, 0x66, 0x53, 0x64, 0xd7, 0x10, 0x22, 0x55, 0x9c, 0x50 + ]), + orchard_raw_addr: Some([ + 0x7c, 0x98, 0xb8, 0xf6, 0x13, 0xf9, 0xff, 0x02, 0x74, 0x6b, 0xea, 0x2a, 0x16, 0x7c, 0xfd, 0x1b, 0xd3, 0xa1, 0x86, 0x2a, 0xf9, 0x63, 0x1b, 0xf6, 0x1d, 0x9d, 0x60, 0x4e, 0x08, 0x24, 0xe2, 0xcb, 0x84, 0x67, 0xa1, 0xe5, 0x49, 0xdb, 0x87, 0xa7, 0x6e, 0x7a, 0x8a + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1xjkw3lwwf9crx8cz050gdwfejufzhcusc37ged99w8fyj7tyx3e7hgmauyuv538dak2sepq6wjv4tyyjnhcef02dr682y5dsuzuftsx83lrvfc6dxd0kk260m4p3c9ka96vf3z9u6axvsj47mfd6kszy39e5gma28yg88yp92kxjt8ah0x329j4gxjdfyn0n2wp3urwrxxz6z0ynx82", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 17, + diversifier_index: 0, + }, + TestVector { + p2pkh_bytes: Some([ + 0x69, 0xf4, 0x8a, 0x49, 0x74, 0xe8, 0x07, 0x58, 0xed, 0x43, 0x55, 0x92, 0xa1, 0xdd, 0x4e, 0x4b, 0x38, 0x82, 0x6c, 0xbc + ]), + p2sh_bytes: None, + sapling_raw_addr: Some([ + 0x25, 0xc2, 0x5d, 0x58, 0xc5, 0x05, 0x33, 0xdf, 0xb5, 0x5d, 0x29, 0xf9, 0xa8, 0x86, 0x4f, 0x58, 0xf0, 0x2e, 0xa4, 0xfe, 0xd4, 0x43, 0x69, 0x35, 0x2c, 0x43, 0x53, 0x8c, 0xdf, 0x95, 0x45, 0xb9, 0x05, 0xbb, 0x2e, 0xf0, 0x96, 0x1b, 0xd2, 0xda, 0xf2, 0x58, 0x83 + ]), + orchard_raw_addr: Some([ + 0x8a, 0x1b, 0xff, 0x2a, 0x9d, 0x92, 0x1e, 0x11, 0x53, 0xb3, 0xcb, 0x26, 0x4b, 0xc0, 0x51, 0x85, 0xa9, 0x81, 0x1d, 0xe9, 0x11, 0xd5, 0x34, 0x67, 0x93, 0x54, 0x34, 0xd6, 0x53, 0x7d, 0x30, 0x67, 0x52, 0xd0, 0x20, 0x54, 0xfe, 0x5a, 0x17, 0x04, 0x64, 0x25, 0x9d + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1p4c4u3uz2vtkedv78d4phjav86exankz0x9wmrmz8q4mxqaf43gwd0qt486jk5jvpvyccc6lyy2vaq3ht8ngnw4vusryxd9erhhl2uy5x6x4huyfdymwxj7dkyyeut8ld36kxwu3v5wjg7jwp9kr8ul7u3xdakfunvmwq0rkv6y4k0ngm2n24x763uurfmrr685welsefyys2xwp8ug", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 17, + diversifier_index: 1, + }, + TestVector { + p2pkh_bytes: Some([ + 0xf1, 0xbc, 0x3d, 0x72, 0x61, 0xbf, 0x77, 0xfe, 0x80, 0x8e, 0x2b, 0x71, 0x78, 0x98, 0x1c, 0x7c, 0xfe, 0x55, 0x70, 0xfd + ]), + p2sh_bytes: None, + sapling_raw_addr: Some([ + 0x54, 0xb7, 0xfc, 0x0c, 0x85, 0xd3, 0x78, 0xf3, 0x75, 0xbe, 0x48, 0x21, 0x8a, 0x85, 0x42, 0x4b, 0xb9, 0xe7, 0xa3, 0x04, 0x83, 0x0e, 0x9e, 0xb7, 0x25, 0x5a, 0x12, 0xa0, 0x9c, 0x96, 0x1c, 0xca, 0x1f, 0x62, 0x9b, 0x86, 0x7e, 0x13, 0x24, 0x2e, 0xd9, 0x0d, 0x92 + ]), + orchard_raw_addr: Some([ + 0x14, 0xad, 0xca, 0x6f, 0x61, 0x6a, 0xbc, 0xbe, 0x5b, 0xc8, 0x50, 0xcc, 0x61, 0x7d, 0xcf, 0x99, 0x95, 0x17, 0xa9, 0xa7, 0x90, 0x29, 0x2f, 0xec, 0x6b, 0xc0, 0x76, 0x1e, 0xaa, 0x79, 0x03, 0x33, 0xe7, 0xd0, 0x6d, 0x01, 0x6d, 0xe0, 0x5b, 0xca, 0x7c, 0x67, 0x12 + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1ap7zakdnuefrgdglr334cw62hnqjkhr65t7tketyym0amkhdvyedpucuyxwu9z2te5vp0jf75jgsm36d7r09h6z3qe5rkgd8y28er6fz8z5rckspevxnx4y9wfk49njpcujh5gle7mfan90m9tt9a2gltyh8hx27cwt7h6u8ndmzhtk8qrq8hjytnakjqm0n658llh4z0277cyl2rcu", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 17, + diversifier_index: 2, + }, + TestVector { + p2pkh_bytes: Some([ + 0x40, 0x71, 0x58, 0xfc, 0x80, 0x43, 0x61, 0xfc, 0xb9, 0x65, 0xdf, 0xa4, 0x88, 0x2f, 0x0f, 0x1d, 0xf5, 0xa4, 0x9f, 0x47 + ]), + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0xa8, 0x04, 0x05, 0xd5, 0x56, 0x8a, 0xb8, 0xab, 0x8f, 0x85, 0x46, 0x16, 0x3d, 0x95, 0x1a, 0xb2, 0x97, 0xfd, 0x5e, 0x6f, 0x43, 0xe7, 0xfc, 0xeb, 0xcb, 0x66, 0x4f, 0xea, 0xcf, 0xab, 0x5a, 0xfd, 0x80, 0xaa, 0xf7, 0xf3, 0x54, 0xc0, 0x7a, 0x99, 0x01, 0x78, 0x8c + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1udmzarqn6y9026whk083lm5vs8pv282egeln6xg0n2a3w4klkpn6208h68ntuus7gp54d937u4f724v2xgdx6qeu74j45vxfn822xty2yyx6u0ecakj8r9uu3r2jqafj64w7updkhtq", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 18, + diversifier_index: 0, + }, + TestVector { + p2pkh_bytes: Some([ + 0xf5, 0x97, 0x98, 0x0d, 0x65, 0xca, 0x2e, 0xcd, 0x0f, 0xab, 0x53, 0x54, 0xe6, 0x6b, 0xa9, 0xd4, 0xcd, 0x50, 0xf4, 0x63 + ]), + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0x33, 0x11, 0x2c, 0xb9, 0x23, 0xb3, 0x19, 0x7a, 0x38, 0xc7, 0xa6, 0xeb, 0x50, 0xa8, 0x37, 0xb0, 0xa4, 0x49, 0x52, 0xfe, 0x31, 0xe5, 0x28, 0xa1, 0x51, 0x29, 0x94, 0xfc, 0xfa, 0x2b, 0x5f, 0x87, 0xb9, 0xc8, 0x6e, 0xd9, 0x23, 0x44, 0x26, 0xd3, 0xbb, 0xb5, 0x26 + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1fyvdgdehrx3gvjx5f2ez2lkcm0lcrfxg8hksdmg3g8zujfz8xk2kyhu4dafs99y96sq2t5c3d3zsxhhnlfmj6trmttg5awtwczz8g8xjr7u30hxc4nkyfyefyl4xt3dxdjevsnrkqdg", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 18, + diversifier_index: 1, + }, + TestVector { + p2pkh_bytes: Some([ + 0x3e, 0x7f, 0x16, 0x83, 0x6d, 0x93, 0xb5, 0x41, 0x74, 0x45, 0xad, 0x0f, 0xc9, 0xf7, 0xba, 0x02, 0x36, 0x17, 0xe2, 0xb3 + ]), + p2sh_bytes: None, + sapling_raw_addr: None, + orchard_raw_addr: Some([ + 0x0d, 0xac, 0xf7, 0xb7, 0x68, 0xab, 0xb0, 0x4a, 0x02, 0xb2, 0xe3, 0x0b, 0xf3, 0x14, 0x40, 0x30, 0x0b, 0x64, 0x27, 0x5f, 0x36, 0x77, 0xd0, 0x2e, 0x52, 0xba, 0x0f, 0x4c, 0xe7, 0x79, 0xca, 0xfe, 0xe8, 0xce, 0xea, 0x69, 0xac, 0xf2, 0xe1, 0xf0, 0xfe, 0xe9, 0x26 + ]), + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1kfzux4hf9favh8jmssqa2h04k87advldqz5ze7a8t4un3nkegklhz3ewzk6lmqg0uy7matdway9vn2q8q9rxp0fjwuewpcjtwrwavxjdsfxdvsk5nkx4q35atp0tfepfdsapqkk4en5", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 18, + diversifier_index: 2, + }, + TestVector { + p2pkh_bytes: Some([ + 0x29, 0xb0, 0x6b, 0x22, 0x8e, 0xb6, 0xb7, 0x0f, 0xda, 0x05, 0x1f, 0xf9, 0xe0, 0x1b, 0xcb, 0x27, 0x1b, 0x51, 0xc6, 0x83 + ]), + p2sh_bytes: None, + sapling_raw_addr: Some([ + 0x86, 0x60, 0x07, 0x0e, 0x37, 0x57, 0xff, 0x65, 0x07, 0x06, 0x07, 0x91, 0xfd, 0x69, 0x4f, 0x6a, 0x63, 0x1b, 0x84, 0x95, 0xa2, 0xb7, 0x4f, 0xfa, 0x39, 0x23, 0x6c, 0xf6, 0x53, 0xca, 0xea, 0x55, 0x75, 0xb8, 0x6a, 0xf3, 0x20, 0x0b, 0x01, 0x0e, 0x51, 0x3b, 0xab + ]), + orchard_raw_addr: None, + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1hrwrtyl3m8m2c6vkhu8wng43j5yvwweg37n2qstsqwc9dfw4vhs69m09064522758p44pfz42gu6hydjxua0wt0ge907sgrxkc9mft4gyfjevkhsyl4d8lnzgyd90arhx4t6v20zlfz", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 19, + diversifier_index: 5, + }, + TestVector { + p2pkh_bytes: Some([ + 0x29, 0x09, 0x9a, 0x65, 0x1d, 0x55, 0x61, 0xf8, 0x00, 0xe5, 0x8f, 0x3e, 0x33, 0xc2, 0x7f, 0x07, 0x8a, 0x98, 0x58, 0x1f + ]), + p2sh_bytes: None, + sapling_raw_addr: Some([ + 0x6d, 0x75, 0xa1, 0xa9, 0x48, 0xa4, 0xe7, 0x30, 0xdb, 0x3b, 0x4b, 0x81, 0x6d, 0xbc, 0x7d, 0x80, 0xb4, 0xeb, 0x1b, 0xc6, 0x8d, 0xe9, 0xac, 0x87, 0xb0, 0xcd, 0x1f, 0x1b, 0x3e, 0x60, 0x68, 0xe6, 0x77, 0x88, 0x8e, 0x10, 0x5a, 0xc7, 0x27, 0xc0, 0xd1, 0x4b, 0x49 + ]), + orchard_raw_addr: None, + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1rf4n5f682jspygln8r5pjwh6fmta7xz6n9x868f5wgc9prxsqkrh8jkpmn7wfnag56ml7czw68dv96299ft6s98p05u4jvdx3elyr83jqnzr603vw8yarptpg5pj73zlea0sksuje3r", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 19, + diversifier_index: 11, + }, + TestVector { + p2pkh_bytes: Some([ + 0x47, 0x54, 0x94, 0x43, 0x2c, 0x34, 0x37, 0xd5, 0x0e, 0xf2, 0x36, 0x23, 0xcb, 0x67, 0x67, 0x0f, 0xef, 0x27, 0xd8, 0xf5 + ]), + p2sh_bytes: None, + sapling_raw_addr: Some([ + 0x38, 0xb1, 0x4b, 0x44, 0xed, 0x6f, 0x4a, 0x3a, 0xe8, 0xc5, 0xc3, 0x92, 0x3e, 0x57, 0x70, 0xb7, 0x86, 0xf9, 0xb4, 0x1d, 0x46, 0xc6, 0x5a, 0x14, 0x9b, 0x13, 0x91, 0x0f, 0x4a, 0x0a, 0x64, 0xe8, 0x3b, 0xb9, 0xbc, 0x98, 0xe8, 0x0d, 0x95, 0x76, 0xfb, 0xf7, 0x6e + ]), + orchard_raw_addr: None, + unknown_typecode: None, + unknown_bytes: None, + unified_addr: "u1l6exm3zmfsr74sqvlwgc0zf7mydwf6z5r79amka84kfwzwef3wxs0yupl2lwhws85vdmqet3rtz795gpnm4h0jjfv4hanwqta0ezlxqe4p578a4aq09s93xhhtf3xhtrlh575qrsf5g", + root_seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + ], + account: 19, + diversifier_index: 15, + }, +]; diff --git a/components/zcash_address/src/kind/unified/fvk.rs b/components/zcash_address/src/kind/unified/fvk.rs new file mode 100644 index 0000000000..534d6c7834 --- /dev/null +++ b/components/zcash_address/src/kind/unified/fvk.rs @@ -0,0 +1,384 @@ +use alloc::vec::Vec; +use core::convert::{TryFrom, TryInto}; +use zcash_protocol::constants; + +use super::{ + private::{SealedContainer, SealedItem}, + Container, Encoding, ParseError, Typecode, +}; + +/// The set of known FVKs for Unified FVKs. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum Fvk { + /// The raw encoding of an Orchard Full Viewing Key. + /// + /// `(ak, nk, rivk)` each 32 bytes. + Orchard([u8; 96]), + + /// Data contained within the Sapling component of a Unified Full Viewing Key + /// + /// `(ak, nk, ovk, dk)` each 32 bytes. + Sapling([u8; 128]), + + /// A pruned version of the extended public key for the BIP 44 account corresponding to the + /// transparent address subtree from which transparent addresses are derived. This + /// includes just the chain code (32 bytes) and the compressed public key (33 bytes), and excludes + /// the depth of in the derivation tree, the parent key fingerprint, and the child key + /// number (which would reveal the wallet account number for which this UFVK was generated). + /// + /// Transparent addresses don't have "viewing keys" - the addresses themselves serve + /// that purpose. However, we want the ability to derive diversified Unified Addresses + /// from Unified Viewing Keys, and to not break the unlinkability property when they + /// include transparent receivers. To achieve this, we treat the last hardened node in + /// the BIP 44 derivation path as the "transparent viewing key"; all addresses derived + /// from this node use non-hardened derivation, and can thus be derived just from this + /// pruned extended public key. + P2pkh([u8; 65]), + + Unknown { + typecode: u32, + data: Vec, + }, +} + +impl TryFrom<(u32, &[u8])> for Fvk { + type Error = ParseError; + + fn try_from((typecode, data): (u32, &[u8])) -> Result { + let data = data.to_vec(); + match typecode.try_into()? { + Typecode::P2pkh => data.try_into().map(Fvk::P2pkh), + Typecode::P2sh => Err(data), + Typecode::Sapling => data.try_into().map(Fvk::Sapling), + Typecode::Orchard => data.try_into().map(Fvk::Orchard), + Typecode::Unknown(_) => Ok(Fvk::Unknown { typecode, data }), + } + .map_err(|e| { + ParseError::InvalidEncoding(format!("Invalid fvk for typecode {}: {:?}", typecode, e)) + }) + } +} + +impl SealedItem for Fvk { + fn typecode(&self) -> Typecode { + match self { + Fvk::P2pkh(_) => Typecode::P2pkh, + Fvk::Sapling(_) => Typecode::Sapling, + Fvk::Orchard(_) => Typecode::Orchard, + Fvk::Unknown { typecode, .. } => Typecode::Unknown(*typecode), + } + } + + fn data(&self) -> &[u8] { + match self { + Fvk::P2pkh(data) => data, + Fvk::Sapling(data) => data, + Fvk::Orchard(data) => data, + Fvk::Unknown { data, .. } => data, + } + } +} + +/// A Unified Full Viewing Key. +/// +/// # Examples +/// +/// ``` +/// use zcash_address::unified::{self, Container, Encoding}; +/// +/// # #[cfg(not(feature = "std"))] +/// # fn main() {} +/// # #[cfg(feature = "std")] +/// # fn main() -> Result<(), Box> { +/// # let ufvk_from_user = || "uview1cgrqnry478ckvpr0f580t6fsahp0a5mj2e9xl7hv2d2jd4ldzy449mwwk2l9yeuts85wjls6hjtghdsy5vhhvmjdw3jxl3cxhrg3vs296a3czazrycrr5cywjhwc5c3ztfyjdhmz0exvzzeyejamyp0cr9z8f9wj0953fzht0m4lenk94t70ruwgjxag2tvp63wn9ftzhtkh20gyre3w5s24f6wlgqxnjh40gd2lxe75sf3z8h5y2x0atpxcyf9t3em4h0evvsftluruqne6w4sm066sw0qe5y8qg423grple5fftxrqyy7xmqmatv7nzd7tcjadu8f7mqz4l83jsyxy4t8pkayytyk7nrp467ds85knekdkvnd7hqkfer8mnqd7pv"; +/// let example_ufvk: &str = ufvk_from_user(); +/// +/// let (network, ufvk) = unified::Ufvk::decode(example_ufvk)?; +/// +/// // We can obtain the pool-specific Full Viewing Keys for the UFVK in preference +/// // order (the order in which wallets should prefer to use their corresponding +/// // address receivers): +/// let fvks: Vec = ufvk.items(); +/// +/// // And we can create the UFVK from a list of FVKs: +/// let new_ufvk = unified::Ufvk::try_from_items(fvks)?; +/// assert_eq!(new_ufvk, ufvk); +/// # Ok(()) +/// # } +/// ``` +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Ufvk(pub(crate) Vec); + +impl Container for Ufvk { + type Item = Fvk; + + /// Returns the FVKs contained within this UFVK, in the order they were + /// parsed from the string encoding. + /// + /// This API is for advanced usage; in most cases you should use `Ufvk::receivers`. + fn items_as_parsed(&self) -> &[Fvk] { + &self.0 + } +} + +impl Encoding for Ufvk {} + +impl SealedContainer for Ufvk { + /// The HRP for a Bech32m-encoded mainnet Unified FVK. + /// + /// Defined in [ZIP 316][zip-0316]. + /// + /// [zip-0316]: https://zips.z.cash/zip-0316 + const MAINNET: &'static str = constants::mainnet::HRP_UNIFIED_FVK; + + /// The HRP for a Bech32m-encoded testnet Unified FVK. + /// + /// Defined in [ZIP 316][zip-0316]. + /// + /// [zip-0316]: https://zips.z.cash/zip-0316 + const TESTNET: &'static str = constants::testnet::HRP_UNIFIED_FVK; + + /// The HRP for a Bech32m-encoded regtest Unified FVK. + /// + /// Defined in [ZIP 316][zip-0316]. + /// + /// [zip-0316]: https://zips.z.cash/zip-0316 + const REGTEST: &'static str = constants::regtest::HRP_UNIFIED_FVK; + + fn from_inner(fvks: Vec) -> Self { + Self(fvks) + } +} + +#[cfg(test)] +mod tests { + use alloc::borrow::ToOwned; + use alloc::vec::Vec; + + use assert_matches::assert_matches; + + use proptest::{array::uniform1, array::uniform32, prelude::*, sample::select}; + + use super::{Fvk, ParseError, Typecode, Ufvk}; + use crate::kind::unified::{ + private::{SealedContainer, SealedItem}, + Container, Encoding, + }; + use zcash_protocol::consensus::NetworkType; + + prop_compose! { + fn uniform128()(a in uniform96(), b in uniform32(0u8..)) -> [u8; 128] { + let mut fvk = [0; 128]; + fvk[..96].copy_from_slice(&a); + fvk[96..].copy_from_slice(&b); + fvk + } + } + + prop_compose! { + fn uniform96()(a in uniform32(0u8..), b in uniform32(0u8..), c in uniform32(0u8..)) -> [u8; 96] { + let mut fvk = [0; 96]; + fvk[..32].copy_from_slice(&a); + fvk[32..64].copy_from_slice(&b); + fvk[64..].copy_from_slice(&c); + fvk + } + } + + prop_compose! { + fn uniform65()(a in uniform32(0u8..), b in uniform32(0u8..), c in uniform1(0u8..)) -> [u8; 65] { + let mut fvk = [0; 65]; + fvk[..32].copy_from_slice(&a); + fvk[32..64].copy_from_slice(&b); + fvk[64..].copy_from_slice(&c); + fvk + } + } + + pub fn arb_orchard_fvk() -> impl Strategy { + uniform96().prop_map(Fvk::Orchard) + } + + pub fn arb_sapling_fvk() -> impl Strategy { + uniform128().prop_map(Fvk::Sapling) + } + + fn arb_shielded_fvk() -> impl Strategy> { + prop_oneof![ + vec![arb_sapling_fvk().boxed()], + vec![arb_orchard_fvk().boxed()], + vec![arb_sapling_fvk().boxed(), arb_orchard_fvk().boxed()], + ] + } + + fn arb_transparent_fvk() -> BoxedStrategy { + uniform65().prop_map(Fvk::P2pkh).boxed() + } + + prop_compose! { + fn arb_unified_fvk()( + shielded in arb_shielded_fvk(), + transparent in prop::option::of(arb_transparent_fvk()), + ) -> Ufvk { + let mut items: Vec<_> = transparent.into_iter().chain(shielded).collect(); + items.sort_unstable_by(Fvk::encoding_order); + Ufvk(items) + } + } + + proptest! { + #[test] + fn ufvk_roundtrip( + network in select(vec![NetworkType::Main, NetworkType::Test, NetworkType::Regtest]), + ufvk in arb_unified_fvk(), + ) { + let encoded = ufvk.encode(&network); + let decoded = Ufvk::decode(&encoded); + prop_assert_eq!(decoded, Ok((network, ufvk))); + } + } + + #[test] + fn padding() { + // The test cases below use `Ufvk(vec![Fvk::Orchard([1; 96])])` as base. + + // Invalid padding ([0xff; 16] instead of [b'u', 0x00, 0x00, 0x00...]) + let invalid_padding = [ + 0x6b, 0x32, 0x44, 0xf1, 0xb, 0x67, 0xe9, 0x8f, 0x6, 0x57, 0xe3, 0x5, 0x17, 0xa0, 0x7, + 0x5c, 0xb0, 0xc9, 0x23, 0xcc, 0xb7, 0x54, 0xac, 0x55, 0x6a, 0x65, 0x99, 0x95, 0x32, + 0x97, 0xd5, 0x34, 0xa7, 0xc8, 0x6f, 0xc, 0xd7, 0x3b, 0xe0, 0x88, 0x19, 0xf3, 0x3e, + 0x26, 0x19, 0xd6, 0x5f, 0x9a, 0x62, 0xc9, 0x6f, 0xad, 0x3b, 0xe5, 0xdd, 0xf1, 0xff, + 0x5b, 0x4a, 0x13, 0x61, 0xc0, 0xd5, 0xa5, 0x87, 0xc5, 0x69, 0x48, 0xdb, 0x7e, 0xc6, + 0x4e, 0xb0, 0x55, 0x41, 0x3f, 0xc0, 0x53, 0xbb, 0x79, 0x8b, 0x24, 0xa0, 0xfa, 0xd1, + 0x6e, 0xea, 0x9, 0xea, 0xb3, 0xaf, 0x0, 0x7d, 0x86, 0x47, 0xdb, 0x8b, 0x38, 0xdd, 0x7b, + 0xdf, 0x63, 0xe7, 0xef, 0x65, 0x6b, 0x18, 0x23, 0xf7, 0x3e, 0x35, 0x7c, 0xf3, 0xc4, + ]; + assert_eq!( + Ufvk::parse_internal(Ufvk::MAINNET, &invalid_padding[..]), + Err(ParseError::InvalidEncoding( + "Invalid padding bytes".to_owned() + )) + ); + + // Short padding (padded to 15 bytes instead of 16) + let truncated_padding = [ + 0xdf, 0xea, 0x84, 0x55, 0xc3, 0x4a, 0x7c, 0x6e, 0x9f, 0x83, 0x3, 0x21, 0x14, 0xb0, + 0xcf, 0xb0, 0x60, 0x84, 0x75, 0x3a, 0xdc, 0xb9, 0x93, 0x16, 0xc0, 0x8f, 0x28, 0x5f, + 0x61, 0x5e, 0xf0, 0x8e, 0x44, 0xae, 0xa6, 0x74, 0xc5, 0x64, 0xad, 0xfa, 0xdc, 0x7d, + 0x64, 0x2a, 0x9, 0x47, 0x16, 0xf6, 0x5d, 0x8e, 0x46, 0xc4, 0xf0, 0x54, 0xfa, 0x5, 0x28, + 0x1e, 0x3d, 0x7d, 0x37, 0xa5, 0x9f, 0x8b, 0x62, 0x78, 0xf6, 0x50, 0x18, 0x63, 0xe4, + 0x51, 0x14, 0xae, 0x89, 0x41, 0x86, 0xd4, 0x9f, 0x10, 0x4b, 0x66, 0x2b, 0xf9, 0x46, + 0x9c, 0xeb, 0xe8, 0x90, 0x8, 0xad, 0xd9, 0x6c, 0x6a, 0xf1, 0xed, 0xeb, 0x72, 0x44, + 0x43, 0x8e, 0xc0, 0x3e, 0x9f, 0xf4, 0xf1, 0x80, 0x32, 0xcf, 0x2f, 0x7e, 0x7f, 0x91, + ]; + assert_eq!( + Ufvk::parse_internal(Ufvk::MAINNET, &truncated_padding[..]), + Err(ParseError::InvalidEncoding( + "Invalid padding bytes".to_owned() + )) + ); + } + + #[test] + fn truncated() { + // The test cases below start from an encoding of + // `Ufvk(vec![Fvk::Orchard([1; 96]), Fvk::Sapling([2; 96])])` + // with the fvk data truncated, but valid padding. + + // - Missing the last data byte of the Sapling fvk. + let truncated_sapling_data = vec![ + 0x43, 0xbf, 0x17, 0xa2, 0xb7, 0x85, 0xe7, 0x8e, 0xa4, 0x6d, 0x36, 0xa5, 0xf1, 0x1d, + 0x74, 0xd1, 0x40, 0x6e, 0xed, 0xbd, 0x6b, 0x51, 0x6a, 0x36, 0x9c, 0xb3, 0x28, 0xd, + 0x90, 0xa1, 0x1e, 0x3a, 0x67, 0xa2, 0x15, 0xc5, 0xfb, 0x82, 0x96, 0xf4, 0x35, 0x57, + 0x71, 0x5d, 0xbb, 0xac, 0x30, 0x1d, 0x1, 0x6d, 0xdd, 0x2e, 0xf, 0x8, 0x4b, 0xcf, 0x5, + 0xfe, 0x86, 0xd7, 0xa0, 0x9d, 0x94, 0x9f, 0x16, 0x5e, 0xa0, 0x3, 0x58, 0x81, 0x71, + 0x40, 0xe4, 0xb8, 0xfc, 0x64, 0x75, 0x80, 0x46, 0x4f, 0x51, 0x2d, 0xb2, 0x51, 0xf, + 0x22, 0x49, 0x53, 0x95, 0xbd, 0x7b, 0x66, 0xd9, 0x17, 0xda, 0x15, 0x62, 0xe0, 0xc6, + 0xf8, 0x5c, 0xdf, 0x75, 0x6d, 0x7, 0xb, 0xf7, 0xab, 0xfc, 0x20, 0x61, 0xd0, 0xf4, 0x79, + 0xfa, 0x4, 0xd3, 0xac, 0x8b, 0xf, 0x3c, 0x30, 0x23, 0x32, 0x37, 0x51, 0xc5, 0xfc, 0x66, + 0x7e, 0xe1, 0x9c, 0xa8, 0xec, 0x52, 0x57, 0x7e, 0xc0, 0x31, 0x83, 0x1c, 0x31, 0x5, + 0x1b, 0xc3, 0x70, 0xd3, 0x44, 0x74, 0xd2, 0x8a, 0xda, 0x32, 0x4, 0x93, 0xd2, 0xbf, + 0xb4, 0xbb, 0xa, 0x9e, 0x8c, 0xe9, 0x8f, 0xe7, 0x8a, 0x95, 0xc8, 0x21, 0xfa, 0x12, + 0x41, 0x2e, 0x69, 0x54, 0xf0, 0x7a, 0x9e, 0x20, 0x94, 0xa3, 0xaa, 0xc3, 0x50, 0x43, + 0xc5, 0xe2, 0x32, 0x8b, 0x2e, 0x4f, 0xbb, 0xb4, 0xc0, 0x7f, 0x47, 0x35, 0xab, 0x89, + 0x8c, 0x7a, 0xbf, 0x7b, 0x9a, 0xdd, 0xee, 0x18, 0x2c, 0x2d, 0xc2, 0xfc, + ]; + assert_matches!( + Ufvk::parse_internal(Ufvk::MAINNET, &truncated_sapling_data[..]), + Err(ParseError::InvalidEncoding(_)) + ); + + // - Truncated after the typecode of the Sapling fvk. + let truncated_after_sapling_typecode = [ + 0xac, 0x26, 0x5b, 0x19, 0x8f, 0x88, 0xb0, 0x7, 0xb3, 0x0, 0x91, 0x19, 0x52, 0xe1, 0x73, + 0x48, 0xff, 0x66, 0x7a, 0xef, 0xcf, 0x57, 0x9c, 0x65, 0xe4, 0x6a, 0x7a, 0x1d, 0x19, + 0x75, 0x6b, 0x43, 0xdd, 0xcf, 0xb9, 0x9a, 0xf3, 0x7a, 0xf8, 0xb, 0x23, 0x96, 0x64, + 0x8c, 0x57, 0x56, 0x67, 0x9, 0x40, 0x35, 0xcb, 0xb1, 0xa4, 0x91, 0x4f, 0xdc, 0x39, 0x0, + 0x98, 0x56, 0xa8, 0xf7, 0x25, 0x1a, 0xc8, 0xbc, 0xd7, 0xb3, 0xb0, 0xfa, 0x78, 0x6, + 0xe8, 0x50, 0xfe, 0x92, 0xec, 0x5b, 0x1f, 0x74, 0xb9, 0xcf, 0x1f, 0x2e, 0x3b, 0x41, + 0x54, 0xd1, 0x9e, 0xec, 0x8b, 0xef, 0x35, 0xb8, 0x44, 0xdd, 0xab, 0x9a, 0x8d, + ]; + assert_matches!( + Ufvk::parse_internal(Ufvk::MAINNET, &truncated_after_sapling_typecode[..]), + Err(ParseError::InvalidEncoding(_)) + ); + } + + #[test] + fn duplicate_typecode() { + // Construct and serialize an invalid Ufvk. This must be done using private + // methods, as the public API does not permit construction of such invalid values. + let ufvk = Ufvk(vec![Fvk::Sapling([1; 128]), Fvk::Sapling([2; 128])]); + let encoded = ufvk.to_jumbled_bytes(Ufvk::MAINNET); + assert_eq!( + Ufvk::parse_internal(Ufvk::MAINNET, &encoded[..]), + Err(ParseError::DuplicateTypecode(Typecode::Sapling)) + ); + } + + #[test] + fn only_transparent() { + // Raw encoding of `Ufvk(vec![Fvk::P2pkh([0; 65])])`. + let encoded = [ + 0xc4, 0x70, 0xc8, 0x7a, 0xcc, 0xe6, 0x6b, 0x1a, 0x62, 0xc7, 0xcd, 0x5f, 0x76, 0xd8, + 0xcc, 0x9c, 0x50, 0xbd, 0xce, 0x85, 0x80, 0xd7, 0x78, 0x25, 0x3e, 0x47, 0x9, 0x57, + 0x7d, 0x6a, 0xdb, 0x10, 0xb4, 0x11, 0x80, 0x13, 0x4c, 0x83, 0x76, 0xb4, 0x6b, 0xbd, + 0xef, 0x83, 0x5c, 0xa7, 0x68, 0xe6, 0xba, 0x41, 0x12, 0xbd, 0x43, 0x24, 0xf5, 0xaa, + 0xa0, 0xf5, 0xf8, 0xe1, 0x59, 0xa0, 0x95, 0x85, 0x86, 0xf1, 0x9e, 0xcf, 0x8f, 0x94, + 0xf4, 0xf5, 0x16, 0xef, 0x5c, 0xe0, 0x26, 0xbc, 0x23, 0x73, 0x76, 0x3f, 0x4b, + ]; + + assert_eq!( + Ufvk::parse_internal(Ufvk::MAINNET, &encoded[..]), + Err(ParseError::OnlyTransparent) + ); + } + + #[test] + fn fvks_are_sorted() { + // Construct a UFVK with fvks in an unsorted order. + let ufvk = Ufvk(vec![ + Fvk::P2pkh([0; 65]), + Fvk::Orchard([0; 96]), + Fvk::Unknown { + typecode: 0xff, + data: vec![], + }, + Fvk::Sapling([0; 128]), + ]); + + // `Ufvk::items` sorts the fvks in priority order. + assert_eq!( + ufvk.items(), + vec![ + Fvk::Orchard([0; 96]), + Fvk::Sapling([0; 128]), + Fvk::P2pkh([0; 65]), + Fvk::Unknown { + typecode: 0xff, + data: vec![], + }, + ] + ) + } +} diff --git a/components/zcash_address/src/kind/unified/ivk.rs b/components/zcash_address/src/kind/unified/ivk.rs new file mode 100644 index 0000000000..7b776b0008 --- /dev/null +++ b/components/zcash_address/src/kind/unified/ivk.rs @@ -0,0 +1,362 @@ +use alloc::vec::Vec; +use core::convert::{TryFrom, TryInto}; +use zcash_protocol::constants; + +use super::{ + private::{SealedContainer, SealedItem}, + Container, Encoding, ParseError, Typecode, +}; + +/// The set of known IVKs for Unified IVKs. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum Ivk { + /// The raw encoding of an Orchard Incoming Viewing Key. + /// + /// `(dk, ivk)` each 32 bytes. + Orchard([u8; 64]), + + /// Data contained within the Sapling component of a Unified Incoming Viewing Key. + /// + /// In order to ensure that Unified Addresses can always be derived from UIVKs, we + /// store more data here than was specified to be part of a Sapling IVK. Specifically, + /// we store the same data here as we do for Orchard. + /// + /// `(dk, ivk)` each 32 bytes. + Sapling([u8; 64]), + + /// A pruned version of the extended public key for the BIP 44 account corresponding to the + /// transparent address subtree from which transparent addresses are derived, + /// at the external `change` BIP 44 path, i.e. `m/44'/133'/'/0`. This + /// includes just the chain code (32 bytes) and the compressed public key (33 bytes), and excludes + /// the depth of in the derivation tree, the parent key fingerprint, and the child key + /// number (which would reveal the wallet account number for which this UFVK was generated). + /// + /// Transparent addresses don't have "viewing keys" - the addresses themselves serve + /// that purpose. However, we want the ability to derive diversified Unified Addresses + /// from Unified Viewing Keys, and to not break the unlinkability property when they + /// include transparent receivers. To achieve this, we treat the last hardened node in + /// the BIP 44 derivation path as the "transparent viewing key"; all addresses derived + /// from this node use non-hardened derivation, and can thus be derived just from this + /// pruned extended public key. + P2pkh([u8; 65]), + + Unknown { + typecode: u32, + data: Vec, + }, +} + +impl TryFrom<(u32, &[u8])> for Ivk { + type Error = ParseError; + + fn try_from((typecode, data): (u32, &[u8])) -> Result { + let data = data.to_vec(); + match typecode.try_into()? { + Typecode::P2pkh => data.try_into().map(Ivk::P2pkh), + Typecode::P2sh => Err(data), + Typecode::Sapling => data.try_into().map(Ivk::Sapling), + Typecode::Orchard => data.try_into().map(Ivk::Orchard), + Typecode::Unknown(_) => Ok(Ivk::Unknown { typecode, data }), + } + .map_err(|e| { + ParseError::InvalidEncoding(format!("Invalid ivk for typecode {}: {:?}", typecode, e)) + }) + } +} + +impl SealedItem for Ivk { + fn typecode(&self) -> Typecode { + match self { + Ivk::P2pkh(_) => Typecode::P2pkh, + Ivk::Sapling(_) => Typecode::Sapling, + Ivk::Orchard(_) => Typecode::Orchard, + Ivk::Unknown { typecode, .. } => Typecode::Unknown(*typecode), + } + } + + fn data(&self) -> &[u8] { + match self { + Ivk::P2pkh(data) => data, + Ivk::Sapling(data) => data, + Ivk::Orchard(data) => data, + Ivk::Unknown { data, .. } => data, + } + } +} + +/// A Unified Incoming Viewing Key. +/// +/// # Examples +/// +/// ``` +/// use zcash_address::unified::{self, Container, Encoding}; +/// +/// # #[cfg(not(feature = "std"))] +/// # fn main() {} +/// # #[cfg(feature = "std")] +/// # fn main() -> Result<(), Box> { +/// # let uivk_from_user = || "uivk1djetqg3fws7y7qu5tekynvcdhz69gsyq07ewvppmzxdqhpfzdgmx8urnkqzv7ylz78ez43ux266pqjhecd59fzhn7wpe6zarnzh804hjtkyad25ryqla5pnc8p5wdl3phj9fczhz64zprun3ux7y9jc08567xryumuz59rjmg4uuflpjqwnq0j0tzce0x74t4tv3gfjq7nczkawxy6y7hse733ae3vw7qfjd0ss0pytvezxp42p6rrpzeh6t2zrz7zpjk0xhngcm6gwdppxs58jkx56gsfflugehf5vjlmu7vj3393gj6u37wenavtqyhdvcdeaj86s6jczl4zq"; +/// let example_uivk: &str = uivk_from_user(); +/// +/// let (network, uivk) = unified::Uivk::decode(example_uivk)?; +/// +/// // We can obtain the pool-specific Incoming Viewing Keys for the UIVK in +/// // preference order (the order in which wallets should prefer to use their +/// // corresponding address receivers): +/// let ivks: Vec = uivk.items(); +/// +/// // And we can create the UIVK from a list of IVKs: +/// let new_uivk = unified::Uivk::try_from_items(ivks)?; +/// assert_eq!(new_uivk, uivk); +/// # Ok(()) +/// # } +/// ``` +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Uivk(pub(crate) Vec); + +impl Container for Uivk { + type Item = Ivk; + + /// Returns the IVKs contained within this UIVK, in the order they were + /// parsed from the string encoding. + /// + /// This API is for advanced usage; in most cases you should use `Uivk::items`. + fn items_as_parsed(&self) -> &[Ivk] { + &self.0 + } +} + +impl Encoding for Uivk {} + +impl SealedContainer for Uivk { + /// The HRP for a Bech32m-encoded mainnet Unified IVK. + /// + /// Defined in [ZIP 316][zip-0316]. + /// + /// [zip-0316]: https://zips.z.cash/zip-0316 + const MAINNET: &'static str = constants::mainnet::HRP_UNIFIED_IVK; + + /// The HRP for a Bech32m-encoded testnet Unified IVK. + /// + /// Defined in [ZIP 316][zip-0316]. + /// + /// [zip-0316]: https://zips.z.cash/zip-0316 + const TESTNET: &'static str = constants::testnet::HRP_UNIFIED_IVK; + + /// The HRP for a Bech32m-encoded regtest Unified IVK. + const REGTEST: &'static str = constants::regtest::HRP_UNIFIED_IVK; + + fn from_inner(ivks: Vec) -> Self { + Self(ivks) + } +} + +#[cfg(test)] +mod tests { + use alloc::borrow::ToOwned; + use alloc::vec::Vec; + + use assert_matches::assert_matches; + + use proptest::{ + array::{uniform1, uniform32}, + prelude::*, + sample::select, + }; + + use super::{Ivk, ParseError, Typecode, Uivk}; + use crate::kind::unified::{ + private::{SealedContainer, SealedItem}, + Container, Encoding, + }; + use zcash_protocol::consensus::NetworkType; + + prop_compose! { + fn uniform64()(a in uniform32(0u8..), b in uniform32(0u8..)) -> [u8; 64] { + let mut c = [0; 64]; + c[..32].copy_from_slice(&a); + c[32..].copy_from_slice(&b); + c + } + } + + prop_compose! { + fn uniform65()(a in uniform1(0u8..), b in uniform64()) -> [u8; 65] { + let mut c = [0; 65]; + c[..1].copy_from_slice(&a); + c[1..].copy_from_slice(&b); + c + } + } + + fn arb_shielded_ivk() -> impl Strategy> { + prop_oneof![ + vec![uniform64().prop_map(Ivk::Sapling)], + vec![uniform64().prop_map(Ivk::Orchard)], + vec![ + uniform64().prop_map(Ivk::Sapling as fn([u8; 64]) -> Ivk), + uniform64().prop_map(Ivk::Orchard) + ], + ] + } + + fn arb_transparent_ivk() -> impl Strategy { + uniform65().prop_map(Ivk::P2pkh) + } + + prop_compose! { + fn arb_unified_ivk()( + shielded in arb_shielded_ivk(), + transparent in prop::option::of(arb_transparent_ivk()), + ) -> Uivk { + let mut items: Vec<_> = transparent.into_iter().chain(shielded).collect(); + items.sort_unstable_by(Ivk::encoding_order); + Uivk(items) + } + } + + proptest! { + #[test] + fn uivk_roundtrip( + network in select(vec![NetworkType::Main, NetworkType::Test, NetworkType::Regtest]), + uivk in arb_unified_ivk(), + ) { + let encoded = uivk.encode(&network); + let decoded = Uivk::decode(&encoded); + prop_assert_eq!(decoded, Ok((network, uivk))); + } + } + + #[test] + fn padding() { + // The test cases below use `Uivk(vec![Ivk::Orchard([1; 64])])` as base. + + // Invalid padding ([0xff; 16] instead of [b'u', 0x00, 0x00, 0x00...]) + let invalid_padding = [ + 0xba, 0xbc, 0xc0, 0x71, 0xcd, 0x3b, 0xfd, 0x9a, 0x32, 0x19, 0x7e, 0xeb, 0x8a, 0xa7, + 0x6e, 0xd4, 0xac, 0xcb, 0x59, 0xc2, 0x54, 0x26, 0xc6, 0xab, 0x71, 0xc7, 0xc3, 0x72, + 0xc, 0xa9, 0xad, 0xa4, 0xad, 0x8c, 0x9e, 0x35, 0x7b, 0x4c, 0x5d, 0xc7, 0x66, 0x12, + 0x8a, 0xc5, 0x42, 0x89, 0xc1, 0x77, 0x32, 0xdc, 0xe8, 0x4b, 0x51, 0x31, 0x30, 0x3, + 0x20, 0xe3, 0xb6, 0x8c, 0xbb, 0xab, 0xe8, 0x89, 0xf8, 0xed, 0xac, 0x6d, 0x8e, 0xb1, + 0x83, 0xe8, 0x92, 0x18, 0x28, 0x70, 0x1e, 0x81, 0x76, 0x56, 0xb6, 0x15, + ]; + assert_eq!( + Uivk::parse_internal(Uivk::MAINNET, &invalid_padding[..]), + Err(ParseError::InvalidEncoding( + "Invalid padding bytes".to_owned() + )) + ); + + // Short padding (padded to 15 bytes instead of 16) + let truncated_padding = [ + 0x96, 0x73, 0x6a, 0x56, 0xbc, 0x44, 0x38, 0xe2, 0x47, 0x41, 0x1c, 0x70, 0xe4, 0x6, + 0x87, 0xbe, 0xb6, 0x90, 0xbd, 0xab, 0x1b, 0xd8, 0x27, 0x10, 0x0, 0x21, 0x30, 0x2, 0x77, + 0x87, 0x0, 0x25, 0x96, 0x94, 0x8f, 0x1e, 0x39, 0xd2, 0xd8, 0x65, 0xb4, 0x3c, 0x72, + 0xd8, 0xac, 0xec, 0x5b, 0xa2, 0x18, 0x62, 0x3f, 0xb, 0x88, 0xb4, 0x41, 0xf1, 0x55, + 0x39, 0x53, 0xbf, 0x2a, 0xd6, 0xcf, 0xdd, 0x46, 0xb7, 0xd8, 0xc1, 0x39, 0x34, 0x4d, + 0xf9, 0x65, 0x49, 0x14, 0xab, 0x7c, 0x55, 0x7b, 0x39, 0x47, + ]; + assert_eq!( + Uivk::parse_internal(Uivk::MAINNET, &truncated_padding[..]), + Err(ParseError::InvalidEncoding( + "Invalid padding bytes".to_owned() + )) + ); + } + + #[test] + fn truncated() { + // The test cases below start from an encoding of + // `Uivk(vec![Ivk::Orchard([1; 64]), Ivk::Sapling([2; 64])])` + // with the ivk data truncated, but valid padding. + + // - Missing the last data byte of the Sapling ivk. + let truncated_sapling_data = [ + 0xce, 0xbc, 0xfe, 0xc5, 0xef, 0x2d, 0xe, 0x66, 0xc2, 0x8c, 0x34, 0xdc, 0x2e, 0x24, + 0xd2, 0xc7, 0x4b, 0xac, 0x36, 0xe0, 0x43, 0x72, 0xa7, 0x33, 0xa4, 0xe, 0xe0, 0x52, + 0x15, 0x64, 0x66, 0x92, 0x36, 0xa7, 0x60, 0x8e, 0x48, 0xe8, 0xb0, 0x30, 0x4d, 0xcb, + 0xd, 0x6f, 0x5, 0xd4, 0xb8, 0x72, 0x6a, 0xdc, 0x6c, 0x5c, 0xa, 0xf8, 0xdf, 0x95, 0x5a, + 0xba, 0xe1, 0xaa, 0x82, 0x51, 0xe2, 0x70, 0x8d, 0x13, 0x16, 0x88, 0x6a, 0xc0, 0xc1, + 0x99, 0x3c, 0xaf, 0x2c, 0x16, 0x54, 0x80, 0x7e, 0xb, 0xad, 0x31, 0x29, 0x26, 0xdd, + 0x7a, 0x55, 0x98, 0x1, 0x18, 0xb, 0x14, 0x94, 0xb2, 0x6b, 0x81, 0x67, 0x73, 0xa6, 0xd0, + 0x20, 0x94, 0x17, 0x3a, 0xf9, 0x98, 0x43, 0x58, 0xd6, 0x1, 0x10, 0x73, 0x32, 0xb4, + 0x99, 0xad, 0x6b, 0xfe, 0xc0, 0x97, 0xaf, 0xd2, 0xee, 0x8, 0xe5, 0x83, 0x6b, 0xb6, + 0xd9, 0x0, 0xef, 0x84, 0xff, 0xe8, 0x58, 0xba, 0xe8, 0x10, 0xea, 0x2d, 0xee, 0x72, + 0xf5, 0xd5, 0x8a, 0xb5, 0x1a, + ]; + assert_matches!( + Uivk::parse_internal(Uivk::MAINNET, &truncated_sapling_data[..]), + Err(ParseError::InvalidEncoding(_)) + ); + + // - Truncated after the typecode of the Sapling ivk. + let truncated_after_sapling_typecode = [ + 0xf7, 0x3, 0xd8, 0xbe, 0x6a, 0x27, 0xfa, 0xa1, 0xd3, 0x11, 0xea, 0x25, 0x94, 0xe2, 0xb, + 0xde, 0xed, 0x6a, 0xaa, 0x8, 0x46, 0x7d, 0xe4, 0xb1, 0xe, 0xf1, 0xde, 0x61, 0xd7, 0x95, + 0xf7, 0x82, 0x62, 0x32, 0x7a, 0x73, 0x8c, 0x55, 0x93, 0xa1, 0x63, 0x75, 0xe2, 0xca, + 0xcb, 0x73, 0xd5, 0xe5, 0xa3, 0xbd, 0xb3, 0xf2, 0x26, 0xfa, 0x1c, 0xa2, 0xad, 0xb6, + 0xd8, 0x21, 0x5e, 0x8, 0xa, 0x82, 0x95, 0x21, 0x74, + ]; + assert_matches!( + Uivk::parse_internal(Uivk::MAINNET, &truncated_after_sapling_typecode[..]), + Err(ParseError::InvalidEncoding(_)) + ); + } + + #[test] + fn duplicate_typecode() { + // Construct and serialize an invalid UIVK. + let uivk = Uivk(vec![Ivk::Sapling([1; 64]), Ivk::Sapling([2; 64])]); + let encoded = uivk.encode(&NetworkType::Main); + assert_eq!( + Uivk::decode(&encoded), + Err(ParseError::DuplicateTypecode(Typecode::Sapling)) + ); + } + + #[test] + fn only_transparent() { + // Raw Encoding of `Uivk(vec![Ivk::P2pkh([0; 65])])`. + let encoded = [ + 0x12, 0x51, 0x37, 0xc7, 0xac, 0x8c, 0xd, 0x13, 0x3a, 0x5f, 0xc6, 0x84, 0x53, 0x90, + 0xf8, 0xe7, 0x23, 0x34, 0xfb, 0xda, 0x49, 0x3c, 0x87, 0x1c, 0x8f, 0x1a, 0xe1, 0x63, + 0xba, 0xdf, 0x77, 0x64, 0x43, 0xcf, 0xdc, 0x37, 0x1f, 0xd2, 0x89, 0x60, 0xe3, 0x77, + 0x20, 0xd0, 0x1c, 0x5, 0x40, 0xe5, 0x43, 0x55, 0xc4, 0xe5, 0xf8, 0xaa, 0xe, 0x7a, 0xe7, + 0x8c, 0x53, 0x15, 0xb8, 0x8f, 0x90, 0x14, 0x33, 0x30, 0x52, 0x2b, 0x8, 0x89, 0x90, + 0xbd, 0xfe, 0xa4, 0xb7, 0x47, 0x20, 0x92, 0x6, 0xf0, 0x0, 0xf9, 0x64, + ]; + + assert_eq!( + Uivk::parse_internal(Uivk::MAINNET, &encoded[..]), + Err(ParseError::OnlyTransparent) + ); + } + + #[test] + fn ivks_are_sorted() { + // Construct a UIVK with ivks in an unsorted order. + let uivk = Uivk(vec![ + Ivk::P2pkh([0; 65]), + Ivk::Orchard([0; 64]), + Ivk::Unknown { + typecode: 0xff, + data: vec![], + }, + Ivk::Sapling([0; 64]), + ]); + + // `Uivk::items` sorts the ivks in priority order. + assert_eq!( + uivk.items(), + vec![ + Ivk::Orchard([0; 64]), + Ivk::Sapling([0; 64]), + Ivk::P2pkh([0; 65]), + Ivk::Unknown { + typecode: 0xff, + data: vec![], + }, + ] + ) + } +} diff --git a/components/zcash_address/src/lib.rs b/components/zcash_address/src/lib.rs new file mode 100644 index 0000000000..e07ad40778 --- /dev/null +++ b/components/zcash_address/src/lib.rs @@ -0,0 +1,396 @@ +//! *Parser for all defined Zcash address types.* +//! +//! This crate implements address parsing as a two-phase process, built around the opaque +//! [`ZcashAddress`] type. +//! +//! - [`ZcashAddress`] can be parsed from, and encoded to, strings. +//! - [`ZcashAddress::convert`] or [`ZcashAddress::convert_if_network`] can be used to +//! convert a parsed address into custom types that implement the [`TryFromAddress`] or +//! [`TryFromRawAddress`] traits. +//! - Custom types can be converted into a [`ZcashAddress`] via its implementation of the +//! [`ToAddress`] trait. +//! +//! ```text +//! s.parse() .convert() +//! --------> ---------> +//! Strings ZcashAddress Custom types +//! <-------- <--------- +//! .encode() ToAddress +//! ``` +//! +//! It is important to note that this crate does not depend on any of the Zcash protocol +//! crates (e.g. `sapling-crypto` or `orchard`). This crate has minimal dependencies by +//! design; it focuses solely on parsing, handling those concerns for you, while exposing +//! APIs that enable you to convert the parsed data into the Rust types you want to use. +//! +//! # Using this crate +//! +//! ## I just need to validate Zcash addresses +//! +//! ``` +//! # use zcash_address::ZcashAddress; +//! fn is_valid_zcash_address(addr_string: &str) -> bool { +//! addr_string.parse::().is_ok() +//! } +//! ``` +//! +//! ## I want to parse Zcash addresses in a Rust wallet app that uses the `zcash_primitives` transaction builder +//! +//! Use `zcash_client_backend::address::RecipientAddress`, which implements the traits in +//! this crate to parse address strings into protocol types that work with the transaction +//! builder in the `zcash_primitives` crate (as well as the wallet functionality in the +//! `zcash_client_backend` crate itself). +//! +//! > We intend to refactor the key and address types from the `zcash_client_backend` and +//! > `zcash_primitives` crates into a separate crate focused on dealing with Zcash key +//! > material. That crate will then be what you should use. +//! +//! ## I want to parse Unified Addresses +//! +//! See the [`unified::Address`] documentation for examples. +//! +//! While the [`unified::Address`] type does have parsing methods, you should still parse +//! your address strings with [`ZcashAddress`] and then convert; this will ensure that for +//! other Zcash address types you get a [`ConversionError::Unsupported`], which is a +//! better error for your users. +//! +//! ## I want to parse mainnet Zcash addresses in a language that supports C FFI +//! +//! As an example, you could use static functions to create the address types in the +//! target language from the parsed data. +//! +//! ``` +//! use std::ffi::{CStr, c_char, c_void}; +//! use std::ptr; +//! +//! use zcash_address::{ConversionError, TryFromRawAddress, ZcashAddress}; +//! use zcash_protocol::consensus::NetworkType; +//! +//! // Functions that return a pointer to a heap-allocated address of the given kind in +//! // the target language. These should be augmented to return any relevant errors. +//! extern { +//! fn addr_from_sapling(data: *const u8) -> *mut c_void; +//! fn addr_from_transparent_p2pkh(data: *const u8) -> *mut c_void; +//! } +//! +//! struct ParsedAddress(*mut c_void); +//! +//! impl TryFromRawAddress for ParsedAddress { +//! type Error = &'static str; +//! +//! fn try_from_raw_sapling( +//! data: [u8; 43], +//! ) -> Result> { +//! let parsed = unsafe { addr_from_sapling(data[..].as_ptr()) }; +//! if parsed.is_null() { +//! Err("Reason for the failure".into()) +//! } else { +//! Ok(Self(parsed)) +//! } +//! } +//! +//! fn try_from_raw_transparent_p2pkh( +//! data: [u8; 20], +//! ) -> Result> { +//! let parsed = unsafe { addr_from_transparent_p2pkh(data[..].as_ptr()) }; +//! if parsed.is_null() { +//! Err("Reason for the failure".into()) +//! } else { +//! Ok(Self(parsed)) +//! } +//! } +//! } +//! +//! pub extern "C" fn parse_zcash_address(encoded: *const c_char) -> *mut c_void { +//! let encoded = unsafe { CStr::from_ptr(encoded) }.to_str().expect("valid"); +//! +//! let addr = match ZcashAddress::try_from_encoded(encoded) { +//! Ok(addr) => addr, +//! Err(e) => { +//! // This was either an invalid address encoding, or not a Zcash address. +//! // You should pass this error back across the FFI. +//! return ptr::null_mut(); +//! } +//! }; +//! +//! match addr.convert_if_network::(NetworkType::Main) { +//! Ok(parsed) => parsed.0, +//! Err(e) => { +//! // We didn't implement all of the methods of `TryFromRawAddress`, so if an +//! // address with one of those kinds is parsed, it will result in an error +//! // here that should be passed back across the FFI. +//! ptr::null_mut() +//! } +//! } +//! } +//! ``` + +#![no_std] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +// Catch documentation errors caused by code changes. +#![deny(rustdoc::broken_intra_doc_links)] + +#[macro_use] +extern crate alloc; + +#[cfg(feature = "std")] +extern crate std; + +use alloc::string::String; + +mod convert; +mod encoding; +mod kind; + +#[cfg(any(test, feature = "test-dependencies"))] +pub mod test_vectors; + +pub use convert::{ + ConversionError, ToAddress, TryFromAddress, TryFromRawAddress, UnsupportedAddress, +}; +pub use encoding::ParseError; +pub use kind::unified; +use kind::unified::Receiver; + +#[deprecated(note = "use ::zcash_protocol::consensus::NetworkType instead")] +pub type Network = zcash_protocol::consensus::NetworkType; + +use zcash_protocol::{consensus::NetworkType, PoolType}; + +/// A Zcash address. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct ZcashAddress { + net: NetworkType, + kind: AddressKind, +} + +/// Known kinds of Zcash addresses. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +enum AddressKind { + Sprout([u8; 64]), + Sapling([u8; 43]), + Unified(unified::Address), + P2pkh([u8; 20]), + P2sh([u8; 20]), + Tex([u8; 20]), +} + +impl ZcashAddress { + /// Encodes this Zcash address in its canonical string representation. + /// + /// This provides the encoded string representation of the address as defined by the + /// [Zcash protocol specification](https://zips.z.cash/protocol.pdf) and/or + /// [ZIP 316](https://zips.z.cash/zip-0316). The [`Display` implementation] can also + /// be used to produce this encoding using [`address.to_string()`]. + /// + /// [`Display` implementation]: std::fmt::Display + /// [`address.to_string()`]: std::string::ToString + pub fn encode(&self) -> String { + format!("{}", self) + } + + /// Attempts to parse the given string as a Zcash address. + /// + /// This simply calls [`s.parse()`], leveraging the [`FromStr` implementation]. + /// + /// [`s.parse()`]: std::primitive::str::parse + /// [`FromStr` implementation]: ZcashAddress#impl-FromStr + /// + /// # Errors + /// + /// - If the parser can detect that the string _must_ contain an address encoding used + /// by Zcash, [`ParseError::InvalidEncoding`] will be returned if any subsequent + /// part of that encoding is invalid. + /// + /// - In all other cases, [`ParseError::NotZcash`] will be returned on failure. + /// + /// # Examples + /// + /// ``` + /// use zcash_address::ZcashAddress; + /// + /// let encoded = "zs1z7rejlpsa98s2rrrfkwmaxu53e4ue0ulcrw0h4x5g8jl04tak0d3mm47vdtahatqrlkngh9sly"; + /// let addr = ZcashAddress::try_from_encoded(&encoded); + /// assert_eq!(encoded.parse(), addr); + /// ``` + pub fn try_from_encoded(s: &str) -> Result { + s.parse() + } + + /// Converts this address into another type. + /// + /// `convert` can convert into any type that implements the [`TryFromAddress`] trait. + /// This enables `ZcashAddress` to be used as a common parsing and serialization + /// interface for Zcash addresses, while delegating operations on those addresses + /// (such as constructing transactions) to downstream crates. + /// + /// If you want to get the encoded string for this address, use the [`encode`] + /// method or the [`Display` implementation] via [`address.to_string()`] instead. + /// + /// [`encode`]: Self::encode + /// [`Display` implementation]: std::fmt::Display + /// [`address.to_string()`]: std::string::ToString + pub fn convert(self) -> Result> { + match self.kind { + AddressKind::Sprout(data) => T::try_from_sprout(self.net, data), + AddressKind::Sapling(data) => T::try_from_sapling(self.net, data), + AddressKind::Unified(data) => T::try_from_unified(self.net, data), + AddressKind::P2pkh(data) => T::try_from_transparent_p2pkh(self.net, data), + AddressKind::P2sh(data) => T::try_from_transparent_p2sh(self.net, data), + AddressKind::Tex(data) => T::try_from_tex(self.net, data), + } + } + + /// Converts this address into another type, if it matches the expected network. + /// + /// `convert_if_network` can convert into any type that implements the + /// [`TryFromRawAddress`] trait. This enables `ZcashAddress` to be used as a common + /// parsing and serialization interface for Zcash addresses, while delegating + /// operations on those addresses (such as constructing transactions) to downstream + /// crates. + /// + /// If you want to get the encoded string for this address, use the [`encode`] + /// method or the [`Display` implementation] via [`address.to_string()`] instead. + /// + /// [`encode`]: Self::encode + /// [`Display` implementation]: std::fmt::Display + /// [`address.to_string()`]: std::string::ToString + pub fn convert_if_network( + self, + net: NetworkType, + ) -> Result> { + let network_matches = self.net == net; + // The Sprout and transparent address encodings use the same prefix for testnet + // and regtest, so we need to allow parsing testnet addresses as regtest. + let regtest_exception = + network_matches || (self.net == NetworkType::Test && net == NetworkType::Regtest); + + match self.kind { + AddressKind::Sprout(data) if regtest_exception => T::try_from_raw_sprout(data), + AddressKind::Sapling(data) if network_matches => T::try_from_raw_sapling(data), + AddressKind::Unified(data) if network_matches => T::try_from_raw_unified(data), + AddressKind::P2pkh(data) if regtest_exception => { + T::try_from_raw_transparent_p2pkh(data) + } + AddressKind::P2sh(data) if regtest_exception => T::try_from_raw_transparent_p2sh(data), + AddressKind::Tex(data) if network_matches => T::try_from_raw_tex(data), + _ => Err(ConversionError::IncorrectNetwork { + expected: net, + actual: self.net, + }), + } + } + + /// Returns whether this address has the ability to receive transfers of the given pool type. + pub fn can_receive_as(&self, pool_type: PoolType) -> bool { + use AddressKind::*; + match &self.kind { + Sprout(_) => false, + Sapling(_) => pool_type == PoolType::SAPLING, + Unified(addr) => addr.has_receiver_of_type(pool_type), + P2pkh(_) | P2sh(_) | Tex(_) => pool_type == PoolType::TRANSPARENT, + } + } + + /// Returns whether this address can receive a memo. + pub fn can_receive_memo(&self) -> bool { + use AddressKind::*; + match &self.kind { + Sprout(_) | Sapling(_) => true, + Unified(addr) => addr.can_receive_memo(), + P2pkh(_) | P2sh(_) | Tex(_) => false, + } + } + + /// Returns whether or not this address contains or corresponds to the given unified address + /// receiver. + pub fn matches_receiver(&self, receiver: &Receiver) -> bool { + match (&self.kind, receiver) { + (AddressKind::Unified(ua), r) => ua.contains_receiver(r), + (AddressKind::Sapling(d), Receiver::Sapling(r)) => r == d, + (AddressKind::P2pkh(d), Receiver::P2pkh(r)) => r == d, + (AddressKind::Tex(d), Receiver::P2pkh(r)) => r == d, + (AddressKind::P2sh(d), Receiver::P2sh(r)) => r == d, + _ => false, + } + } +} + +#[cfg(feature = "test-dependencies")] +pub mod testing { + use core::convert::TryInto; + + use proptest::{array::uniform20, collection::vec, prelude::any, prop_compose, prop_oneof}; + + use crate::{unified::address::testing::arb_unified_address, AddressKind, ZcashAddress}; + use zcash_protocol::consensus::NetworkType; + + prop_compose! { + fn arb_sprout_addr_kind()( + r_bytes in vec(any::(), 64) + ) -> AddressKind { + AddressKind::Sprout(r_bytes.try_into().unwrap()) + } + } + + prop_compose! { + fn arb_sapling_addr_kind()( + r_bytes in vec(any::(), 43) + ) -> AddressKind { + AddressKind::Sapling(r_bytes.try_into().unwrap()) + } + } + + prop_compose! { + fn arb_p2pkh_addr_kind()( + r_bytes in uniform20(any::()) + ) -> AddressKind { + AddressKind::P2pkh(r_bytes) + } + } + + prop_compose! { + fn arb_p2sh_addr_kind()( + r_bytes in uniform20(any::()) + ) -> AddressKind { + AddressKind::P2sh(r_bytes) + } + } + + prop_compose! { + fn arb_unified_addr_kind()( + uaddr in arb_unified_address() + ) -> AddressKind { + AddressKind::Unified(uaddr) + } + } + + prop_compose! { + fn arb_tex_addr_kind()( + r_bytes in uniform20(any::()) + ) -> AddressKind { + AddressKind::Tex(r_bytes) + } + } + + prop_compose! { + /// Create an arbitrary, structurally-valid `ZcashAddress` value. + /// + /// Note that the data contained in the generated address does _not_ necessarily correspond + /// to a valid address according to the Zcash protocol; binary data in the resulting value + /// is entirely random. + pub fn arb_address(net: NetworkType)( + kind in prop_oneof!( + arb_sprout_addr_kind(), + arb_sapling_addr_kind(), + arb_p2pkh_addr_kind(), + arb_p2sh_addr_kind(), + arb_unified_addr_kind(), + arb_tex_addr_kind() + ) + ) -> ZcashAddress { + ZcashAddress { net, kind } + } + } +} diff --git a/components/zcash_address/src/test_vectors.rs b/components/zcash_address/src/test_vectors.rs new file mode 100644 index 0000000000..a7278734eb --- /dev/null +++ b/components/zcash_address/src/test_vectors.rs @@ -0,0 +1,53 @@ +/// Export test vectors for reuse by implementers of address parsing libraries. +#[cfg(feature = "test-dependencies")] +pub use crate::unified::address::test_vectors::TEST_VECTORS as UNIFIED; + +#[cfg(test)] +use { + crate::{ + unified::{ + self, + address::{test_vectors::TEST_VECTORS, Receiver}, + }, + ToAddress, ZcashAddress, + }, + alloc::string::ToString, + core::iter, + zcash_protocol::consensus::NetworkType, +}; + +#[test] +fn unified() { + for tv in TEST_VECTORS { + // Double-check test vectors match requirements: + // - Only one of P2PKH and P2SH. + assert!(tv.p2pkh_bytes.is_none() || tv.p2sh_bytes.is_none()); + // - At least one shielded receiver. + assert!(tv.sapling_raw_addr.is_some() || tv.orchard_raw_addr.is_some()); + + let unknown_tc = tv.unknown_typecode; + let unknown_bytes = tv.unknown_bytes; + let receivers = iter::empty() + .chain(tv.p2pkh_bytes.map(Receiver::P2pkh)) + .chain(tv.p2sh_bytes.map(Receiver::P2sh)) + .chain(tv.sapling_raw_addr.map(Receiver::Sapling)) + .chain(tv.orchard_raw_addr.map(Receiver::Orchard)) + .chain(unknown_tc.and_then(|typecode| { + unknown_bytes.map(move |data| Receiver::Unknown { + typecode, + data: data.to_vec(), + }) + })) + .collect(); + + let expected_addr = + ZcashAddress::from_unified(NetworkType::Main, unified::Address(receivers)); + + // Test parsing + let addr: ZcashAddress = tv.unified_addr.parse().unwrap(); + assert_eq!(addr, expected_addr); + + // Test serialization + assert_eq!(expected_addr.to_string(), tv.unified_addr.to_string()); + } +} diff --git a/components/zcash_encoding/CHANGELOG.md b/components/zcash_encoding/CHANGELOG.md new file mode 100644 index 0000000000..5aca6da70a --- /dev/null +++ b/components/zcash_encoding/CHANGELOG.md @@ -0,0 +1,27 @@ +# Changelog +All notable changes to this library will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this library adheres to Rust's notion of +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Changed +- Migrated to `nonempty 0.11` + +## [0.2.2] - 2024-12-13 +### Added +- `no-std` support, via a default-enabled `std` feature flag. + +## [0.2.1] - 2024-08-19 +### Added +- `zcash_encoding::CompactSize::serialized_size` +- `zcash_encoding::Vector::serialized_size_of_u8_vec` + +## [0.2.0] - 2022-10-19 +### Changed +- MSRV is now 1.56.1 + +## [0.1.0] - 2022-05-11 +Initial release. diff --git a/components/zcash_encoding/Cargo.toml b/components/zcash_encoding/Cargo.toml new file mode 100644 index 0000000000..f2847204f7 --- /dev/null +++ b/components/zcash_encoding/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "zcash_encoding" +description = "Binary encodings used throughout the Zcash ecosystem." +version = "0.2.2" +authors = [ + "Jack Grigg ", + "Kris Nuttycombe ", +] +homepage = "https://github.com/zcash/librustzcash" +repository = "https://github.com/zcash/librustzcash" +readme = "README.md" +license = "MIT OR Apache-2.0" +edition = "2021" +rust-version = "1.56.1" +categories = ["cryptography::cryptocurrencies", "encoding"] +keywords = ["zcash"] + +[dependencies] +core2.workspace = true +nonempty.workspace = true + +[features] +default = ["std"] +std = ["core2/std"] + +[lib] +bench = false + +[lints] +workspace = true diff --git a/components/zcash_encoding/LICENSE-APACHE b/components/zcash_encoding/LICENSE-APACHE new file mode 100644 index 0000000000..1e5006dc14 --- /dev/null +++ b/components/zcash_encoding/LICENSE-APACHE @@ -0,0 +1,202 @@ + 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 [yyyy] [name of copyright owner] + +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/components/zcash_encoding/LICENSE-MIT b/components/zcash_encoding/LICENSE-MIT new file mode 100644 index 0000000000..9500c140cc --- /dev/null +++ b/components/zcash_encoding/LICENSE-MIT @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2021 Electric Coin Company + +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/components/zcash_encoding/README.md b/components/zcash_encoding/README.md new file mode 100644 index 0000000000..69c20b1bf8 --- /dev/null +++ b/components/zcash_encoding/README.md @@ -0,0 +1,21 @@ +# zcash_encodings + +This library provides common encoding and decoding operations for stable binary +encodings used throughout the Zcash ecosystem. + +## License + +Licensed under either of + + * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or + http://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you, as defined in the Apache-2.0 +license, shall be dual licensed as above, without any additional terms or +conditions. diff --git a/components/zcash_encoding/src/lib.rs b/components/zcash_encoding/src/lib.rs new file mode 100644 index 0000000000..4e611e25a6 --- /dev/null +++ b/components/zcash_encoding/src/lib.rs @@ -0,0 +1,437 @@ +//! *Zcash binary encodings.* +//! +//! `zcash_encoding` is a library that provides common encoding and decoding operations +//! for stable binary encodings used throughout the Zcash ecosystem. + +#![no_std] +// Catch documentation errors caused by code changes. +#![deny(rustdoc::broken_intra_doc_links)] +#![deny(missing_docs)] +#![deny(unsafe_code)] + +#[cfg_attr(test, macro_use)] +extern crate alloc; + +use alloc::vec::Vec; + +use core::iter::FromIterator; +use core2::io::{self, Read, Write}; + +use nonempty::NonEmpty; + +/// The maximum allowed value representable as a `[CompactSize]` +pub const MAX_COMPACT_SIZE: u32 = 0x02000000; + +/// Namespace for functions for compact encoding of integers. +/// +/// This codec requires integers to be in the range `0x0..=0x02000000`, for compatibility +/// with Zcash consensus rules. +pub struct CompactSize; + +impl CompactSize { + /// Reads an integer encoded in compact form. + pub fn read(mut reader: R) -> io::Result { + let mut flag_bytes = [0; 1]; + reader.read_exact(&mut flag_bytes)?; + let flag = flag_bytes[0]; + + let result = if flag < 253 { + Ok(flag as u64) + } else if flag == 253 { + let mut bytes = [0; 2]; + reader.read_exact(&mut bytes)?; + match u16::from_le_bytes(bytes) { + n if n < 253 => Err(io::Error::new( + io::ErrorKind::InvalidInput, + "non-canonical CompactSize", + )), + n => Ok(n as u64), + } + } else if flag == 254 { + let mut bytes = [0; 4]; + reader.read_exact(&mut bytes)?; + match u32::from_le_bytes(bytes) { + n if n < 0x10000 => Err(io::Error::new( + io::ErrorKind::InvalidInput, + "non-canonical CompactSize", + )), + n => Ok(n as u64), + } + } else { + let mut bytes = [0; 8]; + reader.read_exact(&mut bytes)?; + match u64::from_le_bytes(bytes) { + n if n < 0x100000000 => Err(io::Error::new( + io::ErrorKind::InvalidInput, + "non-canonical CompactSize", + )), + n => Ok(n), + } + }?; + + match result { + s if s > ::from(MAX_COMPACT_SIZE) => Err(io::Error::new( + io::ErrorKind::InvalidInput, + "CompactSize too large", + )), + s => Ok(s), + } + } + + /// Reads an integer encoded in contact form and performs checked conversion + /// to the target type. + pub fn read_t>(mut reader: R) -> io::Result { + let n = Self::read(&mut reader)?; + ::try_from(n).map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidInput, + "CompactSize value exceeds range of target type.", + ) + }) + } + + /// Writes the provided `usize` value to the provided Writer in compact form. + pub fn write(mut writer: W, size: usize) -> io::Result<()> { + match size { + s if s < 253 => writer.write_all(&[s as u8]), + s if s <= 0xFFFF => { + writer.write_all(&[253])?; + writer.write_all(&(s as u16).to_le_bytes()) + } + s if s <= 0xFFFFFFFF => { + writer.write_all(&[254])?; + writer.write_all(&(s as u32).to_le_bytes()) + } + s => { + writer.write_all(&[255])?; + writer.write_all(&(s as u64).to_le_bytes()) + } + } + } + + /// Returns the number of bytes needed to encode the given size in compact form. + pub fn serialized_size(size: usize) -> usize { + match size { + s if s < 253 => 1, + s if s <= 0xFFFF => 3, + s if s <= 0xFFFFFFFF => 5, + _ => 9, + } + } +} + +/// Namespace for functions that perform encoding of vectors. +/// +/// The length of a vector is restricted to at most `0x02000000`, for compatibility with +/// the Zcash consensus rules. +pub struct Vector; + +impl Vector { + /// Reads a vector, assuming the encoding written by [`Vector::write`], using the provided + /// function to decode each element of the vector. + pub fn read(reader: R, func: F) -> io::Result> + where + F: Fn(&mut R) -> io::Result, + { + Self::read_collected(reader, func) + } + + /// Reads a CompactSize-prefixed series of elements into a collection, assuming the encoding + /// written by [`Vector::write`], using the provided function to decode each element. + pub fn read_collected>(reader: R, func: F) -> io::Result + where + F: Fn(&mut R) -> io::Result, + { + Self::read_collected_mut(reader, func) + } + + /// Reads a CompactSize-prefixed series of elements into a collection, assuming the encoding + /// written by [`Vector::write`], using the provided function to decode each element. + pub fn read_collected_mut>( + mut reader: R, + func: F, + ) -> io::Result + where + F: FnMut(&mut R) -> io::Result, + { + let count: usize = CompactSize::read_t(&mut reader)?; + Array::read_collected_mut(reader, count, func) + } + + /// Writes a slice of values by writing [`CompactSize`]-encoded integer specifying the length + /// of the slice to the stream, followed by the encoding of each element of the slice as + /// performed by the provided function. + pub fn write(writer: W, vec: &[E], func: F) -> io::Result<()> + where + F: Fn(&mut W, &E) -> io::Result<()>, + { + Self::write_sized(writer, vec.iter(), func) + } + + /// Writes a NonEmpty container of values to the stream using the same encoding as + /// `[Vector::write]` + pub fn write_nonempty( + mut writer: W, + vec: &NonEmpty, + func: F, + ) -> io::Result<()> + where + F: Fn(&mut W, &E) -> io::Result<()>, + { + CompactSize::write(&mut writer, vec.len())?; + vec.iter().try_for_each(|e| func(&mut writer, e)) + } + + /// Writes an iterator of values by writing [`CompactSize`]-encoded integer specifying + /// the length of the iterator to the stream, followed by the encoding of each element + /// of the iterator as performed by the provided function. + pub fn write_sized + ExactSizeIterator>( + mut writer: W, + mut items: I, + func: F, + ) -> io::Result<()> + where + F: Fn(&mut W, E) -> io::Result<()>, + { + CompactSize::write(&mut writer, items.len())?; + items.try_for_each(|e| func(&mut writer, e)) + } + + /// Returns the serialized size of a vector of `u8` as written by `[Vector::write]`. + pub fn serialized_size_of_u8_vec(vec: &[u8]) -> usize { + let length = vec.len(); + CompactSize::serialized_size(length) + length + } +} + +/// Namespace for functions that perform encoding of array contents. +/// +/// This is similar to the [`Vector`] encoding except that no length information is +/// written as part of the encoding, so length must be statically known or obtained from +/// other parts of the input stream. +pub struct Array; + +impl Array { + /// Reads `count` elements from a stream into a vector, assuming the encoding written by + /// [`Array::write`], using the provided function to decode each element. + pub fn read(reader: R, count: usize, func: F) -> io::Result> + where + F: Fn(&mut R) -> io::Result, + { + Self::read_collected(reader, count, func) + } + + /// Reads `count` elements into a collection, assuming the encoding written by + /// [`Array::write`], using the provided function to decode each element. + pub fn read_collected>( + reader: R, + count: usize, + func: F, + ) -> io::Result + where + F: Fn(&mut R) -> io::Result, + { + Self::read_collected_mut(reader, count, func) + } + + /// Reads `count` elements into a collection, assuming the encoding written by + /// [`Array::write`], using the provided function to decode each element. + pub fn read_collected_mut>( + mut reader: R, + count: usize, + mut func: F, + ) -> io::Result + where + F: FnMut(&mut R) -> io::Result, + { + (0..count).map(|_| func(&mut reader)).collect() + } + + /// Writes an iterator full of values to a stream by sequentially + /// encoding each element using the provided function. + pub fn write, F>( + mut writer: W, + vec: I, + func: F, + ) -> io::Result<()> + where + F: Fn(&mut W, &E) -> io::Result<()>, + { + vec.into_iter().try_for_each(|e| func(&mut writer, &e)) + } +} + +/// Namespace for functions that perform encoding of [`Option`] values. +pub struct Optional; + +impl Optional { + /// Reads an optional value, assuming the encoding written by [`Optional::write`], using the + /// provided function to decode the contained element if present. + pub fn read(mut reader: R, func: F) -> io::Result> + where + F: Fn(R) -> io::Result, + { + let mut bytes = [0; 1]; + reader.read_exact(&mut bytes)?; + match bytes[0] { + 0 => Ok(None), + 1 => Ok(Some(func(reader)?)), + _ => Err(io::Error::new( + io::ErrorKind::InvalidInput, + "non-canonical Option", + )), + } + } + + /// Writes an optional value to a stream by writing a flag byte with a value of 0 if no value + /// is present, or 1 if there is a value, followed by the encoding of the contents of the + /// option as performed by the provided function. + pub fn write(mut writer: W, val: Option, func: F) -> io::Result<()> + where + F: Fn(W, T) -> io::Result<()>, + { + match val { + None => writer.write_all(&[0]), + Some(e) => { + writer.write_all(&[1])?; + func(writer, e) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use core::fmt::Debug; + + #[test] + fn compact_size() { + fn eval + TryInto + Eq + Debug + Copy>(value: T, expected: &[u8]) + where + >::Error: Debug, + { + let mut data = vec![]; + let value_usize: usize = value.try_into().unwrap(); + CompactSize::write(&mut data, value_usize).unwrap(); + assert_eq!(&data[..], expected); + let serialized_size = CompactSize::serialized_size(value_usize); + assert_eq!(serialized_size, expected.len()); + let result: io::Result = CompactSize::read_t(&data[..]); + match result { + Ok(n) => assert_eq!(n, value), + Err(e) => panic!("Unexpected error: {:?}", e), + } + } + + eval(0, &[0]); + eval(1, &[1]); + eval(252, &[252]); + eval(253, &[253, 253, 0]); + eval(254, &[253, 254, 0]); + eval(255, &[253, 255, 0]); + eval(256, &[253, 0, 1]); + eval(256, &[253, 0, 1]); + eval(65535, &[253, 255, 255]); + eval(65536, &[254, 0, 0, 1, 0]); + eval(65537, &[254, 1, 0, 1, 0]); + + eval(33554432, &[254, 0, 0, 0, 2]); + + { + let value = 33554433; + let encoded = &[254, 1, 0, 0, 2][..]; + let mut data = vec![]; + CompactSize::write(&mut data, value).unwrap(); + assert_eq!(&data[..], encoded); + let serialized_size = CompactSize::serialized_size(value); + assert_eq!(serialized_size, encoded.len()); + assert!(CompactSize::read(encoded).is_err()); + } + } + + #[allow(clippy::useless_vec)] + #[test] + fn vector() { + macro_rules! eval { + ($value:expr, $expected:expr) => { + let mut data = vec![]; + Vector::write(&mut data, &$value, |w, e| w.write_all(&[*e])).unwrap(); + assert_eq!(&data[..], &$expected[..]); + let serialized_size = Vector::serialized_size_of_u8_vec(&$value); + assert_eq!(serialized_size, $expected.len()); + match Vector::read(&data[..], |r| { + let mut bytes = [0; 1]; + r.read_exact(&mut bytes).map(|_| bytes[0]) + }) { + Ok(v) => assert_eq!(v, $value), + Err(e) => panic!("Unexpected error: {:?}", e), + } + }; + } + + eval!(vec![], [0]); + eval!(vec![0], [1, 0]); + eval!(vec![1], [1, 1]); + eval!(vec![5; 8], [8, 5, 5, 5, 5, 5, 5, 5, 5]); + + { + // expected = [253, 4, 1, 7, 7, 7, ...] + let mut expected = vec![7; 263]; + expected[0] = 253; + expected[1] = 4; + expected[2] = 1; + + eval!(vec![7; 260], expected); + } + } + + #[test] + fn optional() { + macro_rules! eval { + ($value:expr, $expected:expr, $write:expr, $read:expr) => { + let mut data = vec![]; + Optional::write(&mut data, $value, $write).unwrap(); + assert_eq!(&data[..], &$expected[..]); + match Optional::read(&data[..], $read) { + Ok(v) => assert_eq!(v, $value), + Err(e) => panic!("Unexpected error: {:?}", e), + } + }; + } + + macro_rules! eval_u8 { + ($value:expr, $expected:expr) => { + eval!($value, $expected, |w, e| w.write_all(&[e]), |mut r| { + let mut bytes = [0; 1]; + r.read_exact(&mut bytes).map(|_| bytes[0]) + }) + }; + } + + macro_rules! eval_vec { + ($value:expr, $expected:expr) => { + eval!( + $value, + $expected, + |w, v| Vector::write(w, &v, |w, e| w.write_all(&[*e])), + |r| Vector::read(r, |r| { + let mut bytes = [0; 1]; + r.read_exact(&mut bytes).map(|_| bytes[0]) + }) + ) + }; + } + + eval_u8!(None, [0]); + eval_u8!(Some(0), [1, 0]); + eval_u8!(Some(1), [1, 1]); + eval_u8!(Some(5), [1, 5]); + + eval_vec!(None as Option>, [0]); + eval_vec!(Some(vec![]), [1, 0]); + eval_vec!(Some(vec![0]), [1, 1, 0]); + eval_vec!(Some(vec![1]), [1, 1, 1]); + eval_vec!(Some(vec![5; 8]), [1, 8, 5, 5, 5, 5, 5, 5, 5, 5]); + } +} diff --git a/components/zcash_note_encryption/Cargo.toml b/components/zcash_note_encryption/Cargo.toml deleted file mode 100644 index bed67dafee..0000000000 --- a/components/zcash_note_encryption/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "zcash_note_encryption" -description = "TBD" -version = "0.0.0" -authors = [ - "Jack Grigg ", - "Kris Nuttycombe " -] -homepage = "https://github.com/zcash/librustzcash" -repository = "https://github.com/zcash/librustzcash" -license = "MIT OR Apache-2.0" -edition = "2018" - -[dependencies] -blake2b_simd = "0.5" -byteorder = "1" -crypto_api_chachapoly = "0.4" -ff = "0.8" -group = "0.8" -rand_core = "0.5.1" -subtle = "2.2.3" - -[dev-dependencies] -zcash_primitives = { version = "0.5", path = "../../zcash_primitives" } -jubjub = "0.5.1" diff --git a/components/zcash_note_encryption/src/lib.rs b/components/zcash_note_encryption/src/lib.rs deleted file mode 100644 index 481ee1e81e..0000000000 --- a/components/zcash_note_encryption/src/lib.rs +++ /dev/null @@ -1,500 +0,0 @@ -//! Implementation of in-band secret distribution abstractions -//! for Zcash transactions. The implementations here provide -//! functionality that is shared between the Sapling and Orchard -//! protocols. - -use crypto_api_chachapoly::{ChaCha20Ietf, ChachaPolyIetf}; -use rand_core::RngCore; -use std::convert::TryFrom; -use subtle::{Choice, ConstantTimeEq}; - -pub const COMPACT_NOTE_SIZE: usize = 1 + // version - 11 + // diversifier - 8 + // value - 32; // rseed (or rcm prior to ZIP 212) -pub const NOTE_PLAINTEXT_SIZE: usize = COMPACT_NOTE_SIZE + 512; -pub const OUT_PLAINTEXT_SIZE: usize = 32 + // pk_d - 32; // esk -pub const AEAD_TAG_SIZE: usize = 16; -pub const ENC_CIPHERTEXT_SIZE: usize = NOTE_PLAINTEXT_SIZE + AEAD_TAG_SIZE; -pub const OUT_CIPHERTEXT_SIZE: usize = OUT_PLAINTEXT_SIZE + AEAD_TAG_SIZE; - -/// A symmetric key that can be used to recover a single Sapling or Orchard output. -pub struct OutgoingCipherKey(pub [u8; 32]); - -impl From<[u8; 32]> for OutgoingCipherKey { - fn from(ock: [u8; 32]) -> Self { - OutgoingCipherKey(ock) - } -} - -impl AsRef<[u8]> for OutgoingCipherKey { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} - -pub struct EphemeralKeyBytes(pub [u8; 32]); - -impl AsRef<[u8]> for EphemeralKeyBytes { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} - -impl From<[u8; 32]> for EphemeralKeyBytes { - fn from(value: [u8; 32]) -> EphemeralKeyBytes { - EphemeralKeyBytes(value) - } -} - -impl ConstantTimeEq for EphemeralKeyBytes { - fn ct_eq(&self, other: &Self) -> Choice { - self.0.ct_eq(&other.0) - } -} - -pub struct NotePlaintextBytes(pub [u8; NOTE_PLAINTEXT_SIZE]); -pub struct OutPlaintextBytes(pub [u8; OUT_PLAINTEXT_SIZE]); - -#[derive(Copy, Clone, PartialEq, Eq)] -pub enum NoteValidity { - Valid, - Invalid, -} - -pub trait Domain { - type EphemeralSecretKey; - type EphemeralPublicKey; - type SharedSecret; - type SymmetricKey: AsRef<[u8]>; - type Note; - type Recipient; - type DiversifiedTransmissionKey; - type IncomingViewingKey; - type OutgoingViewingKey; - type ValueCommitment; - type ExtractedCommitment; - type ExtractedCommitmentBytes: Eq + TryFrom; - type Memo; - - fn derive_esk(note: &Self::Note) -> Option; - - fn get_pk_d(note: &Self::Note) -> Self::DiversifiedTransmissionKey; - - fn ka_derive_public( - note: &Self::Note, - esk: &Self::EphemeralSecretKey, - ) -> Self::EphemeralPublicKey; - - fn ka_agree_enc( - esk: &Self::EphemeralSecretKey, - pk_d: &Self::DiversifiedTransmissionKey, - ) -> Self::SharedSecret; - - fn ka_agree_dec( - ivk: &Self::IncomingViewingKey, - epk: &Self::EphemeralPublicKey, - ) -> Self::SharedSecret; - - fn kdf(secret: Self::SharedSecret, ephemeral_key: &EphemeralKeyBytes) -> Self::SymmetricKey; - - // for right now, we just need `recipient` to get `d`; in the future when we - // can get that from a Sapling note, the recipient parameter will be able - // to be removed. - fn note_plaintext_bytes( - note: &Self::Note, - recipient: &Self::Recipient, - memo: &Self::Memo, - ) -> NotePlaintextBytes; - - fn derive_ock( - ovk: &Self::OutgoingViewingKey, - cv: &Self::ValueCommitment, - cmstar: &Self::ExtractedCommitment, - ephemeral_key: &EphemeralKeyBytes, - ) -> OutgoingCipherKey; - - fn outgoing_plaintext_bytes( - note: &Self::Note, - esk: &Self::EphemeralSecretKey, - ) -> OutPlaintextBytes; - - fn epk_bytes(epk: &Self::EphemeralPublicKey) -> EphemeralKeyBytes; - - fn check_epk_bytes NoteValidity>( - note: &Self::Note, - check: F, - ) -> NoteValidity; - - fn cmstar(note: &Self::Note) -> Self::ExtractedCommitment; - - fn parse_note_plaintext_without_memo_ivk( - &self, - ivk: &Self::IncomingViewingKey, - plaintext: &[u8], - ) -> Option<(Self::Note, Self::Recipient)>; - - fn parse_note_plaintext_without_memo_ovk( - &self, - pk_d: &Self::DiversifiedTransmissionKey, - esk: &Self::EphemeralSecretKey, - epk: &Self::EphemeralPublicKey, - plaintext: &[u8], - ) -> Option<(Self::Note, Self::Recipient)>; - - // &self is passed here in anticipation of future changes - // to memo handling where the memos may no longer be - // part of the note plaintext. - fn extract_memo(&self, plaintext: &[u8]) -> Self::Memo; - - fn extract_pk_d( - out_plaintext: &[u8; OUT_CIPHERTEXT_SIZE], - ) -> Option; - - fn extract_esk(out_plaintext: &[u8; OUT_CIPHERTEXT_SIZE]) -> Option; -} - -pub trait ShieldedOutput { - fn epk(&self) -> &D::EphemeralPublicKey; - fn cmstar_bytes(&self) -> D::ExtractedCommitmentBytes; - fn enc_ciphertext(&self) -> &[u8]; -} - -/// A struct containing context required for encrypting Sapling and Orchard notes. -/// -/// This struct provides a safe API for encrypting Sapling and Orchard notes. In particular, it -/// enforces that fresh ephemeral keys are used for every note, and that the ciphertexts are -/// consistent with each other. -/// -/// Implements section 4.19 of the -/// [Zcash Protocol Specification](https://zips.z.cash/protocol/nu5.pdf#saplingandorchardinband) -/// NB: the example code is only covering the post-Canopy case. -/// -/// # Examples -/// -/// ``` -/// extern crate ff; -/// extern crate rand_core; -/// extern crate zcash_primitives; -/// -/// use ff::Field; -/// use rand_core::OsRng; -/// use zcash_primitives::{ -/// consensus::{TEST_NETWORK, TestNetwork, NetworkUpgrade, Parameters}, -/// memo::MemoBytes, -/// sapling::{ -/// keys::{OutgoingViewingKey, prf_expand}, -/// note_encryption::sapling_note_encryption, -/// util::generate_random_rseed, -/// Diversifier, PaymentAddress, Rseed, ValueCommitment -/// }, -/// }; -/// -/// let mut rng = OsRng; -/// -/// let diversifier = Diversifier([0; 11]); -/// let pk_d = diversifier.g_d().unwrap(); -/// let to = PaymentAddress::from_parts(diversifier, pk_d).unwrap(); -/// let ovk = Some(OutgoingViewingKey([0; 32])); -/// -/// let value = 1000; -/// let rcv = jubjub::Fr::random(&mut rng); -/// let cv = ValueCommitment { -/// value, -/// randomness: rcv.clone(), -/// }; -/// let height = TEST_NETWORK.activation_height(NetworkUpgrade::Canopy).unwrap(); -/// let rseed = generate_random_rseed(&TEST_NETWORK, height, &mut rng); -/// let note = to.create_note(value, rseed).unwrap(); -/// let cmu = note.cmu(); -/// -/// let mut enc = sapling_note_encryption::<_, TestNetwork>(ovk, note, to, MemoBytes::empty(), &mut rng); -/// let encCiphertext = enc.encrypt_note_plaintext(); -/// let outCiphertext = enc.encrypt_outgoing_plaintext(&cv.commitment().into(), &cmu, &mut rng); -/// ``` -pub struct NoteEncryption { - epk: D::EphemeralPublicKey, - esk: D::EphemeralSecretKey, - note: D::Note, - to: D::Recipient, - memo: D::Memo, - /// `None` represents the `ovk = ⊥` case. - ovk: Option, -} - -impl NoteEncryption { - /// Construct a new note encryption context for the specified note, - /// recipient, and memo. - pub fn new( - ovk: Option, - note: D::Note, - to: D::Recipient, - memo: D::Memo, - ) -> Self { - let esk = D::derive_esk(¬e).expect("ZIP 212 is active."); - Self::new_with_esk(esk, ovk, note, to, memo) - } - - /// For use only with Sapling. This method is preserved in order that test code - /// be able to generate pre-ZIP-212 ciphertexts so that tests can continue to - /// cover pre-ZIP-212 transaction decryption. - pub fn new_with_esk( - esk: D::EphemeralSecretKey, - ovk: Option, - note: D::Note, - to: D::Recipient, - memo: D::Memo, - ) -> Self { - NoteEncryption { - epk: D::ka_derive_public(¬e, &esk), - esk, - note, - to, - memo, - ovk, - } - } - - /// Exposes the ephemeral secret key being used to encrypt this note. - pub fn esk(&self) -> &D::EphemeralSecretKey { - &self.esk - } - - /// Exposes the encoding of the ephemeral public key being used to encrypt this note. - pub fn epk(&self) -> &D::EphemeralPublicKey { - &self.epk - } - - /// Generates `encCiphertext` for this note. - pub fn encrypt_note_plaintext(&self) -> [u8; ENC_CIPHERTEXT_SIZE] { - let pk_d = D::get_pk_d(&self.note); - let shared_secret = D::ka_agree_enc(&self.esk, &pk_d); - let key = D::kdf(shared_secret, &D::epk_bytes(&self.epk)); - let input = D::note_plaintext_bytes(&self.note, &self.to, &self.memo); - - let mut output = [0u8; ENC_CIPHERTEXT_SIZE]; - assert_eq!( - ChachaPolyIetf::aead_cipher() - .seal_to(&mut output, &input.0, &[], key.as_ref(), &[0u8; 12]) - .unwrap(), - ENC_CIPHERTEXT_SIZE - ); - - output - } - - /// Generates `outCiphertext` for this note. - pub fn encrypt_outgoing_plaintext( - &self, - cv: &D::ValueCommitment, - cmstar: &D::ExtractedCommitment, - rng: &mut R, - ) -> [u8; OUT_CIPHERTEXT_SIZE] { - let (ock, input) = if let Some(ovk) = &self.ovk { - let ock = D::derive_ock(ovk, &cv, &cmstar, &D::epk_bytes(&self.epk)); - let input = D::outgoing_plaintext_bytes(&self.note, &self.esk); - - (ock, input) - } else { - // ovk = ⊥ - let mut ock = OutgoingCipherKey([0; 32]); - let mut input = [0u8; OUT_PLAINTEXT_SIZE]; - - rng.fill_bytes(&mut ock.0); - rng.fill_bytes(&mut input); - - (ock, OutPlaintextBytes(input)) - }; - - let mut output = [0u8; OUT_CIPHERTEXT_SIZE]; - assert_eq!( - ChachaPolyIetf::aead_cipher() - .seal_to(&mut output, &input.0, &[], ock.as_ref(), &[0u8; 12]) - .unwrap(), - OUT_CIPHERTEXT_SIZE - ); - - output - } -} - -/// Trial decryption of the full note plaintext by the recipient. -/// -/// Attempts to decrypt and validate the given `enc_ciphertext` using the given `ivk`. -/// If successful, the corresponding Sapling note and memo are returned, along with the -/// `PaymentAddress` to which the note was sent. -/// -/// Implements section 4.19.2 of the -/// [Zcash Protocol Specification](https://zips.z.cash/protocol/nu5.pdf#decryptivk) -pub fn try_note_decryption>( - domain: &D, - ivk: &D::IncomingViewingKey, - output: &Output, -) -> Option<(D::Note, D::Recipient, D::Memo)> { - assert_eq!(output.enc_ciphertext().len(), ENC_CIPHERTEXT_SIZE); - - let shared_secret = D::ka_agree_dec(ivk, output.epk()); - let key = D::kdf(shared_secret, &D::epk_bytes(output.epk())); - - let mut plaintext = [0; ENC_CIPHERTEXT_SIZE]; - assert_eq!( - ChachaPolyIetf::aead_cipher() - .open_to( - &mut plaintext, - output.enc_ciphertext(), - &[], - key.as_ref(), - &[0u8; 12] - ) - .ok()?, - NOTE_PLAINTEXT_SIZE - ); - - let (note, to) = parse_note_plaintext_without_memo_ivk( - domain, - ivk, - output.epk(), - &output.cmstar_bytes(), - &plaintext, - )?; - let memo = domain.extract_memo(&plaintext); - - Some((note, to, memo)) -} - -fn parse_note_plaintext_without_memo_ivk( - domain: &D, - ivk: &D::IncomingViewingKey, - epk: &D::EphemeralPublicKey, - cmstar_bytes: &D::ExtractedCommitmentBytes, - plaintext: &[u8], -) -> Option<(D::Note, D::Recipient)> { - let (note, to) = domain.parse_note_plaintext_without_memo_ivk(ivk, &plaintext)?; - - if let NoteValidity::Valid = check_note_validity::(¬e, epk, cmstar_bytes) { - Some((note, to)) - } else { - None - } -} - -fn check_note_validity( - note: &D::Note, - epk: &D::EphemeralPublicKey, - cmstar_bytes: &D::ExtractedCommitmentBytes, -) -> NoteValidity { - if D::ExtractedCommitmentBytes::try_from(D::cmstar(¬e)) - .map_or(false, |cs| &cs == cmstar_bytes) - { - let epk_bytes = D::epk_bytes(epk); - D::check_epk_bytes(¬e, |derived_esk| { - if D::epk_bytes(&D::ka_derive_public(¬e, &derived_esk)) - .ct_eq(&epk_bytes) - .into() - { - NoteValidity::Valid - } else { - NoteValidity::Invalid - } - }) - } else { - // Published commitment doesn't match calculated commitment - NoteValidity::Invalid - } -} - -/// Trial decryption of the compact note plaintext by the recipient for light clients. -/// -/// Attempts to decrypt and validate the first 52 bytes of `enc_ciphertext` using the -/// given `ivk`. If successful, the corresponding Sapling note is returned, along with the -/// `PaymentAddress` to which the note was sent. -/// -/// Implements the procedure specified in [`ZIP 307`]. -/// -/// [`ZIP 307`]: https://zips.z.cash/zip-0307 -pub fn try_compact_note_decryption>( - domain: &D, - ivk: &D::IncomingViewingKey, - output: &Output, -) -> Option<(D::Note, D::Recipient)> { - assert_eq!(output.enc_ciphertext().len(), COMPACT_NOTE_SIZE); - - let shared_secret = D::ka_agree_dec(&ivk, output.epk()); - let key = D::kdf(shared_secret, &D::epk_bytes(output.epk())); - - // Start from block 1 to skip over Poly1305 keying output - let mut plaintext = [0; COMPACT_NOTE_SIZE]; - plaintext.copy_from_slice(output.enc_ciphertext()); - ChaCha20Ietf::xor(key.as_ref(), &[0u8; 12], 1, &mut plaintext); - - parse_note_plaintext_without_memo_ivk( - domain, - ivk, - output.epk(), - &output.cmstar_bytes(), - &plaintext, - ) -} - -/// Recovery of the full note plaintext by the sender. -/// -/// Attempts to decrypt and validate the given `enc_ciphertext` using the given `ock`. -/// If successful, the corresponding Sapling note and memo are returned, along with the -/// `PaymentAddress` to which the note was sent. -/// -/// Implements part of section 4.19.3 of the -/// [Zcash Protocol Specification](https://zips.z.cash/protocol/nu5.pdf#decryptovk) -/// For decryption using a Full Viewing Key see [`try_sapling_output_recovery`]. -pub fn try_output_recovery_with_ock>( - domain: &D, - ock: &OutgoingCipherKey, - output: &Output, - out_ciphertext: &[u8], -) -> Option<(D::Note, D::Recipient, D::Memo)> { - assert_eq!(output.enc_ciphertext().len(), ENC_CIPHERTEXT_SIZE); - assert_eq!(out_ciphertext.len(), OUT_CIPHERTEXT_SIZE); - - let mut op = [0; OUT_CIPHERTEXT_SIZE]; - assert_eq!( - ChachaPolyIetf::aead_cipher() - .open_to(&mut op, &out_ciphertext, &[], ock.as_ref(), &[0u8; 12]) - .ok()?, - OUT_PLAINTEXT_SIZE - ); - - let pk_d = D::extract_pk_d(&op)?; - let esk = D::extract_esk(&op)?; - - let shared_secret = D::ka_agree_enc(&esk, &pk_d); - // The small-order point check at the point of output parsing rejects - // non-canonical encodings, so reencoding here for the KDF should - // be okay. - let key = D::kdf(shared_secret, &D::epk_bytes(output.epk())); - - let mut plaintext = [0; ENC_CIPHERTEXT_SIZE]; - assert_eq!( - ChachaPolyIetf::aead_cipher() - .open_to( - &mut plaintext, - output.enc_ciphertext(), - &[], - key.as_ref(), - &[0u8; 12] - ) - .ok()?, - NOTE_PLAINTEXT_SIZE - ); - - let (note, to) = - domain.parse_note_plaintext_without_memo_ovk(&pk_d, &esk, output.epk(), &plaintext)?; - let memo = domain.extract_memo(&plaintext); - - if let NoteValidity::Valid = - check_note_validity::(¬e, output.epk(), &output.cmstar_bytes()) - { - Some((note, to, memo)) - } else { - None - } -} diff --git a/components/zcash_protocol/CHANGELOG.md b/components/zcash_protocol/CHANGELOG.md new file mode 100644 index 0000000000..b0fa65b910 --- /dev/null +++ b/components/zcash_protocol/CHANGELOG.md @@ -0,0 +1,120 @@ +# Changelog +All notable changes to this library will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this library adheres to Rust's notion of +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Changed +- `zcash_protocol::consensus::NetworkConstants` has added methods: + - `hrp_unified_address` + - `hrp_unified_fvk` + - `hrp_unified_ivk` +- Migrated to `incrementalmerkletree 0.8` for functionality provided + under the `test-dependencies` feature flag. + +## [0.4.3] - 2024-12-16 +### Added +- `zcash_protocol::TxId` (moved from `zcash_primitives::transaction`). + +## [0.4.2] - 2024-12-13 +### Added +- `no-std` compatibility (`alloc` is required). A default-enabled `std` feature + flag has been added gating the `std::error::Error` and `memuse` usage. + +## [0.4.1] - 2024-11-13 +### Added +- `zcash_protocol::value::QuotRem` +- `zcash_protocol::value::Zatoshis::div_with_remainder` +- `impl Mul for zcash_protocol::value::Zatoshis` +- `impl Div for zcash_protocol::value::Zatoshis` + +## [0.4.0] - 2024-10-02 +### Added +- `impl Sub for BlockHeight` unlike the implementation that was + removed in version `0.3.0`, a saturating subtraction for block heights having + a return type of `u32` makes sense for `BlockHeight`. Subtracting one block + height from another yields the delta between them. + +### Changed +- Mainnet activation height has been set for `consensus::BranchId::Nu6`. +- Adding a delta to a `BlockHeight` now uses saturating addition. +- Subtracting a delta to a `BlockHeight` now uses saturating subtraction. + +## [0.3.0] - 2024-08-26 +### Changed +- Testnet activation height has been set for `consensus::BranchId::Nu6`. + +### Removed +- `impl {Add, Sub} for BlockHeight` - these operations were unused, and it + does not make sense to add block heights (it is not a monoid.) + +## [0.2.0] - 2024-08-19 +### Added +- `zcash_protocol::PoolType::{TRANSPARENT, SAPLING, ORCHARD}` + +### Changed +- MSRV is now 1.70.0. +- `consensus::BranchId` now has an additional `Nu6` variant. + +## [0.1.1] - 2024-03-25 +### Added +- `zcash_protocol::memo`: + - `impl TryFrom<&MemoBytes> for Memo` + +### Removed +- `unstable-nu6` and `zfuture` feature flags (use `--cfg zcash_unstable=\"nu6\"` + or `--cfg zcash_unstable=\"zfuture\"` in `RUSTFLAGS` and `RUSTDOCFLAGS` + instead). + +## [0.1.0] - 2024-03-06 +The entries below are relative to the `zcash_primitives` crate as of the tag +`zcash_primitives-0.14.0`. + +### Added +- The following modules have been extracted from `zcash_primitives` and + moved to this crate: + - `consensus` + - `constants` + - `zcash_protocol::value` replaces `zcash_primitives::transaction::components::amount` +- `zcash_protocol::consensus`: + - `NetworkConstants` has been extracted from the `Parameters` trait. Relative to the + state prior to the extraction: + - The Bech32 prefixes now return `&'static str` instead of `&str`. + - Added `NetworkConstants::hrp_tex_address`. + - `NetworkType` + - `Parameters::b58_sprout_address_prefix` +- `zcash_protocol::consensus`: + - `impl Hash for LocalNetwork` +- `zcash_protocol::constants::{mainnet, testnet}::B58_SPROUT_ADDRESS_PREFIX` +- Added in `zcash_protocol::value`: + - `Zatoshis` + - `ZatBalance` + - `MAX_BALANCE` has been added to replace previous instances where + `zcash_protocol::value::MAX_MONEY` was used as a signed value. + +### Changed +- `zcash_protocol::value::COIN` has been changed from an `i64` to a `u64` +- `zcash_protocol::value::MAX_MONEY` has been changed from an `i64` to a `u64` +- `zcash_protocol::consensus::Parameters` has been split into two traits, with + the newly added `NetworkConstants` trait providing all network constant + accessors. Also, the `address_network` method has been replaced with a new + `network_type` method that serves the same purpose. A blanket impl of + `NetworkConstants` is provided for all types that implement `Parameters`, + so call sites for methods that have moved to `NetworkConstants` should + remain unchanged (though they may require an additional `use` statement.) + +### Removed +- From `zcash_protocol::value`: + - `NonNegativeAmount` (use `Zatoshis` instead.) + - `Amount` (use `ZatBalance` instead.) + - The following conversions have been removed relative to `zcash_primitives-0.14.0`, + as `zcash_protocol` does not depend on the `orchard` or `sapling-crypto` crates. + - `From for orchard::NoteValue>` + - `TryFrom for Amount` + - `From for sapling::value::NoteValue>` + - `TryFrom for NonNegativeAmount` + - `impl AddAssign for NonNegativeAmount` + - `impl SubAssign for NonNegativeAmount` diff --git a/components/zcash_protocol/Cargo.toml b/components/zcash_protocol/Cargo.toml new file mode 100644 index 0000000000..9992326369 --- /dev/null +++ b/components/zcash_protocol/Cargo.toml @@ -0,0 +1,59 @@ +[package] +name = "zcash_protocol" +description = "Zcash protocol network constants and value types." +version = "0.4.3" +authors = [ + "Jack Grigg ", + "Kris Nuttycombe ", +] +homepage = "https://github.com/zcash/librustzcash" +repository.workspace = true +readme = "README.md" +license.workspace = true +edition.workspace = true +rust-version = "1.70" +categories = ["cryptography::cryptocurrencies"] +keywords = ["zcash"] + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +# - Logging and metrics +memuse = { workspace = true, optional = true } + +# Dependencies used internally: +# (Breaking upgrades to these are usually backwards-compatible, but check MSRVs.) +# - Documentation +document-features = { workspace = true, optional = true } + +# - Encodings +core2.workspace = true +hex.workspace = true + +# - Test dependencies +proptest = { workspace = true, optional = true } +incrementalmerkletree = { workspace = true, optional = true } +incrementalmerkletree-testing = { workspace = true, optional = true } + +[dev-dependencies] +proptest.workspace = true + +[features] +default = ["std"] +std = ["document-features", "dep:memuse"] + +## Exposes APIs that are useful for testing, such as `proptest` strategies. +test-dependencies = [ + "dep:incrementalmerkletree", + "dep:incrementalmerkletree-testing", + "dep:proptest", + "incrementalmerkletree?/test-dependencies", +] + +## Exposes support for working with a local consensus (e.g. regtest). +local-consensus = [] + +[lints] +workspace = true diff --git a/components/zcash_protocol/LICENSE-APACHE b/components/zcash_protocol/LICENSE-APACHE new file mode 100644 index 0000000000..1e5006dc14 --- /dev/null +++ b/components/zcash_protocol/LICENSE-APACHE @@ -0,0 +1,202 @@ + 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 [yyyy] [name of copyright owner] + +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/components/zcash_protocol/LICENSE-MIT b/components/zcash_protocol/LICENSE-MIT new file mode 100644 index 0000000000..c869731ad4 --- /dev/null +++ b/components/zcash_protocol/LICENSE-MIT @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2021-2024 Electric Coin Company + +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/components/zcash_protocol/README.md b/components/zcash_protocol/README.md new file mode 100644 index 0000000000..862adf0a84 --- /dev/null +++ b/components/zcash_protocol/README.md @@ -0,0 +1,20 @@ +# zcash_protocol + +Zcash network constants and value types. + +## License + +Licensed under either of + + * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or + http://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you, as defined in the Apache-2.0 +license, shall be dual licensed as above, without any additional terms or +conditions. diff --git a/components/zcash_protocol/src/consensus.rs b/components/zcash_protocol/src/consensus.rs new file mode 100644 index 0000000000..b32be4ddb0 --- /dev/null +++ b/components/zcash_protocol/src/consensus.rs @@ -0,0 +1,848 @@ +//! Consensus logic and parameters. + +use core::cmp::{Ord, Ordering}; +use core::convert::TryFrom; +use core::fmt; +use core::ops::{Add, Bound, RangeBounds, Sub}; + +#[cfg(feature = "std")] +use memuse::DynamicUsage; + +use crate::constants::{mainnet, regtest, testnet}; + +/// A wrapper type representing blockchain heights. +/// +/// Safe conversion from various integer types, as well as addition and subtraction, are +/// provided. +#[repr(transparent)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct BlockHeight(u32); + +#[cfg(feature = "std")] +memuse::impl_no_dynamic_usage!(BlockHeight); + +/// The height of the genesis block on a network. +pub const H0: BlockHeight = BlockHeight(0); + +impl BlockHeight { + pub const fn from_u32(v: u32) -> BlockHeight { + BlockHeight(v) + } + + /// Subtracts the provided value from this height, returning `H0` if this would result in + /// underflow of the wrapped `u32`. + pub fn saturating_sub(self, v: u32) -> BlockHeight { + BlockHeight(self.0.saturating_sub(v)) + } +} + +impl fmt::Display for BlockHeight { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(formatter) + } +} + +impl Ord for BlockHeight { + fn cmp(&self, other: &Self) -> Ordering { + self.0.cmp(&other.0) + } +} + +impl PartialOrd for BlockHeight { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl From for BlockHeight { + fn from(value: u32) -> Self { + BlockHeight(value) + } +} + +impl From for u32 { + fn from(value: BlockHeight) -> u32 { + value.0 + } +} + +impl TryFrom for BlockHeight { + type Error = core::num::TryFromIntError; + + fn try_from(value: u64) -> Result { + u32::try_from(value).map(BlockHeight) + } +} + +impl From for u64 { + fn from(value: BlockHeight) -> u64 { + value.0 as u64 + } +} + +impl TryFrom for BlockHeight { + type Error = core::num::TryFromIntError; + + fn try_from(value: i32) -> Result { + u32::try_from(value).map(BlockHeight) + } +} + +impl TryFrom for BlockHeight { + type Error = core::num::TryFromIntError; + + fn try_from(value: i64) -> Result { + u32::try_from(value).map(BlockHeight) + } +} + +impl From for i64 { + fn from(value: BlockHeight) -> i64 { + value.0 as i64 + } +} + +impl Add for BlockHeight { + type Output = Self; + + fn add(self, other: u32) -> Self { + BlockHeight(self.0.saturating_add(other)) + } +} + +impl Sub for BlockHeight { + type Output = Self; + + fn sub(self, other: u32) -> Self { + BlockHeight(self.0.saturating_sub(other)) + } +} + +impl Sub for BlockHeight { + type Output = u32; + + fn sub(self, other: BlockHeight) -> u32 { + self.0.saturating_sub(other.0) + } +} + +/// Constants associated with a given Zcash network. +pub trait NetworkConstants: Clone { + /// The coin type for ZEC, as defined by [SLIP 44]. + /// + /// [SLIP 44]: https://github.com/satoshilabs/slips/blob/master/slip-0044.md + fn coin_type(&self) -> u32; + + /// Returns the human-readable prefix for Bech32-encoded Sapling extended spending keys + /// for the network to which this NetworkConstants value applies. + /// + /// Defined in [ZIP 32]. + /// + /// [`ExtendedSpendingKey`]: zcash_primitives::zip32::ExtendedSpendingKey + /// [ZIP 32]: https://github.com/zcash/zips/blob/master/zip-0032.rst + fn hrp_sapling_extended_spending_key(&self) -> &'static str; + + /// Returns the human-readable prefix for Bech32-encoded Sapling extended full + /// viewing keys for the network to which this NetworkConstants value applies. + /// + /// Defined in [ZIP 32]. + /// + /// [`ExtendedFullViewingKey`]: zcash_primitives::zip32::ExtendedFullViewingKey + /// [ZIP 32]: https://github.com/zcash/zips/blob/master/zip-0032.rst + fn hrp_sapling_extended_full_viewing_key(&self) -> &'static str; + + /// Returns the Bech32-encoded human-readable prefix for Sapling payment addresses + /// for the network to which this NetworkConstants value applies. + /// + /// Defined in section 5.6.4 of the [Zcash Protocol Specification]. + /// + /// [`PaymentAddress`]: zcash_primitives::primitives::PaymentAddress + /// [Zcash Protocol Specification]: https://github.com/zcash/zips/blob/master/protocol/protocol.pdf + fn hrp_sapling_payment_address(&self) -> &'static str; + + /// Returns the human-readable prefix for Base58Check-encoded Sprout + /// payment addresses for the network to which this NetworkConstants value + /// applies. + /// + /// Defined in the [Zcash Protocol Specification section 5.6.3][sproutpaymentaddrencoding]. + /// + /// [sproutpaymentaddrencoding]: https://zips.z.cash/protocol/protocol.pdf#sproutpaymentaddrencoding + fn b58_sprout_address_prefix(&self) -> [u8; 2]; + + /// Returns the human-readable prefix for Base58Check-encoded transparent + /// pay-to-public-key-hash payment addresses for the network to which this NetworkConstants value + /// applies. + /// + /// [`TransparentAddress::PublicKey`]: zcash_primitives::legacy::TransparentAddress::PublicKey + fn b58_pubkey_address_prefix(&self) -> [u8; 2]; + + /// Returns the human-readable prefix for Base58Check-encoded transparent pay-to-script-hash + /// payment addresses for the network to which this NetworkConstants value applies. + /// + /// [`TransparentAddress::Script`]: zcash_primitives::legacy::TransparentAddress::Script + fn b58_script_address_prefix(&self) -> [u8; 2]; + + /// Returns the Bech32-encoded human-readable prefix for TEX addresses, for the + /// network to which this `NetworkConstants` value applies. + /// + /// Defined in [ZIP 320]. + /// + /// [ZIP 320]: https://zips.z.cash/zip-0320 + fn hrp_tex_address(&self) -> &'static str; + + /// The HRP for a Bech32m-encoded mainnet Unified Address. + /// + /// Defined in [ZIP 316][zip-0316]. + /// + /// [zip-0316]: https://zips.z.cash/zip-0316 + fn hrp_unified_address(&self) -> &'static str; + + /// The HRP for a Bech32m-encoded mainnet Unified FVK. + /// + /// Defined in [ZIP 316][zip-0316]. + /// + /// [zip-0316]: https://zips.z.cash/zip-0316 + fn hrp_unified_fvk(&self) -> &'static str; + + /// The HRP for a Bech32m-encoded mainnet Unified IVK. + /// + /// Defined in [ZIP 316][zip-0316]. + /// + /// [zip-0316]: https://zips.z.cash/zip-0316 + fn hrp_unified_ivk(&self) -> &'static str; +} + +/// The enumeration of known Zcash network types. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum NetworkType { + /// Zcash Mainnet. + Main, + /// Zcash Testnet. + Test, + /// Private integration / regression testing, used in `zcashd`. + /// + /// For some address types there is no distinction between test and regtest encodings; + /// those will always be parsed as `Network::Test`. + Regtest, +} + +#[cfg(feature = "std")] +memuse::impl_no_dynamic_usage!(NetworkType); + +impl NetworkConstants for NetworkType { + fn coin_type(&self) -> u32 { + match self { + NetworkType::Main => mainnet::COIN_TYPE, + NetworkType::Test => testnet::COIN_TYPE, + NetworkType::Regtest => regtest::COIN_TYPE, + } + } + + fn hrp_sapling_extended_spending_key(&self) -> &'static str { + match self { + NetworkType::Main => mainnet::HRP_SAPLING_EXTENDED_SPENDING_KEY, + NetworkType::Test => testnet::HRP_SAPLING_EXTENDED_SPENDING_KEY, + NetworkType::Regtest => regtest::HRP_SAPLING_EXTENDED_SPENDING_KEY, + } + } + + fn hrp_sapling_extended_full_viewing_key(&self) -> &'static str { + match self { + NetworkType::Main => mainnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, + NetworkType::Test => testnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, + NetworkType::Regtest => regtest::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, + } + } + + fn hrp_sapling_payment_address(&self) -> &'static str { + match self { + NetworkType::Main => mainnet::HRP_SAPLING_PAYMENT_ADDRESS, + NetworkType::Test => testnet::HRP_SAPLING_PAYMENT_ADDRESS, + NetworkType::Regtest => regtest::HRP_SAPLING_PAYMENT_ADDRESS, + } + } + + fn b58_sprout_address_prefix(&self) -> [u8; 2] { + match self { + NetworkType::Main => mainnet::B58_SPROUT_ADDRESS_PREFIX, + NetworkType::Test => testnet::B58_SPROUT_ADDRESS_PREFIX, + NetworkType::Regtest => regtest::B58_SPROUT_ADDRESS_PREFIX, + } + } + + fn b58_pubkey_address_prefix(&self) -> [u8; 2] { + match self { + NetworkType::Main => mainnet::B58_PUBKEY_ADDRESS_PREFIX, + NetworkType::Test => testnet::B58_PUBKEY_ADDRESS_PREFIX, + NetworkType::Regtest => regtest::B58_PUBKEY_ADDRESS_PREFIX, + } + } + + fn b58_script_address_prefix(&self) -> [u8; 2] { + match self { + NetworkType::Main => mainnet::B58_SCRIPT_ADDRESS_PREFIX, + NetworkType::Test => testnet::B58_SCRIPT_ADDRESS_PREFIX, + NetworkType::Regtest => regtest::B58_SCRIPT_ADDRESS_PREFIX, + } + } + + fn hrp_tex_address(&self) -> &'static str { + match self { + NetworkType::Main => mainnet::HRP_TEX_ADDRESS, + NetworkType::Test => testnet::HRP_TEX_ADDRESS, + NetworkType::Regtest => regtest::HRP_TEX_ADDRESS, + } + } + + fn hrp_unified_address(&self) -> &'static str { + match self { + NetworkType::Main => mainnet::HRP_UNIFIED_ADDRESS, + NetworkType::Test => testnet::HRP_UNIFIED_ADDRESS, + NetworkType::Regtest => regtest::HRP_UNIFIED_ADDRESS, + } + } + + fn hrp_unified_fvk(&self) -> &'static str { + match self { + NetworkType::Main => mainnet::HRP_UNIFIED_FVK, + NetworkType::Test => testnet::HRP_UNIFIED_FVK, + NetworkType::Regtest => regtest::HRP_UNIFIED_FVK, + } + } + + fn hrp_unified_ivk(&self) -> &'static str { + match self { + NetworkType::Main => mainnet::HRP_UNIFIED_IVK, + NetworkType::Test => testnet::HRP_UNIFIED_IVK, + NetworkType::Regtest => regtest::HRP_UNIFIED_IVK, + } + } +} + +/// Zcash consensus parameters. +pub trait Parameters: Clone { + /// Returns the type of network configured by this set of consensus parameters. + fn network_type(&self) -> NetworkType; + + /// Returns the activation height for a particular network upgrade, + /// if an activation height has been set. + fn activation_height(&self, nu: NetworkUpgrade) -> Option; + + /// Determines whether the specified network upgrade is active as of the + /// provided block height on the network to which this Parameters value applies. + fn is_nu_active(&self, nu: NetworkUpgrade, height: BlockHeight) -> bool { + self.activation_height(nu).is_some_and(|h| h <= height) + } +} + +impl NetworkConstants for P { + fn coin_type(&self) -> u32 { + self.network_type().coin_type() + } + + fn hrp_sapling_extended_spending_key(&self) -> &'static str { + self.network_type().hrp_sapling_extended_spending_key() + } + + fn hrp_sapling_extended_full_viewing_key(&self) -> &'static str { + self.network_type().hrp_sapling_extended_full_viewing_key() + } + + fn hrp_sapling_payment_address(&self) -> &'static str { + self.network_type().hrp_sapling_payment_address() + } + + fn b58_sprout_address_prefix(&self) -> [u8; 2] { + self.network_type().b58_sprout_address_prefix() + } + + fn b58_pubkey_address_prefix(&self) -> [u8; 2] { + self.network_type().b58_pubkey_address_prefix() + } + + fn b58_script_address_prefix(&self) -> [u8; 2] { + self.network_type().b58_script_address_prefix() + } + + fn hrp_tex_address(&self) -> &'static str { + self.network_type().hrp_tex_address() + } + + fn hrp_unified_address(&self) -> &'static str { + self.network_type().hrp_unified_address() + } + + fn hrp_unified_fvk(&self) -> &'static str { + self.network_type().hrp_unified_fvk() + } + + fn hrp_unified_ivk(&self) -> &'static str { + self.network_type().hrp_unified_ivk() + } +} + +/// Marker struct for the production network. +#[derive(PartialEq, Eq, Copy, Clone, Debug)] +pub struct MainNetwork; + +#[cfg(feature = "std")] +memuse::impl_no_dynamic_usage!(MainNetwork); + +/// The production network. +pub const MAIN_NETWORK: MainNetwork = MainNetwork; + +impl Parameters for MainNetwork { + fn network_type(&self) -> NetworkType { + NetworkType::Main + } + + fn activation_height(&self, nu: NetworkUpgrade) -> Option { + match nu { + NetworkUpgrade::Overwinter => Some(BlockHeight(347_500)), + NetworkUpgrade::Sapling => Some(BlockHeight(419_200)), + NetworkUpgrade::Blossom => Some(BlockHeight(653_600)), + NetworkUpgrade::Heartwood => Some(BlockHeight(903_000)), + NetworkUpgrade::Canopy => Some(BlockHeight(1_046_400)), + NetworkUpgrade::Nu5 => Some(BlockHeight(1_687_104)), + NetworkUpgrade::Nu6 => Some(BlockHeight(2_726_400)), + #[cfg(zcash_unstable = "zfuture")] + NetworkUpgrade::ZFuture => None, + } + } +} + +/// Marker struct for the test network. +#[derive(PartialEq, Eq, Copy, Clone, Debug)] +pub struct TestNetwork; + +#[cfg(feature = "std")] +memuse::impl_no_dynamic_usage!(TestNetwork); + +/// The test network. +pub const TEST_NETWORK: TestNetwork = TestNetwork; + +impl Parameters for TestNetwork { + fn network_type(&self) -> NetworkType { + NetworkType::Test + } + + fn activation_height(&self, nu: NetworkUpgrade) -> Option { + match nu { + NetworkUpgrade::Overwinter => Some(BlockHeight(207_500)), + NetworkUpgrade::Sapling => Some(BlockHeight(280_000)), + NetworkUpgrade::Blossom => Some(BlockHeight(584_000)), + NetworkUpgrade::Heartwood => Some(BlockHeight(903_800)), + NetworkUpgrade::Canopy => Some(BlockHeight(1_028_500)), + NetworkUpgrade::Nu5 => Some(BlockHeight(1_842_420)), + NetworkUpgrade::Nu6 => Some(BlockHeight(2_976_000)), + #[cfg(zcash_unstable = "zfuture")] + NetworkUpgrade::ZFuture => None, + } + } +} + +/// The enumeration of known Zcash networks. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Network { + /// Zcash Mainnet. + MainNetwork, + /// Zcash Testnet. + TestNetwork, +} + +#[cfg(feature = "std")] +memuse::impl_no_dynamic_usage!(Network); + +impl Parameters for Network { + fn network_type(&self) -> NetworkType { + match self { + Network::MainNetwork => NetworkType::Main, + Network::TestNetwork => NetworkType::Test, + } + } + + fn activation_height(&self, nu: NetworkUpgrade) -> Option { + match self { + Network::MainNetwork => MAIN_NETWORK.activation_height(nu), + Network::TestNetwork => TEST_NETWORK.activation_height(nu), + } + } +} + +/// An event that occurs at a specified height on the Zcash chain, at which point the +/// consensus rules enforced by the network are altered. +/// +/// See [ZIP 200](https://zips.z.cash/zip-0200) for more details. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NetworkUpgrade { + /// The [Overwinter] network upgrade. + /// + /// [Overwinter]: https://z.cash/upgrade/overwinter/ + Overwinter, + /// The [Sapling] network upgrade. + /// + /// [Sapling]: https://z.cash/upgrade/sapling/ + Sapling, + /// The [Blossom] network upgrade. + /// + /// [Blossom]: https://z.cash/upgrade/blossom/ + Blossom, + /// The [Heartwood] network upgrade. + /// + /// [Heartwood]: https://z.cash/upgrade/heartwood/ + Heartwood, + /// The [Canopy] network upgrade. + /// + /// [Canopy]: https://z.cash/upgrade/canopy/ + Canopy, + /// The [Nu5] network upgrade. + /// + /// [Nu5]: https://z.cash/upgrade/nu5/ + Nu5, + /// The [Nu6] network upgrade. + /// + /// [Nu6]: https://z.cash/upgrade/nu6/ + Nu6, + /// The ZFUTURE network upgrade. + /// + /// This upgrade is expected never to activate on mainnet; + /// it is intended for use in integration testing of functionality + /// that is a candidate for integration in a future network upgrade. + #[cfg(zcash_unstable = "zfuture")] + ZFuture, +} + +#[cfg(feature = "std")] +memuse::impl_no_dynamic_usage!(NetworkUpgrade); + +impl fmt::Display for NetworkUpgrade { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + NetworkUpgrade::Overwinter => write!(f, "Overwinter"), + NetworkUpgrade::Sapling => write!(f, "Sapling"), + NetworkUpgrade::Blossom => write!(f, "Blossom"), + NetworkUpgrade::Heartwood => write!(f, "Heartwood"), + NetworkUpgrade::Canopy => write!(f, "Canopy"), + NetworkUpgrade::Nu5 => write!(f, "Nu5"), + NetworkUpgrade::Nu6 => write!(f, "Nu6"), + #[cfg(zcash_unstable = "zfuture")] + NetworkUpgrade::ZFuture => write!(f, "ZFUTURE"), + } + } +} + +impl NetworkUpgrade { + fn branch_id(self) -> BranchId { + match self { + NetworkUpgrade::Overwinter => BranchId::Overwinter, + NetworkUpgrade::Sapling => BranchId::Sapling, + NetworkUpgrade::Blossom => BranchId::Blossom, + NetworkUpgrade::Heartwood => BranchId::Heartwood, + NetworkUpgrade::Canopy => BranchId::Canopy, + NetworkUpgrade::Nu5 => BranchId::Nu5, + NetworkUpgrade::Nu6 => BranchId::Nu6, + #[cfg(zcash_unstable = "zfuture")] + NetworkUpgrade::ZFuture => BranchId::ZFuture, + } + } +} + +/// The network upgrades on the Zcash chain in order of activation. +/// +/// This order corresponds to the activation heights, but because Rust enums are +/// full-fledged algebraic data types, we need to define it manually. +const UPGRADES_IN_ORDER: &[NetworkUpgrade] = &[ + NetworkUpgrade::Overwinter, + NetworkUpgrade::Sapling, + NetworkUpgrade::Blossom, + NetworkUpgrade::Heartwood, + NetworkUpgrade::Canopy, + NetworkUpgrade::Nu5, + NetworkUpgrade::Nu6, +]; + +/// The "grace period" defined in [ZIP 212]. +/// +/// [ZIP 212]: https://zips.z.cash/zip-0212#changes-to-the-process-of-receiving-sapling-or-orchard-notes +pub const ZIP212_GRACE_PERIOD: u32 = 32256; + +/// A globally-unique identifier for a set of consensus rules within the Zcash chain. +/// +/// Each branch ID in this enum corresponds to one of the epochs between a pair of Zcash +/// network upgrades. For example, `BranchId::Overwinter` corresponds to the blocks +/// starting at Overwinter activation, and ending the block before Sapling activation. +/// +/// The main use of the branch ID is in signature generation: transactions commit to a +/// specific branch ID by including it as part of [`signature_hash`]. This ensures +/// two-way replay protection for transactions across network upgrades. +/// +/// See [ZIP 200](https://zips.z.cash/zip-0200) for more details. +/// +/// [`signature_hash`]: https://docs.rs/zcash_primitives/latest/zcash_primitives/transaction/sighash/fn.signature_hash.html +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BranchId { + /// The consensus rules at the launch of Zcash. + Sprout, + /// The consensus rules deployed by [`NetworkUpgrade::Overwinter`]. + Overwinter, + /// The consensus rules deployed by [`NetworkUpgrade::Sapling`]. + Sapling, + /// The consensus rules deployed by [`NetworkUpgrade::Blossom`]. + Blossom, + /// The consensus rules deployed by [`NetworkUpgrade::Heartwood`]. + Heartwood, + /// The consensus rules deployed by [`NetworkUpgrade::Canopy`]. + Canopy, + /// The consensus rules deployed by [`NetworkUpgrade::Nu5`]. + Nu5, + /// The consensus rules deployed by [`NetworkUpgrade::Nu6`]. + Nu6, + /// Candidates for future consensus rules; this branch will never + /// activate on mainnet. + #[cfg(zcash_unstable = "zfuture")] + ZFuture, +} + +#[cfg(feature = "std")] +memuse::impl_no_dynamic_usage!(BranchId); + +impl TryFrom for BranchId { + type Error = &'static str; + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(BranchId::Sprout), + 0x5ba8_1b19 => Ok(BranchId::Overwinter), + 0x76b8_09bb => Ok(BranchId::Sapling), + 0x2bb4_0e60 => Ok(BranchId::Blossom), + 0xf5b9_230b => Ok(BranchId::Heartwood), + 0xe9ff_75a6 => Ok(BranchId::Canopy), + 0xc2d6_d0b4 => Ok(BranchId::Nu5), + 0xc8e7_1055 => Ok(BranchId::Nu6), + #[cfg(zcash_unstable = "zfuture")] + 0xffff_ffff => Ok(BranchId::ZFuture), + _ => Err("Unknown consensus branch ID"), + } + } +} + +impl From for u32 { + fn from(consensus_branch_id: BranchId) -> u32 { + match consensus_branch_id { + BranchId::Sprout => 0, + BranchId::Overwinter => 0x5ba8_1b19, + BranchId::Sapling => 0x76b8_09bb, + BranchId::Blossom => 0x2bb4_0e60, + BranchId::Heartwood => 0xf5b9_230b, + BranchId::Canopy => 0xe9ff_75a6, + BranchId::Nu5 => 0xc2d6_d0b4, + BranchId::Nu6 => 0xc8e7_1055, + #[cfg(zcash_unstable = "zfuture")] + BranchId::ZFuture => 0xffff_ffff, + } + } +} + +impl BranchId { + /// Returns the branch ID corresponding to the consensus rule set that is active at + /// the given height. + /// + /// This is the branch ID that should be used when creating transactions. + pub fn for_height(parameters: &P, height: BlockHeight) -> Self { + for nu in UPGRADES_IN_ORDER.iter().rev() { + if parameters.is_nu_active(*nu, height) { + return nu.branch_id(); + } + } + + // Sprout rules apply before any network upgrade + BranchId::Sprout + } + + /// Returns the range of heights for the consensus epoch associated with this branch id. + /// + /// The resulting tuple implements the [`RangeBounds`] trait. + pub fn height_range(&self, params: &P) -> Option> { + self.height_bounds(params).map(|(lower, upper)| { + ( + Bound::Included(lower), + upper.map_or(Bound::Unbounded, Bound::Excluded), + ) + }) + } + + /// Returns the range of heights for the consensus epoch associated with this branch id. + /// + /// The return type of this value is slightly more precise than [`Self::height_range`]: + /// - `Some((x, Some(y)))` means that the consensus rules corresponding to this branch id + /// are in effect for the range `x..y` + /// - `Some((x, None))` means that the consensus rules corresponding to this branch id are + /// in effect for the range `x..` + /// - `None` means that the consensus rules corresponding to this branch id are never in effect. + pub fn height_bounds( + &self, + params: &P, + ) -> Option<(BlockHeight, Option)> { + match self { + BranchId::Sprout => params + .activation_height(NetworkUpgrade::Overwinter) + .map(|upper| (BlockHeight(0), Some(upper))), + BranchId::Overwinter => params + .activation_height(NetworkUpgrade::Overwinter) + .map(|lower| (lower, params.activation_height(NetworkUpgrade::Sapling))), + BranchId::Sapling => params + .activation_height(NetworkUpgrade::Sapling) + .map(|lower| (lower, params.activation_height(NetworkUpgrade::Blossom))), + BranchId::Blossom => params + .activation_height(NetworkUpgrade::Blossom) + .map(|lower| (lower, params.activation_height(NetworkUpgrade::Heartwood))), + BranchId::Heartwood => params + .activation_height(NetworkUpgrade::Heartwood) + .map(|lower| (lower, params.activation_height(NetworkUpgrade::Canopy))), + BranchId::Canopy => params + .activation_height(NetworkUpgrade::Canopy) + .map(|lower| (lower, params.activation_height(NetworkUpgrade::Nu5))), + BranchId::Nu5 => params + .activation_height(NetworkUpgrade::Nu5) + .map(|lower| (lower, params.activation_height(NetworkUpgrade::Nu6))), + BranchId::Nu6 => params.activation_height(NetworkUpgrade::Nu6).map(|lower| { + #[cfg(zcash_unstable = "zfuture")] + let upper = params.activation_height(NetworkUpgrade::ZFuture); + #[cfg(not(zcash_unstable = "zfuture"))] + let upper = None; + (lower, upper) + }), + #[cfg(zcash_unstable = "zfuture")] + BranchId::ZFuture => params + .activation_height(NetworkUpgrade::ZFuture) + .map(|lower| (lower, None)), + } + } + + pub fn sprout_uses_groth_proofs(&self) -> bool { + !matches!(self, BranchId::Sprout | BranchId::Overwinter) + } +} + +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing { + use proptest::sample::select; + use proptest::strategy::{Just, Strategy}; + + use super::{BlockHeight, BranchId, Parameters}; + + pub fn arb_branch_id() -> impl Strategy { + select(vec![ + BranchId::Sprout, + BranchId::Overwinter, + BranchId::Sapling, + BranchId::Blossom, + BranchId::Heartwood, + BranchId::Canopy, + BranchId::Nu5, + BranchId::Nu6, + #[cfg(zcash_unstable = "zfuture")] + BranchId::ZFuture, + ]) + } + + pub fn arb_height( + branch_id: BranchId, + params: &P, + ) -> impl Strategy> { + branch_id + .height_bounds(params) + .map_or(Strategy::boxed(Just(None)), |(lower, upper)| { + Strategy::boxed( + (lower.0..upper.map_or(u32::MAX, |u| u.0)).prop_map(|h| Some(BlockHeight(h))), + ) + }) + } + + #[cfg(feature = "test-dependencies")] + impl incrementalmerkletree_testing::TestCheckpoint for BlockHeight { + fn from_u64(value: u64) -> Self { + BlockHeight(u32::try_from(value).expect("Test checkpoint ids do not exceed 32 bits")) + } + } +} + +#[cfg(test)] +mod tests { + use super::{ + BlockHeight, BranchId, NetworkUpgrade, Parameters, MAIN_NETWORK, UPGRADES_IN_ORDER, + }; + + #[test] + fn nu_ordering() { + for i in 1..UPGRADES_IN_ORDER.len() { + let nu_a = UPGRADES_IN_ORDER[i - 1]; + let nu_b = UPGRADES_IN_ORDER[i]; + match ( + MAIN_NETWORK.activation_height(nu_a), + MAIN_NETWORK.activation_height(nu_b), + ) { + (Some(a), Some(b)) if a < b => (), + (Some(_), None) => (), + (None, None) => (), + _ => panic!( + "{} should not be before {} in UPGRADES_IN_ORDER", + nu_a, nu_b + ), + } + } + } + + #[test] + fn nu_is_active() { + assert!(!MAIN_NETWORK.is_nu_active(NetworkUpgrade::Overwinter, BlockHeight(0))); + assert!(!MAIN_NETWORK.is_nu_active(NetworkUpgrade::Overwinter, BlockHeight(347_499))); + assert!(MAIN_NETWORK.is_nu_active(NetworkUpgrade::Overwinter, BlockHeight(347_500))); + } + + #[test] + fn branch_id_from_u32() { + assert_eq!(BranchId::try_from(0), Ok(BranchId::Sprout)); + assert!(BranchId::try_from(1).is_err()); + } + + #[test] + fn branch_id_for_height() { + assert_eq!( + BranchId::for_height(&MAIN_NETWORK, BlockHeight(0)), + BranchId::Sprout, + ); + assert_eq!( + BranchId::for_height(&MAIN_NETWORK, BlockHeight(419_199)), + BranchId::Overwinter, + ); + assert_eq!( + BranchId::for_height(&MAIN_NETWORK, BlockHeight(419_200)), + BranchId::Sapling, + ); + assert_eq!( + BranchId::for_height(&MAIN_NETWORK, BlockHeight(903_000)), + BranchId::Heartwood, + ); + assert_eq!( + BranchId::for_height(&MAIN_NETWORK, BlockHeight(1_046_400)), + BranchId::Canopy, + ); + assert_eq!( + BranchId::for_height(&MAIN_NETWORK, BlockHeight(1_687_104)), + BranchId::Nu5, + ); + assert_eq!( + BranchId::for_height(&MAIN_NETWORK, BlockHeight(2_726_399)), + BranchId::Nu5, + ); + assert_eq!( + BranchId::for_height(&MAIN_NETWORK, BlockHeight(2_726_400)), + BranchId::Nu6, + ); + assert_eq!( + BranchId::for_height(&MAIN_NETWORK, BlockHeight(5_000_000)), + BranchId::Nu6, + ); + } +} diff --git a/components/zcash_protocol/src/constants.rs b/components/zcash_protocol/src/constants.rs new file mode 100644 index 0000000000..fc56eca937 --- /dev/null +++ b/components/zcash_protocol/src/constants.rs @@ -0,0 +1,5 @@ +//! Network-specific Zcash constants. + +pub mod mainnet; +pub mod regtest; +pub mod testnet; diff --git a/components/zcash_protocol/src/constants/mainnet.rs b/components/zcash_protocol/src/constants/mainnet.rs new file mode 100644 index 0000000000..ada1a42d16 --- /dev/null +++ b/components/zcash_protocol/src/constants/mainnet.rs @@ -0,0 +1,73 @@ +//! Constants for the Zcash main network. + +/// The mainnet coin type for ZEC, as defined by [SLIP 44]. +/// +/// [SLIP 44]: https://github.com/satoshilabs/slips/blob/master/slip-0044.md +pub const COIN_TYPE: u32 = 133; + +/// The HRP for a Bech32-encoded mainnet Sapling [`ExtendedSpendingKey`]. +/// +/// Defined in [ZIP 32]. +/// +/// [`ExtendedSpendingKey`]: https://docs.rs/sapling-crypto/latest/sapling_crypto/zip32/struct.ExtendedSpendingKey.html +/// [ZIP 32]: https://github.com/zcash/zips/blob/master/zip-0032.rst +pub const HRP_SAPLING_EXTENDED_SPENDING_KEY: &str = "secret-extended-key-main"; + +/// The HRP for a Bech32-encoded mainnet [`ExtendedFullViewingKey`]. +/// +/// Defined in [ZIP 32]. +/// +/// [`ExtendedFullViewingKey`]: https://docs.rs/sapling-crypto/latest/sapling_crypto/zip32/struct.ExtendedFullViewingKey.html +/// [ZIP 32]: https://github.com/zcash/zips/blob/master/zip-0032.rst +pub const HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY: &str = "zxviews"; + +/// The HRP for a Bech32-encoded mainnet Sapling [`PaymentAddress`]. +/// +/// Defined in section 5.6.4 of the [Zcash Protocol Specification]. +/// +/// [`PaymentAddress`]: https://docs.rs/sapling-crypto/latest/sapling_crypto/struct.PaymentAddress.html +/// [Zcash Protocol Specification]: https://github.com/zcash/zips/blob/master/protocol/protocol.pdf +pub const HRP_SAPLING_PAYMENT_ADDRESS: &str = "zs"; + +/// The prefix for a Base58Check-encoded mainnet Sprout address. +/// +/// Defined in the [Zcash Protocol Specification section 5.6.3][sproutpaymentaddrencoding]. +/// +/// [sproutpaymentaddrencoding]: https://zips.z.cash/protocol/protocol.pdf#sproutpaymentaddrencoding +pub const B58_SPROUT_ADDRESS_PREFIX: [u8; 2] = [0x16, 0x9a]; + +/// The prefix for a Base58Check-encoded mainnet [`PublicKeyHash`]. +/// +/// [`PublicKeyHash`]: https://docs.rs/zcash_primitives/latest/zcash_primitives/legacy/enum.TransparentAddress.html +pub const B58_PUBKEY_ADDRESS_PREFIX: [u8; 2] = [0x1c, 0xb8]; + +/// The prefix for a Base58Check-encoded mainnet [`ScriptHash`]. +/// +/// [`ScriptHash`]: https://docs.rs/zcash_primitives/latest/zcash_primitives/legacy/enum.TransparentAddress.html +pub const B58_SCRIPT_ADDRESS_PREFIX: [u8; 2] = [0x1c, 0xbd]; + +/// The HRP for a Bech32m-encoded mainnet [ZIP 320] TEX address. +/// +/// [ZIP 320]: https://zips.z.cash/zip-0320 +pub const HRP_TEX_ADDRESS: &str = "tex"; + +/// The HRP for a Bech32m-encoded mainnet Unified Address. +/// +/// Defined in [ZIP 316][zip-0316]. +/// +/// [zip-0316]: https://zips.z.cash/zip-0316 +pub const HRP_UNIFIED_ADDRESS: &str = "u"; + +/// The HRP for a Bech32m-encoded mainnet Unified FVK. +/// +/// Defined in [ZIP 316][zip-0316]. +/// +/// [zip-0316]: https://zips.z.cash/zip-0316 +pub const HRP_UNIFIED_FVK: &str = "uview"; + +/// The HRP for a Bech32m-encoded mainnet Unified IVK. +/// +/// Defined in [ZIP 316][zip-0316]. +/// +/// [zip-0316]: https://zips.z.cash/zip-0316 +pub const HRP_UNIFIED_IVK: &str = "uivk"; diff --git a/components/zcash_protocol/src/constants/regtest.rs b/components/zcash_protocol/src/constants/regtest.rs new file mode 100644 index 0000000000..c78f9a2950 --- /dev/null +++ b/components/zcash_protocol/src/constants/regtest.rs @@ -0,0 +1,72 @@ +//! # Regtest constants +//! +//! `regtest` is a `zcashd`-specific environment used for local testing. They mostly reuse +//! the testnet constants. +//! These constants are defined in [the `zcashd` codebase]. +//! +//! [the `zcashd` codebase]: + +/// The regtest cointype reuses the testnet cointype +pub const COIN_TYPE: u32 = 1; + +/// The HRP for a Bech32-encoded regtest Sapling [`ExtendedSpendingKey`]. +/// +/// It is defined in [the `zcashd` codebase]. +/// +/// [`ExtendedSpendingKey`]: https://docs.rs/sapling-crypto/latest/sapling_crypto/zip32/struct.ExtendedSpendingKey.html +/// [the `zcashd` codebase]: +pub const HRP_SAPLING_EXTENDED_SPENDING_KEY: &str = "secret-extended-key-regtest"; + +/// The HRP for a Bech32-encoded regtest Sapling [`ExtendedFullViewingKey`]. +/// +/// It is defined in [the `zcashd` codebase]. +/// +/// [`ExtendedFullViewingKey`]: https://docs.rs/sapling-crypto/latest/sapling_crypto/zip32/struct.ExtendedFullViewingKey.html +/// [the `zcashd` codebase]: +pub const HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY: &str = "zxviewregtestsapling"; + +/// The HRP for a Bech32-encoded regtest Sapling [`PaymentAddress`]. +/// +/// It is defined in [the `zcashd` codebase]. +/// +/// [`PaymentAddress`]: https://docs.rs/sapling-crypto/latest/sapling_crypto/struct.PaymentAddress.html +/// [the `zcashd` codebase]: +pub const HRP_SAPLING_PAYMENT_ADDRESS: &str = "zregtestsapling"; + +/// The prefix for a Base58Check-encoded regtest Sprout address. +/// +/// Defined in the [Zcash Protocol Specification section 5.6.3][sproutpaymentaddrencoding]. +/// Same as the testnet prefix. +/// +/// [sproutpaymentaddrencoding]: https://zips.z.cash/protocol/protocol.pdf#sproutpaymentaddrencoding +pub const B58_SPROUT_ADDRESS_PREFIX: [u8; 2] = [0x16, 0xb6]; + +/// The prefix for a Base58Check-encoded regtest transparent [`PublicKeyHash`]. +/// Same as the testnet prefix. +/// +/// [`PublicKeyHash`]: https://docs.rs/zcash_primitives/latest/zcash_primitives/legacy/enum.TransparentAddress.html +pub const B58_PUBKEY_ADDRESS_PREFIX: [u8; 2] = [0x1d, 0x25]; + +/// The prefix for a Base58Check-encoded regtest transparent [`ScriptHash`]. +/// Same as the testnet prefix. +/// +/// [`ScriptHash`]: https://docs.rs/zcash_primitives/latest/zcash_primitives/legacy/enum.TransparentAddress.html +pub const B58_SCRIPT_ADDRESS_PREFIX: [u8; 2] = [0x1c, 0xba]; + +/// The HRP for a Bech32m-encoded regtest [ZIP 320] TEX address. +/// +/// [ZIP 320]: https://zips.z.cash/zip-0320 +pub const HRP_TEX_ADDRESS: &str = "texregtest"; + +/// The HRP for a Bech32m-encoded regtest Unified Address. +/// +/// Defined in [ZIP 316][zip-0316]. +/// +/// [zip-0316]: https://zips.z.cash/zip-0316 +pub const HRP_UNIFIED_ADDRESS: &str = "uregtest"; + +/// The HRP for a Bech32m-encoded regtest Unified FVK. +pub const HRP_UNIFIED_FVK: &str = "uviewregtest"; + +/// The HRP for a Bech32m-encoded regtest Unified IVK. +pub const HRP_UNIFIED_IVK: &str = "uivkregtest"; diff --git a/components/zcash_protocol/src/constants/testnet.rs b/components/zcash_protocol/src/constants/testnet.rs new file mode 100644 index 0000000000..52e90a2561 --- /dev/null +++ b/components/zcash_protocol/src/constants/testnet.rs @@ -0,0 +1,73 @@ +//! Constants for the Zcash test network. + +/// The testnet coin type for ZEC, as defined by [SLIP 44]. +/// +/// [SLIP 44]: https://github.com/satoshilabs/slips/blob/master/slip-0044.md +pub const COIN_TYPE: u32 = 1; + +/// The HRP for a Bech32-encoded testnet Sapling [`ExtendedSpendingKey`]. +/// +/// Defined in [ZIP 32]. +/// +/// [`ExtendedSpendingKey`]: https://docs.rs/sapling-crypto/latest/sapling_crypto/zip32/struct.ExtendedSpendingKey.html +/// [ZIP 32]: https://github.com/zcash/zips/blob/master/zip-0032.rst +pub const HRP_SAPLING_EXTENDED_SPENDING_KEY: &str = "secret-extended-key-test"; + +/// The HRP for a Bech32-encoded testnet Sapling [`ExtendedFullViewingKey`]. +/// +/// Defined in [ZIP 32]. +/// +/// [`ExtendedFullViewingKey`]: https://docs.rs/sapling-crypto/latest/sapling_crypto/zip32/struct.ExtendedFullViewingKey.html +/// [ZIP 32]: https://github.com/zcash/zips/blob/master/zip-0032.rst +pub const HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY: &str = "zxviewtestsapling"; + +/// The HRP for a Bech32-encoded testnet Sapling [`PaymentAddress`]. +/// +/// Defined in section 5.6.4 of the [Zcash Protocol Specification]. +/// +/// [`PaymentAddress`]: https://docs.rs/sapling-crypto/latest/sapling_crypto/struct.PaymentAddress.html +/// [Zcash Protocol Specification]: https://github.com/zcash/zips/blob/master/protocol/protocol.pdf +pub const HRP_SAPLING_PAYMENT_ADDRESS: &str = "ztestsapling"; + +/// The prefix for a Base58Check-encoded testnet Sprout address. +/// +/// Defined in the [Zcash Protocol Specification section 5.6.3][sproutpaymentaddrencoding]. +/// +/// [sproutpaymentaddrencoding]: https://zips.z.cash/protocol/protocol.pdf#sproutpaymentaddrencoding +pub const B58_SPROUT_ADDRESS_PREFIX: [u8; 2] = [0x16, 0xb6]; + +/// The prefix for a Base58Check-encoded testnet transparent [`PublicKeyHash`]. +/// +/// [`PublicKeyHash`]: https://docs.rs/zcash_primitives/latest/zcash_primitives/legacy/enum.TransparentAddress.html +pub const B58_PUBKEY_ADDRESS_PREFIX: [u8; 2] = [0x1d, 0x25]; + +/// The prefix for a Base58Check-encoded testnet transparent [`ScriptHash`]. +/// +/// [`ScriptHash`]: https://docs.rs/zcash_primitives/latest/zcash_primitives/legacy/enum.TransparentAddress.html +pub const B58_SCRIPT_ADDRESS_PREFIX: [u8; 2] = [0x1c, 0xba]; + +/// The HRP for a Bech32m-encoded testnet [ZIP 320] TEX address. +/// +/// [ZIP 320]: https://zips.z.cash/zip-0320 +pub const HRP_TEX_ADDRESS: &str = "textest"; + +/// The HRP for a Bech32m-encoded testnet Unified Address. +/// +/// Defined in [ZIP 316][zip-0316]. +/// +/// [zip-0316]: https://zips.z.cash/zip-0316 +pub const HRP_UNIFIED_ADDRESS: &str = "utest"; + +/// The HRP for a Bech32m-encoded testnet Unified FVK. +/// +/// Defined in [ZIP 316][zip-0316]. +/// +/// [zip-0316]: https://zips.z.cash/zip-0316 +pub const HRP_UNIFIED_FVK: &str = "uviewtest"; + +/// The HRP for a Bech32m-encoded testnet Unified IVK. +/// +/// Defined in [ZIP 316][zip-0316]. +/// +/// [zip-0316]: https://zips.z.cash/zip-0316 +pub const HRP_UNIFIED_IVK: &str = "uivktest"; diff --git a/components/zcash_protocol/src/lib.rs b/components/zcash_protocol/src/lib.rs new file mode 100644 index 0000000000..ae25f66d52 --- /dev/null +++ b/components/zcash_protocol/src/lib.rs @@ -0,0 +1,69 @@ +//! *A crate for Zcash protocol constants and value types.* +//! +//! `zcash_protocol` contains Rust structs, traits and functions that provide the network constants +//! for the Zcash main and test networks, as well types for representing ZEC amounts and value +//! balances. +//! +#![cfg_attr(feature = "std", doc = "## Feature flags")] +#![cfg_attr(feature = "std", doc = document_features::document_features!())] +//! + +#![no_std] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +// Catch documentation errors caused by code changes. +#![deny(rustdoc::broken_intra_doc_links)] +// Temporary until we have addressed all Result cases. +#![allow(clippy::result_unit_err)] + +#[cfg_attr(any(test, feature = "test-dependencies"), macro_use)] +extern crate alloc; + +#[cfg(feature = "std")] +extern crate std; + +use core::fmt; + +pub mod consensus; +pub mod constants; +#[cfg(feature = "local-consensus")] +pub mod local_consensus; +pub mod memo; +pub mod value; + +mod txid; +pub use txid::TxId; + +/// A Zcash shielded transfer protocol. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum ShieldedProtocol { + /// The Sapling protocol + Sapling, + /// The Orchard protocol + Orchard, +} + +/// A value pool in the Zcash protocol. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum PoolType { + /// The transparent value pool + Transparent, + /// A shielded value pool. + Shielded(ShieldedProtocol), +} + +impl PoolType { + pub const TRANSPARENT: PoolType = PoolType::Transparent; + pub const SAPLING: PoolType = PoolType::Shielded(ShieldedProtocol::Sapling); + pub const ORCHARD: PoolType = PoolType::Shielded(ShieldedProtocol::Orchard); +} + +impl fmt::Display for PoolType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PoolType::Transparent => f.write_str("Transparent"), + PoolType::Shielded(ShieldedProtocol::Sapling) => f.write_str("Sapling"), + PoolType::Shielded(ShieldedProtocol::Orchard) => f.write_str("Orchard"), + } + } +} diff --git a/components/zcash_protocol/src/local_consensus.rs b/components/zcash_protocol/src/local_consensus.rs new file mode 100644 index 0000000000..722d772f23 --- /dev/null +++ b/components/zcash_protocol/src/local_consensus.rs @@ -0,0 +1,212 @@ +use crate::consensus::{BlockHeight, NetworkType, NetworkUpgrade, Parameters}; + +/// a `LocalNetwork` setup should define the activation heights +/// of network upgrades. `None` is considered as "not activated" +/// These heights are not validated. Callers shall initialized +/// them according to the settings used on the Full Nodes they +/// are connecting to. +/// +/// Example: +/// Regtest Zcashd using the following `zcash.conf` +/// ``` +/// ## NUPARAMS +/// nuparams=5ba81b19:1 # Overwinter +/// nuparams=76b809bb:1 # Sapling +/// nuparams=2bb40e60:1 # Blossom +/// nuparams=f5b9230b:1 # Heartwood +/// nuparams=e9ff75a6:1 # Canopy +/// nuparams=c2d6d0b4:1 # NU5 +/// nuparams=c8e71055:1 # NU6 +/// ``` +/// would use the following `LocalNetwork` struct +/// ``` +/// let regtest = LocalNetwork { +/// overwinter: Some(BlockHeight::from_u32(1)), +/// sapling: Some(BlockHeight::from_u32(1)), +/// blossom: Some(BlockHeight::from_u32(1)), +/// heartwood: Some(BlockHeight::from_u32(1)), +/// canopy: Some(BlockHeight::from_u32(1)), +/// nu5: Some(BlockHeight::from_u32(1)), +/// nu6: Some(BlockHeight::from_u32(1)), +/// }; +/// ``` +/// +#[derive(Clone, PartialEq, Eq, Copy, Debug, Hash)] +pub struct LocalNetwork { + pub overwinter: Option, + pub sapling: Option, + pub blossom: Option, + pub heartwood: Option, + pub canopy: Option, + pub nu5: Option, + pub nu6: Option, + #[cfg(zcash_unstable = "zfuture")] + pub z_future: Option, +} + +/// Parameters implementation for `LocalNetwork` +impl Parameters for LocalNetwork { + fn network_type(&self) -> NetworkType { + NetworkType::Regtest + } + + fn activation_height(&self, nu: NetworkUpgrade) -> Option { + match nu { + NetworkUpgrade::Overwinter => self.overwinter, + NetworkUpgrade::Sapling => self.sapling, + NetworkUpgrade::Blossom => self.blossom, + NetworkUpgrade::Heartwood => self.heartwood, + NetworkUpgrade::Canopy => self.canopy, + NetworkUpgrade::Nu5 => self.nu5, + NetworkUpgrade::Nu6 => self.nu6, + #[cfg(zcash_unstable = "zfuture")] + NetworkUpgrade::ZFuture => self.z_future, + } + } +} + +#[cfg(test)] +mod tests { + use crate::{ + consensus::{BlockHeight, NetworkConstants, NetworkUpgrade, Parameters}, + constants, + local_consensus::LocalNetwork, + }; + + #[test] + fn regtest_nu_activation() { + let expected_overwinter = BlockHeight::from_u32(1); + let expected_sapling = BlockHeight::from_u32(2); + let expected_blossom = BlockHeight::from_u32(3); + let expected_heartwood = BlockHeight::from_u32(4); + let expected_canopy = BlockHeight::from_u32(5); + let expected_nu5 = BlockHeight::from_u32(6); + let expected_nu6 = BlockHeight::from_u32(7); + #[cfg(zcash_unstable = "zfuture")] + let expected_z_future = BlockHeight::from_u32(8); + + let regtest = LocalNetwork { + overwinter: Some(expected_overwinter), + sapling: Some(expected_sapling), + blossom: Some(expected_blossom), + heartwood: Some(expected_heartwood), + canopy: Some(expected_canopy), + nu5: Some(expected_nu5), + nu6: Some(expected_nu6), + #[cfg(zcash_unstable = "zfuture")] + z_future: Some(expected_z_future), + }; + + assert!(regtest.is_nu_active(NetworkUpgrade::Overwinter, expected_overwinter)); + assert!(regtest.is_nu_active(NetworkUpgrade::Sapling, expected_sapling)); + assert!(regtest.is_nu_active(NetworkUpgrade::Blossom, expected_blossom)); + assert!(regtest.is_nu_active(NetworkUpgrade::Heartwood, expected_heartwood)); + assert!(regtest.is_nu_active(NetworkUpgrade::Canopy, expected_canopy)); + assert!(regtest.is_nu_active(NetworkUpgrade::Nu5, expected_nu5)); + assert!(regtest.is_nu_active(NetworkUpgrade::Nu6, expected_nu6)); + #[cfg(zcash_unstable = "zfuture")] + assert!(!regtest.is_nu_active(NetworkUpgrade::ZFuture, expected_nu5)); + } + + #[test] + fn regtest_activation_heights() { + let expected_overwinter = BlockHeight::from_u32(1); + let expected_sapling = BlockHeight::from_u32(2); + let expected_blossom = BlockHeight::from_u32(3); + let expected_heartwood = BlockHeight::from_u32(4); + let expected_canopy = BlockHeight::from_u32(5); + let expected_nu5 = BlockHeight::from_u32(6); + let expected_nu6 = BlockHeight::from_u32(7); + #[cfg(zcash_unstable = "zfuture")] + let expected_z_future = BlockHeight::from_u32(8); + + let regtest = LocalNetwork { + overwinter: Some(expected_overwinter), + sapling: Some(expected_sapling), + blossom: Some(expected_blossom), + heartwood: Some(expected_heartwood), + canopy: Some(expected_canopy), + nu5: Some(expected_nu5), + nu6: Some(expected_nu6), + #[cfg(zcash_unstable = "zfuture")] + z_future: Some(expected_z_future), + }; + + assert_eq!( + regtest.activation_height(NetworkUpgrade::Overwinter), + Some(expected_overwinter) + ); + assert_eq!( + regtest.activation_height(NetworkUpgrade::Sapling), + Some(expected_sapling) + ); + assert_eq!( + regtest.activation_height(NetworkUpgrade::Blossom), + Some(expected_blossom) + ); + assert_eq!( + regtest.activation_height(NetworkUpgrade::Heartwood), + Some(expected_heartwood) + ); + assert_eq!( + regtest.activation_height(NetworkUpgrade::Canopy), + Some(expected_canopy) + ); + assert_eq!( + regtest.activation_height(NetworkUpgrade::Nu5), + Some(expected_nu5) + ); + #[cfg(zcash_unstable = "zfuture")] + assert_eq!( + regtest.activation_height(NetworkUpgrade::ZFuture), + Some(expected_z_future) + ); + } + + #[test] + fn regtests_constants() { + let expected_overwinter = BlockHeight::from_u32(1); + let expected_sapling = BlockHeight::from_u32(2); + let expected_blossom = BlockHeight::from_u32(3); + let expected_heartwood = BlockHeight::from_u32(4); + let expected_canopy = BlockHeight::from_u32(5); + let expected_nu5 = BlockHeight::from_u32(6); + let expected_nu6 = BlockHeight::from_u32(7); + #[cfg(zcash_unstable = "zfuture")] + let expected_z_future = BlockHeight::from_u32(8); + + let regtest = LocalNetwork { + overwinter: Some(expected_overwinter), + sapling: Some(expected_sapling), + blossom: Some(expected_blossom), + heartwood: Some(expected_heartwood), + canopy: Some(expected_canopy), + nu5: Some(expected_nu5), + nu6: Some(expected_nu6), + #[cfg(zcash_unstable = "zfuture")] + z_future: Some(expected_z_future), + }; + + assert_eq!(regtest.coin_type(), constants::regtest::COIN_TYPE); + assert_eq!( + regtest.hrp_sapling_extended_spending_key(), + constants::regtest::HRP_SAPLING_EXTENDED_SPENDING_KEY + ); + assert_eq!( + regtest.hrp_sapling_extended_full_viewing_key(), + constants::regtest::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY + ); + assert_eq!( + regtest.hrp_sapling_payment_address(), + constants::regtest::HRP_SAPLING_PAYMENT_ADDRESS + ); + assert_eq!( + regtest.b58_pubkey_address_prefix(), + constants::regtest::B58_PUBKEY_ADDRESS_PREFIX + ); + assert_eq!( + regtest.b58_script_address_prefix(), + constants::regtest::B58_SCRIPT_ADDRESS_PREFIX + ); + } +} diff --git a/zcash_primitives/src/memo.rs b/components/zcash_protocol/src/memo.rs similarity index 95% rename from zcash_primitives/src/memo.rs rename to components/zcash_protocol/src/memo.rs index 7f8b385977..be14f4ea60 100644 --- a/zcash_primitives/src/memo.rs +++ b/components/zcash_protocol/src/memo.rs @@ -1,11 +1,15 @@ //! Structs for handling encrypted memos. -use std::cmp::Ordering; -use std::convert::{TryFrom, TryInto}; +use alloc::borrow::ToOwned; +use alloc::boxed::Box; +use alloc::string::String; +use core::cmp::Ordering; +use core::fmt; +use core::ops::Deref; +use core::str; + +#[cfg(feature = "std")] use std::error; -use std::fmt; -use std::ops::Deref; -use std::str; /// Format a byte array as a colon-delimited hex string. /// @@ -29,9 +33,9 @@ where } /// Errors that may result from attempting to construct an invalid memo. -#[derive(Debug, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum Error { - InvalidUtf8(std::str::Utf8Error), + InvalidUtf8(core::str::Utf8Error), TooLong(usize), } @@ -44,6 +48,7 @@ impl fmt::Display for Error { } } +#[cfg(feature = "std")] impl error::Error for Error {} /// The unencrypted memo bytes received alongside a shielded note in a Zcash transaction. @@ -126,7 +131,7 @@ impl MemoBytes { } /// Type-safe wrapper around String to enforce memo length requirements. -#[derive(Clone, PartialEq)] +#[derive(Clone, PartialEq, Eq)] pub struct TextMemo(String); impl From for String { @@ -145,9 +150,10 @@ impl Deref for TextMemo { } /// An unencrypted memo received alongside a shielded note in a Zcash transaction. -#[derive(Clone)] +#[derive(Clone, Default)] pub enum Memo { /// An empty memo field. + #[default] Empty, /// A memo field containing a UTF-8 string. Text(TextMemo), @@ -172,12 +178,6 @@ impl fmt::Debug for Memo { } } -impl Default for Memo { - fn default() -> Self { - Memo::Empty - } -} - impl PartialEq for Memo { fn eq(&self, rhs: &Memo) -> bool { match (self, rhs) { @@ -198,13 +198,25 @@ impl TryFrom for Memo { /// Returns an error if the provided slice does not represent a valid `Memo` (for /// example, if the slice is not 512 bytes, or the encoded `Memo` is non-canonical). fn try_from(bytes: MemoBytes) -> Result { + Self::try_from(&bytes) + } +} + +impl TryFrom<&MemoBytes> for Memo { + type Error = Error; + + /// Parses a `Memo` from its ZIP 302 serialization. + /// + /// Returns an error if the provided slice does not represent a valid `Memo` (for + /// example, if the slice is not 512 bytes, or the encoded `Memo` is non-canonical). + fn try_from(bytes: &MemoBytes) -> Result { match bytes.0[0] { 0xF6 if bytes.0.iter().skip(1).all(|&b| b == 0) => Ok(Memo::Empty), 0xFF => Ok(Memo::Arbitrary(Box::new(bytes.0[1..].try_into().unwrap()))), b if b <= 0xF4 => str::from_utf8(bytes.as_slice()) .map(|r| Memo::Text(TextMemo(r.to_owned()))) .map_err(Error::InvalidUtf8), - _ => Ok(Memo::Future(bytes)), + _ => Ok(Memo::Future(bytes.clone())), } } } @@ -275,8 +287,8 @@ impl str::FromStr for Memo { #[cfg(test)] mod tests { - use std::convert::TryInto; - use std::str::FromStr; + use alloc::boxed::Box; + use alloc::str::FromStr; use super::{Error, Memo, MemoBytes}; diff --git a/components/zcash_protocol/src/txid.rs b/components/zcash_protocol/src/txid.rs new file mode 100644 index 0000000000..245d06fb86 --- /dev/null +++ b/components/zcash_protocol/src/txid.rs @@ -0,0 +1,65 @@ +use alloc::string::ToString; +use core::fmt; +use core2::io::{self, Read, Write}; + +#[cfg(feature = "std")] +use memuse::DynamicUsage; + +/// The identifier for a Zcash transaction. +/// +/// - For v1-4 transactions, this is a double-SHA-256 hash of the encoded transaction. +/// This means that it is malleable, and only a reliable identifier for transactions +/// that have been mined. +/// - For v5 transactions onwards, this identifier is derived only from "effecting" data, +/// and is non-malleable in all contexts. +#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] +pub struct TxId([u8; 32]); + +#[cfg(feature = "std")] +memuse::impl_no_dynamic_usage!(TxId); + +impl fmt::Debug for TxId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // The (byte-flipped) hex string is more useful than the raw bytes, because we can + // look that up in RPC methods and block explorers. + let txid_str = self.to_string(); + f.debug_tuple("TxId").field(&txid_str).finish() + } +} + +impl fmt::Display for TxId { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut data = self.0; + data.reverse(); + formatter.write_str(&hex::encode(data)) + } +} + +impl AsRef<[u8; 32]> for TxId { + fn as_ref(&self) -> &[u8; 32] { + &self.0 + } +} + +impl From for [u8; 32] { + fn from(value: TxId) -> Self { + value.0 + } +} + +impl TxId { + pub const fn from_bytes(bytes: [u8; 32]) -> Self { + TxId(bytes) + } + + pub fn read(mut reader: R) -> io::Result { + let mut hash = [0u8; 32]; + reader.read_exact(&mut hash)?; + Ok(TxId::from_bytes(hash)) + } + + pub fn write(&self, mut writer: W) -> io::Result<()> { + writer.write_all(&self.0)?; + Ok(()) + } +} diff --git a/components/zcash_protocol/src/value.rs b/components/zcash_protocol/src/value.rs new file mode 100644 index 0000000000..91385b5e87 --- /dev/null +++ b/components/zcash_protocol/src/value.rs @@ -0,0 +1,575 @@ +use core::convert::{Infallible, TryFrom}; +use core::fmt; +use core::iter::Sum; +use core::num::NonZeroU64; +use core::ops::{Add, Div, Mul, Neg, Sub}; + +#[cfg(feature = "std")] +use std::error; + +#[cfg(feature = "std")] +use memuse::DynamicUsage; + +pub const COIN: u64 = 1_0000_0000; +pub const MAX_MONEY: u64 = 21_000_000 * COIN; +pub const MAX_BALANCE: i64 = MAX_MONEY as i64; + +/// A type-safe representation of a Zcash value delta, in zatoshis. +/// +/// An ZatBalance can only be constructed from an integer that is within the valid monetary +/// range of `{-MAX_MONEY..MAX_MONEY}` (where `MAX_MONEY` = 21,000,000 × 10⁸ zatoshis), +/// and this is preserved as an invariant internally. (A [`Transaction`] containing serialized +/// invalid ZatBalances would also be rejected by the network consensus rules.) +/// +/// [`Transaction`]: https://docs.rs/zcash_primitives/latest/zcash_primitives/transaction/struct.Transaction.html +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Eq, Ord)] +pub struct ZatBalance(i64); + +#[cfg(feature = "std")] +memuse::impl_no_dynamic_usage!(ZatBalance); + +impl ZatBalance { + /// Returns a zero-valued ZatBalance. + pub const fn zero() -> Self { + ZatBalance(0) + } + + /// Creates a constant ZatBalance from an i64. + /// + /// Panics: if the amount is outside the range `{-MAX_BALANCE..MAX_BALANCE}`. + pub const fn const_from_i64(amount: i64) -> Self { + assert!(-MAX_BALANCE <= amount && amount <= MAX_BALANCE); // contains is not const + ZatBalance(amount) + } + + /// Creates a constant ZatBalance from a u64. + /// + /// Panics: if the amount is outside the range `{0..MAX_BALANCE}`. + pub const fn const_from_u64(amount: u64) -> Self { + assert!(amount <= MAX_MONEY); // contains is not const + ZatBalance(amount as i64) + } + + /// Creates an ZatBalance from an i64. + /// + /// Returns an error if the amount is outside the range `{-MAX_BALANCE..MAX_BALANCE}`. + pub fn from_i64(amount: i64) -> Result { + if (-MAX_BALANCE..=MAX_BALANCE).contains(&amount) { + Ok(ZatBalance(amount)) + } else if amount < -MAX_BALANCE { + Err(BalanceError::Underflow) + } else { + Err(BalanceError::Overflow) + } + } + + /// Creates a non-negative ZatBalance from an i64. + /// + /// Returns an error if the amount is outside the range `{0..MAX_BALANCE}`. + pub fn from_nonnegative_i64(amount: i64) -> Result { + if (0..=MAX_BALANCE).contains(&amount) { + Ok(ZatBalance(amount)) + } else if amount < 0 { + Err(BalanceError::Underflow) + } else { + Err(BalanceError::Overflow) + } + } + + /// Creates an ZatBalance from a u64. + /// + /// Returns an error if the amount is outside the range `{0..MAX_MONEY}`. + pub fn from_u64(amount: u64) -> Result { + if amount <= MAX_MONEY { + Ok(ZatBalance(amount as i64)) + } else { + Err(BalanceError::Overflow) + } + } + + /// Reads an ZatBalance from a signed 64-bit little-endian integer. + /// + /// Returns an error if the amount is outside the range `{-MAX_BALANCE..MAX_BALANCE}`. + pub fn from_i64_le_bytes(bytes: [u8; 8]) -> Result { + let amount = i64::from_le_bytes(bytes); + ZatBalance::from_i64(amount) + } + + /// Reads a non-negative ZatBalance from a signed 64-bit little-endian integer. + /// + /// Returns an error if the amount is outside the range `{0..MAX_BALANCE}`. + pub fn from_nonnegative_i64_le_bytes(bytes: [u8; 8]) -> Result { + let amount = i64::from_le_bytes(bytes); + ZatBalance::from_nonnegative_i64(amount) + } + + /// Reads an ZatBalance from an unsigned 64-bit little-endian integer. + /// + /// Returns an error if the amount is outside the range `{0..MAX_BALANCE}`. + pub fn from_u64_le_bytes(bytes: [u8; 8]) -> Result { + let amount = u64::from_le_bytes(bytes); + ZatBalance::from_u64(amount) + } + + /// Returns the ZatBalance encoded as a signed 64-bit little-endian integer. + pub fn to_i64_le_bytes(self) -> [u8; 8] { + self.0.to_le_bytes() + } + + /// Returns `true` if `self` is positive and `false` if the ZatBalance is zero or + /// negative. + pub const fn is_positive(self) -> bool { + self.0.is_positive() + } + + /// Returns `true` if `self` is negative and `false` if the ZatBalance is zero or + /// positive. + pub const fn is_negative(self) -> bool { + self.0.is_negative() + } + + pub fn sum>(values: I) -> Option { + let mut result = ZatBalance::zero(); + for value in values { + result = (result + value)?; + } + Some(result) + } +} + +impl TryFrom for ZatBalance { + type Error = BalanceError; + + fn try_from(value: i64) -> Result { + ZatBalance::from_i64(value) + } +} + +impl From for i64 { + fn from(amount: ZatBalance) -> i64 { + amount.0 + } +} + +impl From<&ZatBalance> for i64 { + fn from(amount: &ZatBalance) -> i64 { + amount.0 + } +} + +impl TryFrom for u64 { + type Error = BalanceError; + + fn try_from(value: ZatBalance) -> Result { + value.0.try_into().map_err(|_| BalanceError::Underflow) + } +} + +impl Add for ZatBalance { + type Output = Option; + + fn add(self, rhs: ZatBalance) -> Option { + ZatBalance::from_i64(self.0 + rhs.0).ok() + } +} + +impl Add for Option { + type Output = Self; + + fn add(self, rhs: ZatBalance) -> Option { + self.and_then(|lhs| lhs + rhs) + } +} + +impl Sub for ZatBalance { + type Output = Option; + + fn sub(self, rhs: ZatBalance) -> Option { + ZatBalance::from_i64(self.0 - rhs.0).ok() + } +} + +impl Sub for Option { + type Output = Self; + + fn sub(self, rhs: ZatBalance) -> Option { + self.and_then(|lhs| lhs - rhs) + } +} + +impl Sum for Option { + fn sum>(mut iter: I) -> Self { + iter.try_fold(ZatBalance::zero(), |acc, a| acc + a) + } +} + +impl<'a> Sum<&'a ZatBalance> for Option { + fn sum>(mut iter: I) -> Self { + iter.try_fold(ZatBalance::zero(), |acc, a| acc + *a) + } +} + +impl Neg for ZatBalance { + type Output = Self; + + fn neg(self) -> Self { + ZatBalance(-self.0) + } +} + +impl Mul for ZatBalance { + type Output = Option; + + fn mul(self, rhs: usize) -> Option { + let rhs: i64 = rhs.try_into().ok()?; + self.0 + .checked_mul(rhs) + .and_then(|i| ZatBalance::try_from(i).ok()) + } +} + +/// A type-safe representation of some nonnegative amount of Zcash. +/// +/// A Zatoshis can only be constructed from an integer that is within the valid monetary +/// range of `{0..MAX_MONEY}` (where `MAX_MONEY` = 21,000,000 × 10⁸ zatoshis). +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Eq, Ord)] +pub struct Zatoshis(u64); + +/// A struct that provides both the quotient and remainder of a division operation. +pub struct QuotRem { + quotient: A, + remainder: A, +} + +impl QuotRem { + /// Returns the quotient portion of the value. + pub fn quotient(&self) -> &A { + &self.quotient + } + + /// Returns the remainder portion of the value. + pub fn remainder(&self) -> &A { + &self.remainder + } +} + +impl Zatoshis { + /// Returns the identity `Zatoshis` + pub const ZERO: Self = Zatoshis(0); + + /// Returns this Zatoshis as a u64. + pub fn into_u64(self) -> u64 { + self.0 + } + + /// Creates a Zatoshis from a u64. + /// + /// Returns an error if the amount is outside the range `{0..MAX_MONEY}`. + pub fn from_u64(amount: u64) -> Result { + if (0..=MAX_MONEY).contains(&amount) { + Ok(Zatoshis(amount)) + } else { + Err(BalanceError::Overflow) + } + } + + /// Creates a constant Zatoshis from a u64. + /// + /// Panics: if the amount is outside the range `{0..MAX_MONEY}`. + pub const fn const_from_u64(amount: u64) -> Self { + assert!(amount <= MAX_MONEY); // contains is not const + Zatoshis(amount) + } + + /// Creates a Zatoshis from an i64. + /// + /// Returns an error if the amount is outside the range `{0..MAX_MONEY}`. + pub fn from_nonnegative_i64(amount: i64) -> Result { + u64::try_from(amount) + .map_err(|_| BalanceError::Underflow) + .and_then(Self::from_u64) + } + + /// Reads an Zatoshis from an unsigned 64-bit little-endian integer. + /// + /// Returns an error if the amount is outside the range `{0..MAX_MONEY}`. + pub fn from_u64_le_bytes(bytes: [u8; 8]) -> Result { + let amount = u64::from_le_bytes(bytes); + Self::from_u64(amount) + } + + /// Reads a Zatoshis from a signed integer represented as a two's + /// complement 64-bit little-endian value. + /// + /// Returns an error if the amount is outside the range `{0..MAX_MONEY}`. + pub fn from_nonnegative_i64_le_bytes(bytes: [u8; 8]) -> Result { + let amount = i64::from_le_bytes(bytes); + Self::from_nonnegative_i64(amount) + } + + /// Returns this Zatoshis encoded as a signed two's complement 64-bit + /// little-endian value. + pub fn to_i64_le_bytes(self) -> [u8; 8] { + (self.0 as i64).to_le_bytes() + } + + /// Returns whether or not this `Zatoshis` is the zero value. + pub fn is_zero(&self) -> bool { + self == &Zatoshis::ZERO + } + + /// Returns whether or not this `Zatoshis` is positive. + pub fn is_positive(&self) -> bool { + self > &Zatoshis::ZERO + } + + /// Divides this `Zatoshis` value by the given divisor and returns the quotient and remainder. + pub fn div_with_remainder(&self, divisor: NonZeroU64) -> QuotRem { + let divisor = u64::from(divisor); + // `self` is already bounds-checked, and both the quotient and remainder + // are <= self, so we don't need to re-check them in division. + QuotRem { + quotient: Zatoshis(self.0 / divisor), + remainder: Zatoshis(self.0 % divisor), + } + } +} + +impl From for ZatBalance { + fn from(n: Zatoshis) -> Self { + ZatBalance(n.0 as i64) + } +} + +impl From<&Zatoshis> for ZatBalance { + fn from(n: &Zatoshis) -> Self { + ZatBalance(n.0 as i64) + } +} + +impl From for u64 { + fn from(n: Zatoshis) -> Self { + n.into_u64() + } +} + +impl TryFrom for Zatoshis { + type Error = BalanceError; + + fn try_from(value: u64) -> Result { + Zatoshis::from_u64(value) + } +} + +impl TryFrom for Zatoshis { + type Error = BalanceError; + + fn try_from(value: ZatBalance) -> Result { + Zatoshis::from_nonnegative_i64(value.0) + } +} + +impl Add for Zatoshis { + type Output = Option; + + fn add(self, rhs: Zatoshis) -> Option { + Self::from_u64(self.0.checked_add(rhs.0)?).ok() + } +} + +impl Add for Option { + type Output = Self; + + fn add(self, rhs: Zatoshis) -> Option { + self.and_then(|lhs| lhs + rhs) + } +} + +impl Sub for Zatoshis { + type Output = Option; + + fn sub(self, rhs: Zatoshis) -> Option { + Zatoshis::from_u64(self.0.checked_sub(rhs.0)?).ok() + } +} + +impl Sub for Option { + type Output = Self; + + fn sub(self, rhs: Zatoshis) -> Option { + self.and_then(|lhs| lhs - rhs) + } +} + +impl Mul for Zatoshis { + type Output = Option; + + fn mul(self, rhs: u64) -> Option { + Zatoshis::from_u64(self.0.checked_mul(rhs)?).ok() + } +} + +impl Mul for Zatoshis { + type Output = Option; + + fn mul(self, rhs: usize) -> Option { + self * u64::try_from(rhs).ok()? + } +} + +impl Sum for Option { + fn sum>(mut iter: I) -> Self { + iter.try_fold(Zatoshis::ZERO, |acc, a| acc + a) + } +} + +impl<'a> Sum<&'a Zatoshis> for Option { + fn sum>(mut iter: I) -> Self { + iter.try_fold(Zatoshis::ZERO, |acc, a| acc + *a) + } +} + +impl Div for Zatoshis { + type Output = Zatoshis; + + fn div(self, rhs: NonZeroU64) -> Zatoshis { + // `self` is already bounds-checked and the quotient is <= self, so + // we don't need to re-check it + Zatoshis(self.0 / u64::from(rhs)) + } +} + +/// A type for balance violations in amount addition and subtraction +/// (overflow and underflow of allowed ranges) +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum BalanceError { + Overflow, + Underflow, +} + +#[cfg(feature = "std")] +impl error::Error for BalanceError {} + +impl fmt::Display for BalanceError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self { + BalanceError::Overflow => { + write!( + f, + "ZatBalance addition resulted in a value outside the valid range." + ) + } + BalanceError::Underflow => write!( + f, + "ZatBalance subtraction resulted in a value outside the valid range." + ), + } + } +} + +impl From for BalanceError { + fn from(_value: Infallible) -> Self { + unreachable!() + } +} + +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing { + use proptest::prelude::prop_compose; + + use super::{ZatBalance, Zatoshis, MAX_BALANCE, MAX_MONEY}; + + prop_compose! { + pub fn arb_zat_balance()(amt in -MAX_BALANCE..MAX_BALANCE) -> ZatBalance { + ZatBalance::from_i64(amt).unwrap() + } + } + + prop_compose! { + pub fn arb_positive_zat_balance()(amt in 1i64..MAX_BALANCE) -> ZatBalance { + ZatBalance::from_i64(amt).unwrap() + } + } + + prop_compose! { + pub fn arb_nonnegative_zat_balance()(amt in 0i64..MAX_BALANCE) -> ZatBalance { + ZatBalance::from_i64(amt).unwrap() + } + } + + prop_compose! { + pub fn arb_zatoshis()(amt in 0u64..MAX_MONEY) -> Zatoshis { + Zatoshis::from_u64(amt).unwrap() + } + } +} + +#[cfg(test)] +mod tests { + use crate::value::MAX_BALANCE; + + use super::ZatBalance; + + #[test] + fn amount_in_range() { + let zero = b"\x00\x00\x00\x00\x00\x00\x00\x00"; + assert_eq!(ZatBalance::from_u64_le_bytes(*zero).unwrap(), ZatBalance(0)); + assert_eq!( + ZatBalance::from_nonnegative_i64_le_bytes(*zero).unwrap(), + ZatBalance(0) + ); + assert_eq!(ZatBalance::from_i64_le_bytes(*zero).unwrap(), ZatBalance(0)); + + let neg_one = b"\xff\xff\xff\xff\xff\xff\xff\xff"; + assert!(ZatBalance::from_u64_le_bytes(*neg_one).is_err()); + assert!(ZatBalance::from_nonnegative_i64_le_bytes(*neg_one).is_err()); + assert_eq!( + ZatBalance::from_i64_le_bytes(*neg_one).unwrap(), + ZatBalance(-1) + ); + + let max_money = b"\x00\x40\x07\x5a\xf0\x75\x07\x00"; + assert_eq!( + ZatBalance::from_u64_le_bytes(*max_money).unwrap(), + ZatBalance(MAX_BALANCE) + ); + assert_eq!( + ZatBalance::from_nonnegative_i64_le_bytes(*max_money).unwrap(), + ZatBalance(MAX_BALANCE) + ); + assert_eq!( + ZatBalance::from_i64_le_bytes(*max_money).unwrap(), + ZatBalance(MAX_BALANCE) + ); + + let max_money_p1 = b"\x01\x40\x07\x5a\xf0\x75\x07\x00"; + assert!(ZatBalance::from_u64_le_bytes(*max_money_p1).is_err()); + assert!(ZatBalance::from_nonnegative_i64_le_bytes(*max_money_p1).is_err()); + assert!(ZatBalance::from_i64_le_bytes(*max_money_p1).is_err()); + + let neg_max_money = b"\x00\xc0\xf8\xa5\x0f\x8a\xf8\xff"; + assert!(ZatBalance::from_u64_le_bytes(*neg_max_money).is_err()); + assert!(ZatBalance::from_nonnegative_i64_le_bytes(*neg_max_money).is_err()); + assert_eq!( + ZatBalance::from_i64_le_bytes(*neg_max_money).unwrap(), + ZatBalance(-MAX_BALANCE) + ); + + let neg_max_money_m1 = b"\xff\xbf\xf8\xa5\x0f\x8a\xf8\xff"; + assert!(ZatBalance::from_u64_le_bytes(*neg_max_money_m1).is_err()); + assert!(ZatBalance::from_nonnegative_i64_le_bytes(*neg_max_money_m1).is_err()); + assert!(ZatBalance::from_i64_le_bytes(*neg_max_money_m1).is_err()); + } + + #[test] + fn add_overflow() { + let v = ZatBalance(MAX_BALANCE); + assert_eq!(v + ZatBalance(1), None) + } + + #[test] + fn sub_underflow() { + let v = ZatBalance(-MAX_BALANCE); + assert_eq!(v - ZatBalance(1), None) + } +} diff --git a/components/zip321/CHANGELOG.md b/components/zip321/CHANGELOG.md new file mode 100644 index 0000000000..8a465764e3 --- /dev/null +++ b/components/zip321/CHANGELOG.md @@ -0,0 +1,48 @@ +# Changelog +All notable changes to this library will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this library adheres to Rust's notion of +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Changed +- MSRV is now 1.81.0. + +## [0.2.0] 2024-10-04 + +### Changed +- Migrated to `zcash_address 0.6` + +## [0.1.0] 2024-08-20 + +The contents of this crate were factored out from `zcash_client_backend` to +provide a better separation of concerns and simpler integration with WASM +builds. The entries below are relative to the `zcash_client_backend` crate as +of `zcash_client_backend-0.10.0`. + +### Added +- `zip321::Payment::new` +- `impl From> for Zip321Error` + +### Changed +- Fields of `zip321::Payment` are now private. Accessors have been provided for + the fields that are no longer public, and `Payment::new` has been added to + serve the needs of payment construction. +- `zip321::Payment::recipient_address()` returns `zcash_address::ZcashAddress` +- `zip321::Payment::without_memo` now takes a `zcash_address::ZcashAddress` for + its `recipient_address` argument. +- Uses of `zcash_primitives::transaction::components::amount::NonNegartiveAmount` + have been replace with `zcash_protocol::value::Zatoshis`. Also, some incorrect + uses of the signed `zcash_primitives::transaction::components::Amount` + type have been corrected via replacement with the `Zatoshis` type. +- The following methods that previously required a + `zcash_primitives::consensus::Parameters` argument to facilitate address + parsing no longer take such an argument. + - `zip321::TransactionRequest::{to_uri, from_uri}` + - `zip321::render::addr_param` + - `zip321::parse::{lead_addr, zcashparam}` +- `zip321::Param::Memo` now boxes its argument. +- `zip321::Param::Addr` now wraps a `zcash_address::ZcashAddress` +- MSRV is now 1.70.0. diff --git a/components/zip321/Cargo.toml b/components/zip321/Cargo.toml new file mode 100644 index 0000000000..96704e411d --- /dev/null +++ b/components/zip321/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "zip321" +description = "Parsing functions and data types for Zcash ZIP 321 Payment Request URIs" +version = "0.2.0" +authors = [ + "Kris Nuttycombe " +] +homepage = "https://github.com/zcash/librustzcash" +repository.workspace = true +readme = "README.md" +license.workspace = true +edition.workspace = true +rust-version.workspace = true +categories.workspace = true + +[dependencies] +zcash_address.workspace = true +zcash_protocol = { workspace = true, features = ["std"] } + +# - Parsing and Encoding +nom = "7" +base64.workspace = true +percent-encoding.workspace = true + +# - Test dependencies +proptest = { workspace = true, optional = true } + +[dev-dependencies] +zcash_address = { workspace = true, features = ["test-dependencies"] } +zcash_protocol = { workspace = true, features = ["std", "test-dependencies"] } +proptest.workspace = true + +[features] +## Exposes APIs that are useful for testing, such as `proptest` strategies. +test-dependencies = ["dep:proptest"] + +[lints] +workspace = true diff --git a/components/zip321/LICENSE-APACHE b/components/zip321/LICENSE-APACHE new file mode 100644 index 0000000000..1e5006dc14 --- /dev/null +++ b/components/zip321/LICENSE-APACHE @@ -0,0 +1,202 @@ + 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 [yyyy] [name of copyright owner] + +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/components/zip321/LICENSE-MIT b/components/zip321/LICENSE-MIT new file mode 100644 index 0000000000..35ad1aa6e9 --- /dev/null +++ b/components/zip321/LICENSE-MIT @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017-2024 Electric Coin Company + +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/components/zip321/README.md b/components/zip321/README.md new file mode 100644 index 0000000000..bccff5b0ad --- /dev/null +++ b/components/zip321/README.md @@ -0,0 +1,22 @@ +# zip321 + +This library contains Rust parsing functions and data types for working with +Zcash ZIP 321 Payment Request URIs. + +## License + +Licensed under either of + + * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or + http://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you, as defined in the Apache-2.0 +license, shall be dual licensed as above, without any additional terms or +conditions. + diff --git a/components/zip321/src/lib.rs b/components/zip321/src/lib.rs new file mode 100644 index 0000000000..ff7a745f41 --- /dev/null +++ b/components/zip321/src/lib.rs @@ -0,0 +1,1177 @@ +//! Reference implementation of the ZIP-321 standard for payment requests. +//! +//! This crate provides data structures, parsing, and rendering functions +//! for interpreting and producing valid ZIP 321 URIs. +//! +//! The specification for ZIP 321 URIs may be found at +use core::fmt::Debug; +use std::{ + collections::BTreeMap, + fmt::{self, Display}, +}; + +use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine}; +use nom::{ + character::complete::char, combinator::all_consuming, multi::separated_list0, + sequence::preceded, +}; + +use zcash_address::{ConversionError, ZcashAddress}; +use zcash_protocol::{ + memo::{self, MemoBytes}, + value::BalanceError, + value::Zatoshis, +}; + +/// Errors that may be produced in decoding of payment requests. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Zip321Error { + /// A memo field in the ZIP 321 URI was not properly base-64 encoded + InvalidBase64(base64::DecodeError), + /// A memo value exceeded 512 bytes in length or could not be interpreted as a UTF-8 string + /// when using a valid UTF-8 lead byte. + MemoBytesError(memo::Error), + /// The ZIP 321 request included more payments than can be created within a single Zcash + /// transaction. The wrapped value is the number of payments in the request. + TooManyPayments(usize), + /// Parsing encountered a duplicate ZIP 321 URI parameter for the returned payment index. + DuplicateParameter(parse::Param, usize), + /// The payment at the wrapped index attempted to include a memo when sending to a + /// transparent recipient address, which is not supported by the protocol. + TransparentMemo(usize), + /// The payment at the wrapped index did not include a recipient address. + RecipientMissing(usize), + /// The ZIP 321 URI was malformed and failed to parse. + ParseError(String), +} + +impl From> for Zip321Error { + fn from(value: ConversionError) -> Self { + Zip321Error::ParseError(format!("Address parsing failed: {}", value)) + } +} + +impl Display for Zip321Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Zip321Error::InvalidBase64(err) => { + write!(f, "Memo value was not correctly base64-encoded: {:?}", err) + } + Zip321Error::MemoBytesError(err) => write!( + f, + "Memo exceeded maximum length or violated UTF-8 encoding restrictions: {:?}", + err + ), + Zip321Error::TooManyPayments(n) => write!( + f, + "Cannot create a Zcash transaction containing {} payments", + n + ), + Zip321Error::DuplicateParameter(param, idx) => write!( + f, + "There is a duplicate {} parameter at index {}", + param.name(), + idx + ), + Zip321Error::TransparentMemo(idx) => write!( + f, + "Payment {} is invalid: cannot send a memo to a transparent recipient address", + idx + ), + Zip321Error::RecipientMissing(idx) => { + write!(f, "Payment {} is missing its recipient address", idx) + } + Zip321Error::ParseError(s) => write!(f, "Parse failure: {}", s), + } + } +} + +impl std::error::Error for Zip321Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Zip321Error::InvalidBase64(err) => Some(err), + Zip321Error::MemoBytesError(err) => Some(err), + _ => None, + } + } +} + +/// Converts a [`MemoBytes`] value to a ZIP 321 compatible base64-encoded string. +/// +/// [`MemoBytes`]: zcash_protocol::memo::MemoBytes +pub fn memo_to_base64(memo: &MemoBytes) -> String { + BASE64_URL_SAFE_NO_PAD.encode(memo.as_slice()) +} + +/// Parse a [`MemoBytes`] value from a ZIP 321 compatible base64-encoded string. +/// +/// [`MemoBytes`]: zcash_protocol::memo::MemoBytes +pub fn memo_from_base64(s: &str) -> Result { + BASE64_URL_SAFE_NO_PAD + .decode(s) + .map_err(Zip321Error::InvalidBase64) + .and_then(|b| MemoBytes::from_bytes(&b).map_err(Zip321Error::MemoBytesError)) +} + +/// A single payment being requested. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Payment { + /// The address to which the payment should be sent. + recipient_address: ZcashAddress, + /// The amount of the payment that is being requested. + amount: Zatoshis, + /// A memo that, if included, must be provided with the payment. + /// If a memo is present and [`recipient_address`] is not a shielded + /// address, the wallet should report an error. + /// + /// [`recipient_address`]: #structfield.recipient_address + memo: Option, + /// A human-readable label for this payment within the larger structure + /// of the transaction request. + label: Option, + /// A human-readable message to be displayed to the user describing the + /// purpose of this payment. + message: Option, + /// A list of other arbitrary key/value pairs associated with this payment. + other_params: Vec<(String, String)>, +} + +impl Payment { + /// Constructs a new [`Payment`] from its constituent parts. + /// + /// Returns `None` if the payment requests that a memo be sent to a recipient that cannot + /// receive a memo. + pub fn new( + recipient_address: ZcashAddress, + amount: Zatoshis, + memo: Option, + label: Option, + message: Option, + other_params: Vec<(String, String)>, + ) -> Option { + if memo.is_none() || recipient_address.can_receive_memo() { + Some(Self { + recipient_address, + amount, + memo, + label, + message, + other_params, + }) + } else { + None + } + } + + /// Constructs a new [`Payment`] paying the given address the specified amount. + pub fn without_memo(recipient_address: ZcashAddress, amount: Zatoshis) -> Self { + Self { + recipient_address, + amount, + memo: None, + label: None, + message: None, + other_params: vec![], + } + } + + /// Returns the payment address to which the payment should be sent. + pub fn recipient_address(&self) -> &ZcashAddress { + &self.recipient_address + } + + /// Returns the value of the payment that is being requested, in zatoshis. + pub fn amount(&self) -> Zatoshis { + self.amount + } + + /// Returns the memo that, if included, must be provided with the payment. + pub fn memo(&self) -> Option<&MemoBytes> { + self.memo.as_ref() + } + + /// A human-readable label for this payment within the larger structure + /// of the transaction request. + pub fn label(&self) -> Option<&String> { + self.label.as_ref() + } + + /// A human-readable message to be displayed to the user describing the + /// purpose of this payment. + pub fn message(&self) -> Option<&String> { + self.message.as_ref() + } + + /// A list of other arbitrary key/value pairs associated with this payment. + pub fn other_params(&self) -> &[(String, String)] { + self.other_params.as_ref() + } + + /// A utility for use in tests to help check round-trip serialization properties. + #[cfg(any(test, feature = "test-dependencies"))] + pub(crate) fn normalize(&mut self) { + self.other_params.sort(); + } +} + +/// A ZIP321 transaction request. +/// +/// A ZIP 321 request may include one or more such requests for payment. +/// When constructing a transaction in response to such a request, +/// a separate output should be added to the transaction for each +/// payment value in the request. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TransactionRequest { + payments: BTreeMap, +} + +impl TransactionRequest { + /// Constructs a new empty transaction request. + pub fn empty() -> Self { + Self { + payments: BTreeMap::new(), + } + } + + /// Constructs a new transaction request that obeys the ZIP-321 invariants. + pub fn new(payments: Vec) -> Result { + // Payment indices are limited to 4 digits + if payments.len() > 9999 { + return Err(Zip321Error::TooManyPayments(payments.len())); + } + + let request = TransactionRequest { + payments: payments.into_iter().enumerate().collect(), + }; + + // Enforce validity requirements. + if !request.payments.is_empty() { + TransactionRequest::from_uri(&request.to_uri())?; + } + + Ok(request) + } + + /// Constructs a new transaction request from the provided map from payment + /// index to payment. + /// + /// Payment index 0 will be mapped to the empty payment index. + pub fn from_indexed( + payments: BTreeMap, + ) -> Result { + if let Some(k) = payments.keys().find(|k| **k > 9999) { + // This is not quite the correct error, but close enough. + return Err(Zip321Error::TooManyPayments(*k)); + } + + Ok(TransactionRequest { payments }) + } + + /// Returns the map of payments that make up this request. + /// + /// This is a map from payment index to payment. Payment index `0` is used to denote + /// the empty payment index in the returned values. + pub fn payments(&self) -> &BTreeMap { + &self.payments + } + + /// Returns the total value of payments to be made. + /// + /// Returns `Err` in the case of overflow, or if the value is + /// outside the range `0..=MAX_MONEY` zatoshis. + pub fn total(&self) -> Result { + self.payments + .values() + .map(|p| p.amount) + .try_fold(Zatoshis::ZERO, |acc, a| { + (acc + a).ok_or(BalanceError::Overflow) + }) + } + + /// A utility for use in tests to help check round-trip serialization properties. + #[cfg(any(test, feature = "test-dependencies"))] + pub(crate) fn normalize(&mut self) { + for p in &mut self.payments.values_mut() { + p.normalize(); + } + } + + /// A utility for use in tests to help check round-trip serialization properties. + /// by comparing a two transaction requests for equality after normalization. + #[cfg(test)] + pub(crate) fn normalize_and_eq(a: &mut TransactionRequest, b: &mut TransactionRequest) -> bool { + a.normalize(); + b.normalize(); + + a == b + } + + /// Convert this request to a URI string. + /// + /// Returns None if the payment request is empty. + pub fn to_uri(&self) -> String { + fn payment_params( + payment: &Payment, + payment_index: Option, + ) -> impl IntoIterator + '_ { + std::iter::empty() + .chain(Some(render::amount_param(payment.amount, payment_index))) + .chain( + payment + .memo + .as_ref() + .map(|m| render::memo_param(m, payment_index)), + ) + .chain( + payment + .label + .as_ref() + .map(|m| render::str_param("label", m, payment_index)), + ) + .chain( + payment + .message + .as_ref() + .map(|m| render::str_param("message", m, payment_index)), + ) + .chain( + payment + .other_params + .iter() + .map(move |(name, value)| render::str_param(name, value, payment_index)), + ) + } + + match self.payments.len() { + 0 => "zcash:".to_string(), + 1 if *self.payments.iter().next().unwrap().0 == 0 => { + let (_, payment) = self.payments.iter().next().unwrap(); + let query_params = payment_params(payment, None) + .into_iter() + .collect::>(); + + format!( + "zcash:{}{}{}", + payment.recipient_address.encode(), + if query_params.is_empty() { "" } else { "?" }, + query_params.join("&") + ) + } + _ => { + let query_params = self + .payments + .iter() + .flat_map(|(i, payment)| { + let idx = if *i == 0 { None } else { Some(*i) }; + let primary_address = payment.recipient_address.clone(); + std::iter::empty() + .chain(Some(render::addr_param(&primary_address, idx))) + .chain(payment_params(payment, idx)) + }) + .collect::>(); + + format!("zcash:?{}", query_params.join("&")) + } + } + } + + /// Parse the provided URI to a payment request value. + pub fn from_uri(uri: &str) -> Result { + // Parse the leading zcash:
+ let (rest, primary_addr_param) = parse::lead_addr(uri) + .map_err(|e| Zip321Error::ParseError(format!("Error parsing lead address: {}", e)))?; + + // Parse the remaining parameters as an undifferentiated list + let (_, xs) = if rest.is_empty() { + ("", vec![]) + } else { + all_consuming(preceded( + char('?'), + separated_list0(char('&'), parse::zcashparam), + ))(rest) + .map_err(|e| { + Zip321Error::ParseError(format!("Error parsing query parameters: {}", e)) + })? + }; + + // Construct sets of payment parameters, keyed by the payment index. + let mut params_by_index: BTreeMap> = BTreeMap::new(); + + // Add the primary address, if any, to the index. + if let Some(p) = primary_addr_param { + params_by_index.insert(p.payment_index, vec![p.param]); + } + + // Group the remaining parameters by payment index + for p in xs { + match params_by_index.get_mut(&p.payment_index) { + None => { + params_by_index.insert(p.payment_index, vec![p.param]); + } + + Some(current) => { + if parse::has_duplicate_param(current, &p.param) { + return Err(Zip321Error::DuplicateParameter(p.param, p.payment_index)); + } else { + current.push(p.param); + } + } + } + } + + // Build the actual payment values from the index. + params_by_index + .into_iter() + .map(|(i, params)| parse::to_payment(params, i).map(|payment| (i, payment))) + .collect::, _>>() + .map(|payments| TransactionRequest { payments }) + } +} + +mod render { + use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; + use zcash_address::ZcashAddress; + use zcash_protocol::{ + memo::MemoBytes, + value::{Zatoshis, COIN}, + }; + + use super::memo_to_base64; + + /// The set of ASCII characters that must be percent-encoded according + /// to the definition of ZIP 321. This is the complement of the subset of + /// ASCII characters defined by `qchar` + /// + // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + // allowed-delims = "!" / "$" / "'" / "(" / ")" / "*" / "+" / "," / ";" + // qchar = unreserved / pct-encoded / allowed-delims / ":" / "@" + pub const QCHAR_ENCODE: &AsciiSet = &CONTROLS + .add(b' ') + .add(b'"') + .add(b'#') + .add(b'%') + .add(b'&') + .add(b'/') + .add(b'<') + .add(b'=') + .add(b'>') + .add(b'?') + .add(b'[') + .add(b'\\') + .add(b']') + .add(b'^') + .add(b'`') + .add(b'{') + .add(b'|') + .add(b'}'); + + /// Converts a parameter index value to the `String` representation + /// that must be appended to a parameter name when constructing a ZIP 321 URI. + pub fn param_index(idx: Option) -> String { + match idx { + Some(i) if i > 0 => format!(".{}", i), + _otherwise => "".to_string(), + } + } + + /// Constructs an "address" key/value pair containing the encoded recipient address + /// at the specified parameter index. + pub fn addr_param(addr: &ZcashAddress, idx: Option) -> String { + format!("address{}={}", param_index(idx), addr.encode()) + } + + /// Converts a [`Zatoshis`] value to a correctly formatted decimal ZEC + /// value for inclusion in a ZIP 321 URI. + pub fn amount_str(amount: Zatoshis) -> String { + let coins = u64::from(amount) / COIN; + let zats = u64::from(amount) % COIN; + if zats == 0 { + format!("{}", coins) + } else { + format!("{}.{:0>8}", coins, zats) + .trim_end_matches('0') + .to_string() + } + } + + /// Constructs an "amount" key/value pair containing the encoded ZEC amount + /// at the specified parameter index. + pub fn amount_param(amount: Zatoshis, idx: Option) -> String { + format!("amount{}={}", param_index(idx), amount_str(amount)) + } + + /// Constructs a "memo" key/value pair containing the base64URI-encoded memo + /// at the specified parameter index. + pub fn memo_param(value: &MemoBytes, idx: Option) -> String { + format!("{}{}={}", "memo", param_index(idx), memo_to_base64(value)) + } + + /// Utility function for an arbitrary string key/value pair for inclusion in + /// a ZIP 321 URI at the specified parameter index. + pub fn str_param(label: &str, value: &str, idx: Option) -> String { + format!( + "{}{}={}", + label, + param_index(idx), + utf8_percent_encode(value, QCHAR_ENCODE) + ) + } +} + +mod parse { + use core::fmt::Debug; + + use nom::{ + bytes::complete::{tag, take_till}, + character::complete::{alpha1, char, digit0, digit1, one_of}, + combinator::{all_consuming, map_opt, map_res, opt, recognize}, + sequence::{preceded, separated_pair, tuple}, + AsChar, IResult, InputTakeAtPosition, + }; + use percent_encoding::percent_decode; + use zcash_address::ZcashAddress; + use zcash_protocol::value::BalanceError; + use zcash_protocol::{ + memo::MemoBytes, + value::{Zatoshis, COIN}, + }; + + use super::{memo_from_base64, Payment, Zip321Error}; + + /// A data type that defines the possible parameter types which may occur within a + /// ZIP 321 URI. + #[derive(Debug, Clone, PartialEq, Eq)] + pub enum Param { + Addr(Box), + Amount(Zatoshis), + Memo(Box), + Label(String), + Message(String), + Other(String, String), + } + + impl Param { + /// Returns the name of the parameter from which this value was parsed. + pub fn name(&self) -> String { + match self { + Param::Addr(_) => "address".to_owned(), + Param::Amount(_) => "amount".to_owned(), + Param::Memo(_) => "memo".to_owned(), + Param::Label(_) => "label".to_owned(), + Param::Message(_) => "message".to_owned(), + Param::Other(name, _) => name.clone(), + } + } + } + + /// A [`Param`] value with its associated index. + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct IndexedParam { + pub param: Param, + pub payment_index: usize, + } + + /// Utility function for determining parameter uniqueness. + /// + /// Utility function for determining whether a newly parsed param is a duplicate + /// of a previous parameter. + pub fn has_duplicate_param(v: &[Param], p: &Param) -> bool { + for p0 in v { + match (p0, p) { + (Param::Addr(_), Param::Addr(_)) => return true, + (Param::Amount(_), Param::Amount(_)) => return true, + (Param::Memo(_), Param::Memo(_)) => return true, + (Param::Label(_), Param::Label(_)) => return true, + (Param::Message(_), Param::Message(_)) => return true, + (Param::Other(n, _), Param::Other(n0, _)) if (n == n0) => return true, + _otherwise => continue, + } + } + + false + } + + /// Converts an vector of [`Param`] values to a [`Payment`]. + /// + /// This function performs checks to ensure that the resulting [`Payment`] is structurally + /// valid; for example, a request for memo contents may not be associated with a + /// transparent payment address. + pub fn to_payment(vs: Vec, i: usize) -> Result { + let addr = vs.iter().find_map(|v| match v { + Param::Addr(a) => Some(a.clone()), + _otherwise => None, + }); + + let mut payment = Payment { + recipient_address: *addr.ok_or(Zip321Error::RecipientMissing(i))?, + amount: Zatoshis::ZERO, + memo: None, + label: None, + message: None, + other_params: vec![], + }; + + for v in vs { + match v { + Param::Amount(a) => payment.amount = a, + Param::Memo(m) => { + if payment.recipient_address.can_receive_memo() { + payment.memo = Some(*m); + } else { + return Err(Zip321Error::TransparentMemo(i)); + } + } + Param::Label(m) => payment.label = Some(m), + Param::Message(m) => payment.message = Some(m), + Param::Other(n, m) => payment.other_params.push((n, m)), + _otherwise => {} + } + } + + Ok(payment) + } + + /// Parses and consumes the leading "zcash:\[address\]" from a ZIP 321 URI. + pub fn lead_addr(input: &str) -> IResult<&str, Option> { + map_opt( + preceded(tag("zcash:"), take_till(|c| c == '?')), + |addr_str: &str| { + if addr_str.is_empty() { + Some(None) // no address is ok, so wrap in `Some` + } else { + // `try_from_encoded(..).ok()` returns `None` on error, which we want to then + // cause `map_opt` to fail. + ZcashAddress::try_from_encoded(addr_str) + .map(|a| { + Some(IndexedParam { + param: Param::Addr(Box::new(a)), + payment_index: 0, + }) + }) + .ok() + } + }, + )(input) + } + + /// The primary parser for `name=value` query-string parameter pairs. + pub fn zcashparam(input: &str) -> IResult<&str, IndexedParam> { + map_res( + separated_pair(indexed_name, char('='), recognize(qchars)), + to_indexed_param, + )(input) + } + + /// Extension for the `alphanumeric0` parser which extends that parser + /// by also permitting the characters that are members of the `allowed` + /// string. + fn alphanum_or(allowed: &str) -> impl (Fn(&str) -> IResult<&str, &str>) + '_ { + move |input| { + input.split_at_position_complete(|item| { + let c = item.as_char(); + !(c.is_alphanum() || allowed.contains(c)) + }) + } + } + + /// Parses valid characters which may appear in parameter values. + pub fn qchars(input: &str) -> IResult<&str, &str> { + alphanum_or("-._~!$'()*+,;:@%")(input) + } + + /// Parses valid characters that may appear in parameter names. + pub fn namechars(input: &str) -> IResult<&str, &str> { + alphanum_or("+-")(input) + } + + /// Parses a parameter name and its associated index. + pub fn indexed_name(input: &str) -> IResult<&str, (&str, Option<&str>)> { + let paramname = recognize(tuple((alpha1, namechars))); + + tuple(( + paramname, + opt(preceded( + char('.'), + recognize(tuple(( + one_of("123456789"), + map_opt(digit0, |s: &str| if s.len() > 3 { None } else { Some(s) }), + ))), + )), + ))(input) + } + + /// Parses a value in decimal ZEC. + pub fn parse_amount(input: &str) -> IResult<&str, Zatoshis> { + map_res( + all_consuming(tuple(( + digit1, + opt(preceded( + char('.'), + map_opt(digit1, |s: &str| if s.len() > 8 { None } else { Some(s) }), + )), + ))), + |(whole_s, decimal_s): (&str, Option<&str>)| { + let coins: u64 = whole_s + .to_string() + .parse::() + .map_err(|e| e.to_string())?; + + let zats: u64 = match decimal_s { + Some(d) => format!("{:0<8}", d) + .parse::() + .map_err(|e| e.to_string())?, + None => 0, + }; + + coins + .checked_mul(COIN) + .and_then(|coin_zats| coin_zats.checked_add(zats)) + .ok_or(BalanceError::Overflow) + .and_then(Zatoshis::from_u64) + .map_err(|_| format!("Not a valid zat amount: {}.{}", coins, zats)) + }, + )(input) + } + + fn to_indexed_param( + ((name, iopt), value): ((&str, Option<&str>), &str), + ) -> Result { + let param = match name { + "address" => ZcashAddress::try_from_encoded(value) + .map(Box::new) + .map(Param::Addr) + .map_err(|err| { + format!( + "Could not interpret {} as a valid Zcash address: {}", + value, err + ) + }), + + "amount" => parse_amount(value) + .map_err(|e| e.to_string()) + .map(|(_, a)| Param::Amount(a)), + + "label" => percent_decode(value.as_bytes()) + .decode_utf8() + .map(|s| Param::Label(s.into_owned())) + .map_err(|e| e.to_string()), + + "message" => percent_decode(value.as_bytes()) + .decode_utf8() + .map(|s| Param::Message(s.into_owned())) + .map_err(|e| e.to_string()), + + "memo" => memo_from_base64(value) + .map(Box::new) + .map(Param::Memo) + .map_err(|e| format!("Decoded memo was invalid: {:?}", e)), + + other if other.starts_with("req-") => { + Err(format!("Required parameter {} not recognized", other)) + } + + other => percent_decode(value.as_bytes()) + .decode_utf8() + .map(|s| Param::Other(other.to_string(), s.into_owned())) + .map_err(|e| e.to_string()), + }?; + + let payment_index = match iopt { + Some(istr) => istr.parse::().map(Some).map_err(|e| e.to_string()), + None => Ok(None), + }?; + + Ok(IndexedParam { + param, + payment_index: payment_index.unwrap_or(0), + }) + } +} + +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing { + use proptest::collection::btree_map; + use proptest::collection::vec; + use proptest::option; + use proptest::prelude::{any, prop_compose}; + + use zcash_address::testing::arb_address; + use zcash_protocol::{consensus::NetworkType, value::testing::arb_zatoshis}; + + use super::{MemoBytes, Payment, TransactionRequest}; + pub const VALID_PARAMNAME: &str = "[a-zA-Z][a-zA-Z0-9+-]*"; + + prop_compose! { + pub fn arb_valid_memo()(bytes in vec(any::(), 0..512)) -> MemoBytes { + MemoBytes::from_bytes(&bytes).unwrap() + } + } + + prop_compose! { + pub fn arb_zip321_payment(network: NetworkType)( + recipient_address in arb_address(network), + amount in arb_zatoshis(), + memo in option::of(arb_valid_memo()), + message in option::of(any::()), + label in option::of(any::()), + // prevent duplicates by generating a set rather than a vec + other_params in btree_map(VALID_PARAMNAME, any::(), 0..3), + ) -> Payment { + let memo = memo.filter(|_| recipient_address.can_receive_memo()); + Payment { + recipient_address, + amount, + memo, + label, + message, + other_params: other_params.into_iter().collect(), + } + } + } + + prop_compose! { + pub fn arb_zip321_request(network: NetworkType)( + payments in btree_map(0usize..10000, arb_zip321_payment(network), 1..10) + ) -> TransactionRequest { + let mut req = TransactionRequest::from_indexed(payments).unwrap(); + req.normalize(); // just to make test comparisons easier + req + } + } + + prop_compose! { + pub fn arb_zip321_request_sequential(network: NetworkType)( + payments in vec(arb_zip321_payment(network), 1..10) + ) -> TransactionRequest { + let mut req = TransactionRequest::new(payments).unwrap(); + req.normalize(); // just to make test comparisons easier + req + } + } + + prop_compose! { + pub fn arb_zip321_uri(network: NetworkType)(req in arb_zip321_request(network)) -> String { + req.to_uri() + } + } + + prop_compose! { + pub fn arb_addr_str(network: NetworkType)( + recipient_address in arb_address(network) + ) -> String { + recipient_address.encode() + } + } +} + +#[cfg(test)] +mod tests { + use proptest::prelude::{any, proptest}; + use std::str::FromStr; + + use zcash_address::{testing::arb_address, ZcashAddress}; + use zcash_protocol::{ + consensus::NetworkType, + memo::{Memo, MemoBytes}, + value::{testing::arb_zatoshis, Zatoshis}, + }; + + use super::{ + memo_from_base64, memo_to_base64, + parse::{parse_amount, zcashparam, Param}, + render::{amount_str, memo_param, str_param}, + testing::{arb_addr_str, arb_valid_memo, arb_zip321_request, arb_zip321_uri}, + Payment, TransactionRequest, + }; + + fn check_roundtrip(req: TransactionRequest) { + let req_uri = req.to_uri(); + let parsed = TransactionRequest::from_uri(&req_uri).unwrap(); + assert_eq!(parsed, req); + } + + #[test] + fn test_zip321_roundtrip_simple_amounts() { + let amounts = vec![1u64, 1000u64, 100000u64, 100000000u64, 100000000000u64]; + + for amt_u64 in amounts { + let amt = Zatoshis::const_from_u64(amt_u64); + let amt_str = amount_str(amt); + assert_eq!(amt, parse_amount(&amt_str).unwrap().1); + } + } + + #[test] + fn test_zip321_parse_empty_message() { + let fragment = "message="; + + let result = zcashparam(fragment).unwrap().1.param; + assert_eq!(result, Param::Message("".to_string())); + } + + #[test] + fn test_zip321_parse_simple() { + let uri = "zcash:ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k?amount=3768769.02796286&message="; + let parse_result = TransactionRequest::from_uri(uri).unwrap(); + + let expected = TransactionRequest::new( + vec![ + Payment { + recipient_address: ZcashAddress::try_from_encoded("ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap(), + amount: Zatoshis::const_from_u64(376876902796286), + memo: None, + label: None, + message: Some("".to_string()), + other_params: vec![], + } + ] + ).unwrap(); + + assert_eq!(parse_result, expected); + } + + #[test] + fn test_zip321_parse_no_query_params() { + let uri = "zcash:ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k"; + let parse_result = TransactionRequest::from_uri(uri).unwrap(); + + let expected = TransactionRequest::new( + vec![ + Payment { + recipient_address: ZcashAddress::try_from_encoded("ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap(), + amount: Zatoshis::ZERO, + memo: None, + label: None, + message: None, + other_params: vec![], + } + ] + ).unwrap(); + + assert_eq!(parse_result, expected); + } + + #[test] + fn test_zip321_roundtrip_empty_message() { + let req = TransactionRequest::new( + vec![ + Payment { + recipient_address: ZcashAddress::try_from_encoded("ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap(), + amount: Zatoshis::ZERO, + memo: None, + label: None, + message: Some("".to_string()), + other_params: vec![] + } + ] + ).unwrap(); + + check_roundtrip(req); + } + + #[test] + fn test_zip321_memos() { + let m_simple: MemoBytes = Memo::from_str("This is a simple memo.").unwrap().into(); + let m_simple_64 = memo_to_base64(&m_simple); + assert_eq!(memo_from_base64(&m_simple_64).unwrap(), m_simple); + + let m_json: MemoBytes = Memo::from_str("{ \"key\": \"This is a JSON-structured memo.\" }") + .unwrap() + .into(); + let m_json_64 = memo_to_base64(&m_json); + assert_eq!(memo_from_base64(&m_json_64).unwrap(), m_json); + + let m_unicode: MemoBytes = Memo::from_str("This is a unicode memo ✨🦄🏆🎉") + .unwrap() + .into(); + let m_unicode_64 = memo_to_base64(&m_unicode); + assert_eq!(memo_from_base64(&m_unicode_64).unwrap(), m_unicode); + } + + #[test] + fn test_zip321_spec_valid_examples() { + let valid_0 = "zcash:"; + let v0r = TransactionRequest::from_uri(valid_0).unwrap(); + assert!(v0r.payments.is_empty()); + + let valid_0 = "zcash:?"; + let v0r = TransactionRequest::from_uri(valid_0).unwrap(); + assert!(v0r.payments.is_empty()); + + let valid_1 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=1&memo=VGhpcyBpcyBhIHNpbXBsZSBtZW1vLg&message=Thank%20you%20for%20your%20purchase"; + let v1r = TransactionRequest::from_uri(valid_1).unwrap(); + assert_eq!( + v1r.payments.get(&0).map(|p| p.amount), + Some(Zatoshis::const_from_u64(100000000)) + ); + + let valid_2 = "zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=123.456&address.1=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.1=0.789&memo.1=VGhpcyBpcyBhIHVuaWNvZGUgbWVtbyDinKjwn6aE8J-PhvCfjok"; + let mut v2r = TransactionRequest::from_uri(valid_2).unwrap(); + v2r.normalize(); + assert_eq!( + v2r.payments.get(&0).map(|p| p.amount), + Some(Zatoshis::const_from_u64(12345600000)) + ); + assert_eq!( + v2r.payments.get(&1).map(|p| p.amount), + Some(Zatoshis::const_from_u64(78900000)) + ); + + // valid; amount just less than MAX_MONEY + // 20999999.99999999 + let valid_3 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=20999999.99999999"; + let v3r = TransactionRequest::from_uri(valid_3).unwrap(); + assert_eq!( + v3r.payments.get(&0).map(|p| p.amount), + Some(Zatoshis::const_from_u64(2099999999999999)) + ); + + // valid; MAX_MONEY + // 21000000 + let valid_4 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=21000000"; + let v4r = TransactionRequest::from_uri(valid_4).unwrap(); + assert_eq!( + v4r.payments.get(&0).map(|p| p.amount), + Some(Zatoshis::const_from_u64(2100000000000000)) + ); + } + + #[test] + fn test_zip321_spec_regtest_valid_examples() { + let valid_1 = "zcash:zregtestsapling1qqqqqqqqqqqqqqqqqqcguyvaw2vjk4sdyeg0lc970u659lvhqq7t0np6hlup5lusxle7505hlz3?amount=1&memo=VGhpcyBpcyBhIHNpbXBsZSBtZW1vLg&message=Thank%20you%20for%20your%20purchase"; + let v1r = TransactionRequest::from_uri(valid_1).unwrap(); + assert_eq!( + v1r.payments.get(&0).map(|p| p.amount), + Some(Zatoshis::const_from_u64(100000000)) + ); + } + + #[test] + fn test_zip321_spec_invalid_examples() { + // invalid; empty string + let invalid_0 = ""; + let i0r = TransactionRequest::from_uri(invalid_0); + assert!(i0r.is_err()); + + // invalid; missing `address=` + let invalid_1 = "zcash:?amount=3491405.05201255&address.1=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.1=5740296.87793245"; + let i1r = TransactionRequest::from_uri(invalid_1); + assert!(i1r.is_err()); + + // invalid; missing `address.1=` + let invalid_2 = "zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=1&amount.1=2&address.2=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez"; + let i2r = TransactionRequest::from_uri(invalid_2); + assert!(i2r.is_err()); + + // invalid; `address.0=` and `amount.0=` are not permitted (leading 0s). + let invalid_3 = "zcash:?address.0=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.0=2"; + let i3r = TransactionRequest::from_uri(invalid_3); + assert!(i3r.is_err()); + + // invalid; duplicate `amount=` field + let invalid_4 = + "zcash:?amount=1.234&amount=2.345&address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU"; + let i4r = TransactionRequest::from_uri(invalid_4); + assert!(i4r.is_err()); + + // invalid; duplicate `amount.1=` field + let invalid_5 = + "zcash:?amount.1=1.234&amount.1=2.345&address.1=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU"; + let i5r = TransactionRequest::from_uri(invalid_5); + assert!(i5r.is_err()); + + //invalid; memo associated with t-addr + let invalid_6 = "zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=123.456&memo=eyAia2V5IjogIlRoaXMgaXMgYSBKU09OLXN0cnVjdHVyZWQgbWVtby4iIH0&address.1=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.1=0.789&memo.1=VGhpcyBpcyBhIHVuaWNvZGUgbWVtbyDinKjwn6aE8J-PhvCfjok"; + let i6r = TransactionRequest::from_uri(invalid_6); + assert!(i6r.is_err()); + + // invalid; amount component exceeds an i64 + // 9223372036854775808 = i64::MAX + 1 + let invalid_7 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=9223372036854775808"; + let i7r = TransactionRequest::from_uri(invalid_7); + assert!(i7r.is_err()); + + // invalid; amount component wraps into a valid small positive i64 + // 18446744073709551624 + let invalid_7a = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=18446744073709551624"; + let i7ar = TransactionRequest::from_uri(invalid_7a); + assert!(i7ar.is_err()); + + // invalid; amount component is MAX_MONEY + // 21000000.00000001 + let invalid_8 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=21000000.00000001"; + let i8r = TransactionRequest::from_uri(invalid_8); + assert!(i8r.is_err()); + + // invalid; negative amount + let invalid_9 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=-1"; + let i9r = TransactionRequest::from_uri(invalid_9); + assert!(i9r.is_err()); + + // invalid; parameter index too large + let invalid_10 = + "zcash:?amount.10000=1.23&address.10000=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU"; + let i10r = TransactionRequest::from_uri(invalid_10); + assert!(i10r.is_err()); + + // invalid: bad amount format + let invalid_11 = "zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=123."; + let i11r = TransactionRequest::from_uri(invalid_11); + assert!(i11r.is_err()); + } + + proptest! { + #[test] + fn prop_zip321_roundtrip_address(addr in arb_address(NetworkType::Test)) { + let a = addr.encode(); + assert_eq!(ZcashAddress::try_from_encoded(&a), Ok(addr)); + } + + #[test] + fn prop_zip321_roundtrip_address_str(a in arb_addr_str(NetworkType::Test)) { + let addr = ZcashAddress::try_from_encoded(&a).unwrap(); + assert_eq!(addr.encode(), a); + } + + #[test] + fn prop_zip321_roundtrip_amount(amt in arb_zatoshis()) { + let amt_str = amount_str(amt); + assert_eq!(amt, parse_amount(&amt_str).unwrap().1); + } + + #[test] + fn prop_zip321_roundtrip_str_param( + message in any::(), i in proptest::option::of(0usize..2000)) { + let fragment = str_param("message", &message, i); + let (rest, iparam) = zcashparam(&fragment).unwrap(); + assert_eq!(rest, ""); + assert_eq!(iparam.param, Param::Message(message)); + assert_eq!(iparam.payment_index, i.unwrap_or(0)); + } + + #[test] + fn prop_zip321_roundtrip_memo_param( + memo in arb_valid_memo(), i in proptest::option::of(0usize..2000)) { + let fragment = memo_param(&memo, i); + let (rest, iparam) = zcashparam(&fragment).unwrap(); + assert_eq!(rest, ""); + assert_eq!(iparam.param, Param::Memo(Box::new(memo))); + assert_eq!(iparam.payment_index, i.unwrap_or(0)); + } + + #[test] + fn prop_zip321_roundtrip_request(mut req in arb_zip321_request(NetworkType::Test)) { + let req_uri = req.to_uri(); + let mut parsed = TransactionRequest::from_uri(&req_uri).unwrap(); + assert!(TransactionRequest::normalize_and_eq(&mut parsed, &mut req)); + } + + #[test] + fn prop_zip321_roundtrip_uri(uri in arb_zip321_uri(NetworkType::Test)) { + let mut parsed = TransactionRequest::from_uri(&uri).unwrap(); + parsed.normalize(); + let serialized = parsed.to_uri(); + assert_eq!(serialized, uri) + } + } +} diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000000..d9b2c8f95a --- /dev/null +++ b/deny.toml @@ -0,0 +1,65 @@ +# Configuration file for cargo-deny + +[graph] +targets = [ + # Targets used by zcashd + { triple = "aarch64-unknown-linux-gnu" }, + { triple = "x86_64-apple-darwin" }, + { triple = "x86_64-pc-windows-gnu" }, + { triple = "x86_64-unknown-freebsd" }, + { triple = "x86_64-unknown-linux-gnu" }, + # Targets used by zcash-android-wallet-sdk + { triple = "aarch64-linux-android" }, + { triple = "armv7-linux-androideabi" }, + { triple = "i686-linux-android" }, + { triple = "x86_64-linux-android" }, + # Targets used by zcash-swift-wallet-sdk + { triple = "aarch64-apple-darwin" }, + { triple = "aarch64-apple-ios" }, + { triple = "aarch64-apple-ios-sim" }, + { triple = "x86_64-apple-darwin" }, + { triple = "x86_64-apple-ios" }, +] +all-features = true +exclude-dev = true + +[licenses] +version = 2 +allow = [ + "Apache-2.0", + "MIT", +] +exceptions = [ + { name = "arrayref", allow = ["BSD-2-Clause"] }, + { name = "async_executors", allow = ["Unlicense"] }, + { name = "bounded-vec-deque", allow = ["BSD-3-Clause"] }, + { name = "coarsetime", allow = ["ISC"] }, + { name = "curve25519-dalek", allow = ["BSD-3-Clause"] }, + { name = "ed25519-dalek", allow = ["BSD-3-Clause"] }, + { name = "inotify", allow = ["ISC"] }, + { name = "inotify-sys", allow = ["ISC"] }, + { name = "matchit", allow = ["BSD-3-Clause"] }, + { name = "minreq", allow = ["ISC"] }, + { name = "notify", allow = ["CC0-1.0"] }, + { name = "option-ext", allow = ["MPL-2.0"] }, + { name = "priority-queue", allow = ["MPL-2.0"] }, + { name = "ring", allow = ["LicenseRef-ring"] }, + { name = "rustls-webpki", allow = ["ISC"] }, + { name = "secp256k1", allow = ["CC0-1.0"] }, + { name = "secp256k1-sys", allow = ["CC0-1.0"] }, + { name = "simple_asn1", allow = ["ISC"] }, + { name = "slotmap", allow = ["Zlib"] }, + { name = "subtle", allow = ["BSD-3-Clause"] }, + { name = "tinystr", allow = ["Unicode-3.0"] }, + { name = "unicode-ident", allow = ["Unicode-DFS-2016"] }, + { name = "untrusted", allow = ["ISC"] }, + { name = "webpki-roots", allow = ["MPL-2.0"] }, + { name = "x25519-dalek", allow = ["BSD-3-Clause"] }, +] + +[[licenses.clarify]] +name = "ring" +expression = "LicenseRef-ring" +license-files = [ + { path = "LICENSE", hash = 0xbd0eed23 }, +] diff --git a/pczt/CHANGELOG.md b/pczt/CHANGELOG.md new file mode 100644 index 0000000000..a5ce72ccf6 --- /dev/null +++ b/pczt/CHANGELOG.md @@ -0,0 +1,36 @@ +# Changelog +All notable changes to this library will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this library adheres to Rust's notion of +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- `pczt::common`: + - `Global::{tx_version, version_group_id, consensus_branch_id, expiry_height}` + - `determine_lock_time` + - `LockTimeInput` trait +- `pczt::orchard`: + - `Bundle::{flags, value_sum, anchor}` + - `Action::cv_net` + - `Spend::rk` + - `Output::{cmx, ephemeral_key, enc_ciphertext, out_ciphertext}` +- `pczt::roles`: + - `low_level_signer` module + - `redactor` module +- `pczt::sapling`: + - `Bundle::{value_sum, anchor}` + - `Spend::{cv, nullifier, rk}` + - `Output::{cv, cmu, ephemeral_key, enc_ciphertext, out_ciphertext}` +- `pczt::transparent`: + - `Input::{sequence, script_pubkey}` + - `Output::{value, script_pubkey}` + +### Changed +- MSRV is now 1.81.0. +- Migrated to `nonempty 0.11`, `secp256k1 0.29`. + +## [0.1.0] - 2024-12-16 +Initial release supporting the PCZT v1 format. diff --git a/pczt/Cargo.toml b/pczt/Cargo.toml new file mode 100644 index 0000000000..d4568d5707 --- /dev/null +++ b/pczt/Cargo.toml @@ -0,0 +1,100 @@ +[package] +name = "pczt" +version = "0.1.0" +authors = ["Jack Grigg "] +edition.workspace = true +rust-version.workspace = true +description = "Tools for working with partially-created Zcash transactions" +homepage = "https://github.com/zcash/librustzcash" +repository.workspace = true +license.workspace = true +categories.workspace = true + +[dependencies] +zcash_note_encryption = { workspace = true, optional = true } +zcash_primitives = { workspace = true, optional = true } +zcash_protocol = { workspace = true, optional = true } + +blake2b_simd = { workspace = true, optional = true } +rand_core = { workspace = true, optional = true } + +# Encoding +postcard = { version = "1", features = ["alloc"] } +serde.workspace = true +serde_with = { version = "3", default-features = false, features = ["alloc", "macros"] } + +# Payment protocols +# - Transparent +secp256k1 = { workspace = true, optional = true } +transparent = { workspace = true, optional = true } + +# - Sapling +bls12_381 = { workspace = true, optional = true } +ff = { workspace = true, optional = true } +jubjub = { workspace = true, optional = true } +redjubjub = { workspace = true, optional = true } +sapling = { workspace = true, optional = true } + +# - Orchard +nonempty = { workspace = true, optional = true } +orchard = { workspace = true, optional = true } +pasta_curves = { workspace = true, optional = true } + +# - Bolierplate +getset = "0.1" + +[dev-dependencies] +incrementalmerkletree.workspace = true +secp256k1 = { workspace = true, features = ["rand"] } +shardtree.workspace = true +zcash_primitives = { workspace = true, features = [ + "test-dependencies", + "transparent-inputs", +] } +zcash_proofs = { workspace = true, features = ["bundled-prover"] } +zip32.workspace = true + +[features] +orchard = [ + "dep:ff", + "dep:nonempty", + "dep:orchard", + "dep:pasta_curves", + "dep:zcash_protocol", +] +sapling = [ + "dep:bls12_381", + "dep:ff", + "dep:jubjub", + "dep:redjubjub", + "dep:sapling", + "dep:zcash_note_encryption", + "dep:zcash_protocol", +] +transparent = ["dep:secp256k1", "dep:transparent", "dep:zcash_protocol"] +zcp-builder = ["dep:zcash_primitives", "dep:zcash_protocol"] +io-finalizer = ["dep:zcash_primitives", "orchard", "sapling", "transparent"] +prover = ["dep:rand_core", "sapling?/temporary-zcashd"] +signer = [ + "dep:blake2b_simd", + "dep:rand_core", + "dep:zcash_primitives", + "orchard", + "sapling", + "transparent", +] +spend-finalizer = ["transparent"] +tx-extractor = [ + "dep:rand_core", + "dep:zcash_primitives", + "orchard", + "sapling", + "transparent", +] + +[[test]] +name = "end_to_end" +required-features = ["io-finalizer", "prover", "signer", "tx-extractor"] + +[lints] +workspace = true diff --git a/pczt/README.md b/pczt/README.md new file mode 100644 index 0000000000..e5a4c7718d --- /dev/null +++ b/pczt/README.md @@ -0,0 +1,21 @@ +# pczt + +TBD + +## License + +Licensed under either of + + * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or + http://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you, as defined in the Apache-2.0 +license, shall be dual licensed as above, without any additional terms or +conditions. + diff --git a/pczt/src/common.rs b/pczt/src/common.rs new file mode 100644 index 0000000000..f6cdb29cb3 --- /dev/null +++ b/pczt/src/common.rs @@ -0,0 +1,344 @@ +use alloc::collections::BTreeMap; +use alloc::string::String; +use alloc::vec::Vec; + +use getset::Getters; +use serde::{Deserialize, Serialize}; + +use crate::roles::combiner::merge_map; + +pub(crate) const FLAG_TRANSPARENT_INPUTS_MODIFIABLE: u8 = 0b0000_0001; +pub(crate) const FLAG_TRANSPARENT_OUTPUTS_MODIFIABLE: u8 = 0b0000_0010; +pub(crate) const FLAG_HAS_SIGHASH_SINGLE: u8 = 0b0000_0100; +pub(crate) const FLAG_SHIELDED_MODIFIABLE: u8 = 0b1000_0000; + +/// Global fields that are relevant to the transaction as a whole. +#[derive(Clone, Debug, Serialize, Deserialize, Getters)] +pub struct Global { + // + // Transaction effecting data. + // + // These are required fields that are part of the final transaction, and are filled in + // by the Creator when initializing the PCZT. + // + #[getset(get = "pub")] + pub(crate) tx_version: u32, + #[getset(get = "pub")] + pub(crate) version_group_id: u32, + + /// The consensus branch ID for the chain in which this transaction will be mined. + /// + /// Non-optional because this commits to the set of consensus rules that will apply to + /// the transaction; differences therein can affect every role. + #[getset(get = "pub")] + pub(crate) consensus_branch_id: u32, + + /// The transaction locktime to use if no inputs specify a required locktime. + /// + /// - This is set by the Creator. + /// - If omitted, the fallback locktime is assumed to be 0. + pub(crate) fallback_lock_time: Option, + + #[getset(get = "pub")] + pub(crate) expiry_height: u32, + + /// The [SLIP 44] coin type, indicating the network for which this transaction is + /// being constructed. + /// + /// This is technically information that could be determined indirectly from the + /// `consensus_branch_id` but is included explicitly to enable easy identification. + /// Note that this field is not included in the transaction and has no consensus + /// effect (`consensus_branch_id` fills that role). + /// + /// - This is set by the Creator. + /// - Roles that encode network-specific information (for example, derivation paths + /// for key identification) should check against this field for correctness. + /// + /// [SLIP 44]: https://github.com/satoshilabs/slips/blob/master/slip-0044.md + pub(crate) coin_type: u32, + + /// A bitfield for various transaction modification flags. + /// + /// - Bit 0 is the Transparent Inputs Modifiable Flag and indicates whether + /// transparent inputs can be modified. + /// - This is set to `true` by the Creator. + /// - This is checked by the Constructor before adding transparent inputs, and may + /// be set to `false` by the Constructor. + /// - This is set to `false` by the IO Finalizer if there are shielded spends or + /// outputs. + /// - This is set to `false` by a Signer that adds a signature that does not use + /// `SIGHASH_ANYONECANPAY` (which includes all shielded signatures). + /// - The Combiner merges this bit towards `false`. + /// - Bit 1 is the Transparent Outputs Modifiable Flag and indicates whether + /// transparent outputs can be modified. + /// - This is set to `true` by the Creator. + /// - This is checked by the Constructor before adding transparent outputs, and may + /// be set to `false` by the Constructor. + /// - This is set to `false` by the IO Finalizer if there are shielded spends or + /// outputs. + /// - This is set to `false` by a Signer that adds a signature that does not use + /// `SIGHASH_NONE` (which includes all shielded signatures). + /// - The Combiner merges this bit towards `false`. + /// - Bit 2 is the Has `SIGHASH_SINGLE` Flag and indicates whether the transaction has + /// a `SIGHASH_SINGLE` transparent signature who's input and output pairing must be + /// preserved. + /// - This is set to `false` by the Creator. + /// - This is updated by a Constructor. + /// - This is set to `true` by a Signer that adds a signature that uses + /// `SIGHASH_SINGLE`. + /// - This essentially indicates that the Constructor must iterate the transparent + /// inputs to determine whether and how to add a transparent input. + /// - The Combiner merges this bit towards `true`. + /// - Bits 3-6 must be 0. + /// - Bit 7 is the Shielded Modifiable Flag and indicates whether shielded spends or + /// outputs can be modified. + /// - This is set to `true` by the Creator. + /// - This is checked by the Constructor before adding shielded spends or outputs, + /// and may be set to `false` by the Constructor. + /// - This is set to `false` by the IO Finalizer if there are shielded spends or + /// outputs. + /// - This is set to `false` by every Signer (as all signatures commit to all + /// shielded spends and outputs). + /// - The Combiner merges this bit towards `false`. + pub(crate) tx_modifiable: u8, + + /// Proprietary fields related to the overall transaction. + #[getset(get = "pub")] + pub(crate) proprietary: BTreeMap>, +} + +impl Global { + /// Returns whether transparent inputs can be added to or removed from the + /// transaction. + pub fn inputs_modifiable(&self) -> bool { + (self.tx_modifiable & FLAG_TRANSPARENT_INPUTS_MODIFIABLE) != 0 + } + + /// Returns whether transparent outputs can be added to or removed from the + /// transaction. + pub fn outputs_modifiable(&self) -> bool { + (self.tx_modifiable & FLAG_TRANSPARENT_OUTPUTS_MODIFIABLE) != 0 + } + + /// Returns whether the transaction has a `SIGHASH_SINGLE` transparent signature who's + /// input and output pairing must be preserved. + pub fn has_sighash_single(&self) -> bool { + (self.tx_modifiable & FLAG_HAS_SIGHASH_SINGLE) != 0 + } + + /// Returns whether shielded spends or outputs can be added to or removed from the + /// transaction. + pub fn shielded_modifiable(&self) -> bool { + (self.tx_modifiable & FLAG_SHIELDED_MODIFIABLE) != 0 + } + + pub(crate) fn merge(mut self, other: Self) -> Option { + let Self { + tx_version, + version_group_id, + consensus_branch_id, + fallback_lock_time, + expiry_height, + coin_type, + tx_modifiable, + proprietary, + } = other; + + if self.tx_version != tx_version + || self.version_group_id != version_group_id + || self.consensus_branch_id != consensus_branch_id + || self.fallback_lock_time != fallback_lock_time + || self.expiry_height != expiry_height + || self.coin_type != coin_type + { + return None; + } + + // `tx_modifiable` is explicitly a bitmap; merge it bit-by-bit. + // - Bit 0 and Bit 1 merge towards `false`. + if (tx_modifiable & FLAG_TRANSPARENT_INPUTS_MODIFIABLE) == 0 { + self.tx_modifiable &= !FLAG_TRANSPARENT_INPUTS_MODIFIABLE; + } + if (tx_modifiable & FLAG_TRANSPARENT_OUTPUTS_MODIFIABLE) == 0 { + self.tx_modifiable &= !FLAG_TRANSPARENT_OUTPUTS_MODIFIABLE; + } + // - Bit 2 merges towards `true`. + if (tx_modifiable & FLAG_HAS_SIGHASH_SINGLE) != 0 { + self.tx_modifiable |= FLAG_HAS_SIGHASH_SINGLE; + } + // - Bits 3-6 must be 0. + if ((self.tx_modifiable & !FLAG_SHIELDED_MODIFIABLE) >> 3) != 0 + || ((tx_modifiable & !FLAG_SHIELDED_MODIFIABLE) >> 3) != 0 + { + return None; + } + // - Bit 7 merges towards `false`. + if (tx_modifiable & FLAG_SHIELDED_MODIFIABLE) == 0 { + self.tx_modifiable &= !FLAG_SHIELDED_MODIFIABLE; + } + + if !merge_map(&mut self.proprietary, proprietary) { + return None; + } + + Some(self) + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub(crate) struct Zip32Derivation { + /// The [ZIP 32 seed fingerprint](https://zips.z.cash/zip-0032#seed-fingerprints). + pub(crate) seed_fingerprint: [u8; 32], + + /// The sequence of indices corresponding to the shielded HD path. + /// + /// Indices can be hardened or non-hardened (i.e. the hardened flag bit may be set). + /// When used with a Sapling or Orchard spend, the derivation path will generally be + /// entirely hardened; when used with a transparent spend, the derivation path will + /// generally include a non-hardened section matching either the [BIP 44] path, or the + /// path at which ephemeral addresses are derived for [ZIP 320] transactions. + /// + /// [BIP 44]: https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki + /// [ZIP 320]: https://zips.z.cash/zip-0320 + pub(crate) derivation_path: Vec, +} + +/// Determines the lock time for the transaction. +/// +/// Implemented following the specification in [BIP 370], with the rationale that this +/// makes integration of PCZTs simpler for codebases that already support PSBTs. +/// +/// [BIP 370]: https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki#determining-lock-time +pub fn determine_lock_time( + global: &crate::common::Global, + inputs: &[L], +) -> Option { + // The nLockTime field of a transaction is determined by inspecting the + // `Global.fallback_lock_time` and each input's `required_time_lock_time` and + // `required_height_lock_time` fields. + + // If one or more inputs have a `required_time_lock_time` or `required_height_lock_time`, + let have_required_lock_time = inputs.iter().any(|input| { + input.required_time_lock_time().is_some() || input.required_height_lock_time().is_some() + }); + // then the field chosen is the one which is supported by all of the inputs. This can + // be determined by looking at all of the inputs which specify a locktime in either of + // those fields, and choosing the field which is present in all of those inputs. + // Inputs not specifying a lock time field can take both types of lock times, as can + // those that specify both. + let time_lock_time_unsupported = inputs + .iter() + .any(|input| input.required_height_lock_time().is_some()); + let height_lock_time_unsupported = inputs + .iter() + .any(|input| input.required_time_lock_time().is_some()); + + // The lock time chosen is then the maximum value of the chosen type of lock time. + match ( + have_required_lock_time, + time_lock_time_unsupported, + height_lock_time_unsupported, + ) { + (true, true, true) => None, + (true, false, true) => Some( + inputs + .iter() + .filter_map(|input| input.required_time_lock_time()) + .max() + .expect("iterator is non-empty because have_required_lock_time is true"), + ), + // If a PSBT has both types of locktimes possible because one or more inputs + // specify both `required_time_lock_time` and `required_height_lock_time`, then a + // locktime determined by looking at the `required_height_lock_time` fields of the + // inputs must be chosen. + (true, _, false) => Some( + inputs + .iter() + .filter_map(|input| input.required_height_lock_time()) + .max() + .expect("iterator is non-empty because have_required_lock_time is true"), + ), + // If none of the inputs have a `required_time_lock_time` and + // `required_height_lock_time`, then `Global.fallback_lock_time` must be used. If + // `Global.fallback_lock_time` is not provided, then it is assumed to be 0. + (false, _, _) => Some(global.fallback_lock_time.unwrap_or(0)), + } +} + +pub trait LockTimeInput { + fn required_time_lock_time(&self) -> Option; + fn required_height_lock_time(&self) -> Option; +} + +impl LockTimeInput for crate::transparent::Input { + fn required_time_lock_time(&self) -> Option { + self.required_time_lock_time + } + + fn required_height_lock_time(&self) -> Option { + self.required_height_lock_time + } +} + +#[cfg(feature = "transparent")] +impl LockTimeInput for ::transparent::pczt::Input { + fn required_time_lock_time(&self) -> Option { + *self.required_time_lock_time() + } + + fn required_height_lock_time(&self) -> Option { + *self.required_height_lock_time() + } +} + +#[cfg(test)] +mod tests { + use alloc::collections::BTreeMap; + + use super::Global; + + #[test] + fn tx_modifiable() { + let base = Global { + tx_version: 0, + version_group_id: 0, + consensus_branch_id: 0, + fallback_lock_time: None, + expiry_height: 0, + coin_type: 0, + tx_modifiable: 0b0000_0000, + proprietary: BTreeMap::new(), + }; + + for (left, right, expected) in [ + (0b0000_0000, 0b0000_0000, Some(0b0000_0000)), + (0b0000_0000, 0b0000_0011, Some(0b0000_0000)), + (0b0000_0001, 0b0000_0011, Some(0b0000_0001)), + (0b0000_0010, 0b0000_0011, Some(0b0000_0010)), + (0b0000_0011, 0b0000_0011, Some(0b0000_0011)), + (0b0000_0000, 0b0000_0100, Some(0b0000_0100)), + (0b0000_0100, 0b0000_0100, Some(0b0000_0100)), + (0b0000_0011, 0b0000_0111, Some(0b0000_0111)), + (0b0000_0000, 0b0000_1000, None), + (0b0000_0000, 0b0001_0000, None), + (0b0000_0000, 0b0010_0000, None), + (0b0000_0000, 0b0100_0000, None), + (0b0000_0000, 0b1000_0000, Some(0b0000_0000)), + (0b1000_0000, 0b1000_0000, Some(0b1000_0000)), + ] { + let mut a = base.clone(); + a.tx_modifiable = left; + + let mut b = base.clone(); + b.tx_modifiable = right; + + assert_eq!( + a.clone() + .merge(b.clone()) + .map(|global| global.tx_modifiable), + expected + ); + assert_eq!(b.merge(a).map(|global| global.tx_modifiable), expected); + } + } +} diff --git a/pczt/src/lib.rs b/pczt/src/lib.rs new file mode 100644 index 0000000000..cbdcba0bff --- /dev/null +++ b/pczt/src/lib.rs @@ -0,0 +1,155 @@ +//! The Partially Created Zcash Transaction (PCZT) format. +//! +//! Goal is to split up the parts of creating a transaction across distinct entities. +//! The entity roles roughly match BIP 174: Partially Signed Bitcoin Transaction Format. +//! - Creator (single entity) +//! - Creates the base PCZT with no information about spends or outputs. +//! - Constructor (anyone can contribute) +//! - Adds spends and outputs to the PCZT. +//! - Before any input or output may be added, the constructor must check the +//! `Global.tx_modifiable` field. Inputs may only be added if the Inputs Modifiable +//! flag is True. Outputs may only be added if the Outputs Modifiable flag is True. +//! - A single entity is likely to be both a Creator and Constructor. +//! - IO Finalizer (anyone can execute) +//! - Sets the appropriate bits in `Global.tx_modifiable` to 0. +//! - Updates the various bsk values using the rcv information from spends and outputs. +//! - Updater (anyone can contribute) +//! - Adds information necessary for subsequent entities to proceed, such as key paths +//! for signing spends. +//! - Redactor (anyone can execute) +//! - Removes information that is unnecessary for subsequent entities to proceed. +//! - This can be useful e.g. when creating a transaction that has inputs from multiple +//! independent Signers; each can receive a PCZT with just the information they need +//! to sign, but (e.g.) not the `alpha` values for other Signers. +//! - Prover (capability holders can contribute) +//! - Needs all private information for a single spend or output. +//! - In practice, the Updater that adds a given spend or output will either act as +//! the Prover themselves, or add the necessary data, offload to the Prover, and +//! then receive back the PCZT with private data stripped and proof added. +//! - Signer (capability holders can contribute) +//! - Needs the spend authorization randomizers to create signatures. +//! - Needs sufficient information to verify that the proof is over the correct data, +//! without needing to verify the proof itself. +//! - A Signer should only need to implement: +//! - Pedersen commitments using Jubjub / Pallas arithmetic (for note and value +//! commitments) +//! - BLAKE2b and BLAKE2s (and the various PRFs / CRHs they are used in) +//! - Nullifier check (using Jubjub / Pallas arithmetic) +//! - KDF plus note decryption (AEAD_CHACHA20_POLY1305) +//! - SignatureHash algorithm +//! - Signatures (RedJubjub / RedPallas) +//! - A source of randomness. +//! - Combiner (anyone can execute) +//! - Combines several PCZTs that represent the same transaction into a single PCZT. +//! - Spend Finalizer (anyone can execute) +//! - Combines partial transparent signatures into `script_sig`s. +//! - Transaction Extractor (anyone can execute) +//! - Creates bindingSig and extracts the final transaction. + +#![no_std] + +#[macro_use] +extern crate alloc; + +use alloc::vec::Vec; + +use getset::Getters; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "signer")] +use {roles::signer::EffectsOnly, zcash_primitives::transaction::TransactionData}; + +pub mod roles; + +pub mod common; +pub mod orchard; +pub mod sapling; +pub mod transparent; + +const MAGIC_BYTES: &[u8] = b"PCZT"; +const PCZT_VERSION_1: u32 = 1; + +#[cfg(feature = "zcp-builder")] +const SAPLING_TX_VERSION: u32 = 4; +const V5_TX_VERSION: u32 = 5; +const V5_VERSION_GROUP_ID: u32 = 0x26A7270A; + +/// A partially-created Zcash transaction. +#[derive(Clone, Debug, Serialize, Deserialize, Getters)] +pub struct Pczt { + /// Global fields that are relevant to the transaction as a whole. + #[getset(get = "pub")] + global: common::Global, + + // + // Protocol-specific fields. + // + // Unlike the `TransactionData` type in `zcash_primitives`, these are not optional. + // This is because a PCZT does not always contain a semantically-valid transaction, + // and there may be phases where we need to store protocol-specific metadata before + // it has been determined whether there are protocol-specific inputs or outputs. + // + #[getset(get = "pub")] + transparent: transparent::Bundle, + #[getset(get = "pub")] + sapling: sapling::Bundle, + #[getset(get = "pub")] + orchard: orchard::Bundle, +} + +impl Pczt { + /// Parses a PCZT from its encoding. + pub fn parse(bytes: &[u8]) -> Result { + if bytes.len() < 8 { + return Err(ParseError::TooShort); + } + if &bytes[..4] != MAGIC_BYTES { + return Err(ParseError::NotPczt); + } + let version = u32::from_le_bytes(bytes[4..8].try_into().unwrap()); + if version != PCZT_VERSION_1 { + return Err(ParseError::UnknownVersion(version)); + } + + // This is a v1 PCZT. + postcard::from_bytes(&bytes[8..]).map_err(ParseError::Invalid) + } + + /// Serializes this PCZT. + pub fn serialize(&self) -> Vec { + let mut bytes = vec![]; + bytes.extend_from_slice(MAGIC_BYTES); + bytes.extend_from_slice(&PCZT_VERSION_1.to_le_bytes()); + postcard::to_extend(self, bytes).expect("can serialize into memory") + } + + /// Gets the effects of this transaction. + #[cfg(feature = "signer")] + pub fn into_effects(self) -> Option> { + let Self { + global, + transparent, + sapling, + orchard, + } = self; + + let transparent = transparent.into_parsed().ok()?; + let sapling = sapling.into_parsed().ok()?; + let orchard = orchard.into_parsed().ok()?; + + roles::signer::pczt_to_tx_data(&global, &transparent, &sapling, &orchard).ok() + } +} + +/// Errors that can occur while parsing a PCZT. +#[derive(Debug)] +pub enum ParseError { + /// The bytes do not contain a PCZT. + NotPczt, + /// The PCZT encoding was invalid. + Invalid(postcard::Error), + /// The bytes are too short to contain a PCZT. + TooShort, + /// The PCZT has an unknown version. + UnknownVersion(u32), +} diff --git a/pczt/src/orchard.rs b/pczt/src/orchard.rs new file mode 100644 index 0000000000..59abdec1aa --- /dev/null +++ b/pczt/src/orchard.rs @@ -0,0 +1,572 @@ +use alloc::collections::BTreeMap; +use alloc::string::String; +use alloc::vec::Vec; +use core::cmp::Ordering; + +#[cfg(feature = "orchard")] +use ff::PrimeField; +use getset::Getters; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +use crate::{ + common::{Global, Zip32Derivation}, + roles::combiner::{merge_map, merge_optional}, +}; + +/// PCZT fields that are specific to producing the transaction's Orchard bundle (if any). +#[derive(Clone, Debug, Serialize, Deserialize, Getters)] +pub struct Bundle { + /// The Orchard actions in this bundle. + /// + /// Entries are added by the Constructor, and modified by an Updater, IO Finalizer, + /// Signer, Combiner, or Spend Finalizer. + #[getset(get = "pub")] + pub(crate) actions: Vec, + + /// The flags for the Orchard bundle. + /// + /// Contains: + /// - `enableSpendsOrchard` flag (bit 0) + /// - `enableOutputsOrchard` flag (bit 1) + /// - Reserved, zeros (bits 2..=7) + /// + /// This is set by the Creator. The Constructor MUST only add spends and outputs that + /// are consistent with these flags (i.e. are dummies as appropriate). + #[getset(get = "pub")] + pub(crate) flags: u8, + + /// The net value of Orchard spends minus outputs. + /// + /// This is initialized by the Creator, and updated by the Constructor as spends or + /// outputs are added to the PCZT. It enables per-spend and per-output values to be + /// redacted from the PCZT after they are no longer necessary. + #[getset(get = "pub")] + pub(crate) value_sum: (u64, bool), + + /// The Orchard anchor for this transaction. + /// + /// Set by the Creator. + #[getset(get = "pub")] + pub(crate) anchor: [u8; 32], + + /// The Orchard bundle proof. + /// + /// This is `None` until it is set by the Prover. + pub(crate) zkproof: Option>, + + /// The Orchard binding signature signing key. + /// + /// - This is `None` until it is set by the IO Finalizer. + /// - The Transaction Extractor uses this to produce the binding signature. + pub(crate) bsk: Option<[u8; 32]>, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Getters)] +pub struct Action { + // + // Action effecting data. + // + // These are required fields that are part of the final transaction, and are filled in + // by the Constructor when adding an output. + // + #[getset(get = "pub")] + pub(crate) cv_net: [u8; 32], + #[getset(get = "pub")] + pub(crate) spend: Spend, + #[getset(get = "pub")] + pub(crate) output: Output, + + /// The value commitment randomness. + /// + /// - This is set by the Constructor. + /// - The IO Finalizer compresses it into the bsk. + /// - This is required by the Prover. + /// - This may be used by Signers to verify that the value correctly matches `cv`. + /// + /// This opens `cv` for all participants. For Signers who don't need this information, + /// or after proofs / signatures have been applied, this can be redacted. + pub(crate) rcv: Option<[u8; 32]>, +} + +/// Information about a Sapling spend within a transaction. +#[serde_as] +#[derive(Clone, Debug, Serialize, Deserialize, Getters)] +pub struct Spend { + // + // Spend-specific Action effecting data. + // + // These are required fields that are part of the final transaction, and are filled in + // by the Constructor when adding a spend. + // + #[getset(get = "pub")] + pub(crate) nullifier: [u8; 32], + #[getset(get = "pub")] + pub(crate) rk: [u8; 32], + + /// The spend authorization signature. + /// + /// This is set by the Signer. + #[serde_as(as = "Option<[_; 64]>")] + pub(crate) spend_auth_sig: Option<[u8; 64]>, + + /// The [raw encoding] of the Orchard payment address that received the note being spent. + /// + /// - This is set by the Constructor. + /// - This is required by the Prover. + /// + /// [raw encoding]: https://zips.z.cash/protocol/protocol.pdf#orchardpaymentaddrencoding + #[serde_as(as = "Option<[_; 43]>")] + pub(crate) recipient: Option<[u8; 43]>, + + /// The value of the input being spent. + /// + /// - This is required by the Prover. + /// - This may be used by Signers to verify that the value matches `cv`, and to + /// confirm the values and change involved in the transaction. + /// + /// This exposes the input value to all participants. For Signers who don't need this + /// information, or after signatures have been applied, this can be redacted. + pub(crate) value: Option, + + /// The rho value for the note being spent. + /// + /// - This is set by the Constructor. + /// - This is required by the Prover. + pub(crate) rho: Option<[u8; 32]>, + + /// The seed randomness for the note being spent. + /// + /// - This is set by the Constructor. + /// - This is required by the Prover. + pub(crate) rseed: Option<[u8; 32]>, + + /// The full viewing key that received the note being spent. + /// + /// - This is set by the Updater. + /// - This is required by the Prover. + #[serde_as(as = "Option<[_; 96]>")] + pub(crate) fvk: Option<[u8; 96]>, + + /// A witness from the note to the bundle's anchor. + /// + /// - This is set by the Updater. + /// - This is required by the Prover. + pub(crate) witness: Option<(u32, [[u8; 32]; 32])>, + + /// The spend authorization randomizer. + /// + /// - This is chosen by the Constructor. + /// - This is required by the Signer for creating `spend_auth_sig`, and may be used to + /// validate `rk`. + /// - After `zkproof` / `spend_auth_sig` has been set, this can be redacted. + pub(crate) alpha: Option<[u8; 32]>, + + /// The ZIP 32 derivation path at which the spending key can be found for the note + /// being spent. + pub(crate) zip32_derivation: Option, + + /// The spending key for this spent note, if it is a dummy note. + /// + /// - This is chosen by the Constructor. + /// - This is required by the IO Finalizer, and is cleared by it once used. + /// - Signers MUST reject PCZTs that contain `dummy_sk` values. + pub(crate) dummy_sk: Option<[u8; 32]>, + + /// Proprietary fields related to the note being spent. + #[getset(get = "pub")] + pub(crate) proprietary: BTreeMap>, +} + +/// Information about an Orchard output within a transaction. +#[serde_as] +#[derive(Clone, Debug, Serialize, Deserialize, Getters)] +pub struct Output { + // + // Output-specific Action effecting data. + // + // These are required fields that are part of the final transaction, and are filled in + // by the Constructor when adding an output. + // + #[getset(get = "pub")] + pub(crate) cmx: [u8; 32], + #[getset(get = "pub")] + pub(crate) ephemeral_key: [u8; 32], + /// The encrypted note plaintext for the output. + /// + /// Encoded as a `Vec` because its length depends on the transaction version. + /// + /// Once we have memo bundles, we will be able to set memos independently of Outputs. + /// For now, the Constructor sets both at the same time. + #[getset(get = "pub")] + pub(crate) enc_ciphertext: Vec, + /// The encrypted note plaintext for the output. + /// + /// Encoded as a `Vec` because its length depends on the transaction version. + #[getset(get = "pub")] + pub(crate) out_ciphertext: Vec, + + /// The [raw encoding] of the Orchard payment address that will receive the output. + /// + /// - This is set by the Constructor. + /// - This is required by the Prover. + /// + /// [raw encoding]: https://zips.z.cash/protocol/protocol.pdf#orchardpaymentaddrencoding + #[serde_as(as = "Option<[_; 43]>")] + #[getset(get = "pub")] + pub(crate) recipient: Option<[u8; 43]>, + + /// The value of the output. + /// + /// This may be used by Signers to verify that the value matches `cv`, and to confirm + /// the values and change involved in the transaction. + /// + /// This exposes the value to all participants. For Signers who don't need this + /// information, we can drop the values and compress the rcvs into the bsk global. + #[getset(get = "pub")] + pub(crate) value: Option, + + /// The seed randomness for the output. + /// + /// - This is set by the Constructor. + /// - This is required by the Prover, instead of disclosing `shared_secret` to them. + #[getset(get = "pub")] + pub(crate) rseed: Option<[u8; 32]>, + + /// The `ock` value used to encrypt `out_ciphertext`. + /// + /// This enables Signers to verify that `out_ciphertext` is correctly encrypted. + /// + /// This may be `None` if the Constructor added the output using an OVK policy of + /// "None", to make the output unrecoverable from the chain by the sender. + pub(crate) ock: Option<[u8; 32]>, + + /// The ZIP 32 derivation path at which the spending key can be found for the output. + pub(crate) zip32_derivation: Option, + + /// The user-facing address to which this output is being sent, if any. + /// + /// - This is set by an Updater. + /// - Signers must parse this address (if present) and confirm that it contains + /// `recipient` (either directly, or e.g. as a receiver within a Unified Address). + #[getset(get = "pub")] + pub(crate) user_address: Option, + + /// Proprietary fields related to the note being created. + #[getset(get = "pub")] + pub(crate) proprietary: BTreeMap>, +} + +impl Bundle { + /// Merges this bundle with another. + /// + /// Returns `None` if the bundles have conflicting data. + pub(crate) fn merge( + mut self, + other: Self, + self_global: &Global, + other_global: &Global, + ) -> Option { + // Destructure `other` to ensure we handle everything. + let Self { + mut actions, + flags, + value_sum, + anchor, + zkproof, + bsk, + } = other; + + if self.flags != flags { + return None; + } + + // If `bsk` is set on either bundle, the IO Finalizer has run, which means we + // cannot have differing numbers of actions, and the value sums must match. + match (self.bsk.as_mut(), bsk) { + (Some(lhs), Some(rhs)) if lhs != &rhs => return None, + (Some(_), _) | (_, Some(_)) + if self.actions.len() != actions.len() || self.value_sum != value_sum => + { + return None + } + // IO Finalizer has run, and neither bundle has excess spends or outputs. + (Some(_), _) | (_, Some(_)) => (), + // IO Finalizer has not run on either bundle. + (None, None) => match ( + self_global.shielded_modifiable(), + other_global.shielded_modifiable(), + self.actions.len().cmp(&actions.len()), + ) { + // Fail if the merge would add actions to a non-modifiable bundle. + (false, _, Ordering::Less) | (_, false, Ordering::Greater) => return None, + // If the other bundle has more actions than us, move them over; these + // cannot conflict by construction. + (true, _, Ordering::Less) => { + self.actions.extend(actions.drain(self.actions.len()..)); + + // We check below that the overlapping actions match. Assuming here + // that they will, we can take the other bundle's value sum. + self.value_sum = value_sum; + } + // Do nothing otherwise. + (_, _, Ordering::Equal) | (_, true, Ordering::Greater) => (), + }, + } + + if self.anchor != anchor { + return None; + } + + if !merge_optional(&mut self.zkproof, zkproof) { + return None; + } + + // Leverage the early-exit behaviour of zip to confirm that the remaining data in + // the other bundle matches this one. + for (lhs, rhs) in self.actions.iter_mut().zip(actions.into_iter()) { + // Destructure `rhs` to ensure we handle everything. + let Action { + cv_net, + spend: + Spend { + nullifier, + rk, + spend_auth_sig, + recipient, + value, + rho, + rseed, + fvk, + witness, + alpha, + zip32_derivation: spend_zip32_derivation, + dummy_sk, + proprietary: spend_proprietary, + }, + output: + Output { + cmx, + ephemeral_key, + enc_ciphertext, + out_ciphertext, + recipient: output_recipient, + value: output_value, + rseed: output_rseed, + ock, + zip32_derivation: output_zip32_derivation, + user_address, + proprietary: output_proprietary, + }, + rcv, + } = rhs; + + if lhs.cv_net != cv_net + || lhs.spend.nullifier != nullifier + || lhs.spend.rk != rk + || lhs.output.cmx != cmx + || lhs.output.ephemeral_key != ephemeral_key + || lhs.output.enc_ciphertext != enc_ciphertext + || lhs.output.out_ciphertext != out_ciphertext + { + return None; + } + + if !(merge_optional(&mut lhs.spend.spend_auth_sig, spend_auth_sig) + && merge_optional(&mut lhs.spend.recipient, recipient) + && merge_optional(&mut lhs.spend.value, value) + && merge_optional(&mut lhs.spend.rho, rho) + && merge_optional(&mut lhs.spend.rseed, rseed) + && merge_optional(&mut lhs.spend.fvk, fvk) + && merge_optional(&mut lhs.spend.witness, witness) + && merge_optional(&mut lhs.spend.alpha, alpha) + && merge_optional(&mut lhs.spend.zip32_derivation, spend_zip32_derivation) + && merge_optional(&mut lhs.spend.dummy_sk, dummy_sk) + && merge_map(&mut lhs.spend.proprietary, spend_proprietary) + && merge_optional(&mut lhs.output.recipient, output_recipient) + && merge_optional(&mut lhs.output.value, output_value) + && merge_optional(&mut lhs.output.rseed, output_rseed) + && merge_optional(&mut lhs.output.ock, ock) + && merge_optional(&mut lhs.output.zip32_derivation, output_zip32_derivation) + && merge_optional(&mut lhs.output.user_address, user_address) + && merge_map(&mut lhs.output.proprietary, output_proprietary) + && merge_optional(&mut lhs.rcv, rcv)) + { + return None; + } + } + + Some(self) + } +} + +#[cfg(feature = "orchard")] +impl Bundle { + pub(crate) fn into_parsed(self) -> Result { + let actions = self + .actions + .into_iter() + .map(|action| { + let spend = orchard::pczt::Spend::parse( + action.spend.nullifier, + action.spend.rk, + action.spend.spend_auth_sig, + action.spend.recipient, + action.spend.value, + action.spend.rho, + action.spend.rseed, + action.spend.fvk, + action.spend.witness, + action.spend.alpha, + action + .spend + .zip32_derivation + .map(|z| { + orchard::pczt::Zip32Derivation::parse( + z.seed_fingerprint, + z.derivation_path, + ) + }) + .transpose()?, + action.spend.dummy_sk, + action.spend.proprietary, + )?; + + let output = orchard::pczt::Output::parse( + *spend.nullifier(), + action.output.cmx, + action.output.ephemeral_key, + action.output.enc_ciphertext, + action.output.out_ciphertext, + action.output.recipient, + action.output.value, + action.output.rseed, + action.output.ock, + action + .output + .zip32_derivation + .map(|z| { + orchard::pczt::Zip32Derivation::parse( + z.seed_fingerprint, + z.derivation_path, + ) + }) + .transpose()?, + action.output.user_address, + action.output.proprietary, + )?; + + orchard::pczt::Action::parse(action.cv_net, spend, output, action.rcv) + }) + .collect::>()?; + + orchard::pczt::Bundle::parse( + actions, + self.flags, + self.value_sum, + self.anchor, + self.zkproof, + self.bsk, + ) + } + + pub(crate) fn serialize_from(bundle: orchard::pczt::Bundle) -> Self { + let actions = bundle + .actions() + .iter() + .map(|action| { + let spend = action.spend(); + let output = action.output(); + + Action { + cv_net: action.cv_net().to_bytes(), + spend: Spend { + nullifier: spend.nullifier().to_bytes(), + rk: spend.rk().into(), + spend_auth_sig: spend.spend_auth_sig().as_ref().map(|s| s.into()), + recipient: action + .spend() + .recipient() + .map(|recipient| recipient.to_raw_address_bytes()), + value: spend.value().map(|value| value.inner()), + rho: spend.rho().map(|rho| rho.to_bytes()), + rseed: spend.rseed().map(|rseed| *rseed.as_bytes()), + fvk: spend.fvk().as_ref().map(|fvk| fvk.to_bytes()), + witness: spend.witness().as_ref().map(|witness| { + ( + u32::try_from(u64::from(witness.position())) + .expect("Sapling positions fit in u32"), + witness + .auth_path() + .iter() + .map(|node| node.to_bytes()) + .collect::>()[..] + .try_into() + .expect("path is length 32"), + ) + }), + alpha: spend.alpha().map(|alpha| alpha.to_repr()), + zip32_derivation: spend.zip32_derivation().as_ref().map(|z| { + Zip32Derivation { + seed_fingerprint: *z.seed_fingerprint(), + derivation_path: z + .derivation_path() + .iter() + .map(|i| i.index()) + .collect(), + } + }), + dummy_sk: action + .spend() + .dummy_sk() + .map(|dummy_sk| *dummy_sk.to_bytes()), + proprietary: spend.proprietary().clone(), + }, + output: Output { + cmx: output.cmx().to_bytes(), + ephemeral_key: output.encrypted_note().epk_bytes, + enc_ciphertext: output.encrypted_note().enc_ciphertext.to_vec(), + out_ciphertext: output.encrypted_note().out_ciphertext.to_vec(), + recipient: action + .output() + .recipient() + .map(|recipient| recipient.to_raw_address_bytes()), + value: output.value().map(|value| value.inner()), + rseed: output.rseed().map(|rseed| *rseed.as_bytes()), + ock: output.ock().as_ref().map(|ock| ock.0), + zip32_derivation: output.zip32_derivation().as_ref().map(|z| { + Zip32Derivation { + seed_fingerprint: *z.seed_fingerprint(), + derivation_path: z + .derivation_path() + .iter() + .map(|i| i.index()) + .collect(), + } + }), + user_address: output.user_address().clone(), + proprietary: output.proprietary().clone(), + }, + rcv: action.rcv().as_ref().map(|rcv| rcv.to_bytes()), + } + }) + .collect(); + + let value_sum = { + let (magnitude, sign) = bundle.value_sum().magnitude_sign(); + (magnitude, matches!(sign, orchard::value::Sign::Negative)) + }; + + Self { + actions, + flags: bundle.flags().to_byte(), + value_sum, + anchor: bundle.anchor().to_bytes(), + zkproof: bundle + .zkproof() + .as_ref() + .map(|zkproof| zkproof.as_ref().to_vec()), + bsk: bundle.bsk().as_ref().map(|bsk| bsk.into()), + } + } +} diff --git a/pczt/src/roles.rs b/pczt/src/roles.rs new file mode 100644 index 0000000000..687da1a265 --- /dev/null +++ b/pczt/src/roles.rs @@ -0,0 +1,70 @@ +pub mod creator; + +#[cfg(feature = "io-finalizer")] +pub mod io_finalizer; + +pub mod verifier; + +pub mod updater; + +pub mod redactor; + +#[cfg(feature = "prover")] +pub mod prover; + +#[cfg(feature = "signer")] +pub mod signer; + +pub mod low_level_signer; + +pub mod combiner; + +#[cfg(feature = "spend-finalizer")] +pub mod spend_finalizer; + +#[cfg(feature = "tx-extractor")] +pub mod tx_extractor; + +#[cfg(test)] +mod tests { + #[cfg(feature = "tx-extractor")] + #[test] + fn extract_fails_on_empty() { + use zcash_protocol::consensus::BranchId; + + use crate::roles::{ + creator::Creator, + tx_extractor::{self, TransactionExtractor}, + }; + + let pczt = Creator::new(BranchId::Nu6.into(), 10_000_000, 133, [0; 32], [0; 32]).build(); + + // Extraction fails because we haven't run the IO Finalizer. + // Extraction fails in Sapling because we happen to extract it before Orchard. + assert!(matches!( + TransactionExtractor::new(pczt).extract().unwrap_err(), + tx_extractor::Error::Sapling(tx_extractor::SaplingError::Extract( + sapling::pczt::TxExtractorError::MissingBindingSignatureSigningKey + )), + )); + } + + #[cfg(feature = "io-finalizer")] + #[test] + fn io_finalizer_fails_on_empty() { + use zcash_protocol::consensus::BranchId; + + use crate::roles::{ + creator::Creator, + io_finalizer::{self, IoFinalizer}, + }; + + let pczt = Creator::new(BranchId::Nu6.into(), 10_000_000, 133, [0; 32], [0; 32]).build(); + + // IO finalization fails on spends because we happen to check them first. + assert!(matches!( + IoFinalizer::new(pczt).finalize_io().unwrap_err(), + io_finalizer::Error::NoSpends, + )); + } +} diff --git a/pczt/src/roles/combiner/mod.rs b/pczt/src/roles/combiner/mod.rs new file mode 100644 index 0000000000..3226019b38 --- /dev/null +++ b/pczt/src/roles/combiner/mod.rs @@ -0,0 +1,103 @@ +use alloc::collections::BTreeMap; +use alloc::vec::Vec; + +use crate::Pczt; + +pub struct Combiner { + pczts: Vec, +} + +impl Combiner { + /// Instantiates the Combiner role with the given PCZTs. + pub fn new(pczts: Vec) -> Self { + Self { pczts } + } + + /// Combines the PCZTs. + pub fn combine(self) -> Result { + self.pczts + .into_iter() + .try_fold(None, |acc, pczt| match acc { + None => Ok(Some(pczt)), + Some(acc) => merge(acc, pczt).map(Some), + }) + .transpose() + .unwrap_or(Err(Error::NoPczts)) + } +} + +fn merge(lhs: Pczt, rhs: Pczt) -> Result { + // Per-protocol bundles are merged first, because each is only interpretable in the + // context of its own global. + let transparent = lhs + .transparent + .merge(rhs.transparent, &lhs.global, &rhs.global) + .ok_or(Error::DataMismatch)?; + let sapling = lhs + .sapling + .merge(rhs.sapling, &lhs.global, &rhs.global) + .ok_or(Error::DataMismatch)?; + let orchard = lhs + .orchard + .merge(rhs.orchard, &lhs.global, &rhs.global) + .ok_or(Error::DataMismatch)?; + + // Now that the per-protocol bundles are merged, merge the globals. + let global = lhs.global.merge(rhs.global).ok_or(Error::DataMismatch)?; + + Ok(Pczt { + global, + transparent, + sapling, + orchard, + }) +} + +/// Merges two values for an optional field together. +/// +/// Returns `false` if the values cannot be merged. +pub(crate) fn merge_optional(lhs: &mut Option, rhs: Option) -> bool { + match (&lhs, rhs) { + // If the RHS is not present, keep the LHS. + (_, None) => (), + // If the LHS is not present, set it to the RHS. + (None, Some(rhs)) => *lhs = Some(rhs), + // If both are present and are equal, nothing to do. + (Some(lhs), Some(rhs)) if lhs == &rhs => (), + // If both are present and are not equal, fail. Here we differ from BIP 174. + (Some(_), Some(_)) => return false, + } + + // Success! + true +} + +/// Merges two maps together. +/// +/// Returns `false` if the values cannot be merged. +pub(crate) fn merge_map( + lhs: &mut BTreeMap, + rhs: BTreeMap, +) -> bool { + for (key, rhs_value) in rhs.into_iter() { + if let Some(lhs_value) = lhs.get_mut(&key) { + // If the key is present in both maps, and their values are not equal, fail. + // Here we differ from BIP 174. + if lhs_value != &rhs_value { + return false; + } + } else { + lhs.insert(key, rhs_value); + } + } + + // Success! + true +} + +/// Errors that can occur while combining PCZTs. +#[derive(Debug)] +pub enum Error { + NoPczts, + DataMismatch, +} diff --git a/pczt/src/roles/creator/mod.rs b/pczt/src/roles/creator/mod.rs new file mode 100644 index 0000000000..8b310e571b --- /dev/null +++ b/pczt/src/roles/creator/mod.rs @@ -0,0 +1,172 @@ +use alloc::collections::BTreeMap; + +use crate::{ + common::{ + FLAG_SHIELDED_MODIFIABLE, FLAG_TRANSPARENT_INPUTS_MODIFIABLE, + FLAG_TRANSPARENT_OUTPUTS_MODIFIABLE, + }, + Pczt, V5_TX_VERSION, V5_VERSION_GROUP_ID, +}; + +/// Initial flags allowing any modification. +const INITIAL_TX_MODIFIABLE: u8 = FLAG_TRANSPARENT_INPUTS_MODIFIABLE + | FLAG_TRANSPARENT_OUTPUTS_MODIFIABLE + | FLAG_SHIELDED_MODIFIABLE; + +const ORCHARD_SPENDS_AND_OUTPUTS_ENABLED: u8 = 0b0000_0011; + +pub struct Creator { + tx_version: u32, + version_group_id: u32, + consensus_branch_id: u32, + fallback_lock_time: Option, + expiry_height: u32, + coin_type: u32, + orchard_flags: u8, + sapling_anchor: [u8; 32], + orchard_anchor: [u8; 32], +} + +impl Creator { + pub fn new( + consensus_branch_id: u32, + expiry_height: u32, + coin_type: u32, + sapling_anchor: [u8; 32], + orchard_anchor: [u8; 32], + ) -> Self { + Self { + // Default to v5 transaction format. + tx_version: V5_TX_VERSION, + version_group_id: V5_VERSION_GROUP_ID, + consensus_branch_id, + fallback_lock_time: None, + expiry_height, + coin_type, + orchard_flags: ORCHARD_SPENDS_AND_OUTPUTS_ENABLED, + sapling_anchor, + orchard_anchor, + } + } + + pub fn with_fallback_lock_time(mut self, fallback: u32) -> Self { + self.fallback_lock_time = Some(fallback); + self + } + + #[cfg(feature = "orchard")] + pub fn with_orchard_flags(mut self, orchard_flags: orchard::bundle::Flags) -> Self { + self.orchard_flags = orchard_flags.to_byte(); + self + } + + pub fn build(self) -> Pczt { + Pczt { + global: crate::common::Global { + tx_version: self.tx_version, + version_group_id: self.version_group_id, + consensus_branch_id: self.consensus_branch_id, + fallback_lock_time: self.fallback_lock_time, + expiry_height: self.expiry_height, + coin_type: self.coin_type, + tx_modifiable: INITIAL_TX_MODIFIABLE, + proprietary: BTreeMap::new(), + }, + transparent: crate::transparent::Bundle { + inputs: vec![], + outputs: vec![], + }, + sapling: crate::sapling::Bundle { + spends: vec![], + outputs: vec![], + value_sum: 0, + anchor: self.sapling_anchor, + bsk: None, + }, + orchard: crate::orchard::Bundle { + actions: vec![], + flags: self.orchard_flags, + value_sum: (0, true), + anchor: self.orchard_anchor, + zkproof: None, + bsk: None, + }, + } + } + + /// Builds a PCZT from the output of a [`Builder`]. + /// + /// Returns `None` if the `TxVersion` is incompatible with PCZTs. + /// + /// [`Builder`]: zcash_primitives::transaction::builder::Builder + #[cfg(feature = "zcp-builder")] + pub fn build_from_parts( + parts: zcash_primitives::transaction::builder::PcztParts

, + ) -> Option { + use ::transparent::sighash::{SIGHASH_ANYONECANPAY, SIGHASH_SINGLE}; + use zcash_protocol::consensus::NetworkConstants; + + use crate::{common::FLAG_HAS_SIGHASH_SINGLE, SAPLING_TX_VERSION}; + + let tx_version = match parts.version { + zcash_primitives::transaction::TxVersion::Sprout(_) + | zcash_primitives::transaction::TxVersion::Overwinter => None, + zcash_primitives::transaction::TxVersion::Sapling => Some(SAPLING_TX_VERSION), + zcash_primitives::transaction::TxVersion::Zip225 => Some(V5_TX_VERSION), + #[cfg(zcash_unstable = "zfuture")] + zcash_primitives::transaction::TxVersion::ZFuture => None, + }?; + + // Spends and outputs not modifiable. + let mut tx_modifiable = 0b0000_0000; + // Check if any input is using `SIGHASH_SINGLE` (with or without `ANYONECANPAY`). + if parts.transparent.as_ref().is_some_and(|bundle| { + bundle.inputs().iter().any(|input| { + (input.sighash_type().encode() & !SIGHASH_ANYONECANPAY) == SIGHASH_SINGLE + }) + }) { + tx_modifiable |= FLAG_HAS_SIGHASH_SINGLE; + } + + Some(Pczt { + global: crate::common::Global { + tx_version, + version_group_id: parts.version.version_group_id(), + consensus_branch_id: parts.consensus_branch_id.into(), + fallback_lock_time: Some(parts.lock_time), + expiry_height: parts.expiry_height.into(), + coin_type: parts.params.network_type().coin_type(), + tx_modifiable, + proprietary: BTreeMap::new(), + }, + transparent: parts + .transparent + .map(crate::transparent::Bundle::serialize_from) + .unwrap_or_else(|| crate::transparent::Bundle { + inputs: vec![], + outputs: vec![], + }), + sapling: parts + .sapling + .map(crate::sapling::Bundle::serialize_from) + .unwrap_or_else(|| crate::sapling::Bundle { + spends: vec![], + outputs: vec![], + value_sum: 0, + anchor: sapling::Anchor::empty_tree().to_bytes(), + bsk: None, + }), + orchard: parts + .orchard + .map(crate::orchard::Bundle::serialize_from) + .unwrap_or_else(|| crate::orchard::Bundle { + actions: vec![], + flags: ORCHARD_SPENDS_AND_OUTPUTS_ENABLED, + value_sum: (0, true), + anchor: orchard::Anchor::empty_tree().to_bytes(), + zkproof: None, + bsk: None, + }), + }) + } +} diff --git a/pczt/src/roles/io_finalizer/mod.rs b/pczt/src/roles/io_finalizer/mod.rs new file mode 100644 index 0000000000..89963a01bf --- /dev/null +++ b/pczt/src/roles/io_finalizer/mod.rs @@ -0,0 +1,120 @@ +use rand_core::OsRng; +use zcash_primitives::transaction::{ + sighash::SignableInput, sighash_v5::v5_signature_hash, txid::TxIdDigester, +}; + +use crate::{ + common::{ + FLAG_SHIELDED_MODIFIABLE, FLAG_TRANSPARENT_INPUTS_MODIFIABLE, + FLAG_TRANSPARENT_OUTPUTS_MODIFIABLE, + }, + Pczt, V5_TX_VERSION, V5_VERSION_GROUP_ID, +}; + +use super::signer::pczt_to_tx_data; + +pub struct IoFinalizer { + pczt: Pczt, +} + +impl IoFinalizer { + /// Instantiates the IO Finalizer role with the given PCZT. + pub fn new(pczt: Pczt) -> Self { + Self { pczt } + } + + /// Finalizes the IO of the PCZT. + pub fn finalize_io(self) -> Result { + let Self { pczt } = self; + + let has_shielded_spends = + !(pczt.sapling.spends.is_empty() && pczt.orchard.actions.is_empty()); + let has_shielded_outputs = + !(pczt.sapling.outputs.is_empty() && pczt.orchard.actions.is_empty()); + + // We can't build a transaction that has no spends or outputs. + // However, we don't attempt to reject an entirely dummy transaction. + if pczt.transparent.inputs.is_empty() && !has_shielded_spends { + return Err(Error::NoSpends); + } + if pczt.transparent.outputs.is_empty() && !has_shielded_outputs { + return Err(Error::NoOutputs); + } + + let Pczt { + mut global, + transparent, + sapling, + orchard, + } = pczt; + + // After shielded IO finalization, the transaction effects cannot be modified + // because dummy spends will have been signed. + if has_shielded_spends || has_shielded_outputs { + global.tx_modifiable &= !(FLAG_TRANSPARENT_INPUTS_MODIFIABLE + | FLAG_TRANSPARENT_OUTPUTS_MODIFIABLE + | FLAG_SHIELDED_MODIFIABLE); + } + + let transparent = transparent.into_parsed().map_err(Error::TransparentParse)?; + let mut sapling = sapling.into_parsed().map_err(Error::SaplingParse)?; + let mut orchard = orchard.into_parsed().map_err(Error::OrchardParse)?; + + let tx_data = pczt_to_tx_data(&global, &transparent, &sapling, &orchard)?; + let txid_parts = tx_data.digest(TxIdDigester); + + // TODO: Pick sighash based on tx version. + match (global.tx_version, global.version_group_id) { + (V5_TX_VERSION, V5_VERSION_GROUP_ID) => Ok(()), + (version, version_group_id) => Err(Error::UnsupportedTxVersion { + version, + version_group_id, + }), + }?; + let shielded_sighash = v5_signature_hash(&tx_data, &SignableInput::Shielded, &txid_parts) + .as_ref() + .try_into() + .expect("correct length"); + + sapling + .finalize_io(shielded_sighash, OsRng) + .map_err(Error::SaplingFinalize)?; + orchard + .finalize_io(shielded_sighash, OsRng) + .map_err(Error::OrchardFinalize)?; + + Ok(Pczt { + global, + transparent: crate::transparent::Bundle::serialize_from(transparent), + sapling: crate::sapling::Bundle::serialize_from(sapling), + orchard: crate::orchard::Bundle::serialize_from(orchard), + }) + } +} + +/// Errors that can occur while finalizing the IO of a PCZT. +#[derive(Debug)] +pub enum Error { + NoOutputs, + NoSpends, + OrchardFinalize(orchard::pczt::IoFinalizerError), + OrchardParse(orchard::pczt::ParseError), + SaplingFinalize(sapling::pczt::IoFinalizerError), + SaplingParse(sapling::pczt::ParseError), + Sign(super::signer::Error), + TransparentParse(transparent::pczt::ParseError), + UnsupportedTxVersion { version: u32, version_group_id: u32 }, +} + +impl From for Error { + fn from(e: super::signer::Error) -> Self { + match e { + super::signer::Error::OrchardParse(parse_error) => Error::OrchardParse(parse_error), + super::signer::Error::SaplingParse(parse_error) => Error::SaplingParse(parse_error), + super::signer::Error::TransparentParse(parse_error) => { + Error::TransparentParse(parse_error) + } + _ => Error::Sign(e), + } + } +} diff --git a/pczt/src/roles/low_level_signer/mod.rs b/pczt/src/roles/low_level_signer/mod.rs new file mode 100644 index 0000000000..61816681a4 --- /dev/null +++ b/pczt/src/roles/low_level_signer/mod.rs @@ -0,0 +1,80 @@ +use crate::Pczt; + +pub struct Signer { + pczt: Pczt, +} + +impl Signer { + /// Instantiates the low-level Signer role with the given PCZT. + pub fn new(pczt: Pczt) -> Self { + Self { pczt } + } + + /// Exposes the capability to sign the Orchard spends. + #[cfg(feature = "orchard")] + pub fn sign_orchard_with(self, f: F) -> Result + where + E: From, + F: FnOnce(&Pczt, &mut orchard::pczt::Bundle, &mut u8) -> Result<(), E>, + { + let mut pczt = self.pczt; + + let mut tx_modifiable = pczt.global.tx_modifiable; + + let mut bundle = pczt.orchard.clone().into_parsed()?; + + f(&pczt, &mut bundle, &mut tx_modifiable)?; + + pczt.global.tx_modifiable = tx_modifiable; + pczt.orchard = crate::orchard::Bundle::serialize_from(bundle); + + Ok(Self { pczt }) + } + + /// Exposes the capability to sign the Sapling spends. + #[cfg(feature = "sapling")] + pub fn sign_sapling_with(self, f: F) -> Result + where + E: From, + F: FnOnce(&Pczt, &mut sapling::pczt::Bundle, &mut u8) -> Result<(), E>, + { + let mut pczt = self.pczt; + + let mut tx_modifiable = pczt.global.tx_modifiable; + + let mut bundle = pczt.sapling.clone().into_parsed()?; + + f(&pczt, &mut bundle, &mut tx_modifiable)?; + + pczt.global.tx_modifiable = tx_modifiable; + pczt.sapling = crate::sapling::Bundle::serialize_from(bundle); + + Ok(Self { pczt }) + } + + /// Exposes the capability to sign the transparent spends. + #[cfg(feature = "transparent")] + pub fn sign_transparent_with(self, f: F) -> Result + where + E: From, + F: FnOnce(&Pczt, &mut transparent::pczt::Bundle, &mut u8) -> Result<(), E>, + { + let mut pczt = self.pczt; + + let mut tx_modifiable = pczt.global.tx_modifiable; + + let mut bundle = pczt.transparent.clone().into_parsed()?; + + f(&pczt, &mut bundle, &mut tx_modifiable)?; + + pczt.global.tx_modifiable = tx_modifiable; + pczt.transparent = crate::transparent::Bundle::serialize_from(bundle); + + Ok(Self { pczt }) + } + + /// Finishes the low-level Signer role, returning the updated PCZT. + pub fn finish(self) -> Pczt { + self.pczt + } +} diff --git a/pczt/src/roles/prover/mod.rs b/pczt/src/roles/prover/mod.rs new file mode 100644 index 0000000000..9772a93890 --- /dev/null +++ b/pczt/src/roles/prover/mod.rs @@ -0,0 +1,27 @@ +use crate::Pczt; + +#[cfg(feature = "orchard")] +mod orchard; +#[cfg(feature = "orchard")] +pub use orchard::OrchardError; + +#[cfg(feature = "sapling")] +mod sapling; +#[cfg(feature = "sapling")] +pub use sapling::SaplingError; + +pub struct Prover { + pczt: Pczt, +} + +impl Prover { + /// Instantiates the Prover role with the given PCZT. + pub fn new(pczt: Pczt) -> Self { + Self { pczt } + } + + /// Finishes the Prover role, returning the updated PCZT. + pub fn finish(self) -> Pczt { + self.pczt + } +} diff --git a/pczt/src/roles/prover/orchard.rs b/pczt/src/roles/prover/orchard.rs new file mode 100644 index 0000000000..237b9bdc3e --- /dev/null +++ b/pczt/src/roles/prover/orchard.rs @@ -0,0 +1,37 @@ +use orchard::circuit::ProvingKey; +use rand_core::OsRng; + +use crate::Pczt; + +impl super::Prover { + pub fn create_orchard_proof(self, pk: &ProvingKey) -> Result { + let Pczt { + global, + transparent, + sapling, + orchard, + } = self.pczt; + + let mut bundle = orchard.into_parsed().map_err(OrchardError::Parser)?; + + bundle + .create_proof(pk, OsRng) + .map_err(OrchardError::Prover)?; + + Ok(Self { + pczt: Pczt { + global, + transparent, + sapling, + orchard: crate::orchard::Bundle::serialize_from(bundle), + }, + }) + } +} + +/// Errors that can occur while creating Orchard proofs for a PCZT. +#[derive(Debug)] +pub enum OrchardError { + Parser(orchard::pczt::ParseError), + Prover(orchard::pczt::ProverError), +} diff --git a/pczt/src/roles/prover/sapling.rs b/pczt/src/roles/prover/sapling.rs new file mode 100644 index 0000000000..877e22bab7 --- /dev/null +++ b/pczt/src/roles/prover/sapling.rs @@ -0,0 +1,45 @@ +use rand_core::OsRng; +use sapling::prover::{OutputProver, SpendProver}; + +use crate::Pczt; + +impl super::Prover { + pub fn create_sapling_proofs( + self, + spend_prover: &S, + output_prover: &O, + ) -> Result + where + S: SpendProver, + O: OutputProver, + { + let Pczt { + global, + transparent, + sapling, + orchard, + } = self.pczt; + + let mut bundle = sapling.into_parsed().map_err(SaplingError::Parser)?; + + bundle + .create_proofs(spend_prover, output_prover, OsRng) + .map_err(SaplingError::Prover)?; + + Ok(Self { + pczt: Pczt { + global, + transparent, + sapling: crate::sapling::Bundle::serialize_from(bundle), + orchard, + }, + }) + } +} + +/// Errors that can occur while creating Sapling proofs for a PCZT. +#[derive(Debug)] +pub enum SaplingError { + Parser(sapling::pczt::ParseError), + Prover(sapling::pczt::ProverError), +} diff --git a/pczt/src/roles/redactor/mod.rs b/pczt/src/roles/redactor/mod.rs new file mode 100644 index 0000000000..7dad3a8262 --- /dev/null +++ b/pczt/src/roles/redactor/mod.rs @@ -0,0 +1,45 @@ +use crate::{common::Global, Pczt}; + +pub mod orchard; +pub mod sapling; +pub mod transparent; + +pub struct Redactor { + pczt: Pczt, +} + +impl Redactor { + /// Instantiates the Redactor role with the given PCZT. + pub fn new(pczt: Pczt) -> Self { + Self { pczt } + } + + /// Redacts the global transaction details with the given closure. + pub fn redact_global_with(mut self, f: F) -> Self + where + F: FnOnce(GlobalRedactor<'_>), + { + f(GlobalRedactor(&mut self.pczt.global)); + self + } + + /// Finishes the Redactor role, returning the redacted PCZT. + pub fn finish(self) -> Pczt { + self.pczt + } +} + +/// An Redactor for the global transaction details. +pub struct GlobalRedactor<'a>(&'a mut Global); + +impl GlobalRedactor<'_> { + /// Redacts the proprietary value at the given key. + pub fn redact_proprietary(&mut self, key: &str) { + self.0.proprietary.remove(key); + } + + /// Removes all proprietary values. + pub fn clear_proprietary(&mut self) { + self.0.proprietary.clear(); + } +} diff --git a/pczt/src/roles/redactor/orchard.rs b/pczt/src/roles/redactor/orchard.rs new file mode 100644 index 0000000000..d094a810d7 --- /dev/null +++ b/pczt/src/roles/redactor/orchard.rs @@ -0,0 +1,222 @@ +use crate::orchard::{Action, Bundle}; + +impl super::Redactor { + /// Redacts the Orchard bundle with the given closure. + pub fn redact_orchard_with(mut self, f: F) -> Self + where + F: FnOnce(OrchardRedactor<'_>), + { + f(OrchardRedactor(&mut self.pczt.orchard)); + self + } +} + +/// A Redactor for the Orchard bundle. +pub struct OrchardRedactor<'a>(&'a mut Bundle); + +impl OrchardRedactor<'_> { + /// Redacts all actions in the same way. + pub fn redact_actions(&mut self, f: F) + where + F: FnOnce(ActionRedactor<'_>), + { + f(ActionRedactor(Actions::All(&mut self.0.actions))); + } + + /// Redacts the action at the given index. + /// + /// Does nothing if the index is out of range. + pub fn redact_action(&mut self, index: usize, f: F) + where + F: FnOnce(ActionRedactor<'_>), + { + if let Some(action) = self.0.actions.get_mut(index) { + f(ActionRedactor(Actions::One(action))); + } + } + + /// Removes the proof. + pub fn clear_zkproof(&mut self) { + self.0.zkproof = None; + } + + /// Removes the proof. + pub fn clear_bsk(&mut self) { + self.0.bsk = None; + } +} + +/// A Redactor for Orchard actions. +pub struct ActionRedactor<'a>(Actions<'a>); + +enum Actions<'a> { + All(&'a mut [Action]), + One(&'a mut Action), +} + +impl ActionRedactor<'_> { + fn redact(&mut self, f: F) + where + F: Fn(&mut Action), + { + match &mut self.0 { + Actions::All(actions) => { + for action in actions.iter_mut() { + f(action); + } + } + Actions::One(action) => { + f(action); + } + } + } + + /// Removes the spend authorizing signature. + pub fn clear_spend_auth_sig(&mut self) { + self.redact(|action| { + action.spend.spend_auth_sig = None; + }); + } + + /// Removes the spend's recipient. + pub fn clear_spend_recipient(&mut self) { + self.redact(|action| { + action.spend.recipient = None; + }); + } + + /// Removes the spend's value. + pub fn clear_spend_value(&mut self) { + self.redact(|action| { + action.spend.value = None; + }); + } + + /// Removes the rho value for the note being spent. + pub fn clear_spend_rho(&mut self) { + self.redact(|action| { + action.spend.rho = None; + }); + } + + /// Removes the seed randomness for the note being spent. + pub fn clear_spend_rseed(&mut self) { + self.redact(|action| { + action.spend.rseed = None; + }); + } + + /// Removes the spend's full viewing key. + pub fn clear_spend_fvk(&mut self) { + self.redact(|action| { + action.spend.fvk = None; + }); + } + + /// Removes the witness from the spent note to the bundle's anchor. + pub fn clear_spend_witness(&mut self) { + self.redact(|action| { + action.spend.witness = None; + }); + } + + /// Removes the spend authorization randomizer. + pub fn clear_spend_alpha(&mut self) { + self.redact(|action| { + action.spend.alpha = None; + }); + } + + /// Removes the ZIP 32 derivation path at which the spending key can be found for the + /// note being spent. + pub fn clear_spend_zip32_derivation(&mut self) { + self.redact(|action| { + action.spend.zip32_derivation = None; + }); + } + + /// Removes the spending key for this spent note, if it is a dummy note. + pub fn clear_spend_dummy_sk(&mut self) { + self.redact(|action| { + action.spend.dummy_sk = None; + }); + } + + /// Redacts the spend-specific proprietary value at the given key. + pub fn redact_spend_proprietary(&mut self, key: &str) { + self.redact(|action| { + action.spend.proprietary.remove(key); + }); + } + + /// Removes all spend-specific proprietary values. + pub fn clear_spend_proprietary(&mut self) { + self.redact(|action| { + action.spend.proprietary.clear(); + }); + } + + /// Removes the output's recipient. + pub fn clear_output_recipient(&mut self) { + self.redact(|action| { + action.output.recipient = None; + }); + } + + /// Removes the output's value. + pub fn clear_output_value(&mut self) { + self.redact(|action| { + action.output.value = None; + }); + } + + /// Removes the seed randomness for the note being created. + pub fn clear_output_rseed(&mut self) { + self.redact(|action| { + action.output.rseed = None; + }); + } + + /// Removes the `ock` value used to encrypt `out_ciphertext`. + pub fn clear_output_ock(&mut self) { + self.redact(|action| { + action.output.ock = None; + }); + } + + /// Removes the ZIP 32 derivation path at which the spending key can be found for the + /// note being created. + pub fn clear_output_zip32_derivation(&mut self) { + self.redact(|action| { + action.output.zip32_derivation = None; + }); + } + + /// Removes the user-facing address to which the output is being sent, if any. + pub fn clear_output_user_address(&mut self) { + self.redact(|spend| { + spend.output.user_address = None; + }); + } + + /// Redacts the output-specific proprietary value at the given key. + pub fn redact_output_proprietary(&mut self, key: &str) { + self.redact(|action| { + action.output.proprietary.remove(key); + }); + } + + /// Removes all output-specific proprietary values. + pub fn clear_output_proprietary(&mut self) { + self.redact(|action| { + action.output.proprietary.clear(); + }); + } + + /// Removes the value commitment randomness. + pub fn clear_rcv(&mut self) { + self.redact(|action| { + action.rcv = None; + }); + } +} diff --git a/pczt/src/roles/redactor/sapling.rs b/pczt/src/roles/redactor/sapling.rs new file mode 100644 index 0000000000..67035db51b --- /dev/null +++ b/pczt/src/roles/redactor/sapling.rs @@ -0,0 +1,284 @@ +use crate::sapling::{Bundle, Output, Spend}; + +impl super::Redactor { + /// Redacts the Sapling bundle with the given closure. + pub fn redact_sapling_with(mut self, f: F) -> Self + where + F: FnOnce(SaplingRedactor<'_>), + { + f(SaplingRedactor(&mut self.pczt.sapling)); + self + } +} + +/// A Redactor for the Sapling bundle. +pub struct SaplingRedactor<'a>(&'a mut Bundle); + +impl SaplingRedactor<'_> { + /// Redacts all spends in the same way. + pub fn redact_spends(&mut self, f: F) + where + F: FnOnce(SpendRedactor<'_>), + { + f(SpendRedactor(Spends::All(&mut self.0.spends))); + } + + /// Redacts the spend at the given index. + /// + /// Does nothing if the index is out of range. + pub fn redact_spend(&mut self, index: usize, f: F) + where + F: FnOnce(SpendRedactor<'_>), + { + if let Some(spend) = self.0.spends.get_mut(index) { + f(SpendRedactor(Spends::One(spend))); + } + } + + /// Redacts all outputs in the same way. + pub fn redact_outputs(&mut self, f: F) + where + F: FnOnce(OutputRedactor<'_>), + { + f(OutputRedactor(Outputs::All(&mut self.0.outputs))); + } + + /// Redacts the output at the given index. + /// + /// Does nothing if the index is out of range. + pub fn redact_output(&mut self, index: usize, f: F) + where + F: FnOnce(OutputRedactor<'_>), + { + if let Some(output) = self.0.outputs.get_mut(index) { + f(OutputRedactor(Outputs::One(output))); + } + } + + /// Removes the proof. + pub fn clear_bsk(&mut self) { + self.0.bsk = None; + } +} + +/// A Redactor for Sapling spends. +pub struct SpendRedactor<'a>(Spends<'a>); + +enum Spends<'a> { + All(&'a mut [Spend]), + One(&'a mut Spend), +} + +impl SpendRedactor<'_> { + fn redact(&mut self, f: F) + where + F: Fn(&mut Spend), + { + match &mut self.0 { + Spends::All(spends) => { + for spend in spends.iter_mut() { + f(spend); + } + } + Spends::One(spend) => { + f(spend); + } + } + } + + /// Removes the proof. + pub fn clear_zkproof(&mut self) { + self.redact(|spend| { + spend.zkproof = None; + }); + } + + /// Removes the spend authorizing signature. + pub fn clear_spend_auth_sig(&mut self) { + self.redact(|spend| { + spend.spend_auth_sig = None; + }); + } + + /// Removes the recipient. + pub fn clear_recipient(&mut self) { + self.redact(|spend| { + spend.recipient = None; + }); + } + + /// Removes the value. + pub fn clear_value(&mut self) { + self.redact(|spend| { + spend.value = None; + }); + } + + /// Removes the note commitment randomness. + pub fn clear_rcm(&mut self) { + self.redact(|spend| { + spend.rcm = None; + }); + } + + /// Removes the seed randomness for the note being spent. + pub fn clear_rseed(&mut self) { + self.redact(|spend| { + spend.rseed = None; + }); + } + + /// Removes the value commitment randomness. + pub fn clear_rcv(&mut self) { + self.redact(|spend| { + spend.rcv = None; + }); + } + + /// Removes the proof generation key. + pub fn clear_proof_generation_key(&mut self) { + self.redact(|spend| { + spend.proof_generation_key = None; + }); + } + + /// Removes the witness from the note to the bundle's anchor. + pub fn clear_witness(&mut self) { + self.redact(|spend| { + spend.witness = None; + }); + } + + /// Removes the spend authorization randomizer. + pub fn clear_alpha(&mut self) { + self.redact(|spend| { + spend.alpha = None; + }); + } + + /// Removes the ZIP 32 derivation path at which the spending key can be found for the + /// note being spent. + pub fn clear_zip32_derivation(&mut self) { + self.redact(|spend| { + spend.zip32_derivation = None; + }); + } + + /// Removes the spend authorizing key for this spent note, if it is a dummy note. + pub fn clear_dummy_ask(&mut self) { + self.redact(|spend| { + spend.dummy_ask = None; + }); + } + + /// Redacts the proprietary value at the given key. + pub fn redact_proprietary(&mut self, key: &str) { + self.redact(|spend| { + spend.proprietary.remove(key); + }); + } + + /// Removes all proprietary values. + pub fn clear_proprietary(&mut self) { + self.redact(|spend| { + spend.proprietary.clear(); + }); + } +} + +/// A Redactor for Sapling outputs. +pub struct OutputRedactor<'a>(Outputs<'a>); + +enum Outputs<'a> { + All(&'a mut [Output]), + One(&'a mut Output), +} + +impl OutputRedactor<'_> { + fn redact(&mut self, f: F) + where + F: Fn(&mut Output), + { + match &mut self.0 { + Outputs::All(outputs) => { + for output in outputs.iter_mut() { + f(output); + } + } + Outputs::One(output) => { + f(output); + } + } + } + + /// Removes the proof. + pub fn clear_zkproof(&mut self) { + self.redact(|output| { + output.zkproof = None; + }); + } + + /// Removes the recipient. + pub fn clear_recipient(&mut self) { + self.redact(|output| { + output.recipient = None; + }); + } + + /// Removes the value. + pub fn clear_value(&mut self) { + self.redact(|output| { + output.value = None; + }); + } + + /// Removes the seed randomness for the note being created. + pub fn clear_rseed(&mut self) { + self.redact(|output| { + output.rseed = None; + }); + } + + /// Removes the value commitment randomness. + pub fn clear_rcv(&mut self) { + self.redact(|output| { + output.rcv = None; + }); + } + + /// Removes the `ock` value used to encrypt `out_ciphertext`. + pub fn clear_ock(&mut self) { + self.redact(|output| { + output.ock = None; + }); + } + + /// Removes the ZIP 32 derivation path at which the spending key can be found for the + /// note being created. + pub fn clear_zip32_derivation(&mut self) { + self.redact(|output| { + output.zip32_derivation = None; + }); + } + + /// Removes the user-facing address to which this output is being sent, if any. + pub fn clear_user_address(&mut self) { + self.redact(|output| { + output.user_address = None; + }); + } + + /// Redacts the proprietary value at the given key. + pub fn redact_proprietary(&mut self, key: &str) { + self.redact(|output| { + output.proprietary.remove(key); + }); + } + + /// Removes all proprietary values. + pub fn clear_proprietary(&mut self) { + self.redact(|output| { + output.proprietary.clear(); + }); + } +} diff --git a/pczt/src/roles/redactor/transparent.rs b/pczt/src/roles/redactor/transparent.rs new file mode 100644 index 0000000000..8eac4c184e --- /dev/null +++ b/pczt/src/roles/redactor/transparent.rs @@ -0,0 +1,263 @@ +use crate::transparent::{Bundle, Input, Output}; + +impl super::Redactor { + /// Redacts the transparent bundle with the given closure. + pub fn redact_transparent_with(mut self, f: F) -> Self + where + F: FnOnce(TransparentRedactor<'_>), + { + f(TransparentRedactor(&mut self.pczt.transparent)); + self + } +} + +/// A Redactor for the transparent bundle. +pub struct TransparentRedactor<'a>(&'a mut Bundle); + +impl TransparentRedactor<'_> { + /// Redacts all inputs in the same way. + pub fn redact_inputs(&mut self, f: F) + where + F: FnOnce(InputRedactor<'_>), + { + f(InputRedactor(Inputs::All(&mut self.0.inputs))); + } + + /// Redacts the input at the given index. + /// + /// Does nothing if the index is out of range. + pub fn redact_input(&mut self, index: usize, f: F) + where + F: FnOnce(InputRedactor<'_>), + { + if let Some(input) = self.0.inputs.get_mut(index) { + f(InputRedactor(Inputs::One(input))); + } + } + + /// Redacts all outputs in the same way. + pub fn redact_outputs(&mut self, f: F) + where + F: FnOnce(OutputRedactor<'_>), + { + f(OutputRedactor(Outputs::All(&mut self.0.outputs))); + } + + /// Redacts the output at the given index. + /// + /// Does nothing if the index is out of range. + pub fn redact_output(&mut self, index: usize, f: F) + where + F: FnOnce(OutputRedactor<'_>), + { + if let Some(output) = self.0.outputs.get_mut(index) { + f(OutputRedactor(Outputs::One(output))); + } + } +} + +/// A Redactor for transparent inputs. +pub struct InputRedactor<'a>(Inputs<'a>); + +enum Inputs<'a> { + All(&'a mut [Input]), + One(&'a mut Input), +} + +impl InputRedactor<'_> { + fn redact(&mut self, f: F) + where + F: Fn(&mut Input), + { + match &mut self.0 { + Inputs::All(inputs) => { + for input in inputs.iter_mut() { + f(input); + } + } + Inputs::One(input) => { + f(input); + } + } + } + + /// Removes the `script_sig`. + pub fn clear_script_sig(&mut self) { + self.redact(|input| { + input.script_sig = None; + }); + } + + /// Removes the `redeem_script`. + pub fn clear_redeem_script(&mut self) { + self.redact(|input| { + input.redeem_script = None; + }); + } + + /// Redacts the signature for the given pubkey. + pub fn redact_partial_signature(&mut self, pubkey: [u8; 33]) { + self.redact(|input| { + input.partial_signatures.remove(&pubkey); + }); + } + + /// Removes all signatures. + pub fn clear_partial_signatures(&mut self) { + self.redact(|input| { + input.partial_signatures.clear(); + }); + } + + /// Redacts the BIP 32 derivation path for the given pubkey. + pub fn redact_bip32_derivation(&mut self, pubkey: [u8; 33]) { + self.redact(|input| { + input.bip32_derivation.remove(&pubkey); + }); + } + + /// Removes all BIP 32 derivation paths. + pub fn clear_bip32_derivation(&mut self) { + self.redact(|input| { + input.bip32_derivation.clear(); + }); + } + + /// Redacts the RIPEMD160 preimage for the given hash. + pub fn redact_ripemd160_preimage(&mut self, hash: [u8; 20]) { + self.redact(|input| { + input.ripemd160_preimages.remove(&hash); + }); + } + + /// Removes all RIPEMD160 preimages. + pub fn clear_ripemd160_preimages(&mut self) { + self.redact(|input| { + input.ripemd160_preimages.clear(); + }); + } + + /// Redacts the SHA256 preimage for the given hash. + pub fn redact_sha256_preimage(&mut self, hash: [u8; 32]) { + self.redact(|input| { + input.sha256_preimages.remove(&hash); + }); + } + + /// Removes all SHA256 preimages. + pub fn clear_sha256_preimages(&mut self) { + self.redact(|input| { + input.sha256_preimages.clear(); + }); + } + + /// Redacts the HASH160 preimage for the given hash. + pub fn redact_hash160_preimage(&mut self, hash: [u8; 20]) { + self.redact(|input| { + input.hash160_preimages.remove(&hash); + }); + } + + /// Removes all HASH160 preimages. + pub fn clear_hash160_preimages(&mut self) { + self.redact(|input| { + input.hash160_preimages.clear(); + }); + } + + /// Redacts the HASH256 preimage for the given hash. + pub fn redact_hash256_preimage(&mut self, hash: [u8; 32]) { + self.redact(|input| { + input.hash256_preimages.remove(&hash); + }); + } + + /// Removes all HASH256 preimages. + pub fn clear_hash256_preimages(&mut self) { + self.redact(|input| { + input.hash256_preimages.clear(); + }); + } + + /// Redacts the proprietary value at the given key. + pub fn redact_proprietary(&mut self, key: &str) { + self.redact(|input| { + input.proprietary.remove(key); + }); + } + + /// Removes all proprietary values. + pub fn clear_proprietary(&mut self) { + self.redact(|input| { + input.proprietary.clear(); + }); + } +} + +/// A Redactor for transparent outputs. +pub struct OutputRedactor<'a>(Outputs<'a>); + +enum Outputs<'a> { + All(&'a mut [Output]), + One(&'a mut Output), +} + +impl OutputRedactor<'_> { + fn redact(&mut self, f: F) + where + F: Fn(&mut Output), + { + match &mut self.0 { + Outputs::All(outputs) => { + for output in outputs.iter_mut() { + f(output); + } + } + Outputs::One(output) => { + f(output); + } + } + } + + /// Removes the `redeem_script`. + pub fn clear_redeem_script(&mut self) { + self.redact(|output| { + output.redeem_script = None; + }); + } + + /// Redacts the BIP 32 derivation path for the given pubkey. + pub fn redact_bip32_derivation(&mut self, pubkey: [u8; 33]) { + self.redact(|output| { + output.bip32_derivation.remove(&pubkey); + }); + } + + /// Removes all BIP 32 derivation paths. + pub fn clear_bip32_derivation(&mut self) { + self.redact(|output| { + output.bip32_derivation.clear(); + }); + } + + /// Removes the user-facing address to which this output is being sent, if any. + pub fn clear_user_address(&mut self) { + self.redact(|output| { + output.user_address = None; + }); + } + + /// Redacts the proprietary value at the given key. + pub fn redact_proprietary(&mut self, key: &str) { + self.redact(|output| { + output.proprietary.remove(key); + }); + } + + /// Removes all proprietary values. + pub fn clear_proprietary(&mut self) { + self.redact(|output| { + output.proprietary.clear(); + }); + } +} diff --git a/pczt/src/roles/signer/mod.rs b/pczt/src/roles/signer/mod.rs new file mode 100644 index 0000000000..7ab2fb9ea1 --- /dev/null +++ b/pczt/src/roles/signer/mod.rs @@ -0,0 +1,311 @@ +use blake2b_simd::Hash as Blake2bHash; +use rand_core::OsRng; + +use ::transparent::sighash::{SIGHASH_ANYONECANPAY, SIGHASH_NONE, SIGHASH_SINGLE}; +use zcash_primitives::transaction::{ + sighash::SignableInput, sighash_v5::v5_signature_hash, txid::TxIdDigester, Authorization, + TransactionData, TxDigests, TxVersion, +}; +use zcash_protocol::consensus::BranchId; + +use crate::{ + common::{ + Global, FLAG_HAS_SIGHASH_SINGLE, FLAG_SHIELDED_MODIFIABLE, + FLAG_TRANSPARENT_INPUTS_MODIFIABLE, FLAG_TRANSPARENT_OUTPUTS_MODIFIABLE, + }, + Pczt, +}; + +use crate::common::determine_lock_time; + +const V5_TX_VERSION: u32 = 5; +const V5_VERSION_GROUP_ID: u32 = 0x26A7270A; + +pub struct Signer { + global: Global, + transparent: transparent::pczt::Bundle, + sapling: sapling::pczt::Bundle, + orchard: orchard::pczt::Bundle, + /// Cached across multiple signatures. + tx_data: TransactionData, + txid_parts: TxDigests, + shielded_sighash: [u8; 32], + secp: secp256k1::Secp256k1, +} + +impl Signer { + /// Instantiates the Signer role with the given PCZT. + pub fn new(pczt: Pczt) -> Result { + let Pczt { + global, + transparent, + sapling, + orchard, + } = pczt; + + let transparent = transparent.into_parsed().map_err(Error::TransparentParse)?; + let sapling = sapling.into_parsed().map_err(Error::SaplingParse)?; + let orchard = orchard.into_parsed().map_err(Error::OrchardParse)?; + + let tx_data = pczt_to_tx_data(&global, &transparent, &sapling, &orchard)?; + let txid_parts = tx_data.digest(TxIdDigester); + + // TODO: Pick sighash based on tx version. + match (global.tx_version, global.version_group_id) { + (V5_TX_VERSION, V5_VERSION_GROUP_ID) => Ok(()), + (version, version_group_id) => Err(Error::Global(GlobalError::UnsupportedTxVersion { + version, + version_group_id, + })), + }?; + let shielded_sighash = v5_signature_hash(&tx_data, &SignableInput::Shielded, &txid_parts) + .as_ref() + .try_into() + .expect("correct length"); + + Ok(Self { + global, + transparent, + sapling, + orchard, + tx_data, + txid_parts, + shielded_sighash, + secp: secp256k1::Secp256k1::signing_only(), + }) + } + + /// Signs the transparent spend at the given index with the given spending key. + /// + /// It is the caller's responsibility to perform any semantic validity checks on the + /// PCZT (for example, comfirming that the change amounts are correct) before calling + /// this method. + pub fn sign_transparent( + &mut self, + index: usize, + sk: &secp256k1::SecretKey, + ) -> Result<(), Error> { + let input = self + .transparent + .inputs_mut() + .get_mut(index) + .ok_or(Error::InvalidIndex)?; + + // Check consistency of the input being signed. + // TODO + + input + .sign( + index, + |input| { + v5_signature_hash( + &self.tx_data, + &SignableInput::Transparent(input), + &self.txid_parts, + ) + .as_ref() + .try_into() + .unwrap() + }, + sk, + &self.secp, + ) + .map_err(Error::TransparentSign)?; + + // Update transaction modifiability: + // - If the Signer added a signature that does not use `SIGHASH_ANYONECANPAY`, the + // Transparent Inputs Modifiable Flag must be set to False (because the + // signature commits to all inputs, not just the one at `index`). + if input.sighash_type().encode() & SIGHASH_ANYONECANPAY == 0 { + self.global.tx_modifiable &= !FLAG_TRANSPARENT_INPUTS_MODIFIABLE; + } + // - If the Signer added a signature that does not use `SIGHASH_NONE`, the + // Transparent Outputs Modifiable Flag must be set to False. Note that this + // applies to `SIGHASH_SINGLE` because we could otherwise remove the output at + // `index`, which would not remove the signature. + if (input.sighash_type().encode() & !SIGHASH_ANYONECANPAY) != SIGHASH_NONE { + self.global.tx_modifiable &= !FLAG_TRANSPARENT_OUTPUTS_MODIFIABLE; + } + // - If the Signer added a signature that uses `SIGHASH_SINGLE`, the Has + // `SIGHASH_SINGLE` flag must be set to True. + if (input.sighash_type().encode() & !SIGHASH_ANYONECANPAY) == SIGHASH_SINGLE { + self.global.tx_modifiable |= FLAG_HAS_SIGHASH_SINGLE; + } + // - Always set the Shielded Modifiable Flag to False. + self.global.tx_modifiable &= !FLAG_SHIELDED_MODIFIABLE; + + Ok(()) + } + + /// Signs the Sapling spend at the given index with the given spend authorizing key. + /// + /// Requires the spend's `proof_generation_key` field to be set (because the API does + /// not take an FVK). + /// + /// It is the caller's responsibility to perform any semantic validity checks on the + /// PCZT (for example, comfirming that the change amounts are correct) before calling + /// this method. + pub fn sign_sapling( + &mut self, + index: usize, + ask: &sapling::keys::SpendAuthorizingKey, + ) -> Result<(), Error> { + let spend = self + .sapling + .spends_mut() + .get_mut(index) + .ok_or(Error::InvalidIndex)?; + + // Check consistency of the input being signed if we have its note components. + match spend.verify_nullifier(None) { + Err( + sapling::pczt::VerifyError::MissingRecipient + | sapling::pczt::VerifyError::MissingValue + | sapling::pczt::VerifyError::MissingRandomSeed, + ) => Ok(()), + r => r, + } + .map_err(Error::SaplingVerify)?; + + spend + .sign(self.shielded_sighash, ask, OsRng) + .map_err(Error::SaplingSign)?; + + // Update transaction modifiability: all transaction effects have been committed + // to by the signature. + self.global.tx_modifiable &= !(FLAG_TRANSPARENT_INPUTS_MODIFIABLE + | FLAG_TRANSPARENT_OUTPUTS_MODIFIABLE + | FLAG_SHIELDED_MODIFIABLE); + + Ok(()) + } + + /// Signs the Orchard spend at the given index with the given spend authorizing key. + /// + /// Requires the spend's `fvk` field to be set (because the API does not take an FVK). + /// + /// It is the caller's responsibility to perform any semantic validity checks on the + /// PCZT (for example, comfirming that the change amounts are correct) before calling + /// this method. + pub fn sign_orchard( + &mut self, + index: usize, + ask: &orchard::keys::SpendAuthorizingKey, + ) -> Result<(), Error> { + let action = self + .orchard + .actions_mut() + .get_mut(index) + .ok_or(Error::InvalidIndex)?; + + // Check consistency of the input being signed if we have its note components. + match action.spend().verify_nullifier(None) { + Err( + orchard::pczt::VerifyError::MissingRecipient + | orchard::pczt::VerifyError::MissingValue + | orchard::pczt::VerifyError::MissingRho + | orchard::pczt::VerifyError::MissingRandomSeed, + ) => Ok(()), + r => r, + } + .map_err(Error::OrchardVerify)?; + + action + .sign(self.shielded_sighash, ask, OsRng) + .map_err(Error::OrchardSign)?; + + // Update transaction modifiability: all transaction effects have been committed + // to by the signature. + self.global.tx_modifiable &= !(FLAG_TRANSPARENT_INPUTS_MODIFIABLE + | FLAG_TRANSPARENT_OUTPUTS_MODIFIABLE + | FLAG_SHIELDED_MODIFIABLE); + + Ok(()) + } + + /// Finishes the Signer role, returning the updated PCZT. + pub fn finish(self) -> Pczt { + Pczt { + global: self.global, + transparent: crate::transparent::Bundle::serialize_from(self.transparent), + sapling: crate::sapling::Bundle::serialize_from(self.sapling), + orchard: crate::orchard::Bundle::serialize_from(self.orchard), + } + } +} + +/// Extracts an unauthorized `TransactionData` from the PCZT. +/// +/// We don't care about existing proofs or signatures here, because they do not affect the +/// sighash; we only want the effects of the transaction. +pub(crate) fn pczt_to_tx_data( + global: &Global, + transparent: &transparent::pczt::Bundle, + sapling: &sapling::pczt::Bundle, + orchard: &orchard::pczt::Bundle, +) -> Result, Error> { + let version = match (global.tx_version, global.version_group_id) { + (V5_TX_VERSION, V5_VERSION_GROUP_ID) => Ok(TxVersion::Zip225), + (version, version_group_id) => Err(Error::Global(GlobalError::UnsupportedTxVersion { + version, + version_group_id, + })), + }?; + + let consensus_branch_id = BranchId::try_from(global.consensus_branch_id) + .map_err(|_| Error::Global(GlobalError::UnknownConsensusBranchId))?; + + let transparent_bundle = transparent + .extract_effects() + .map_err(Error::TransparentExtract)?; + + let sapling_bundle = sapling.extract_effects().map_err(Error::SaplingExtract)?; + + let orchard_bundle = orchard.extract_effects().map_err(Error::OrchardExtract)?; + + Ok(TransactionData::from_parts( + version, + consensus_branch_id, + determine_lock_time(global, transparent.inputs()).ok_or(Error::IncompatibleLockTimes)?, + global.expiry_height.into(), + transparent_bundle, + None, + sapling_bundle, + orchard_bundle, + )) +} + +pub struct EffectsOnly; + +impl Authorization for EffectsOnly { + type TransparentAuth = transparent::bundle::EffectsOnly; + type SaplingAuth = sapling::bundle::EffectsOnly; + type OrchardAuth = orchard::bundle::EffectsOnly; + #[cfg(zcash_unstable = "zfuture")] + type TzeAuth = core::convert::Infallible; +} + +/// Errors that can occur while creating signatures for a PCZT. +#[derive(Debug)] +pub enum Error { + Global(GlobalError), + IncompatibleLockTimes, + InvalidIndex, + OrchardExtract(orchard::pczt::TxExtractorError), + OrchardParse(orchard::pczt::ParseError), + OrchardSign(orchard::pczt::SignerError), + OrchardVerify(orchard::pczt::VerifyError), + SaplingExtract(sapling::pczt::TxExtractorError), + SaplingParse(sapling::pczt::ParseError), + SaplingSign(sapling::pczt::SignerError), + SaplingVerify(sapling::pczt::VerifyError), + TransparentExtract(transparent::pczt::TxExtractorError), + TransparentParse(transparent::pczt::ParseError), + TransparentSign(transparent::pczt::SignerError), +} + +#[derive(Debug)] +pub enum GlobalError { + UnknownConsensusBranchId, + UnsupportedTxVersion { version: u32, version_group_id: u32 }, +} diff --git a/pczt/src/roles/spend_finalizer/mod.rs b/pczt/src/roles/spend_finalizer/mod.rs new file mode 100644 index 0000000000..12dc01862f --- /dev/null +++ b/pczt/src/roles/spend_finalizer/mod.rs @@ -0,0 +1,42 @@ +use crate::Pczt; + +pub struct SpendFinalizer { + pczt: Pczt, +} + +impl SpendFinalizer { + /// Instantiates the Spend Finalizer role with the given PCZT. + pub fn new(pczt: Pczt) -> Self { + Self { pczt } + } + + /// Finalizes the spends of the PCZT. + pub fn finalize_spends(self) -> Result { + let Pczt { + global, + transparent, + sapling, + orchard, + } = self.pczt; + + let mut transparent = transparent.into_parsed().map_err(Error::TransparentParse)?; + + transparent + .finalize_spends() + .map_err(Error::TransparentFinalize)?; + + Ok(Pczt { + global, + transparent: crate::transparent::Bundle::serialize_from(transparent), + sapling, + orchard, + }) + } +} + +/// Errors that can occur while finalizing the spends of a PCZT. +#[derive(Debug)] +pub enum Error { + TransparentFinalize(transparent::pczt::SpendFinalizerError), + TransparentParse(transparent::pczt::ParseError), +} diff --git a/pczt/src/roles/tx_extractor/mod.rs b/pczt/src/roles/tx_extractor/mod.rs new file mode 100644 index 0000000000..4b11695662 --- /dev/null +++ b/pczt/src/roles/tx_extractor/mod.rs @@ -0,0 +1,175 @@ +use core::marker::PhantomData; +use rand_core::OsRng; + +use zcash_primitives::transaction::{ + sighash::{signature_hash, SignableInput}, + txid::TxIdDigester, + Authorization, Transaction, TransactionData, TxVersion, +}; +use zcash_protocol::consensus::BranchId; + +use crate::{common::determine_lock_time, Pczt, V5_TX_VERSION, V5_VERSION_GROUP_ID}; + +mod orchard; +pub use self::orchard::OrchardError; + +mod sapling; +pub use self::sapling::SaplingError; + +mod transparent; +pub use self::transparent::TransparentError; + +pub struct TransactionExtractor<'a> { + pczt: Pczt, + sapling_vk: Option<( + &'a ::sapling::circuit::SpendVerifyingKey, + &'a ::sapling::circuit::OutputVerifyingKey, + )>, + orchard_vk: Option<&'a ::orchard::circuit::VerifyingKey>, + _unused: PhantomData<&'a ()>, +} + +impl<'a> TransactionExtractor<'a> { + /// Instantiates the Transaction Extractor role with the given PCZT. + pub fn new(pczt: Pczt) -> Self { + Self { + pczt, + sapling_vk: None, + orchard_vk: None, + _unused: PhantomData, + } + } + + /// Provides the Sapling Spend and Output verifying keys for validating the Sapling + /// proofs (if any). + /// + /// If not provided, and the PCZT has a Sapling bundle, [`Self::extract`] will return + /// an error. + pub fn with_sapling( + mut self, + spend_vk: &'a ::sapling::circuit::SpendVerifyingKey, + output_vk: &'a ::sapling::circuit::OutputVerifyingKey, + ) -> Self { + self.sapling_vk = Some((spend_vk, output_vk)); + self + } + + /// Provides an existing Orchard verifying key for validating the Orchard proof (if + /// any). + /// + /// If not provided, and the PCZT has an Orchard bundle, an Orchard verifying key will + /// be generated on the fly. + pub fn with_orchard(mut self, orchard_vk: &'a ::orchard::circuit::VerifyingKey) -> Self { + self.orchard_vk = Some(orchard_vk); + self + } + + /// Attempts to extract a valid transaction from the PCZT. + pub fn extract(self) -> Result { + let Self { + pczt, + sapling_vk, + orchard_vk, + _unused, + } = self; + + let version = match (pczt.global.tx_version, pczt.global.version_group_id) { + (V5_TX_VERSION, V5_VERSION_GROUP_ID) => Ok(TxVersion::Zip225), + (version, version_group_id) => Err(Error::Global(GlobalError::UnsupportedTxVersion { + version, + version_group_id, + })), + }?; + + let consensus_branch_id = BranchId::try_from(pczt.global.consensus_branch_id) + .map_err(|_| Error::Global(GlobalError::UnknownConsensusBranchId))?; + + let lock_time = determine_lock_time(&pczt.global, &pczt.transparent.inputs) + .ok_or(Error::IncompatibleLockTimes)?; + + let transparent_bundle = + transparent::extract_bundle(pczt.transparent).map_err(Error::Transparent)?; + let sapling_bundle = sapling::extract_bundle(pczt.sapling).map_err(Error::Sapling)?; + let orchard_bundle = orchard::extract_bundle(pczt.orchard).map_err(Error::Orchard)?; + + let tx_data = TransactionData::::from_parts( + version, + consensus_branch_id, + lock_time, + pczt.global.expiry_height.into(), + transparent_bundle, + None, + sapling_bundle, + orchard_bundle, + ); + + // The commitment being signed is shared across all shielded inputs. + let txid_parts = tx_data.digest(TxIdDigester); + let shielded_sighash = signature_hash(&tx_data, &SignableInput::Shielded, &txid_parts); + + // Create the binding signatures. + let tx_data = tx_data.try_map_bundles( + |t| Ok(t.map(|t| t.map_authorization(transparent::RemoveInputInfo))), + |s| { + s.map(|s| { + s.apply_binding_signature(*shielded_sighash.as_ref(), OsRng) + .ok_or(Error::SighashMismatch) + }) + .transpose() + }, + |o| { + o.map(|o| { + o.apply_binding_signature(*shielded_sighash.as_ref(), OsRng) + .ok_or(Error::SighashMismatch) + }) + .transpose() + }, + #[cfg(zcash_unstable = "zfuture")] + |_| unimplemented!("PCZT support for TZEs is not implemented."), + )?; + + let tx = tx_data.freeze().expect("v5 tx can't fail here"); + + // Now that we have a supposedly fully-authorized transaction, verify it. + if let Some(bundle) = tx.sapling_bundle() { + let (spend_vk, output_vk) = sapling_vk.ok_or(Error::SaplingRequired)?; + + sapling::verify_bundle(bundle, spend_vk, output_vk, *shielded_sighash.as_ref()) + .map_err(Error::Sapling)?; + } + if let Some(bundle) = tx.orchard_bundle() { + orchard::verify_bundle(bundle, orchard_vk, *shielded_sighash.as_ref()) + .map_err(Error::Orchard)?; + } + + Ok(tx) + } +} + +struct Unbound; + +impl Authorization for Unbound { + type TransparentAuth = ::transparent::pczt::Unbound; + type SaplingAuth = ::sapling::pczt::Unbound; + type OrchardAuth = ::orchard::pczt::Unbound; + #[cfg(zcash_unstable = "zfuture")] + type TzeAuth = core::convert::Infallible; +} + +/// Errors that can occur while extracting a transaction from a PCZT. +#[derive(Debug)] +pub enum Error { + Global(GlobalError), + IncompatibleLockTimes, + Orchard(OrchardError), + Sapling(SaplingError), + SaplingRequired, + SighashMismatch, + Transparent(TransparentError), +} + +#[derive(Debug)] +pub enum GlobalError { + UnknownConsensusBranchId, + UnsupportedTxVersion { version: u32, version_group_id: u32 }, +} diff --git a/pczt/src/roles/tx_extractor/orchard.rs b/pczt/src/roles/tx_extractor/orchard.rs new file mode 100644 index 0000000000..df99d57156 --- /dev/null +++ b/pczt/src/roles/tx_extractor/orchard.rs @@ -0,0 +1,46 @@ +use orchard::{bundle::Authorized, circuit::VerifyingKey, pczt::Unbound, Bundle}; +use rand_core::OsRng; +use zcash_protocol::value::ZatBalance; + +pub(super) fn extract_bundle( + bundle: crate::orchard::Bundle, +) -> Result>, OrchardError> { + bundle + .into_parsed() + .map_err(OrchardError::Parse)? + .extract() + .map_err(OrchardError::Extract) +} + +pub(super) fn verify_bundle( + bundle: &Bundle, + orchard_vk: Option<&VerifyingKey>, + sighash: [u8; 32], +) -> Result<(), OrchardError> { + let mut validator = orchard::bundle::BatchValidator::new(); + let rng = OsRng; + + validator.add_bundle(bundle, sighash); + + if let Some(vk) = orchard_vk { + if validator.validate(vk, rng) { + Ok(()) + } else { + Err(OrchardError::InvalidProof) + } + } else { + let vk = VerifyingKey::build(); + if validator.validate(&vk, rng) { + Ok(()) + } else { + Err(OrchardError::InvalidProof) + } + } +} + +#[derive(Debug)] +pub enum OrchardError { + Extract(orchard::pczt::TxExtractorError), + InvalidProof, + Parse(orchard::pczt::ParseError), +} diff --git a/pczt/src/roles/tx_extractor/sapling.rs b/pczt/src/roles/tx_extractor/sapling.rs new file mode 100644 index 0000000000..7d4562a29c --- /dev/null +++ b/pczt/src/roles/tx_extractor/sapling.rs @@ -0,0 +1,45 @@ +use rand_core::OsRng; +use sapling::{ + bundle::Authorized, + circuit::{OutputVerifyingKey, SpendVerifyingKey}, + pczt::Unbound, + BatchValidator, Bundle, +}; +use zcash_protocol::value::ZatBalance; + +pub(super) fn extract_bundle( + bundle: crate::sapling::Bundle, +) -> Result>, SaplingError> { + bundle + .into_parsed() + .map_err(SaplingError::Parse)? + .extract() + .map_err(SaplingError::Extract) +} + +pub(super) fn verify_bundle( + bundle: &Bundle, + spend_vk: &SpendVerifyingKey, + output_vk: &OutputVerifyingKey, + sighash: [u8; 32], +) -> Result<(), SaplingError> { + let mut validator = BatchValidator::new(); + + if !validator.check_bundle(bundle.clone(), sighash) { + return Err(SaplingError::ConsensusRuleViolation); + } + + if !validator.validate(spend_vk, output_vk, OsRng) { + return Err(SaplingError::InvalidProofsOrSignatures); + } + + Ok(()) +} + +#[derive(Debug)] +pub enum SaplingError { + ConsensusRuleViolation, + Extract(sapling::pczt::TxExtractorError), + InvalidProofsOrSignatures, + Parse(sapling::pczt::ParseError), +} diff --git a/pczt/src/roles/tx_extractor/transparent.rs b/pczt/src/roles/tx_extractor/transparent.rs new file mode 100644 index 0000000000..75a793da81 --- /dev/null +++ b/pczt/src/roles/tx_extractor/transparent.rs @@ -0,0 +1,35 @@ +use transparent::{ + bundle::{Authorization, Authorized, Bundle, MapAuth}, + pczt::{ParseError, TxExtractorError, Unbound}, +}; + +pub(super) fn extract_bundle( + bundle: crate::transparent::Bundle, +) -> Result>, TransparentError> { + bundle + .into_parsed() + .map_err(TransparentError::Parse)? + .extract() + .map_err(TransparentError::Extract) +} + +pub(super) struct RemoveInputInfo; + +impl MapAuth for RemoveInputInfo { + fn map_script_sig( + &self, + s: ::ScriptSig, + ) -> ::ScriptSig { + s + } + + fn map_authorization(&self, _: Unbound) -> Authorized { + Authorized + } +} + +#[derive(Debug)] +pub enum TransparentError { + Extract(TxExtractorError), + Parse(ParseError), +} diff --git a/pczt/src/roles/updater/mod.rs b/pczt/src/roles/updater/mod.rs new file mode 100644 index 0000000000..0f688a1c9a --- /dev/null +++ b/pczt/src/roles/updater/mod.rs @@ -0,0 +1,69 @@ +use alloc::string::String; +use alloc::vec::Vec; + +use crate::{common::Global, Pczt}; + +#[cfg(feature = "orchard")] +mod orchard; +#[cfg(feature = "orchard")] +pub use orchard::OrchardError; + +#[cfg(feature = "sapling")] +mod sapling; +#[cfg(feature = "sapling")] +pub use sapling::SaplingError; + +#[cfg(feature = "transparent")] +mod transparent; +#[cfg(feature = "transparent")] +pub use transparent::TransparentError; + +pub struct Updater { + pczt: Pczt, +} + +impl Updater { + /// Instantiates the Updater role with the given PCZT. + pub fn new(pczt: Pczt) -> Self { + Self { pczt } + } + + /// Updates the global transaction details with information in the given closure. + pub fn update_global_with(self, f: F) -> Self + where + F: FnOnce(GlobalUpdater<'_>), + { + let Pczt { + mut global, + transparent, + sapling, + orchard, + } = self.pczt; + + f(GlobalUpdater(&mut global)); + + Self { + pczt: Pczt { + global, + transparent, + sapling, + orchard, + }, + } + } + + /// Finishes the Updater role, returning the updated PCZT. + pub fn finish(self) -> Pczt { + self.pczt + } +} + +/// An updater for a transparent PCZT output. +pub struct GlobalUpdater<'a>(&'a mut Global); + +impl GlobalUpdater<'_> { + /// Stores the given proprietary value at the given key. + pub fn set_proprietary(&mut self, key: String, value: Vec) { + self.0.proprietary.insert(key, value); + } +} diff --git a/pczt/src/roles/updater/orchard.rs b/pczt/src/roles/updater/orchard.rs new file mode 100644 index 0000000000..5701b1c486 --- /dev/null +++ b/pczt/src/roles/updater/orchard.rs @@ -0,0 +1,38 @@ +use orchard::pczt::{ParseError, Updater, UpdaterError}; + +use crate::Pczt; + +impl super::Updater { + /// Updates the Orchard bundle with information in the given closure. + pub fn update_orchard_with(self, f: F) -> Result + where + F: FnOnce(Updater<'_>) -> Result<(), UpdaterError>, + { + let Pczt { + global, + transparent, + sapling, + orchard, + } = self.pczt; + + let mut bundle = orchard.into_parsed().map_err(OrchardError::Parser)?; + + bundle.update_with(f).map_err(OrchardError::Updater)?; + + Ok(Self { + pczt: Pczt { + global, + transparent, + sapling, + orchard: crate::orchard::Bundle::serialize_from(bundle), + }, + }) + } +} + +/// Errors that can occur while updating the Orchard bundle of a PCZT. +#[derive(Debug)] +pub enum OrchardError { + Parser(ParseError), + Updater(UpdaterError), +} diff --git a/pczt/src/roles/updater/sapling.rs b/pczt/src/roles/updater/sapling.rs new file mode 100644 index 0000000000..685ee11135 --- /dev/null +++ b/pczt/src/roles/updater/sapling.rs @@ -0,0 +1,38 @@ +use sapling::pczt::{ParseError, Updater, UpdaterError}; + +use crate::Pczt; + +impl super::Updater { + /// Updates the Sapling bundle with information in the given closure. + pub fn update_sapling_with(self, f: F) -> Result + where + F: FnOnce(Updater<'_>) -> Result<(), UpdaterError>, + { + let Pczt { + global, + transparent, + sapling, + orchard, + } = self.pczt; + + let mut bundle = sapling.into_parsed().map_err(SaplingError::Parser)?; + + bundle.update_with(f).map_err(SaplingError::Updater)?; + + Ok(Self { + pczt: Pczt { + global, + transparent, + sapling: crate::sapling::Bundle::serialize_from(bundle), + orchard, + }, + }) + } +} + +/// Errors that can occur while updating the Sapling bundle of a PCZT. +#[derive(Debug)] +pub enum SaplingError { + Parser(ParseError), + Updater(UpdaterError), +} diff --git a/pczt/src/roles/updater/transparent.rs b/pczt/src/roles/updater/transparent.rs new file mode 100644 index 0000000000..6acac712dc --- /dev/null +++ b/pczt/src/roles/updater/transparent.rs @@ -0,0 +1,40 @@ +use transparent::pczt::{ParseError, Updater, UpdaterError}; + +use crate::Pczt; + +impl super::Updater { + /// Updates the transparent bundle with information in the given closure. + pub fn update_transparent_with(self, f: F) -> Result + where + F: FnOnce(Updater<'_>) -> Result<(), UpdaterError>, + { + let Pczt { + global, + transparent, + sapling, + orchard, + } = self.pczt; + + let mut bundle = transparent + .into_parsed() + .map_err(TransparentError::Parser)?; + + bundle.update_with(f).map_err(TransparentError::Updater)?; + + Ok(Self { + pczt: Pczt { + global, + transparent: crate::transparent::Bundle::serialize_from(bundle), + sapling, + orchard, + }, + }) + } +} + +/// Errors that can occur while updating the transparent bundle of a PCZT. +#[derive(Debug)] +pub enum TransparentError { + Parser(ParseError), + Updater(UpdaterError), +} diff --git a/pczt/src/roles/verifier/mod.rs b/pczt/src/roles/verifier/mod.rs new file mode 100644 index 0000000000..fe2a66fc87 --- /dev/null +++ b/pczt/src/roles/verifier/mod.rs @@ -0,0 +1,32 @@ +use crate::Pczt; + +#[cfg(feature = "orchard")] +mod orchard; +#[cfg(feature = "orchard")] +pub use orchard::OrchardError; + +#[cfg(feature = "sapling")] +mod sapling; +#[cfg(feature = "sapling")] +pub use sapling::SaplingError; + +#[cfg(feature = "transparent")] +mod transparent; +#[cfg(feature = "transparent")] +pub use transparent::TransparentError; + +pub struct Verifier { + pczt: Pczt, +} + +impl Verifier { + /// Instantiates the Verifier role with the given PCZT. + pub fn new(pczt: Pczt) -> Self { + Self { pczt } + } + + /// Finishes the Verifier role, returning the updated PCZT. + pub fn finish(self) -> Pczt { + self.pczt + } +} diff --git a/pczt/src/roles/verifier/orchard.rs b/pczt/src/roles/verifier/orchard.rs new file mode 100644 index 0000000000..5f5046a9de --- /dev/null +++ b/pczt/src/roles/verifier/orchard.rs @@ -0,0 +1,43 @@ +use crate::Pczt; + +impl super::Verifier { + /// Parses the Orchard bundle and then verifies it in the given closure. + pub fn with_orchard(self, f: F) -> Result> + where + F: FnOnce(&orchard::pczt::Bundle) -> Result<(), OrchardError>, + { + let Pczt { + global, + transparent, + sapling, + orchard, + } = self.pczt; + + let bundle = orchard.into_parsed().map_err(OrchardError::Parse)?; + + f(&bundle)?; + + Ok(Self { + pczt: Pczt { + global, + transparent, + sapling, + orchard: crate::orchard::Bundle::serialize_from(bundle), + }, + }) + } +} + +/// Errors that can occur while verifying the Orchard bundle of a PCZT. +#[derive(Debug)] +pub enum OrchardError { + Parse(orchard::pczt::ParseError), + Verify(orchard::pczt::VerifyError), + Custom(E), +} + +impl From for OrchardError { + fn from(e: orchard::pczt::VerifyError) -> Self { + OrchardError::Verify(e) + } +} diff --git a/pczt/src/roles/verifier/sapling.rs b/pczt/src/roles/verifier/sapling.rs new file mode 100644 index 0000000000..36415aace9 --- /dev/null +++ b/pczt/src/roles/verifier/sapling.rs @@ -0,0 +1,43 @@ +use crate::Pczt; + +impl super::Verifier { + /// Parses the Sapling bundle and then verifies it in the given closure. + pub fn with_sapling(self, f: F) -> Result> + where + F: FnOnce(&sapling::pczt::Bundle) -> Result<(), SaplingError>, + { + let Pczt { + global, + transparent, + sapling, + orchard, + } = self.pczt; + + let bundle = sapling.into_parsed().map_err(SaplingError::Parser)?; + + f(&bundle)?; + + Ok(Self { + pczt: Pczt { + global, + transparent, + sapling: crate::sapling::Bundle::serialize_from(bundle), + orchard, + }, + }) + } +} + +/// Errors that can occur while verifying the Sapling bundle of a PCZT. +#[derive(Debug)] +pub enum SaplingError { + Parser(sapling::pczt::ParseError), + Verifier(sapling::pczt::VerifyError), + Custom(E), +} + +impl From for SaplingError { + fn from(e: sapling::pczt::VerifyError) -> Self { + SaplingError::Verifier(e) + } +} diff --git a/pczt/src/roles/verifier/transparent.rs b/pczt/src/roles/verifier/transparent.rs new file mode 100644 index 0000000000..b63e9bf83c --- /dev/null +++ b/pczt/src/roles/verifier/transparent.rs @@ -0,0 +1,45 @@ +use crate::Pczt; + +impl super::Verifier { + /// Parses the Transparent bundle and then verifies it in the given closure. + pub fn with_transparent(self, f: F) -> Result> + where + F: FnOnce(&transparent::pczt::Bundle) -> Result<(), TransparentError>, + { + let Pczt { + global, + transparent, + sapling, + orchard, + } = self.pczt; + + let bundle = transparent + .into_parsed() + .map_err(TransparentError::Parser)?; + + f(&bundle)?; + + Ok(Self { + pczt: Pczt { + global, + transparent: crate::transparent::Bundle::serialize_from(bundle), + sapling, + orchard, + }, + }) + } +} + +/// Errors that can occur while verifying the Transparent bundle of a PCZT. +#[derive(Debug)] +pub enum TransparentError { + Parser(transparent::pczt::ParseError), + Verifier(transparent::pczt::VerifyError), + Custom(E), +} + +impl From for TransparentError { + fn from(e: transparent::pczt::VerifyError) -> Self { + TransparentError::Verifier(e) + } +} diff --git a/pczt/src/sapling.rs b/pczt/src/sapling.rs new file mode 100644 index 0000000000..edf52d3450 --- /dev/null +++ b/pczt/src/sapling.rs @@ -0,0 +1,600 @@ +use alloc::collections::BTreeMap; +use alloc::string::String; +use alloc::vec::Vec; +use core::cmp::Ordering; + +use getset::Getters; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +use crate::{ + common::{Global, Zip32Derivation}, + roles::combiner::{merge_map, merge_optional}, +}; + +const GROTH_PROOF_SIZE: usize = 48 + 96 + 48; + +/// PCZT fields that are specific to producing the transaction's Sapling bundle (if any). +#[derive(Clone, Debug, Serialize, Deserialize, Getters)] +pub struct Bundle { + #[getset(get = "pub")] + pub(crate) spends: Vec, + #[getset(get = "pub")] + pub(crate) outputs: Vec, + + /// The net value of Sapling spends minus outputs. + /// + /// This is initialized by the Creator, and updated by the Constructor as spends or + /// outputs are added to the PCZT. It enables per-spend and per-output values to be + /// redacted from the PCZT after they are no longer necessary. + #[getset(get = "pub")] + pub(crate) value_sum: i128, + + /// The Sapling anchor for this transaction. + /// + /// Set by the Creator. + #[getset(get = "pub")] + pub(crate) anchor: [u8; 32], + + /// The Sapling binding signature signing key. + /// + /// - This is `None` until it is set by the IO Finalizer. + /// - The Transaction Extractor uses this to produce the binding signature. + pub(crate) bsk: Option<[u8; 32]>, +} + +/// Information about a Sapling spend within a transaction. +#[serde_as] +#[derive(Clone, Debug, Serialize, Deserialize, Getters)] +pub struct Spend { + // + // SpendDescription effecting data. + // + // These are required fields that are part of the final transaction, and are filled in + // by the Constructor when adding an output. + // + #[getset(get = "pub")] + pub(crate) cv: [u8; 32], + #[getset(get = "pub")] + pub(crate) nullifier: [u8; 32], + #[getset(get = "pub")] + pub(crate) rk: [u8; 32], + + /// The Spend proof. + /// + /// This is set by the Prover. + #[serde_as(as = "Option<[_; GROTH_PROOF_SIZE]>")] + pub(crate) zkproof: Option<[u8; GROTH_PROOF_SIZE]>, + + /// The spend authorization signature. + /// + /// This is set by the Signer. + #[serde_as(as = "Option<[_; 64]>")] + pub(crate) spend_auth_sig: Option<[u8; 64]>, + + /// The [raw encoding] of the Sapling payment address that received the note being spent. + /// + /// - This is set by the Constructor. + /// - This is required by the Prover. + /// + /// [raw encoding]: https://zips.z.cash/protocol/protocol.pdf#saplingpaymentaddrencoding + #[serde_as(as = "Option<[_; 43]>")] + pub(crate) recipient: Option<[u8; 43]>, + + /// The value of the input being spent. + /// + /// This may be used by Signers to verify that the value matches `cv`, and to confirm + /// the values and change involved in the transaction. + /// + /// This exposes the input value to all participants. For Signers who don't need this + /// information, or after signatures have been applied, this can be redacted. + pub(crate) value: Option, + + /// The note commitment randomness. + /// + /// - This is set by the Constructor. It MUST NOT be set if the note has an `rseed` + /// (i.e. was created after [ZIP 212] activation). + /// - The Prover requires either this or `rseed`. + /// + /// [ZIP 212]: https://zips.z.cash/zip-0212 + pub(crate) rcm: Option<[u8; 32]>, + + /// The seed randomness for the note being spent. + /// + /// - This is set by the Constructor. It MUST NOT be set if the note has no `rseed` + /// (i.e. was created before [ZIP 212] activation). + /// - The Prover requires either this or `rcm`. + /// + /// [ZIP 212]: https://zips.z.cash/zip-0212 + pub(crate) rseed: Option<[u8; 32]>, + + /// The value commitment randomness. + /// + /// - This is set by the Constructor. + /// - The IO Finalizer compresses it into `bsk`. + /// - This is required by the Prover. + /// - This may be used by Signers to verify that the value correctly matches `cv`. + /// + /// This opens `cv` for all participants. For Signers who don't need this information, + /// or after proofs / signatures have been applied, this can be redacted. + pub(crate) rcv: Option<[u8; 32]>, + + /// The proof generation key `(ak, nsk)` corresponding to the recipient that received + /// the note being spent. + /// + /// - This is set by the Updater. + /// - This is required by the Prover. + pub(crate) proof_generation_key: Option<([u8; 32], [u8; 32])>, + + /// A witness from the note to the bundle's anchor. + /// + /// - This is set by the Updater. + /// - This is required by the Prover. + pub(crate) witness: Option<(u32, [[u8; 32]; 32])>, + + /// The spend authorization randomizer. + /// + /// - This is chosen by the Constructor. + /// - This is required by the Signer for creating `spend_auth_sig`, and may be used to + /// validate `rk`. + /// - After `zkproof` / `spend_auth_sig` has been set, this can be redacted. + pub(crate) alpha: Option<[u8; 32]>, + + /// The ZIP 32 derivation path at which the spending key can be found for the note + /// being spent. + pub(crate) zip32_derivation: Option, + + /// The spend authorizing key for this spent note, if it is a dummy note. + /// + /// - This is chosen by the Constructor. + /// - This is required by the IO Finalizer, and is cleared by it once used. + /// - Signers MUST reject PCZTs that contain `dummy_ask` values. + pub(crate) dummy_ask: Option<[u8; 32]>, + + /// Proprietary fields related to the note being spent. + #[getset(get = "pub")] + pub(crate) proprietary: BTreeMap>, +} + +/// Information about a Sapling output within a transaction. +#[serde_as] +#[derive(Clone, Debug, Serialize, Deserialize, Getters)] +pub struct Output { + // + // OutputDescription effecting data. + // + // These are required fields that are part of the final transaction, and are filled in + // by the Constructor when adding an output. + // + #[getset(get = "pub")] + pub(crate) cv: [u8; 32], + #[getset(get = "pub")] + pub(crate) cmu: [u8; 32], + #[getset(get = "pub")] + pub(crate) ephemeral_key: [u8; 32], + /// The encrypted note plaintext for the output. + /// + /// Encoded as a `Vec` because its length depends on the transaction version. + /// + /// Once we have [memo bundles], we will be able to set memos independently of + /// Outputs. For now, the Constructor sets both at the same time. + /// + /// [memo bundles]: https://zips.z.cash/zip-0231 + #[getset(get = "pub")] + pub(crate) enc_ciphertext: Vec, + /// The encrypted note plaintext for the output. + /// + /// Encoded as a `Vec` because its length depends on the transaction version. + #[getset(get = "pub")] + pub(crate) out_ciphertext: Vec, + + /// The Output proof. + /// + /// This is set by the Prover. + #[serde_as(as = "Option<[_; GROTH_PROOF_SIZE]>")] + pub(crate) zkproof: Option<[u8; GROTH_PROOF_SIZE]>, + + /// The [raw encoding] of the Sapling payment address that will receive the output. + /// + /// - This is set by the Constructor. + /// - This is required by the Prover. + /// + /// [raw encoding]: https://zips.z.cash/protocol/protocol.pdf#saplingpaymentaddrencoding + #[serde_as(as = "Option<[_; 43]>")] + #[getset(get = "pub")] + pub(crate) recipient: Option<[u8; 43]>, + + /// The value of the output. + /// + /// This may be used by Signers to verify that the value matches `cv`, and to confirm + /// the values and change involved in the transaction. + /// + /// This exposes the output value to all participants. For Signers who don't need this + /// information, or after signatures have been applied, this can be redacted. + #[getset(get = "pub")] + pub(crate) value: Option, + + /// The seed randomness for the output. + /// + /// - This is set by the Constructor. + /// - This is required by the Prover, instead of disclosing `shared_secret` to them. + #[getset(get = "pub")] + pub(crate) rseed: Option<[u8; 32]>, + + /// The value commitment randomness. + /// + /// - This is set by the Constructor. + /// - The IO Finalizer compresses it into `bsk`. + /// - This is required by the Prover. + /// - This may be used by Signers to verify that the value correctly matches `cv`. + /// + /// This opens `cv` for all participants. For Signers who don't need this information, + /// or after proofs / signatures have been applied, this can be redacted. + pub(crate) rcv: Option<[u8; 32]>, + + /// The `ock` value used to encrypt `out_ciphertext`. + /// + /// This enables Signers to verify that `out_ciphertext` is correctly encrypted. + /// + /// This may be `None` if the Constructor added the output using an OVK policy of + /// "None", to make the output unrecoverable from the chain by the sender. + pub(crate) ock: Option<[u8; 32]>, + + /// The ZIP 32 derivation path at which the spending key can be found for the output. + pub(crate) zip32_derivation: Option, + + /// The user-facing address to which this output is being sent, if any. + /// + /// - This is set by an Updater. + /// - Signers must parse this address (if present) and confirm that it contains + /// `recipient` (either directly, or e.g. as a receiver within a Unified Address). + #[getset(get = "pub")] + pub(crate) user_address: Option, + + /// Proprietary fields related to the note being spent. + #[getset(get = "pub")] + pub(crate) proprietary: BTreeMap>, +} + +impl Bundle { + /// Merges this bundle with another. + /// + /// Returns `None` if the bundles have conflicting data. + pub(crate) fn merge( + mut self, + other: Self, + self_global: &Global, + other_global: &Global, + ) -> Option { + // Destructure `other` to ensure we handle everything. + let Self { + mut spends, + mut outputs, + value_sum, + anchor, + bsk, + } = other; + + // If `bsk` is set on either bundle, the IO Finalizer has run, which means we + // cannot have differing numbers of spends or outputs, and the value balances must + // match. + match (self.bsk.as_mut(), bsk) { + (Some(lhs), Some(rhs)) if lhs != &rhs => return None, + (Some(_), _) | (_, Some(_)) + if self.spends.len() != spends.len() + || self.outputs.len() != outputs.len() + || self.value_sum != value_sum => + { + return None + } + // IO Finalizer has run, and neither bundle has excess spends or outputs. + (Some(_), _) | (_, Some(_)) => (), + // IO Finalizer has not run on either bundle. + (None, None) => { + let (spends_cmp_other, outputs_cmp_other) = match ( + self.spends.len().cmp(&spends.len()), + self.outputs.len().cmp(&outputs.len()), + ) { + // These cases require us to recalculate the value sum, which we can't + // do without a parsed bundle. + (Ordering::Less, Ordering::Greater) | (Ordering::Greater, Ordering::Less) => { + return None + } + // These cases mean that at least one of the two value sums is correct + // and we can use it directly. + (spends, outputs) => (spends, outputs), + }; + + match ( + self_global.shielded_modifiable(), + other_global.shielded_modifiable(), + spends_cmp_other, + ) { + // Fail if the merge would add spends to a non-modifiable bundle. + (false, _, Ordering::Less) | (_, false, Ordering::Greater) => return None, + // If the other bundle has more spends than us, move them over; these cannot + // conflict by construction. + (true, _, Ordering::Less) => { + self.spends.extend(spends.drain(self.spends.len()..)) + } + // Do nothing otherwise. + (_, _, Ordering::Equal) | (_, true, Ordering::Greater) => (), + } + + match ( + self_global.shielded_modifiable(), + other_global.shielded_modifiable(), + outputs_cmp_other, + ) { + // Fail if the merge would add outputs to a non-modifiable bundle. + (false, _, Ordering::Less) | (_, false, Ordering::Greater) => return None, + // If the other bundle has more outputs than us, move them over; these cannot + // conflict by construction. + (true, _, Ordering::Less) => { + self.outputs.extend(outputs.drain(self.outputs.len()..)) + } + // Do nothing otherwise. + (_, _, Ordering::Equal) | (_, true, Ordering::Greater) => (), + } + + if matches!(spends_cmp_other, Ordering::Less) + || matches!(outputs_cmp_other, Ordering::Less) + { + // We check below that the overlapping spends and outputs match. + // Assuming here that they will, we take the other bundle's value sum. + self.value_sum = value_sum; + } + } + } + + if self.anchor != anchor { + return None; + } + + // Leverage the early-exit behaviour of zip to confirm that the remaining data in + // the other bundle matches this one. + for (lhs, rhs) in self.spends.iter_mut().zip(spends.into_iter()) { + // Destructure `rhs` to ensure we handle everything. + let Spend { + cv, + nullifier, + rk, + zkproof, + spend_auth_sig, + recipient, + value, + rcm, + rseed, + rcv, + proof_generation_key, + witness, + alpha, + zip32_derivation, + dummy_ask, + proprietary, + } = rhs; + + if lhs.cv != cv || lhs.nullifier != nullifier || lhs.rk != rk { + return None; + } + + if !(merge_optional(&mut lhs.zkproof, zkproof) + && merge_optional(&mut lhs.spend_auth_sig, spend_auth_sig) + && merge_optional(&mut lhs.recipient, recipient) + && merge_optional(&mut lhs.value, value) + && merge_optional(&mut lhs.rcm, rcm) + && merge_optional(&mut lhs.rseed, rseed) + && merge_optional(&mut lhs.rcv, rcv) + && merge_optional(&mut lhs.proof_generation_key, proof_generation_key) + && merge_optional(&mut lhs.witness, witness) + && merge_optional(&mut lhs.alpha, alpha) + && merge_optional(&mut lhs.zip32_derivation, zip32_derivation) + && merge_optional(&mut lhs.dummy_ask, dummy_ask) + && merge_map(&mut lhs.proprietary, proprietary)) + { + return None; + } + } + + for (lhs, rhs) in self.outputs.iter_mut().zip(outputs.into_iter()) { + // Destructure `rhs` to ensure we handle everything. + let Output { + cv, + cmu, + ephemeral_key, + enc_ciphertext, + out_ciphertext, + zkproof, + recipient, + value, + rseed, + rcv, + ock, + zip32_derivation, + user_address, + proprietary, + } = rhs; + + if lhs.cv != cv + || lhs.cmu != cmu + || lhs.ephemeral_key != ephemeral_key + || lhs.enc_ciphertext != enc_ciphertext + || lhs.out_ciphertext != out_ciphertext + { + return None; + } + + if !(merge_optional(&mut lhs.zkproof, zkproof) + && merge_optional(&mut lhs.recipient, recipient) + && merge_optional(&mut lhs.value, value) + && merge_optional(&mut lhs.rseed, rseed) + && merge_optional(&mut lhs.rcv, rcv) + && merge_optional(&mut lhs.ock, ock) + && merge_optional(&mut lhs.zip32_derivation, zip32_derivation) + && merge_optional(&mut lhs.user_address, user_address) + && merge_map(&mut lhs.proprietary, proprietary)) + { + return None; + } + } + + Some(self) + } +} + +#[cfg(feature = "sapling")] +impl Bundle { + pub(crate) fn into_parsed(self) -> Result { + let spends = self + .spends + .into_iter() + .map(|spend| { + sapling::pczt::Spend::parse( + spend.cv, + spend.nullifier, + spend.rk, + spend.zkproof, + spend.spend_auth_sig, + spend.recipient, + spend.value, + spend.rcm, + spend.rseed, + spend.rcv, + spend.proof_generation_key, + spend.witness, + spend.alpha, + spend + .zip32_derivation + .map(|z| { + sapling::pczt::Zip32Derivation::parse( + z.seed_fingerprint, + z.derivation_path, + ) + }) + .transpose()?, + spend.dummy_ask, + spend.proprietary, + ) + }) + .collect::>()?; + + let outputs = self + .outputs + .into_iter() + .map(|output| { + sapling::pczt::Output::parse( + output.cv, + output.cmu, + output.ephemeral_key, + output.enc_ciphertext, + output.out_ciphertext, + output.zkproof, + output.recipient, + output.value, + output.rseed, + output.rcv, + output.ock, + output + .zip32_derivation + .map(|z| { + sapling::pczt::Zip32Derivation::parse( + z.seed_fingerprint, + z.derivation_path, + ) + }) + .transpose()?, + output.user_address, + output.proprietary, + ) + }) + .collect::>()?; + + sapling::pczt::Bundle::parse(spends, outputs, self.value_sum, self.anchor, self.bsk) + } + + pub(crate) fn serialize_from(bundle: sapling::pczt::Bundle) -> Self { + let spends = bundle + .spends() + .iter() + .map(|spend| { + let (rcm, rseed) = match spend.rseed() { + Some(sapling::Rseed::BeforeZip212(rcm)) => (Some(rcm.to_bytes()), None), + Some(sapling::Rseed::AfterZip212(rseed)) => (None, Some(*rseed)), + None => (None, None), + }; + + Spend { + cv: spend.cv().to_bytes(), + nullifier: spend.nullifier().0, + rk: (*spend.rk()).into(), + zkproof: *spend.zkproof(), + spend_auth_sig: spend.spend_auth_sig().map(|s| s.into()), + recipient: spend.recipient().map(|recipient| recipient.to_bytes()), + value: spend.value().map(|value| value.inner()), + rcm, + rseed, + rcv: spend.rcv().as_ref().map(|rcv| rcv.inner().to_bytes()), + proof_generation_key: spend + .proof_generation_key() + .as_ref() + .map(|key| (key.ak.to_bytes(), key.nsk.to_bytes())), + witness: spend.witness().as_ref().map(|witness| { + ( + u32::try_from(u64::from(witness.position())) + .expect("Sapling positions fit in u32"), + witness + .path_elems() + .iter() + .map(|node| node.to_bytes()) + .collect::>()[..] + .try_into() + .expect("path is length 32"), + ) + }), + alpha: spend.alpha().map(|alpha| alpha.to_bytes()), + zip32_derivation: spend.zip32_derivation().as_ref().map(|z| Zip32Derivation { + seed_fingerprint: *z.seed_fingerprint(), + derivation_path: z.derivation_path().iter().map(|i| i.index()).collect(), + }), + dummy_ask: spend + .dummy_ask() + .as_ref() + .map(|dummy_ask| dummy_ask.to_bytes()), + proprietary: spend.proprietary().clone(), + } + }) + .collect(); + + let outputs = bundle + .outputs() + .iter() + .map(|output| Output { + cv: output.cv().to_bytes(), + cmu: output.cmu().to_bytes(), + ephemeral_key: output.ephemeral_key().0, + enc_ciphertext: output.enc_ciphertext().to_vec(), + out_ciphertext: output.out_ciphertext().to_vec(), + zkproof: *output.zkproof(), + recipient: output.recipient().map(|recipient| recipient.to_bytes()), + value: output.value().map(|value| value.inner()), + rseed: *output.rseed(), + rcv: output.rcv().as_ref().map(|rcv| rcv.inner().to_bytes()), + ock: output.ock().as_ref().map(|ock| ock.0), + zip32_derivation: output.zip32_derivation().as_ref().map(|z| Zip32Derivation { + seed_fingerprint: *z.seed_fingerprint(), + derivation_path: z.derivation_path().iter().map(|i| i.index()).collect(), + }), + user_address: output.user_address().clone(), + proprietary: output.proprietary().clone(), + }) + .collect(); + + Self { + spends, + outputs, + value_sum: bundle.value_sum().to_raw(), + anchor: bundle.anchor().to_bytes(), + bsk: bundle.bsk().map(|bsk| bsk.into()), + } + } +} diff --git a/pczt/src/transparent.rs b/pczt/src/transparent.rs new file mode 100644 index 0000000000..2c3af6c626 --- /dev/null +++ b/pczt/src/transparent.rs @@ -0,0 +1,455 @@ +use alloc::collections::BTreeMap; +use alloc::string::String; +use alloc::vec::Vec; +use core::cmp::Ordering; + +use crate::{ + common::{Global, Zip32Derivation}, + roles::combiner::{merge_map, merge_optional}, +}; + +use getset::Getters; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +/// PCZT fields that are specific to producing the transaction's transparent bundle (if +/// any). +#[derive(Clone, Debug, Serialize, Deserialize, Getters)] +pub struct Bundle { + #[getset(get = "pub")] + pub(crate) inputs: Vec, + #[getset(get = "pub")] + pub(crate) outputs: Vec, +} + +#[serde_as] +#[derive(Clone, Debug, Serialize, Deserialize, Getters)] +pub struct Input { + // + // Transparent effecting data. + // + // These are required fields that are part of the final transaction, and are filled in + // by the Constructor when adding an output. + // + #[getset(get = "pub")] + pub(crate) prevout_txid: [u8; 32], + #[getset(get = "pub")] + pub(crate) prevout_index: u32, + + /// The sequence number of this input. + /// + /// - This is set by the Constructor. + /// - If omitted, the sequence number is assumed to be the final sequence number + /// (`0xffffffff`). + #[getset(get = "pub")] + pub(crate) sequence: Option, + + /// The minimum Unix timstamp that this input requires to be set as the transaction's + /// lock time. + /// + /// - This is set by the Constructor. + /// - This must be greater than or equal to 500000000. + pub(crate) required_time_lock_time: Option, + + /// The minimum block height that this input requires to be set as the transaction's + /// lock time. + /// + /// - This is set by the Constructor. + /// - This must be greater than 0 and less than 500000000. + pub(crate) required_height_lock_time: Option, + + /// A satisfying witness for the `script_pubkey` of the input being spent. + /// + /// This is set by the Spend Finalizer. + pub(crate) script_sig: Option>, + + // These are required by the Transaction Extractor, to derive the shielded sighash + // needed for computing the binding signatures. + #[getset(get = "pub")] + pub(crate) value: u64, + #[getset(get = "pub")] + pub(crate) script_pubkey: Vec, + + /// The script required to spend this output, if it is P2SH. + /// + /// Set to `None` if this is a P2PKH output. + pub(crate) redeem_script: Option>, + + /// A map from a pubkey to a signature created by it. + /// + /// - Each pubkey should appear in `script_pubkey` or `redeem_script`. + /// - Each entry is set by a Signer, and should contain an ECDSA signature that is + /// valid under the corresponding pubkey. + /// - These are required by the Spend Finalizer to assemble `script_sig`. + #[serde_as(as = "BTreeMap<[_; 33], _>")] + pub(crate) partial_signatures: BTreeMap<[u8; 33], Vec>, + + /// The sighash type to be used for this input. + /// + /// - Signers must use this sighash type to produce their signatures. Signers that + /// cannot produce signatures for this sighash type must not provide a signature. + /// - Spend Finalizers must fail to finalize inputs which have signatures not matching + /// this sighash type. + pub(crate) sighash_type: u8, + + /// A map from a pubkey to the BIP 32 derivation path at which its corresponding + /// spending key can be found. + /// + /// - The pubkeys should appear in `script_pubkey` or `redeem_script`. + /// - Each entry is set by an Updater. + /// - Individual entries may be required by a Signer. + /// - It is not required that the map include entries for all of the used pubkeys. + /// In particular, it is not possible to include entries for non-BIP-32 pubkeys. + #[serde_as(as = "BTreeMap<[_; 33], _>")] + pub(crate) bip32_derivation: BTreeMap<[u8; 33], Zip32Derivation>, + + /// Mappings of the form `key = RIPEMD160(value)`. + /// + /// - These may be used by the Signer to inspect parts of `script_pubkey` or + /// `redeem_script`. + pub(crate) ripemd160_preimages: BTreeMap<[u8; 20], Vec>, + + /// Mappings of the form `key = SHA256(value)`. + /// + /// - These may be used by the Signer to inspect parts of `script_pubkey` or + /// `redeem_script`. + pub(crate) sha256_preimages: BTreeMap<[u8; 32], Vec>, + + /// Mappings of the form `key = RIPEMD160(SHA256(value))`. + /// + /// - These may be used by the Signer to inspect parts of `script_pubkey` or + /// `redeem_script`. + pub(crate) hash160_preimages: BTreeMap<[u8; 20], Vec>, + + /// Mappings of the form `key = SHA256(SHA256(value))`. + /// + /// - These may be used by the Signer to inspect parts of `script_pubkey` or + /// `redeem_script`. + pub(crate) hash256_preimages: BTreeMap<[u8; 32], Vec>, + + /// Proprietary fields related to the note being spent. + #[getset(get = "pub")] + pub(crate) proprietary: BTreeMap>, +} + +#[serde_as] +#[derive(Clone, Debug, Serialize, Deserialize, Getters)] +pub struct Output { + // + // Transparent effecting data. + // + // These are required fields that are part of the final transaction, and are filled in + // by the Constructor when adding an output. + // + #[getset(get = "pub")] + pub(crate) value: u64, + #[getset(get = "pub")] + pub(crate) script_pubkey: Vec, + + /// The script required to spend this output, if it is P2SH. + /// + /// Set to `None` if this is a P2PKH output. + pub(crate) redeem_script: Option>, + + /// A map from a pubkey to the BIP 32 derivation path at which its corresponding + /// spending key can be found. + /// + /// - The pubkeys should appear in `script_pubkey` or `redeem_script`. + /// - Each entry is set by an Updater. + /// - Individual entries may be required by a Signer. + /// - It is not required that the map include entries for all of the used pubkeys. + /// In particular, it is not possible to include entries for non-BIP-32 pubkeys. + #[serde_as(as = "BTreeMap<[_; 33], _>")] + pub(crate) bip32_derivation: BTreeMap<[u8; 33], Zip32Derivation>, + + /// The user-facing address to which this output is being sent, if any. + /// + /// - This is set by an Updater. + /// - Signers must parse this address (if present) and confirm that it contains + /// `recipient` (either directly, or e.g. as a receiver within a Unified Address). + #[getset(get = "pub")] + pub(crate) user_address: Option, + + /// Proprietary fields related to the note being spent. + #[getset(get = "pub")] + pub(crate) proprietary: BTreeMap>, +} + +impl Bundle { + /// Merges this bundle with another. + /// + /// Returns `None` if the bundles have conflicting data. + pub(crate) fn merge( + mut self, + other: Self, + self_global: &Global, + other_global: &Global, + ) -> Option { + // Destructure `other` to ensure we handle everything. + let Self { + mut inputs, + mut outputs, + } = other; + + match ( + self_global.inputs_modifiable(), + other_global.inputs_modifiable(), + self.inputs.len().cmp(&inputs.len()), + ) { + // Fail if the merge would add inputs to a non-modifiable bundle. + (false, _, Ordering::Less) | (_, false, Ordering::Greater) => return None, + // If the other bundle has more inputs than us, move them over; these cannot + // conflict by construction. + (true, _, Ordering::Less) => self.inputs.extend(inputs.drain(self.inputs.len()..)), + // Do nothing otherwise. + (_, _, Ordering::Equal) | (_, true, Ordering::Greater) => (), + } + + match ( + self_global.outputs_modifiable(), + other_global.outputs_modifiable(), + self.outputs.len().cmp(&outputs.len()), + ) { + // Fail if the merge would add outputs to a non-modifiable bundle. + (false, _, Ordering::Less) | (_, false, Ordering::Greater) => return None, + // If the other bundle has more outputs than us, move them over; these cannot + // conflict by construction. + (true, _, Ordering::Less) => self.outputs.extend(outputs.drain(self.outputs.len()..)), + // Do nothing otherwise. + (_, _, Ordering::Equal) | (_, true, Ordering::Greater) => (), + } + + // Leverage the early-exit behaviour of zip to confirm that the remaining data in + // the other bundle matches this one. + for (lhs, rhs) in self.inputs.iter_mut().zip(inputs.into_iter()) { + // Destructure `rhs` to ensure we handle everything. + let Input { + prevout_txid, + prevout_index, + sequence, + required_time_lock_time, + required_height_lock_time, + script_sig, + value, + script_pubkey, + redeem_script, + partial_signatures, + sighash_type, + bip32_derivation, + ripemd160_preimages, + sha256_preimages, + hash160_preimages, + hash256_preimages, + proprietary, + } = rhs; + + if lhs.prevout_txid != prevout_txid + || lhs.prevout_index != prevout_index + || lhs.value != value + || lhs.script_pubkey != script_pubkey + || lhs.sighash_type != sighash_type + { + return None; + } + + if !(merge_optional(&mut lhs.sequence, sequence) + && merge_optional(&mut lhs.required_time_lock_time, required_time_lock_time) + && merge_optional( + &mut lhs.required_height_lock_time, + required_height_lock_time, + ) + && merge_optional(&mut lhs.script_sig, script_sig) + && merge_optional(&mut lhs.redeem_script, redeem_script) + && merge_map(&mut lhs.partial_signatures, partial_signatures) + && merge_map(&mut lhs.bip32_derivation, bip32_derivation) + && merge_map(&mut lhs.ripemd160_preimages, ripemd160_preimages) + && merge_map(&mut lhs.sha256_preimages, sha256_preimages) + && merge_map(&mut lhs.hash160_preimages, hash160_preimages) + && merge_map(&mut lhs.hash256_preimages, hash256_preimages) + && merge_map(&mut lhs.proprietary, proprietary)) + { + return None; + } + } + + for (lhs, rhs) in self.outputs.iter_mut().zip(outputs.into_iter()) { + // Destructure `rhs` to ensure we handle everything. + let Output { + value, + script_pubkey, + redeem_script, + bip32_derivation, + user_address, + proprietary, + } = rhs; + + if lhs.value != value || lhs.script_pubkey != script_pubkey { + return None; + } + + if !(merge_optional(&mut lhs.redeem_script, redeem_script) + && merge_map(&mut lhs.bip32_derivation, bip32_derivation) + && merge_optional(&mut lhs.user_address, user_address) + && merge_map(&mut lhs.proprietary, proprietary)) + { + return None; + } + } + + Some(self) + } +} + +#[cfg(feature = "transparent")] +impl Bundle { + pub(crate) fn into_parsed( + self, + ) -> Result { + let inputs = self + .inputs + .into_iter() + .map(|input| { + transparent::pczt::Input::parse( + input.prevout_txid, + input.prevout_index, + input.sequence, + input.required_time_lock_time, + input.required_height_lock_time, + input.script_sig, + input.value, + input.script_pubkey, + input.redeem_script, + input.partial_signatures, + input.sighash_type, + input + .bip32_derivation + .into_iter() + .map(|(k, v)| { + transparent::pczt::Bip32Derivation::parse( + v.seed_fingerprint, + v.derivation_path, + ) + .map(|v| (k, v)) + }) + .collect::>()?, + input.ripemd160_preimages, + input.sha256_preimages, + input.hash160_preimages, + input.hash256_preimages, + input.proprietary, + ) + }) + .collect::>()?; + + let outputs = self + .outputs + .into_iter() + .map(|output| { + transparent::pczt::Output::parse( + output.value, + output.script_pubkey, + output.redeem_script, + output + .bip32_derivation + .into_iter() + .map(|(k, v)| { + transparent::pczt::Bip32Derivation::parse( + v.seed_fingerprint, + v.derivation_path, + ) + .map(|v| (k, v)) + }) + .collect::>()?, + output.user_address, + output.proprietary, + ) + }) + .collect::>()?; + + transparent::pczt::Bundle::parse(inputs, outputs) + } + + pub(crate) fn serialize_from(bundle: transparent::pczt::Bundle) -> Self { + let inputs = bundle + .inputs() + .iter() + .map(|input| Input { + prevout_txid: (*input.prevout_txid()).into(), + prevout_index: *input.prevout_index(), + sequence: *input.sequence(), + required_time_lock_time: *input.required_time_lock_time(), + required_height_lock_time: *input.required_height_lock_time(), + script_sig: input + .script_sig() + .as_ref() + .map(|script_sig| script_sig.0.clone()), + value: input.value().into_u64(), + script_pubkey: input.script_pubkey().0.clone(), + redeem_script: input + .redeem_script() + .as_ref() + .map(|redeem_script| redeem_script.0.clone()), + partial_signatures: input.partial_signatures().clone(), + sighash_type: input.sighash_type().encode(), + bip32_derivation: input + .bip32_derivation() + .iter() + .map(|(k, v)| { + ( + *k, + Zip32Derivation { + seed_fingerprint: *v.seed_fingerprint(), + derivation_path: v + .derivation_path() + .iter() + .copied() + .map(u32::from) + .collect(), + }, + ) + }) + .collect(), + ripemd160_preimages: input.ripemd160_preimages().clone(), + sha256_preimages: input.sha256_preimages().clone(), + hash160_preimages: input.hash160_preimages().clone(), + hash256_preimages: input.hash256_preimages().clone(), + proprietary: input.proprietary().clone(), + }) + .collect(); + + let outputs = bundle + .outputs() + .iter() + .map(|output| Output { + value: output.value().into_u64(), + script_pubkey: output.script_pubkey().0.clone(), + redeem_script: output + .redeem_script() + .as_ref() + .map(|redeem_script| redeem_script.0.clone()), + bip32_derivation: output + .bip32_derivation() + .iter() + .map(|(k, v)| { + ( + *k, + Zip32Derivation { + seed_fingerprint: *v.seed_fingerprint(), + derivation_path: v + .derivation_path() + .iter() + .copied() + .map(u32::from) + .collect(), + }, + ) + }) + .collect(), + user_address: output.user_address().clone(), + proprietary: output.proprietary().clone(), + }) + .collect(); + + Self { inputs, outputs } + } +} diff --git a/pczt/tests/end_to_end.rs b/pczt/tests/end_to_end.rs new file mode 100644 index 0000000000..1a8965d113 --- /dev/null +++ b/pczt/tests/end_to_end.rs @@ -0,0 +1,418 @@ +use rand_core::OsRng; +use std::sync::OnceLock; + +use ::transparent::{ + bundle as transparent, + keys::{AccountPrivKey, IncomingViewingKey}, +}; +use orchard::tree::MerkleHashOrchard; +use pczt::{ + roles::{ + combiner::Combiner, creator::Creator, io_finalizer::IoFinalizer, prover::Prover, + signer::Signer, spend_finalizer::SpendFinalizer, tx_extractor::TransactionExtractor, + updater::Updater, + }, + Pczt, +}; +use shardtree::{store::memory::MemoryShardStore, ShardTree}; +use zcash_note_encryption::try_note_decryption; +use zcash_primitives::transaction::{ + builder::{BuildConfig, Builder, PcztResult}, + fees::zip317, +}; +use zcash_proofs::prover::LocalTxProver; +use zcash_protocol::{consensus::MainNetwork, memo::MemoBytes, value::Zatoshis}; + +static ORCHARD_PROVING_KEY: OnceLock = OnceLock::new(); + +fn orchard_proving_key() -> &'static orchard::circuit::ProvingKey { + ORCHARD_PROVING_KEY.get_or_init(orchard::circuit::ProvingKey::build) +} + +fn check_round_trip(pczt: &Pczt) { + let encoded = pczt.serialize(); + assert_eq!(encoded, Pczt::parse(&encoded).unwrap().serialize()); +} + +#[test] +fn transparent_to_orchard() { + let params = MainNetwork; + let rng = OsRng; + + // Create a transparent account to send funds from. + let transparent_account_sk = + AccountPrivKey::from_seed(¶ms, &[1; 32], zip32::AccountId::ZERO).unwrap(); + let (transparent_addr, address_index) = transparent_account_sk + .to_account_pubkey() + .derive_external_ivk() + .unwrap() + .default_address(); + let transparent_sk = transparent_account_sk + .derive_external_secret_key(address_index) + .unwrap(); + let secp = secp256k1::Secp256k1::signing_only(); + let transparent_pubkey = transparent_sk.public_key(&secp); + + // Create an Orchard account to receive funds. + let orchard_sk = orchard::keys::SpendingKey::from_bytes([0; 32]).unwrap(); + let orchard_fvk = orchard::keys::FullViewingKey::from(&orchard_sk); + let orchard_ovk = orchard_fvk.to_ovk(orchard::keys::Scope::External); + let recipient = orchard_fvk.address_at(0u32, orchard::keys::Scope::External); + + // Pretend we already have a transparent coin. + let utxo = transparent::OutPoint::fake(); + let coin = transparent::TxOut { + value: Zatoshis::const_from_u64(1_000_000), + script_pubkey: transparent_addr.script(), + }; + + // Create the transaction's I/O. + let mut builder = Builder::new( + params, + 10_000_000.into(), + BuildConfig::Standard { + sapling_anchor: None, + orchard_anchor: Some(orchard::Anchor::empty_tree()), + }, + ); + builder + .add_transparent_input(transparent_pubkey, utxo, coin) + .unwrap(); + builder + .add_orchard_output::( + Some(orchard_ovk), + recipient, + 100_000, + MemoBytes::empty(), + ) + .unwrap(); + builder + .add_orchard_output::( + Some(orchard_fvk.to_ovk(zip32::Scope::Internal)), + orchard_fvk.address_at(0u32, orchard::keys::Scope::Internal), + 885_000, + MemoBytes::empty(), + ) + .unwrap(); + let PcztResult { pczt_parts, .. } = builder + .build_for_pczt(rng, &zip317::FeeRule::standard()) + .unwrap(); + + // Create the base PCZT. + let pczt = Creator::build_from_parts(pczt_parts).unwrap(); + check_round_trip(&pczt); + + // Finalize the I/O. + let pczt = IoFinalizer::new(pczt).finalize_io().unwrap(); + check_round_trip(&pczt); + + // Create proofs. + let pczt = Prover::new(pczt) + .create_orchard_proof(orchard_proving_key()) + .unwrap() + .finish(); + check_round_trip(&pczt); + + // Apply signatures. + let mut signer = Signer::new(pczt).unwrap(); + signer.sign_transparent(0, &transparent_sk).unwrap(); + let pczt = signer.finish(); + check_round_trip(&pczt); + + // Finalize spends. + let pczt = SpendFinalizer::new(pczt).finalize_spends().unwrap(); + check_round_trip(&pczt); + + // We should now be able to extract the fully authorized transaction. + let tx = TransactionExtractor::new(pczt).extract().unwrap(); + + assert_eq!(u32::from(tx.expiry_height()), 10_000_040); + + // TODO: Validate the transaction. +} + +#[test] +fn sapling_to_orchard() { + let mut rng = OsRng; + + // Create a Sapling account to send funds from. + let sapling_extsk = sapling::zip32::ExtendedSpendingKey::master(&[1; 32]); + let sapling_dfvk = sapling_extsk.to_diversifiable_full_viewing_key(); + let sapling_internal_dfvk = sapling_extsk + .derive_internal() + .to_diversifiable_full_viewing_key(); + let sapling_recipient = sapling_dfvk.default_address().1; + + // Create an Orchard account to receive funds. + let orchard_sk = orchard::keys::SpendingKey::from_bytes([0; 32]).unwrap(); + let orchard_fvk = orchard::keys::FullViewingKey::from(&orchard_sk); + let recipient = orchard_fvk.address_at(0u32, orchard::keys::Scope::External); + + // Pretend we already received a note. + let value = sapling::value::NoteValue::from_raw(1_000_000); + let note = { + let mut sapling_builder = sapling::builder::Builder::new( + sapling::note_encryption::Zip212Enforcement::On, + sapling::builder::BundleType::DEFAULT, + sapling::Anchor::empty_tree(), + ); + sapling_builder + .add_output(None, sapling_recipient, value, None) + .unwrap(); + let (bundle, meta) = sapling_builder + .build::(&[], &mut rng) + .unwrap() + .unwrap(); + let output = bundle + .shielded_outputs() + .get(meta.output_index(0).unwrap()) + .unwrap(); + let domain = sapling::note_encryption::SaplingDomain::new( + sapling::note_encryption::Zip212Enforcement::On, + ); + let (note, _, _) = + try_note_decryption(&domain, &sapling_dfvk.to_external_ivk().prepare(), output) + .unwrap(); + note + }; + + // Use the tree with a single leaf. + let (anchor, merkle_path) = { + let cmu = note.cmu(); + let leaf = sapling::Node::from_cmu(&cmu); + let mut tree = + ShardTree::<_, 32, 16>::new(MemoryShardStore::::empty(), 100); + tree.append(leaf, incrementalmerkletree::Retention::Marked) + .unwrap(); + tree.checkpoint(9_999_999).unwrap(); + let position = 0.into(); + let merkle_path = tree + .witness_at_checkpoint_depth(position, 0) + .unwrap() + .unwrap(); + let anchor = merkle_path.root(leaf); + (anchor.into(), merkle_path) + }; + + // Build the Orchard bundle we'll be using. + let mut builder = Builder::new( + MainNetwork, + 10_000_000.into(), + BuildConfig::Standard { + sapling_anchor: Some(anchor), + orchard_anchor: Some(orchard::Anchor::empty_tree()), + }, + ); + builder + .add_sapling_spend::(sapling_dfvk.fvk().clone(), note, merkle_path) + .unwrap(); + builder + .add_orchard_output::( + Some(sapling_dfvk.to_ovk(zip32::Scope::External).0.into()), + recipient, + 100_000, + MemoBytes::empty(), + ) + .unwrap(); + builder + .add_sapling_output::( + Some(sapling_dfvk.to_ovk(zip32::Scope::Internal)), + sapling_internal_dfvk.find_address(0u32.into()).unwrap().1, + Zatoshis::const_from_u64(880_000), + MemoBytes::empty(), + ) + .unwrap(); + let PcztResult { + pczt_parts, + sapling_meta, + .. + } = builder + .build_for_pczt(OsRng, &zip317::FeeRule::standard()) + .unwrap(); + + // Create the base PCZT. + let pczt = Creator::build_from_parts(pczt_parts).unwrap(); + check_round_trip(&pczt); + + // Finalize the I/O. + let pczt = IoFinalizer::new(pczt).finalize_io().unwrap(); + check_round_trip(&pczt); + + // Update the Sapling bundle with its proof generation key. + let index = sapling_meta.spend_index(0).unwrap(); + let pczt = Updater::new(pczt) + .update_sapling_with(|mut updater| { + updater.update_spend_with(index, |mut spend_updater| { + spend_updater.set_proof_generation_key(sapling_extsk.expsk.proof_generation_key()) + }) + }) + .unwrap() + .finish(); + + // To test the Combiner, we will create the Sapling proofs, Sapling signatures, and + // Orchard proof "in parallel". + + // Create Sapling proofs. + let sapling_prover = LocalTxProver::bundled(); + let pczt_with_sapling_proofs = Prover::new(pczt.clone()) + .create_sapling_proofs(&sapling_prover, &sapling_prover) + .unwrap() + .finish(); + check_round_trip(&pczt_with_sapling_proofs); + + // Create Orchard proof. + let pczt_with_orchard_proof = Prover::new(pczt.clone()) + .create_orchard_proof(orchard_proving_key()) + .unwrap() + .finish(); + check_round_trip(&pczt_with_orchard_proof); + + // Pass the PCZT to be signed through a serialization cycle to ensure we don't lose + // any information. This emulates passing it to another device. + let pczt = Pczt::parse(&pczt.serialize()).unwrap(); + + // Apply signatures. + let mut signer = Signer::new(pczt).unwrap(); + signer + .sign_sapling(index, &sapling_extsk.expsk.ask) + .unwrap(); + let pczt_with_sapling_signatures = signer.finish(); + check_round_trip(&pczt_with_sapling_signatures); + + // Emulate passing the signed PCZT back to the first device. + let pczt_with_sapling_signatures = + Pczt::parse(&pczt_with_sapling_signatures.serialize()).unwrap(); + + // Combine the three PCZTs into one. + let pczt = Combiner::new(vec![ + pczt_with_sapling_proofs, + pczt_with_orchard_proof, + pczt_with_sapling_signatures, + ]) + .combine() + .unwrap(); + check_round_trip(&pczt); + + // We should now be able to extract the fully authorized transaction. + let (spend_vk, output_vk) = sapling_prover.verifying_keys(); + let tx = TransactionExtractor::new(pczt) + .with_sapling(&spend_vk, &output_vk) + .extract() + .unwrap(); + + assert_eq!(u32::from(tx.expiry_height()), 10_000_040); +} + +#[test] +fn orchard_to_orchard() { + let mut rng = OsRng; + + // Create an Orchard account to receive funds. + let orchard_sk = orchard::keys::SpendingKey::from_bytes([0; 32]).unwrap(); + let orchard_ask = orchard::keys::SpendAuthorizingKey::from(&orchard_sk); + let orchard_fvk = orchard::keys::FullViewingKey::from(&orchard_sk); + let orchard_ivk = orchard_fvk.to_ivk(orchard::keys::Scope::External); + let orchard_ovk = orchard_fvk.to_ovk(orchard::keys::Scope::External); + let recipient = orchard_fvk.address_at(0u32, orchard::keys::Scope::External); + + // Pretend we already received a note. + let value = orchard::value::NoteValue::from_raw(1_000_000); + let note = { + let mut orchard_builder = orchard::builder::Builder::new( + orchard::builder::BundleType::DEFAULT, + orchard::Anchor::empty_tree(), + ); + orchard_builder + .add_output(None, recipient, value, None) + .unwrap(); + let (bundle, meta) = orchard_builder.build::(&mut rng).unwrap().unwrap(); + let action = bundle + .actions() + .get(meta.output_action_index(0).unwrap()) + .unwrap(); + let domain = orchard::note_encryption::OrchardDomain::for_action(action); + let (note, _, _) = try_note_decryption(&domain, &orchard_ivk.prepare(), action).unwrap(); + note + }; + + // Use the tree with a single leaf. + let (anchor, merkle_path) = { + let cmx: orchard::note::ExtractedNoteCommitment = note.commitment().into(); + let leaf = MerkleHashOrchard::from_cmx(&cmx); + let mut tree = + ShardTree::<_, 32, 16>::new(MemoryShardStore::::empty(), 100); + tree.append(leaf, incrementalmerkletree::Retention::Marked) + .unwrap(); + tree.checkpoint(9_999_999).unwrap(); + let position = 0.into(); + let merkle_path = tree + .witness_at_checkpoint_depth(position, 0) + .unwrap() + .unwrap(); + let anchor = merkle_path.root(leaf); + (anchor.into(), merkle_path.into()) + }; + + // Build the Orchard bundle we'll be using. + let mut builder = Builder::new( + MainNetwork, + 10_000_000.into(), + BuildConfig::Standard { + sapling_anchor: None, + orchard_anchor: Some(anchor), + }, + ); + builder + .add_orchard_spend::(orchard_fvk.clone(), note, merkle_path) + .unwrap(); + builder + .add_orchard_output::( + Some(orchard_ovk), + recipient, + 100_000, + MemoBytes::empty(), + ) + .unwrap(); + builder + .add_orchard_output::( + Some(orchard_fvk.to_ovk(zip32::Scope::Internal)), + orchard_fvk.address_at(0u32, orchard::keys::Scope::Internal), + 890_000, + MemoBytes::empty(), + ) + .unwrap(); + let PcztResult { + pczt_parts, + orchard_meta, + .. + } = builder + .build_for_pczt(OsRng, &zip317::FeeRule::standard()) + .unwrap(); + + // Create the base PCZT. + let pczt = Creator::build_from_parts(pczt_parts).unwrap(); + check_round_trip(&pczt); + + // Finalize the I/O. + let pczt = IoFinalizer::new(pczt).finalize_io().unwrap(); + check_round_trip(&pczt); + + // Create proofs. + let pczt = Prover::new(pczt) + .create_orchard_proof(orchard_proving_key()) + .unwrap() + .finish(); + check_round_trip(&pczt); + + // Apply signatures. + let index = orchard_meta.spend_action_index(0).unwrap(); + let mut signer = Signer::new(pczt).unwrap(); + signer.sign_orchard(index, &orchard_ask).unwrap(); + let pczt = signer.finish(); + check_round_trip(&pczt); + + // We should now be able to extract the fully authorized transaction. + let tx = TransactionExtractor::new(pczt).extract().unwrap(); + + assert_eq!(u32::from(tx.expiry_height()), 10_000_040); +} diff --git a/rust-toolchain b/rust-toolchain deleted file mode 100644 index ba0a719118..0000000000 --- a/rust-toolchain +++ /dev/null @@ -1 +0,0 @@ -1.51.0 diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000000..95c5b61b4f --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.81.0" +components = [ "clippy", "rustfmt" ] diff --git a/supply-chain/audits.toml b/supply-chain/audits.toml new file mode 100644 index 0000000000..6e035db535 --- /dev/null +++ b/supply-chain/audits.toml @@ -0,0 +1,1094 @@ + +# cargo-vet audits file + +[criteria.crypto-reviewed] +description = "The cryptographic code in this crate has been reviewed for correctness by a member of a designated set of cryptography experts within the project." + +[criteria.license-reviewed] +description = "The license of this crate has been reviewed for compatibility with its usage in this repository." + +[[audits.ambassador]] +who = "Kris Nuttycombe " +criteria = "safe-to-deploy" +version = "0.4.1" +notes = "Crate uses no unsafe code and the macros introduced by this crate generate the expected trait implementations without introducing additional unexpected operations." + +[[audits.anyhow]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "1.0.82 -> 1.0.83" + +[[audits.async-trait]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.1.78 -> 0.1.80" + +[[audits.async-trait]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.1.80 -> 0.1.81" +notes = "Changes to generated code look fine." + +[[audits.autocfg]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "1.2.0 -> 1.3.0" + +[[audits.bip32]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +version = "0.5.1" +notes = """ +- Crate has no unsafe code, and sets `#![forbid(unsafe_code)]`. +- Crate has no powerful imports. Only filesystem acces is via `include_str!`, and is safe. +""" + +[[audits.bytemuck]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-run" +delta = "1.15.0 -> 1.16.0" + +[[audits.cc]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "1.0.94 -> 1.0.97" + +[[audits.ciborium]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-run" +delta = "0.2.1 -> 0.2.2" + +[[audits.ciborium-io]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-run" +delta = "0.2.1 -> 0.2.2" + +[[audits.ciborium-ll]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-run" +delta = "0.2.1 -> 0.2.2" + +[[audits.clap]] +who = "Jack Grigg " +criteria = "safe-to-run" +delta = "4.4.14 -> 4.4.18" + +[[audits.clap_builder]] +who = "Jack Grigg " +criteria = "safe-to-run" +delta = "4.5.0 -> 4.4.18" + +[[audits.cpp_demangle]] +who = "Kris Nuttycombe " +criteria = "safe-to-run" +delta = "0.4.3 -> 0.4.4" +notes = "No added unsafe code; adds support for additional c++23 types." + +[[audits.darling]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.20.9 -> 0.20.10" + +[[audits.darling_core]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.20.9 -> 0.20.10" + +[[audits.darling_macro]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.20.9 -> 0.20.10" + +[[audits.either]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.11.0 -> 1.13.0" + +[[audits.errno]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.3.8 -> 0.3.9" + +[[audits.fastrand]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "2.0.2 -> 2.1.0" +notes = """ +As noted in the changelog, this version produces different output for a given seed. +The documentation did not mention stability. It is possible that some uses relying on +determinism across the update would be broken. + +The new constants do appear to match WyRand v4.2 (modulo ordering issues that I have not checked): +https://github.com/wangyi-fudan/wyhash/blob/408620b6d12b7d667b3dd6ae39b7929a39e8fa05/wyhash.h#L145 +I have no way to check whether these constants are an improvement or not. +""" + +[[audits.futures]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.28 -> 0.3.30" +notes = "Only sub-crate updates and corresponding changes to tests." + +[[audits.futures-executor]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.28 -> 0.3.30" + +[[audits.futures-io]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.28 -> 0.3.30" + +[[audits.futures-macro]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.28 -> 0.3.29" + +[[audits.futures-macro]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.3.29 -> 0.3.30" + +[[audits.futures-sink]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.3.29 -> 0.3.30" + +[[audits.getset]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +version = "0.1.3" +notes = """ +Does what it says on the tin. The proc macro generates unsurprising and obvious +code, and does not produce unsafe code or access any imports. +""" + +[[audits.h2]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.3.21 -> 0.3.26" + +[[audits.h2]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.26 -> 0.4.5" + +[[audits.half]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-run" +delta = "1.8.2 -> 2.2.1" +notes = """ +All new uses of unsafe are either just accessing bit representations, or plausibly reasonable uses of intrinsics. I have not checked safety +requirements on the latter. +""" + +[[audits.hashbrown]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.14.2 -> 0.14.5" +notes = "I did not thoroughly check the safety argument for fold_impl, but it at least seems to be well documented." + +[[audits.home]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.5.5 -> 0.5.9" +notes = """ +`unsafe` changes are to switch Windows logic from `SHGetFolderPathW` to +`SHGetKnownFolderPath`. I checked that the parameters and return values were +being handled correctly per the Windows documentation. +""" + +[[audits.http-body]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.0 -> 1.0.1" + +[[audits.http-body-util]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.1.0 -> 0.1.2" +notes = "New uses of pin_project! look fine." + +[[audits.hyper-timeout]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.4.1 -> 0.5.1" +notes = "New uses of pin_project! look fine." + +[[audits.hyper-util]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.1.5 -> 0.1.6" + +[[audits.inferno]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-run" +delta = "0.11.17 -> 0.11.19" + +[[audits.inferno]] +who = "Kris Nuttycombe " +criteria = "safe-to-run" +delta = "0.11.19 -> 0.11.21" +notes = "No added unsafe code." + +[[audits.is-terminal]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-run" +delta = "0.4.9 -> 0.4.12" + +[[audits.js-sys]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.3.65 -> 0.3.66" + +[[audits.lock_api]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-run" +delta = "0.4.11 -> 0.4.12" + +[[audits.memchr]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "2.7.2 -> 2.7.4" + +[[audits.memmap2]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.9.3 -> 0.9.4" + +[[audits.minreq]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "2.11.0 -> 2.11.2" + +[[audits.minreq]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "2.11.2 -> 2.12.0" + +[[audits.nonempty]] +who = "Kris Nuttycombe " +criteria = "safe-to-deploy" +version = "0.11.0" +notes = """ +Additional use of `unsafe` to wrap `NonZeroUsize::new_unchecked`; in both cases +the argument to this method is ` + 1`; in general this +is safe with the exception that if an existing `Vec` has length or capacity +`usize::MAX` this could wrap into zero; it would be better to use the safe +operation and then `expect` to generate a panic, rather than risk undefined +behavior. + +Additions are: +- no_std support +- sorting +- `nonzero` module (just wrappers +- `serde` support +- `nonempty macro` (trivial, verified safe) +""" + +[[audits.num-bigint]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.4.4 -> 0.4.5" +notes = "New uses of unsafe look reasonable." + +[[audits.num_enum]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.7.0 -> 0.7.2" + +[[audits.num_enum_derive]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.7.0 -> 0.7.2" + +[[audits.oorandom]] +who = "Jack Grigg " +criteria = "safe-to-run" +delta = "11.1.3 -> 11.1.4" + +[[audits.parking_lot]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-run" +delta = "0.12.1 -> 0.12.2" + +[[audits.parking_lot]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.12.2 -> 0.12.3" + +[[audits.parking_lot_core]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-run" +delta = "0.9.9 -> 0.9.10" + +[[audits.pczt]] +who = "Kris Nuttycombe " +criteria = "safe-to-deploy" +version = "0.0.0" +notes = "Initial empty crate release." + +[[audits.pin-project-internal]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "1.1.3 -> 1.1.5" + +[[audits.pkg-config]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.3.29 -> 0.3.30" + +[[audits.prettyplease]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.2.15 -> 0.2.20" + +[[audits.proc-macro2]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "1.0.81 -> 1.0.82" + +[[audits.proptest]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "1.3.1 -> 1.4.0" + +[[audits.prost]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.12.1 -> 0.12.3" + +[[audits.prost]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.13.1 -> 0.13.4" +notes = """ +- The new `unsafe` block in `encoded_len_varint` has correct safety documentation. +- The other changes to `unsafe` code are a move of existing `unsafe` code. +""" + +[[audits.prost-build]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.12.1 -> 0.12.3" + +[[audits.prost-build]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.13.1 -> 0.13.4" +notes = """ +- Changes to generated code make sense. +- Changes to `protoc` path handling don't alter existing usages (just allow the + path to be explicitly set). +""" + +[[audits.prost-derive]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.12.1 -> 0.12.3" + +[[audits.prost-derive]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.12.3 -> 0.12.6" +notes = "Changes to proc macro code are to fix lints after bumping MSRV." + +[[audits.prost-derive]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.13.1 -> 0.13.4" + +[[audits.prost-types]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.12.1 -> 0.12.3" + +[[audits.prost-types]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.13.1 -> 0.13.4" + +[[audits.redox_syscall]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-run" +delta = "0.4.1 -> 0.5.1" +notes = "Uses of unsafe look plausible." + +[[audits.regex-automata]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.4.6 -> 0.4.7" + +[[audits.regex-syntax]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.8.3 -> 0.8.4" + +[[audits.rgb]] +who = "Kris Nuttycombe " +criteria = "safe-to-run" +delta = "0.8.37 -> 0.8.50" +notes = """ +Some clearly-marked unsafe code is moved; adds safer alternative to the +`as-bytes` feature (which is still enabled by default) +""" + +[[audits.rustc-demangle]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.1.23 -> 0.1.24" + +[[audits.rustls]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.21.8 -> 0.21.12" +notes = """ +A comment in get_sni_extension asks whether the behaviour of parsing an IPv4 or IPv6 address +in a host_name field of a server_name extension, but then ignoring the extension (because +'Literal IPv4 and IPv6 addresses are not permitted in \"HostName\"'), as the server, is +compliant with RFC 6066. As an original author of RFC 3546 which has very similar wording, +I can speak to the intent: yes this is fine. The client is clearly nonconformant in this +case, but the server isn't. + +RFC 3546 said \"If the server understood the client hello extension but does not recognize +the server name, it SHOULD send an \"unrecognized_name\" alert (which MAY be fatal).\" +This wording was preserved in RFC 5746, and then updated in RFC 6066 to: + + If the server understood the ClientHello extension but + does not recognize the server name, the server SHOULD take one of two + actions: either abort the handshake by sending a fatal-level + unrecognized_name(112) alert or continue the handshake. It is NOT + RECOMMENDED to send a warning-level unrecognized_name(112) alert, + because the client's behavior in response to warning-level alerts is + unpredictable. If there is a mismatch between the server name used + by the client application and the server name of the credential + chosen by the server, this mismatch will become apparent when the + client application performs the server endpoint identification, at + which point the client application will have to decide whether to + proceed with the communication. + +To me it's clear that it is reasonable to consider an IP address as a name that the +server does not recognize. And so the server SHOULD *either* send a fatal unrecognized_name +alert, *or* continue the handshake and let the client application decide when it \"performs +the server endpoint identification\". There's no conformance requirement for the server to +take any notice of a host_name that is \"not permitted\". (It would have been clearer to +express this by specifying the allowed client and server behaviour separately, i.e. saying +that the client MUST NOT send an IP address in host_name, and then explicitly specifying +the server behaviour if it does so anyway. That's how I would write it now. But honestly +this extension was one of the most bikeshedded parts of RFC 3546, to a much greater extent +than I'd anticipated, and I was tired.) +""" + +[[audits.rustversion]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "1.0.15 -> 1.0.16" + +[[audits.rustversion]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "1.0.16 -> 1.0.17" + +[[audits.ryu]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-run" +delta = "1.0.17 -> 1.0.18" + +[[audits.secp256k1]] +who = "Jack Grigg " +criteria = ["safe-to-deploy", "crypto-reviewed"] +delta = "0.26.0 -> 0.27.0" + +[[audits.semver]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.22 -> 1.0.23" +notes = """ +`build.rs` change is to enable checking for expected `#[cfg]` names if compiling +with Rust 1.80 or later. +""" + +[[audits.serde]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "1.0.201 -> 1.0.202" + +[[audits.serde_derive]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "1.0.201 -> 1.0.202" + +[[audits.serde_json]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-run" +delta = "1.0.116 -> 1.0.117" + +[[audits.serde_json]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.117 -> 1.0.120" + +[[audits.smallvec]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "1.11.1 -> 1.13.2" + +[[audits.socket2]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.5.6 -> 0.5.7" +notes = "The new uses of unsafe to access getsockopt/setsockopt look reasonable." + +[[audits.symbolic-common]] +who = "Kris Nuttycombe " +criteria = "safe-to-run" +delta = "12.9.2 -> 12.13.3" +notes = "Just minor code & Cargo.toml cleanups." + +[[audits.syn]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "2.0.53 -> 2.0.60" + +[[audits.syn]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "2.0.60 -> 2.0.63" + +[[audits.sync_wrapper]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.1.2 -> 1.0.1" + +[[audits.thiserror]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "1.0.58 -> 1.0.60" + +[[audits.thiserror]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.60 -> 1.0.61" + +[[audits.thiserror]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.61 -> 1.0.63" + +[[audits.thiserror-impl]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "1.0.58 -> 1.0.60" + +[[audits.thiserror-impl]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.60 -> 1.0.61" + +[[audits.thiserror-impl]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.61 -> 1.0.63" + +[[audits.tokio-stream]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.1.14 -> 0.1.15" + +[[audits.tokio-util]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.7.10 -> 0.7.11" + +[[audits.tonic]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.10.2 -> 0.11.0" + +[[audits.tonic]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.12.0 -> 0.12.1" +notes = "Changes to generics bounds look fine" + +[[audits.tonic-build]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.10.2 -> 0.11.0" + +[[audits.tonic-build]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.11.0 -> 0.12.0" + +[[audits.tonic-build]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.12.0 -> 0.12.1" + +[[audits.tonic-build]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.12.1 -> 0.12.3" +notes = "Changes to generated code make sense and don't result in anything unexpected." + +[[audits.utf8parse]] +who = "Jack Grigg " +criteria = "safe-to-run" +delta = "0.2.1 -> 0.2.2" + +[[audits.visibility]] +who = "Kris Nuttycombe " +criteria = ["safe-to-deploy", "license-reviewed"] +version = "0.1.1" +notes = """ +- Crate has no unsafe code, and sets `#![forbid(unsafe_code)]`. +- Crate has no powerful imports, and exclusively provides a proc macro + that safely malleates a visibility modifier. +""" + +[[audits.walkdir]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-run" +delta = "2.4.0 -> 2.5.0" + +[[audits.wasm-bindgen-backend]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.2.88 -> 0.2.89" + +[[audits.wasm-bindgen-macro]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.2.88 -> 0.2.89" + +[[audits.web-sys]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.3.65 -> 0.3.66" + +[[audits.webpki-roots]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.25.2 -> 0.25.4" +notes = "I have not checked consistency with the Mozilla IncludedCACertificateReportPEMCSV report." + +[[audits.which]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "6.0.1 -> 6.0.3" + +[[audits.winapi-util]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-run" +delta = "0.1.6 -> 0.1.8" + +[[audits.zcash_address]] +who = "Kris Nuttycombe " +criteria = "safe-to-deploy" +delta = "0.3.2 -> 0.4.0" +notes = "This release contains no unsafe code and consists soley of added convenience methods." + +[[audits.zcash_encoding]] +who = "Kris Nuttycombe " +criteria = "safe-to-deploy" +delta = "0.2.0 -> 0.2.1" +notes = "This release adds minor convenience methods and involves no unsafe code." + +[[audits.zcash_keys]] +who = "Kris Nuttycombe " +criteria = "safe-to-deploy" +delta = "0.2.0 -> 0.3.0" + +[[audits.zcash_note_encryption]] +who = "Kris Nuttycombe " +criteria = "safe-to-deploy" +version = "0.4.1" +notes = "Additive-only change that exposes the ability to decrypt by pk_d and esk. No functional changes." + +[[audits.zcash_primitives]] +who = "Kris Nuttycombe " +criteria = "safe-to-deploy" +delta = "0.15.1 -> 0.16.0" +notes = "The primary change here is the switch from the `hdwallet` dependency to using `bip32`." + +[[audits.zcash_proofs]] +who = "Kris Nuttycombe " +criteria = "safe-to-deploy" +delta = "0.15.0 -> 0.16.0" +notes = "This release involves only updates of previously-vetted dependencies." + +[[audits.zerocopy]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.7.32 -> 0.7.34" + +[[audits.zerocopy-derive]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.7.32 -> 0.7.34" + +[[audits.zeroize]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "1.6.0 -> 1.7.0" + +[[trusted.equihash]] +criteria = "safe-to-deploy" +user-id = 6289 # Jack Grigg (str4d) +start = "2020-06-26" +end = "2025-04-22" + +[[trusted.f4jumble]] +criteria = ["safe-to-deploy", "crypto-reviewed"] +user-id = 6289 # Jack Grigg (str4d) +start = "2021-09-22" +end = "2025-04-22" + +[[trusted.halo2_gadgets]] +criteria = "safe-to-deploy" +user-id = 6289 # Jack Grigg (str4d) +start = "2022-02-15" +end = "2025-12-16" + +[[trusted.halo2_gadgets]] +criteria = ["safe-to-deploy", "crypto-reviewed"] +user-id = 1244 # ebfull +start = "2022-05-10" +end = "2025-04-22" + +[[trusted.halo2_legacy_pdqsort]] +criteria = ["safe-to-deploy", "crypto-reviewed"] +user-id = 199950 # Daira Emma Hopwood (daira) +start = "2023-02-24" +end = "2025-04-22" + +[[trusted.halo2_poseidon]] +criteria = "safe-to-deploy" +user-id = 6289 # Jack Grigg (str4d) +start = "2024-12-13" +end = "2025-12-16" + +[[trusted.halo2_proofs]] +criteria = ["safe-to-deploy", "crypto-reviewed"] +user-id = 1244 # ebfull +start = "2022-05-10" +end = "2025-04-22" + +[[trusted.incrementalmerkletree]] +criteria = "safe-to-deploy" +user-id = 1244 # ebfull +start = "2021-06-24" +end = "2025-04-22" + +[[trusted.incrementalmerkletree]] +criteria = "safe-to-deploy" +user-id = 6289 # Jack Grigg (str4d) +start = "2021-12-17" +end = "2025-04-22" + +[[trusted.incrementalmerkletree]] +criteria = "safe-to-deploy" +user-id = 169181 # Kris Nuttycombe (nuttycom) +start = "2023-02-28" +end = "2025-04-22" + +[[trusted.incrementalmerkletree-testing]] +criteria = "safe-to-deploy" +user-id = 169181 # Kris Nuttycombe (nuttycom) +start = "2024-09-25" +end = "2025-10-02" + +[[trusted.memuse]] +criteria = "safe-to-deploy" +user-id = 6289 # Jack Grigg (str4d) +start = "2021-09-03" +end = "2025-12-16" + +[[trusted.orchard]] +criteria = "safe-to-deploy" +user-id = 169181 # Kris Nuttycombe (nuttycom) +start = "2024-08-12" +end = "2025-08-12" + +[[trusted.orchard]] +criteria = ["safe-to-deploy", "crypto-reviewed", "license-reviewed"] +user-id = 1244 # ebfull +start = "2022-10-19" +end = "2025-04-22" + +[[trusted.orchard]] +criteria = ["safe-to-deploy", "crypto-reviewed", "license-reviewed"] +user-id = 6289 # Jack Grigg (str4d) +start = "2021-01-07" +end = "2025-04-22" + +[[trusted.orchard]] +criteria = ["safe-to-deploy", "crypto-reviewed", "license-reviewed"] +user-id = 169181 # Kris Nuttycombe (nuttycom) +start = "2024-08-12" +end = "2025-08-12" + +[[trusted.pczt]] +criteria = "safe-to-deploy" +user-id = 169181 # Kris Nuttycombe (nuttycom) +start = "2024-12-17" +end = "2025-12-17" + +[[trusted.sapling-crypto]] +criteria = "safe-to-deploy" +user-id = 169181 # Kris Nuttycombe (nuttycom) +start = "2024-08-12" +end = "2025-08-12" + +[[trusted.sapling-crypto]] +criteria = ["safe-to-deploy", "crypto-reviewed"] +user-id = 6289 # Jack Grigg (str4d) +start = "2024-01-26" +end = "2025-04-22" + +[[trusted.sapling-crypto]] +criteria = ["safe-to-deploy", "crypto-reviewed", "license-reviewed"] +user-id = 169181 # Kris Nuttycombe (nuttycom) +start = "2024-08-12" +end = "2025-08-12" + +[[trusted.schemerz]] +criteria = "safe-to-deploy" +user-id = 6289 # Jack Grigg (str4d) +start = "2024-10-15" +end = "2025-10-15" + +[[trusted.schemerz-rusqlite]] +criteria = "safe-to-deploy" +user-id = 6289 # Jack Grigg (str4d) +start = "2024-10-15" +end = "2025-10-15" + +[[trusted.shardtree]] +criteria = "safe-to-deploy" +user-id = 169181 # Kris Nuttycombe (nuttycom) +start = "2022-12-15" +end = "2025-04-22" + +[[trusted.sinsemilla]] +criteria = "safe-to-deploy" +user-id = 6289 # Jack Grigg (str4d) +start = "2024-12-13" +end = "2025-12-16" + +[[trusted.windows-sys]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2021-11-15" +end = "2025-04-22" + +[[trusted.windows-targets]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2022-09-09" +end = "2025-04-22" + +[[trusted.windows_aarch64_gnullvm]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2022-09-01" +end = "2025-04-22" + +[[trusted.windows_aarch64_msvc]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2021-11-05" +end = "2025-04-22" + +[[trusted.windows_i686_gnu]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2021-10-28" +end = "2025-04-22" + +[[trusted.windows_i686_gnullvm]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2024-04-02" +end = "2025-05-15" + +[[trusted.windows_i686_msvc]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2021-10-27" +end = "2025-04-22" + +[[trusted.windows_x86_64_gnu]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2021-10-28" +end = "2025-04-22" + +[[trusted.windows_x86_64_gnullvm]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2022-09-01" +end = "2025-04-22" + +[[trusted.windows_x86_64_msvc]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2021-10-27" +end = "2025-04-22" + +[[trusted.zcash]] +criteria = "safe-to-deploy" +user-id = 6289 # Jack Grigg (str4d) +start = "2024-07-15" +end = "2025-07-19" + +[[trusted.zcash_address]] +criteria = "safe-to-deploy" +user-id = 1244 # ebfull +start = "2022-10-19" +end = "2025-04-22" + +[[trusted.zcash_address]] +criteria = "safe-to-deploy" +user-id = 6289 # Jack Grigg (str4d) +start = "2021-03-07" +end = "2025-04-22" + +[[trusted.zcash_address]] +criteria = "safe-to-deploy" +user-id = 169181 # Kris Nuttycombe (nuttycom) +start = "2024-08-20" +end = "2025-08-26" + +[[trusted.zcash_client_backend]] +criteria = "safe-to-deploy" +user-id = 169181 # Kris Nuttycombe (nuttycom) +start = "2024-03-25" +end = "2025-04-22" + +[[trusted.zcash_client_sqlite]] +criteria = "safe-to-deploy" +user-id = 6289 # Jack Grigg (str4d) +start = "2020-06-25" +end = "2025-10-22" + +[[trusted.zcash_client_sqlite]] +criteria = "safe-to-deploy" +user-id = 169181 # Kris Nuttycombe (nuttycom) +start = "2024-03-25" +end = "2025-04-22" + +[[trusted.zcash_encoding]] +criteria = "safe-to-deploy" +user-id = 1244 # ebfull +start = "2022-10-19" +end = "2025-04-22" + +[[trusted.zcash_encoding]] +criteria = "safe-to-deploy" +user-id = 6289 # Jack Grigg (str4d) +start = "2021-08-31" +end = "2025-12-13" + +[[trusted.zcash_extensions]] +criteria = "safe-to-deploy" +user-id = 6289 # Jack Grigg (str4d) +start = "2020-04-24" +end = "2025-04-23" + +[[trusted.zcash_history]] +criteria = "safe-to-deploy" +user-id = 1244 # ebfull +start = "2020-03-04" +end = "2025-04-22" + +[[trusted.zcash_history]] +criteria = "safe-to-deploy" +user-id = 6289 # Jack Grigg (str4d) +start = "2024-03-01" +end = "2025-04-22" + +[[trusted.zcash_keys]] +criteria = "safe-to-deploy" +user-id = 169181 # Kris Nuttycombe (nuttycom) +start = "2024-01-15" +end = "2025-04-22" + +[[trusted.zcash_note_encryption]] +criteria = ["safe-to-deploy", "crypto-reviewed"] +user-id = 169181 # Kris Nuttycombe (nuttycom) +start = "2023-03-22" +end = "2025-04-22" + +[[trusted.zcash_primitives]] +criteria = "safe-to-deploy" +user-id = 169181 # Kris Nuttycombe (nuttycom) +start = "2024-08-20" +end = "2025-08-26" + +[[trusted.zcash_primitives]] +criteria = ["safe-to-deploy", "crypto-reviewed", "license-reviewed"] +user-id = 1244 # ebfull +start = "2019-10-08" +end = "2025-04-22" + +[[trusted.zcash_primitives]] +criteria = ["safe-to-deploy", "crypto-reviewed", "license-reviewed"] +user-id = 6289 # Jack Grigg (str4d) +start = "2021-03-26" +end = "2025-04-22" + +[[trusted.zcash_proofs]] +criteria = "safe-to-deploy" +user-id = 169181 # Kris Nuttycombe (nuttycom) +start = "2024-08-20" +end = "2025-08-26" + +[[trusted.zcash_proofs]] +criteria = ["safe-to-deploy", "crypto-reviewed", "license-reviewed"] +user-id = 6289 # Jack Grigg (str4d) +start = "2021-03-26" +end = "2025-04-22" + +[[trusted.zcash_protocol]] +criteria = "safe-to-deploy" +user-id = 6289 # Jack Grigg (str4d) +start = "2024-12-13" +end = "2025-12-13" + +[[trusted.zcash_protocol]] +criteria = "safe-to-deploy" +user-id = 169181 # Kris Nuttycombe (nuttycom) +start = "2024-01-27" +end = "2025-04-22" + +[[trusted.zcash_spec]] +criteria = ["safe-to-deploy", "crypto-reviewed", "license-reviewed"] +user-id = 6289 # Jack Grigg (str4d) +start = "2023-12-07" +end = "2025-04-22" + +[[trusted.zcash_transparent]] +criteria = "safe-to-deploy" +user-id = 6289 # Jack Grigg (str4d) +start = "2024-12-14" +end = "2025-12-16" + +[[trusted.zcash_transparent]] +criteria = "safe-to-deploy" +user-id = 169181 # Kris Nuttycombe (nuttycom) +start = "2024-12-17" +end = "2025-12-17" + +[[trusted.zip32]] +criteria = "safe-to-deploy" +user-id = 6289 # Jack Grigg (str4d) +start = "2023-12-06" +end = "2025-04-22" + +[[trusted.zip321]] +criteria = "safe-to-deploy" +user-id = 169181 # Kris Nuttycombe (nuttycom) +start = "2024-01-15" +end = "2025-04-22" diff --git a/supply-chain/config.toml b/supply-chain/config.toml new file mode 100644 index 0000000000..0bdc6d76e6 --- /dev/null +++ b/supply-chain/config.toml @@ -0,0 +1,1614 @@ + +# cargo-vet config file + +[cargo-vet] +version = "0.10" + +[imports.bytecode-alliance] +url = "https://raw.githubusercontent.com/bytecodealliance/wasmtime/main/supply-chain/audits.toml" + +[imports.embark-studios] +url = "https://raw.githubusercontent.com/EmbarkStudios/rust-ecosystem/main/audits.toml" + +[imports.fermyon] +url = "https://raw.githubusercontent.com/fermyon/spin/main/supply-chain/audits.toml" + +[imports.google] +url = "https://raw.githubusercontent.com/google/supply-chain/main/audits.toml" + +[imports.isrg] +url = "https://raw.githubusercontent.com/divviup/libprio-rs/main/supply-chain/audits.toml" + +[imports.mozilla] +url = "https://raw.githubusercontent.com/mozilla/supply-chain/main/audits.toml" + +[imports.zcash] +url = "https://raw.githubusercontent.com/zcash/rust-ecosystem/main/supply-chain/audits.toml" + +[policy.equihash] +audit-as-crates-io = true + +[policy.f4jumble] +audit-as-crates-io = true + +[policy.pczt] +audit-as-crates-io = true + +[policy.zcash] +audit-as-crates-io = true + +[policy.zcash_address] +audit-as-crates-io = true + +[policy.zcash_client_backend] +audit-as-crates-io = true + +[policy.zcash_client_sqlite] +audit-as-crates-io = true + +[policy.zcash_encoding] +audit-as-crates-io = true + +[policy.zcash_extensions] +audit-as-crates-io = true + +[policy.zcash_history] +audit-as-crates-io = true + +[policy.zcash_keys] +audit-as-crates-io = true + +[policy.zcash_primitives] +audit-as-crates-io = true + +[policy.zcash_proofs] +audit-as-crates-io = true + +[policy.zcash_protocol] +audit-as-crates-io = true + +[policy.zcash_transparent] +audit-as-crates-io = true + +[policy.zip321] +audit-as-crates-io = true + +[[exemptions.addr2line]] +version = "0.21.0" +criteria = "safe-to-deploy" + +[[exemptions.aead]] +version = "0.5.2" +criteria = "safe-to-deploy" + +[[exemptions.aes]] +version = "0.8.3" +criteria = "safe-to-deploy" + +[[exemptions.ahash]] +version = "0.8.6" +criteria = "safe-to-deploy" + +[[exemptions.aho-corasick]] +version = "1.1.2" +criteria = "safe-to-deploy" + +[[exemptions.amplify]] +version = "4.6.0" +criteria = "safe-to-deploy" + +[[exemptions.amplify_derive]] +version = "4.0.0" +criteria = "safe-to-deploy" + +[[exemptions.amplify_num]] +version = "0.5.2" +criteria = "safe-to-deploy" + +[[exemptions.amplify_syn]] +version = "2.0.1" +criteria = "safe-to-deploy" + +[[exemptions.android-tzdata]] +version = "0.1.1" +criteria = "safe-to-deploy" + +[[exemptions.arrayvec]] +version = "0.7.4" +criteria = "safe-to-deploy" + +[[exemptions.arti-client]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.ascii]] +version = "1.1.0" +criteria = "safe-to-deploy" + +[[exemptions.assert_matches]] +version = "1.5.0" +criteria = "safe-to-deploy" + +[[exemptions.async-compression]] +version = "0.4.11" +criteria = "safe-to-deploy" + +[[exemptions.async-trait]] +version = "0.1.78" +criteria = "safe-to-deploy" + +[[exemptions.async_executors]] +version = "0.7.0" +criteria = "safe-to-deploy" + +[[exemptions.asynchronous-codec]] +version = "0.7.0" +criteria = "safe-to-deploy" + +[[exemptions.atomic]] +version = "0.5.3" +criteria = "safe-to-deploy" + +[[exemptions.atomic]] +version = "0.6.0" +criteria = "safe-to-deploy" + +[[exemptions.atomic-polyfill]] +version = "1.0.3" +criteria = "safe-to-deploy" + +[[exemptions.atomic-waker]] +version = "1.1.2" +criteria = "safe-to-deploy" + +[[exemptions.axum]] +version = "0.7.5" +criteria = "safe-to-deploy" + +[[exemptions.axum-core]] +version = "0.4.3" +criteria = "safe-to-deploy" + +[[exemptions.backtrace]] +version = "0.3.69" +criteria = "safe-to-deploy" + +[[exemptions.base16ct]] +version = "0.2.0" +criteria = "safe-to-deploy" + +[[exemptions.base64ct]] +version = "1.6.0" +criteria = "safe-to-deploy" + +[[exemptions.bech32]] +version = "0.11.0" +criteria = "safe-to-deploy" + +[[exemptions.bellman]] +version = "0.14.0" +criteria = "safe-to-deploy" + +[[exemptions.bip0039]] +version = "0.11.0" +criteria = "safe-to-deploy" + +[[exemptions.bip32]] +version = "0.6.0-pre.1" +criteria = "safe-to-deploy" + +[[exemptions.bitvec]] +version = "1.0.1" +criteria = "safe-to-deploy" + +[[exemptions.blake2b_simd]] +version = "1.0.1" +criteria = "safe-to-deploy" + +[[exemptions.blake2s_simd]] +version = "1.0.1" +criteria = "safe-to-deploy" + +[[exemptions.blanket]] +version = "0.3.0" +criteria = "safe-to-deploy" + +[[exemptions.block-buffer]] +version = "0.11.0-rc.3" +criteria = "safe-to-deploy" + +[[exemptions.bls12_381]] +version = "0.8.0" +criteria = "safe-to-deploy" + +[[exemptions.bounded-vec-deque]] +version = "0.1.1" +criteria = "safe-to-deploy" + +[[exemptions.bs58]] +version = "0.5.0" +criteria = "safe-to-deploy" + +[[exemptions.by_address]] +version = "1.2.1" +criteria = "safe-to-deploy" + +[[exemptions.bytes]] +version = "1.5.0" +criteria = "safe-to-deploy" + +[[exemptions.caret]] +version = "0.5.0" +criteria = "safe-to-deploy" + +[[exemptions.cbc]] +version = "0.1.2" +criteria = "safe-to-deploy" + +[[exemptions.cc]] +version = "1.1.30" +criteria = "safe-to-deploy" + +[[exemptions.chacha20]] +version = "0.9.1" +criteria = "safe-to-deploy" + +[[exemptions.chacha20poly1305]] +version = "0.10.1" +criteria = "safe-to-deploy" + +[[exemptions.chrono]] +version = "0.4.38" +criteria = "safe-to-deploy" + +[[exemptions.ciborium]] +version = "0.2.1" +criteria = "safe-to-run" + +[[exemptions.ciborium-io]] +version = "0.2.1" +criteria = "safe-to-run" + +[[exemptions.ciborium-ll]] +version = "0.2.1" +criteria = "safe-to-run" + +[[exemptions.coarsetime]] +version = "0.1.34" +criteria = "safe-to-deploy" + +[[exemptions.concurrent-queue]] +version = "2.5.0" +criteria = "safe-to-deploy" + +[[exemptions.const-oid]] +version = "0.9.6" +criteria = "safe-to-deploy" + +[[exemptions.convert_case]] +version = "0.6.0" +criteria = "safe-to-deploy" + +[[exemptions.core2]] +version = "0.3.3" +criteria = "safe-to-deploy" + +[[exemptions.cpufeatures]] +version = "0.2.11" +criteria = "safe-to-deploy" + +[[exemptions.criterion]] +version = "0.4.0" +criteria = "safe-to-run" + +[[exemptions.criterion-plot]] +version = "0.5.0" +criteria = "safe-to-run" + +[[exemptions.critical-section]] +version = "1.2.0" +criteria = "safe-to-deploy" + +[[exemptions.crossbeam-channel]] +version = "0.5.8" +criteria = "safe-to-deploy" + +[[exemptions.crossbeam-deque]] +version = "0.8.3" +criteria = "safe-to-deploy" + +[[exemptions.crossbeam-epoch]] +version = "0.9.15" +criteria = "safe-to-deploy" + +[[exemptions.crossbeam-utils]] +version = "0.8.19" +criteria = "safe-to-deploy" + +[[exemptions.crypto-bigint]] +version = "0.5.5" +criteria = "safe-to-deploy" + +[[exemptions.crypto-common]] +version = "0.2.0-rc.1" +criteria = "safe-to-deploy" + +[[exemptions.ctr]] +version = "0.9.2" +criteria = "safe-to-deploy" + +[[exemptions.curve25519-dalek]] +version = "4.1.0" +criteria = "safe-to-deploy" + +[[exemptions.curve25519-dalek-derive]] +version = "0.1.0" +criteria = "safe-to-deploy" + +[[exemptions.daggy]] +version = "0.8.0" +criteria = "safe-to-deploy" + +[[exemptions.darling]] +version = "0.14.4" +criteria = "safe-to-deploy" + +[[exemptions.darling]] +version = "0.20.9" +criteria = "safe-to-deploy" + +[[exemptions.darling_core]] +version = "0.14.4" +criteria = "safe-to-deploy" + +[[exemptions.darling_core]] +version = "0.20.9" +criteria = "safe-to-deploy" + +[[exemptions.darling_macro]] +version = "0.14.4" +criteria = "safe-to-deploy" + +[[exemptions.darling_macro]] +version = "0.20.9" +criteria = "safe-to-deploy" + +[[exemptions.data-encoding]] +version = "2.6.0" +criteria = "safe-to-deploy" + +[[exemptions.der]] +version = "0.7.8" +criteria = "safe-to-deploy" + +[[exemptions.derive-adhoc]] +version = "0.7.3" +criteria = "safe-to-deploy" + +[[exemptions.derive-adhoc-macros]] +version = "0.7.3" +criteria = "safe-to-deploy" + +[[exemptions.derive-deftly]] +version = "0.14.2" +criteria = "safe-to-deploy" + +[[exemptions.derive-deftly-macros]] +version = "0.14.2" +criteria = "safe-to-deploy" + +[[exemptions.derive_builder_core_fork_arti]] +version = "0.11.2" +criteria = "safe-to-deploy" + +[[exemptions.derive_builder_fork_arti]] +version = "0.11.2" +criteria = "safe-to-deploy" + +[[exemptions.derive_builder_macro_fork_arti]] +version = "0.11.2" +criteria = "safe-to-deploy" + +[[exemptions.derive_more]] +version = "1.0.0" +criteria = "safe-to-deploy" + +[[exemptions.derive_more-impl]] +version = "1.0.0" +criteria = "safe-to-deploy" + +[[exemptions.digest]] +version = "0.9.0" +criteria = "safe-to-deploy" + +[[exemptions.digest]] +version = "0.11.0-pre.9" +criteria = "safe-to-deploy" + +[[exemptions.directories]] +version = "5.0.1" +criteria = "safe-to-deploy" + +[[exemptions.dirs]] +version = "5.0.1" +criteria = "safe-to-deploy" + +[[exemptions.dirs-sys]] +version = "0.4.1" +criteria = "safe-to-deploy" + +[[exemptions.downcast-rs]] +version = "1.2.1" +criteria = "safe-to-deploy" + +[[exemptions.dyn-clone]] +version = "1.0.17" +criteria = "safe-to-deploy" + +[[exemptions.dynosaur]] +version = "0.1.1" +criteria = "safe-to-deploy" + +[[exemptions.dynosaur_derive]] +version = "0.1.1" +criteria = "safe-to-deploy" + +[[exemptions.ecdsa]] +version = "0.16.9" +criteria = "safe-to-deploy" + +[[exemptions.ed25519]] +version = "2.2.1" +criteria = "safe-to-deploy" + +[[exemptions.ed25519-dalek]] +version = "2.1.1" +criteria = "safe-to-deploy" + +[[exemptions.ed25519-zebra]] +version = "3.0.0" +criteria = "safe-to-deploy" + +[[exemptions.educe]] +version = "0.4.23" +criteria = "safe-to-deploy" + +[[exemptions.elliptic-curve]] +version = "0.13.8" +criteria = "safe-to-deploy" + +[[exemptions.embedded-io]] +version = "0.6.1" +criteria = "safe-to-deploy" + +[[exemptions.enum-ordinalize]] +version = "3.1.15" +criteria = "safe-to-deploy" + +[[exemptions.event-listener]] +version = "5.3.1" +criteria = "safe-to-deploy" + +[[exemptions.fallible-iterator]] +version = "0.2.0" +criteria = "safe-to-deploy" + +[[exemptions.fallible-streaming-iterator]] +version = "0.1.9" +criteria = "safe-to-deploy" + +[[exemptions.ff]] +version = "0.13.0" +criteria = "safe-to-deploy" + +[[exemptions.figment]] +version = "0.10.19" +criteria = "safe-to-deploy" + +[[exemptions.filetime]] +version = "0.2.25" +criteria = "safe-to-deploy" + +[[exemptions.findshlibs]] +version = "0.10.2" +criteria = "safe-to-run" + +[[exemptions.fixed-hash]] +version = "0.8.0" +criteria = "safe-to-deploy" + +[[exemptions.fixedbitset]] +version = "0.4.2" +criteria = "safe-to-deploy" + +[[exemptions.fluid-let]] +version = "1.0.0" +criteria = "safe-to-deploy" + +[[exemptions.fpe]] +version = "0.6.1" +criteria = "safe-to-deploy" + +[[exemptions.fs-mistrust]] +version = "0.8.0" +criteria = "safe-to-deploy" + +[[exemptions.fslock]] +version = "0.2.1" +criteria = "safe-to-deploy" + +[[exemptions.funty]] +version = "2.0.0" +criteria = "safe-to-deploy" + +[[exemptions.futures-macro]] +version = "0.3.21" +criteria = "safe-to-deploy" + +[[exemptions.futures-rustls]] +version = "0.26.0" +criteria = "safe-to-deploy" + +[[exemptions.futures-sink]] +version = "0.3.29" +criteria = "safe-to-deploy" + +[[exemptions.futures-task]] +version = "0.3.29" +criteria = "safe-to-deploy" + +[[exemptions.futures-util]] +version = "0.3.21" +criteria = "safe-to-deploy" + +[[exemptions.generic-array]] +version = "0.14.7" +criteria = "safe-to-deploy" + +[[exemptions.getrandom]] +version = "0.2.11" +criteria = "safe-to-deploy" + +[[exemptions.gimli]] +version = "0.28.1" +criteria = "safe-to-deploy" + +[[exemptions.glob-match]] +version = "0.2.1" +criteria = "safe-to-deploy" + +[[exemptions.group]] +version = "0.13.0" +criteria = "safe-to-deploy" + +[[exemptions.gumdrop]] +version = "0.8.1" +criteria = "safe-to-deploy" + +[[exemptions.gumdrop_derive]] +version = "0.8.1" +criteria = "safe-to-deploy" + +[[exemptions.h2]] +version = "0.3.21" +criteria = "safe-to-deploy" + +[[exemptions.hash32]] +version = "0.2.1" +criteria = "safe-to-deploy" + +[[exemptions.hashbrown]] +version = "0.14.2" +criteria = "safe-to-deploy" + +[[exemptions.hashbrown]] +version = "0.15.0" +criteria = "safe-to-deploy" + +[[exemptions.hashlink]] +version = "0.7.0" +criteria = "safe-to-deploy" + +[[exemptions.heapless]] +version = "0.7.17" +criteria = "safe-to-deploy" + +[[exemptions.hermit-abi]] +version = "0.3.3" +criteria = "safe-to-deploy" + +[[exemptions.hkdf]] +version = "0.12.4" +criteria = "safe-to-deploy" + +[[exemptions.hmac]] +version = "0.13.0-pre.4" +criteria = "safe-to-deploy" + +[[exemptions.home]] +version = "0.5.5" +criteria = "safe-to-deploy" + +[[exemptions.hostname-validator]] +version = "1.1.1" +criteria = "safe-to-deploy" + +[[exemptions.http]] +version = "1.1.0" +criteria = "safe-to-deploy" + +[[exemptions.httparse]] +version = "1.8.0" +criteria = "safe-to-deploy" + +[[exemptions.humantime]] +version = "2.1.0" +criteria = "safe-to-deploy" + +[[exemptions.humantime-serde]] +version = "1.1.1" +criteria = "safe-to-deploy" + +[[exemptions.hybrid-array]] +version = "0.2.3" +criteria = "safe-to-deploy" + +[[exemptions.hyper]] +version = "1.4.1" +criteria = "safe-to-deploy" + +[[exemptions.hyper-timeout]] +version = "0.4.1" +criteria = "safe-to-deploy" + +[[exemptions.hyper-util]] +version = "0.1.5" +criteria = "safe-to-deploy" + +[[exemptions.iana-time-zone]] +version = "0.1.60" +criteria = "safe-to-deploy" + +[[exemptions.indexmap]] +version = "1.9.3" +criteria = "safe-to-deploy" + +[[exemptions.indexmap]] +version = "2.6.0" +criteria = "safe-to-deploy" + +[[exemptions.inferno]] +version = "0.11.17" +criteria = "safe-to-run" + +[[exemptions.inotify]] +version = "0.9.6" +criteria = "safe-to-deploy" + +[[exemptions.inotify-sys]] +version = "0.1.5" +criteria = "safe-to-deploy" + +[[exemptions.inventory]] +version = "0.3.15" +criteria = "safe-to-deploy" + +[[exemptions.itertools]] +version = "0.10.5" +criteria = "safe-to-deploy" + +[[exemptions.itertools]] +version = "0.11.0" +criteria = "safe-to-deploy" + +[[exemptions.itertools]] +version = "0.13.0" +criteria = "safe-to-deploy" + +[[exemptions.jobserver]] +version = "0.1.31" +criteria = "safe-to-deploy" + +[[exemptions.js-sys]] +version = "0.3.65" +criteria = "safe-to-deploy" + +[[exemptions.jubjub]] +version = "0.10.0" +criteria = "safe-to-deploy" + +[[exemptions.kqueue]] +version = "1.0.8" +criteria = "safe-to-deploy" + +[[exemptions.kqueue-sys]] +version = "1.0.4" +criteria = "safe-to-deploy" + +[[exemptions.libc]] +version = "0.2.154" +criteria = "safe-to-deploy" + +[[exemptions.libm]] +version = "0.2.2" +criteria = "safe-to-deploy" + +[[exemptions.libredox]] +version = "0.0.1" +criteria = "safe-to-deploy" + +[[exemptions.libsqlite3-sys]] +version = "0.30.1" +criteria = "safe-to-deploy" + +[[exemptions.linux-raw-sys]] +version = "0.4.12" +criteria = "safe-to-deploy" + +[[exemptions.lock_api]] +version = "0.4.12" +criteria = "safe-to-deploy" + +[[exemptions.lzma-sys]] +version = "0.1.20" +criteria = "safe-to-deploy" + +[[exemptions.matchit]] +version = "0.7.3" +criteria = "safe-to-deploy" + +[[exemptions.memchr]] +version = "2.6.4" +criteria = "safe-to-deploy" + +[[exemptions.memmap2]] +version = "0.5.4" +criteria = "safe-to-deploy" + +[[exemptions.merlin]] +version = "3.0.0" +criteria = "safe-to-deploy" + +[[exemptions.mime]] +version = "0.3.17" +criteria = "safe-to-deploy" + +[[exemptions.minimal-lexical]] +version = "0.2.1" +criteria = "safe-to-deploy" + +[[exemptions.minreq]] +version = "2.11.0" +criteria = "safe-to-deploy" + +[[exemptions.mio]] +version = "0.8.10" +criteria = "safe-to-deploy" + +[[exemptions.multimap]] +version = "0.8.3" +criteria = "safe-to-deploy" + +[[exemptions.notify]] +version = "6.1.1" +criteria = "safe-to-deploy" + +[[exemptions.num-bigint-dig]] +version = "0.8.4" +criteria = "safe-to-deploy" + +[[exemptions.num-format]] +version = "0.4.4" +criteria = "safe-to-run" + +[[exemptions.num_cpus]] +version = "1.16.0" +criteria = "safe-to-deploy" + +[[exemptions.object]] +version = "0.32.1" +criteria = "safe-to-deploy" + +[[exemptions.once_cell]] +version = "1.18.0" +criteria = "safe-to-deploy" + +[[exemptions.oneshot-fused-workaround]] +version = "0.2.0" +criteria = "safe-to-deploy" + +[[exemptions.option-ext]] +version = "0.2.0" +criteria = "safe-to-deploy" + +[[exemptions.ordered-float]] +version = "2.10.1" +criteria = "safe-to-deploy" + +[[exemptions.p256]] +version = "0.13.2" +criteria = "safe-to-deploy" + +[[exemptions.p384]] +version = "0.13.0" +criteria = "safe-to-deploy" + +[[exemptions.p521]] +version = "0.13.3" +criteria = "safe-to-deploy" + +[[exemptions.pairing]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.parking]] +version = "2.2.0" +criteria = "safe-to-deploy" + +[[exemptions.parking_lot]] +version = "0.12.2" +criteria = "safe-to-deploy" + +[[exemptions.parking_lot_core]] +version = "0.9.10" +criteria = "safe-to-deploy" + +[[exemptions.password-hash]] +version = "0.3.2" +criteria = "safe-to-deploy" + +[[exemptions.pasta_curves]] +version = "0.5.1" +criteria = "safe-to-deploy" + +[[exemptions.paste]] +version = "1.0.15" +criteria = "safe-to-deploy" + +[[exemptions.pbkdf2]] +version = "0.9.0" +criteria = "safe-to-deploy" + +[[exemptions.pem-rfc7468]] +version = "0.7.0" +criteria = "safe-to-deploy" + +[[exemptions.petgraph]] +version = "0.6.5" +criteria = "safe-to-deploy" + +[[exemptions.phf]] +version = "0.8.0" +criteria = "safe-to-deploy" + +[[exemptions.phf_generator]] +version = "0.8.0" +criteria = "safe-to-deploy" + +[[exemptions.phf_macros]] +version = "0.10.0" +criteria = "safe-to-deploy" + +[[exemptions.phf_shared]] +version = "0.8.0" +criteria = "safe-to-deploy" + +[[exemptions.pin-project]] +version = "1.1.5" +criteria = "safe-to-deploy" + +[[exemptions.pin-project-internal]] +version = "1.1.3" +criteria = "safe-to-deploy" + +[[exemptions.pkcs1]] +version = "0.7.5" +criteria = "safe-to-deploy" + +[[exemptions.pkcs8]] +version = "0.10.2" +criteria = "safe-to-deploy" + +[[exemptions.plotters]] +version = "0.3.5" +criteria = "safe-to-run" + +[[exemptions.plotters-backend]] +version = "0.3.5" +criteria = "safe-to-run" + +[[exemptions.plotters-svg]] +version = "0.3.5" +criteria = "safe-to-run" + +[[exemptions.poly1305]] +version = "0.8.0" +criteria = "safe-to-deploy" + +[[exemptions.postage]] +version = "0.5.0" +criteria = "safe-to-deploy" + +[[exemptions.postcard]] +version = "1.1.1" +criteria = "safe-to-deploy" + +[[exemptions.pprof]] +version = "0.13.0" +criteria = "safe-to-run" + +[[exemptions.ppv-lite86]] +version = "0.2.17" +criteria = "safe-to-deploy" + +[[exemptions.prettyplease]] +version = "0.2.15" +criteria = "safe-to-deploy" + +[[exemptions.primeorder]] +version = "0.13.6" +criteria = "safe-to-deploy" + +[[exemptions.primitive-types]] +version = "0.12.2" +criteria = "safe-to-deploy" + +[[exemptions.priority-queue]] +version = "2.1.1" +criteria = "safe-to-deploy" + +[[exemptions.proc-macro-crate]] +version = "1.2.1" +criteria = "safe-to-deploy" + +[[exemptions.proc-macro-error-attr2]] +version = "2.0.0" +criteria = "safe-to-deploy" + +[[exemptions.proc-macro-error2]] +version = "2.0.1" +criteria = "safe-to-deploy" + +[[exemptions.proptest]] +version = "1.3.1" +criteria = "safe-to-deploy" + +[[exemptions.prost]] +version = "0.13.1" +criteria = "safe-to-deploy" + +[[exemptions.prost-build]] +version = "0.13.1" +criteria = "safe-to-deploy" + +[[exemptions.prost-derive]] +version = "0.13.1" +criteria = "safe-to-deploy" + +[[exemptions.prost-types]] +version = "0.13.1" +criteria = "safe-to-deploy" + +[[exemptions.pwd-grp]] +version = "0.1.1" +criteria = "safe-to-deploy" + +[[exemptions.quick-error]] +version = "1.2.3" +criteria = "safe-to-deploy" + +[[exemptions.quick-xml]] +version = "0.26.0" +criteria = "safe-to-run" + +[[exemptions.radium]] +version = "0.7.0" +criteria = "safe-to-deploy" + +[[exemptions.rand]] +version = "0.8.5" +criteria = "safe-to-deploy" + +[[exemptions.reddsa]] +version = "0.5.1" +criteria = "safe-to-deploy" + +[[exemptions.redox_syscall]] +version = "0.5.1" +criteria = "safe-to-deploy" + +[[exemptions.redox_users]] +version = "0.4.3" +criteria = "safe-to-deploy" + +[[exemptions.regex]] +version = "1.10.2" +criteria = "safe-to-deploy" + +[[exemptions.regex-automata]] +version = "0.1.10" +criteria = "safe-to-deploy" + +[[exemptions.regex-automata]] +version = "0.4.3" +criteria = "safe-to-deploy" + +[[exemptions.regex-syntax]] +version = "0.6.26" +criteria = "safe-to-deploy" + +[[exemptions.regex-syntax]] +version = "0.7.5" +criteria = "safe-to-deploy" + +[[exemptions.retry-error]] +version = "0.6.0" +criteria = "safe-to-deploy" + +[[exemptions.rfc6979]] +version = "0.4.0" +criteria = "safe-to-deploy" + +[[exemptions.rgb]] +version = "0.8.37" +criteria = "safe-to-run" + +[[exemptions.ring]] +version = "0.16.12" +criteria = "safe-to-deploy" + +[[exemptions.ring]] +version = "0.17.8" +criteria = "safe-to-deploy" + +[[exemptions.ripemd]] +version = "0.1.3" +criteria = "safe-to-deploy" + +[[exemptions.ripemd]] +version = "0.2.0-pre.4" +criteria = "safe-to-deploy" + +[[exemptions.rsa]] +version = "0.9.6" +criteria = "safe-to-deploy" + +[[exemptions.rusqlite]] +version = "0.32.1" +criteria = "safe-to-deploy" + +[[exemptions.rust_decimal]] +version = "1.35.0" +criteria = "safe-to-deploy" + +[[exemptions.rustix]] +version = "0.38.34" +criteria = "safe-to-deploy" + +[[exemptions.rustls]] +version = "0.21.8" +criteria = "safe-to-deploy" + +[[exemptions.rustls]] +version = "0.23.12" +criteria = "safe-to-deploy" + +[[exemptions.rustls-pemfile]] +version = "2.1.2" +criteria = "safe-to-deploy" + +[[exemptions.rustls-pki-types]] +version = "1.7.0" +criteria = "safe-to-deploy" + +[[exemptions.rustls-webpki]] +version = "0.101.7" +criteria = "safe-to-deploy" + +[[exemptions.rustls-webpki]] +version = "0.102.6" +criteria = "safe-to-deploy" + +[[exemptions.rusty-fork]] +version = "0.3.0" +criteria = "safe-to-deploy" + +[[exemptions.ryu]] +version = "1.0.18" +criteria = "safe-to-deploy" + +[[exemptions.safelog]] +version = "0.4.0" +criteria = "safe-to-deploy" + +[[exemptions.same-file]] +version = "1.0.6" +criteria = "safe-to-deploy" + +[[exemptions.sanitize-filename]] +version = "0.5.0" +criteria = "safe-to-deploy" + +[[exemptions.scopeguard]] +version = "1.1.0" +criteria = "safe-to-deploy" + +[[exemptions.sct]] +version = "0.7.1" +criteria = "safe-to-deploy" + +[[exemptions.sec1]] +version = "0.7.3" +criteria = "safe-to-deploy" + +[[exemptions.secp256k1]] +version = "0.29.1" +criteria = "safe-to-deploy" + +[[exemptions.secp256k1-sys]] +version = "0.10.1" +criteria = "safe-to-deploy" + +[[exemptions.secrecy]] +version = "0.8.0" +criteria = "safe-to-deploy" + +[[exemptions.serde-value]] +version = "0.7.0" +criteria = "safe-to-deploy" + +[[exemptions.serde_ignored]] +version = "0.1.10" +criteria = "safe-to-deploy" + +[[exemptions.serde_json]] +version = "1.0.117" +criteria = "safe-to-deploy" + +[[exemptions.serde_spanned]] +version = "0.6.8" +criteria = "safe-to-deploy" + +[[exemptions.serde_with]] +version = "3.8.1" +criteria = "safe-to-deploy" + +[[exemptions.serde_with_macros]] +version = "3.8.1" +criteria = "safe-to-deploy" + +[[exemptions.sha2]] +version = "0.11.0-pre.4" +criteria = "safe-to-deploy" + +[[exemptions.shellexpand]] +version = "3.1.0" +criteria = "safe-to-deploy" + +[[exemptions.simple_asn1]] +version = "0.6.2" +criteria = "safe-to-deploy" + +[[exemptions.siphasher]] +version = "0.3.10" +criteria = "safe-to-deploy" + +[[exemptions.slab]] +version = "0.4.9" +criteria = "safe-to-deploy" + +[[exemptions.slotmap]] +version = "1.0.7" +criteria = "safe-to-deploy" + +[[exemptions.smallvec]] +version = "1.11.1" +criteria = "safe-to-deploy" + +[[exemptions.socket2]] +version = "0.5.5" +criteria = "safe-to-deploy" + +[[exemptions.spin]] +version = "0.5.2" +criteria = "safe-to-deploy" + +[[exemptions.spin]] +version = "0.9.8" +criteria = "safe-to-deploy" + +[[exemptions.spki]] +version = "0.7.3" +criteria = "safe-to-deploy" + +[[exemptions.ssh-cipher]] +version = "0.2.0" +criteria = "safe-to-deploy" + +[[exemptions.ssh-encoding]] +version = "0.2.0" +criteria = "safe-to-deploy" + +[[exemptions.ssh-key]] +version = "0.6.6" +criteria = "safe-to-deploy" + +[[exemptions.stable_deref_trait]] +version = "1.2.0" +criteria = "safe-to-deploy" + +[[exemptions.str_stack]] +version = "0.1.0" +criteria = "safe-to-run" + +[[exemptions.symbolic-common]] +version = "12.9.2" +criteria = "safe-to-run" + +[[exemptions.symbolic-demangle]] +version = "12.13.3" +criteria = "safe-to-run" + +[[exemptions.syn]] +version = "1.0.96" +criteria = "safe-to-deploy" + +[[exemptions.syn]] +version = "2.0.53" +criteria = "safe-to-deploy" + +[[exemptions.sync_wrapper]] +version = "0.1.2" +criteria = "safe-to-deploy" + +[[exemptions.tempfile]] +version = "3.8.1" +criteria = "safe-to-deploy" + +[[exemptions.time]] +version = "0.3.23" +criteria = "safe-to-deploy" + +[[exemptions.tokio]] +version = "1.35.1" +criteria = "safe-to-deploy" + +[[exemptions.tokio-macros]] +version = "2.2.0" +criteria = "safe-to-deploy" + +[[exemptions.tokio-rustls]] +version = "0.26.0" +criteria = "safe-to-deploy" + +[[exemptions.tokio-util]] +version = "0.7.10" +criteria = "safe-to-deploy" + +[[exemptions.toml]] +version = "0.8.19" +criteria = "safe-to-deploy" + +[[exemptions.toml_datetime]] +version = "0.6.8" +criteria = "safe-to-deploy" + +[[exemptions.toml_edit]] +version = "0.19.15" +criteria = "safe-to-deploy" + +[[exemptions.toml_edit]] +version = "0.22.22" +criteria = "safe-to-deploy" + +[[exemptions.tonic]] +version = "0.12.0" +criteria = "safe-to-deploy" + +[[exemptions.tonic-build]] +version = "0.10.2" +criteria = "safe-to-deploy" + +[[exemptions.tor-async-utils]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-basic-utils]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-bytes]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-cell]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-cert]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-chanmgr]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-checkable]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-circmgr]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-config]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-consdiff]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-dirclient]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-dirmgr]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-error]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-guardmgr]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-hscrypto]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-key-forge]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-keymgr]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-linkspec]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-llcrypto]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-log-ratelim]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-memquota]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-netdir]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-netdoc]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-persist]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-proto]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-protover]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-relay-selection]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-rtcompat]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-rtmock]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-socksproto]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-units]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tower]] +version = "0.4.13" +criteria = "safe-to-deploy" + +[[exemptions.tower-layer]] +version = "0.3.2" +criteria = "safe-to-deploy" + +[[exemptions.tower-service]] +version = "0.3.2" +criteria = "safe-to-deploy" + +[[exemptions.tracing]] +version = "0.1.40" +criteria = "safe-to-deploy" + +[[exemptions.tracing-attributes]] +version = "0.1.27" +criteria = "safe-to-deploy" + +[[exemptions.tracing-log]] +version = "0.2.0" +criteria = "safe-to-deploy" + +[[exemptions.tracing-test]] +version = "0.2.5" +criteria = "safe-to-deploy" + +[[exemptions.tracing-test-macro]] +version = "0.2.5" +criteria = "safe-to-deploy" + +[[exemptions.trait-variant]] +version = "0.1.2" +criteria = "safe-to-deploy" + +[[exemptions.typed-index-collections]] +version = "3.1.0" +criteria = "safe-to-deploy" + +[[exemptions.typenum]] +version = "1.17.0" +criteria = "safe-to-deploy" + +[[exemptions.uint]] +version = "0.9.5" +criteria = "safe-to-deploy" + +[[exemptions.unarray]] +version = "0.1.4" +criteria = "safe-to-deploy" + +[[exemptions.uncased]] +version = "0.9.10" +criteria = "safe-to-deploy" + +[[exemptions.unicode-segmentation]] +version = "1.12.0" +criteria = "safe-to-deploy" + +[[exemptions.untrusted]] +version = "0.9.0" +criteria = "safe-to-deploy" + +[[exemptions.uuid]] +version = "1.8.0" +criteria = "safe-to-deploy" + +[[exemptions.wait-timeout]] +version = "0.2.0" +criteria = "safe-to-deploy" + +[[exemptions.walkdir]] +version = "2.5.0" +criteria = "safe-to-deploy" + +[[exemptions.wasi]] +version = "0.11.0+wasi-snapshot-preview1" +criteria = "safe-to-deploy" + +[[exemptions.wasix]] +version = "0.12.21" +criteria = "safe-to-deploy" + +[[exemptions.wasm-bindgen]] +version = "0.2.92" +criteria = "safe-to-deploy" + +[[exemptions.wasm-bindgen-backend]] +version = "0.2.88" +criteria = "safe-to-deploy" + +[[exemptions.wasm-bindgen-macro]] +version = "0.2.88" +criteria = "safe-to-deploy" + +[[exemptions.weak-table]] +version = "0.3.2" +criteria = "safe-to-deploy" + +[[exemptions.web-sys]] +version = "0.3.65" +criteria = "safe-to-deploy" + +[[exemptions.webpki-roots]] +version = "0.26.3" +criteria = "safe-to-deploy" + +[[exemptions.which]] +version = "4.4.2" +criteria = "safe-to-deploy" + +[[exemptions.winapi]] +version = "0.3.9" +criteria = "safe-to-deploy" + +[[exemptions.winapi-i686-pc-windows-gnu]] +version = "0.4.0" +criteria = "safe-to-deploy" + +[[exemptions.winapi-util]] +version = "0.1.8" +criteria = "safe-to-deploy" + +[[exemptions.winapi-x86_64-pc-windows-gnu]] +version = "0.4.0" +criteria = "safe-to-deploy" + +[[exemptions.windows-core]] +version = "0.52.0" +criteria = "safe-to-deploy" + +[[exemptions.winnow]] +version = "0.5.40" +criteria = "safe-to-deploy" + +[[exemptions.winnow]] +version = "0.6.20" +criteria = "safe-to-deploy" + +[[exemptions.winsafe]] +version = "0.0.19" +criteria = "safe-to-deploy" + +[[exemptions.wyz]] +version = "0.5.1" +criteria = "safe-to-deploy" + +[[exemptions.x25519-dalek]] +version = "2.0.1" +criteria = "safe-to-deploy" + +[[exemptions.x509-signature]] +version = "0.5.0" +criteria = "safe-to-deploy" + +[[exemptions.xdg]] +version = "2.5.2" +criteria = "safe-to-deploy" + +[[exemptions.xz2]] +version = "0.1.7" +criteria = "safe-to-deploy" + +[[exemptions.zeroize]] +version = "1.6.0" +criteria = "safe-to-deploy" + +[[exemptions.zstd]] +version = "0.13.1" +criteria = "safe-to-deploy" + +[[exemptions.zstd-safe]] +version = "7.1.0" +criteria = "safe-to-deploy" + +[[exemptions.zstd-sys]] +version = "2.0.10+zstd.1.5.6" +criteria = "safe-to-deploy" diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock new file mode 100644 index 0000000000..6509ab2185 --- /dev/null +++ b/supply-chain/imports.lock @@ -0,0 +1,3495 @@ + +# cargo-vet imports lock + +[[publisher.bumpalo]] +version = "3.16.0" +when = "2024-04-08" +user-id = 696 +user-login = "fitzgen" +user-name = "Nick Fitzgerald" + +[[publisher.core-foundation-sys]] +version = "0.8.4" +when = "2023-04-03" +user-id = 5946 +user-login = "jrmuizel" +user-name = "Jeff Muizelaar" + +[[publisher.equihash]] +version = "0.2.0" +when = "2022-06-24" +user-id = 6289 +user-login = "str4d" +user-name = "Jack Grigg" + +[[publisher.f4jumble]] +version = "0.1.1" +when = "2024-12-13" +user-id = 6289 +user-login = "str4d" +user-name = "Jack Grigg" + +[[publisher.halo2_gadgets]] +version = "0.3.1" +when = "2024-12-16" +user-id = 6289 +user-login = "str4d" +user-name = "Jack Grigg" + +[[publisher.halo2_legacy_pdqsort]] +version = "0.1.0" +when = "2023-03-10" +user-id = 199950 +user-login = "daira" +user-name = "Daira Emma Hopwood" + +[[publisher.halo2_poseidon]] +version = "0.1.0" +when = "2024-12-16" +user-id = 6289 +user-login = "str4d" +user-name = "Jack Grigg" + +[[publisher.halo2_proofs]] +version = "0.3.0" +when = "2023-03-22" +user-id = 1244 +user-login = "ebfull" + +[[publisher.incrementalmerkletree]] +version = "0.8.2" +when = "2025-02-01" +user-id = 169181 +user-login = "nuttycom" +user-name = "Kris Nuttycombe" + +[[publisher.incrementalmerkletree-testing]] +version = "0.3.0" +when = "2025-01-31" +user-id = 169181 +user-login = "nuttycom" +user-name = "Kris Nuttycombe" + +[[publisher.memuse]] +version = "0.2.2" +when = "2024-12-13" +user-id = 6289 +user-login = "str4d" +user-name = "Jack Grigg" + +[[publisher.pczt]] +version = "0.1.0" +when = "2024-12-17" +user-id = 169181 +user-login = "nuttycom" +user-name = "Kris Nuttycombe" + +[[publisher.schemerz]] +version = "0.2.0" +when = "2024-10-16" +user-id = 6289 +user-login = "str4d" +user-name = "Jack Grigg" + +[[publisher.schemerz-rusqlite]] +version = "0.320.0" +when = "2024-10-16" +user-id = 6289 +user-login = "str4d" +user-name = "Jack Grigg" + +[[publisher.shardtree]] +version = "0.6.1" +when = "2025-01-31" +user-id = 169181 +user-login = "nuttycom" +user-name = "Kris Nuttycombe" + +[[publisher.sinsemilla]] +version = "0.1.0" +when = "2024-12-14" +user-id = 6289 +user-login = "str4d" +user-name = "Jack Grigg" + +[[publisher.windows-sys]] +version = "0.48.0" +when = "2023-03-31" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-sys]] +version = "0.52.0" +when = "2023-11-15" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-sys]] +version = "0.59.0" +when = "2024-07-30" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-targets]] +version = "0.48.5" +when = "2023-08-18" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-targets]] +version = "0.52.6" +when = "2024-07-03" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_aarch64_gnullvm]] +version = "0.48.5" +when = "2023-08-18" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_aarch64_gnullvm]] +version = "0.52.6" +when = "2024-07-03" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_aarch64_msvc]] +version = "0.48.5" +when = "2023-08-18" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_aarch64_msvc]] +version = "0.52.6" +when = "2024-07-03" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_i686_gnu]] +version = "0.48.5" +when = "2023-08-18" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_i686_gnu]] +version = "0.52.6" +when = "2024-07-03" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_i686_gnullvm]] +version = "0.52.6" +when = "2024-07-03" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_i686_msvc]] +version = "0.48.5" +when = "2023-08-18" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_i686_msvc]] +version = "0.52.6" +when = "2024-07-03" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_x86_64_gnu]] +version = "0.48.5" +when = "2023-08-18" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_x86_64_gnu]] +version = "0.52.6" +when = "2024-07-03" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_x86_64_gnullvm]] +version = "0.48.5" +when = "2023-08-18" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_x86_64_gnullvm]] +version = "0.52.6" +when = "2024-07-03" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_x86_64_msvc]] +version = "0.48.5" +when = "2023-08-18" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_x86_64_msvc]] +version = "0.52.6" +when = "2024-07-03" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.zcash]] +version = "0.1.0" +when = "2024-07-15" +user-id = 6289 +user-login = "str4d" +user-name = "Jack Grigg" + +[[publisher.zcash_address]] +version = "0.6.2" +when = "2024-12-13" +user-id = 6289 +user-login = "str4d" +user-name = "Jack Grigg" + +[[publisher.zcash_client_backend]] +version = "0.16.0" +when = "2024-12-17" +user-id = 169181 +user-login = "nuttycom" +user-name = "Kris Nuttycombe" + +[[publisher.zcash_client_sqlite]] +version = "0.14.0" +when = "2024-12-17" +user-id = 169181 +user-login = "nuttycom" +user-name = "Kris Nuttycombe" + +[[publisher.zcash_encoding]] +version = "0.2.2" +when = "2024-12-13" +user-id = 6289 +user-login = "str4d" +user-name = "Jack Grigg" + +[[publisher.zcash_extensions]] +version = "0.1.0" +when = "2024-07-15" +user-id = 6289 +user-login = "str4d" +user-name = "Jack Grigg" + +[[publisher.zcash_history]] +version = "0.4.0" +when = "2024-03-01" +user-id = 6289 +user-login = "str4d" +user-name = "Jack Grigg" + +[[publisher.zcash_keys]] +version = "0.6.0" +when = "2024-12-17" +user-id = 169181 +user-login = "nuttycom" +user-name = "Kris Nuttycombe" + +[[publisher.zcash_primitives]] +version = "0.21.0" +when = "2024-12-17" +user-id = 169181 +user-login = "nuttycom" +user-name = "Kris Nuttycombe" + +[[publisher.zcash_proofs]] +version = "0.21.0" +when = "2024-12-17" +user-id = 169181 +user-login = "nuttycom" +user-name = "Kris Nuttycombe" + +[[publisher.zcash_protocol]] +version = "0.4.3" +when = "2024-12-17" +user-id = 169181 +user-login = "nuttycom" +user-name = "Kris Nuttycombe" + +[[publisher.zcash_spec]] +version = "0.1.2" +when = "2024-10-22" +user-id = 6289 +user-login = "str4d" +user-name = "Jack Grigg" + +[[publisher.zcash_transparent]] +version = "0.1.0" +when = "2024-12-17" +user-id = 169181 +user-login = "nuttycom" +user-name = "Kris Nuttycombe" + +[[publisher.zip32]] +version = "0.1.3" +when = "2024-12-13" +user-id = 6289 +user-login = "str4d" +user-name = "Jack Grigg" + +[[publisher.zip321]] +version = "0.2.0" +when = "2024-10-04" +user-id = 169181 +user-login = "nuttycom" +user-name = "Kris Nuttycombe" + +[[audits.bytecode-alliance.wildcard-audits.bumpalo]] +who = "Nick Fitzgerald " +criteria = "safe-to-deploy" +user-id = 696 # Nick Fitzgerald (fitzgen) +start = "2019-03-16" +end = "2025-07-30" + +[[audits.bytecode-alliance.audits.adler]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "1.0.2" +notes = "This is a small crate which forbids unsafe code and is a straightforward implementation of the adler hashing algorithm." + +[[audits.bytecode-alliance.audits.anes]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.1.6" +notes = "Contains no unsafe code, no IO, no build.rs." + +[[audits.bytecode-alliance.audits.anyhow]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +delta = "1.0.69 -> 1.0.71" + +[[audits.bytecode-alliance.audits.arrayref]] +who = "Nick Fitzgerald " +criteria = "safe-to-deploy" +version = "0.3.6" +notes = """ +Unsafe code, but its logic looks good to me. Necessary given what it is +doing. Well tested, has quickchecks. +""" + +[[audits.bytecode-alliance.audits.base64]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.21.0" +notes = "This crate has no dependencies, no build.rs, and contains no unsafe code." + +[[audits.bytecode-alliance.audits.base64]] +who = "Andrew Brown " +criteria = "safe-to-deploy" +delta = "0.21.3 -> 0.22.1" + +[[audits.bytecode-alliance.audits.block-buffer]] +who = "Benjamin Bouvier " +criteria = "safe-to-deploy" +delta = "0.9.0 -> 0.10.2" + +[[audits.bytecode-alliance.audits.cipher]] +who = "Andrew Brown " +criteria = "safe-to-deploy" +version = "0.4.4" +notes = "Most unsafe is hidden by `inout` dependency; only remaining unsafe is raw-splitting a slice and an unreachable hint. Older versions of this regularly reach ~150k daily downloads." + +[[audits.bytecode-alliance.audits.cobs]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "0.2.3" +notes = "No `unsafe` code in the crate and no usage of `std`" + +[[audits.bytecode-alliance.audits.constant_time_eq]] +who = "Nick Fitzgerald " +criteria = "safe-to-deploy" +version = "0.2.4" +notes = "A few tiny blocks of `unsafe` but each of them is very obviously correct." + +[[audits.bytecode-alliance.audits.core-foundation-sys]] +who = "Dan Gohman " +criteria = "safe-to-deploy" +delta = "0.8.4 -> 0.8.6" +notes = """ +The changes here are all typical bindings updates: new functions, types, and +constants. I have not audited all the bindings for ABI conformance. +""" + +[[audits.bytecode-alliance.audits.crypto-common]] +who = "Benjamin Bouvier " +criteria = "safe-to-deploy" +version = "0.1.3" + +[[audits.bytecode-alliance.audits.digest]] +who = "Benjamin Bouvier " +criteria = "safe-to-deploy" +delta = "0.9.0 -> 0.10.3" + +[[audits.bytecode-alliance.audits.embedded-io]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "0.4.0" +notes = "No `unsafe` code and only uses `std` in ways one would expect the crate to do so." + +[[audits.bytecode-alliance.audits.errno]] +who = "Dan Gohman " +criteria = "safe-to-deploy" +version = "0.3.0" +notes = "This crate uses libc and windows-sys APIs to get and set the raw OS error value." + +[[audits.bytecode-alliance.audits.errno]] +who = "Dan Gohman " +criteria = "safe-to-deploy" +delta = "0.3.0 -> 0.3.1" +notes = "Just a dependency version bump and a bug fix for redox" + +[[audits.bytecode-alliance.audits.fallible-iterator]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "0.2.0 -> 0.3.0" +notes = """ +This major version update has a few minor breaking changes but everything +this crate has to do with iterators and `Result` and such. No `unsafe` or +anything like that, all looks good. +""" + +[[audits.bytecode-alliance.audits.fastrand]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "2.0.0 -> 2.0.1" +notes = """ +This update had a few doc updates but no otherwise-substantial source code +updates. +""" + +[[audits.bytecode-alliance.audits.futures-channel]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.3.27" +notes = "build.rs is just detecting the target and setting cfg. unsafety is for implementing a concurrency primitives using atomics and unsafecell, and is not obviously incorrect (this is the sort of thing I wouldn't certify as correct without formal methods)" + +[[audits.bytecode-alliance.audits.futures-core]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.3.27" +notes = "Unsafe used to implement a concurrency primitive AtomicWaker. Well-commented and not obviously incorrect. Like my other audits of these concurrency primitives inside the futures family, I couldn't certify that it is correct without formal methods, but that is out of scope for this vetting." + +[[audits.bytecode-alliance.audits.futures-executor]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.3.27" +notes = "Unsafe used to implement the unpark mutex, which is well commented and not obviously incorrect. Like with futures-channel I wouldn't be able to certify it as correct without formal methods." + +[[audits.bytecode-alliance.audits.futures-io]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.3.27" + +[[audits.bytecode-alliance.audits.heck]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "0.4.1 -> 0.5.0" +notes = "Minor changes for a `no_std` upgrade but otherwise everything looks as expected." + +[[audits.bytecode-alliance.audits.http-body]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "1.0.0-rc.2" + +[[audits.bytecode-alliance.audits.http-body]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "1.0.0-rc.2 -> 1.0.0" +notes = "Only minor changes made for a stable release." + +[[audits.bytecode-alliance.audits.http-body-util]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.1.0-rc.2" +notes = "only one use of unsafe related to pin projection. unclear to me why pin_project! is used in many modules of the project, but the expanded output of that macro is inlined in either.rs" + +[[audits.bytecode-alliance.audits.http-body-util]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "0.1.0-rc.2 -> 0.1.0" +notes = "Minor documentation updates an additions, nothing major." + +[[audits.bytecode-alliance.audits.iana-time-zone-haiku]] +who = "Dan Gohman " +criteria = "safe-to-deploy" +version = "0.1.2" + +[[audits.bytecode-alliance.audits.libm]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "0.2.2 -> 0.2.4" +notes = """ +This diff primarily fixes a few issues with the `fma`-related functions, +but also contains some other minor fixes as well. Everything looks A-OK and +as expected. +""" + +[[audits.bytecode-alliance.audits.libm]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "0.2.4 -> 0.2.7" +notes = """ +This is a minor update which has some testing affordances as well as some +updated math algorithms. +""" + +[[audits.bytecode-alliance.audits.matchers]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.1.0" + +[[audits.bytecode-alliance.audits.miniz_oxide]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "0.7.1" +notes = """ +This crate is a Rust implementation of zlib compression/decompression and has +been used by default by the Rust standard library for quite some time. It's also +a default dependency of the popular `backtrace` crate for decompressing debug +information. This crate forbids unsafe code and does not otherwise access system +resources. It's originally a port of the `miniz.c` library as well, and given +its own longevity should be relatively hardened against some of the more common +compression-related issues. +""" + +[[audits.bytecode-alliance.audits.nu-ansi-term]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.46.0" +notes = "one use of unsafe to call windows specific api to get console handle." + +[[audits.bytecode-alliance.audits.overload]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.1.1" +notes = "small crate, only defines macro-rules!, nicely documented as well" + +[[audits.bytecode-alliance.audits.percent-encoding]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "2.2.0" +notes = """ +This crate is a single-file crate that does what it says on the tin. There are +a few `unsafe` blocks related to utf-8 validation which are locally verifiable +as correct and otherwise this crate is good to go. +""" + +[[audits.bytecode-alliance.audits.pin-utils]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.1.0" + +[[audits.bytecode-alliance.audits.pkg-config]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.3.25" +notes = "This crate shells out to the pkg-config executable, but it appears to sanitize inputs reasonably." + +[[audits.bytecode-alliance.audits.pkg-config]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "0.3.26 -> 0.3.29" +notes = """ +No `unsafe` additions or anything outside of the purview of the crate in this +change. +""" + +[[audits.bytecode-alliance.audits.rustc-demangle]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "0.1.21" +notes = "I am the author of this crate." + +[[audits.bytecode-alliance.audits.semver]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "1.0.17" +notes = "plenty of unsafe pointer and vec tricks, but in well-structured and commented code that appears to be correct" + +[[audits.bytecode-alliance.audits.sharded-slab]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.1.4" +notes = "I always really enjoy reading eliza's code, she left perfect comments at every use of unsafe." + +[[audits.bytecode-alliance.audits.shlex]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "1.1.0" +notes = "Only minor `unsafe` code blocks which look valid and otherwise does what it says on the tin." + +[[audits.bytecode-alliance.audits.signal-hook-registry]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "1.4.1" + +[[audits.bytecode-alliance.audits.thread_local]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "1.1.4" +notes = "uses unsafe to implement thread local storage of objects" + +[[audits.bytecode-alliance.audits.tinyvec]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "1.6.0" +notes = """ +This crate, while it implements collections, does so without `std::*` APIs and +without `unsafe`. Skimming the crate everything looks reasonable and what one +would expect from idiomatic safe collections in Rust. +""" + +[[audits.bytecode-alliance.audits.tokio-rustls]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.24.0" +notes = "no unsafe, no build, no ambient capabilities" + +[[audits.bytecode-alliance.audits.tracing-subscriber]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.3.17" + +[[audits.bytecode-alliance.audits.try-lock]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.2.4" +notes = "Implements a concurrency primitive with atomics, and is not obviously incorrect" + +[[audits.bytecode-alliance.audits.vcpkg]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.2.15" +notes = "no build.rs, no macros, no unsafe. It reads the filesystem and makes copies of DLLs into OUT_DIR." + +[[audits.bytecode-alliance.audits.want]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.3.0" + +[[audits.bytecode-alliance.audits.webpki-roots]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +delta = "0.22.4 -> 0.23.0" + +[[audits.bytecode-alliance.audits.webpki-roots]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +delta = "0.23.0 -> 0.25.2" + +[[audits.embark-studios.audits.anyhow]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +version = "1.0.58" + +[[audits.embark-studios.audits.ident_case]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +version = "1.0.1" +notes = "No unsafe usage or ambient capabilities" + +[[audits.embark-studios.audits.num_enum]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +version = "0.5.11" +notes = "No unsafe usage or ambient capabilities" + +[[audits.embark-studios.audits.num_enum]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +delta = "0.5.11 -> 0.6.1" +notes = "Minor changes" + +[[audits.embark-studios.audits.num_enum]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +delta = "0.6.1 -> 0.7.0" + +[[audits.embark-studios.audits.num_enum_derive]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +version = "0.5.11" +notes = "Proc macro that generates some unsafe code for conversion but looks sound, no ambient capabilities" + +[[audits.embark-studios.audits.num_enum_derive]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +delta = "0.5.11 -> 0.6.1" +notes = "Minor changes" + +[[audits.embark-studios.audits.num_enum_derive]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +delta = "0.6.1 -> 0.7.0" + +[[audits.embark-studios.audits.tap]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +version = "1.0.1" +notes = "No unsafe usage or ambient capabilities" + +[[audits.embark-studios.audits.thiserror]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +version = "1.0.40" +notes = "Wrapper over implementation crate, found no unsafe or ambient capabilities used" + +[[audits.embark-studios.audits.thiserror-impl]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +version = "1.0.40" +notes = "Found no unsafe or ambient capabilities used" + +[[audits.embark-studios.audits.valuable]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +version = "0.1.0" +notes = "No unsafe usage or ambient capabilities, sane build script" + +[[audits.embark-studios.audits.webpki-roots]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +version = "0.22.4" +notes = "Inspected it to confirm that it only contains data definitions and no runtime code" + +[[audits.fermyon.audits.oorandom]] +who = "Radu Matei " +criteria = "safe-to-run" +version = "11.1.3" + +[[audits.google.audits.anstyle]] +who = "Yu-An Wang " +criteria = "safe-to-run" +version = "1.0.4" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.anstyle]] +who = "Lukasz Anforowicz " +criteria = "safe-to-run" +delta = "1.0.4 -> 1.0.6" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.anstyle]] +who = "danakj " +criteria = "safe-to-run" +delta = "1.0.6 -> 1.0.7" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.anstyle]] +who = "Lukasz Anforowicz " +criteria = "safe-to-run" +delta = "1.0.7 -> 1.0.8" +notes = "Only Cargo.toml changes in the 1.0.7 => 1.0.8 delta." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.async-stream]] +who = "Tyler Mandry " +criteria = "safe-to-deploy" +version = "0.3.4" +notes = "Reviewed on https://fxrev.dev/761470" +aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.async-stream]] +who = "David Koloski " +criteria = "safe-to-deploy" +delta = "0.3.4 -> 0.3.5" +notes = "Reviewed on https://fxrev.dev/906795" +aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.async-stream-impl]] +who = "Tyler Mandry " +criteria = "safe-to-deploy" +version = "0.3.4" +notes = "Reviewed on https://fxrev.dev/761470" +aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.async-stream-impl]] +who = "David Koloski " +criteria = "safe-to-deploy" +delta = "0.3.4 -> 0.3.5" +notes = "Reviewed on https://fxrev.dev/906795" +aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.autocfg]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.1.0" +notes = """ +Grepped for `-i cipher`, `-i crypto`, `'\bfs\b'``, `'\bnet\b'``, `'\bunsafe\b'`` +and there were no hits except for reasonable, client-controlled usage of +`std::fs` in `AutoCfg::with_dir`. + +This crate has been added to Chromium in +https://source.chromium.org/chromium/chromium/src/+/591a0f30c5eac93b6a3d981c2714ffa4db28dbcb +The CL description contains a link to a Google-internal document with audit details. +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.autocfg]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.1.0 -> 1.2.0" +notes = ''' +Grepped for `-i cipher`, `-i crypto`, `'\bfs\b'``, `'\bnet\b'``, `'\bunsafe\b'`` +and nothing changed from the baseline audit of 1.1.0. Skimmed through the +1.1.0 => 1.2.0 delta and everything seemed okay. +''' +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.bitflags]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.3.2" +notes = """ +Security review of earlier versions of the crate can be found at +(Google-internal, sorry): go/image-crate-chromium-security-review + +The crate exposes a function marked as `unsafe`, but doesn't use any +`unsafe` blocks (except for tests of the single `unsafe` function). I +think this justifies marking this crate as `ub-risk-1`. + +Additional review comments can be found at https://crrev.com/c/4723145/31 +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.bitflags]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "2.4.2" +notes = """ +Audit notes: + +* I've checked for any discussion in Google-internal cl/546819168 (where audit + of version 2.3.3 happened) +* `src/lib.rs` contains `#![cfg_attr(not(test), forbid(unsafe_code))]` +* There are 2 cases of `unsafe` in `src/external.rs` but they seem to be + correct in a straightforward way - they just propagate the marker trait's + impl (e.g. `impl bytemuck::Pod`) from the inner to the outer type +* Additional discussion and/or notes may be found in https://crrev.com/c/5238056 +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.bitflags]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "2.4.2 -> 2.5.0" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.bitflags]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "2.5.0 -> 2.6.0" +notes = "The changes from the previous version are negligible and thus it retains the same properties." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.bytemuck]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.16.3" +notes = """ +Review notes from the original audit (of 1.14.3) may be found in +https://crrev.com/c/5362675. Note that this audit has initially missed UB risk +that was fixed in 1.16.2 - see https://github.com/Lokathor/bytemuck/pull/258. +Because of this, the original audit has been edited to certify version `1.16.3` +instead (see also https://crrev.com/c/5771867). +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.byteorder]] +who = "danakj " +criteria = "safe-to-deploy" +version = "1.5.0" +notes = "Unsafe review in https://crrev.com/c/5838022" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.cast]] +who = "George Burgess IV " +criteria = "safe-to-run" +version = "0.3.0" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.cfg-if]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "1.0.0" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.clap]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "4.5.15" +notes = ''' +Grepped for `-i cipher`, `-i crypto`, `'\bfs\b'`, `'\bnet\b'`, `'\bunsafe\b'` +and there were no hits, except for `std::net::IpAddr` usage in +`examples/typed-derive.rs`. +''' +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.clap]] +who = "danakj " +criteria = "safe-to-deploy" +delta = "4.5.15 -> 4.5.17" +notes = "Minor code change and toml changes." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.clap]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "4.5.17 -> 4.5.18" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.clap]] +who = "danakj " +criteria = "safe-to-deploy" +delta = "4.5.18 -> 4.5.20" +notes = "Trivial changes" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.clap_builder]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "4.5.15" +notes = ''' +Grepped for `-i cipher`, `-i crypto`, `'\bfs\b'`, `'\bnet\b'`, `'\bunsafe\b'` +and there were no hits. +''' +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.clap_builder]] +who = "danakj " +criteria = "safe-to-deploy" +delta = "4.5.15 -> 4.5.17" +notes = "No new unsafe, net, fs" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.clap_builder]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "4.5.17 -> 4.5.18" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.clap_builder]] +who = "danakj " +criteria = "safe-to-run" +delta = "4.5.18 -> 4.5.20" +notes = "No new unsafe" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.clap_lex]] +who = "Ying Hsu " +criteria = "safe-to-run" +version = "0.7.0" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.clap_lex]] +who = "Adrian Taylor " +criteria = "safe-to-run" +delta = "0.7.0 -> 0.7.1" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.clap_lex]] +who = "Lukasz Anforowicz " +criteria = "safe-to-run" +delta = "0.7.1 -> 0.7.2" +notes = "No `.rs` changes in the delta." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.cpp_demangle]] +who = "Hidenori Kobayashi " +criteria = "safe-to-run" +version = "0.4.3" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.crc32fast]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.4.2" +notes = """ +Security review of earlier versions of the crate can be found at +(Google-internal, sorry): go/image-crate-chromium-security-review + +Audit comments for 1.4.2 can be found at https://crrev.com/c/4723145. +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.equivalent]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "1.0.1" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.fastrand]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "1.9.0" +notes = """ +`does-not-implement-crypto` is certified because this crate explicitly says +that the RNG here is not cryptographically secure. +""" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.flate2]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.0.30" +notes = ''' +WARNING: This certification is a result of a **partial** audit. The +`any_zlib` code has **not** been audited. Ability to track partial +audits is tracked in https://github.com/mozilla/cargo-vet/issues/380 +Chromium does use the `any_zlib` feature(s). Accidentally depending on +this feature in the future is prevented using the `ban_features` feature +of `gnrt` - see: +https://crrev.com/c/4723145/31/third_party/rust/chromium_crates_io/gnrt_config.toml + +Security review of earlier versions of the crate can be found at +(Google-internal, sorry): go/image-crate-chromium-security-review + +I grepped for `-i cipher`, `-i crypto`, `'\bfs\b'`, `'\bnet\b'`, `'\bunsafe\b'`. + +All `unsafe` in `flate2` is gated behind `#[cfg(feature = "any_zlib")]`: + +* The code under `src/ffi/...` will not be used because the `mod c` + declaration in `src/ffi/mod.rs` depends on the `any_zlib` config +* 7 uses of `unsafe` in `src/mem.rs` also all depend on the + `any_zlib` config: + - 2 in `fn set_dictionary` (under `impl Compress`) + - 2 in `fn set_level` (under `impl Compress`) + - 3 in `fn set_dictionary` (under `impl Decompress`) + +All hits of `'\bfs\b'` are in comments, or example code, or test code +(but not in product code). + +There were no hits of `-i cipher`, `-i crypto`, `'\bnet\b'`. +''' +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.futures]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "0.3.28" +notes = """ +`futures` has no logic other than tests - it simply `pub use`s things from +other crates. +""" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.heck]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "0.4.1" +notes = """ +Grepped for `-i cipher`, `-i crypto`, `'\bfs\b'``, `'\bnet\b'``, `'\bunsafe\b'`` +and there were no hits. + +`heck` (version `0.3.3`) has been added to Chromium in +https://source.chromium.org/chromium/chromium/src/+/28841c33c77833cc30b286f9ae24c97e7a8f4057 +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.httpdate]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "1.0.3" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.is-terminal]] +who = "George Burgess IV " +criteria = "safe-to-run" +version = "0.4.2" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.is-terminal]] +who = "George Burgess IV " +criteria = "safe-to-run" +delta = "0.4.2 -> 0.4.9" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.itoa]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.0.10" +notes = ''' +I grepped for \"crypt\", \"cipher\", \"fs\", \"net\" - there were no hits. + +There are a few places where `unsafe` is used. Unsafe review notes can be found +in https://crrev.com/c/5350697. + +Version 1.0.1 of this crate has been added to Chromium in +https://crrev.com/c/3321896. +''' +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.itoa]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.0.10 -> 1.0.11" +notes = """ +Straightforward diff between 1.0.10 and 1.0.11 - only 3 commits: + +* Bumping up the version +* A touch up of comments +* And my own PR to make `unsafe` blocks more granular: + https://github.com/dtolnay/itoa/pull/42 +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.lazy_static]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.4.0" +notes = ''' +I grepped for \"crypt\", \"cipher\", \"fs\", \"net\" - there were no hits. + +There are two places where `unsafe` is used. Unsafe review notes can be found +in https://crrev.com/c/5347418. + +This crate has been added to Chromium in https://crrev.com/c/3321895. +''' +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.lazy_static]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.4.0 -> 1.5.0" +notes = "Unsafe review notes: https://crrev.com/c/5650836" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.nix]] +who = "David Koloski " +criteria = "safe-to-run" +version = "0.26.2" +notes = """ +Reviewed on https://fxrev.dev/780283 +Issues: +- https://github.com/nix-rust/nix/issues/1975 +- https://github.com/nix-rust/nix/issues/1977 +- https://github.com/nix-rust/nix/pull/1978 +- https://github.com/nix-rust/nix/pull/1979 +- https://github.com/nix-rust/nix/issues/1980 +- https://github.com/nix-rust/nix/issues/1981 +- https://github.com/nix-rust/nix/pull/1983 +- https://github.com/nix-rust/nix/issues/1990 +- https://github.com/nix-rust/nix/pull/1992 +- https://github.com/nix-rust/nix/pull/1993 +""" +aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.nom]] +who = "danakj@chromium.org" +criteria = "safe-to-deploy" +version = "7.1.3" +notes = """ +Reviewed in https://chromium-review.googlesource.com/c/chromium/src/+/5046153 +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.num-iter]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "0.1.43" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.password-hash]] +who = "Joshua Liebow-Feeser " +criteria = "safe-to-deploy" +delta = "0.3.2 -> 0.4.2" +aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.pbkdf2]] +who = "Joshua Liebow-Feeser " +criteria = "safe-to-deploy" +delta = "0.9.0 -> 0.11.0" +aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.pin-project-lite]] +who = "David Koloski " +criteria = "safe-to-deploy" +version = "0.2.9" +notes = "Reviewed on https://fxrev.dev/824504" +aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.pin-project-lite]] +who = "David Koloski " +criteria = "safe-to-deploy" +delta = "0.2.9 -> 0.2.13" +notes = "Audited at https://fxrev.dev/946396" +aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.0.78" +notes = """ +Grepped for \"crypt\", \"cipher\", \"fs\", \"net\" - there were no hits +(except for a benign \"fs\" hit in a doc comment) + +Notes from the `unsafe` review can be found in https://crrev.com/c/5385745. +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "1.0.78 -> 1.0.79" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "1.0.79 -> 1.0.80" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "Dustin J. Mitchell " +criteria = "safe-to-deploy" +delta = "1.0.80 -> 1.0.81" +notes = "Comment changes only" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "Dustin J. Mitchell " +criteria = "safe-to-deploy" +delta = "1.0.82 -> 1.0.83" +notes = "Substantive change is replacing String with Box, saving memory." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.0.83 -> 1.0.84" +notes = "Only doc comment changes in `src/lib.rs`." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "danakj@chromium.org" +criteria = "safe-to-deploy" +delta = "1.0.84 -> 1.0.85" +notes = "Test-only changes." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.0.85 -> 1.0.86" +notes = """ +Comment-only changes in `build.rs`. +Reordering of `Cargo.toml` entries. +Just bumping up the version number in `lib.rs`. +Config-related changes in `test_size.rs`. +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.quote]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.0.35" +notes = """ +Grepped for \"unsafe\", \"crypt\", \"cipher\", \"fs\", \"net\" - there were no hits +(except for benign \"net\" hit in tests and \"fs\" hit in README.md) +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.quote]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "1.0.35 -> 1.0.36" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.ring]] +who = "Laura Peskin " +criteria = "safe-to-deploy" +delta = "0.16.12 -> 0.16.20" +notes = """ +Reviewed on: https://fxrev.dev/923001 (0.16.13 -> 0.16.20) +Reviewed on: https://fxrev.dev/716624 (0.16.12 -> 0.16.13) +""" +aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.rustversion]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.0.14" +notes = """ +Grepped for `-i cipher`, `-i crypto`, `'\bfs\b'``, `'\bnet\b'``, `'\bunsafe\b'`` +and there were no hits except for: + +* Using trivially-safe `unsafe` in test code: + + ``` + tests/test_const.rs:unsafe fn _unsafe() {} + tests/test_const.rs:const _UNSAFE: () = unsafe { _unsafe() }; + ``` + +* Using `unsafe` in a string: + + ``` + src/constfn.rs: \"unsafe\" => Qualifiers::Unsafe, + ``` + +* Using `std::fs` in `build/build.rs` to write `${OUT_DIR}/version.expr` + which is later read back via `include!` used in `src/lib.rs`. + +Version `1.0.6` of this crate has been added to Chromium in +https://source.chromium.org/chromium/chromium/src/+/28841c33c77833cc30b286f9ae24c97e7a8f4057 +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.rustversion]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "1.0.14 -> 1.0.15" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.0.197" +notes = """ +Grepped for `-i cipher`, `-i crypto`, `'\bfs\b'`, `'\bnet\b'`, `'\bunsafe\b'`. + +There were some hits for `net`, but they were related to serialization and +not actually opening any connections or anything like that. + +There were 2 hits of `unsafe` when grepping: +* In `fn as_str` in `impl Buf` +* In `fn serialize` in `impl Serialize for net::Ipv4Addr` + +Unsafe review comments can be found in https://crrev.com/c/5350573/2 (this +review also covered `serde_json_lenient`). + +Version 1.0.130 of the crate has been added to Chromium in +https://crrev.com/c/3265545. The CL description contains a link to a +(Google-internal, sorry) document with a mini security review. +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Dustin J. Mitchell " +criteria = "safe-to-deploy" +delta = "1.0.197 -> 1.0.198" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "danakj " +criteria = "safe-to-deploy" +delta = "1.0.198 -> 1.0.201" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.0.202 -> 1.0.203" +notes = "s/doc_cfg/docsrs/ + tuple_impls/tuple_impl_body-related changes" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "1.0.203 -> 1.0.204" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.0.197" +notes = "Grepped for \"unsafe\", \"crypt\", \"cipher\", \"fs\", \"net\" - there were no hits" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "danakj " +criteria = "safe-to-deploy" +delta = "1.0.197 -> 1.0.201" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.0.202 -> 1.0.203" +notes = "Grepped for \"unsafe\", \"crypt\", \"cipher\", \"fs\", \"net\" - there were no hits" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "1.0.203 -> 1.0.204" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.sha1]] +who = "David Koloski " +criteria = "safe-to-deploy" +version = "0.10.5" +notes = "Reviewed on https://fxrev.dev/712371." +aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.static_assertions]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.1.0" +notes = """ +Grepped for `-i cipher`, `-i crypto`, `'\bfs\b'`, `'\bnet\b'`, `'\bunsafe\b'` +and there were no hits except for one `unsafe`. + +The lambda where `unsafe` is used is never invoked (e.g. the `unsafe` code +never runs) and is only introduced for some compile-time checks. Additional +unsafe review comments can be found in https://crrev.com/c/5353376. + +This crate has been added to Chromium in https://crrev.com/c/3736562. The CL +description contains a link to a document with an additional security review. +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.strsim]] +who = "danakj@chromium.org" +criteria = "safe-to-deploy" +version = "0.10.0" +notes = """ +Reviewed in https://crrev.com/c/5171063 + +Previously reviewed during security review and the audit is grandparented in. +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.strum]] +who = "danakj@chromium.org" +criteria = "safe-to-deploy" +version = "0.25.0" +notes = """ +Reviewed in https://crrev.com/c/5171063 + +Previously reviewed during security review and the audit is grandparented in. +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.strum_macros]] +who = "danakj@chromium.org" +criteria = "safe-to-deploy" +version = "0.25.3" +notes = """ +Reviewed in https://crrev.com/c/5171063 + +Previously reviewed during security review and the audit is grandparented in. +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.tinytemplate]] +who = "Ying Hsu " +criteria = "safe-to-run" +version = "1.2.1" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.tinyvec]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "1.6.0 -> 1.6.1" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.tinyvec]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "1.6.1 -> 1.7.0" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.tinyvec]] +who = "Dustin J. Mitchell " +criteria = "safe-to-deploy" +delta = "1.7.0 -> 1.8.0" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.tinyvec_macros]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "0.1.0" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.tokio-stream]] +who = "David Koloski " +criteria = "safe-to-deploy" +version = "0.1.11" +notes = "Reviewed on https://fxrev.dev/804724" +aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.tokio-stream]] +who = "David Koloski " +criteria = "safe-to-deploy" +delta = "0.1.11 -> 0.1.14" +notes = "Reviewed on https://fxrev.dev/907732." +aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.unicode-ident]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.0.12" +notes = ''' +I grepped for \"crypt\", \"cipher\", \"fs\", \"net\" - there were no hits. + +All two functions from the public API of this crate use `unsafe` to avoid bound +checks for an array access. Cross-module analysis shows that the offsets can +be statically proven to be within array bounds. More details can be found in +the unsafe review CL at https://crrev.com/c/5350386. + +This crate has been added to Chromium in https://crrev.com/c/3891618. +''' +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.unicode-xid]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "0.2.4" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.version_check]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "0.9.4" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.void]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "1.0.2" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.isrg.audits.aes]] +who = "Tim Geoghegan " +criteria = "safe-to-deploy" +delta = "0.8.3 -> 0.8.4" +notes = """ +Change affects some unsafe code. The only functional change is to add an +assertion checking alignment and to change an `as *mut u32` cast to a +call to `std::pointer::cast`. +""" + +[[audits.isrg.audits.base64]] +who = "Tim Geoghegan " +criteria = "safe-to-deploy" +delta = "0.21.0 -> 0.21.1" + +[[audits.isrg.audits.base64]] +who = "Brandon Pitman " +criteria = "safe-to-deploy" +delta = "0.21.1 -> 0.21.2" + +[[audits.isrg.audits.base64]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.21.2 -> 0.21.3" + +[[audits.isrg.audits.block-buffer]] +who = "David Cook " +criteria = "safe-to-deploy" +version = "0.9.0" + +[[audits.isrg.audits.criterion]] +who = "Brandon Pitman " +criteria = "safe-to-run" +delta = "0.4.0 -> 0.5.1" + +[[audits.isrg.audits.crunchy]] +who = "David Cook " +criteria = "safe-to-deploy" +version = "0.2.2" + +[[audits.isrg.audits.digest]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.10.6 -> 0.10.7" + +[[audits.isrg.audits.either]] +who = "David Cook " +criteria = "safe-to-deploy" +version = "1.6.1" + +[[audits.isrg.audits.fiat-crypto]] +who = "David Cook " +criteria = "safe-to-deploy" +version = "0.1.17" +notes = """ +This crate does not contain any unsafe code, and does not use any items from +the standard library or other crates, aside from operations backed by +`std::ops`. All paths with array indexing use integer literals for indexes, so +there are no panics due to indexes out of bounds (as rustc would catch an +out-of-bounds literal index). I did not check whether arithmetic overflows +could cause a panic, and I am relying on the Coq code having satisfied the +necessary preconditions to ensure panics due to overflows are unreachable. +""" + +[[audits.isrg.audits.fiat-crypto]] +who = "Brandon Pitman " +criteria = "safe-to-deploy" +delta = "0.1.17 -> 0.1.18" + +[[audits.isrg.audits.fiat-crypto]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.1.18 -> 0.1.19" +notes = """ +This release renames many items and adds a new module. The code in the new +module is entirely composed of arithmetic and array accesses. +""" + +[[audits.isrg.audits.fiat-crypto]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.1.19 -> 0.1.20" + +[[audits.isrg.audits.fiat-crypto]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.1.20 -> 0.2.0" + +[[audits.isrg.audits.fiat-crypto]] +who = "Brandon Pitman " +criteria = "safe-to-deploy" +delta = "0.2.0 -> 0.2.1" + +[[audits.isrg.audits.fiat-crypto]] +who = "Tim Geoghegan " +criteria = "safe-to-deploy" +delta = "0.2.1 -> 0.2.2" +notes = "No changes to `unsafe` code, or any functional changes that I can detect at all." + +[[audits.isrg.audits.fiat-crypto]] +who = "Brandon Pitman " +criteria = "safe-to-deploy" +delta = "0.2.2 -> 0.2.4" + +[[audits.isrg.audits.fiat-crypto]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.2.4 -> 0.2.5" + +[[audits.isrg.audits.fiat-crypto]] +who = "Brandon Pitman " +criteria = "safe-to-deploy" +delta = "0.2.5 -> 0.2.6" + +[[audits.isrg.audits.fiat-crypto]] +who = "Brandon Pitman " +criteria = "safe-to-deploy" +delta = "0.2.6 -> 0.2.7" + +[[audits.isrg.audits.fiat-crypto]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.2.7 -> 0.2.8" + +[[audits.isrg.audits.fiat-crypto]] +who = "Tim Geoghegan " +criteria = "safe-to-deploy" +delta = "0.2.8 -> 0.2.9" +notes = "No changes to Rust code between 0.2.8 and 0.2.9" + +[[audits.isrg.audits.getrandom]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.2.11 -> 0.2.12" + +[[audits.isrg.audits.getrandom]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.2.12 -> 0.2.14" + +[[audits.isrg.audits.getrandom]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.2.14 -> 0.2.15" + +[[audits.isrg.audits.hmac]] +who = "David Cook " +criteria = "safe-to-deploy" +version = "0.12.1" + +[[audits.isrg.audits.keccak]] +who = "David Cook " +criteria = "safe-to-deploy" +version = "0.1.2" + +[[audits.isrg.audits.keccak]] +who = "Brandon Pitman " +criteria = "safe-to-deploy" +delta = "0.1.2 -> 0.1.3" + +[[audits.isrg.audits.keccak]] +who = "Brandon Pitman " +criteria = "safe-to-deploy" +delta = "0.1.3 -> 0.1.4" + +[[audits.isrg.audits.num-bigint]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.4.3 -> 0.4.4" + +[[audits.isrg.audits.num-integer]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.1.45 -> 0.1.46" + +[[audits.isrg.audits.num-iter]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.1.43 -> 0.1.44" + +[[audits.isrg.audits.num-iter]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.1.44 -> 0.1.45" + +[[audits.isrg.audits.num-traits]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.2.15 -> 0.2.16" + +[[audits.isrg.audits.num-traits]] +who = "Ameer Ghani " +criteria = "safe-to-deploy" +delta = "0.2.16 -> 0.2.17" + +[[audits.isrg.audits.num-traits]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.2.17 -> 0.2.18" + +[[audits.isrg.audits.num-traits]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.2.18 -> 0.2.19" + +[[audits.isrg.audits.once_cell]] +who = "Brandon Pitman " +criteria = "safe-to-deploy" +delta = "1.18.0 -> 1.19.0" + +[[audits.isrg.audits.opaque-debug]] +who = "David Cook " +criteria = "safe-to-deploy" +version = "0.3.0" + +[[audits.isrg.audits.rand_chacha]] +who = "David Cook " +criteria = "safe-to-deploy" +version = "0.3.1" + +[[audits.isrg.audits.rand_core]] +who = "David Cook " +criteria = "safe-to-deploy" +version = "0.6.3" + +[[audits.isrg.audits.rayon]] +who = "Brandon Pitman " +criteria = "safe-to-deploy" +delta = "1.6.1 -> 1.7.0" + +[[audits.isrg.audits.rayon]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "1.7.0 -> 1.8.0" + +[[audits.isrg.audits.rayon]] +who = "Ameer Ghani " +criteria = "safe-to-deploy" +delta = "1.8.0 -> 1.8.1" + +[[audits.isrg.audits.rayon]] +who = "Brandon Pitman " +criteria = "safe-to-deploy" +delta = "1.8.1 -> 1.9.0" + +[[audits.isrg.audits.rayon]] +who = "Brandon Pitman " +criteria = "safe-to-deploy" +delta = "1.9.0 -> 1.10.0" + +[[audits.isrg.audits.rayon-core]] +who = "Ameer Ghani " +criteria = "safe-to-deploy" +version = "1.12.1" + +[[audits.isrg.audits.sha2]] +who = "David Cook " +criteria = "safe-to-deploy" +version = "0.10.2" + +[[audits.isrg.audits.sha3]] +who = "David Cook " +criteria = "safe-to-deploy" +version = "0.10.6" + +[[audits.isrg.audits.sha3]] +who = "Brandon Pitman " +criteria = "safe-to-deploy" +delta = "0.10.6 -> 0.10.7" + +[[audits.isrg.audits.sha3]] +who = "Brandon Pitman " +criteria = "safe-to-deploy" +delta = "0.10.7 -> 0.10.8" + +[[audits.isrg.audits.subtle]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "2.5.0 -> 2.6.1" + +[[audits.isrg.audits.thiserror]] +who = "Brandon Pitman " +criteria = "safe-to-deploy" +delta = "1.0.40 -> 1.0.43" + +[[audits.isrg.audits.thiserror-impl]] +who = "Brandon Pitman " +criteria = "safe-to-deploy" +delta = "1.0.40 -> 1.0.43" + +[[audits.isrg.audits.universal-hash]] +who = "David Cook " +criteria = "safe-to-deploy" +version = "0.4.1" + +[[audits.isrg.audits.universal-hash]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.5.0 -> 0.5.1" + +[[audits.isrg.audits.untrusted]] +who = "David Cook " +criteria = "safe-to-deploy" +version = "0.7.1" + +[[audits.isrg.audits.wasm-bindgen-shared]] +who = "David Cook " +criteria = "safe-to-deploy" +version = "0.2.83" + +[[audits.mozilla.wildcard-audits.core-foundation-sys]] +who = "Bobby Holley " +criteria = "safe-to-deploy" +user-id = 5946 # Jeff Muizelaar (jrmuizel) +start = "2020-10-14" +end = "2023-05-04" +renew = false +notes = "I've reviewed every source contribution that was neither authored nor reviewed by Mozilla." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.wildcard-audits.unicode-normalization]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +user-id = 1139 +start = "2019-11-06" +end = "2024-05-03" +notes = "All code written or reviewed by Manish" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.ahash]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +delta = "0.8.7 -> 0.8.11" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.allocator-api2]] +who = "Nicolas Silva " +criteria = "safe-to-deploy" +version = "0.2.18" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.android_system_properties]] +who = "Nicolas Silva " +criteria = "safe-to-deploy" +version = "0.1.2" +notes = "I wrote this crate, reviewed by jimb. It is mostly a Rust port of some C++ code we already ship." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.android_system_properties]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.1.2 -> 0.1.4" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.android_system_properties]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.1.4 -> 0.1.5" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.anyhow]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "1.0.57 -> 1.0.61" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.anyhow]] +who = "Bobby Holley " +criteria = "safe-to-deploy" +delta = "1.0.58 -> 1.0.57" +notes = "No functional differences, just CI config and docs." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.anyhow]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "1.0.61 -> 1.0.62" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.anyhow]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "1.0.62 -> 1.0.68" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.anyhow]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "1.0.68 -> 1.0.69" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bit-set]] +who = "Aria Beingessner " +criteria = "safe-to-deploy" +version = "0.5.2" +notes = "Another crate I own via contain-rs that is ancient and maintenance mode, no known issues." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bit-set]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.5.2 -> 0.5.3" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bit-vec]] +who = "Aria Beingessner " +criteria = "safe-to-deploy" +version = "0.6.3" +notes = "Another crate I own via contain-rs that is ancient and in maintenance mode but otherwise perfectly fine." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.block-buffer]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.10.2 -> 0.10.3" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.crossbeam-channel]] +who = "Jan-Erik Rediger " +criteria = "safe-to-deploy" +delta = "0.5.8 -> 0.5.11" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.crossbeam-channel]] +who = "Jan-Erik Rediger " +criteria = "safe-to-deploy" +delta = "0.5.11 -> 0.5.12" +notes = "Minimal change fixing a memory leak." +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.crossbeam-queue]] +who = "Matthew Gregan " +criteria = "safe-to-deploy" +version = "0.3.8" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.crossbeam-utils]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +delta = "0.8.19 -> 0.8.20" +notes = "Minor changes." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.crypto-common]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.1.3 -> 0.1.6" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.debugid]] +who = "Gabriele Svelto " +criteria = "safe-to-deploy" +version = "0.8.0" +notes = "This crates was written by Sentry and I've fully audited it as Firefox crash reporting machinery relies on it." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.deranged]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +version = "0.3.11" +notes = """ +This crate contains a decent bit of `unsafe` code, however all internal +unsafety is verified with copious assertions (many are compile-time), and +otherwise the unsafety is documented and left to the caller to verify. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.digest]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.10.3 -> 0.10.6" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.displaydoc]] +who = "Makoto Kato " +criteria = "safe-to-deploy" +version = "0.2.3" +notes = """ +This crate is convenient macros to implement core::fmt::Display trait. +Although `unsafe` is used for test code to call `libc::abort()`, it has no `unsafe` code in this crate. And there is no file access. +It meets the criteria for safe-to-deploy. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.displaydoc]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.2.3 -> 0.2.4" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.document-features]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +version = "0.2.8" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.either]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "1.6.1 -> 1.7.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.either]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "1.7.0 -> 1.8.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.either]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "1.8.0 -> 1.8.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.errno]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.3.1 -> 0.3.3" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.fastrand]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "1.9.0 -> 2.0.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.fnv]] +who = "Bobby Holley " +criteria = "safe-to-deploy" +version = "1.0.7" +notes = "Simple hasher implementation with no unsafe code." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.futures-channel]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.3.27 -> 0.3.28" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.futures-core]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.3.27 -> 0.3.28" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.futures-executor]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.3.27 -> 0.3.28" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.futures-io]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.3.27 -> 0.3.28" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.futures-macro]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.3.21 -> 0.3.23" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.futures-macro]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.3.23 -> 0.3.25" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.futures-macro]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.3.25 -> 0.3.26" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.futures-macro]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.3.26 -> 0.3.28" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.futures-util]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.3.21 -> 0.3.23" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.futures-util]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.3.23 -> 0.3.25" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.futures-util]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.3.25 -> 0.3.26" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.futures-util]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.3.26 -> 0.3.28" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.half]] +who = "John M. Schanck " +criteria = "safe-to-deploy" +version = "1.8.2" +notes = """ +This crate contains unsafe code for bitwise casts to/from binary16 floating-point +format. I've reviewed these and found no issues. There are no uses of ambient +capabilities. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.hashbrown]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +version = "0.12.3" +notes = "This version is used in rust's libstd, so effectively we're already trusting it" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.hashlink]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.7.0 -> 0.8.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.hashlink]] +who = "Mark Hammond " +criteria = "safe-to-deploy" +delta = "0.8.1 -> 0.9.1" +notes = "New CursorMut struct and other relatively straight-forward changes." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.hex]] +who = "Simon Friedberger " +criteria = "safe-to-deploy" +version = "0.4.3" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.litrs]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +version = "0.4.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.log]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +version = "0.4.17" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.log]] +who = "Jan-Erik Rediger " +criteria = "safe-to-deploy" +delta = "0.4.17 -> 0.4.18" +notes = "One dependency removed, others updated (which we don't rely on), some APIs (which we don't use) changed." +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.log]] +who = "Kagami Sascha Rosylight " +criteria = "safe-to-deploy" +delta = "0.4.18 -> 0.4.20" +notes = "Only cfg attribute and internal macro changes and module refactorings" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.memmap2]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.5.4 -> 0.5.7" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.memmap2]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.5.7 -> 0.5.8" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.memmap2]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.5.8 -> 0.5.9" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.memmap2]] +who = "Gabriele Svelto " +criteria = "safe-to-deploy" +delta = "0.5.9 -> 0.8.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.memmap2]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.8.0 -> 0.9.3" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.num-bigint]] +who = "Josh Stone " +criteria = "safe-to-deploy" +version = "0.4.3" +notes = "All code written or reviewed by Josh Stone." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.num-conv]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +version = "0.1.0" +notes = """ +Very straightforward, simple crate. No dependencies, unsafe, extern, +side-effectful std functions, etc. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.num-integer]] +who = "Josh Stone " +criteria = "safe-to-deploy" +version = "0.1.45" +notes = "All code written or reviewed by Josh Stone." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.num-traits]] +who = "Josh Stone " +criteria = "safe-to-deploy" +version = "0.2.15" +notes = "All code written or reviewed by Josh Stone." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.percent-encoding]] +who = "Valentin Gosu " +criteria = "safe-to-deploy" +delta = "2.2.0 -> 2.3.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.percent-encoding]] +who = "Valentin Gosu " +criteria = "safe-to-deploy" +delta = "2.3.0 -> 2.3.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.phf_macros]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.10.0 -> 0.11.2" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.pkg-config]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.3.25 -> 0.3.26" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.powerfmt]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +version = "0.2.0" +notes = """ +A tiny bit of unsafe code to implement functionality that isn't in stable rust +yet, but it's all valid. Otherwise it's a pretty simple crate. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.rand_core]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.6.3 -> 0.6.4" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.rayon]] +who = "Josh Stone " +criteria = "safe-to-deploy" +version = "1.5.3" +notes = "All code written or reviewed by Josh Stone or Niko Matsakis." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.rayon]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "1.5.3 -> 1.6.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.regex-syntax]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.6.26 -> 0.6.27" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.regex-syntax]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.6.27 -> 0.6.28" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.sha2]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.10.2 -> 0.10.6" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.sha2]] +who = "Jeff Muizelaar " +criteria = "safe-to-deploy" +delta = "0.10.6 -> 0.10.8" +notes = """ +The bulk of this is https://github.com/RustCrypto/hashes/pull/490 which adds aarch64 support along with another PR adding longson. +I didn't check the implementation thoroughly but there wasn't anything obviously nefarious. 0.10.8 has been out for more than a year +which suggests no one else has found anything either. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.shlex]] +who = "Max Inden " +criteria = "safe-to-deploy" +delta = "1.1.0 -> 1.3.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.strsim]] +who = "Ben Dean-Kawamura " +criteria = "safe-to-deploy" +delta = "0.10.0 -> 0.11.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.strum]] +who = "Teodor Tanasoaia " +criteria = "safe-to-deploy" +delta = "0.25.0 -> 0.26.3" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.strum_macros]] +who = "Teodor Tanasoaia " +criteria = "safe-to-deploy" +delta = "0.25.3 -> 0.26.4" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.subtle]] +who = "Simon Friedberger " +criteria = "safe-to-deploy" +version = "2.5.0" +notes = "The goal is to provide some constant-time correctness for cryptographic implementations. The approach is reasonable, it is known to be insufficient but this is pointed out in the documentation." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.syn]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "1.0.96 -> 1.0.99" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.syn]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "1.0.99 -> 1.0.107" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.time]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +delta = "0.3.23 -> 0.3.36" +notes = """ +There's a bit of new unsafe code that is self-imposed because they now assert +that ordinals are non-zero. All unsafe code was checked to ensure that the +invariants claimed were true. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-core]] +who = "Kershaw Chang " +criteria = "safe-to-deploy" +version = "0.1.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-core]] +who = "Kershaw Chang " +criteria = "safe-to-deploy" +delta = "0.1.0 -> 0.1.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-core]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +delta = "0.1.1 -> 0.1.2" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-macros]] +who = "Kershaw Chang " +criteria = "safe-to-deploy" +version = "0.2.6" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-macros]] +who = "Kershaw Chang " +criteria = "safe-to-deploy" +delta = "0.2.6 -> 0.2.10" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-macros]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +delta = "0.2.10 -> 0.2.18" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.tinystr]] +who = "Makoto Kato " +criteria = "safe-to-deploy" +version = "0.7.0" +notes = "One of original auther was Zibi Braniecki who worked at Mozilla and maintained by ICU4X developers (Google and Mozilla). I've vetted the one instance of unsafe code." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.tinystr]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.7.0 -> 0.7.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.tinystr]] +who = "Makoto Kato " +criteria = "safe-to-deploy" +delta = "0.7.1 -> 0.7.4" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.tinystr]] +who = "Makoto Kato " +criteria = "safe-to-deploy" +delta = "0.7.4 -> 0.7.6" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.tracing-core]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +version = "0.1.30" +notes = """ +Most unsafe code is in implementing non-std sync primitives. Unsafe impls are +logically correct and justified in comments, and unsafe code is sound and +justified in comments. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.unicode-xid]] +who = "Teodor Tanasoaia " +criteria = "safe-to-deploy" +delta = "0.2.4 -> 0.2.5" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.unicode-xid]] +who = "Jim Blandy " +criteria = "safe-to-deploy" +delta = "0.2.5 -> 0.2.6" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.zerocopy]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +version = "0.7.32" +notes = """ +This crate is `no_std` so doesn't use any side-effectful std functions. It +contains quite a lot of `unsafe` code, however. I verified portions of this. It +also has a large, thorough test suite. The project claims to run tests with +Miri to have stronger soundness checks, and also claims to use formal +verification tools to prove correctness. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.zerocopy-derive]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +version = "0.7.32" +notes = "Clean, safe macros for zerocopy." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.zeroize_derive]] +who = "Benjamin Beurdouche " +criteria = "safe-to-deploy" +version = "1.4.2" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.zcash.audits.ahash]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.8.6 -> 0.8.7" +notes = "Build-time `stdsimd` detection is replaced with a nightly-only feature flag." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.aho-corasick]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "1.1.2 -> 1.1.3" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.anyhow]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.71 -> 1.0.75" +notes = """ +`unsafe` changes are migrating from `core::any::Demand` to `std::error::Request` when the +nightly features are available. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.anyhow]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.75 -> 1.0.77" +notes = """ +- Build script changes are to rerun cargo if the `RUSTC_BOOTSTRAP` env variable + changes, and enable a few more `rustc` config flags. +- Some `unsafe fn`s were altered to add `unsafe` blocks, to make the safety + contracts in the code clearer (instead of using the `unsafe fn`'s implicit + `unsafe` block); no actual `unsafe` changes were made. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.anyhow]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.77 -> 1.0.79" +notes = """ +Build script changes are to refactor the existing probe into a separate file +(which removes a filesystem write), and adjust how it gets rerun in response to +changes in the build environment. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.anyhow]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "1.0.79 -> 1.0.82" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.arrayref]] +who = "Sean Bowe " +criteria = "safe-to-deploy" +delta = "0.3.6 -> 0.3.7" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.backtrace]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.3.69 -> 0.3.71" +notes = "This crate inherently requires a lot of `unsafe` code, but the changes look plausible." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.blake2b_simd]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.1 -> 1.0.2" +notes = "Switches to `constant_time_eq 0.3.0`, which bumps its MSRV to 1.66." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.blake2s_simd]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.1 -> 1.0.2" +notes = "Switches to `constant_time_eq 0.3.0`, which bumps its MSRV to 1.66." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.block-buffer]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.10.3 -> 0.10.4" +notes = "Adds panics to prevent a block size of zero from causing unsoundness." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.bs58]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.5.0 -> 0.5.1" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.bytes]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "1.5.0 -> 1.6.0" +notes = """ +There is significant use of `unsafe` code, but safety requirements are well documented +and appear correct as far as I can see. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.constant_time_eq]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.2.4 -> 0.2.5" +notes = "No code changes." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.constant_time_eq]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.2.5 -> 0.2.6" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.constant_time_eq]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.2.6 -> 0.3.0" +notes = "Replaces some `unsafe` code by bumping MSRV to 1.66 (to access `core::hint::black_box`)." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.cpufeatures]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.2.11 -> 0.2.12" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.crossbeam-deque]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.8.3 -> 0.8.4" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.crossbeam-deque]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.8.4 -> 0.8.5" +notes = "Changes to `unsafe` code look okay." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.crossbeam-epoch]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.9.15 -> 0.9.16" +notes = "Moved an `unsafe` block while removing `scopeguard` dependency." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.crossbeam-epoch]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.9.16 -> 0.9.17" +notes = """ +Changes to `unsafe` code are to replace manual pointer logic with equivalent +`unsafe` stdlib methods, now that MSRV is high enough to use them. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.crossbeam-epoch]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.9.17 -> 0.9.18" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.curve25519-dalek]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "4.1.0 -> 4.1.1" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.curve25519-dalek]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "4.1.1 -> 4.1.2" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.curve25519-dalek]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "4.1.2 -> 4.1.3" +notes = """ +- New unsafe is adding `core::ptr::read_volatile` calls for black box + optimization barriers. +- `build.rs` changes are to use `CARGO_CFG_TARGET_POINTER_WIDTH` instead of + `TARGET` and the `platforms` crate for deciding on the target pointer width. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.curve25519-dalek-derive]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.1.0 -> 0.1.1" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.der]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.7.8 -> 0.7.9" +notes = "The change to ignore RUSTSEC-2023-0071 is correct for this crate." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.ed25519]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "2.2.1 -> 2.2.2" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.ed25519]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "2.2.2 -> 2.2.3" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.ed25519-zebra]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "3.0.0 -> 3.1.0" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.ed25519-zebra]] +who = "Daira Emma Hopwood " +criteria = "safe-to-deploy" +delta = "3.1.0 -> 4.0.0" +notes = """ +Changes are mainly in the pem and pkcs8 features and in Java or Scala code. These do not introduce unsafe code, +but I cannot vouch for their cryptographic correctness or conformance to PEM or PKCS8 standards. I reviewed the +remaining changes from 3.1.0 to 4.0.0 fully. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.ed25519-zebra]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "4.0.0 -> 4.0.3" +notes = """ +`SigningKey::from([u8; 32])` parsing now uses `Scalar::from_bytes_mod_order` instead of +`Scalar::from_bits`. This means that the clamped scalar bits are now reduced before they +are used, which removes the implicit mul-by-cofactor during scalar multiplication (as the +last 3 bits of the scalar are no longer guaranteed to be zero). However, this happens to +be fine in the context of this crate: + +- `SigningKey` does not expose its inner `Scalar` directly, so we only need to consider + how it is used within the crate. +- For multiplication within a prime-order (sub)group, we get the same result whether we + reduce before or not. This means that the field-element multiplication during signing, + and the prime-order subgroup component of any group-element scalar multiplication, are + unaffected. +- The only group element that the `Scalar` is multiplied by is the Ed25519 basepoint, + which is torsion free (so the implicit mul-by-cofactor is unnecessary). +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.either]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.8.1 -> 1.9.0" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.either]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "1.9.0 -> 1.11.0" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.errno]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.3 -> 0.3.8" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.fastrand]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "2.0.1 -> 2.0.2" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.futures-channel]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.28 -> 0.3.29" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.futures-channel]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.29 -> 0.3.30" +notes = "Removes `build.rs` now that it can rely on the `target_has_atomic` attribute." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.futures-core]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.28 -> 0.3.29" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.futures-core]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.29 -> 0.3.30" +notes = "Removes `build.rs` now that it can rely on the `target_has_atomic` attribute." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.futures-task]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.29 -> 0.3.30" +notes = "Removes `build.rs` now that it can rely on the `target_has_atomic` attribute." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.futures-util]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.28 -> 0.3.29" +notes = """ +Only change to `unsafe` code is to add a `Fut: Send` bound to the +`unsafe impl Sync for FuturesUnordered`. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.futures-util]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.29 -> 0.3.30" +notes = """ +- Removes `build.rs` now that it can rely on the `target_has_atomic` attribute. +- Almost all changes to `unsafe` blocks are to either move them around, or + replace them with safe method calls. +- One new `unsafe` block is added for a slice lifetime transmutation. The slice + reconstruction is obviously correct. AFAICT the lifetime transmutation is also + correct; the slice's lifetime logically comes from the `AsyncBufRead` reader + inside `FillBuf`, rather than the `Context`. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.hermit-abi]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.3.3 -> 0.3.9" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.inout]] +who = "Daira Hopwood " +criteria = "safe-to-deploy" +version = "0.1.3" +notes = "Reviewed in full." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.js-sys]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.3.66 -> 0.3.69" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.known-folders]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +version = "1.0.1" +notes = """ +Uses `unsafe` blocks to interact with `windows-sys` crate. +- `SHGetKnownFolderPath` safety requirements are met. +- `CoTaskMemFree` has no effect if passed `NULL`, so there is no issue if some + future refactor created a pathway where `ffi::Guard` could be dropped before + `SHGetKnownFolderPath` is called. +- Small nit: `ffi::Guard::as_pwstr` takes `&self` but returns `PWSTR` which is + the mutable type; it should instead return `PCWSTR` which is the const type + (and what `lstrlenW` takes) instead of implicitly const-casting the pointer, + as this would better reflect the intent to take an immutable reference. +- The slice constructed from the `PWSTR` correctly goes out of scope before + `guard` is dropped. +- A code comment says that `path_ptr` is valid for `len` bytes, but `PCWSTR` is + a `*const u16` and `lstrlenW` returns its length \"in characters\" (which the + Windows documentation confirms means the number of `WCHAR` values). This is + likely a typo; the code checks that `len * size_of::() <= isize::MAX`. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.known-folders]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.1 -> 1.1.0" +notes = "Addresses the notes from my previous review :)" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.libm]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.2.7 -> 0.2.8" +notes = "Forces some intermediate values to not have too much precision on the x87 FPU." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.libredox]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.0.1 -> 0.1.3" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.linux-raw-sys]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.4.12 -> 0.4.13" +notes = "Low-level OS interface crate, so `unsafe` code is expected." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.log]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.4.20 -> 0.4.21" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.maybe-rayon]] +who = "Sean Bowe " +criteria = "safe-to-deploy" +version = "0.1.1" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.memchr]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "2.6.4 -> 2.7.1" +notes = """ +Change to an `unsafe fn` is to rework the short-tail handling of a fixed-length +comparison between `u8` pointers. The new tail code matches the existing head +code (but adapted to `u16` and `u8` reads, instead of `u32`). +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.memchr]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "2.7.1 -> 2.7.2" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.miniz_oxide]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.7.1 -> 0.7.2" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.mio]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.8.10 -> 0.8.11" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.nix]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.26.2 -> 0.26.4" +notes = """ +Most of the `unsafe` changes are cleaning up their usage: +- Replacing `data.len() * std::mem::size_of::<$ty>()` with `std::mem::size_of_val(data)`. +- Removing some `mem::transmute`s. +- Using `*mut` instead of `*const` to convey intended semantics. + +A new unsafe trait method `SockaddrLike::set_length` is added; it's impls look fine. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.object]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.32.1 -> 0.32.2" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.opaque-debug]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.3.0 -> 0.3.1" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.phf]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.8.0 -> 0.11.1" +notes = """ +Mostly modernisation, migrating to `PhfBorrow`, and making more things `&'static`. +No unsafe code in the new `OrderedMap` and `OrderedSet` types. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.phf]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.11.1 -> 0.11.2" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.phf_generator]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.8.0 -> 0.11.1" +notes = "Just dependency and edition bumps and code formatting." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.phf_generator]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.11.1 -> 0.11.2" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.phf_shared]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.8.0 -> 0.11.1" +notes = """ +Adds `uncased` dependency, and newly generates unsafe code to transmute `&'static str` +into `&'static UncasedStr`. I verified that `UncasedStr` is a `#[repr(transparent)]` +newtype around `str`. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.phf_shared]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.11.1 -> 0.11.2" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.pin-project-lite]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.2.13 -> 0.2.14" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.proc-macro-crate]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.2.1 -> 1.3.0" +notes = "Migrates from `toml` to `toml_edit`." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.proc-macro-crate]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.3.0 -> 1.3.1" +notes = "Bumps MSRV to 1.60." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.rand_xorshift]] +who = "Sean Bowe " +criteria = "safe-to-deploy" +version = "0.3.0" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.redox_users]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.4.3 -> 0.4.4" +notes = "Switches from `redox_syscall` crate to `libredox` crate for syscalls." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.redox_users]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.4.4 -> 0.4.5" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.regex]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "1.10.2 -> 1.10.4" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.regex-automata]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.4.3 -> 0.4.6" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.regex-syntax]] +who = "Sean Bowe " +criteria = "safe-to-deploy" +delta = "0.6.28 -> 0.6.29" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.regex-syntax]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.7.5 -> 0.8.2" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.regex-syntax]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.8.2 -> 0.8.3" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.rustc-demangle]] +who = "Sean Bowe " +criteria = "safe-to-deploy" +delta = "0.1.21 -> 0.1.22" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.rustc-demangle]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.1.22 -> 0.1.23" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.rustc_version]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +version = "0.4.0" +notes = """ +Most of the crate is code to parse and validate the output of `rustc -vV`. The caller can +choose which `rustc` to use, or can use `rustc_version::{version, version_meta}` which will +try `$RUSTC` followed by `rustc`. + +If an adversary can arbitrarily set the `$RUSTC` environment variable then this crate will +execute arbitrary code. But when this crate is used within a build script, `$RUSTC` should +be set correctly by `cargo`. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.scopeguard]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.1.0 -> 1.2.0" +notes = "Only change to an `unsafe` block is to replace a `mem::forget` with `ManuallyDrop`." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.semver]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.17 -> 1.0.18" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.semver]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.18 -> 1.0.19" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.semver]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.19 -> 1.0.20" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.semver]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "1.0.20 -> 1.0.22" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.sharded-slab]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.1.4 -> 0.1.7" +notes = "Only change to an `unsafe` block is to fix a clippy lint." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.signature]] +who = "Daira Emma Hopwood " +criteria = "safe-to-deploy" +version = "2.1.0" +notes = """ +This crate uses `#![forbid(unsafe_code)]`, has no build script, and only provides traits with some trivial default implementations. +I did not review whether implementing these APIs would present any undocumented cryptographic hazards. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.signature]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "2.1.0 -> 2.2.0" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.siphasher]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.10 -> 0.3.11" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.socket2]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.5.5 -> 0.5.6" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.syn]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.107 -> 1.0.109" +notes = "Fixes string literal parsing to only skip specified whitespace characters." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.tempfile]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "3.8.1 -> 3.9.0" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.tempfile]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "3.9.0 -> 3.10.1" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.thiserror]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.43 -> 1.0.48" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.thiserror]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.48 -> 1.0.51" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.thiserror]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.51 -> 1.0.52" +notes = "Reruns the build script if the `RUSTC_BOOTSTRAP` env variable changes." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.thiserror]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.52 -> 1.0.56" +notes = """ +Build script changes are to refactor the existing probe into a separate file +(which removes a filesystem write), and adjust how it gets rerun in response to +changes in the build environment. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.thiserror]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "1.0.56 -> 1.0.58" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.thiserror-impl]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.43 -> 1.0.48" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.thiserror-impl]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.48 -> 1.0.51" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.thiserror-impl]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.51 -> 1.0.52" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.thiserror-impl]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.52 -> 1.0.56" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.thiserror-impl]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "1.0.56 -> 1.0.58" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.thread_local]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.1.4 -> 1.1.7" +notes = """ +New `unsafe` usage: +- An extra `deallocate_bucket`, to replace a `Mutex::lock` with a `compare_exchange`. +- Setting and getting a `#[thread_local] static mut Option` on nightly. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.thread_local]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "1.1.7 -> 1.1.8" +notes = """ +Adds `unsafe` code that makes an assumption that `ptr::null_mut::>()` is a valid representation +of an `AtomicPtr>`, but this is likely a correct assumption. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.tinyvec_macros]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.1.0 -> 0.1.1" +notes = "Adds `#![forbid(unsafe_code)]` and license files." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.tokio]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "1.35.1 -> 1.37.0" +notes = "Cursory review, but new and changed uses of `unsafe` code look fine, as far as I can see." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.tracing-core]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.1.30 -> 0.1.31" +notes = """ +The only new `unsafe` block is to intentionally leak a scoped subscriber onto +the heap when setting it as the global default dispatcher. I checked that the +global default can only be set once and is never dropped. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.tracing-core]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.1.31 -> 0.1.32" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.tracing-subscriber]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.17 -> 0.3.18" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.try-lock]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.2.4 -> 0.2.5" +notes = "Bumps MSRV to remove unsafe code block." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.universal-hash]] +who = "Daira Hopwood " +criteria = "safe-to-deploy" +delta = "0.4.1 -> 0.5.0" +notes = "I checked correctness of to_blocks which uses unsafe code in a safe function." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.wagyu-zcash-parameters]] +who = "Sean Bowe " +criteria = "safe-to-deploy" +version = "0.2.0" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.wagyu-zcash-parameters-1]] +who = "Sean Bowe " +criteria = "safe-to-deploy" +version = "0.2.0" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.wagyu-zcash-parameters-2]] +who = "Sean Bowe " +criteria = "safe-to-deploy" +version = "0.2.0" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.wagyu-zcash-parameters-3]] +who = "Sean Bowe " +criteria = "safe-to-deploy" +version = "0.2.0" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.wagyu-zcash-parameters-4]] +who = "Sean Bowe " +criteria = "safe-to-deploy" +version = "0.2.0" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.wagyu-zcash-parameters-5]] +who = "Sean Bowe " +criteria = "safe-to-deploy" +version = "0.2.0" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.wagyu-zcash-parameters-6]] +who = "Sean Bowe " +criteria = "safe-to-deploy" +version = "0.2.0" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.want]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.0 -> 0.3.1" +notes = """ +Migrates to `try-lock 0.2.4` to replace some unsafe APIs that were not marked +`unsafe` (but that were being used safely). +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.wasm-bindgen-backend]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.2.89 -> 0.2.92" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.wasm-bindgen-macro]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.2.89 -> 0.2.92" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.wasm-bindgen-macro-support]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +version = "0.2.92" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.wasm-bindgen-shared]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.2.83 -> 0.2.84" +notes = "Bumps the schema version to add `linked_modules`." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.wasm-bindgen-shared]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.2.84 -> 0.2.87" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.wasm-bindgen-shared]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.2.87 -> 0.2.89" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.wasm-bindgen-shared]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.2.89 -> 0.2.92" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.web-sys]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.3.66 -> 0.3.69" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.which]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "4.4.2 -> 6.0.1" +notes = """ +Mostly refactoring to newer APIs. New `winsafe` dependency is only used to check +for extensionless Windows executables. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" diff --git a/zcash/CHANGELOG.md b/zcash/CHANGELOG.md new file mode 100644 index 0000000000..bc76b10a1d --- /dev/null +++ b/zcash/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog +All notable changes to this library will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this library adheres to Rust's notion of +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Changed +- MSRV is now 1.81.0. + +## [0.1.0] - 2024-07-15 +Initial release that re-exports other crates. Expect that the API surface of +this crate will change significantly in future releases. +MSRV is 1.70.0. diff --git a/zcash/Cargo.toml b/zcash/Cargo.toml new file mode 100644 index 0000000000..327dca65f6 --- /dev/null +++ b/zcash/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "zcash" +version = "0.1.0" +authors = [ + "Jack Grigg ", +] +edition.workspace = true +rust-version.workspace = true +description = "Zcash Rust APIs" +readme = "README.md" +homepage = "https://github.com/zcash/librustzcash" +repository.workspace = true +license.workspace = true +categories.workspace = true + +[dependencies] +# Dependencies exposed in a public API: +# (Breaking upgrades to these require a breaking upgrade to this crate.) +zcash_primitives.workspace = true + +# Dependencies used internally: +# (Breaking upgrades to these are usually backwards-compatible, but check MSRVs.) +# - Documentation +document-features.workspace = true + +[features] +default = ["multicore"] + +## Enables multithreading support for creating proofs. +multicore = ["zcash_primitives/multicore"] + +## Enables use of the transparent payment protocol for inputs. +transparent-inputs = ["zcash_primitives/transparent-inputs"] + +[lints] +workspace = true diff --git a/zcash/README.md b/zcash/README.md new file mode 100644 index 0000000000..d741b3c800 --- /dev/null +++ b/zcash/README.md @@ -0,0 +1,23 @@ +# zcash + +This library exposes APIs for working with the Zcash ecosystem. + +It currently just re-exports the APIs of other crates. Expect that the API +surface of this crate will change significantly in future releases. + +## License + +Licensed under either of + + * Apache License, Version 2.0, ([LICENSE-APACHE](../LICENSE-APACHE) or + http://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](../LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you, as defined in the Apache-2.0 +license, shall be dual licensed as above, without any additional terms or +conditions. diff --git a/zcash/src/lib.rs b/zcash/src/lib.rs new file mode 100644 index 0000000000..a10121533d --- /dev/null +++ b/zcash/src/lib.rs @@ -0,0 +1,14 @@ +//! *Zcash Rust APIs.* +//! +//! ## Feature flags +#![doc = document_features::document_features!()] +//! + +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +// Catch documentation errors caused by code changes. +#![deny(rustdoc::broken_intra_doc_links)] +#![deny(missing_docs)] +#![deny(unsafe_code)] + +pub use zcash_primitives as primitives; diff --git a/zcash_client_backend/.gitignore b/zcash_client_backend/.gitignore deleted file mode 100644 index 7025829d91..0000000000 --- a/zcash_client_backend/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Protobufs -src/proto/ diff --git a/zcash_client_backend/CHANGELOG.md b/zcash_client_backend/CHANGELOG.md index 5ead377dac..e14fce74dd 100644 --- a/zcash_client_backend/CHANGELOG.md +++ b/zcash_client_backend/CHANGELOG.md @@ -6,12 +6,1282 @@ and this library adheres to Rust's notion of [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +### Changed +- MSRV is now 1.81.0. +- Migrated to `bip32 =0.6.0-pre.1`, `nonempty 0.11`, `incrementalmerkletree 0.8`, + `shardtree 0.6`. +- `zcash_client_backend::wallet::Recipient` has changed: + - The `Recipient::External` variant is now a structured variant. + - The `Recipient::EphemeralTransparent` variant is now only available if + `zcash_client_backend` is built using the `transparent-inputs` feature flag. + - The `N` and `O` type pararameters to this type have been replaced by + concrete uses of `Box` and `Outpoint` instead. The + `map_internal_account_note` and `map_ephemeral_transparent_outpoint` and + `internal_account_note_transpose_option` methods have consequently been + removed. +- `zcash_client_backend::data_api::WalletRead::get_known_ephemeral_addresses` + now takes a `Range` as its + argument instead of a `Range` + +### Deprecated +- `zcash_client_backend::address` (use `zcash_keys::address` instead) +- `zcash_client_backend::encoding` (use `zcash_keys::encoding` instead) +- `zcash_client_backend::keys` (use `zcash_keys::keys` instead) +- `zcash_client_backend::zip321` (use the `zip321` crate instead) +- `zcash_client_backend::PoolType` (use `zcash_protocol::PoolType` instead) +- `zcash_client_backend::ShieldedProtocol` (use `zcash_protocol::ShieldedProtocol` instead) + +## [0.16.0] - 2024-12-16 + +### Added +- `zcash_client_backend::data_api` + - `AccountSource::key_derivation` + - `error::PcztError` + - `wallet::ExtractErrT` + - `wallet::create_pczt_from_proposal` + - `wallet::extract_and_store_transaction_from_pczt` + +### Changed +- Migrated to `sapling-crypto 0.4`, `zcash_keys 0.6`, `zcash_primitives 0.21`, + `zcash_proofs 0.21`. +- `zcash_client_backend::data_api::AccountBalance`: Refactored to use `Balance` + for transparent funds (issue #1411). It now has an `unshielded_balance()` + method that returns `Balance`, allowing the unshielded spendable, unshielded + pending change, and unshielded pending non-change values to be tracked + separately. +- `zcash_client_backend::data_api::WalletRead`: + - The `create_account`, `import_account_hd`, and `import_account_ufvk` + methods now each take additional `account_name` and `key_source` arguments. + These allow the wallet backend to store additional metadata that is useful + to applications managing these accounts. +- `zcash_client_backend::data_api::AccountSource`: + - Both `Derived` and `Imported` alternatives of `AccountSource` now have an + additional `key_source` field that is used to convey application-specific + key source metadata. + - The `Copy` impl for this type has been removed. + - The `request` argument to `WalletRead::get_next_available_address` is now optional. +- `zcash_client_backend::data_api::Account` has an additional `name` method + that returns the human-readable name of the account, if any. +- `zcash_client_backend::data_api::error::Error` has new variants: + - `AccountIdNotRecognized` + - `AccountCannotSpend` + - `Pczt` + +### Deprecated +- `AccountBalance::unshielded`. Instead use `unshielded_balance` which + provides a `Balance` value. Its `total()` method can be used to obtain the + total of transparent funds. + +### Removed +- `zcash_client_backend::AccountBalance::add_unshielded_value`. Instead use + `AccountBalance::with_unshielded_balance_mut` with a closure that calls + the appropriate `add_*_value` method(s) of `Balance` on its argument. + Note that the appropriate method(s) depend on whether the funds are + spendable, pending change, or pending non-change (previously, only the + total unshielded value was tracked). + +## [0.15.0] - 2024-11-14 + +### Added +- `zcash_client_backend::data_api`: + - `Progress` + - `WalletSummary::progress` + - `PoolMeta` + - `AccountMeta` + - `impl Default for wallet::input_selection::GreedyInputSelector` + - `BoundedU8` + - `NoteFilter` +- `zcash_client_backend::fees` + - `SplitPolicy` + - `StandardFeeRule` has been moved here from `zcash_primitives::fees`. Relative + to that type, the deprecated `PreZip313` and `Zip313` variants have been + removed. + - `zip317::{MultiOutputChangeStrategy, Zip317FeeRule}` + - `standard::MultiOutputChangeStrategy` +- A new feature flag, `non-standard-fees`, has been added. This flag is now + required in order to make use of any types or methods that enable non-standard + fee calculation. +- `zcash_client_backend::tor::http::cryptex`: + - `LocalExchange`, a variant of the `Exchange` trait without `Send` bounds. + - `DynExchange` + - `DynLocalExchange` + +### Changed +- MSRV is now 1.77.0. +- Migrated to `zcash_primitives 0.20.0`, `zcash_keys 0.5.0`. +- Migrated to `arti-client 0.23`. +- `zcash_client_backend::data_api`: + - `InputSource` has an added method `get_account_metadata` + - `error::Error` has additional variant `Error::Change`. This necessitates + the addition of two type parameters to the `Error` type, + `ChangeErrT` and `NoteRefT`. + - The following methods each now take an additional `change_strategy` + argument, along with an associated `ChangeT` type parameter: + - `wallet::spend` + - `wallet::propose_transfer` + - `wallet::propose_shielding`. This method also now takes an additional + `to_account` argument. + - `wallet::shield_transparent_funds`. This method also now takes an + additional `to_account` argument. + - `wallet::input_selection::InputSelectionError` now has an additional `Change` + variant. This necessitates the addition of two type parameters. + - `wallet::input_selection::InputSelector::propose_transaction` takes an + additional `change_strategy` argument, along with an associated `ChangeT` + type parameter. + - The `wallet::input_selection::InputSelector::FeeRule` associated type has + been removed. The fee rule is now part of the change strategy passed to + `propose_transaction`. + - `wallet::input_selection::ShieldingSelector::propose_shielding` takes an + additional `change_strategy` argument, along with an associated `ChangeT` + type parameter. In addition, it also takes a new `to_account` argument + that identifies the destination account for the shielded notes. + - The `wallet::input_selection::ShieldingSelector::FeeRule` associated type + has been removed. The fee rule is now part of the change strategy passed to + `propose_shielding`. + - The `Change` variant of `wallet::input_selection::GreedyInputSelectorError` + has been removed, along with the additional type parameters it necessitated. + - The arguments to `wallet::input_selection::GreedyInputSelector::new` have + changed. +- `zcash_client_backend::fees`: + - `ChangeStrategy` has changed. It has two new associated types, `MetaSource` + and `AccountMetaT`, and its `FeeRule` associated type now has an additional + `Clone` bound. In addition, it defines a new `fetch_wallet_meta` method, and + the arguments to `compute_balance` have changed. + - `zip317::SingleOutputChangeStrategy` has been made polymorphic in the fee + rule type, and takes an additional type parameter as a consequence. + - The following methods now take an additional `DustOutputPolicy` argument, + and carry an additional type parameter: + - `fixed::SingleOutputChangeStrategy::new` + - `standard::SingleOutputChangeStrategy::new` + - `zip317::SingleOutputChangeStrategy::new` +- `zcash_client_backend::proto::ProposalDecodingError` has modified variants. + `ProposalDecodingError::FeeRuleNotSpecified` has been removed, and + `ProposalDecodingError::FeeRuleNotSupported` has been added to replace it. +- `zcash_client_backend::data_api::fees::fixed` is now available only via the + use of the `non-standard-fees` feature flag. +- `zcash_client_backend::tor::http::cryptex`: + - The `Exchange` trait is no longer object-safe. Replace any existing uses of + `dyn Exchange` with `DynExchange`. + +### Removed +- `zcash_client_backend::data_api`: + - `WalletSummary::scan_progress` and `WalletSummary::recovery_progress` have + been removed. Use `WalletSummary::progress` instead. + - `testing::input_selector` use explicit `InputSelector` constructors + directly instead. + - The deprecated `wallet::create_spend_to_address` and `wallet::spend` + methods have been removed. Use `propose_transfer` and + `create_proposed_transaction` instead. +- `zcash_client_backend::fees`: + - `impl From for ChangeError<...>` + +## [0.14.0] - 2024-10-04 + +### Added +- `zcash_client_backend::data_api`: + - `GAP_LIMIT` + - `WalletSummary::recovery_progress` + - `SpendableNotes::{take_sapling, take_orchard}` + - Tests and testing infrastructure have been migrated from the + `zcash_client_sqlite` internal tests to the `testing` module, and have been + generalized so that they may be used for testing arbitrary implementations + of the `zcash_client_backend::data_api` interfaces. The following have been + added under the `test-dependencies` feature flag as part of this migration: + - `WalletTest` + - `testing::AddressType` + - `testing::CachedBlock` + - `testing::DataStoreFactory` + - `testing::FakeCompactOutput` + - `testing::InitialChainState` + - `testing::NoteCommitments` + - `testing::Reset` + - `testing::TestAccount` + - `testing::TestBuilder` + - `testing::TestCache` + - `testing::TestFvk` + - `testing::TestState` + - `testing::TransactionSummary` + - `testing::input_selector` + - `testing::orchard` + - `testing::pool` + - `testing::sapling` + +### Changed +- Migrated to `orchard 0.10`, `sapling-crypto 0.3`, `shardtree 0.5`, + `zcash_address 0.6`, `zcash_primitives 0.19`, `zcash_proofs 0.19`, + `zcash_protocol 0.4`. +- The `Account` trait now uses an associated type for its `AccountId` + type instead of a type parameter. This change allows for the simplification + of some type signatures. +- `zcash_client_backend::data_api`: + - `WalletSummary::scan_progress` now only reports progress for scanning blocks + "near" the chain tip. Progress for scanning earlier blocks is now reported + via `WalletSummary::recovery_progress`. + - `WalletRead::get_min_unspent_height` has been removed. This was added to make + it possible to obtain a "safe truncation" height in order to facilitate rewinds + to a greater depth than the available note commitment tree checkpoints provide, + but such rewinds are no longer supported. +- `zcash_client_backend::sync::run`: + - Transparent outputs are now refreshed in addition to shielded notes. +- `zcash_client_backend::proposal::ProposalError` has a new `AnchorNotFound` + variant. + +### Fixed +- `zcash_client_backend::tor::grpc` now needs the `lightwalletd-tonic-tls-webpki-roots` + feature flag instead of `lightwalletd-tonic`, to fix compilation issues. + +## [0.13.0] - 2024-08-20 + +`zcash_client_backend` now supports TEX (transparent-source-only) addresses as specified +in ZIP 320. Sending to one or more TEX addresses will automatically create a multi-step +proposal that uses two transactions. + +In order to take advantage of this support, client wallets will need to be able to send +multiple transactions created from `zcash_client_backend::data_api::wallet::create_proposed_transactions`. +This API was added in `zcash_client_backend` 0.11.0 but previously could only return a +single transaction. + +**Note:** This feature changes the use of transparent addresses in ways that are relevant +to security and access to funds, and that may interact with other wallet behaviour. In +particular it exposes new ephemeral transparent addresses belonging to the wallet, which +need to be scanned in order to recover funds if the first transaction of the proposal is +mined but the second is not, or if someone (e.g. the TEX-address recipient) sends back +funds to those addresses. See [ZIP 320](https://zips.z.cash/zip-0320) for details. + +### Added +- `zcash_client_backend::data_api`: + - `chain::BlockCache` trait, behind the `sync` feature flag. + - `WalletRead::get_spendable_transparent_outputs` + - `DecryptedTransaction::mined_height` + - `TransactionDataRequest` + - `TransactionStatus` + - `AccountType` +- `zcash_client_backend::fees`: + - `EphemeralBalance` + - `ChangeValue::shielded, is_ephemeral` + - `ChangeValue::ephemeral_transparent` (when "transparent-inputs" is enabled) + - `sapling::EmptyBundleView` + - `orchard::EmptyBundleView` +- `zcash_client_backend::proposal`: + - `impl Hash for {StepOutput, StepOutputIndex}` +- `zcash_client_backend::scanning`: + - `testing` module +- `zcash_client_backend::sync` module, behind the `sync` feature flag. +- `zcash_client_backend::tor` module, behind the `tor` feature flag. +- `zcash_client_backend::wallet`: + - `Recipient::map_ephemeral_transparent_outpoint` + - `WalletTransparentOutput::mined_height` + +### Changed +- MSRV is now 1.70.0. +- Updated dependencies: + - `zcash_address 0.4` + - `zcash_encoding 0.2.1` + - `zcash_keys 0.3` + - `zcash_primitives 0.16` + - `zcash_protocol 0.2` + - `zip321 0.1` +- Migrated to `tonic 0.12`. + - The `lightwalletd-tonic` feature flag no longer works on `wasm32-wasi` due + to https://github.com/hyperium/tonic/issues/1783. +- `zcash_client_backend::{fixed,standard,zip317}::SingleOutputChangeStrategy` + now implement a different strategy for choosing whether there will be any + change, and its value. This can avoid leaking information about note amounts + in some cases. It also ensures that there will be a change output whenever a + `change_memo` is given, and defends against losing money by using + `DustAction::AddDustToFee` with a too-high dust threshold. + See [#1430](https://github.com/zcash/librustzcash/pull/1430) for details. +- `zcash_client_backend::zip321` has been extracted to, and is now a reexport + of the root module of the `zip321` crate. Several of the APIs of this module + have changed as a consequence of this extraction; please see the `zip321` + CHANGELOG for details. +- `zcash_client_backend::data_api`: + - `WalletRead` has a new `transaction_data_requests` method. + - `WalletRead` has new `get_known_ephemeral_addresses`, + `find_account_for_ephemeral_address`, and `get_transparent_address_metadata` + methods when the "transparent-inputs" feature is enabled. + - `WalletWrite` has a new `reserve_next_n_ephemeral_addresses` method when + the "transparent-inputs" feature is enabled. + - `WalletWrite` has new methods `import_account_hd`, `import_account_ufvk`, + and `set_transaction_status`. + - `error::Error` has new `Address` and (when the "transparent-inputs" feature + is enabled) `PaysEphemeralTransparentAddress` variants. + - The `WalletWrite::store_sent_tx` method has been renamed to + `store_transactions_to_be_sent`, and its signature changed to take a slice + of `SentTransaction`s. This can be used by the wallet storage backend (e.g. + `zcash_client_sqlite`) to improve transactionality of writes for multi-step + proposals. + - `wallet::input_selection::InputSelectorError` has a new `Address` variant. + - `wallet::decrypt_and_store_transaction` now takes an additional optional + `mined_height` argument that can be used to provide the mined height + returned by the light wallet server in a `RawTransaction` value directly to + the back end. + - `DecryptedTransaction::new` takes an additional `mined_height` argument. + - `SentTransaction` now stores its `outputs` and `utxos_spent` fields as + references to slices, with a corresponding change to `SentTransaction::new`. + - `SentTransaction` takes an additional `target_height` argument, which is used + to record the target height used in transaction generation. + - `AccountSource::Imported` is now a struct variant with a `purpose` field. + - The `Account` trait now defines a new `purpose` method with a default + implementation (which need not be overridden.) +- `zcash_client_backend::data_api::fees` + - When the "transparent-inputs" feature is enabled, `ChangeValue` can also + represent an ephemeral transparent output in a proposal. Accordingly, the + return type of `ChangeValue::output_pool` has (unconditionally) changed + from `ShieldedProtocol` to `zcash_protocol::PoolType`. + - `ChangeStrategy::compute_balance`: this trait method has an additional + `Option<&EphemeralBalance>` parameter. If the "transparent-inputs" feature is + enabled, this can be used to specify whether the change memo should be + ignored, and the amounts of additional transparent P2PKH inputs and + outputs. Passing `None` will retain the previous + behaviour (and is necessary when the "transparent-inputs" feature is + not enabled). +- `zcash_client_backend::input_selection::GreedyInputSelectorError` has a + new variant `UnsupportedTexAddress`. +- `zcash_client_backend::proposal::ProposalError` has new variants + `SpendsChange`, `EphemeralOutputLeftUnspent`, and `PaysTexFromShielded`. + (the last two are conditional on the "transparent-inputs" feature). +- `zcash_client_backend::proto`: + - `ProposalDecodingError` has a new variant `InvalidEphemeralRecipient`. + - `proposal::Proposal::{from_standard_proposal, try_into_standard_proposal}` + each no longer require a `consensus::Parameters` argument. +- `zcash_client_backend::wallet::Recipient` variants have changed. Instead of + wrapping protocol-address types, the `External` and `InternalAccount` variants + now wrap a `zcash_address::ZcashAddress`. This simplifies the process of + tracking the original address to which value was sent. There is also a new + `EphemeralTransparent` variant, and an additional generic parameter for the + type of metadata associated with an ephemeral transparent outpoint. +- `zcash_client_backend::wallet::WalletTransparentOutput::from_parts` + now takes its height argument as `Option` rather than + `BlockHeight`. + +### Removed +- `zcash_client_backend::data_api`: + - `WalletRead::get_unspent_transparent_outputs` has been removed because its + semantics were unclear and could not be clarified. Use + `WalletRead::get_spendable_transparent_outputs` instead. +- `zcash_client_backend::fees::ChangeValue::new`. Use `ChangeValue::shielded` + or `ChangeValue::ephemeral_transparent` instead. +- `zcash_client_backend::wallet::WalletTransparentOutput::height` + (use `WalletTransparentOutput::mined_height` instead). + +## [0.12.1] - 2024-03-27 + +### Fixed +- This release fixes a problem in note selection when sending to a transparent + recipient, whereby available funds were being incorrectly excluded from + input selection. + +## [0.12.0] - 2024-03-25 + +### Added +- A new `orchard` feature flag has been added to make it possible to + build client code without `orchard` dependencies. Additions and + changes related to `Orchard` below are introduced under this feature + flag. +- `zcash_client_backend::data_api`: + - `Account` + - `AccountBalance::with_orchard_balance_mut` + - `AccountBirthday::orchard_frontier` + - `AccountSource` + - `BlockMetadata::orchard_tree_size` + - `DecryptedTransaction::{new, tx(), orchard_outputs()}` + - `NoteRetention` + - `ScannedBlock::orchard` + - `ScannedBlockCommitments::orchard` + - `SeedRelevance` + - `SentTransaction::new` + - `SpendableNotes` + - `ORCHARD_SHARD_HEIGHT` + - `BlockMetadata::orchard_tree_size` + - `WalletSummary::next_orchard_subtree_index` + - `chain::ChainState` + - `chain::ScanSummary::{spent_orchard_note_count, received_orchard_note_count}` + - `impl Debug for chain::CommitmentTreeRoot` +- `zcash_client_backend::fees`: + - `orchard` + - `ChangeValue::orchard` +- `zcash_client_backend::proto`: + - `service::TreeState::orchard_tree` + - `service::TreeState::to_chain_state` + - `impl TryFrom<&CompactOrchardAction> for CompactAction` + - `CompactOrchardAction::{cmx, nf, ephemeral_key}` +- `zcash_client_backend::scanning`: + - `impl ScanningKeyOps for ScanningKey<..>` for Orchard key types. + - `ScanningKeys::orchard` + - `Nullifiers::{orchard, extend_orchard, retain_orchard}` + - `TaggedOrchardBatch` + - `TaggedOrchardBatchRunner` +- `zcash_client_backend::wallet`: + - `Note::Orchard` + - `WalletOrchardSpend` + - `WalletOrchardOutput` + - `WalletTx::{orchard_spends, orchard_outputs}` + - `ReceivedNote::map_note` + - `ReceivedNote<_, sapling::Note>::note_value` + - `ReceivedNote<_, orchard::note::Note>::note_value` +- `zcash_client_backend::zip321::Payment::without_memo` + +### Changed +- `zcash_client_backend::data_api`: + - Arguments to `AccountBirthday::from_parts` have changed. + - Arguments to `BlockMetadata::from_parts` have changed. + - Arguments to `ScannedBlock::from_parts` have changed. + - Changes to the `WalletRead` trait: + - Added `Account` associated type. + - Added `validate_seed` method. + - Added `is_seed_relevant_to_any_derived_accounts` method. + - Added `get_account` method. + - Added `get_derived_account` method. + - `get_account_for_ufvk` now returns `Self::Account` instead of a bare + `AccountId`. + - Added `get_orchard_nullifiers` method. + - `get_transaction` now returns `Result, _>` rather + than returning an `Err` if the `txid` parameter does not correspond to + a transaction in the database. + - `WalletWrite::create_account` now takes its `AccountBirthday` argument by + reference. + - Changes to the `InputSource` trait: + - `select_spendable_notes` now takes its `target_value` argument as a + `NonNegativeAmount`. Also, it now returns a `SpendableNotes` data + structure instead of a vector. + - Fields of `DecryptedTransaction` are now private. Use `DecryptedTransaction::new` + and the newly provided accessors instead. + - Fields of `SentTransaction` are now private. Use `SentTransaction::new` + and the newly provided accessors instead. + - `ShieldedProtocol` has a new `Orchard` variant. + - `WalletCommitmentTrees` + - `type OrchardShardStore` + - `fn with_orchard_tree_mut` + - `fn put_orchard_subtree_roots` + - Removed `Error::AccountNotFound` variant. + - `WalletSummary::new` now takes an additional `next_orchard_subtree_index` + argument when the `orchard` feature flag is enabled. +- `zcash_client_backend::decrypt`: + - Fields of `DecryptedOutput` are now private. Use `DecryptedOutput::new` + and the newly provided accessors instead. + - `decrypt_transaction` now returns a `DecryptedTransaction` + instead of a `DecryptedOutput` and will decrypt Orchard + outputs when the `orchard` feature is enabled. In addition, the type + constraint on its `` parameter has been strengthened to `Copy`. +- `zcash_client_backend::fees`: + - Arguments to `ChangeStrategy::compute_balance` have changed. + - `ChangeError::DustInputs` now has an `orchard` field behind the `orchard` + feature flag. +- `zcash_client_backend::proto`: + - `ProposalDecodingError` has a new variant `TransparentMemo`. +- `zcash_client_backend::wallet::Recipient::InternalAccount` is now a structured + variant with an additional `external_address` field. +- `zcash_client_backend::zip321::render::amount_str` now takes a + `NonNegativeAmount` rather than a signed `Amount` as its argument. +- `zcash_client_backend::zip321::parse::parse_amount` now parses a + `NonNegativeAmount` rather than a signed `Amount`. +- `zcash_client_backend::zip321::TransactionRequest::total` now + returns `Result<_, BalanceError>` instead of `Result<_, ()>`. + +### Removed +- `zcash_client_backend::PoolType::is_receiver`: use + `zcash_keys::Address::has_receiver` instead. +- `zcash_client_backend::wallet::ReceivedNote::traverse_opt` removed as + unnecessary. + +### Fixed +- This release fixes an error in amount parsing in `zip321` that previously + allowed amounts having a decimal point but no decimal value to be parsed + as valid. + +## [0.11.1] - 2024-03-09 + +### Fixed +- Documentation now correctly builds with all feature flags. + +## [0.11.0] - 2024-03-01 + +### Added +- `zcash_client_backend`: + - `{PoolType, ShieldedProtocol}` (moved from `zcash_client_backend::data_api`). + - `PoolType::is_receiver` +- `zcash_client_backend::data_api`: + - `InputSource` + - `ScannedBlock::{into_commitments, sapling}` + - `ScannedBundles` + - `ScannedBlockCommitments` + - `Balance::{add_spendable_value, add_pending_change_value, add_pending_spendable_value}` + - `AccountBalance::{ + with_sapling_balance_mut, + add_unshielded_value + }` + - `WalletSummary::next_sapling_subtree_index` + - `wallet`: + - `propose_standard_transfer_to_address` + - `create_proposed_transactions` + - `input_selection`: + - `ShieldingSelector`, behind the `transparent-inputs` feature flag + (refactored out from the `InputSelector` trait). + - `impl std::error::Error for InputSelectorError` +- `zcash_client_backend::fees`: + - `standard` and `sapling` modules. + - `ChangeValue::new` +- `zcash_client_backend::wallet`: + - `{NoteId, Recipient}` (moved from `zcash_client_backend::data_api`). + - `Note` + - `ReceivedNote` + - `Recipient::{map_internal_account, internal_account_transpose_option}` + - `WalletOutput` + - `WalletSaplingOutput::{key_source, account_id, recipient_key_scope}` + - `WalletSaplingSpend::account_id` + - `WalletSpend` + - `WalletTx::new` + - `WalletTx` getter methods `{txid, block_index, sapling_spends, sapling_outputs}` + (replacing what were previously public fields.) + - `TransparentAddressMetadata` (which replaces `zcash_keys::address::AddressMetadata`). + - `impl {Debug, Clone} for OvkPolicy` +- `zcash_client_backend::proposal`: + - `Proposal::{shielded_inputs, payment_pools, single_step, multi_step}` + - `ShieldedInputs` + - `Step` +- `zcash_client_backend::proto`: + - `PROPOSAL_SER_V1` + - `ProposalDecodingError` + - `proposal` module, for parsing and serializing transaction proposals. + - `impl TryFrom<&CompactSaplingOutput> for CompactOutputDescription` +- `zcash_client_backend::scanning`: + - `ScanningKeyOps` has replaced the `ScanningKey` trait. + - `ScanningKeys` + - `Nullifiers` +- `impl Clone for zcash_client_backend::{ + zip321::{Payment, TransactionRequest, Zip321Error, parse::Param, parse::IndexedParam}, + wallet::WalletTransparentOutput, + proposal::Proposal, + }` +- `impl {PartialEq, Eq} for zcash_client_backend::{ + zip321::{Zip321Error, parse::Param, parse::IndexedParam}, + wallet::WalletTransparentOutput, + proposal::Proposal, + }` +- `zcash_client_backend::zip321`: + - `TransactionRequest::{total, from_indexed}` + - `parse::Param::name` + ### Changed -- MSRV is now 1.51.0. -- Renamed the following in `zcash_client_backend::data_api` to use lower-case - abbreviations (matching Rust naming conventions): - - `error::Error::InvalidExtSK` to `Error::InvalidExtSk` - - `testing::MockWalletDB` to `testing::MockWalletDb` +- Migrated to `zcash_primitives 0.14`, `orchard 0.7`. +- Several structs and functions now take an `AccountId` type parameter + in order to decouple the concept of an account identifier from + the ZIP 32 account index. Many APIs that previously referenced + `zcash_primitives::zip32::AccountId` now reference the generic type. + Impacted types and functions are: + - `zcash_client_backend::data_api`: + - `WalletRead` now has an associated `AccountId` type. + - `WalletRead::{ + get_account_birthday, + get_current_address, + get_unified_full_viewing_keys, + get_account_for_ufvk, + get_wallet_summary, + get_sapling_nullifiers, + get_transparent_receivers, + get_transparent_balances, + get_account_ids + }` now refer to the `WalletRead::AccountId` associated type. + - `WalletWrite::{create_account, get_next_available_address}` + now refer to the `WalletRead::AccountId` associated type. + - `ScannedBlock` now takes an additional `AccountId` type parameter. + - `DecryptedTransaction` is now parameterized by `AccountId` + - `SentTransaction` is now parameterized by `AccountId` + - `SentTransactionOutput` is now parameterized by `AccountId` + - `WalletSummary` is now parameterized by `AccountId` + - `zcash_client_backend::decrypt` + - `DecryptedOutput` is now parameterized by `AccountId` + - `decrypt_transaction` is now parameterized by `AccountId` + - `zcash_client_backend::scanning::scan_block` is now parameterized by `AccountId` + - `zcash_client_backend::wallet`: + - `Recipient` now takes an additional `AccountId` type parameter. + - `WalletTx` now takes an additional `AccountId` type parameter. + - `WalletSaplingSpend` now takes an additional `AccountId` type parameter. + - `WalletSaplingOutput` now takes an additional `AccountId` type parameter. +- `zcash_client_backend::data_api`: + - `BlockMetadata::sapling_tree_size` now returns an `Option` instead of + a `u32` for future consistency with Orchard. + - `ScannedBlock` is no longer parameterized by the nullifier type as a consequence + of the `WalletTx` change. + - `ScannedBlock::metadata` has been renamed to `to_block_metadata` and now + returns an owned value rather than a reference. + - Fields of `Balance` and `AccountBalance` have been made private and the values + of these fields have been made available via methods having the same names + as the previously-public fields. + - `WalletSummary::new` now takes an additional `next_sapling_subtree_index` argument. + - `WalletSummary::new` now takes a `HashMap` instead of a `BTreeMap` for its + `account_balances` argument. + - `WalletSummary::account_balances` now returns a `HashMap` instead of a `BTreeMap`. + - Changes to the `WalletRead` trait: + - Added associated type `AccountId`. + - Added `get_account` function. + - `get_checkpoint_depth` has been removed without replacement. This is no + longer needed given the change to use the stored anchor height for + transaction proposal execution. + - `is_valid_account_extfvk` has been removed; it was unused in the ECC + mobile wallet SDKs and has been superseded by `get_account_for_ufvk`. + - `get_spendable_sapling_notes`, `select_spendable_sapling_notes`, and + `get_unspent_transparent_outputs` have been removed; use + `data_api::InputSource` instead. + - Added `get_account_ids`. + - `get_transparent_receivers` and `get_transparent_balances` are now + guarded by the `transparent-inputs` feature flag, with noop default + implementations provided. + - `get_transparent_receivers` now returns + `Option` as part of + its result where previously it returned `zcash_keys::address::AddressMetadata`. + - `WalletWrite::get_next_available_address` now takes an additional + `UnifiedAddressRequest` argument. + - `chain::scan_cached_blocks` now returns a `ScanSummary` containing metadata + about the scanned blocks on success. + - `error::Error` enum changes: + - The `NoteMismatch` variant now wraps a `NoteId` instead of a + backend-specific note identifier. The related `NoteRef` type parameter has + been removed from `error::Error`. + - New variants have been added: + - `Error::UnsupportedChangeType` + - `Error::NoSupportedReceivers` + - `Error::NoSpendingKey` + - `Error::Proposal` + - `Error::ProposalNotSupported` + - Variant `ChildIndexOutOfRange` has been removed. + - `wallet`: + - `shield_transparent_funds` no longer takes a `memo` argument; instead, + memos to be associated with the shielded outputs should be specified in + the construction of the value of the `input_selector` argument, which is + used to construct the proposed shielded values as internal "change" + outputs. Also, it returns its result as a `NonEmpty` instead of a + single `TxId`. + - `create_proposed_transaction` has been replaced by + `create_proposed_transactions`. Relative to the prior method, the new + method has the following changes: + - It no longer takes a `change_memo` argument; instead, change memos are + represented in the individual values of the `proposed_change` field of + the `Proposal`'s `TransactionBalance`. + - `create_proposed_transactions` takes its `proposal` argument by + reference instead of as an owned value. + - `create_proposed_transactions` no longer takes a `min_confirmations` + argument. Instead, it uses the anchor height from its `proposal` + argument. + - `create_proposed_transactions` forces implementations to ignore the + database identifiers for its contained notes by universally quantifying + the `NoteRef` type parameter. + - It returns a `NonEmpty` instead of a single `TxId` value. + - `create_spend_to_address` now takes additional `change_memo` and + `fallback_change_pool` arguments. It also returns its result as a + `NonEmpty` instead of a single `TxId`. + - `spend` returns its result as a `NonEmpty` instead of a single + `TxId`. + - The error type of `create_spend_to_address` has been changed to use + `zcash_primitives::transaction::fees::zip317::FeeError` instead of + `zcash_primitives::transaction::components::amount::BalanceError`. Yes + this is confusing because `create_spend_to_address` is explicitly not + using ZIP 317 fees; it's just an artifact of the internal implementation, + and the error variants are not specific to ZIP 317. + - The following methods now take `&impl SpendProver, &impl OutputProver` + instead of `impl TxProver`: + - `create_proposed_transactions` + - `create_spend_to_address` + - `shield_transparent_funds` + - `spend` + - `propose_shielding` and `shield_transparent_funds` now take their + `min_confirmations` arguments as `u32` rather than a `NonZeroU32`, to + permit implementations to enable zero-conf shielding. + - `input_selection`: + - `InputSelector::propose_shielding` has been moved out to the + newly-created `ShieldingSelector` trait. + - `ShieldingSelector::propose_shielding` has been altered such that it + takes an explicit `target_height` in order to minimize the + capabilities that the `data_api::InputSource` trait must expose. Also, + it now takes its `min_confirmations` argument as `u32` instead of + `NonZeroU32`. + - The `InputSelector::DataSource` associated type has been renamed to + `InputSource`. + - `InputSelectorError` has added variant `Proposal`. + - The signature of `InputSelector::propose_transaction` has been altered + such that it longer takes `min_confirmations` as an argument, instead + taking explicit `target_height` and `anchor_height` arguments. This + helps to minimize the set of capabilities that the + `data_api::InputSource` must expose. + - `GreedyInputSelector` now has relaxed requirements for its `InputSource` + associated type. +- `zcash_client_backend::proposal`: + - Arguments to `Proposal::from_parts` have changed. + - `Proposal::min_anchor_height` has been removed in favor of storing this + value in `SaplingInputs`. + - `Proposal::sapling_inputs` has been replaced by `Proposal::shielded_inputs` + - In addition to having been moved to the `zcash_client_backend::proposal` + module, the `Proposal` type has been substantially modified in order to make + it possible to represent multi-step transactions, such as a deshielding + transaction followed by a zero-conf transfer as required by ZIP 320. Individual + transaction proposals are now represented by the `proposal::Step` type. + - `ProposalError` has new variants: + - `ReferenceError` + - `StepDoubleSpend` + - `ChainDoubleSpend` + - `PaymentPoolsMismatch` +- `zcash_client_backend::fees`: + - `ChangeStrategy::compute_balance` arguments have changed. + - `ChangeValue` is now a struct. In addition to the existing change value, it + now also provides the output pool to which change should be sent and an + optional memo to be associated with the change output. + - `ChangeError` has a new `BundleError` variant. + - `fixed::SingleOutputChangeStrategy::new`, + `zip317::SingleOutputChangeStrategy::new`, and + `standard::SingleOutputChangeStrategy::new` each now accept additional + `change_memo` and `fallback_change_pool` arguments. +- `zcash_client_backend::wallet`: + - `Recipient` is now polymorphic in the type of the payload for wallet-internal + recipients. This simplifies the handling of wallet-internal outputs. + - `SentTransactionOutput::from_parts` now takes a `Recipient`. + - `SentTransactionOutput::recipient` now returns a `Recipient`. + - `OvkPolicy::Custom` is now a structured variant that can contain independent + Sapling and Orchard `OutgoingViewingKey`s. + - `WalletSaplingOutput::from_parts` arguments have changed. + - `WalletSaplingOutput::nf` now returns an `Option`. + - `WalletTx` is no longer parameterized by the nullifier type; instead, the + nullifier is present as an optional value. +- `zcash_client_backend::scanning`: + - Arguments to `scan_blocks` have changed. + - `ScanError` has new variants `TreeSizeInvalid` and `EncodingInvalid`. + - `ScanningKey` is now a concrete type that bundles an incoming viewing key + with an optional nullifier key and key source metadata. The trait that + provides uniform access to scanning key information is now `ScanningKeyOps`. +- `zcash_client_backend::zip321`: + - `TransactionRequest::payments` now returns a `BTreeMap` + instead of `&[Payment]` so that parameter indices may be preserved. + - `TransactionRequest::to_uri` now returns a `String` instead of an + `Option` and provides canonical serialization for the empty + proposal. + - `TransactionRequest::from_uri` previously stripped payment indices, meaning + that round-trip serialization was not supported. Payment indices are now + retained. +- The following fields now have type `NonNegativeAmount` instead of `Amount`: + - `zcash_client_backend::data_api`: + - `error::Error::InsufficientFunds.{available, required}` + - `wallet::input_selection::InputSelectorError::InsufficientFunds.{available, required}` + - `zcash_client_backend::fees`: + - `ChangeError::InsufficientFunds.{available, required}` + - `zcash_client_backend::zip321::Payment.amount` +- The following methods now take `NonNegativeAmount` instead of `Amount`: + - `zcash_client_backend::data_api`: + - `SentTransactionOutput::from_parts` + - `wallet::create_spend_to_address` + - `wallet::input_selection::InputSelector::propose_shielding` + - `zcash_client_backend::fees`: + - `ChangeValue::sapling` + - `DustOutputPolicy::new` + - `TransactionBalance::new` +- The following methods now return `NonNegativeAmount` instead of `Amount`: + - `zcash_client_backend::data_api::SentTransactionOutput::value` + - `zcash_client_backend::fees`: + - `ChangeValue::value` + - `DustOutputPolicy::dust_threshold` + - `TransactionBalance::{fee_required, total}` + - `zcash_client_backend::wallet::WalletTransparentOutput::value` + +### Deprecated +- `zcash_client_backend::data_api::wallet`: + - `spend` (use `propose_transfer` and `create_proposed_transactions` instead). + +### Removed +- `zcash_client_backend::wallet`: + - `ReceivedSaplingNote` (use `zcash_client_backend::ReceivedNote` instead). + - `input_selection::{Proposal, ShieldedInputs, ProposalError}` (moved to + `zcash_client_backend::proposal`). + - `SentTransactionOutput::sapling_change_to` - the note created by an internal + transfer is now conveyed in the `recipient` field. + - `WalletSaplingOutput::cmu` (use `WalletSaplingOutput::note` and + `sapling_crypto::Note::cmu` instead). + - `WalletSaplingOutput::account` (use `WalletSaplingOutput::account_id` instead) + - `WalletSaplingSpend::account` (use `WalletSaplingSpend::account_id` instead) + - `WalletTx` fields `{txid, index, sapling_spends, sapling_outputs}` (use + the new getters instead.) +- `zcash_client_backend::data_api`: + - `{PoolType, ShieldedProtocol}` (moved to `zcash_client_backend`). + - `{NoteId, Recipient}` (moved to `zcash_client_backend::wallet`). + - `ScannedBlock::from_parts` + - `ScannedBlock::{sapling_tree_size, sapling_nullifier_map, sapling_commitments}` + (use `ScannedBundles::{tree_size, nullifier_map, commitments}` instead). + - `ScannedBlock::into_sapling_commitments` + (use `ScannedBlock::into_commitments` instead). + - `wallet::create_proposed_transaction` + (use `wallet::create_proposed_transactions` instead). + - `chain::ScanSummary::from_parts` +- `zcash_client_backend::proposal`: + - `Proposal::min_anchor_height` (use `ShieldedInputs::anchor_height` instead). + - `Proposal::sapling_inputs` (use `Proposal::shielded_inputs` instead). + +## [0.10.0] - 2023-09-25 + +### Notable Changes +- `zcash_client_backend` now supports out-of-order scanning of blockchain history. + See the module documentation for `zcash_client_backend::data_api::chain` + for details on how to make use of the new scanning capabilities. +- This release of `zcash_client_backend` defines the concept of an account + birthday. The account birthday is defined as the minimum height among blocks + to be scanned when recovering an account. +- Account creation now requires the caller to provide account birthday information, + including the state of the note commitment tree at the end of the block prior + to the birthday height. A wallet's birthday is the earliest birthday height + among accounts maintained by the wallet. + +### Added +- `impl Eq for zcash_client_backend::address::RecipientAddress` +- `impl Eq for zcash_client_backend::zip321::{Payment, TransactionRequest}` +- `impl Debug` for `zcash_client_backend::{data_api::wallet::input_selection::Proposal, wallet::ReceivedSaplingNote}` +- `zcash_client_backend::data_api`: + - `AccountBalance` + - `AccountBirthday` + - `Balance` + - `BirthdayError` + - `BlockMetadata` + - `NoteId` + - `NullifierQuery` for use with `WalletRead::get_sapling_nullifiers` + - `Ratio` + - `ScannedBlock` + - `ShieldedProtocol` + - `WalletCommitmentTrees` + - `WalletSummary` + - `WalletRead::{ + chain_height, block_metadata, block_max_scanned, block_fully_scanned, + suggest_scan_ranges, get_wallet_birthday, get_account_birthday, get_wallet_summary + }` + - `WalletWrite::{put_blocks, update_chain_tip}` + - `chain::CommitmentTreeRoot` + - `scanning` A new module containing types required for `suggest_scan_ranges` + - `testing::MockWalletDb::new` + - `wallet::input_selection::Proposal::{min_target_height, min_anchor_height}` + - `SAPLING_SHARD_HEIGHT` constant +- `zcash_client_backend::proto::compact_formats`: + - `impl From<&sapling::SpendDescription> for CompactSaplingSpend` + - `impl From<&sapling::OutputDescription> for CompactSaplingOutput` + - `impl From<&orchard::Action> for CompactOrchardAction` +- `zcash_client_backend::wallet::WalletSaplingOutput::note_commitment_tree_position` +- `zcash_client_backend::scanning`: + - `ScanError` + - `impl ScanningKey for &K` + - `impl ScanningKey for (zip32::Scope, sapling::SaplingIvk, sapling::NullifierDerivingKey)` +- Test utility functions `zcash_client_backend::keys::UnifiedSpendingKey::{default_address, + default_transparent_address}` are now available under the `test-dependencies` feature flag. + +### Changed +- MSRV is now 1.65.0. +- Bumped dependencies to `hdwallet 0.4`, `zcash_primitives 0.13`, `zcash_note_encryption 0.4`, + `incrementalmerkletree 0.5`, `orchard 0.6`, `bs58 0.5`, `tempfile 3.5.0`, `prost 0.12`, + `tonic 0.10`. +- `zcash_client_backend::data_api`: + - `WalletRead::TxRef` has been removed in favor of consistently using `TxId` instead. + - `WalletRead::get_transaction` now takes a `TxId` as its argument. + - `WalletRead::create_account` now takes an additional `birthday` argument. + - `WalletWrite::{store_decrypted_tx, store_sent_tx}` now return `Result<(), Self::Error>` + as the `WalletRead::TxRef` associated type has been removed. Use + `WalletRead::get_transaction` with the transaction's `TxId` instead. + - `WalletRead::get_memo` now takes a `NoteId` as its argument instead of `Self::NoteRef` + and returns `Result, Self::Error>` instead of `Result` in order to make representable wallet states where the full + note plaintext is not available. + - `WalletRead::get_nullifiers` has been renamed to `WalletRead::get_sapling_nullifiers` + and its signature has changed; it now subsumes the removed `WalletRead::get_all_nullifiers`. + - `WalletRead::get_target_and_anchor_heights` now takes its argument as a `NonZeroU32` + - `chain::scan_cached_blocks` now takes a `from_height` argument that + permits the caller to control the starting position of the scan range. + In addition, the `limit` parameter is now required and has type `usize`. + - `chain::BlockSource::with_blocks` now takes its limit as an `Option` + instead of `Option`. It is also now required to return an error if + `from_height` is set to a block that does not exist in `self`. + - A new `CommitmentTree` variant has been added to `data_api::error::Error` + - `wallet::{create_spend_to_address, create_proposed_transaction, + shield_transparent_funds}` all now require that `WalletCommitmentTrees` be + implemented for the type passed to them for the `wallet_db` parameter. + - `wallet::create_proposed_transaction` now takes an additional + `min_confirmations` argument. + - `wallet::{spend, create_spend_to_address, shield_transparent_funds, + propose_transfer, propose_shielding, create_proposed_transaction}` now take their + respective `min_confirmations` arguments as `NonZeroU32` + - A new `Scan` variant replaces the `Chain` variant of `data_api::chain::error::Error`. + The `NoteRef` parameter to `data_api::chain::error::Error` has been removed + in favor of using `NoteId` to report the specific note for which a failure occurred. + - A new `SyncRequired` variant has been added to `data_api::wallet::input_selection::InputSelectorError`. + - The variants of the `PoolType` enum have changed; the `PoolType::Sapling` variant has been + removed in favor of a `PoolType::Shielded` variant that wraps a `ShieldedProtocol` value. +- `zcash_client_backend::wallet`: + - `SpendableNote` has been renamed to `ReceivedSaplingNote`. + - Arguments to `WalletSaplingOutput::from_parts` have changed. +- `zcash_client_backend::data_api::wallet::input_selection::InputSelector`: + - Arguments to `{propose_transaction, propose_shielding}` have changed. + - `InputSelector::{propose_transaction, propose_shielding}` + now take their respective `min_confirmations` arguments as `NonZeroU32` +- `zcash_client_backend::data_api::wallet::{create_spend_to_address, spend, + create_proposed_transaction, shield_transparent_funds}` now return the `TxId` + for the newly created transaction instead an internal database identifier. +- `zcash_client_backend::wallet::ReceivedSaplingNote::note_commitment_tree_position` + has replaced the `witness` field in the same struct. +- `zcash_client_backend::welding_rig` has been renamed to `zcash_client_backend::scanning` +- `zcash_client_backend::scanning::ScanningKey::sapling_nf` has been changed to + take a note position instead of an incremental witness for the note. +- Arguments to `zcash_client_backend::scanning::scan_block` have changed. This + method now takes an optional `BlockMetadata` argument instead of a base commitment + tree and incremental witnesses for each previously-known note. In addition, the + return type has now been updated to return a `Result`. +- `zcash_client_backend::proto::service`: + - The module is no longer behind the `lightwalletd-tonic` feature flag; that + now only gates the `service::compact_tx_streamer_client` submodule. This + exposes the service types to parse messages received by other gRPC clients. + - The module has been updated to include the new gRPC endpoints supported by + `lightwalletd` v0.4.15. + +### Removed +- `zcash_client_backend::data_api`: + - `WalletRead::block_height_extrema` has been removed. Use `chain_height` + instead to obtain the wallet's view of the chain tip instead, or + `suggest_scan_ranges` to obtain information about blocks that need to be + scanned. + - `WalletRead::get_balance_at` has been removed. Use `WalletRead::get_wallet_summary` + instead. + - `WalletRead::{get_all_nullifiers, get_commitment_tree, get_witnesses}` have + been removed without replacement. The utility of these methods is now + subsumed by those available from the `WalletCommitmentTrees` trait. + - `WalletWrite::advance_by_block` (use `WalletWrite::put_blocks` instead). + - `PrunedBlock` has been replaced by `ScannedBlock` + - `testing::MockWalletDb`, which is available under the `test-dependencies` + feature flag, has been modified by the addition of a `sapling_tree` property. + - `wallet::input_selection`: + - `Proposal::target_height` (use `Proposal::min_target_height` instead). +- `zcash_client_backend::data_api::chain::validate_chain` (logic merged into + `chain::scan_cached_blocks`). +- `zcash_client_backend::data_api::chain::error::{ChainError, Cause}` have been + replaced by `zcash_client_backend::scanning::ScanError` +- `zcash_client_backend::proto::compact_formats`: + - `impl From> for CompactSaplingOutput` + (use `From<&sapling::OutputDescription>` instead). +- `zcash_client_backend::wallet::WalletSaplingOutput::{witness, witness_mut}` + have been removed as individual incremental witnesses are no longer tracked on a + per-note basis. The global note commitment tree for the wallet should be used + to obtain witnesses for spend operations instead. +- Default implementations of `zcash_client_backend::data_api::WalletRead::{ + get_target_and_anchor_heights, get_max_height_hash + }` have been removed. These should be implemented in a backend-specific fashion. + + +## [0.9.0] - 2023-04-28 +### Added +- `data_api::SentTransactionOutput::from_parts` +- `data_api::WalletRead::get_min_unspent_height` + +### Changed +- `decrypt::DecryptedOutput` is now parameterized by a `Note` type parameter, + to allow reuse of the data structure for non-Sapling contexts. +- `data_api::SentTransactionOutput` must now be constructed using + `SentTransactionOutput::from_parts`. The internal state of `SentTransactionOutput` + is now private, and accessible via methods that have the same names as the + previously exposed fields. + +### Renamed +- The following types and fields have been renamed in preparation for supporting + `orchard` in wallet APIs: + - `WalletTx::shielded_spends` -> `WalletTx::sapling_spends` + - `WalletTx::shielded_outputs` -> `WalletTx::sapling_outputs` + - `WalletShieldedSpend` -> `WalletSaplingSpend`. Also, the internals of this + data structure have been made private. + - `WalletShieldedOutput` -> `WalletSaplingOutput`. Also, the internals of this + data structure have been made private. +- The `data_api::WalletWrite::rewind_to_height` method has been renamed to + `truncate_to_height` to better reflect its semantics. + +### Removed + - `wallet::WalletTx::num_spends` + - `wallet::WalletTx::num_outputs` + - `wallet::WalletSaplingOutput::to` is redundant and has been removed; the + recipient address can be obtained from the note. + - `decrypt::DecryptedOutput::to` is redundant and has been removed; the + recipient address can be obtained from the note. + +## [0.8.0] - 2023-04-15 +### Changed +- Bumped dependencies to `bls12_381 0.8`, `group 0.13`, `orchard 0.4`, + `tonic 0.9`, `base64 0.21`, `bech32 0.9`, `zcash_primitives 0.11`. +- The dependency on `zcash_primitives` no longer enables the `multicore` feature + by default in order to support compilation under `wasm32-wasi`. Users of other + platforms may need to include an explicit dependency on `zcash_primitives` + without `default-features = false` or otherwise explicitly enable the + `zcash_primitives/multicore` feature if they did not already depend + upon `zcash_primitives` with default features enabled. + +### Fixed +- `zcash_client_backend::fees::zip317::SingleOutputChangeStrategy` now takes + into account the Sapling output padding behaviour of + `zcash_primitives::transaction::components::sapling::builder::SaplingBuilder`. + +## [0.7.0] - 2023-02-01 +### Added +- `zcash_client_backend::data_api::wallet`: + - `input_selection::Proposal::{is_shielding, target_height}` + - `propose_transfer` + - `propose_shielding` + - `create_proposed_transaction` + +### Changed +- MSRV is now 1.60.0. +- Bumped dependencies to `zcash_primitives 0.10`. +- `zcash_client_backend::data_api::chain`: + - `BlockSource::with_blocks` now takes `from_height` as `Option` + instead of `BlockHeight`. Trait implementors should return all available + blocks in the datastore when `from_height` is `None`. + - Various **breaking changes** to `validate_chain`: + - The `parameters: &ParamsT` argument has been removed. When `None` is given + as the `validate_from` argument, `validate_chain` will now pass `None` to + `BlockSource::with_blocks` (instead of the Sapling network upgrade's + activation height). + - A `limit: Option` argument has been added. This enables callers to + validate smaller intervals of blocks already present on the provided + `BlockSource`, shortening processing times of the function call at the + expense of obtaining a partial result. When providing a `limit`, a result + of `Ok(())` means that the chain has been validated on its continuity of + heights and hashes in the range `[validate_from, validate_from + limit)`. + Callers are responsible for making subsequent calls to `validate_chain` in + order to complete validating the totality of `block_source`. +- `zcash_client_backend::data_api::wallet`: + - `input_selection::Proposal` no longer has a `TransparentInput` generic + parameter, and `Proposal::transparent_inputs` now returns + `&[zcash_client_backend::wallet::WalletTransparentOutput]`. + - `shield_transparent_funds` now takes a `shielding_threshold` argument that + can be used to specify the minimum value allowed as input to a shielding + transaction. Previously the shielding threshold was fixed at 100000 zatoshis. +- Note commitments now use + `zcash_primitives::sapling::note::ExtractedNoteCommitment` instead of + `bls12_381::Scalar` in the following places: + - The `cmu` field of `zcash_client_backend::wallet::WalletShieldedOutput`. + - `zcash_client_backend::proto::compact_formats::CompactSaplingOutput::cmu`. + +### Removed +- `zcash_client_backend::data_api`: + - `WalletWrite::remove_unmined_tx` (was behind the `unstable` feature flag). + +## [0.6.1] - 2022-12-06 +### Added +- `zcash_client_backend::data_api::chain::scan_cached_blocks` now generates + `tracing` spans, which can be used for profiling. + +### Fixed +- `zcash_client_backend:zip321` no longer returns an error when trying to parse + a URI without query parameters. + +## [0.6.0] - 2022-11-12 +### Added +- Functionality that enables the receiving and spending of transparent funds, + behind the new `transparent-inputs` feature flag. + - A new `zcash_client_backend::data_api::wallet::shield_transparent_funds` + method has been added to facilitate the automatic shielding of transparent + funds received by the wallet. + - A `zcash_client_backend::wallet::WalletTransparentOutput` type in support of + `transparent-inputs` functionality. +- An `unstable` feature flag; this is added to parts of the API that may change + in any release. +- `zcash_client_backend::address`: + - `RecipientAddress::Unified` + - `AddressMetadata` + - `impl Eq for UnifiedAddress` +- `zcash_client_backend::data_api`: + - `wallet::spend` method, intended to supersede the `wallet::create_spend_to_address` + method. This new method now constructs transactions via interpretation of a + `zcash_client_backend::zip321::TransactionRequest` value. This facilitates + the implementation of ZIP 321 support in wallets and provides substantially + greater flexibility in transaction creation. + - `PoolType` + - `ShieldedPool` + - `Recipient` + - `SentTransactionOutput` + - `WalletRead::get_unified_full_viewing_keys` + - `WalletRead::get_account_for_ufvk` + - `WalletRead::get_current_address` + - `WalletRead::get_all_nullifiers` + - `WalletRead::get_transparent_receivers` + - `WalletRead::get_unspent_transparent_outputs` + - `WalletRead::get_transparent_balances` + - `WalletWrite::create_account` + - `WalletWrite::remove_unmined_tx` (behind the `unstable` feature flag). + - `WalletWrite::get_next_available_address` + - `WalletWrite::put_received_transparent_utxo` + - `impl From for error::Error` + - `chain::error`: a module containing error types that can occur only + in chain validation and sync, separated out from errors related to + other wallet operations. + - `input_selection`: a module containing types related to the process + of selecting inputs to be spent, given a transaction request. +- `zcash_client_backend::decrypt`: + - `TransferType` +- `zcash_client_backend::proto`: + - `actions` field on `compact_formats::CompactTx` + - `compact_formats::CompactOrchardAction` + - gRPC bindings for the `lightwalletd` server, behind a `lightwalletd-tonic` + feature flag. +- `zcash_client_backend::zip321::TransactionRequest` methods: + - `TransactionRequest::empty` for constructing a new empty request. + - `TransactionRequest::new` for constructing a request from `Vec`. + - `TransactionRequest::payments` for accessing the `Payments` that make up a + request. +- `zcash_client_backend::encoding` + - `KeyError` + - `AddressCodec` implementations for `sapling::PaymentAddress` and + `UnifiedAddress`. +- `zcash_client_backend::fees` + - `ChangeError` + - `ChangeStrategy` + - `ChangeValue` + - `TransactionBalance` + - `fixed`, a module containing change selection strategies for the old fixed + fee rule. + - `zip317`, a module containing change selection strategies for the ZIP 317 + fee rule. +- New experimental APIs that should be considered unstable, and are + likely to be modified and/or moved to a different module in a future + release: + - `zcash_client_backend::address::UnifiedAddress` + - `zcash_client_backend::keys::{UnifiedSpendingKey, UnifiedFullViewingKey, Era, DecodingError}` + - `zcash_client_backend::encoding::AddressCodec` + - `zcash_client_backend::encoding::encode_payment_address` + - `zcash_client_backend::encoding::encode_transparent_address` + +### Changed +- MSRV is now 1.56.1. +- Bumped dependencies to `ff 0.12`, `group 0.12`, `bls12_381 0.7` + `zcash_primitives 0.9`, `orchard 0.3`. +- `zcash_client_backend::proto`: + - The Protocol Buffers bindings are now generated for `prost 0.11` instead of + `protobuf 2`. + - `compact_formats::CompactSpend` has been renamed to `CompactSaplingSpend`, + and its `epk` field (and associated `set_epk` method) has been renamed to + `ephemeralKey` (and `set_ephemeralKey`). + - `compact_formats::CompactOutput` has been renamed to `CompactSaplingOutput`. +- `epk: jubjub::ExtendedPoint` has been replaced by + `ephemeral_key: zcash_note_encryption::EphemeralKeyBytes` in various places: + - `zcash_client_backend::wallet::WalletShieldedOutput`: the `epk` field has + been replaced by `ephemeral_key`. + - `zcash_client_backend::proto::compact_formats::CompactSaplingOutput`: the + `epk` method has been replaced by `ephemeral_key`. +- `zcash_client_backend::data_api`: + - Renamed the following to use lower-case abbreviations (matching Rust naming + conventions): + - `testing::MockWalletDB` to `testing::MockWalletDb` + - Changes to the `WalletRead` trait: + - `WalletRead::get_target_and_anchor_heights` now takes + a `min_confirmations` argument that is used to compute an upper bound on + the anchor height being returned; this had previously been hardcoded to + `wallet::ANCHOR_OFFSET`. + - `WalletRead::get_spendable_notes` has been renamed to + `get_spendable_sapling_notes`, and now takes as an argument a vector of + note IDs to be excluded from consideration. + - `WalletRead::select_spendable_notes` has been renamed to + `select_spendable_sapling_notes`, and now takes as an argument a vector of + note IDs to be excluded from consideration. + - The `WalletRead::NoteRef` and `WalletRead::TxRef` associated types are now + required to implement `Eq` and `Ord` + - `WalletWrite::store_received_tx` has been renamed to `store_decrypted_tx`. + - `wallet::decrypt_and_store_transaction` now always stores the transaction by + calling `WalletWrite::store_decrypted_tx`, even if no outputs could be + decrypted. The error type produced by the provided `WalletWrite` instance is + also now returned directly. + - The `SentTransaction` type has been substantially modified to accommodate + handling of transparent inputs. Per-output data has been split out into a + new struct `SentTransactionOutput`, and `SentTransaction` can now contain + multiple outputs, and tracks the fee paid. + - `ReceivedTransaction` has been renamed to `DecryptedTransaction`, and its + `outputs` field has been renamed to `sapling_outputs`. + - `BlockSource` has been moved to the `chain` module. + - The types of the `with_row` callback argument to `BlockSource::with_blocks` + and the return type of this method have been modified to return + `chain::error::Error`. + - `testing::MockBlockSource` has been moved to + `chain::testing::MockBlockSource` module. + - `chain::{validate_chain, scan_cached_blocks}` have altered parameters and + result types. The latter have been modified to return`chain::error::Error` + instead of abstract error types. This new error type now wraps the errors of + the block source and wallet database to which these methods delegate IO + operations directly, which simplifies error handling in cases where callback + functions are involved. + - `error::ChainInvalid` has been moved to `chain::error`. + - `error::Error` has been substantially modified. It now wraps database, + note selection, builder, and other errors. + - Added new error cases: + - `Error::DataSource` + - `Error::NoteSelection` + - `Error::BalanceError` + - `Error::MemoForbidden` + - `Error::AddressNotRecognized` + - `Error::ChildIndexOutOfRange` + - `Error::NoteMismatch` + - `Error::InsufficientBalance` has been renamed to `InsufficientFunds` and + restructured to have named fields. + - `Error::Protobuf` has been removed; these decoding errors are now + produced as data source and/or block-source implementation-specific + errors. + - `Error::InvalidChain` has been removed; its former purpose is now served + by `chain::ChainError`. + - `Error::InvalidNewWitnessAnchor` and `Error::InvalidWitnessAnchor` have + been moved to `chain::error::ContinuityError`. + - `Error::InvalidExtSk` (now unused) has been removed. + - `Error::KeyNotFound` (now unused) has been removed. + - `Error::KeyDerivationError` (now unused) has been removed. + - `Error::SaplingNotActive` (now unused) has been removed. +- `zcash_client_backend::decrypt`: + - `decrypt_transaction` now takes a `HashMap<_, UnifiedFullViewingKey>` + instead of `HashMap<_, ExtendedFullViewingKey>`. +- If no memo is provided when sending to a shielded recipient, the + empty memo will be used. +- `zcash_client_backend::keys::spending_key` has been moved to the + `zcash_client_backend::keys::sapling` module. +- `zcash_client_backend::zip321::MemoError` has been renamed and + expanded into a more comprehensive `Zip321Error` type, and functions in the + `zip321` module have been updated to use this unified error type. The + following error cases have been added: + - `Zip321Error::TooManyPayments(usize)` + - `Zip321Error::DuplicateParameter(parse::Param, usize)` + - `Zip321Error::TransparentMemo(usize)` + - `Zip321Error::RecipientMissing(usize)` + - `Zip321Error::ParseError(String)` +- `zcash_client_backend::welding_rig`: + - The API of `ScanningKey` has changed to accommodate batch decryption and to + correctly handle scanning with the internal (change) keys derived from ZIP + 316 UFVKs and UIVKs. + - `scan_block` now uses batching for trial-decryption of transaction outputs. +- The return type of the following methods in `zcash_client_backend::encoding` + have been changed to improve error reporting: + - `decode_extended_spending_key` + - `decode_extended_full_viewing_key` + - `decode_payment_address` +- `zcash_client_backend::wallet::SpendableNote` is now parameterized by a note + identifier type and has an additional `note_id` field that is used to hold the + identifier used to refer to the note in the wallet database. + +### Deprecated +- `zcash_client_backend::data_api::wallet::create_spend_to_address` has been + deprecated. Use `zcash_client_backend::data_api::wallet::spend` instead. If + you wish to continue using `create_spend_to_address`, note that the arguments + to the function has been modified to take a unified spending key instead of a + Sapling extended spending key, and now also requires a `min_confirmations` + argument that the caller can provide to specify a minimum number of + confirmations required for notes being selected. A minimum of 10 + confirmations is recommended. + +### Removed +- `zcash_client_backend::data_api`: + - `wallet::ANCHOR_OFFSET` + - `WalletRead::get_extended_full_viewing_keys` (use + `WalletRead::get_unified_full_viewing_keys` instead). + - `WalletRead::get_address` (use `WalletRead::get_current_address` or + `WalletWrite::get_next_available_address` instead.) + - `impl From for error::Error` +- `zcash_client_backend::proto::compact_formats`: + - `Compact*::new` methods (use `Default::default` or struct instantiation + instead). + - Getters (use dedicated typed methods or direct field access instead). + - Setters (use direct field access instead). +- `zcash_client_backend::wallet::AccountId` (moved to `zcash_primitives::zip32::AccountId`). +- `impl zcash_client_backend::welding_rig::ScanningKey for ExtendedFullViewingKey` + (use `DiversifiableFullViewingKey` instead). ## [0.5.0] - 2021-03-26 ### Added diff --git a/zcash_client_backend/Cargo.toml b/zcash_client_backend/Cargo.toml index da0518aa7b..25680760c4 100644 --- a/zcash_client_backend/Cargo.toml +++ b/zcash_client_backend/Cargo.toml @@ -1,49 +1,257 @@ [package] name = "zcash_client_backend" description = "APIs for creating shielded Zcash light clients" -version = "0.5.0" +version = "0.16.0" authors = [ "Jack Grigg ", "Kris Nuttycombe " ] homepage = "https://github.com/zcash/librustzcash" -repository = "https://github.com/zcash/librustzcash" +repository.workspace = true readme = "README.md" -license = "MIT OR Apache-2.0" -edition = "2018" +license.workspace = true +edition.workspace = true +rust-version.workspace = true +categories.workspace = true + +# Exclude proto files so crates.io consumers don't need protoc. +exclude = ["*.proto"] + +[package.metadata.cargo-udeps.ignore] +development = ["zcash_proofs"] + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] [dependencies] -bech32 = "0.8" -bls12_381 = "0.3.1" -bs58 = { version = "0.4", features = ["check"] } -base64 = "0.13" -ff = "0.8" -group = "0.8" -hex = "0.4" -jubjub = "0.5.1" -nom = "6.1" -percent-encoding = "2.1.0" -proptest = { version = "0.10.1", optional = true } -protobuf = "2.20" -rand_core = "0.5.1" -subtle = "2.2.3" -time = "0.2" -zcash_note_encryption = { version = "0.0", path = "../components/zcash_note_encryption" } -zcash_primitives = { version = "0.5", path = "../zcash_primitives" } +zcash_address.workspace = true +zcash_encoding.workspace = true +zcash_keys = { workspace = true, features = ["sapling"] } +zcash_note_encryption.workspace = true +zcash_primitives = { workspace = true, features = ["std", "circuits"] } +zcash_protocol.workspace = true +zip32.workspace = true +zip321.workspace = true +transparent.workspace = true +pczt = { workspace = true, optional = true } + +# Dependencies exposed in a public API: +# (Breaking upgrades to these require a breaking upgrade to this crate.) +# - Data Access API +time = "0.3.22" +nonempty.workspace = true + +# - CSPRNG +rand_core.workspace = true + +# - Encodings +base64.workspace = true +bech32.workspace = true +bs58.workspace = true +postcard = { workspace = true, optional = true } + +# - Errors +bip32 = { workspace = true, optional = true } + +# - Logging and metrics +memuse.workspace = true +tracing.workspace = true + +# - Protobuf interfaces and gRPC bindings +hex.workspace = true +prost.workspace = true +tonic = { workspace = true, optional = true, features = ["prost", "codegen"] } + +# - Secret management +secrecy.workspace = true +subtle.workspace = true + +# - Shielded protocols +bls12_381.workspace = true +group.workspace = true +orchard = { workspace = true, optional = true } +sapling.workspace = true + +# - Sync engine +async-trait = { version = "0.1", optional = true } +futures-util = { version = "0.3", optional = true } + +# - Note commitment trees +incrementalmerkletree.workspace = true +shardtree.workspace = true + +# - Test dependencies +ambassador = { workspace = true, optional = true } +assert_matches = { workspace = true, optional = true } +pasta_curves = { workspace = true, optional = true } +proptest = { workspace = true, optional = true } +jubjub = { workspace = true, optional = true } +rand_chacha = { workspace = true, optional = true } +zcash_proofs = { workspace = true, optional = true } + +# - ZIP 321 +nom = "7" + +# - Tor +# -- Exposed error types: `arti_client::Error`, `arti_client::config::ConfigBuildError`, +# `hyper::Error`, `hyper::http::Error`, `serde_json::Error`. We could avoid this with +# changes to error handling. +arti-client = { workspace = true, optional = true } +dynosaur = { workspace = true, optional = true } +hyper = { workspace = true, optional = true, features = ["client", "http1"] } +serde_json = { workspace = true, optional = true } +trait-variant = { workspace = true, optional = true } + +# - Currency conversion +rust_decimal = { workspace = true, optional = true } + +# Dependencies used internally: +# (Breaking upgrades to these are usually backwards-compatible, but check MSRVs.) +# - Documentation +document-features.workspace = true + +# - Encodings +byteorder = { workspace = true, optional = true } +percent-encoding.workspace = true + +# - Scanning +crossbeam-channel.workspace = true +rayon.workspace = true + +# - Tor +tokio = { workspace = true, optional = true, features = ["fs"] } +tor-rtcompat = { workspace = true, optional = true } +tower = { workspace = true, optional = true } + +# - HTTP through Tor +http-body-util = { workspace = true, optional = true } +hyper-util = { workspace = true, optional = true } +rand = { workspace = true, optional = true } +serde = { workspace = true, optional = true } +tokio-rustls = { workspace = true, optional = true } +webpki-roots = { workspace = true, optional = true } [build-dependencies] -protobuf-codegen-pure = "2.20" +tonic-build = { workspace = true, features = ["prost"] } +which = "6" [dev-dependencies] +ambassador.workspace = true +assert_matches.workspace = true gumdrop = "0.8" -rand_core = "0.5.1" -rand_xorshift = "0.2" -tempfile = "3.1.0" -zcash_client_sqlite = { version = "0.3", path = "../zcash_client_sqlite" } -zcash_proofs = { version = "0.5", path = "../zcash_proofs" } +incrementalmerkletree = { workspace = true, features = ["test-dependencies"] } +jubjub.workspace = true +proptest.workspace = true +rand.workspace = true +rand_chacha.workspace = true +shardtree = { workspace = true, features = ["test-dependencies"] } +tokio = { version = "1.21.0", features = ["rt-multi-thread"] } +zcash_address = { workspace = true, features = ["test-dependencies"] } +zcash_keys = { workspace = true, features = ["test-dependencies"] } +zcash_primitives = { workspace = true, features = ["test-dependencies"] } +zcash_proofs = { workspace = true, features = ["bundled-prover"] } +zcash_protocol = { workspace = true, features = ["local-consensus"] } [features] -test-dependencies = ["proptest", "zcash_primitives/test-dependencies"] +## Enables the `tonic` gRPC client bindings for connecting to a `lightwalletd` server. +lightwalletd-tonic = ["dep:tonic", "hyper-util?/tokio"] + +## Enables the `tls-webpki-roots` feature of `tonic`. +lightwalletd-tonic-tls-webpki-roots = ["lightwalletd-tonic", "tonic?/tls-webpki-roots"] + +## Enables the `transport` feature of `tonic` producing a fully-featured client and server implementation +lightwalletd-tonic-transport = ["lightwalletd-tonic", "tonic?/transport"] + +## Enables receiving transparent funds and shielding them. +transparent-inputs = [ + "dep:bip32", + "transparent/transparent-inputs", + "zcash_keys/transparent-inputs", + "zcash_primitives/transparent-inputs", +] + +## Enables receiving and spending Orchard funds. +orchard = ["dep:orchard", "dep:pasta_curves", "zcash_keys/orchard"] + +## Enables creating partially-constructed transactions for use in hardware wallet and multisig scenarios. +pczt = [ + "orchard", + "transparent-inputs", + "pczt/zcp-builder", + "pczt/io-finalizer", + "pczt/prover", + "pczt/signer", + "pczt/spend-finalizer", + "pczt/tx-extractor", + "pczt/zcp-builder", + "dep:postcard", + "dep:serde", +] + +## Exposes a wallet synchronization function that implements the necessary state machine. +sync = [ + "lightwalletd-tonic", + "dep:async-trait", + "dep:futures-util", +] + +## Exposes a Tor client for hiding a wallet's IP address while performing certain wallet +## operations. +tor = [ + "dep:arti-client", + "dep:dynosaur", + "dep:futures-util", + "dep:http-body-util", + "dep:hyper", + "dep:hyper-util", + "dep:rand", + "dep:rust_decimal", + "dep:serde", + "dep:serde_json", + "dep:tokio", + "dep:tokio-rustls", + "dep:tor-rtcompat", + "dep:trait-variant", + "dep:tower", + "dep:webpki-roots", +] + +## Exposes APIs that are useful for testing, such as `proptest` strategies. +test-dependencies = [ + "dep:ambassador", + "dep:assert_matches", + "dep:proptest", + "dep:jubjub", + "dep:rand", + "dep:rand_chacha", + "orchard?/test-dependencies", + "zcash_keys/test-dependencies", + "zcash_primitives/test-dependencies", + "zcash_proofs/bundled-prover", + "zcash_protocol/local-consensus", + "incrementalmerkletree/test-dependencies", +] + +## Exposes APIs that allow calculation of non-standard fees. +non-standard-fees = ["zcash_primitives/non-standard-fees"] + +#! ### Experimental features + +## Exposes unstable APIs. Their behaviour may change at any time. +unstable = ["dep:byteorder", "zcash_keys/unstable"] + +## Exposes APIs for unstable serialization formats. These may change at any time. +unstable-serialization = ["dep:byteorder"] + +## Exposes the [`data_api::scanning::spanning_tree`] module. +unstable-spanning-tree = [] + +[lib] +bench = false [badges] maintenance = { status = "actively-developed" } + +[lints] +workspace = true diff --git a/zcash_client_backend/README.md b/zcash_client_backend/README.md index af9a7ff879..25f3e625cd 100644 --- a/zcash_client_backend/README.md +++ b/zcash_client_backend/README.md @@ -3,6 +3,12 @@ This library contains Rust structs and traits for creating shielded Zcash light clients. +## Building + +Note that in order to (re)build the GRPC interface, you will need `protoc` on +your `$PATH`. This is not required unless you make changes to any of the files +in `./proto/`. + ## License Licensed under either of diff --git a/zcash_client_backend/build.rs b/zcash_client_backend/build.rs index 6d0c8c9260..e2503554a3 100644 --- a/zcash_client_backend/build.rs +++ b/zcash_client_backend/build.rs @@ -1,8 +1,92 @@ -fn main() { - protobuf_codegen_pure::Codegen::new() - .out_dir("src/proto") - .inputs(&["proto/compact_formats.proto"]) - .includes(&["proto"]) - .run() - .expect("Protobuf codegen failed"); +use std::env; +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; + +const COMPACT_FORMATS_PROTO: &str = "proto/compact_formats.proto"; + +const PROPOSAL_PROTO: &str = "proto/proposal.proto"; + +const SERVICE_PROTO: &str = "proto/service.proto"; + +fn main() -> io::Result<()> { + // - We don't include the proto files in releases so that downstreams do not need to + // regenerate the bindings even if protoc is present. + // - We check for the existence of protoc in the same way as prost_build, so that + // people building from source do not need to have protoc installed. If they make + // changes to the proto files, the discrepancy will be caught by CI. + if Path::new(COMPACT_FORMATS_PROTO).exists() + && env::var_os("PROTOC") + .map(PathBuf::from) + .or_else(|| which::which("protoc").ok()) + .is_some() + { + build()?; + } + + Ok(()) +} + +fn build() -> io::Result<()> { + let out: PathBuf = env::var_os("OUT_DIR") + .expect("Cannot find OUT_DIR environment variable") + .into(); + + // Build the compact format types. + tonic_build::compile_protos(COMPACT_FORMATS_PROTO)?; + + // Copy the generated types into the source tree so changes can be committed. + fs::copy( + out.join("cash.z.wallet.sdk.rpc.rs"), + "src/proto/compact_formats.rs", + )?; + + // Build the gRPC types and client. + tonic_build::configure() + .build_server(false) + .client_mod_attribute( + "cash.z.wallet.sdk.rpc", + r#"#[cfg(feature = "lightwalletd-tonic")]"#, + ) + .extern_path( + ".cash.z.wallet.sdk.rpc.ChainMetadata", + "crate::proto::compact_formats::ChainMetadata", + ) + .extern_path( + ".cash.z.wallet.sdk.rpc.CompactBlock", + "crate::proto::compact_formats::CompactBlock", + ) + .extern_path( + ".cash.z.wallet.sdk.rpc.CompactTx", + "crate::proto::compact_formats::CompactTx", + ) + .extern_path( + ".cash.z.wallet.sdk.rpc.CompactSaplingSpend", + "crate::proto::compact_formats::CompactSaplingSpend", + ) + .extern_path( + ".cash.z.wallet.sdk.rpc.CompactSaplingOutput", + "crate::proto::compact_formats::CompactSaplingOutput", + ) + .extern_path( + ".cash.z.wallet.sdk.rpc.CompactOrchardAction", + "crate::proto::compact_formats::CompactOrchardAction", + ) + .compile_protos(&[SERVICE_PROTO], &["proto/"])?; + + // Build the proposal types. + tonic_build::compile_protos(PROPOSAL_PROTO)?; + + // Copy the generated types into the source tree so changes can be committed. + fs::copy( + out.join("cash.z.wallet.sdk.ffi.rs"), + "src/proto/proposal.rs", + )?; + + // Copy the generated types into the source tree so changes can be committed. The + // file has the same name as for the compact format types because they have the + // same package, but we've set things up so this only contains the service types. + fs::copy(out.join("cash.z.wallet.sdk.rpc.rs"), "src/proto/service.rs")?; + + Ok(()) } diff --git a/zcash_client_backend/examples/diversify-address.rs b/zcash_client_backend/examples/diversify-address.rs index 2584001935..6e0b35ec21 100644 --- a/zcash_client_backend/examples/diversify-address.rs +++ b/zcash_client_backend/examples/diversify-address.rs @@ -1,38 +1,26 @@ -use std::convert::TryInto; - use gumdrop::Options; -use zcash_client_backend::encoding::{decode_extended_full_viewing_key, encode_payment_address}; -use zcash_primitives::{ - constants::{mainnet, testnet}, - zip32::{DiversifierIndex, ExtendedFullViewingKey}, -}; +use sapling::zip32::ExtendedFullViewingKey; +use zcash_keys::encoding::{decode_extended_full_viewing_key, encode_payment_address}; +use zcash_protocol::constants::{mainnet, testnet}; +use zip32::DiversifierIndex; fn parse_viewing_key(s: &str) -> Result<(ExtendedFullViewingKey, bool), &'static str> { decode_extended_full_viewing_key(mainnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, s) - .ok() - .flatten() .map(|vk| (vk, true)) - .or_else(|| { + .or_else(|_| { decode_extended_full_viewing_key(testnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, s) - .ok() - .flatten() .map(|vk| (vk, false)) }) - .ok_or("Invalid Sapling viewing key") + .map_err(|_| "Invalid Sapling viewing key") } fn parse_diversifier_index(s: &str) -> Result { let i: u128 = s.parse().map_err(|_| "Diversifier index is not a number")?; - if i >= (1 << 88) { - return Err("Diversifier index too large"); - } - Ok(DiversifierIndex(i.to_le_bytes()[..11].try_into().unwrap())) + DiversifierIndex::try_from(i).map_err(|_| "Diversifier index too large") } fn encode_diversifier_index(di: &DiversifierIndex) -> u128 { - let mut bytes = [0; 16]; - bytes[..11].copy_from_slice(&di.0); - u128::from_le_bytes(bytes) + (*di).into() } #[derive(Debug, Options)] @@ -65,7 +53,7 @@ fn main() { return; }; - let (diversifier_index, address) = extfvk.address(opts.diversifier_index).unwrap(); + let (diversifier_index, address) = extfvk.find_address(opts.diversifier_index).unwrap(); println!( "# Diversifier index: {}", encode_diversifier_index(&diversifier_index) diff --git a/zcash_client_backend/proto/compact_formats.proto b/zcash_client_backend/proto/compact_formats.proto index 7e1bc54127..b00ab1291a 100644 --- a/zcash_client_backend/proto/compact_formats.proto +++ b/zcash_client_backend/proto/compact_formats.proto @@ -1,48 +1,84 @@ +// Copyright (c) 2019-2021 The Zcash developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://www.opensource.org/licenses/mit-license.php . + syntax = "proto3"; package cash.z.wallet.sdk.rpc; option go_package = "walletrpc"; +option swift_prefix = ""; // Remember that proto3 fields are all optional. A field that is not present will be set to its zero value. // bytes fields of hashes are in canonical little-endian format. +// Information about the state of the chain as of a given block. +message ChainMetadata { + uint32 saplingCommitmentTreeSize = 1; // the size of the Sapling note commitment tree as of the end of this block + uint32 orchardCommitmentTreeSize = 2; // the size of the Orchard note commitment tree as of the end of this block +} + +// A compact representation of the shielded data in a Zcash block. +// // CompactBlock is a packaging of ONLY the data from a block that's needed to: -// 1. Detect a payment to your shielded Sapling address -// 2. Detect a spend of your shielded Sapling notes -// 3. Update your witnesses to generate new Sapling spend proofs. +// 1. Detect a payment to your Shielded address +// 2. Detect a spend of your Shielded notes +// 3. Update your witnesses to generate new spend proofs. message CompactBlock { - uint32 protoVersion = 1; // the version of this wire format, for storage - uint64 height = 2; // the height of this block - bytes hash = 3; - bytes prevHash = 4; - uint32 time = 5; - bytes header = 6; // (hash, prevHash, and time) OR (full header) - repeated CompactTx vtx = 7; // compact transactions from this block + uint32 protoVersion = 1; // the version of this wire format, for storage + uint64 height = 2; // the height of this block + bytes hash = 3; // the ID (hash) of this block, same as in block explorers + bytes prevHash = 4; // the ID (hash) of this block's predecessor + uint32 time = 5; // Unix epoch time when the block was mined + bytes header = 6; // (hash, prevHash, and time) OR (full header) + repeated CompactTx vtx = 7; // zero or more compact transactions from this block + ChainMetadata chainMetadata = 8; // information about the state of the chain as of this block } +// A compact representation of the shielded data in a Zcash transaction. +// +// CompactTx contains the minimum information for a wallet to know if this transaction +// is relevant to it (either pays to it or spends from it) via shielded elements +// only. This message will not encode a transparent-to-transparent transaction. message CompactTx { // Index and hash will allow the receiver to call out to chain // explorers or other data structures to retrieve more information // about this transaction. - uint64 index = 1; - bytes hash = 2; + uint64 index = 1; // the index within the full block + bytes hash = 2; // the ID (hash) of this transaction, same as in block explorers // The transaction fee: present if server can provide. In the case of a // stateless server and a transaction with transparent inputs, this will be // unset because the calculation requires reference to prior transactions. - // in a pure-Sapling context, the fee will be calculable as: - // valueBalance + (sum(vPubNew) - sum(vPubOld) - sum(tOut)) + // If there are no transparent inputs, the fee will be calculable as: + // valueBalanceSapling + valueBalanceOrchard + sum(vPubNew) - sum(vPubOld) - sum(tOut) uint32 fee = 3; - repeated CompactSpend spends = 4; - repeated CompactOutput outputs = 5; + repeated CompactSaplingSpend spends = 4; + repeated CompactSaplingOutput outputs = 5; + repeated CompactOrchardAction actions = 6; +} + +// A compact representation of a [Sapling Spend](https://zips.z.cash/protocol/protocol.pdf#spendencodingandconsensus). +// +// CompactSaplingSpend is a Sapling Spend Description as described in 7.3 of the Zcash +// protocol specification. +message CompactSaplingSpend { + bytes nf = 1; // Nullifier (see the Zcash protocol specification) } -message CompactSpend { - bytes nf = 1; +// A compact representation of a [Sapling Output](https://zips.z.cash/protocol/protocol.pdf#outputencodingandconsensus). +// +// It encodes the `cmu` field, `ephemeralKey` field, and a 52-byte prefix of the +// `encCiphertext` field of a Sapling Output Description. Total size is 116 bytes. +message CompactSaplingOutput { + bytes cmu = 1; // Note commitment u-coordinate. + bytes ephemeralKey = 2; // Ephemeral public key. + bytes ciphertext = 3; // First 52 bytes of ciphertext. } -message CompactOutput { - bytes cmu = 1; - bytes epk = 2; - bytes ciphertext = 3; +// A compact representation of an [Orchard Action](https://zips.z.cash/protocol/protocol.pdf#actionencodingandconsensus). +message CompactOrchardAction { + bytes nullifier = 1; // [32] The nullifier of the input note + bytes cmx = 2; // [32] The x-coordinate of the note commitment for the output note + bytes ephemeralKey = 3; // [32] An encoding of an ephemeral Pallas public key + bytes ciphertext = 4; // [52] The first 52 bytes of the encCiphertext field } diff --git a/zcash_client_backend/proto/proposal.proto b/zcash_client_backend/proto/proposal.proto new file mode 100644 index 0000000000..db548c6e23 --- /dev/null +++ b/zcash_client_backend/proto/proposal.proto @@ -0,0 +1,144 @@ +// Copyright (c) 2023 The Zcash developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://www.opensource.org/licenses/mit-license.php . + +syntax = "proto3"; +package cash.z.wallet.sdk.ffi; + +// A data structure that describes a series of transactions to be created. +message Proposal { + // The version of this serialization format. + uint32 protoVersion = 1; + // The fee rule used in constructing this proposal + FeeRule feeRule = 2; + // The target height for which the proposal was constructed + // + // The chain must contain at least this many blocks in order for the proposal to + // be executed. + uint32 minTargetHeight = 3; + // The series of transactions to be created. + repeated ProposalStep steps = 4; +} + +// A data structure that describes the inputs to be consumed and outputs to +// be produced in a proposed transaction. +message ProposalStep { + // ZIP 321 serialized transaction request + string transactionRequest = 1; + // The vector of selected payment index / output pool mappings. Payment index + // 0 corresponds to the payment with no explicit index. + repeated PaymentOutputPool paymentOutputPools = 2; + // The anchor height to be used in creating the transaction, if any. + // Setting the anchor height to zero will disallow the use of any shielded + // inputs. + uint32 anchorHeight = 3; + // The inputs to be used in creating the transaction. + repeated ProposedInput inputs = 4; + // The total value, fee value, and change outputs of the proposed + // transaction + TransactionBalance balance = 5; + // A flag indicating whether the step is for a shielding transaction, + // used for determining which OVK to select for wallet-internal outputs. + bool isShielding = 6; +} + +enum ValuePool { + // Protobuf requires that enums have a zero discriminant as the default + // value. However, we need to require that a known value pool is selected, + // and we do not want to fall back to any default, so sending the + // PoolNotSpecified value will be treated as an error. + PoolNotSpecified = 0; + // The transparent value pool (P2SH is not distinguished from P2PKH) + Transparent = 1; + // The Sapling value pool + Sapling = 2; + // The Orchard value pool + Orchard = 3; +} + +// A mapping from ZIP 321 payment index to the output pool that has been chosen +// for that payment, based upon the payment address and the selected inputs to +// the transaction. +message PaymentOutputPool { + uint32 paymentIndex = 1; + ValuePool valuePool = 2; +} + +// The unique identifier and value for each proposed input that does not +// require a back-reference to a prior step of the proposal. +message ReceivedOutput { + bytes txid = 1; + ValuePool valuePool = 2; + uint32 index = 3; + uint64 value = 4; +} + +// A reference to a payment in a prior step of the proposal. This payment must +// belong to the wallet. +message PriorStepOutput { + uint32 stepIndex = 1; + uint32 paymentIndex = 2; +} + +// A reference to a change or ephemeral output from a prior step of the proposal. +message PriorStepChange { + uint32 stepIndex = 1; + uint32 changeIndex = 2; +} + +// The unique identifier and value for an input to be used in the transaction. +message ProposedInput { + oneof value { + ReceivedOutput receivedOutput = 1; + PriorStepOutput priorStepOutput = 2; + PriorStepChange priorStepChange = 3; + } +} + +// The fee rule used in constructing a Proposal +enum FeeRule { + // Protobuf requires that enums have a zero discriminant as the default + // value. However, we need to require that a known fee rule is selected, + // and we do not want to fall back to any default, so sending the + // FeeRuleNotSpecified value will be treated as an error. + FeeRuleNotSpecified = 0; + // 10000 ZAT + PreZip313 = 1; + // 1000 ZAT + Zip313 = 2; + // MAX(10000, 5000 * logical_actions) ZAT + Zip317 = 3; +} + +// The proposed change outputs and fee value. +message TransactionBalance { + // A list of change or ephemeral output values. + repeated ChangeValue proposedChange = 1; + // The fee to be paid by the proposed transaction, in zatoshis. + uint64 feeRequired = 2; +} + +// A proposed change or ephemeral output. If the transparent value pool is +// selected, the `memo` field must be null. +// +// When the `isEphemeral` field of a `ChangeValue` is set, it represents +// an ephemeral output, which must be spent by a subsequent step. This is +// only supported for transparent outputs. Each ephemeral output will be +// given a unique t-address. +message ChangeValue { + // The value of a change or ephemeral output to be created, in zatoshis. + uint64 value = 1; + // The value pool in which the change or ephemeral output should be created. + ValuePool valuePool = 2; + // The optional memo that should be associated with the newly created output. + // Memos must not be present for transparent outputs. + MemoBytes memo = 3; + // Whether this is to be an ephemeral output. + bool isEphemeral = 4; +} + +// An object wrapper for memo bytes, to facilitate representing the +// `change_memo == None` case. +message MemoBytes { + bytes value = 1; +} diff --git a/zcash_client_backend/proto/service.proto b/zcash_client_backend/proto/service.proto new file mode 100644 index 0000000000..6559e3433e --- /dev/null +++ b/zcash_client_backend/proto/service.proto @@ -0,0 +1,214 @@ +// Copyright (c) 2019-2020 The Zcash developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://www.opensource.org/licenses/mit-license.php . + +syntax = "proto3"; +package cash.z.wallet.sdk.rpc; +option go_package = "lightwalletd/walletrpc"; +option swift_prefix = ""; +import "compact_formats.proto"; + +// A BlockID message contains identifiers to select a block: a height or a +// hash. Specification by hash is not implemented, but may be in the future. +message BlockID { + uint64 height = 1; + bytes hash = 2; +} + +// BlockRange specifies a series of blocks from start to end inclusive. +// Both BlockIDs must be heights; specification by hash is not yet supported. +message BlockRange { + BlockID start = 1; + BlockID end = 2; +} + +// A TxFilter contains the information needed to identify a particular +// transaction: either a block and an index, or a direct transaction hash. +// Currently, only specification by hash is supported. +message TxFilter { + BlockID block = 1; // block identifier, height or hash + uint64 index = 2; // index within the block + bytes hash = 3; // transaction ID (hash, txid) +} + +// RawTransaction contains the complete transaction data. It also optionally includes +// the block height in which the transaction was included, or, when returned +// by GetMempoolStream(), the latest block height. +// +// FIXME: the documentation here about mempool status contradicts the documentation +// for the `height` field. See https://github.com/zcash/librustzcash/issues/1484 +message RawTransaction { + bytes data = 1; // exact data returned by Zcash 'getrawtransaction' + uint64 height = 2; // height that the transaction was mined (or -1) +} + +// A SendResponse encodes an error code and a string. It is currently used +// only by SendTransaction(). If error code is zero, the operation was +// successful; if non-zero, it and the message specify the failure. +message SendResponse { + int32 errorCode = 1; + string errorMessage = 2; +} + +// Chainspec is a placeholder to allow specification of a particular chain fork. +message ChainSpec {} + +// Empty is for gRPCs that take no arguments, currently only GetLightdInfo. +message Empty {} + +// LightdInfo returns various information about this lightwalletd instance +// and the state of the blockchain. +message LightdInfo { + string version = 1; + string vendor = 2; + bool taddrSupport = 3; // true + string chainName = 4; // either "main" or "test" + uint64 saplingActivationHeight = 5; // depends on mainnet or testnet + string consensusBranchId = 6; // protocol identifier, see consensus/upgrades.cpp + uint64 blockHeight = 7; // latest block on the best chain + string gitCommit = 8; + string branch = 9; + string buildDate = 10; + string buildUser = 11; + uint64 estimatedHeight = 12; // less than tip height if zcashd is syncing + string zcashdBuild = 13; // example: "v4.1.1-877212414" + string zcashdSubversion = 14; // example: "/MagicBean:4.1.1/" +} + +// TransparentAddressBlockFilter restricts the results to the given address +// or block range. +message TransparentAddressBlockFilter { + string address = 1; // t-address + BlockRange range = 2; // start, end heights +} + +// Duration is currently used only for testing, so that the Ping rpc +// can simulate a delay, to create many simultaneous connections. Units +// are microseconds. +message Duration { + int64 intervalUs = 1; +} + +// PingResponse is used to indicate concurrency, how many Ping rpcs +// are executing upon entry and upon exit (after the delay). +// This rpc is used for testing only. +message PingResponse { + int64 entry = 1; + int64 exit = 2; +} + +message Address { + string address = 1; +} +message AddressList { + repeated string addresses = 1; +} +message Balance { + int64 valueZat = 1; +} + +message Exclude { + repeated bytes txid = 1; +} + +// The TreeState is derived from the Zcash z_gettreestate rpc. +message TreeState { + string network = 1; // "main" or "test" + uint64 height = 2; // block height + string hash = 3; // block id + uint32 time = 4; // Unix epoch time when the block was mined + string saplingTree = 5; // sapling commitment tree state + string orchardTree = 6; // orchard commitment tree state +} + +enum ShieldedProtocol { + sapling = 0; + orchard = 1; +} + +message GetSubtreeRootsArg { + uint32 startIndex = 1; // Index identifying where to start returning subtree roots + ShieldedProtocol shieldedProtocol = 2; // Shielded protocol to return subtree roots for + uint32 maxEntries = 3; // Maximum number of entries to return, or 0 for all entries. +} +message SubtreeRoot { + bytes rootHash = 2; // The 32-byte Merkle root of the subtree. + bytes completingBlockHash = 3; // The hash of the block that completed this subtree. + uint64 completingBlockHeight = 4; // The height of the block that completed this subtree in the main chain. +} + +// Results are sorted by height, which makes it easy to issue another +// request that picks up from where the previous left off. +message GetAddressUtxosArg { + repeated string addresses = 1; + uint64 startHeight = 2; + uint32 maxEntries = 3; // zero means unlimited +} +message GetAddressUtxosReply { + string address = 6; + bytes txid = 1; + int32 index = 2; + bytes script = 3; + int64 valueZat = 4; + uint64 height = 5; +} +message GetAddressUtxosReplyList { + repeated GetAddressUtxosReply addressUtxos = 1; +} + +service CompactTxStreamer { + // Return the BlockID of the block at the tip of the best chain + rpc GetLatestBlock(ChainSpec) returns (BlockID) {} + // Return the compact block corresponding to the given block identifier + rpc GetBlock(BlockID) returns (CompactBlock) {} + // Same as GetBlock except actions contain only nullifiers + rpc GetBlockNullifiers(BlockID) returns (CompactBlock) {} + // Return a list of consecutive compact blocks + rpc GetBlockRange(BlockRange) returns (stream CompactBlock) {} + // Same as GetBlockRange except actions contain only nullifiers + rpc GetBlockRangeNullifiers(BlockRange) returns (stream CompactBlock) {} + + // Return the requested full (not compact) transaction (as from zcashd) + rpc GetTransaction(TxFilter) returns (RawTransaction) {} + // Submit the given transaction to the Zcash network + rpc SendTransaction(RawTransaction) returns (SendResponse) {} + + // Return the txids corresponding to the given t-address within the given block range + rpc GetTaddressTxids(TransparentAddressBlockFilter) returns (stream RawTransaction) {} + rpc GetTaddressBalance(AddressList) returns (Balance) {} + rpc GetTaddressBalanceStream(stream Address) returns (Balance) {} + + // Return the compact transactions currently in the mempool; the results + // can be a few seconds out of date. If the Exclude list is empty, return + // all transactions; otherwise return all *except* those in the Exclude list + // (if any); this allows the client to avoid receiving transactions that it + // already has (from an earlier call to this rpc). The transaction IDs in the + // Exclude list can be shortened to any number of bytes to make the request + // more bandwidth-efficient; if two or more transactions in the mempool + // match a shortened txid, they are all sent (none is excluded). Transactions + // in the exclude list that don't exist in the mempool are ignored. + rpc GetMempoolTx(Exclude) returns (stream CompactTx) {} + + // Return a stream of current Mempool transactions. This will keep the output stream open while + // there are mempool transactions. It will close the returned stream when a new block is mined. + rpc GetMempoolStream(Empty) returns (stream RawTransaction) {} + + // GetTreeState returns the note commitment tree state corresponding to the given block. + // See section 3.7 of the Zcash protocol specification. It returns several other useful + // values also (even though they can be obtained using GetBlock). + // The block can be specified by either height or hash. + rpc GetTreeState(BlockID) returns (TreeState) {} + rpc GetLatestTreeState(Empty) returns (TreeState) {} + + // Returns a stream of information about roots of subtrees of the Sapling and Orchard + // note commitment trees. + rpc GetSubtreeRoots(GetSubtreeRootsArg) returns (stream SubtreeRoot) {} + + rpc GetAddressUtxos(GetAddressUtxosArg) returns (GetAddressUtxosReplyList) {} + rpc GetAddressUtxosStream(GetAddressUtxosArg) returns (stream GetAddressUtxosReply) {} + + // Return information about this lightwalletd instance and the blockchain + rpc GetLightdInfo(Empty) returns (LightdInfo) {} + // Testing-only, requires lightwalletd --ping-very-insecure (do not enable in production) + rpc Ping(Duration) returns (PingResponse) {} +} diff --git a/zcash_client_backend/src/address.rs b/zcash_client_backend/src/address.rs deleted file mode 100644 index 8b144a3b71..0000000000 --- a/zcash_client_backend/src/address.rs +++ /dev/null @@ -1,56 +0,0 @@ -//! Structs for handling supported address types. - -use zcash_primitives::{consensus, legacy::TransparentAddress, sapling::PaymentAddress}; - -use crate::encoding::{ - decode_payment_address, decode_transparent_address, encode_payment_address, - encode_transparent_address, -}; - -/// An address that funds can be sent to. -#[derive(Debug, PartialEq, Clone)] -pub enum RecipientAddress { - Shielded(PaymentAddress), - Transparent(TransparentAddress), -} - -impl From for RecipientAddress { - fn from(addr: PaymentAddress) -> Self { - RecipientAddress::Shielded(addr) - } -} - -impl From for RecipientAddress { - fn from(addr: TransparentAddress) -> Self { - RecipientAddress::Transparent(addr) - } -} - -impl RecipientAddress { - pub fn decode(params: &P, s: &str) -> Option { - if let Ok(Some(pa)) = decode_payment_address(params.hrp_sapling_payment_address(), s) { - Some(pa.into()) - } else if let Ok(Some(addr)) = decode_transparent_address( - ¶ms.b58_pubkey_address_prefix(), - ¶ms.b58_script_address_prefix(), - s, - ) { - Some(addr.into()) - } else { - None - } - } - - pub fn encode(&self, params: &P) -> String { - match self { - RecipientAddress::Shielded(pa) => { - encode_payment_address(params.hrp_sapling_payment_address(), pa) - } - RecipientAddress::Transparent(addr) => encode_transparent_address( - ¶ms.b58_pubkey_address_prefix(), - ¶ms.b58_script_address_prefix(), - addr, - ), - } - } -} diff --git a/zcash_client_backend/src/data_api.rs b/zcash_client_backend/src/data_api.rs index 166fd80cba..15f9dafbc8 100644 --- a/zcash_client_backend/src/data_api.rs +++ b/zcash_client_backend/src/data_api.rs @@ -1,429 +1,2550 @@ -//! Interfaces for wallet data persistence & low-level wallet utilities. +//! # Utilities for Zcash wallet construction +//! +//! This module defines a set of APIs for wallet data persistence, and provides a suite of methods +//! based upon these APIs that can be used to implement a fully functional Zcash wallet. At +//! present, the interfaces provided here are built primarily around the use of a source of +//! [`CompactBlock`] data such as the Zcash Light Client Protocol as defined in +//! [ZIP 307](https://zips.z.cash/zip-0307) but they may be generalized to full-block use cases in +//! the future. +//! +//! ## Important Concepts +//! +//! There are several important operations that a Zcash wallet must perform that distinguish Zcash +//! wallet design from wallets for other cryptocurrencies. +//! +//! * Viewing Keys: Wallets based upon this module are built around the capabilities of Zcash +//! [`UnifiedFullViewingKey`]s; the wallet backend provides no facilities for the storage +//! of spending keys, and spending keys must be provided by the caller in order to perform +//! transaction creation operations. +//! * Blockchain Scanning: A Zcash wallet must download and trial-decrypt each transaction on the +//! Zcash blockchain using one or more Viewing Keys in order to find new shielded transaction +//! outputs (generally termed "notes") belonging to the wallet. The primary entrypoint for this +//! functionality is the [`scan_cached_blocks`] method. See the [`chain`] module for additional +//! details. +//! * Witness Updates: In order to spend a shielded note, the wallet must be able to compute the +//! Merkle path to that note in the global note commitment tree. When [`scan_cached_blocks`] is +//! used to process a range of blocks, the note commitment tree is updated with the note +//! commitments for the blocks in that range. +//! * Transaction Construction: The [`wallet`] module provides functions for creating Zcash +//! transactions that spend funds belonging to the wallet. +//! +//! ## Core Traits +//! +//! The utility functions described above depend upon four important traits defined in this +//! module, which between them encompass the data storage requirements of a light wallet. +//! The relevant traits are [`InputSource`], [`WalletRead`], [`WalletWrite`], and +//! [`WalletCommitmentTrees`]. A complete implementation of the data storage layer for a wallet +//! will include an implementation of all four of these traits. See the [`zcash_client_sqlite`] +//! crate for a complete example of the implementation of these traits. +//! +//! ## Accounts +//! +//! The operation of the [`InputSource`], [`WalletRead`] and [`WalletWrite`] traits is built around +//! the concept of a wallet having one or more accounts, with a unique `AccountId` for each +//! account. +//! +//! An account identifier corresponds to at most a single [`UnifiedSpendingKey`]'s worth of spend +//! authority, with the received and spent notes of that account tracked via the corresponding +//! [`UnifiedFullViewingKey`]. Both received notes and change spendable by that spending authority +//! (both the external and internal parts of that key, as defined by +//! [ZIP 316](https://zips.z.cash/zip-0316)) will be interpreted as belonging to that account. +//! +//! [`CompactBlock`]: crate::proto::compact_formats::CompactBlock +//! [`scan_cached_blocks`]: crate::data_api::chain::scan_cached_blocks +//! [`zcash_client_sqlite`]: https://crates.io/crates/zcash_client_sqlite +//! [`TransactionRequest`]: crate::zip321::TransactionRequest +//! [`propose_shielding`]: crate::data_api::wallet::propose_shielding -use std::cmp; -use std::collections::HashMap; -use std::fmt::Debug; +use nonempty::NonEmpty; +use secrecy::SecretVec; +use std::{ + collections::HashMap, + fmt::Debug, + hash::Hash, + io, + num::{NonZeroU32, TryFromIntError}, +}; + +use incrementalmerkletree::{frontier::Frontier, Retention}; +use shardtree::{error::ShardTreeError, store::ShardStore, ShardTree}; -use zcash_primitives::{ - block::BlockHash, +use zcash_keys::{ + address::UnifiedAddress, + keys::{ + UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedIncomingViewingKey, UnifiedSpendingKey, + }, +}; +use zcash_primitives::{block::BlockHash, transaction::Transaction}; +use zcash_protocol::{ consensus::BlockHeight, memo::{Memo, MemoBytes}, - merkle_tree::{CommitmentTree, IncrementalWitness}, - sapling::{Node, Nullifier, PaymentAddress}, - transaction::{components::Amount, Transaction, TxId}, - zip32::ExtendedFullViewingKey, + value::{BalanceError, Zatoshis}, + ShieldedProtocol, TxId, }; +use zip32::fingerprint::SeedFingerprint; +use self::{ + chain::{ChainState, CommitmentTreeRoot}, + scanning::ScanRange, +}; use crate::{ - address::RecipientAddress, - data_api::wallet::ANCHOR_OFFSET, decrypt::DecryptedOutput, - proto::compact_formats::CompactBlock, - wallet::{AccountId, SpendableNote, WalletTx}, + proto::service::TreeState, + wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput, WalletTx}, }; +#[cfg(feature = "transparent-inputs")] +use { + crate::wallet::TransparentAddressMetadata, + std::ops::Range, + transparent::{address::TransparentAddress, bundle::OutPoint, keys::NonHardenedChildIndex}, +}; + +#[cfg(feature = "test-dependencies")] +use ambassador::delegatable_trait; + +#[cfg(any(test, feature = "test-dependencies"))] +use {zcash_keys::address::Address, zcash_protocol::consensus::NetworkUpgrade}; + pub mod chain; pub mod error; +pub mod scanning; pub mod wallet; -/// Read-only operations required for light wallet functions. +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing; + +/// The height of subtree roots in the Sapling note commitment tree. /// -/// This trait defines the read-only portion of the storage -/// interface atop which higher-level wallet operations are -/// implemented. It serves to allow wallet functions to be -/// abstracted away from any particular data storage substrate. -pub trait WalletRead { - /// The type of errors produced by a wallet backend. - type Error; +/// This conforms to the structure of subtree data returned by +/// `lightwalletd` when using the `GetSubtreeRoots` GRPC call. +pub const SAPLING_SHARD_HEIGHT: u8 = sapling::NOTE_COMMITMENT_TREE_DEPTH / 2; - /// Backend-specific note identifier. - /// - /// For example, this might be a database identifier type - /// or a UUID. - type NoteRef: Copy + Debug; +/// The height of subtree roots in the Orchard note commitment tree. +/// +/// This conforms to the structure of subtree data returned by +/// `lightwalletd` when using the `GetSubtreeRoots` GRPC call. +#[cfg(feature = "orchard")] +pub const ORCHARD_SHARD_HEIGHT: u8 = { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 } / 2; - /// Backend-specific transaction identifier. - /// - /// For example, this might be a database identifier type - /// or a TxId if the backend is able to support that type - /// directly. - type TxRef: Copy + Debug; +/// The number of ephemeral addresses that can be safely reserved without observing any +/// of them to be mined. This is the same as the gap limit in Bitcoin. +pub const GAP_LIMIT: u32 = 20; - /// Returns the minimum and maximum block heights for stored blocks. - /// - /// This will return `Ok(None)` if no block data is present in the database. - fn block_height_extrema(&self) -> Result, Self::Error>; +/// An enumeration of constraints that can be applied when querying for nullifiers for notes +/// belonging to the wallet. +pub enum NullifierQuery { + Unspent, + All, +} - /// Returns the default target height (for the block in which a new - /// transaction would be mined) and anchor height (to use for a new - /// transaction), given the range of block heights that the backend - /// knows about. - /// - /// This will return `Ok(None)` if no block data is present in the database. - fn get_target_and_anchor_heights( - &self, - ) -> Result, Self::Error> { - self.block_height_extrema().map(|heights| { - heights.map(|(min_height, max_height)| { - let target_height = max_height + 1; - - // Select an anchor ANCHOR_OFFSET back from the target block, - // unless that would be before the earliest block we have. - let anchor_height = BlockHeight::from(cmp::max( - u32::from(target_height).saturating_sub(ANCHOR_OFFSET), - u32::from(min_height), - )); - - (target_height, anchor_height) - }) - }) +/// Balance information for a value within a single pool in an account. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Balance { + spendable_value: Zatoshis, + change_pending_confirmation: Zatoshis, + value_pending_spendability: Zatoshis, +} + +impl Balance { + /// The [`Balance`] value having zero values for all its fields. + pub const ZERO: Self = Self { + spendable_value: Zatoshis::ZERO, + change_pending_confirmation: Zatoshis::ZERO, + value_pending_spendability: Zatoshis::ZERO, + }; + + fn check_total_adding(&self, value: Zatoshis) -> Result { + (self.spendable_value + + self.change_pending_confirmation + + self.value_pending_spendability + + value) + .ok_or(BalanceError::Overflow) } - /// Returns the block hash for the block at the given height, if the - /// associated block data is available. Returns `Ok(None)` if the hash - /// is not found in the database. - fn get_block_hash(&self, block_height: BlockHeight) -> Result, Self::Error>; + /// Returns the value in the account that may currently be spent; it is possible to compute + /// witnesses for all the notes that comprise this value, and all of this value is confirmed to + /// the required confirmation depth. + pub fn spendable_value(&self) -> Zatoshis { + self.spendable_value + } - /// Returns the block hash for the block at the maximum height known - /// in stored data. - /// - /// This will return `Ok(None)` if no block data is present in the database. - fn get_max_height_hash(&self) -> Result, Self::Error> { - self.block_height_extrema() - .and_then(|extrema_opt| { - extrema_opt - .map(|(_, max_height)| { - self.get_block_hash(max_height) - .map(|hash_opt| hash_opt.map(move |hash| (max_height, hash))) - }) - .transpose() - }) - .map(|oo| oo.flatten()) - } - - /// Returns the block height in which the specified transaction was mined, - /// or `Ok(None)` if the transaction is not mined in the main chain. - fn get_tx_height(&self, txid: TxId) -> Result, Self::Error>; + /// Adds the specified value to the spendable total, checking for overflow. + pub fn add_spendable_value(&mut self, value: Zatoshis) -> Result<(), BalanceError> { + self.check_total_adding(value)?; + self.spendable_value = (self.spendable_value + value).unwrap(); + Ok(()) + } - /// Returns the payment address for the specified account, if the account - /// identifier specified refers to a valid account for this wallet. - /// - /// This will return `Ok(None)` if the account identifier does not correspond - /// to a known account. - fn get_address(&self, account: AccountId) -> Result, Self::Error>; + /// Returns the value in the account of shielded change notes that do not yet have sufficient + /// confirmations to be spendable. + pub fn change_pending_confirmation(&self) -> Zatoshis { + self.change_pending_confirmation + } - /// Returns all extended full viewing keys known about by this wallet. - fn get_extended_full_viewing_keys( - &self, - ) -> Result, Self::Error>; + /// Adds the specified value to the pending change total, checking for overflow. + pub fn add_pending_change_value(&mut self, value: Zatoshis) -> Result<(), BalanceError> { + self.check_total_adding(value)?; + self.change_pending_confirmation = (self.change_pending_confirmation + value).unwrap(); + Ok(()) + } - /// Checks whether the specified extended full viewing key is - /// associated with the account. - fn is_valid_account_extfvk( - &self, - account: AccountId, - extfvk: &ExtendedFullViewingKey, - ) -> Result; + /// Returns the value in the account of all remaining received notes that either do not have + /// sufficient confirmations to be spendable, or for which witnesses cannot yet be constructed + /// without additional scanning. + pub fn value_pending_spendability(&self) -> Zatoshis { + self.value_pending_spendability + } - /// Returns the wallet balance for an account as of the specified block - /// height. - /// - /// This may be used to obtain a balance that ignores notes that have been - /// received so recently that they are not yet deemed spendable. - fn get_balance_at( - &self, - account: AccountId, - anchor_height: BlockHeight, - ) -> Result; + /// Adds the specified value to the pending spendable total, checking for overflow. + pub fn add_pending_spendable_value(&mut self, value: Zatoshis) -> Result<(), BalanceError> { + self.check_total_adding(value)?; + self.value_pending_spendability = (self.value_pending_spendability + value).unwrap(); + Ok(()) + } - /// Returns the memo for a note. - /// - /// Implementations of this method must return an error if the note identifier - /// does not appear in the backing data store. - fn get_memo(&self, id_note: Self::NoteRef) -> Result; + /// Returns the total value of funds represented by this [`Balance`]. + pub fn total(&self) -> Zatoshis { + (self.spendable_value + self.change_pending_confirmation + self.value_pending_spendability) + .expect("Balance cannot overflow MAX_MONEY") + } +} - /// Returns the note commitment tree at the specified block height. - fn get_commitment_tree( - &self, - block_height: BlockHeight, - ) -> Result>, Self::Error>; +/// Balance information for a single account. The sum of this struct's fields is the total balance +/// of the wallet. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AccountBalance { + /// The value of unspent Sapling outputs belonging to the account. + sapling_balance: Balance, - /// Returns the incremental witnesses as of the specified block height. - #[allow(clippy::type_complexity)] - fn get_witnesses( - &self, - block_height: BlockHeight, - ) -> Result)>, Self::Error>; + /// The value of unspent Orchard outputs belonging to the account. + orchard_balance: Balance, - /// Returns the unspent nullifiers, along with the account identifiers - /// with which they are associated. - fn get_nullifiers(&self) -> Result, Self::Error>; + /// The value of all unspent transparent outputs belonging to the account. + unshielded_balance: Balance, +} - /// Return all spendable notes. - fn get_spendable_notes( - &self, - account: AccountId, - anchor_height: BlockHeight, - ) -> Result, Self::Error>; +impl AccountBalance { + /// The [`Balance`] value having zero values for all its fields. + pub const ZERO: Self = Self { + sapling_balance: Balance::ZERO, + orchard_balance: Balance::ZERO, + unshielded_balance: Balance::ZERO, + }; - /// Returns a list of spendable notes sufficient to cover the specified - /// target value, if possible. - fn select_spendable_notes( - &self, - account: AccountId, - target_value: Amount, - anchor_height: BlockHeight, - ) -> Result, Self::Error>; + fn check_total(&self) -> Result { + (self.sapling_balance.total() + + self.orchard_balance.total() + + self.unshielded_balance.total()) + .ok_or(BalanceError::Overflow) + } + + /// Returns the [`Balance`] of Sapling funds in the account. + pub fn sapling_balance(&self) -> &Balance { + &self.sapling_balance + } + + /// Provides a mutable reference to the [`Balance`] of Sapling funds in the account + /// to the specified callback, checking invariants after the callback's action has been + /// evaluated. + pub fn with_sapling_balance_mut>( + &mut self, + f: impl FnOnce(&mut Balance) -> Result, + ) -> Result { + let result = f(&mut self.sapling_balance)?; + self.check_total()?; + Ok(result) + } + + /// Returns the [`Balance`] of Orchard funds in the account. + pub fn orchard_balance(&self) -> &Balance { + &self.orchard_balance + } + + /// Provides a mutable reference to the [`Balance`] of Orchard funds in the account + /// to the specified callback, checking invariants after the callback's action has been + /// evaluated. + pub fn with_orchard_balance_mut>( + &mut self, + f: impl FnOnce(&mut Balance) -> Result, + ) -> Result { + let result = f(&mut self.orchard_balance)?; + self.check_total()?; + Ok(result) + } + + /// Returns the total value of unspent transparent transaction outputs belonging to the wallet. + #[deprecated( + note = "this function is deprecated. Please use [`AccountBalance::unshielded_balance`] instead." + )] + pub fn unshielded(&self) -> Zatoshis { + self.unshielded_balance.total() + } + + /// Returns the [`Balance`] of unshielded funds in the account. + pub fn unshielded_balance(&self) -> &Balance { + &self.unshielded_balance + } + + /// Provides a mutable reference to the [`Balance`] of transparent funds in the account + /// to the specified callback, checking invariants after the callback's action has been + /// evaluated. + pub fn with_unshielded_balance_mut>( + &mut self, + f: impl FnOnce(&mut Balance) -> Result, + ) -> Result { + let result = f(&mut self.unshielded_balance)?; + self.check_total()?; + Ok(result) + } + + /// Returns the total value of funds belonging to the account. + pub fn total(&self) -> Zatoshis { + (self.sapling_balance.total() + + self.orchard_balance.total() + + self.unshielded_balance.total()) + .expect("Account balance cannot overflow MAX_MONEY") + } + + /// Returns the total value of shielded (Sapling and Orchard) funds that may immediately be + /// spent. + pub fn spendable_value(&self) -> Zatoshis { + (self.sapling_balance.spendable_value + self.orchard_balance.spendable_value) + .expect("Account balance cannot overflow MAX_MONEY") + } + + /// Returns the total value of change and/or shielding transaction outputs that are awaiting + /// sufficient confirmations for spendability. + pub fn change_pending_confirmation(&self) -> Zatoshis { + (self.sapling_balance.change_pending_confirmation + + self.orchard_balance.change_pending_confirmation) + .expect("Account balance cannot overflow MAX_MONEY") + } + + /// Returns the value of shielded funds that are not yet spendable because additional scanning + /// is required before it will be possible to derive witnesses for the associated notes. + pub fn value_pending_spendability(&self) -> Zatoshis { + (self.sapling_balance.value_pending_spendability + + self.orchard_balance.value_pending_spendability) + .expect("Account balance cannot overflow MAX_MONEY") + } } -/// The subset of information that is relevant to this wallet that has been -/// decrypted and extracted from a [CompactBlock]. -pub struct PrunedBlock<'a> { - pub block_height: BlockHeight, - pub block_hash: BlockHash, - pub block_time: u32, - pub commitment_tree: &'a CommitmentTree, - pub transactions: &'a Vec>, +/// Source metadata for a ZIP 32-derived key. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Zip32Derivation { + seed_fingerprint: SeedFingerprint, + account_index: zip32::AccountId, } -/// A transaction that was detected during scanning of the blockchain, -/// including its decrypted Sapling outputs. -/// -/// The purpose of this struct is to permit atomic updates of the -/// wallet database when transactions are successfully decrypted. -pub struct ReceivedTransaction<'a> { - pub tx: &'a Transaction, - pub outputs: &'a Vec, +impl Zip32Derivation { + /// Constructs new derivation metadata from its constituent parts. + pub fn new(seed_fingerprint: SeedFingerprint, account_index: zip32::AccountId) -> Self { + Self { + seed_fingerprint, + account_index, + } + } + + /// Returns the seed fingerprint. + pub fn seed_fingerprint(&self) -> &SeedFingerprint { + &self.seed_fingerprint + } + + /// Returns the account-level index in the ZIP 32 derivation path. + pub fn account_index(&self) -> zip32::AccountId { + self.account_index + } } -/// A transaction that was constructed and sent by the wallet. -/// -/// The purpose of this struct is to permit atomic updates of the -/// wallet database when transactions are created and submitted -/// to the network. -pub struct SentTransaction<'a> { - pub tx: &'a Transaction, - pub created: time::OffsetDateTime, - /// The index within the transaction that contains the recipient output. - /// - /// - If `recipient_address` is a Sapling address, this is an index into the Sapling - /// outputs of the transaction. - /// - If `recipient_address` is a transparent address, this is an index into the - /// transparent outputs of the transaction. - pub output_index: usize, - pub account: AccountId, - pub recipient_address: &'a RecipientAddress, - pub value: Amount, - pub memo: Option, +/// An enumeration used to control what information is tracked by the wallet for +/// notes received by a given account. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum AccountPurpose { + /// For spending accounts, the wallet will track information needed to spend + /// received notes. + Spending { derivation: Option }, + /// For view-only accounts, the wallet will not track spend information. + ViewOnly, } -/// This trait encapsulates the write capabilities required to update stored -/// wallet data. -pub trait WalletWrite: WalletRead { - #[allow(clippy::type_complexity)] - fn advance_by_block( - &mut self, - block: &PrunedBlock, - updated_witnesses: &[(Self::NoteRef, IncrementalWitness)], - ) -> Result)>, Self::Error>; +/// The kinds of accounts supported by `zcash_client_backend`. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum AccountSource { + /// An account derived from a known seed. + Derived { + derivation: Zip32Derivation, + key_source: Option, + }, - fn store_received_tx( - &mut self, - received_tx: &ReceivedTransaction, - ) -> Result; + /// An account imported from a viewing key. + Imported { + purpose: AccountPurpose, + key_source: Option, + }, +} - fn store_sent_tx(&mut self, sent_tx: &SentTransaction) -> Result; +impl AccountSource { + /// Returns the key derivation metadata for the account source, if any is available. + pub fn key_derivation(&self) -> Option<&Zip32Derivation> { + match self { + AccountSource::Derived { derivation, .. } => Some(derivation), + AccountSource::Imported { + purpose: AccountPurpose::Spending { derivation }, + .. + } => derivation.as_ref(), + _ => None, + } + } - /// Rewinds the wallet database to the specified height. - /// - /// This method assumes that the state of the underlying data store is - /// consistent up to a particular block height. Since it is possible that - /// a chain reorg might invalidate some stored state, this method must be - /// implemented in order to allow users of this API to "reset" the data store - /// to correctly represent chainstate as of a specified block height. + /// Returns the application-level key source identifier. + pub fn key_source(&self) -> Option<&str> { + match self { + AccountSource::Derived { key_source, .. } => key_source.as_ref().map(|s| s.as_str()), + AccountSource::Imported { key_source, .. } => key_source.as_ref().map(|s| s.as_str()), + } + } +} + +/// A set of capabilities that a client account must provide. +pub trait Account { + type AccountId: Copy; + + /// Returns the unique identifier for the account. + fn id(&self) -> Self::AccountId; + + /// Returns the human-readable name for the account, if any has been configured. + fn name(&self) -> Option<&str>; + + /// Returns whether this account is derived or imported, and the derivation parameters + /// if applicable. + fn source(&self) -> &AccountSource; + + /// Returns whether the account is a spending account or a view-only account. + fn purpose(&self) -> AccountPurpose { + match self.source() { + AccountSource::Derived { derivation, .. } => AccountPurpose::Spending { + derivation: Some(derivation.clone()), + }, + AccountSource::Imported { purpose, .. } => purpose.clone(), + } + } + + /// Returns the UFVK that the wallet backend has stored for the account, if any. /// - /// After calling this method, the block at the given height will be the - /// most recent block and all other operations will treat this block - /// as the chain tip for balance determination purposes. + /// Accounts for which this returns `None` cannot be used in wallet contexts, because + /// they are unable to maintain an accurate balance. + fn ufvk(&self) -> Option<&UnifiedFullViewingKey>; + + /// Returns the UIVK that the wallet backend has stored for the account. /// - /// There may be restrictions on how far it is possible to rewind. - fn rewind_to_height(&mut self, block_height: BlockHeight) -> Result<(), Self::Error>; + /// All accounts are required to have at least an incoming viewing key. This gives no + /// indication about whether an account can be used in a wallet context; for that, use + /// [`Account::ufvk`]. + fn uivk(&self) -> UnifiedIncomingViewingKey; } -/// This trait provides sequential access to raw blockchain data via a callback-oriented -/// API. -pub trait BlockSource { - type Error; +#[cfg(any(test, feature = "test-dependencies"))] +impl Account for (A, UnifiedFullViewingKey) { + type AccountId = A; - /// Scan the specified `limit` number of blocks from the blockchain, starting at - /// `from_height`, applying the provided callback to each block. - fn with_blocks( - &self, - from_height: BlockHeight, - limit: Option, - with_row: F, - ) -> Result<(), Self::Error> - where - F: FnMut(CompactBlock) -> Result<(), Self::Error>; + fn id(&self) -> A { + self.0 + } + + fn source(&self) -> &AccountSource { + &AccountSource::Imported { + purpose: AccountPurpose::ViewOnly, + key_source: None, + } + } + + fn ufvk(&self) -> Option<&UnifiedFullViewingKey> { + Some(&self.1) + } + + fn uivk(&self) -> UnifiedIncomingViewingKey { + self.1.to_unified_incoming_viewing_key() + } + + fn name(&self) -> Option<&str> { + None + } } -#[cfg(feature = "test-dependencies")] -pub mod testing { - use std::collections::HashMap; - - use zcash_primitives::{ - block::BlockHash, - consensus::BlockHeight, - memo::Memo, - merkle_tree::{CommitmentTree, IncrementalWitness}, - sapling::{Node, Nullifier, PaymentAddress}, - transaction::{components::Amount, TxId}, - zip32::ExtendedFullViewingKey, - }; +#[cfg(any(test, feature = "test-dependencies"))] +impl Account for (A, UnifiedIncomingViewingKey) { + type AccountId = A; - use crate::{ - proto::compact_formats::CompactBlock, - wallet::{AccountId, SpendableNote}, - }; + fn id(&self) -> A { + self.0 + } - use super::{ - error::Error, BlockSource, PrunedBlock, ReceivedTransaction, SentTransaction, WalletRead, - WalletWrite, - }; + fn name(&self) -> Option<&str> { + None + } - pub struct MockBlockSource {} - - impl BlockSource for MockBlockSource { - type Error = Error; - - fn with_blocks( - &self, - _from_height: BlockHeight, - _limit: Option, - _with_row: F, - ) -> Result<(), Self::Error> - where - F: FnMut(CompactBlock) -> Result<(), Self::Error>, - { - Ok(()) + fn source(&self) -> &AccountSource { + &AccountSource::Imported { + purpose: AccountPurpose::ViewOnly, + key_source: None, } } - pub struct MockWalletDb {} + fn ufvk(&self) -> Option<&UnifiedFullViewingKey> { + None + } - impl WalletRead for MockWalletDb { - type Error = Error; - type NoteRef = u32; - type TxRef = TxId; + fn uivk(&self) -> UnifiedIncomingViewingKey { + self.1.clone() + } +} - fn block_height_extrema(&self) -> Result, Self::Error> { - Ok(None) - } +/// A polymorphic ratio type, usually used for rational numbers. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Ratio { + numerator: T, + denominator: T, +} - fn get_block_hash( - &self, - _block_height: BlockHeight, - ) -> Result, Self::Error> { - Ok(None) +impl Ratio { + /// Constructs a new Ratio from a numerator and a denominator. + pub fn new(numerator: T, denominator: T) -> Self { + Self { + numerator, + denominator, } + } - fn get_tx_height(&self, _txid: TxId) -> Result, Self::Error> { - Ok(None) - } + /// Returns the numerator of the ratio. + pub fn numerator(&self) -> &T { + &self.numerator + } - fn get_address(&self, _account: AccountId) -> Result, Self::Error> { - Ok(None) - } + /// Returns the denominator of the ratio. + pub fn denominator(&self) -> &T { + &self.denominator + } +} - fn get_extended_full_viewing_keys( - &self, - ) -> Result, Self::Error> { - Ok(HashMap::new()) - } +/// A type representing the progress the wallet has made toward detecting all of the funds +/// belonging to the wallet. +/// +/// The window over which progress is computed spans from the wallet's birthday to the current +/// chain tip. It is divided into two regions, the "Scan Window" which covers the region from the +/// wallet recovery height to the current chain tip, and the "Recovery Window" which covers the +/// range from the wallet birthday to the wallet recovery height. If no wallet recovery height is +/// available, the scan window will cover the entire range from the wallet birthday to the chain +/// tip. +/// +/// Progress for both scanning and recovery is represented in terms of the ratio between notes +/// scanned and the total number of notes added to the chain in the relevant window. This ratio +/// should only be used to compute progress percentages for display, and the numerator and +/// denominator should not be treated as authoritative note counts. In the case that there are no +/// notes in a given block range, the denominator of these values will be zero, so callers should always +/// use checked division when converting the resulting values to percentages. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Progress { + scan: Ratio, + recovery: Option>, +} - fn is_valid_account_extfvk( - &self, - _account: AccountId, - _extfvk: &ExtendedFullViewingKey, - ) -> Result { - Ok(false) - } +impl Progress { + /// Constructs a new progress value from its constituent parts. + pub fn new(scan: Ratio, recovery: Option>) -> Self { + Self { scan, recovery } + } - fn get_balance_at( - &self, - _account: AccountId, - _anchor_height: BlockHeight, - ) -> Result { - Ok(Amount::zero()) - } + /// Returns the progress the wallet has made in scanning blocks for shielded notes belonging to + /// the wallet between the wallet recovery height (or the wallet birthday if no recovery height + /// is set) and the chain tip. + pub fn scan(&self) -> Ratio { + self.scan + } - fn get_memo(&self, _id_note: Self::NoteRef) -> Result { - Ok(Memo::Empty) - } + /// Returns the progress the wallet has made in scanning blocks for shielded notes belonging to + /// the wallet between the wallet birthday and the block height at which recovery from seed was + /// initiated. + /// + /// Returns `None` if no recovery height is set for the wallet. + pub fn recovery(&self) -> Option> { + self.recovery + } +} - fn get_commitment_tree( - &self, - _block_height: BlockHeight, - ) -> Result>, Self::Error> { - Ok(None) - } +/// A type representing the potentially-spendable value of unspent outputs in the wallet. +/// +/// The balances reported using this data structure may overestimate the total spendable value of +/// the wallet, in the case that the spend of a previously received shielded note has not yet been +/// detected by the process of scanning the chain. The balances reported using this data structure +/// can only be certain to be unspent in the case that [`Self::is_synced`] is true, and even in +/// this circumstance it is possible that a newly created transaction could conflict with a +/// not-yet-mined transaction in the mempool. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WalletSummary { + account_balances: HashMap, + chain_tip_height: BlockHeight, + fully_scanned_height: BlockHeight, + progress: Progress, + next_sapling_subtree_index: u64, + #[cfg(feature = "orchard")] + next_orchard_subtree_index: u64, +} - #[allow(clippy::type_complexity)] - fn get_witnesses( - &self, - _block_height: BlockHeight, - ) -> Result)>, Self::Error> { - Ok(Vec::new()) +impl WalletSummary { + /// Constructs a new [`WalletSummary`] from its constituent parts. + pub fn new( + account_balances: HashMap, + chain_tip_height: BlockHeight, + fully_scanned_height: BlockHeight, + progress: Progress, + next_sapling_subtree_index: u64, + #[cfg(feature = "orchard")] next_orchard_subtree_index: u64, + ) -> Self { + Self { + account_balances, + chain_tip_height, + fully_scanned_height, + progress, + next_sapling_subtree_index, + #[cfg(feature = "orchard")] + next_orchard_subtree_index, } + } - fn get_nullifiers(&self) -> Result, Self::Error> { - Ok(Vec::new()) - } + /// Returns the balances of accounts in the wallet, keyed by account ID. + pub fn account_balances(&self) -> &HashMap { + &self.account_balances + } - fn get_spendable_notes( - &self, - _account: AccountId, - _anchor_height: BlockHeight, - ) -> Result, Self::Error> { - Ok(Vec::new()) - } + /// Returns the height of the current chain tip. + pub fn chain_tip_height(&self) -> BlockHeight { + self.chain_tip_height + } - fn select_spendable_notes( - &self, - _account: AccountId, - _target_value: Amount, - _anchor_height: BlockHeight, - ) -> Result, Self::Error> { - Ok(Vec::new()) - } + /// Returns the height below which all blocks have been scanned by the wallet, ignoring blocks + /// below the wallet birthday. + pub fn fully_scanned_height(&self) -> BlockHeight { + self.fully_scanned_height } - impl WalletWrite for MockWalletDb { - #[allow(clippy::type_complexity)] - fn advance_by_block( - &mut self, - _block: &PrunedBlock, - _updated_witnesses: &[(Self::NoteRef, IncrementalWitness)], - ) -> Result)>, Self::Error> { - Ok(vec![]) - } + /// Returns the progress of scanning the chain to bring the wallet up to date. + /// + /// This progress metric is intended as an indicator of how close the wallet is to + /// general usability, including the ability to spend existing funds that were + /// previously spendable. + /// + /// The window over which progress is computed spans from the wallet's birthday to the current + /// chain tip. It is divided into two segments: a "recovery" segment, between the wallet + /// birthday and the recovery height (currently the height at which recovery from seed was + /// initiated, but how this boundary is computed may change in the future), and a "scan" + /// segment, between the recovery height and the current chain tip. + /// + /// When converting the ratios returned here to percentages, checked division must be used in + /// order to avoid divide-by-zero errors. A zero denominator in a returned ratio indicates that + /// there are no shielded notes to be scanned in the associated block range. + pub fn progress(&self) -> Progress { + self.progress + } - fn store_received_tx( - &mut self, - _received_tx: &ReceivedTransaction, - ) -> Result { - Ok(TxId([0u8; 32])) - } + /// Returns the Sapling subtree index that should start the next range of subtree + /// roots passed to [`WalletCommitmentTrees::put_sapling_subtree_roots`]. + pub fn next_sapling_subtree_index(&self) -> u64 { + self.next_sapling_subtree_index + } - fn store_sent_tx( - &mut self, - _sent_tx: &SentTransaction, - ) -> Result { - Ok(TxId([0u8; 32])) - } + /// Returns the Orchard subtree index that should start the next range of subtree + /// roots passed to [`WalletCommitmentTrees::put_orchard_subtree_roots`]. + #[cfg(feature = "orchard")] + pub fn next_orchard_subtree_index(&self) -> u64 { + self.next_orchard_subtree_index + } + + /// Returns whether or not wallet scanning is complete. + pub fn is_synced(&self) -> bool { + self.chain_tip_height == self.fully_scanned_height + } +} + +/// A predicate that can be used to choose whether or not a particular note is retained in note +/// selection. +pub trait NoteRetention { + /// Returns whether the specified Sapling note should be retained. + fn should_retain_sapling(&self, note: &ReceivedNote) -> bool; + /// Returns whether the specified Orchard note should be retained. + #[cfg(feature = "orchard")] + fn should_retain_orchard(&self, note: &ReceivedNote) -> bool; +} + +pub(crate) struct SimpleNoteRetention { + pub(crate) sapling: bool, + #[cfg(feature = "orchard")] + pub(crate) orchard: bool, +} + +impl NoteRetention for SimpleNoteRetention { + fn should_retain_sapling(&self, _: &ReceivedNote) -> bool { + self.sapling + } + + #[cfg(feature = "orchard")] + fn should_retain_orchard(&self, _: &ReceivedNote) -> bool { + self.orchard + } +} + +/// Spendable shielded outputs controlled by the wallet. +pub struct SpendableNotes { + sapling: Vec>, + #[cfg(feature = "orchard")] + orchard: Vec>, +} + +/// A request for transaction data enhancement, spentness check, or discovery +/// of spends from a given transparent address within a specific block range. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum TransactionDataRequest { + /// Information about the chain's view of a transaction is requested. + /// + /// The caller evaluating this request on behalf of the wallet backend should respond to this + /// request by determining the status of the specified transaction with respect to the main + /// chain; if using `lightwalletd` for access to chain data, this may be obtained by + /// interpreting the results of the [`GetTransaction`] RPC method. It should then call + /// [`WalletWrite::set_transaction_status`] to provide the resulting transaction status + /// information to the wallet backend. + /// + /// [`GetTransaction`]: crate::proto::service::compact_tx_streamer_client::CompactTxStreamerClient::get_transaction + GetStatus(TxId), + /// Transaction enhancement (download of complete raw transaction data) is requested. + /// + /// The caller evaluating this request on behalf of the wallet backend should respond to this + /// request by providing complete data for the specified transaction to + /// [`wallet::decrypt_and_store_transaction`]; if using `lightwalletd` for access to chain + /// state, this may be obtained via the [`GetTransaction`] RPC method. If no data is available + /// for the specified transaction, this should be reported to the backend using + /// [`WalletWrite::set_transaction_status`]. A [`TransactionDataRequest::Enhancement`] request + /// subsumes any previously existing [`TransactionDataRequest::GetStatus`] request. + /// + /// [`GetTransaction`]: crate::proto::service::compact_tx_streamer_client::CompactTxStreamerClient::get_transaction + Enhancement(TxId), + /// Information about transactions that receive or spend funds belonging to the specified + /// transparent address is requested. + /// + /// Fully transparent transactions, and transactions that do not contain either shielded inputs + /// or shielded outputs belonging to the wallet, may not be discovered by the process of chain + /// scanning; as a consequence, the wallet must actively query to find transactions that spend + /// such funds. Ideally we'd be able to query by [`OutPoint`] but this is not currently + /// functionality that is supported by the light wallet server. + /// + /// The caller evaluating this request on behalf of the wallet backend should respond to this + /// request by detecting transactions involving the specified address within the provided block + /// range; if using `lightwalletd` for access to chain data, this may be performed using the + /// [`GetTaddressTxids`] RPC method. It should then call [`wallet::decrypt_and_store_transaction`] + /// for each transaction so detected. + /// + /// [`GetTaddressTxids`]: crate::proto::service::compact_tx_streamer_client::CompactTxStreamerClient::get_taddress_txids + #[cfg(feature = "transparent-inputs")] + SpendsFromAddress { + address: TransparentAddress, + block_range_start: BlockHeight, + block_range_end: Option, + }, +} + +/// Metadata about the status of a transaction obtained by inspecting the chain state. +#[derive(Clone, Copy, Debug)] +pub enum TransactionStatus { + /// The requested transaction ID was not recognized by the node. + TxidNotRecognized, + /// The requested transaction ID corresponds to a transaction that is recognized by the node, + /// but is in the mempool or is otherwise not mined in the main chain (but may have been mined + /// on a fork that was reorged away). + NotInMainChain, + /// The requested transaction ID corresponds to a transaction that has been included in the + /// block at the provided height. + Mined(BlockHeight), +} + +impl SpendableNotes { + /// Construct a new empty [`SpendableNotes`]. + pub fn empty() -> Self { + Self::new( + vec![], + #[cfg(feature = "orchard")] + vec![], + ) + } - fn rewind_to_height(&mut self, _block_height: BlockHeight) -> Result<(), Self::Error> { - Ok(()) + /// Construct a new [`SpendableNotes`] from its constituent parts. + pub fn new( + sapling: Vec>, + #[cfg(feature = "orchard")] orchard: Vec>, + ) -> Self { + Self { + sapling, + #[cfg(feature = "orchard")] + orchard, } } + + /// Returns the set of spendable Sapling notes. + pub fn sapling(&self) -> &[ReceivedNote] { + self.sapling.as_ref() + } + + /// Consumes this value and returns the Sapling notes contained within it. + pub fn take_sapling(self) -> Vec> { + self.sapling + } + + /// Returns the set of spendable Orchard notes. + #[cfg(feature = "orchard")] + pub fn orchard(&self) -> &[ReceivedNote] { + self.orchard.as_ref() + } + + /// Consumes this value and returns the Orchard notes contained within it. + #[cfg(feature = "orchard")] + pub fn take_orchard(self) -> Vec> { + self.orchard + } + + /// Computes the total value of Sapling notes. + pub fn sapling_value(&self) -> Result { + self.sapling.iter().try_fold(Zatoshis::ZERO, |acc, n| { + (acc + n.note_value()?).ok_or(BalanceError::Overflow) + }) + } + + /// Computes the total value of Sapling notes. + #[cfg(feature = "orchard")] + pub fn orchard_value(&self) -> Result { + self.orchard.iter().try_fold(Zatoshis::ZERO, |acc, n| { + (acc + n.note_value()?).ok_or(BalanceError::Overflow) + }) + } + + /// Computes the total value of spendable inputs + pub fn total_value(&self) -> Result { + #[cfg(not(feature = "orchard"))] + return self.sapling_value(); + + #[cfg(feature = "orchard")] + return (self.sapling_value()? + self.orchard_value()?).ok_or(BalanceError::Overflow); + } + + /// Consumes this [`SpendableNotes`] value and produces a vector of + /// [`ReceivedNote`] values. + pub fn into_vec( + self, + retention: &impl NoteRetention, + ) -> Vec> { + let iter = self.sapling.into_iter().filter_map(|n| { + retention + .should_retain_sapling(&n) + .then(|| n.map_note(Note::Sapling)) + }); + + #[cfg(feature = "orchard")] + let iter = iter.chain(self.orchard.into_iter().filter_map(|n| { + retention + .should_retain_orchard(&n) + .then(|| n.map_note(Note::Orchard)) + })); + + iter.collect() + } +} + +/// Metadata about the structure of unspent outputs in a single pool within a wallet account. +/// +/// This type is often used to represent a filtered view of outputs in the account that were +/// selected according to the conditions imposed by a [`NoteFilter`]. +#[derive(Debug, Clone)] +pub struct PoolMeta { + note_count: usize, + value: Zatoshis, +} + +impl PoolMeta { + /// Constructs a new [`PoolMeta`] value from its constituent parts. + pub fn new(note_count: usize, value: Zatoshis) -> Self { + Self { note_count, value } + } + + /// Returns the number of unspent outputs in the account, potentially selected in accordance + /// with some [`NoteFilter`]. + pub fn note_count(&self) -> usize { + self.note_count + } + + /// Returns the total value of unspent outputs in the account that are accounted for in + /// [`Self::note_count`]. + pub fn value(&self) -> Zatoshis { + self.value + } +} + +/// Metadata about the structure of the wallet for a particular account. +/// +/// At present this just contains counts of unspent outputs in each pool, but it may be extended in +/// the future to contain note values or other more detailed information about wallet structure. +/// +/// Values of this type are intended to be used in selection of change output values. A value of +/// this type may represent filtered data, and may therefore not count all of the unspent notes in +/// the wallet. +/// +/// A [`AccountMeta`] value is normally produced by querying the wallet database via passing a +/// [`NoteFilter`] to [`InputSource::get_account_metadata`]. +#[derive(Debug, Clone)] +pub struct AccountMeta { + sapling: Option, + orchard: Option, +} + +impl AccountMeta { + /// Constructs a new [`AccountMeta`] value from its constituent parts. + pub fn new(sapling: Option, orchard: Option) -> Self { + Self { sapling, orchard } + } + + /// Returns metadata about Sapling notes belonging to the account for which this was generated. + /// + /// Returns [`None`] if no metadata is available or it was not possible to evaluate the query + /// described by a [`NoteFilter`] given the available wallet data. + pub fn sapling(&self) -> Option<&PoolMeta> { + self.sapling.as_ref() + } + + /// Returns metadata about Orchard notes belonging to the account for which this was generated. + /// + /// Returns [`None`] if no metadata is available or it was not possible to evaluate the query + /// described by a [`NoteFilter`] given the available wallet data. + pub fn orchard(&self) -> Option<&PoolMeta> { + self.orchard.as_ref() + } + + fn sapling_note_count(&self) -> Option { + self.sapling.as_ref().map(|m| m.note_count) + } + + fn orchard_note_count(&self) -> Option { + self.orchard.as_ref().map(|m| m.note_count) + } + + /// Returns the number of unspent notes in the wallet for the given shielded protocol. + pub fn note_count(&self, protocol: ShieldedProtocol) -> Option { + match protocol { + ShieldedProtocol::Sapling => self.sapling_note_count(), + ShieldedProtocol::Orchard => self.orchard_note_count(), + } + } + + /// Returns the total number of unspent shielded notes belonging to the account for which this + /// was generated. + /// + /// Returns [`None`] if no metadata is available or it was not possible to evaluate the query + /// described by a [`NoteFilter`] given the available wallet data. If metadata is available + /// only for a single pool, the metadata for that pool will be returned. + pub fn total_note_count(&self) -> Option { + let s = self.sapling_note_count(); + let o = self.orchard_note_count(); + s.zip(o).map(|(s, o)| s + o).or(s).or(o) + } + + fn sapling_value(&self) -> Option { + self.sapling.as_ref().map(|m| m.value) + } + + fn orchard_value(&self) -> Option { + self.orchard.as_ref().map(|m| m.value) + } + + /// Returns the total value of shielded notes represented by [`Self::total_note_count`] + /// + /// Returns [`None`] if no metadata is available or it was not possible to evaluate the query + /// described by a [`NoteFilter`] given the available wallet data. If metadata is available + /// only for a single pool, the metadata for that pool will be returned. + pub fn total_value(&self) -> Option { + let s = self.sapling_value(); + let o = self.orchard_value(); + s.zip(o) + .map(|(s, o)| (s + o).expect("Does not overflow Zcash maximum value.")) + .or(s) + .or(o) + } +} + +/// A `u8` value in the range 0..=MAX +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct BoundedU8(u8); + +impl BoundedU8 { + /// Creates a constant `BoundedU8` from a [`u8`] value. + /// + /// Panics: if the value is outside the range `0..=MAX`. + pub const fn new_const(value: u8) -> Self { + assert!(value <= MAX); + Self(value) + } + + /// Creates a `BoundedU8` from a [`u8`] value. + /// + /// Returns `None` if the provided value is outside the range `0..=MAX`. + pub fn new(value: u8) -> Option { + if value <= MAX { + Some(Self(value)) + } else { + None + } + } + + /// Returns the wrapped [`u8`] value. + pub fn value(&self) -> u8 { + self.0 + } +} + +impl From> for u8 { + fn from(value: BoundedU8) -> Self { + value.0 + } +} + +impl From> for usize { + fn from(value: BoundedU8) -> Self { + usize::from(value.0) + } +} + +/// A small query language for filtering notes belonging to an account. +/// +/// A filter described using this language is applied to notes individually. It is primarily +/// intended for retrieval of account metadata in service of making determinations for how to +/// allocate change notes, and is not currently intended for use in broader note selection +/// contexts. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum NoteFilter { + /// Selects notes having value greater than or equal to the provided value. + ExceedsMinValue(Zatoshis), + /// Selects notes having value greater than or equal to approximately the n'th percentile of + /// previously sent notes in the account, irrespective of pool. The wrapped value must be in + /// the range `1..=99`. The value `n` is respected in a best-effort fashion; results are likely + /// to be inaccurate if the account has not yet completed scanning or if insufficient send data + /// is available to establish a distribution. + // TODO: it might be worthwhile to add an optional parameter here that can be used to ignore + // low-valued (test/memo-only) sends when constructing the distribution to be drawn from. + ExceedsPriorSendPercentile(BoundedU8<99>), + /// Selects notes having value greater than or equal to the specified percentage of the account + /// balance across all shielded pools. The wrapped value must be in the range `1..=99` + ExceedsBalancePercentage(BoundedU8<99>), + /// A note will be selected if it satisfies both of the specified conditions. + /// + /// If it is not possible to evaluate one of the conditions (for example, + /// [`NoteFilter::ExceedsPriorSendPercentile`] cannot be evaluated if no sends have been + /// performed) then that condition will be ignored. If neither condition can be evaluated, + /// then the entire condition cannot be evaluated. + Combine(Box, Box), + /// A note will be selected if it satisfies the first condition; if it is not possible to + /// evaluate that condition (for example, [`NoteFilter::ExceedsPriorSendPercentile`] cannot + /// be evaluated if no sends have been performed) then the second condition will be used for + /// evaluation. + Attempt { + condition: Box, + fallback: Box, + }, +} + +impl NoteFilter { + /// Constructs a [`NoteFilter::Combine`] query node. + pub fn combine(l: NoteFilter, r: NoteFilter) -> Self { + Self::Combine(Box::new(l), Box::new(r)) + } + + /// Constructs a [`NoteFilter::Attempt`] query node. + pub fn attempt(condition: NoteFilter, fallback: NoteFilter) -> Self { + Self::Attempt { + condition: Box::new(condition), + fallback: Box::new(fallback), + } + } +} + +/// A trait representing the capability to query a data store for unspent transaction outputs +/// belonging to a account. +#[cfg_attr(feature = "test-dependencies", delegatable_trait)] +pub trait InputSource { + /// The type of errors produced by a wallet backend. + type Error: Debug; + + /// Backend-specific account identifier. + /// + /// An account identifier corresponds to at most a single unified spending key's worth of spend + /// authority, such that both received notes and change spendable by that spending authority + /// will be interpreted as belonging to that account. This might be a database identifier type + /// or a UUID. + type AccountId: Copy + Debug + Eq + Hash; + + /// Backend-specific note identifier. + /// + /// For example, this might be a database identifier type or a UUID. + type NoteRef: Copy + Debug + Eq + Ord; + + /// Fetches a spendable note by indexing into a transaction's shielded outputs for the + /// specified shielded protocol. + /// + /// Returns `Ok(None)` if the note is not known to belong to the wallet or if the note + /// is not spendable. + fn get_spendable_note( + &self, + txid: &TxId, + protocol: ShieldedProtocol, + index: u32, + ) -> Result>, Self::Error>; + + /// Returns a list of spendable notes sufficient to cover the specified target value, if + /// possible. Only spendable notes corresponding to the specified shielded protocol will + /// be included. + fn select_spendable_notes( + &self, + account: Self::AccountId, + target_value: Zatoshis, + sources: &[ShieldedProtocol], + anchor_height: BlockHeight, + exclude: &[Self::NoteRef], + ) -> Result, Self::Error>; + + /// Returns metadata describing the structure of the wallet for the specified account. + /// + /// The returned metadata value must exclude: + /// - spent notes; + /// - unspent notes excluded by the provided selector; + /// - unspent notes identified in the given `exclude` list. + /// + /// Implementations of this method may limit the complexity of supported queries. Such + /// limitations should be clearly documented for the implementing type. + fn get_account_metadata( + &self, + account: Self::AccountId, + selector: &NoteFilter, + exclude: &[Self::NoteRef], + ) -> Result; + + /// Fetches the transparent output corresponding to the provided `outpoint`. + /// + /// Returns `Ok(None)` if the UTXO is not known to belong to the wallet or is not + /// spendable as of the chain tip height. + #[cfg(feature = "transparent-inputs")] + fn get_unspent_transparent_output( + &self, + _outpoint: &OutPoint, + ) -> Result, Self::Error> { + Ok(None) + } + + /// Returns the list of spendable transparent outputs received by this wallet at `address` + /// such that, at height `target_height`: + /// * the transaction that produced the output had or will have at least `min_confirmations` + /// confirmations; and + /// * the output is unspent as of the current chain tip. + /// + /// An output that is potentially spent by an unmined transaction in the mempool is excluded + /// iff the spending transaction will not be expired at `target_height`. + #[cfg(feature = "transparent-inputs")] + fn get_spendable_transparent_outputs( + &self, + _address: &TransparentAddress, + _target_height: BlockHeight, + _min_confirmations: u32, + ) -> Result, Self::Error> { + Ok(vec![]) + } +} + +/// Read-only operations required for light wallet functions. +/// +/// This trait defines the read-only portion of the storage interface atop which +/// higher-level wallet operations are implemented. It serves to allow wallet functions to +/// be abstracted away from any particular data storage substrate. +#[cfg_attr(feature = "test-dependencies", delegatable_trait)] +pub trait WalletRead { + /// The type of errors that may be generated when querying a wallet data store. + type Error: Debug; + + /// The type of the account identifier. + /// + /// An account identifier corresponds to at most a single unified spending key's worth of spend + /// authority, such that both received notes and change spendable by that spending authority + /// will be interpreted as belonging to that account. + type AccountId: Copy + Debug + Eq + Hash; + + /// The concrete account type used by this wallet backend. + type Account: Account; + + /// Returns a vector with the IDs of all accounts known to this wallet. + fn get_account_ids(&self) -> Result, Self::Error>; + + /// Returns the account corresponding to the given ID, if any. + fn get_account( + &self, + account_id: Self::AccountId, + ) -> Result, Self::Error>; + + /// Returns the account corresponding to a given [`SeedFingerprint`] and + /// [`zip32::AccountId`], if any. + fn get_derived_account( + &self, + seed: &SeedFingerprint, + account_id: zip32::AccountId, + ) -> Result, Self::Error>; + + /// Verifies that the given seed corresponds to the viewing key for the specified account. + /// + /// Returns: + /// - `Ok(true)` if the viewing key for the specified account can be derived from the + /// provided seed. + /// - `Ok(false)` if the derived viewing key does not match, or the specified account is not + /// present in the database. + /// - `Err(_)` if a Unified Spending Key cannot be derived from the seed for the + /// specified account or the account has no known ZIP-32 derivation. + fn validate_seed( + &self, + account_id: Self::AccountId, + seed: &SecretVec, + ) -> Result; + + /// Checks whether the given seed is relevant to any of the derived accounts (where + /// [`Account::source`] is [`AccountSource::Derived`]) in the wallet. + /// + /// This API does not check whether the seed is relevant to any imported account, + /// because that would require brute-forcing the ZIP 32 account index space. + fn seed_relevance_to_derived_accounts( + &self, + seed: &SecretVec, + ) -> Result, Self::Error>; + + /// Returns the account corresponding to a given [`UnifiedFullViewingKey`], if any. + fn get_account_for_ufvk( + &self, + ufvk: &UnifiedFullViewingKey, + ) -> Result, Self::Error>; + + /// Returns the most recently generated unified address for the specified account, if the + /// account identifier specified refers to a valid account for this wallet. + /// + /// This will return `Ok(None)` if the account identifier does not correspond to a known + /// account. + fn get_current_address( + &self, + account: Self::AccountId, + ) -> Result, Self::Error>; + + /// Returns the birthday height for the given account, or an error if the account is not known + /// to the wallet. + fn get_account_birthday(&self, account: Self::AccountId) -> Result; + + /// Returns the birthday height for the wallet. + /// + /// This returns the earliest birthday height among accounts maintained by this wallet, + /// or `Ok(None)` if the wallet has no initialized accounts. + fn get_wallet_birthday(&self) -> Result, Self::Error>; + + /// Returns a [`WalletSummary`] that represents the sync status, and the wallet balances + /// given the specified minimum number of confirmations for all accounts known to the + /// wallet; or `Ok(None)` if the wallet has no summary data available. + fn get_wallet_summary( + &self, + min_confirmations: u32, + ) -> Result>, Self::Error>; + + /// Returns the height of the chain as known to the wallet as of the most recent call to + /// [`WalletWrite::update_chain_tip`]. + /// + /// This will return `Ok(None)` if the height of the current consensus chain tip is unknown. + fn chain_height(&self) -> Result, Self::Error>; + + /// Returns the block hash for the block at the given height, if the + /// associated block data is available. Returns `Ok(None)` if the hash + /// is not found in the database. + fn get_block_hash(&self, block_height: BlockHeight) -> Result, Self::Error>; + + /// Returns the available block metadata for the block at the specified height, if any. + fn block_metadata(&self, height: BlockHeight) -> Result, Self::Error>; + + /// Returns the metadata for the block at the height to which the wallet has been fully + /// scanned. + /// + /// This is the height for which the wallet has fully trial-decrypted this and all preceding + /// blocks above the wallet's birthday height. Along with this height, this method returns + /// metadata describing the state of the wallet's note commitment trees as of the end of that + /// block. + fn block_fully_scanned(&self) -> Result, Self::Error>; + + /// Returns the block height and hash for the block at the maximum scanned block height. + /// + /// This will return `Ok(None)` if no blocks have been scanned. + fn get_max_height_hash(&self) -> Result, Self::Error>; + + /// Returns block metadata for the maximum height that the wallet has scanned. + /// + /// If the wallet is fully synced, this will be equivalent to `block_fully_scanned`; + /// otherwise the maximal scanned height is likely to be greater than the fully scanned height + /// due to the fact that out-of-order scanning can leave gaps. + fn block_max_scanned(&self) -> Result, Self::Error>; + + /// Returns a vector of suggested scan ranges based upon the current wallet state. + /// + /// This method should only be used in cases where the [`CompactBlock`] data that will be made + /// available to `scan_cached_blocks` for the requested block ranges includes note commitment + /// tree size information for each block; or else the scan is likely to fail if notes belonging + /// to the wallet are detected. + /// + /// The returned range(s) may include block heights beyond the current chain tip. Ranges are + /// returned in order of descending priority, and higher-priority ranges should always be + /// scanned before lower-priority ranges; in particular, ranges with [`ScanPriority::Verify`] + /// priority must always be scanned first in order to avoid blockchain continuity errors in the + /// case of a reorg. + /// + /// [`CompactBlock`]: crate::proto::compact_formats::CompactBlock + /// [`ScanPriority::Verify`]: crate::data_api::scanning::ScanPriority + fn suggest_scan_ranges(&self) -> Result, Self::Error>; + + /// Returns the default target height (for the block in which a new + /// transaction would be mined) and anchor height (to use for a new + /// transaction), given the range of block heights that the backend + /// knows about. + /// + /// This will return `Ok(None)` if no block data is present in the database. + fn get_target_and_anchor_heights( + &self, + min_confirmations: NonZeroU32, + ) -> Result, Self::Error>; + + /// Returns the block height in which the specified transaction was mined, or `Ok(None)` if the + /// transaction is not in the main chain. + fn get_tx_height(&self, txid: TxId) -> Result, Self::Error>; + + /// Returns all unified full viewing keys known to this wallet. + fn get_unified_full_viewing_keys( + &self, + ) -> Result, Self::Error>; + + /// Returns the memo for a note. + /// + /// Returns `Ok(None)` if the note is known to the wallet but memo data has not yet been + /// populated for that note, or if the note identifier does not correspond to a note + /// that is known to the wallet. + fn get_memo(&self, note_id: NoteId) -> Result, Self::Error>; + + /// Returns a transaction. + fn get_transaction(&self, txid: TxId) -> Result, Self::Error>; + + /// Returns the nullifiers for Sapling notes that the wallet is tracking, along with their + /// associated account IDs, that are either unspent or have not yet been confirmed as spent (in + /// that a spending transaction known to the wallet has not yet been included in a block). + fn get_sapling_nullifiers( + &self, + query: NullifierQuery, + ) -> Result, Self::Error>; + + /// Returns the nullifiers for Orchard notes that the wallet is tracking, along with their + /// associated account IDs, that are either unspent or have not yet been confirmed as spent (in + /// that a spending transaction known to the wallet has not yet been included in a block). + #[cfg(feature = "orchard")] + fn get_orchard_nullifiers( + &self, + query: NullifierQuery, + ) -> Result, Self::Error>; + + /// Returns the set of non-ephemeral transparent receivers associated with the given + /// account controlled by this wallet. + /// + /// The set contains all non-ephemeral transparent receivers that are known to have + /// been derived under this account. Wallets should scan the chain for UTXOs sent to + /// these receivers. + /// + /// Use [`Self::get_known_ephemeral_addresses`] to obtain the ephemeral transparent + /// receivers. + #[cfg(feature = "transparent-inputs")] + fn get_transparent_receivers( + &self, + _account: Self::AccountId, + ) -> Result>, Self::Error> { + Ok(HashMap::new()) + } + + /// Returns a mapping from each transparent receiver associated with the specified account + /// to its not-yet-shielded UTXO balance as of the end of the block at the provided + /// `max_height`, when that balance is non-zero. + /// + /// Balances of ephemeral transparent addresses will not be included. + #[cfg(feature = "transparent-inputs")] + fn get_transparent_balances( + &self, + _account: Self::AccountId, + _max_height: BlockHeight, + ) -> Result, Self::Error> { + Ok(HashMap::new()) + } + + /// Returns the metadata associated with a given transparent receiver in an account + /// controlled by this wallet, if available. + /// + /// This is equivalent to (but may be implemented more efficiently than): + /// ```compile_fail + /// Ok( + /// if let Some(result) = self.get_transparent_receivers(account)?.get(address) { + /// result.clone() + /// } else { + /// self.get_known_ephemeral_addresses(account, None)? + /// .into_iter() + /// .find(|(known_addr, _)| known_addr == address) + /// .map(|(_, metadata)| metadata) + /// }, + /// ) + /// ``` + /// + /// Returns `Ok(None)` if the address is not recognized, or we do not have metadata for it. + /// Returns `Ok(Some(metadata))` if we have the metadata. + #[cfg(feature = "transparent-inputs")] + fn get_transparent_address_metadata( + &self, + account: Self::AccountId, + address: &TransparentAddress, + ) -> Result, Self::Error> { + // This should be overridden. + Ok( + if let Some(result) = self.get_transparent_receivers(account)?.get(address) { + result.clone() + } else { + self.get_known_ephemeral_addresses(account, None)? + .into_iter() + .find(|(known_addr, _)| known_addr == address) + .map(|(_, metadata)| metadata) + }, + ) + } + + /// Returns a vector of ephemeral transparent addresses associated with the given + /// account controlled by this wallet, along with their metadata. The result includes + /// reserved addresses, and addresses for [`GAP_LIMIT`] additional indices (capped to + /// the maximum index). + /// + /// If `index_range` is some `Range`, it limits the result to addresses with indices + /// in that range. An `index_range` of `None` is defined to be equivalent to + /// `0..(1u32 << 31)`. + /// + /// Wallets should scan the chain for UTXOs sent to these ephemeral transparent + /// receivers, but do not need to do so regularly. Under expected usage, outputs + /// would only be detected with these receivers in the following situations: + /// + /// - This wallet created a payment to a ZIP 320 (TEX) address, but the second + /// transaction (that spent the output sent to the ephemeral address) did not get + /// mined before it expired. + /// - In this case the output will already be known to the wallet (because it + /// stores the transactions that it creates). + /// + /// - Another wallet app using the same seed phrase created a payment to a ZIP 320 + /// address, and this wallet queried for the ephemeral UTXOs after the first + /// transaction was mined but before the second transaction was mined. + /// - In this case, the output should not be considered unspent until the expiry + /// height of the transaction it was received in has passed. Wallets creating + /// payments to TEX addresses generally set the same expiry height for the first + /// and second transactions, meaning that this wallet does not need to observe + /// the second transaction to determine when it would have expired. + /// + /// - A TEX address recipient decided to return funds that the wallet had sent to + /// them. + /// + /// In all cases, the wallet should re-shield the unspent outputs, in a separate + /// transaction per ephemeral address, before re-spending the funds. + #[cfg(feature = "transparent-inputs")] + fn get_known_ephemeral_addresses( + &self, + _account: Self::AccountId, + _index_range: Option>, + ) -> Result, Self::Error> { + Ok(vec![]) + } + + /// If a given ephemeral address might have been reserved, i.e. would be included in + /// the result of `get_known_ephemeral_addresses(account_id, None)` for any of the + /// wallet's accounts, then return `Ok(Some(account_id))`. Otherwise return `Ok(None)`. + /// + /// This is equivalent to (but may be implemented more efficiently than): + /// ```compile_fail + /// for account_id in self.get_account_ids()? { + /// if self + /// .get_known_ephemeral_addresses(account_id, None)? + /// .into_iter() + /// .any(|(known_addr, _)| &known_addr == address) + /// { + /// return Ok(Some(account_id)); + /// } + /// } + /// Ok(None) + /// ``` + #[cfg(feature = "transparent-inputs")] + fn find_account_for_ephemeral_address( + &self, + address: &TransparentAddress, + ) -> Result, Self::Error> { + for account_id in self.get_account_ids()? { + if self + .get_known_ephemeral_addresses(account_id, None)? + .into_iter() + .any(|(known_addr, _)| &known_addr == address) + { + return Ok(Some(account_id)); + } + } + Ok(None) + } + + /// Returns a vector of [`TransactionDataRequest`] values that describe information needed by + /// the wallet to complete its view of transaction history. + /// + /// Requests for the same transaction data may be returned repeatedly by successive data + /// requests. The caller of this method should consider the latest set of requests returned + /// by this method to be authoritative and to subsume that returned by previous calls. + /// + /// Callers should poll this method on a regular interval, not as part of ordinary chain + /// scanning, which already produces requests for transaction data enhancement. Note that + /// responding to a set of transaction data requests may result in the creation of new + /// transaction data requests, such as when it is necessary to fill in purely-transparent + /// transaction history by walking the chain backwards via transparent inputs. + fn transaction_data_requests(&self) -> Result, Self::Error>; +} + +/// Read-only operations required for testing light wallet functions. +/// +/// These methods expose internal details or unstable interfaces, primarily to enable use +/// of the [`testing`] framework. They should not be used in production software. +#[cfg(any(test, feature = "test-dependencies"))] +#[cfg_attr(feature = "test-dependencies", delegatable_trait)] +pub trait WalletTest: InputSource + WalletRead { + /// Returns a vector of transaction summaries. + /// + /// Currently test-only, as production use could return a very large number of results; either + /// pagination or a streaming design will be necessary to stabilize this feature for production + /// use. + fn get_tx_history( + &self, + ) -> Result< + Vec::AccountId>>, + ::Error, + >; + + /// Returns the note IDs for shielded notes sent by the wallet in a particular + /// transaction. + fn get_sent_note_ids( + &self, + _txid: &TxId, + _protocol: ShieldedProtocol, + ) -> Result, ::Error>; + + /// Returns the outputs for a transaction sent by the wallet. + #[allow(clippy::type_complexity)] + fn get_sent_outputs( + &self, + txid: &TxId, + ) -> Result, ::Error>; + + #[allow(clippy::type_complexity)] + fn get_checkpoint_history( + &self, + protocol: &ShieldedProtocol, + ) -> Result< + Vec<(BlockHeight, Option)>, + ::Error, + >; + + /// Fetches the transparent output corresponding to the provided `outpoint`. + /// Allows selecting unspendable outputs for testing purposes. + /// + /// Returns `Ok(None)` if the UTXO is not known to belong to the wallet or is not + /// spendable as of the chain tip height. + #[cfg(feature = "transparent-inputs")] + fn get_transparent_output( + &self, + outpoint: &OutPoint, + allow_unspendable: bool, + ) -> Result, ::Error>; + + /// Returns all the notes that have been received by the wallet. + fn get_notes( + &self, + protocol: ShieldedProtocol, + ) -> Result>, ::Error>; +} + +/// The output of a transaction sent by the wallet. +/// +/// This type is opaque, and exists for use by tests defined in this crate. +#[cfg(any(test, feature = "test-dependencies"))] +#[allow(dead_code)] +#[derive(Clone, Debug)] +pub struct OutputOfSentTx { + value: Zatoshis, + external_recipient: Option

, + ephemeral_address: Option<(Address, u32)>, +} + +#[cfg(any(test, feature = "test-dependencies"))] +impl OutputOfSentTx { + /// Constructs an output from its test-relevant parts. + /// + /// If the output is to an ephemeral address, `ephemeral_address` should contain the + /// address along with the `address_index` it was derived from under the BIP 32 path + /// `m/44'/'/'/2/`. + pub fn from_parts( + value: Zatoshis, + external_recipient: Option
, + ephemeral_address: Option<(Address, u32)>, + ) -> Self { + Self { + value, + external_recipient, + ephemeral_address, + } + } +} + +/// The relevance of a seed to a given wallet. +/// +/// This is the return type for [`WalletRead::seed_relevance_to_derived_accounts`]. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum SeedRelevance { + /// The seed is relevant to at least one derived account within the wallet. + Relevant { account_ids: NonEmpty }, + /// The seed is not relevant to any of the derived accounts within the wallet. + NotRelevant, + /// The wallet contains no derived accounts. + NoDerivedAccounts, + /// The wallet contains no accounts. + NoAccounts, +} + +/// Metadata describing the sizes of the zcash note commitment trees as of a particular block. +#[derive(Debug, Clone, Copy)] +pub struct BlockMetadata { + block_height: BlockHeight, + block_hash: BlockHash, + sapling_tree_size: Option, + #[cfg(feature = "orchard")] + orchard_tree_size: Option, +} + +impl BlockMetadata { + /// Constructs a new [`BlockMetadata`] value from its constituent parts. + pub fn from_parts( + block_height: BlockHeight, + block_hash: BlockHash, + sapling_tree_size: Option, + #[cfg(feature = "orchard")] orchard_tree_size: Option, + ) -> Self { + Self { + block_height, + block_hash, + sapling_tree_size, + #[cfg(feature = "orchard")] + orchard_tree_size, + } + } + + /// Returns the block height. + pub fn block_height(&self) -> BlockHeight { + self.block_height + } + + /// Returns the hash of the block + pub fn block_hash(&self) -> BlockHash { + self.block_hash + } + + /// Returns the size of the Sapling note commitment tree for the final treestate of the block + /// that this [`BlockMetadata`] describes, if available. + pub fn sapling_tree_size(&self) -> Option { + self.sapling_tree_size + } + + /// Returns the size of the Orchard note commitment tree for the final treestate of the block + /// that this [`BlockMetadata`] describes, if available. + #[cfg(feature = "orchard")] + pub fn orchard_tree_size(&self) -> Option { + self.orchard_tree_size + } +} + +/// The protocol-specific note commitment and nullifier data extracted from the per-transaction +/// shielded bundles in [`CompactBlock`], used by the wallet for note commitment tree maintenance +/// and spend detection. +/// +/// [`CompactBlock`]: crate::proto::compact_formats::CompactBlock +pub struct ScannedBundles { + final_tree_size: u32, + commitments: Vec<(NoteCommitment, Retention)>, + nullifier_map: Vec<(TxId, u16, Vec)>, +} + +impl ScannedBundles { + pub(crate) fn new( + final_tree_size: u32, + commitments: Vec<(NoteCommitment, Retention)>, + nullifier_map: Vec<(TxId, u16, Vec)>, + ) -> Self { + Self { + final_tree_size, + nullifier_map, + commitments, + } + } + + /// Returns the size of the note commitment tree as of the end of the scanned block. + pub fn final_tree_size(&self) -> u32 { + self.final_tree_size + } + + /// Returns the vector of nullifiers for each transaction in the block. + /// + /// The returned tuple is keyed by both transaction ID and the index of the transaction within + /// the block, so that either the txid or the combination of the block hash available from + /// [`ScannedBlock::block_hash`] and returned transaction index may be used to uniquely + /// identify the transaction, depending upon the needs of the caller. + pub fn nullifier_map(&self) -> &[(TxId, u16, Vec)] { + &self.nullifier_map + } + + /// Returns the ordered list of note commitments to be added to the note commitment + /// tree. + pub fn commitments(&self) -> &[(NoteCommitment, Retention)] { + &self.commitments + } +} + +/// A struct used to return the vectors of note commitments for a [`ScannedBlock`] +/// as owned values. +pub struct ScannedBlockCommitments { + /// The ordered vector of note commitments for Sapling outputs of the block. + pub sapling: Vec<(sapling::Node, Retention)>, + /// The ordered vector of note commitments for Orchard outputs of the block. + /// Present only when the `orchard` feature is enabled. + #[cfg(feature = "orchard")] + pub orchard: Vec<(orchard::tree::MerkleHashOrchard, Retention)>, +} + +/// The subset of information that is relevant to this wallet that has been +/// decrypted and extracted from a [`CompactBlock`]. +/// +/// [`CompactBlock`]: crate::proto::compact_formats::CompactBlock +pub struct ScannedBlock { + block_height: BlockHeight, + block_hash: BlockHash, + block_time: u32, + transactions: Vec>, + sapling: ScannedBundles, + #[cfg(feature = "orchard")] + orchard: ScannedBundles, +} + +impl ScannedBlock { + /// Constructs a new `ScannedBlock` + pub(crate) fn from_parts( + block_height: BlockHeight, + block_hash: BlockHash, + block_time: u32, + transactions: Vec>, + sapling: ScannedBundles, + #[cfg(feature = "orchard")] orchard: ScannedBundles< + orchard::tree::MerkleHashOrchard, + orchard::note::Nullifier, + >, + ) -> Self { + Self { + block_height, + block_hash, + block_time, + transactions, + sapling, + #[cfg(feature = "orchard")] + orchard, + } + } + + /// Returns the height of the block that was scanned. + pub fn height(&self) -> BlockHeight { + self.block_height + } + + /// Returns the block hash of the block that was scanned. + pub fn block_hash(&self) -> BlockHash { + self.block_hash + } + + /// Returns the block time of the block that was scanned, as a Unix timestamp in seconds. + pub fn block_time(&self) -> u32 { + self.block_time + } + + /// Returns the list of transactions from this block that are relevant to the wallet. + pub fn transactions(&self) -> &[WalletTx] { + &self.transactions + } + + /// Returns the Sapling note commitment tree and nullifier data for the block. + pub fn sapling(&self) -> &ScannedBundles { + &self.sapling + } + + /// Returns the Orchard note commitment tree and nullifier data for the block. + #[cfg(feature = "orchard")] + pub fn orchard( + &self, + ) -> &ScannedBundles { + &self.orchard + } + + /// Consumes `self` and returns the lists of Sapling and Orchard note commitments associated + /// with the scanned block as an owned value. + pub fn into_commitments(self) -> ScannedBlockCommitments { + ScannedBlockCommitments { + sapling: self.sapling.commitments, + #[cfg(feature = "orchard")] + orchard: self.orchard.commitments, + } + } + + /// Returns the [`BlockMetadata`] corresponding to the scanned block. + pub fn to_block_metadata(&self) -> BlockMetadata { + BlockMetadata { + block_height: self.block_height, + block_hash: self.block_hash, + sapling_tree_size: Some(self.sapling.final_tree_size), + #[cfg(feature = "orchard")] + orchard_tree_size: Some(self.orchard.final_tree_size), + } + } +} + +/// A transaction that was detected during scanning of the blockchain, +/// including its decrypted Sapling and/or Orchard outputs. +/// +/// The purpose of this struct is to permit atomic updates of the +/// wallet database when transactions are successfully decrypted. +pub struct DecryptedTransaction<'a, AccountId> { + mined_height: Option, + tx: &'a Transaction, + sapling_outputs: Vec>, + #[cfg(feature = "orchard")] + orchard_outputs: Vec>, +} + +impl<'a, AccountId> DecryptedTransaction<'a, AccountId> { + /// Constructs a new [`DecryptedTransaction`] from its constituent parts. + pub fn new( + mined_height: Option, + tx: &'a Transaction, + sapling_outputs: Vec>, + #[cfg(feature = "orchard")] orchard_outputs: Vec< + DecryptedOutput, + >, + ) -> Self { + Self { + mined_height, + tx, + sapling_outputs, + #[cfg(feature = "orchard")] + orchard_outputs, + } + } + + /// Returns the height at which the transaction was mined, if known. + pub fn mined_height(&self) -> Option { + self.mined_height + } + /// Returns the raw transaction data. + pub fn tx(&self) -> &Transaction { + self.tx + } + /// Returns the Sapling outputs that were decrypted from the transaction. + pub fn sapling_outputs(&self) -> &[DecryptedOutput] { + &self.sapling_outputs + } + /// Returns the Orchard outputs that were decrypted from the transaction. + #[cfg(feature = "orchard")] + pub fn orchard_outputs(&self) -> &[DecryptedOutput] { + &self.orchard_outputs + } +} + +/// A transaction that was constructed and sent by the wallet. +/// +/// The purpose of this struct is to permit atomic updates of the +/// wallet database when transactions are created and submitted +/// to the network. +pub struct SentTransaction<'a, AccountId> { + tx: &'a Transaction, + created: time::OffsetDateTime, + target_height: BlockHeight, + account: AccountId, + outputs: &'a [SentTransactionOutput], + fee_amount: Zatoshis, + #[cfg(feature = "transparent-inputs")] + utxos_spent: &'a [OutPoint], +} + +impl<'a, AccountId> SentTransaction<'a, AccountId> { + /// Constructs a new [`SentTransaction`] from its constituent parts. + /// + /// ### Parameters + /// - `tx`: the raw transaction data + /// - `created`: the system time at which the transaction was created + /// - `target_height`: the target height that was used in the construction of the transaction + /// - `account`: the account that spent funds in creation of the transaction + /// - `outputs`: the outputs created by the transaction, including those sent to external + /// recipients which may not otherwise be recoverable + /// - `fee_amount`: the fee value paid by the transaction + /// - `utxos_spent`: the UTXOs controlled by the wallet that were spent in this transaction + pub fn new( + tx: &'a Transaction, + created: time::OffsetDateTime, + target_height: BlockHeight, + account: AccountId, + outputs: &'a [SentTransactionOutput], + fee_amount: Zatoshis, + #[cfg(feature = "transparent-inputs")] utxos_spent: &'a [OutPoint], + ) -> Self { + Self { + tx, + created, + target_height, + account, + outputs, + fee_amount, + #[cfg(feature = "transparent-inputs")] + utxos_spent, + } + } + + /// Returns the transaction that was sent. + pub fn tx(&self) -> &Transaction { + self.tx + } + /// Returns the timestamp of the transaction's creation. + pub fn created(&self) -> time::OffsetDateTime { + self.created + } + /// Returns the id for the account that created the outputs. + pub fn account_id(&self) -> &AccountId { + &self.account + } + /// Returns the outputs of the transaction. + pub fn outputs(&self) -> &[SentTransactionOutput] { + self.outputs + } + /// Returns the fee paid by the transaction. + pub fn fee_amount(&self) -> Zatoshis { + self.fee_amount + } + /// Returns the list of UTXOs spent in the created transaction. + #[cfg(feature = "transparent-inputs")] + pub fn utxos_spent(&self) -> &[OutPoint] { + self.utxos_spent + } + + /// Returns the block height that this transaction was created to target. + pub fn target_height(&self) -> BlockHeight { + self.target_height + } +} + +/// An output of a transaction generated by the wallet. +/// +/// This type is capable of representing both shielded and transparent outputs. +pub struct SentTransactionOutput { + output_index: usize, + recipient: Recipient, + value: Zatoshis, + memo: Option, +} + +impl SentTransactionOutput { + /// Constructs a new [`SentTransactionOutput`] from its constituent parts. + /// + /// ### Fields: + /// * `output_index` - the index of the output or action in the sent transaction + /// * `recipient` - the recipient of the output, either a Zcash address or a + /// wallet-internal account and the note belonging to the wallet created by + /// the output + /// * `value` - the value of the output, in zatoshis + /// * `memo` - the memo that was sent with this output + pub fn from_parts( + output_index: usize, + recipient: Recipient, + value: Zatoshis, + memo: Option, + ) -> Self { + Self { + output_index, + recipient, + value, + memo, + } + } + + /// Returns the index within the transaction that contains the recipient output. + /// + /// - If `recipient_address` is a Sapling address, this is an index into the Sapling + /// outputs of the transaction. + /// - If `recipient_address` is a transparent address, this is an index into the + /// transparent outputs of the transaction. + pub fn output_index(&self) -> usize { + self.output_index + } + /// Returns the recipient address of the transaction, or the account id and + /// resulting note/outpoint for wallet-internal outputs. + pub fn recipient(&self) -> &Recipient { + &self.recipient + } + /// Returns the value of the newly created output. + pub fn value(&self) -> Zatoshis { + self.value + } + /// Returns the memo that was attached to the output, if any. This will only be `None` + /// for transparent outputs. + pub fn memo(&self) -> Option<&MemoBytes> { + self.memo.as_ref() + } +} + +/// A data structure used to set the birthday height for an account, and ensure that the initial +/// note commitment tree state is recorded at that height. +#[derive(Clone, Debug)] +pub struct AccountBirthday { + prior_chain_state: ChainState, + recover_until: Option, +} + +/// Errors that can occur in the construction of an [`AccountBirthday`] from a [`TreeState`]. +pub enum BirthdayError { + HeightInvalid(TryFromIntError), + Decode(io::Error), +} + +impl From for BirthdayError { + fn from(value: TryFromIntError) -> Self { + Self::HeightInvalid(value) + } +} + +impl From for BirthdayError { + fn from(value: io::Error) -> Self { + Self::Decode(value) + } +} + +impl AccountBirthday { + /// Constructs a new [`AccountBirthday`] from its constituent parts. + /// + /// * `prior_chain_state`: The chain state prior to the birthday height of the account. The + /// birthday height is defined as the height of the first block to be scanned in wallet + /// recovery. + /// * `recover_until`: An optional height at which the wallet should exit "recovery mode". In + /// order to avoid confusing shifts in wallet balance and spendability that may temporarily be + /// visible to a user during the process of recovering from seed, wallets may optionally set a + /// "recover until" height. The wallet is considered to be in "recovery mode" until there + /// exist no unscanned ranges between the wallet's birthday height and the provided + /// `recover_until` height, exclusive. + /// + /// This API is intended primarily to be used in testing contexts; under normal circumstances, + /// [`AccountBirthday::from_treestate`] should be used instead. + #[cfg(any(test, feature = "test-dependencies"))] + pub fn from_parts(prior_chain_state: ChainState, recover_until: Option) -> Self { + Self { + prior_chain_state, + recover_until, + } + } + + /// Constructs a new [`AccountBirthday`] from a [`TreeState`] returned from `lightwalletd`. + /// + /// * `treestate`: The tree state corresponding to the last block prior to the wallet's + /// birthday height. + /// * `recover_until`: An optional height at which the wallet should exit "recovery mode". In + /// order to avoid confusing shifts in wallet balance and spendability that may temporarily be + /// visible to a user during the process of recovering from seed, wallets may optionally set a + /// "recover until" height. The wallet is considered to be in "recovery mode" until there + /// exist no unscanned ranges between the wallet's birthday height and the provided + /// `recover_until` height, exclusive. + pub fn from_treestate( + treestate: TreeState, + recover_until: Option, + ) -> Result { + Ok(Self { + prior_chain_state: treestate.to_chain_state()?, + recover_until, + }) + } + + /// Returns the Sapling note commitment tree frontier as of the end of the block at + /// [`Self::height`]. + pub fn sapling_frontier( + &self, + ) -> &Frontier { + self.prior_chain_state.final_sapling_tree() + } + + /// Returns the Orchard note commitment tree frontier as of the end of the block at + /// [`Self::height`]. + #[cfg(feature = "orchard")] + pub fn orchard_frontier( + &self, + ) -> &Frontier + { + self.prior_chain_state.final_orchard_tree() + } + + /// Returns the birthday height of the account. + pub fn height(&self) -> BlockHeight { + self.prior_chain_state.block_height() + 1 + } + + /// Returns the height at which the wallet should exit "recovery mode". + pub fn recover_until(&self) -> Option { + self.recover_until + } + + #[cfg(any(test, feature = "test-dependencies"))] + /// Constructs a new [`AccountBirthday`] at the given network upgrade's activation, + /// with no "recover until" height. + /// + /// # Panics + /// + /// Panics if the activation height for the given network upgrade is not set. + pub fn from_activation( + params: &P, + network_upgrade: NetworkUpgrade, + prior_block_hash: BlockHash, + ) -> AccountBirthday { + AccountBirthday::from_parts( + ChainState::empty( + params.activation_height(network_upgrade).unwrap() - 1, + prior_block_hash, + ), + None, + ) + } + + #[cfg(any(test, feature = "test-dependencies"))] + /// Constructs a new [`AccountBirthday`] at Sapling activation, with no + /// "recover until" height. + /// + /// # Panics + /// + /// Panics if the Sapling activation height is not set. + pub fn from_sapling_activation( + params: &P, + prior_block_hash: BlockHash, + ) -> AccountBirthday { + Self::from_activation(params, NetworkUpgrade::Sapling, prior_block_hash) + } +} + +/// This trait encapsulates the write capabilities required to update stored wallet data. +/// +/// # Adding accounts +/// +/// This trait provides several methods for adding accounts to the wallet data: +/// - [`WalletWrite::create_account`] +/// - [`WalletWrite::import_account_hd`] +/// - [`WalletWrite::import_account_ufvk`] +/// +/// All of these methods take an [`AccountBirthday`]. The birthday height is defined as +/// the minimum block height that will be scanned for funds belonging to the wallet. If +/// `birthday.height()` is below the current chain tip, the account addition operation +/// will trigger a re-scan of the blocks at and above the provided height. +/// +/// The order in which you call these methods will affect the resulting wallet structure: +/// - If only [`WalletWrite::create_account`] is used, the resulting accounts will have +/// sequential [ZIP 32] account indices within each given seed. +/// - If [`WalletWrite::import_account_hd`] is used to import accounts with non-sequential +/// ZIP 32 account indices from the same seed, a call to [`WalletWrite::create_account`] +/// will use the ZIP 32 account index just after the highest-numbered existing account. +/// - If an account is added to the wallet, and then a later call to one of the methods +/// would produce a UFVK that collides with that account on any FVK component (i.e. +/// Sapling, Orchard, or transparent), an error will be returned. This can occur in the +/// following cases: +/// - An account is created via [`WalletWrite::create_account`] with an auto-selected +/// ZIP 32 account index, and that index is later imported explicitly via either +/// [`WalletWrite::import_account_ufvk`] or [`WalletWrite::import_account_hd`]. +/// - An account is imported via [`WalletWrite::import_account_ufvk`] or +/// [`WalletWrite::import_account_hd`], and then the ZIP 32 account index +/// corresponding to that account's UFVK is later imported either implicitly +/// via [`WalletWrite::create_account`], or explicitly via a call to +/// [`WalletWrite::import_account_ufvk`] or [`WalletWrite::import_account_hd`]. +/// +/// Note that an error will be returned on an FVK collision even if the UFVKs do not +/// match exactly, e.g. if they have different subsets of components. +/// +/// A future change to this trait might introduce a method to "upgrade" an imported +/// account with derivation information. See [zcash/librustzcash#1284] for details. +/// +/// Users of the `WalletWrite` trait should generally distinguish in their APIs and wallet +/// UIs between creating a new account, and importing an account that previously existed. +/// By convention, wallets should only allow a new account to be generated after confirmed +/// funds have been received by the newest existing account; this allows automated account +/// recovery to discover and recover all funds within a particular seed. +/// +/// # Creating a new wallet +/// +/// To create a new wallet: +/// - Generate a new [BIP 39] mnemonic phrase, using a crate like [`bip0039`]. +/// - Derive the corresponding seed from the mnemonic phrase. +/// - Use [`WalletWrite::create_account`] with the resulting seed. +/// +/// Callers should construct the [`AccountBirthday`] using [`AccountBirthday::from_treestate`] for +/// the block at height `chain_tip_height - 100`. Setting the birthday height to a tree state below +/// the pruning depth ensures that reorgs cannot cause funds intended for the wallet to be missed; +/// otherwise, if the chain tip height were used for the wallet birthday, a transaction targeted at +/// a height greater than the chain tip could be mined at a height below that tip as part of a +/// reorg. +/// +/// # Restoring a wallet from backup +/// +/// To restore a backed-up wallet: +/// - Derive the seed from its BIP 39 mnemonic phrase. +/// - Use [`WalletWrite::import_account_hd`] once for each ZIP 32 account index that the +/// user wants to restore. +/// - If the highest previously-used ZIP 32 account index was _not_ restored by the user, +/// remember this index separately as `index_max`. The first time the user wants to +/// generate a new account, use [`WalletWrite::import_account_hd`] to create the account +/// `index_max + 1`. +/// - [`WalletWrite::create_account`] can be used to generate subsequent new accounts in +/// the restored wallet. +/// +/// Automated account recovery has not yet been implemented by this crate. A wallet app +/// that supports multiple accounts can implement it manually by tracking account balances +/// relative to [`WalletSummary::fully_scanned_height`], and creating new accounts as +/// funds appear in existing accounts. +/// +/// If the number of accounts is known in advance, the wallet should create all accounts before +/// scanning the chain so that the scan can be done in a single pass for all accounts. +/// +/// [ZIP 32]: https://zips.z.cash/zip-0032 +/// [zcash/librustzcash#1284]: https://github.com/zcash/librustzcash/issues/1284 +/// [BIP 39]: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki +/// [`bip0039`]: https://crates.io/crates/bip0039 +#[cfg_attr(feature = "test-dependencies", delegatable_trait)] +pub trait WalletWrite: WalletRead { + /// The type of identifiers used to look up transparent UTXOs. + type UtxoRef; + + /// Tells the wallet to track the next available account-level spend authority, given the + /// current set of [ZIP 316] account identifiers known to the wallet database. + /// + /// The "next available account" is defined as the ZIP-32 account index immediately following + /// the highest existing account index among all accounts in the wallet that share the given + /// seed. Users of the [`WalletWrite`] trait that only call this method are guaranteed to have + /// accounts with sequential indices. + /// + /// Returns the account identifier for the newly-created wallet database entry, along with the + /// associated [`UnifiedSpendingKey`]. Note that the unique account identifier should *not* be + /// assumed equivalent to the ZIP 32 account index. It is an opaque identifier for a pool of + /// funds or set of outputs controlled by a single spending authority. + /// + /// The ZIP-32 account index may be obtained by calling [`WalletRead::get_account`] + /// with the returned account identifier. + /// + /// The [`WalletWrite`] trait documentation has more details about account creation and import. + /// + /// # Arguments + /// - `account_name`: A human-readable name for the account. + /// - `seed`: The 256-byte (at least) HD seed from which to derive the account UFVK. + /// - `birthday`: Metadata about where to start scanning blocks to find transactions intended + /// for the account. + /// - `key_source`: A string identifier or other metadata describing the source of the seed. + /// This is treated as opaque metadata by the wallet backend; it is provided for use by + /// applications which need to track additional identifying information for an account. + /// + /// # Implementation notes + /// + /// Implementations of this method **MUST NOT** "fill in gaps" by selecting an account index + /// that is lower than any existing account index among all accounts in the wallet that share + /// the given seed. + /// + /// # Panics + /// + /// Panics if the length of the seed is not between 32 and 252 bytes inclusive. + /// + /// [ZIP 316]: https://zips.z.cash/zip-0316 + fn create_account( + &mut self, + account_name: &str, + seed: &SecretVec, + birthday: &AccountBirthday, + key_source: Option<&str>, + ) -> Result<(Self::AccountId, UnifiedSpendingKey), Self::Error>; + + /// Tells the wallet to track a specific account index for a given seed. + /// + /// Returns details about the imported account, including the unique account identifier for + /// the newly-created wallet database entry, along with the associated [`UnifiedSpendingKey`]. + /// Note that the unique account identifier should *not* be assumed equivalent to the ZIP 32 + /// account index. It is an opaque identifier for a pool of funds or set of outputs controlled + /// by a single spending authority. + /// + /// Import accounts with indices that are exactly one greater than the highest existing account + /// index to ensure account indices are contiguous, thereby facilitating automated account + /// recovery. + /// + /// The [`WalletWrite`] trait documentation has more details about account creation and import. + /// + /// # Arguments + /// - `account_name`: A human-readable name for the account. + /// - `seed`: The 256-byte (at least) HD seed from which to derive the account UFVK. + /// - `account_index`: The ZIP 32 account-level component of the HD derivation path at + /// which to derive the account's UFVK. + /// - `birthday`: Metadata about where to start scanning blocks to find transactions intended + /// for the account. + /// - `key_source`: A string identifier or other metadata describing the source of the seed. + /// This is treated as opaque metadata by the wallet backend; it is provided for use by + /// applications which need to track additional identifying information for an account. + /// + /// # Panics + /// + /// Panics if the length of the seed is not between 32 and 252 bytes inclusive. + /// + /// [ZIP 316]: https://zips.z.cash/zip-0316 + fn import_account_hd( + &mut self, + account_name: &str, + seed: &SecretVec, + account_index: zip32::AccountId, + birthday: &AccountBirthday, + key_source: Option<&str>, + ) -> Result<(Self::Account, UnifiedSpendingKey), Self::Error>; + + /// Tells the wallet to track an account using a unified full viewing key. + /// + /// Returns details about the imported account, including the unique account identifier for + /// the newly-created wallet database entry. Unlike the other account creation APIs + /// ([`Self::create_account`] and [`Self::import_account_hd`]), no spending key is returned + /// because the wallet has no information about how the UFVK was derived. + /// + /// Certain optimizations are possible for accounts which will never be used to spend funds. If + /// `spending_key_available` is `false`, the wallet may choose to optimize for this case, in + /// which case any attempt to spend funds from the account will result in an error. + /// + /// The [`WalletWrite`] trait documentation has more details about account creation and import. + /// + /// # Arguments + /// - `account_name`: A human-readable name for the account. + /// - `unified_key`: The UFVK used to detect transactions involving the account. + /// - `birthday`: Metadata about where to start scanning blocks to find transactions intended + /// for the account. + /// - `purpose`: Metadata describing whether or not data required for spending should be + /// tracked by the wallet. + /// - `key_source`: A string identifier or other metadata describing the source of the seed. + /// This is treated as opaque metadata by the wallet backend; it is provided for use by + /// applications which need to track additional identifying information for an account. + /// + /// # Panics + /// + /// Panics if the length of the seed is not between 32 and 252 bytes inclusive. + fn import_account_ufvk( + &mut self, + account_name: &str, + unified_key: &UnifiedFullViewingKey, + birthday: &AccountBirthday, + purpose: AccountPurpose, + key_source: Option<&str>, + ) -> Result; + + /// Generates and persists the next available diversified address for the specified account, + /// given the current addresses known to the wallet. If the `request` parameter is `None`, + /// an address should be generated using all of the available receivers for the account's UFVK. + /// + /// Returns `Ok(None)` if the account identifier does not correspond to a known + /// account. + fn get_next_available_address( + &mut self, + account: Self::AccountId, + request: Option, + ) -> Result, Self::Error>; + + /// Updates the wallet's view of the blockchain. + /// + /// This method is used to provide the wallet with information about the state of the + /// blockchain, and detect any previously scanned data that needs to be re-validated + /// before proceeding with scanning. It should be called at wallet startup prior to calling + /// [`WalletRead::suggest_scan_ranges`] in order to provide the wallet with the information it + /// needs to correctly prioritize scanning operations. + fn update_chain_tip(&mut self, tip_height: BlockHeight) -> Result<(), Self::Error>; + + /// Updates the state of the wallet database by persisting the provided block information, + /// along with the note commitments that were detected when scanning the block for transactions + /// pertaining to this wallet. + /// + /// ### Arguments + /// - `from_state` must be the chain state for the block height prior to the first + /// block in `blocks`. + /// - `blocks` must be sequential, in order of increasing block height. + fn put_blocks( + &mut self, + from_state: &ChainState, + blocks: Vec>, + ) -> Result<(), Self::Error>; + + /// Adds a transparent UTXO received by the wallet to the data store. + fn put_received_transparent_utxo( + &mut self, + output: &WalletTransparentOutput, + ) -> Result; + + /// Caches a decrypted transaction in the persistent wallet store. + fn store_decrypted_tx( + &mut self, + received_tx: DecryptedTransaction, + ) -> Result<(), Self::Error>; + + /// Saves information about transactions constructed by the wallet to the persistent + /// wallet store. + /// + /// This must be called before the transactions are sent to the network. + /// + /// Transactions that have been stored by this method should be retransmitted while it + /// is still possible that they could be mined. + fn store_transactions_to_be_sent( + &mut self, + transactions: &[SentTransaction], + ) -> Result<(), Self::Error>; + + /// Truncates the wallet database to at most the specified height. + /// + /// Implementations of this method may choose a lower block height to which the data store will + /// be truncated if it is not possible to truncate exactly to the specified height. Upon + /// successful truncation, this method returns the height to which the data store was actually + /// truncated. + /// + /// This method assumes that the state of the underlying data store is consistent up to a + /// particular block height. Since it is possible that a chain reorg might invalidate some + /// stored state, this method must be implemented in order to allow users of this API to + /// "reset" the data store to correctly represent chainstate as of at most the requested block + /// height. + /// + /// After calling this method, the block at the returned height will be the most recent block + /// and all other operations will treat this block as the chain tip for balance determination + /// purposes. + /// + /// There may be restrictions on heights to which it is possible to truncate. Specifically, it + /// will only be possible to truncate to heights at which is is possible to create a witness + /// given the current state of the wallet's note commitment tree. + fn truncate_to_height(&mut self, max_height: BlockHeight) -> Result; + + /// Reserves the next `n` available ephemeral addresses for the given account. + /// This cannot be undone, so as far as possible, errors associated with transaction + /// construction should have been reported before calling this method. + /// + /// To ensure that sufficient information is stored on-chain to allow recovering + /// funds sent back to any of the used addresses, a "gap limit" of 20 addresses + /// should be observed as described in [BIP 44]. + /// + /// Returns an error if there is insufficient space within the gap limit to allocate + /// the given number of addresses, or if the account identifier does not correspond + /// to a known account. + /// + /// [BIP 44]: https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#user-content-Address_gap_limit + #[cfg(feature = "transparent-inputs")] + fn reserve_next_n_ephemeral_addresses( + &mut self, + _account_id: Self::AccountId, + _n: usize, + ) -> Result, Self::Error> { + // Default impl is required for feature-flagged trait methods to prevent + // breakage due to inadvertent activation of features by transitive dependencies + // of the implementing crate. + Ok(vec![]) + } + + /// Updates the wallet backend with respect to the status of a specific transaction, from the + /// perspective of the main chain. + /// + /// Fully transparent transactions, and transactions that do not contain either shielded inputs + /// or shielded outputs belonging to the wallet, may not be discovered by the process of chain + /// scanning; as a consequence, the wallet must actively query to determine whether such + /// transactions have been mined. + fn set_transaction_status( + &mut self, + _txid: TxId, + _status: TransactionStatus, + ) -> Result<(), Self::Error>; +} + +/// This trait describes a capability for manipulating wallet note commitment trees. +#[cfg_attr(feature = "test-dependencies", delegatable_trait)] +pub trait WalletCommitmentTrees { + type Error: Debug; + + /// The type of the backing [`ShardStore`] for the Sapling note commitment tree. + type SaplingShardStore<'a>: ShardStore< + H = sapling::Node, + CheckpointId = BlockHeight, + Error = Self::Error, + >; + + /// Evaluates the given callback function with a reference to the Sapling + /// note commitment tree maintained by the wallet. + fn with_sapling_tree_mut(&mut self, callback: F) -> Result + where + for<'a> F: FnMut( + &'a mut ShardTree< + Self::SaplingShardStore<'a>, + { sapling::NOTE_COMMITMENT_TREE_DEPTH }, + SAPLING_SHARD_HEIGHT, + >, + ) -> Result, + E: From>; + + /// Adds a sequence of Sapling note commitment tree subtree roots to the data store. + /// + /// Each such value should be the Merkle root of a subtree of the Sapling note commitment tree + /// containing 2^[`SAPLING_SHARD_HEIGHT`] note commitments. + fn put_sapling_subtree_roots( + &mut self, + start_index: u64, + roots: &[CommitmentTreeRoot], + ) -> Result<(), ShardTreeError>; + + /// The type of the backing [`ShardStore`] for the Orchard note commitment tree. + #[cfg(feature = "orchard")] + type OrchardShardStore<'a>: ShardStore< + H = orchard::tree::MerkleHashOrchard, + CheckpointId = BlockHeight, + Error = Self::Error, + >; + + /// Evaluates the given callback function with a reference to the Orchard + /// note commitment tree maintained by the wallet. + #[cfg(feature = "orchard")] + fn with_orchard_tree_mut(&mut self, callback: F) -> Result + where + for<'a> F: FnMut( + &'a mut ShardTree< + Self::OrchardShardStore<'a>, + { ORCHARD_SHARD_HEIGHT * 2 }, + ORCHARD_SHARD_HEIGHT, + >, + ) -> Result, + E: From>; + + /// Adds a sequence of Orchard note commitment tree subtree roots to the data store. + /// + /// Each such value should be the Merkle root of a subtree of the Orchard note commitment tree + /// containing 2^[`ORCHARD_SHARD_HEIGHT`] note commitments. + #[cfg(feature = "orchard")] + fn put_orchard_subtree_roots( + &mut self, + start_index: u64, + roots: &[CommitmentTreeRoot], + ) -> Result<(), ShardTreeError>; } diff --git a/zcash_client_backend/src/data_api/chain.rs b/zcash_client_backend/src/data_api/chain.rs index 38a47007f4..d64392e288 100644 --- a/zcash_client_backend/src/data_api/chain.rs +++ b/zcash_client_backend/src/data_api/chain.rs @@ -4,354 +4,722 @@ //! # Examples //! //! ``` -//! use tempfile::NamedTempFile; +//! # #[cfg(feature = "test-dependencies")] +//! # { //! use zcash_primitives::{ -//! consensus::{BlockHeight, Network, Parameters} +//! consensus::{BlockHeight, Network, Parameters}, //! }; //! //! use zcash_client_backend::{ //! data_api::{ -//! BlockSource, WalletRead, WalletWrite, +//! WalletRead, WalletWrite, WalletCommitmentTrees, //! chain::{ -//! validate_chain, +//! BlockSource, +//! CommitmentTreeRoot, +//! error::Error, //! scan_cached_blocks, +//! testing as chain_testing, //! }, -//! error::Error, +//! scanning::ScanPriority, +//! testing, //! }, //! }; //! -//! use zcash_client_sqlite::{ -//! BlockDb, -//! WalletDb, -//! error::SqliteClientError, -//! wallet::{rewind_to_height}, -//! wallet::init::{init_wallet_db}, -//! }; +//! # use std::convert::Infallible; //! -//! # // doctests have a problem with sqlite IO, so we ignore errors -//! # // generated in this example code as it's not really testing anything //! # fn main() { //! # test(); //! # } //! # -//! # fn test() -> Result<(), SqliteClientError> { +//! # fn test() -> Result<(), Error<(), Infallible>> { //! let network = Network::TestNetwork; -//! let cache_file = NamedTempFile::new()?; -//! let db_cache = BlockDb::for_path(cache_file)?; -//! let db_file = NamedTempFile::new()?; -//! let db_read = WalletDb::for_path(db_file, network)?; -//! init_wallet_db(&db_read)?; +//! let block_source = chain_testing::MockBlockSource; +//! let mut wallet_db = testing::MockWalletDb::new(Network::TestNetwork); +//! +//! // 1) Download note commitment tree data from lightwalletd +//! let roots: Vec> = unimplemented!(); +//! +//! // 2) Pass the commitment tree data to the database. +//! wallet_db.put_sapling_subtree_roots(0, &roots).unwrap(); +//! +//! // 3) Download chain tip metadata from lightwalletd +//! let tip_height: BlockHeight = unimplemented!(); +//! +//! // 4) Notify the wallet of the updated chain tip. +//! wallet_db.update_chain_tip(tip_height).map_err(Error::Wallet)?; +//! +//! // 5) Get the suggested scan ranges from the wallet database +//! let mut scan_ranges = wallet_db.suggest_scan_ranges().map_err(Error::Wallet)?; //! -//! let mut db_data = db_read.get_update_ops()?; +//! // 6) Run the following loop until the wallet's view of the chain tip as of the previous wallet +//! // session is valid. +//! loop { +//! // If there is a range of blocks that needs to be verified, it will always be returned as +//! // the first element of the vector of suggested ranges. +//! match scan_ranges.first() { +//! Some(scan_range) if scan_range.priority() == ScanPriority::Verify => { +//! // Download the chain state for the block prior to the start of the range you want +//! // to scan. +//! let chain_state = unimplemented!("get_chain_state(scan_range.block_range().start - 1)?;"); +//! // Download the blocks in `scan_range` into the block source, overwriting any +//! // existing blocks in this range. +//! unimplemented!("cache_blocks(scan_range)?;"); //! -//! // 1) Download new CompactBlocks into db_cache. +//! // Scan the downloaded blocks +//! let scan_result = scan_cached_blocks( +//! &network, +//! &block_source, +//! &mut wallet_db, +//! scan_range.block_range().start, +//! chain_state, +//! scan_range.len() +//! ); //! -//! // 2) Run the chain validator on the received blocks. -//! // -//! // Given that we assume the server always gives us correct-at-the-time blocks, any -//! // errors are in the blocks we have previously cached or scanned. -//! if let Err(e) = validate_chain(&network, &db_cache, db_data.get_max_height_hash()?) { -//! match e { -//! SqliteClientError::BackendError(Error::InvalidChain(lower_bound, _)) => { -//! // a) Pick a height to rewind to. -//! // -//! // This might be informed by some external chain reorg information, or -//! // heuristics such as the platform, available bandwidth, size of recent -//! // CompactBlocks, etc. -//! let rewind_height = lower_bound - 10; +//! // Check for scanning errors that indicate that the wallet's chain tip is out of +//! // sync with blockchain history. +//! match scan_result { +//! Ok(_) => { +//! // At this point, the cache and scanned data are locally consistent (though +//! // not necessarily consistent with the latest chain tip - this would be +//! // discovered the next time this codepath is executed after new blocks are +//! // received) so we can break out of the loop. +//! break; +//! } +//! Err(Error::Scan(err)) if err.is_continuity_error() => { +//! // Pick a height to rewind to, which must be at least one block before +//! // the height at which the error occurred, but may be an earlier height +//! // determined based on heuristics such as the platform, available bandwidth, +//! // size of recent CompactBlocks, etc. +//! let rewind_height = err.at_height().saturating_sub(10); //! -//! // b) Rewind scanned block information. -//! db_data.rewind_to_height(rewind_height); +//! // Rewind to the chosen height. +//! wallet_db.truncate_to_height(rewind_height).map_err(Error::Wallet)?; //! -//! // c) Delete cached blocks from rewind_height onwards. -//! // -//! // This does imply that assumed-valid blocks will be re-downloaded, but it -//! // is also possible that in the intervening time, a chain reorg has -//! // occurred that orphaned some of those blocks. +//! // Delete cached blocks from rewind_height onwards. +//! // +//! // This does imply that assumed-valid blocks will be re-downloaded, but it +//! // is also possible that in the intervening time, a chain reorg has +//! // occurred that orphaned some of those blocks. +//! unimplemented!(); +//! } +//! Err(other) => { +//! // Handle or return other errors +//! } +//! } //! -//! // d) If there is some separate thread or service downloading -//! // CompactBlocks, tell it to go back and download from rewind_height -//! // onwards. +//! // In case we updated the suggested scan ranges, now re-request. +//! scan_ranges = wallet_db.suggest_scan_ranges().map_err(Error::Wallet)?; //! } -//! e => { -//! // Handle or return other errors. -//! return Err(e); +//! _ => { +//! // Nothing to verify; break out of the loop +//! break; //! } //! } //! } //! -//! // 3) Scan (any remaining) cached blocks. -//! // -//! // At this point, the cache and scanned data are locally consistent (though not -//! // necessarily consistent with the latest chain tip - this would be discovered the -//! // next time this codepath is executed after new blocks are received). -//! scan_cached_blocks(&network, &db_cache, &mut db_data, None) +//! // 7) Loop over the remaining suggested scan ranges, retrieving the requested data and calling +//! // `scan_cached_blocks` on each range. Periodically, or if a continuity error is +//! // encountered, this process should be repeated starting at step (3). +//! let scan_ranges = wallet_db.suggest_scan_ranges().map_err(Error::Wallet)?; +//! for scan_range in scan_ranges { +//! // Download the chain state for the block prior to the start of the range you want +//! // to scan. +//! let chain_state = unimplemented!("get_chain_state(scan_range.block_range().start - 1)?;"); +//! // Download the blocks in `scan_range` into the block source. While in this example this +//! // step is performed in-line, it's fine for the download of scan ranges to be asynchronous +//! // and for the scanner to process the downloaded ranges as they become available in a +//! // separate thread. The scan ranges should also be broken down into smaller chunks as +//! // appropriate, and for ranges with priority `Historic` it can be useful to download and +//! // scan the range in reverse order (to discover more recent unspent notes sooner), or from +//! // the start and end of the range inwards. +//! unimplemented!("cache_blocks(scan_range)?;"); +//! +//! // Scan the downloaded blocks. +//! let scan_result = scan_cached_blocks( +//! &network, +//! &block_source, +//! &mut wallet_db, +//! scan_range.block_range().start, +//! chain_state, +//! scan_range.len() +//! )?; +//! +//! // Handle scan errors, etc. +//! } +//! # Ok(()) +//! # } //! # } //! ``` -use std::fmt::Debug; +use std::ops::Range; -use zcash_primitives::{ - block::BlockHash, - consensus::{self, BlockHeight, NetworkUpgrade}, - merkle_tree::CommitmentTree, - sapling::Nullifier, - zip32::ExtendedFullViewingKey, -}; +use incrementalmerkletree::frontier::Frontier; +use subtle::ConditionallySelectable; +use zcash_primitives::block::BlockHash; +use zcash_protocol::consensus::{self, BlockHeight}; use crate::{ - data_api::{ - error::{ChainInvalid, Error}, - BlockSource, PrunedBlock, WalletWrite, - }, + data_api::{NullifierQuery, WalletWrite}, proto::compact_formats::CompactBlock, - wallet::{AccountId, WalletTx}, - welding_rig::scan_block, + scanning::{scan_block_with_runners, BatchRunners, Nullifiers, ScanningKeys}, }; -/// Checks that the scanned blocks in the data database, when combined with the recent -/// `CompactBlock`s in the cache database, form a valid chain. -/// -/// This function is built on the core assumption that the information provided in the -/// cache database is more likely to be accurate than the previously-scanned information. -/// This follows from the design (and trust) assumption that the `lightwalletd` server -/// provides accurate block information as of the time it was requested. -/// -/// Arguments: -/// - `parameters` Network parameters -/// - `cache` Source of compact blocks -/// - `from_tip` Height & hash of last validated block; if no validation has previously -/// been performed, this will begin scanning from `sapling_activation_height - 1` -/// -/// Returns: -/// - `Ok(())` if the combined chain is valid. -/// - `Err(ErrorKind::InvalidChain(upper_bound, cause))` if the combined chain is invalid. -/// `upper_bound` is the height of the highest invalid block (on the assumption that the -/// highest block in the cache database is correct). -/// - `Err(e)` if there was an error during validation unrelated to chain validity. +#[cfg(feature = "sync")] +use { + super::scanning::ScanPriority, crate::data_api::scanning::ScanRange, async_trait::async_trait, +}; + +pub mod error; +use error::Error; + +use super::WalletRead; + +/// A struct containing metadata about a subtree root of the note commitment tree. /// -/// This function does not mutate either of the databases. -pub fn validate_chain( - parameters: &P, - cache: &C, - validate_from: Option<(BlockHeight, BlockHash)>, -) -> Result<(), E> -where - E: From>, - P: consensus::Parameters, - C: BlockSource, -{ - let sapling_activation_height = parameters - .activation_height(NetworkUpgrade::Sapling) - .ok_or(Error::SaplingNotActive)?; - - // The cache will contain blocks above the `validate_from` height. Validate from that maximum - // height up to the chain tip, returning the hash of the block found in the cache at the - // `validate_from` height, which can then be used to verify chain integrity by comparing - // against the `validate_from` hash. - let from_height = validate_from - .map(|(height, _)| height) - .unwrap_or(sapling_activation_height - 1); - - let mut prev_height = from_height; - let mut prev_hash: Option = validate_from.map(|(_, hash)| hash); - - cache.with_blocks(from_height, None, move |block| { - let current_height = block.height(); - let result = if current_height != prev_height + 1 { - Err(ChainInvalid::block_height_discontinuity( - prev_height + 1, - current_height, - )) - } else { - match prev_hash { - None => Ok(()), - Some(h) if h == block.prev_hash() => Ok(()), - Some(_) => Err(ChainInvalid::prev_hash_mismatch(current_height)), - } - }; +/// This stores the block height at which the leaf that completed the subtree was +/// added, and the root hash of the complete subtree. +#[derive(Debug)] +pub struct CommitmentTreeRoot { + subtree_end_height: BlockHeight, + root_hash: H, +} - prev_height = current_height; - prev_hash = Some(block.hash()); - result.map_err(E::from) - }) +impl CommitmentTreeRoot { + /// Construct a new `CommitmentTreeRoot` from its constituent parts. + /// + /// - `subtree_end_height`: The height of the block containing the note commitment that + /// completed the subtree. + /// - `root_hash`: The Merkle root of the completed subtree. + pub fn from_parts(subtree_end_height: BlockHeight, root_hash: H) -> Self { + Self { + subtree_end_height, + root_hash, + } + } + + /// Returns the block height at which the leaf that completed the subtree was added. + pub fn subtree_end_height(&self) -> BlockHeight { + self.subtree_end_height + } + + /// Returns the root of the complete subtree. + pub fn root_hash(&self) -> &H { + &self.root_hash + } +} + +/// This trait provides sequential access to raw blockchain data via a callback-oriented +/// API. +pub trait BlockSource { + type Error; + + /// Scan the specified `limit` number of blocks from the blockchain, starting at + /// `from_height`, applying the provided callback to each block. If `from_height` + /// is `None` then scanning will begin at the first available block. + /// + /// * `WalletErrT`: the types of errors produced by the wallet operations performed + /// as part of processing each row. + /// * `NoteRefT`: the type of note identifiers in the wallet data store, for use in + /// reporting errors related to specific notes. + fn with_blocks( + &self, + from_height: Option, + limit: Option, + with_block: F, + ) -> Result<(), error::Error> + where + F: FnMut(CompactBlock) -> Result<(), error::Error>; } -#[allow(clippy::needless_doctest_main)] -/// Scans at most `limit` new blocks added to the cache for any transactions received by -/// the tracked accounts. +/// `BlockCache` is a trait that extends `BlockSource` and defines methods for managing +/// a cache of compact blocks. /// -/// This function will return without error after scanning at most `limit` new blocks, to -/// enable the caller to update their UI with scanning progress. Repeatedly calling this -/// function will process sequential ranges of blocks, and is equivalent to calling -/// `scan_cached_blocks` and passing `None` for the optional `limit` value. +/// # Examples /// -/// This function pays attention only to cached blocks with heights greater than the -/// highest scanned block in `data`. Cached blocks with lower heights are not verified -/// against previously-scanned blocks. In particular, this function **assumes** that the -/// caller is handling rollbacks. +/// ``` +/// use async_trait::async_trait; +/// use std::sync::{Arc, Mutex}; +/// use zcash_client_backend::data_api::{ +/// chain::{error, BlockCache, BlockSource}, +/// scanning::{ScanPriority, ScanRange}, +/// }; +/// use zcash_client_backend::proto::compact_formats::CompactBlock; +/// use zcash_primitives::consensus::BlockHeight; /// -/// For brand-new light client databases, this function starts scanning from the Sapling -/// activation height. This height can be fast-forwarded to a more recent block by -/// initializing the client database with a starting block (for example, calling -/// `init_blocks_table` before this function if using `zcash_client_sqlite`). +/// struct ExampleBlockCache { +/// cached_blocks: Arc>>, +/// } /// -/// Scanned blocks are required to be height-sequential. If a block is missing from the -/// cache, an error will be returned with kind [`ChainInvalid::BlockHeightDiscontinuity`]. +/// # impl BlockSource for ExampleBlockCache { +/// # type Error = (); +/// # +/// # fn with_blocks( +/// # &self, +/// # _from_height: Option, +/// # _limit: Option, +/// # _with_block: F, +/// # ) -> Result<(), error::Error> +/// # where +/// # F: FnMut(CompactBlock) -> Result<(), error::Error>, +/// # { +/// # Ok(()) +/// # } +/// # } +/// # +/// #[async_trait] +/// impl BlockCache for ExampleBlockCache { +/// fn get_tip_height(&self, range: Option<&ScanRange>) -> Result, Self::Error> { +/// let cached_blocks = self.cached_blocks.lock().unwrap(); +/// let blocks: Vec<&CompactBlock> = match range { +/// Some(range) => cached_blocks +/// .iter() +/// .filter(|&block| { +/// let block_height = BlockHeight::from_u32(block.height as u32); +/// range.block_range().contains(&block_height) +/// }) +/// .collect(), +/// None => cached_blocks.iter().collect(), +/// }; +/// let highest_block = blocks.iter().max_by_key(|&&block| block.height); +/// Ok(highest_block.map(|&block| BlockHeight::from_u32(block.height as u32))) +/// } /// -/// # Examples +/// async fn read(&self, range: &ScanRange) -> Result, Self::Error> { +/// Ok(self +/// .cached_blocks +/// .lock() +/// .unwrap() +/// .iter() +/// .filter(|block| { +/// let block_height = BlockHeight::from_u32(block.height as u32); +/// range.block_range().contains(&block_height) +/// }) +/// .cloned() +/// .collect()) +/// } /// -/// ``` -/// use tempfile::NamedTempFile; -/// use zcash_primitives::consensus::{ -/// Network, -/// Parameters, -/// }; -/// use zcash_client_backend::{ -/// data_api::chain::scan_cached_blocks, -/// }; -/// use zcash_client_sqlite::{ -/// BlockDb, -/// WalletDb, -/// error::SqliteClientError, -/// wallet::init::init_wallet_db, -/// }; +/// async fn insert(&self, mut compact_blocks: Vec) -> Result<(), Self::Error> { +/// self.cached_blocks +/// .lock() +/// .unwrap() +/// .append(&mut compact_blocks); +/// Ok(()) +/// } /// -/// # // doctests have a problem with sqlite IO, so we ignore errors -/// # // generated in this example code as it's not really testing anything -/// # fn main() { -/// # test(); -/// # } -/// # -/// # fn test() -> Result<(), SqliteClientError> { -/// let cache_file = NamedTempFile::new().unwrap(); -/// let cache = BlockDb::for_path(cache_file).unwrap(); +/// async fn delete(&self, range: ScanRange) -> Result<(), Self::Error> { +/// self.cached_blocks +/// .lock() +/// .unwrap() +/// .retain(|block| !range.block_range().contains(&BlockHeight::from_u32(block.height as u32))); +/// Ok(()) +/// } +/// } +/// +/// // Example usage +/// let rt = tokio::runtime::Runtime::new().unwrap(); +/// let mut block_cache = ExampleBlockCache { +/// cached_blocks: Arc::new(Mutex::new(Vec::new())), +/// }; +/// let range = ScanRange::from_parts( +/// BlockHeight::from_u32(1)..BlockHeight::from_u32(3), +/// ScanPriority::Historic, +/// ); +/// # let extsk = sapling::zip32::ExtendedSpendingKey::master(&[]); +/// # let dfvk = extsk.to_diversifiable_full_viewing_key(); +/// # let compact_block1 = zcash_client_backend::scanning::testing::fake_compact_block( +/// # 1u32.into(), +/// # zcash_primitives::block::BlockHash([0; 32]), +/// # sapling::Nullifier([0; 32]), +/// # &dfvk, +/// # zcash_primitives::transaction::components::amount::NonNegativeAmount::const_from_u64(5), +/// # false, +/// # None, +/// # ); +/// # let compact_block2 = zcash_client_backend::scanning::testing::fake_compact_block( +/// # 2u32.into(), +/// # zcash_primitives::block::BlockHash([0; 32]), +/// # sapling::Nullifier([0; 32]), +/// # &dfvk, +/// # zcash_primitives::transaction::components::amount::NonNegativeAmount::const_from_u64(5), +/// # false, +/// # None, +/// # ); +/// let compact_blocks = vec![compact_block1, compact_block2]; +/// +/// // Insert blocks into the block cache +/// rt.block_on(async { +/// block_cache.insert(compact_blocks.clone()).await.unwrap(); +/// }); +/// assert_eq!(block_cache.cached_blocks.lock().unwrap().len(), 2); +/// +/// // Find highest block in the block cache +/// let get_tip_height = block_cache.get_tip_height(None).unwrap(); +/// assert_eq!(get_tip_height, Some(BlockHeight::from_u32(2))); +/// +/// // Read from the block cache +/// rt.block_on(async { +/// let blocks_from_cache = block_cache.read(&range).await.unwrap(); +/// assert_eq!(blocks_from_cache, compact_blocks); +/// }); /// -/// let data_file = NamedTempFile::new().unwrap(); -/// let db_read = WalletDb::for_path(data_file, Network::TestNetwork)?; -/// init_wallet_db(&db_read)?; +/// // Truncate the block cache +/// rt.block_on(async { +/// block_cache.truncate(BlockHeight::from_u32(1)).await.unwrap(); +/// }); +/// assert_eq!(block_cache.cached_blocks.lock().unwrap().len(), 1); +/// assert_eq!( +/// block_cache.get_tip_height(None).unwrap(), +/// Some(BlockHeight::from_u32(1)) +/// ); /// -/// let mut data = db_read.get_update_ops()?; -/// scan_cached_blocks(&Network::TestNetwork, &cache, &mut data, None)?; -/// # Ok(()) -/// # } +/// // Delete blocks from the block cache +/// rt.block_on(async { +/// block_cache.delete(range).await.unwrap(); +/// }); +/// assert_eq!(block_cache.cached_blocks.lock().unwrap().len(), 0); +/// assert_eq!(block_cache.get_tip_height(None).unwrap(), None); /// ``` -pub fn scan_cached_blocks( - params: &P, - cache: &C, - data: &mut D, - limit: Option, -) -> Result<(), E> +#[cfg(feature = "sync")] +#[async_trait] +pub trait BlockCache: BlockSource + Send + Sync where - P: consensus::Parameters, - C: BlockSource, - D: WalletWrite, - N: Copy + Debug, - E: From>, + Self::Error: Send, { - let sapling_activation_height = params - .activation_height(NetworkUpgrade::Sapling) - .ok_or(Error::SaplingNotActive)?; - - // Recall where we synced up to previously. - // If we have never synced, use sapling activation height to select all cached CompactBlocks. - let mut last_height = data.block_height_extrema().map(|opt| { - opt.map(|(_, max)| max) - .unwrap_or(sapling_activation_height - 1) - })?; + /// Finds the height of the highest block known to the block cache within a specified range. + /// + /// If `range` is `None`, returns the tip of the entire cache. + /// If no blocks are found in the cache, returns Ok(`None`). + fn get_tip_height(&self, range: Option<&ScanRange>) + -> Result, Self::Error>; - // Fetch the ExtendedFullViewingKeys we are tracking - let extfvks = data.get_extended_full_viewing_keys()?; - let extfvks: Vec<(&AccountId, &ExtendedFullViewingKey)> = extfvks.iter().collect(); + /// Retrieves contiguous compact blocks specified by the given `range` from the block cache. + /// + /// Short reads are allowed, meaning that this method may return fewer blocks than requested + /// provided that all returned blocks are contiguous and start from `range.block_range().start`. + /// + /// # Errors + /// + /// This method should return an error if contiguous blocks cannot be read from the cache, + /// indicating there are blocks missing. + async fn read(&self, range: &ScanRange) -> Result, Self::Error>; - // Get the most recent CommitmentTree - let mut tree = data - .get_commitment_tree(last_height) - .map(|t| t.unwrap_or_else(CommitmentTree::empty))?; + /// Inserts a vec of compact blocks into the block cache. + /// + /// This method permits insertion of non-contiguous compact blocks. + async fn insert(&self, compact_blocks: Vec) -> Result<(), Self::Error>; - // Get most recent incremental witnesses for the notes we are tracking - let mut witnesses = data.get_witnesses(last_height)?; + /// Removes all cached blocks above a specified block height. + async fn truncate(&self, block_height: BlockHeight) -> Result<(), Self::Error> { + if let Some(latest) = self.get_tip_height(None)? { + self.delete(ScanRange::from_parts( + Range { + start: block_height + 1, + end: latest + 1, + }, + ScanPriority::Ignored, + )) + .await?; + } + Ok(()) + } - // Get the nullifiers for the notes we are tracking - let mut nullifiers = data.get_nullifiers()?; + /// Deletes a range of compact blocks from the block cache. + /// + /// # Errors + /// + /// In the case of an error, some blocks requested for deletion may remain in the block cache. + async fn delete(&self, range: ScanRange) -> Result<(), Self::Error>; +} - cache.with_blocks(last_height, limit, |block: CompactBlock| { - let current_height = block.height(); +/// Metadata about modifications to the wallet state made in the course of scanning a set of +/// blocks. +#[derive(Clone, Debug)] +pub struct ScanSummary { + pub(crate) scanned_range: Range, + pub(crate) spent_sapling_note_count: usize, + pub(crate) received_sapling_note_count: usize, + #[cfg(feature = "orchard")] + pub(crate) spent_orchard_note_count: usize, + #[cfg(feature = "orchard")] + pub(crate) received_orchard_note_count: usize, +} + +impl ScanSummary { + /// Constructs a new [`ScanSummary`] for the provided block range. + pub(crate) fn for_range(scanned_range: Range) -> Self { + Self { + scanned_range, + spent_sapling_note_count: 0, + received_sapling_note_count: 0, + #[cfg(feature = "orchard")] + spent_orchard_note_count: 0, + #[cfg(feature = "orchard")] + received_orchard_note_count: 0, + } + } + + /// Returns the range of blocks successfully scanned. + pub fn scanned_range(&self) -> Range { + self.scanned_range.clone() + } + + /// Returns the number of our previously-detected Sapling notes that were spent in transactions + /// in blocks in the scanned range. If we have not yet detected a particular note as ours, for + /// example because we are scanning the chain in reverse height order, we will not detect it + /// being spent at this time. + pub fn spent_sapling_note_count(&self) -> usize { + self.spent_sapling_note_count + } + + /// Returns the number of Sapling notes belonging to the wallet that were received in blocks in + /// the scanned range. Note that depending upon the scanning order, it is possible that some of + /// the received notes counted here may already have been spent in later blocks closer to the + /// chain tip. + pub fn received_sapling_note_count(&self) -> usize { + self.received_sapling_note_count + } + + /// Returns the number of our previously-detected Orchard notes that were spent in transactions + /// in blocks in the scanned range. If we have not yet detected a particular note as ours, for + /// example because we are scanning the chain in reverse height order, we will not detect it + /// being spent at this time. + #[cfg(feature = "orchard")] + pub fn spent_orchard_note_count(&self) -> usize { + self.spent_orchard_note_count + } + + /// Returns the number of Orchard notes belonging to the wallet that were received in blocks in + /// the scanned range. Note that depending upon the scanning order, it is possible that some of + /// the received notes counted here may already have been spent in later blocks closer to the + /// chain tip. + #[cfg(feature = "orchard")] + pub fn received_orchard_note_count(&self) -> usize { + self.received_orchard_note_count + } +} + +/// The final note commitment tree state for each shielded pool, as of a particular block height. +#[derive(Debug, Clone)] +pub struct ChainState { + block_height: BlockHeight, + block_hash: BlockHash, + final_sapling_tree: Frontier, + #[cfg(feature = "orchard")] + final_orchard_tree: + Frontier, +} - // Scanned blocks MUST be height-sequential. - if current_height != (last_height + 1) { - return Err( - ChainInvalid::block_height_discontinuity(last_height + 1, current_height).into(), - ); +impl ChainState { + /// Construct a new empty chain state. + pub fn empty(block_height: BlockHeight, block_hash: BlockHash) -> Self { + Self { + block_height, + block_hash, + final_sapling_tree: Frontier::empty(), + #[cfg(feature = "orchard")] + final_orchard_tree: Frontier::empty(), } + } + + /// Construct a new [`ChainState`] from its constituent parts. + pub fn new( + block_height: BlockHeight, + block_hash: BlockHash, + final_sapling_tree: Frontier, + #[cfg(feature = "orchard")] final_orchard_tree: Frontier< + orchard::tree::MerkleHashOrchard, + { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }, + >, + ) -> Self { + Self { + block_height, + block_hash, + final_sapling_tree, + #[cfg(feature = "orchard")] + final_orchard_tree, + } + } + + /// Returns the block height to which this chain state applies. + pub fn block_height(&self) -> BlockHeight { + self.block_height + } + + /// Return the hash of the block. + pub fn block_hash(&self) -> BlockHash { + self.block_hash + } + + /// Returns the frontier of the Sapling note commitment tree as of the end of the block at + /// [`Self::block_height`]. + pub fn final_sapling_tree( + &self, + ) -> &Frontier { + &self.final_sapling_tree + } + + /// Returns the frontier of the Orchard note commitment tree as of the end of the block at + /// [`Self::block_height`]. + #[cfg(feature = "orchard")] + pub fn final_orchard_tree( + &self, + ) -> &Frontier + { + &self.final_orchard_tree + } +} + +/// Scans at most `limit` blocks from the provided block source for in order to find transactions +/// received by the accounts tracked in the provided wallet database. +/// +/// This function will return after scanning at most `limit` new blocks, to enable the caller to +/// update their UI with scanning progress. Repeatedly calling this function with `from_height == +/// None` will process sequential ranges of blocks. +/// +/// ## Panics +/// +/// This method will panic if `from_height != from_state.block_height() + 1`. +#[tracing::instrument(skip(params, block_source, data_db, from_state))] +#[allow(clippy::type_complexity)] +pub fn scan_cached_blocks( + params: &ParamsT, + block_source: &BlockSourceT, + data_db: &mut DbT, + from_height: BlockHeight, + from_state: &ChainState, + limit: usize, +) -> Result> +where + ParamsT: consensus::Parameters + Send + 'static, + BlockSourceT: BlockSource, + DbT: WalletWrite, + ::AccountId: ConditionallySelectable + Default + Send + 'static, +{ + assert_eq!(from_height, from_state.block_height + 1); + + // Fetch the UnifiedFullViewingKeys we are tracking + let account_ufvks = data_db + .get_unified_full_viewing_keys() + .map_err(Error::Wallet)?; + let scanning_keys = ScanningKeys::from_account_ufvks(account_ufvks); + let mut runners = BatchRunners::<_, (), ()>::for_keys(100, &scanning_keys); - let block_hash = BlockHash::from_slice(&block.hash); - let block_time = block.time; + block_source.with_blocks::<_, DbT::Error>(Some(from_height), Some(limit), |block| { + runners.add_block(params, block).map_err(|e| e.into()) + })?; + runners.flush(); + + let mut prior_block_metadata = if from_height > BlockHeight::from(0) { + data_db + .block_metadata(from_height - 1) + .map_err(Error::Wallet)? + } else { + None + }; - let txs: Vec> = { - let mut witness_refs: Vec<_> = witnesses.iter_mut().map(|w| &mut w.1).collect(); + // Get the nullifiers for the unspent notes we are tracking + let mut nullifiers = Nullifiers::new( + data_db + .get_sapling_nullifiers(NullifierQuery::Unspent) + .map_err(Error::Wallet)?, + #[cfg(feature = "orchard")] + data_db + .get_orchard_nullifiers(NullifierQuery::Unspent) + .map_err(Error::Wallet)?, + ); - scan_block( + let mut scanned_blocks = vec![]; + let mut scan_summary = ScanSummary::for_range(from_height..from_height); + block_source.with_blocks::<_, DbT::Error>( + Some(from_height), + Some(limit), + |block: CompactBlock| { + scan_summary.scanned_range.end = block.height() + 1; + let scanned_block = scan_block_with_runners::<_, _, _, (), ()>( params, block, - &extfvks, + &scanning_keys, &nullifiers, - &mut tree, - &mut witness_refs[..], + prior_block_metadata.as_ref(), + Some(&mut runners), ) - }; + .map_err(Error::Scan)?; - // Enforce that all roots match. This is slow, so only include in debug builds. - #[cfg(debug_assertions)] - { - let cur_root = tree.root(); - for row in &witnesses { - if row.1.root() != cur_root { - return Err(Error::InvalidWitnessAnchor(row.0, current_height).into()); + for wtx in &scanned_block.transactions { + scan_summary.spent_sapling_note_count += wtx.sapling_spends().len(); + scan_summary.received_sapling_note_count += wtx.sapling_outputs().len(); + #[cfg(feature = "orchard")] + { + scan_summary.spent_orchard_note_count += wtx.orchard_spends().len(); + scan_summary.received_orchard_note_count += wtx.orchard_outputs().len(); } } - for tx in &txs { - for output in tx.shielded_outputs.iter() { - if output.witness.root() != cur_root { - return Err(Error::InvalidNewWitnessAnchor( - output.index, - tx.txid, - current_height, - output.witness.root(), - ) - .into()); - } - } + + let sapling_spent_nf: Vec<&sapling::Nullifier> = scanned_block + .transactions + .iter() + .flat_map(|tx| tx.sapling_spends().iter().map(|spend| spend.nf())) + .collect(); + nullifiers.retain_sapling(|(_, nf)| !sapling_spent_nf.contains(&nf)); + nullifiers.extend_sapling(scanned_block.transactions.iter().flat_map(|tx| { + tx.sapling_outputs() + .iter() + .flat_map(|out| out.nf().into_iter().map(|nf| (*out.account_id(), *nf))) + })); + + #[cfg(feature = "orchard")] + { + let orchard_spent_nf: Vec<&orchard::note::Nullifier> = scanned_block + .transactions + .iter() + .flat_map(|tx| tx.orchard_spends().iter().map(|spend| spend.nf())) + .collect(); + + nullifiers.retain_orchard(|(_, nf)| !orchard_spent_nf.contains(&nf)); + nullifiers.extend_orchard(scanned_block.transactions.iter().flat_map(|tx| { + tx.orchard_outputs() + .iter() + .flat_map(|out| out.nf().into_iter().map(|nf| (*out.account_id(), *nf))) + })); } - } - let new_witnesses = data.advance_by_block( - &(PrunedBlock { - block_height: current_height, - block_hash, - block_time, - commitment_tree: &tree, - transactions: &txs, - }), - &witnesses, - )?; - - let spent_nf: Vec = txs - .iter() - .flat_map(|tx| tx.shielded_spends.iter().map(|spend| spend.nf)) - .collect(); - nullifiers.retain(|(_, nf)| !spent_nf.contains(nf)); - nullifiers.extend( - txs.iter() - .flat_map(|tx| tx.shielded_outputs.iter().map(|out| (out.account, out.nf))), - ); - - witnesses.extend(new_witnesses); - - last_height = current_height; + prior_block_metadata = Some(scanned_block.to_block_metadata()); + scanned_blocks.push(scanned_block); - Ok(()) - })?; + Ok(()) + }, + )?; + + data_db + .put_blocks(from_state, scanned_blocks) + .map_err(Error::Wallet)?; + Ok(scan_summary) +} + +#[cfg(feature = "test-dependencies")] +pub mod testing { + use std::convert::Infallible; + use zcash_protocol::consensus::BlockHeight; + + use crate::proto::compact_formats::CompactBlock; - Ok(()) + use super::{error::Error, BlockSource}; + + pub struct MockBlockSource; + + impl BlockSource for MockBlockSource { + type Error = Infallible; + + fn with_blocks( + &self, + _from_height: Option, + _limit: Option, + _with_row: F, + ) -> Result<(), Error> + where + F: FnMut(CompactBlock) -> Result<(), Error>, + { + Ok(()) + } + } } diff --git a/zcash_client_backend/src/data_api/chain/error.rs b/zcash_client_backend/src/data_api/chain/error.rs new file mode 100644 index 0000000000..3a21884bc6 --- /dev/null +++ b/zcash_client_backend/src/data_api/chain/error.rs @@ -0,0 +1,66 @@ +//! Types for chain scanning error handling. + +use std::error; +use std::fmt::{self, Debug, Display}; + +use crate::scanning::ScanError; + +/// Errors related to chain validation and scanning. +#[derive(Debug)] +pub enum Error { + /// An error that was produced by wallet operations in the course of scanning the chain. + Wallet(WalletError), + + /// An error that was produced by the underlying block data store in the process of validation + /// or scanning. + BlockSource(BlockSourceError), + + /// A block that was received violated rules related to chain continuity or contained note + /// commitments that could not be reconciled with the note commitment tree(s) maintained by the + /// wallet. + Scan(ScanError), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self { + Error::Wallet(e) => { + write!( + f, + "The underlying datasource produced the following error: {}", + e + ) + } + Error::BlockSource(e) => { + write!( + f, + "The underlying block store produced the following error: {}", + e + ) + } + Error::Scan(e) => { + write!(f, "Scanning produced the following error: {}", e) + } + } + } +} + +impl error::Error for Error +where + WE: Debug + Display + error::Error + 'static, + BE: Debug + Display + error::Error + 'static, +{ + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match &self { + Error::Wallet(e) => Some(e), + Error::BlockSource(e) => Some(e), + _ => None, + } + } +} + +impl From for Error { + fn from(e: ScanError) -> Self { + Error::Scan(e) + } +} diff --git a/zcash_client_backend/src/data_api/error.rs b/zcash_client_backend/src/data_api/error.rs index 1968af8f94..90284563ba 100644 --- a/zcash_client_backend/src/data_api/error.rs +++ b/zcash_client_backend/src/data_api/error.rs @@ -1,124 +1,425 @@ //! Types for wallet error handling. use std::error; -use std::fmt; -use zcash_primitives::{ - consensus::BlockHeight, - sapling::Node, - transaction::{builder, components::amount::Amount, TxId}, +use std::fmt::{self, Debug, Display}; + +use shardtree::error::ShardTreeError; +use zcash_address::ConversionError; +use zcash_keys::address::UnifiedAddress; +use zcash_primitives::transaction::builder; +use zcash_protocol::{ + value::{BalanceError, Zatoshis}, + PoolType, +}; + +use crate::{ + data_api::wallet::input_selection::InputSelectorError, fees::ChangeError, + proposal::ProposalError, wallet::NoteId, }; -use crate::wallet::AccountId; +#[cfg(feature = "transparent-inputs")] +use ::transparent::address::TransparentAddress; +/// Errors that can occur as a consequence of wallet operations. #[derive(Debug)] -pub enum ChainInvalid { - /// The hash of the parent block given by a proposed new chain tip does - /// not match the hash of the current chain tip. - PrevHashMismatch, +pub enum Error +{ + /// An error occurred retrieving data from the underlying data source + DataSource(DataSourceError), - /// The block height field of the proposed new chain tip is not equal - /// to the height of the previous chain tip + 1. This variant stores - /// a copy of the incorrect height value for reporting purposes. - BlockHeightDiscontinuity(BlockHeight), -} + /// An error in computations involving the note commitment trees. + CommitmentTree(ShardTreeError), -#[derive(Debug)] -pub enum Error { - /// Unable to create a new spend because the wallet balance is not sufficient. - InsufficientBalance(Amount, Amount), + /// An error in note selection + NoteSelection(SelectionError), - /// Chain validation detected an error in the block at the specified block height. - InvalidChain(BlockHeight, ChainInvalid), + /// An error in change selection during transaction proposal construction + Change(ChangeError), - /// A provided extsk is not associated with the specified account. - InvalidExtSk(AccountId), + /// An error in transaction proposal construction + Proposal(ProposalError), - /// The root of an output's witness tree in a newly arrived transaction does - /// not correspond to root of the stored commitment tree at the recorded height. - /// - /// The `usize` member of this struct is the index of the shielded output within - /// the transaction where the witness root does not match. - InvalidNewWitnessAnchor(usize, TxId, BlockHeight, Node), + /// The proposal was structurally valid, but tried to do one of these unsupported things: + /// * spend a prior shielded output; + /// * pay to an output pool for which the corresponding feature is not enabled; + /// * pay to a TEX address if the "transparent-inputs" feature is not enabled. + ProposalNotSupported, - /// The root of an output's witness tree in a previously stored transaction - /// does not correspond to root of the current commitment tree. - InvalidWitnessAnchor(NoteId, BlockHeight), + /// No account could be found corresponding to a provided ID. + AccountIdNotRecognized, + + /// No account could be found corresponding to a provided spending key. + KeyNotRecognized, + + /// The given account cannot be used for spending, because it is unable to maintain an + /// accurate balance. + AccountCannotSpend, + + /// Zcash amount computation encountered an overflow or underflow. + BalanceError(BalanceError), + + /// Unable to create a new spend because the wallet balance is not sufficient. + InsufficientFunds { + available: Zatoshis, + required: Zatoshis, + }, /// The wallet must first perform a scan of the blockchain before other /// operations can be performed. ScanRequired, /// An error occurred building a new transaction. - Builder(builder::Error), + Builder(builder::Error), - /// An error occurred decoding a protobuf message. - Protobuf(protobuf::ProtobufError), + /// It is forbidden to provide a memo when constructing a transparent output. + MemoForbidden, - /// The wallet attempted a sapling-only operation at a block - /// height when Sapling was not yet active. - SaplingNotActive, + /// Attempted to send change to an unsupported pool. + /// + /// This is indicative of a programming error; execution of a transaction proposal that + /// presumes support for the specified pool was performed using an application that does not + /// provide such support. + UnsupportedChangeType(PoolType), + + /// Attempted to create a spend to an unsupported Unified Address receiver + NoSupportedReceivers(Box), + + /// A proposed transaction cannot be built because it requires spending an input of + /// a type for which a key required to construct the transaction is not available. + KeyNotAvailable(PoolType), + + /// A note being spent does not correspond to either the internal or external + /// full viewing key for an account. + NoteMismatch(NoteId), + + /// An error occurred parsing the address from a payment request. + Address(ConversionError<&'static str>), + + /// The address associated with a record being inserted was not recognized as + /// belonging to the wallet. + #[cfg(feature = "transparent-inputs")] + AddressNotRecognized(TransparentAddress), + + /// The wallet tried to pay to an ephemeral transparent address as a normal + /// output. + #[cfg(feature = "transparent-inputs")] + PaysEphemeralTransparentAddress(String), + + /// An error occurred while working with PCZTs. + #[cfg(feature = "pczt")] + Pczt(PcztError), } -impl ChainInvalid { - pub fn prev_hash_mismatch(at_height: BlockHeight) -> Error { - Error::InvalidChain(at_height, ChainInvalid::PrevHashMismatch) - } +/// Errors that can occur while working with PCZTs. +#[cfg(feature = "pczt")] +#[derive(Debug)] +pub enum PcztError { + /// An error occurred while building a PCZT. + Build, - pub fn block_height_discontinuity(at_height: BlockHeight, found: BlockHeight) -> Error { - Error::InvalidChain(at_height, ChainInvalid::BlockHeightDiscontinuity(found)) - } + /// An error occurred while finalizing the IO of a PCZT. + IoFinalization(pczt::roles::io_finalizer::Error), + + /// An error occurred while updating the Orchard bundle of a PCZT. + UpdateOrchard(pczt::roles::updater::OrchardError), + + /// An error occurred while updating the Sapling bundle of a PCZT. + UpdateSapling(pczt::roles::updater::SaplingError), + + /// An error occurred while updating the transparent bundle of a PCZT. + UpdateTransparent(pczt::roles::updater::TransparentError), + + /// An error occurred while finalizing the spends of a PCZT. + SpendFinalization(pczt::roles::spend_finalizer::Error), + + /// An error occurred while extracting a transaction from a PCZT. + Extraction(pczt::roles::tx_extractor::Error), + + /// PCZT parsing resulted in an invalid condition. + Invalid(String), } -impl fmt::Display for Error { +impl fmt::Display for Error +where + DE: fmt::Display, + TE: fmt::Display, + SE: fmt::Display, + FE: fmt::Display, + CE: fmt::Display, + N: fmt::Display, +{ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match &self { - Error::InsufficientBalance(have, need) => write!( + use fmt::Write; + + match self { + Error::DataSource(e) => { + write!( + f, + "The underlying datasource produced the following error: {}", + e + ) + } + Error::CommitmentTree(e) => { + write!(f, "An error occurred in querying or updating a note commitment tree: {}", e) + } + Error::NoteSelection(e) => { + write!(f, "Note selection encountered the following error: {}", e) + } + Error::Change(e) => { + write!(f, "Change output generation failed: {}", e) + } + Error::Proposal(e) => { + write!(f, "Input selection attempted to construct an invalid proposal: {}", e) + } + Error::ProposalNotSupported => write!( f, - "Insufficient balance (have {}, need {} including fee)", - i64::from(*have), i64::from(*need) + "The proposal was valid but tried to do something that is not supported \ + (spend shielded outputs of prior transaction steps or use a feature that \ + is not enabled).", ), - Error::InvalidChain(upper_bound, cause) => { - write!(f, "Invalid chain (upper bound: {}): {:?}", u32::from(*upper_bound), cause) + Error::KeyNotRecognized => { + write!( + f, + "Wallet does not contain an account corresponding to the provided spending key" + ) + } + Error::AccountCannotSpend => { + write!( + f, + "The given account cannot be used for spending, because it is unable to maintain an accurate balance.", + ) } - Error::InvalidExtSk(account) => { - write!(f, "Incorrect ExtendedSpendingKey for account {}", account.0) + Error::AccountIdNotRecognized => { + write!( + f, + "Wallet does not contain an account corresponding to the provided ID" + ) } - Error::InvalidNewWitnessAnchor(output, txid, last_height, anchor) => write!( + Error::BalanceError(e) => write!( f, - "New witness for output {} in tx {} has incorrect anchor after scanning block {}: {:?}", - output, txid, last_height, anchor, + "The value lies outside the valid range of Zcash amounts: {:?}.", + e ), - Error::InvalidWitnessAnchor(id_note, last_height) => write!( + Error::InsufficientFunds { available, required } => write!( f, - "Witness for note {} has incorrect anchor after scanning block {}", - id_note, last_height + "Insufficient balance (have {}, need {} including fee)", + u64::from(*available), + u64::from(*required) ), Error::ScanRequired => write!(f, "Must scan blocks first"), - Error::Builder(e) => write!(f, "{:?}", e), - Error::Protobuf(e) => write!(f, "{}", e), - Error::SaplingNotActive => write!(f, "Could not determine Sapling upgrade activation height."), + Error::Builder(e) => write!(f, "An error occurred building the transaction: {}", e), + Error::MemoForbidden => write!(f, "It is not possible to send a memo to a transparent address."), + Error::UnsupportedChangeType(t) => write!(f, "Attempted to send change to an unsupported pool type: {}", t), + Error::NoSupportedReceivers(ua) => write!( + f, + "A recipient's unified address does not contain any receivers to which the wallet can send funds; required one of {}", + ua.receiver_types().iter().enumerate().fold(String::new(), |mut acc, (i, tc)| { + let _ = write!(acc, "{}{:?}", if i > 0 { ", " } else { "" }, tc); + acc + }) + ), + Error::KeyNotAvailable(pool) => write!(f, "A key required for transaction construction was not available for pool type {}", pool), + Error::NoteMismatch(n) => write!(f, "A note being spent ({:?}) does not correspond to either the internal or external full viewing key for the provided spending key.", n), + + Error::Address(e) => { + write!(f, "An error occurred decoding the address from a payment request: {}.", e) + } + #[cfg(feature = "transparent-inputs")] + Error::AddressNotRecognized(_) => { + write!(f, "The specified transparent address was not recognized as belonging to the wallet.") + } + #[cfg(feature = "transparent-inputs")] + Error::PaysEphemeralTransparentAddress(addr) => { + write!(f, "The wallet tried to pay to an ephemeral transparent address as a normal output: {}", addr) + } + #[cfg(feature = "pczt")] + Error::Pczt(e) => write!(f, "PCZT error: {e}"), } } } -impl error::Error for Error { +#[cfg(feature = "pczt")] +impl fmt::Display for PcztError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + PcztError::Build => { + write!( + f, + "Failed to generate the PCZT prior to proving or signing." + ) + } + PcztError::IoFinalization(e) => { + write!(f, "Failed to finalize IO: {:?}.", e) + } + PcztError::UpdateOrchard(e) => { + write!(f, "Failed to updating Orchard PCZT data: {:?}.", e) + } + PcztError::UpdateSapling(e) => { + write!(f, "Failed to updating Sapling PCZT data: {:?}.", e) + } + PcztError::UpdateTransparent(e) => { + write!(f, "Failed to updating transparent PCZT data: {:?}.", e) + } + PcztError::SpendFinalization(e) => { + write!(f, "Failed to finalize the PCZT spends: {:?}.", e) + } + PcztError::Extraction(e) => { + write!(f, "Failed to extract the final transaction: {:?}.", e) + } + PcztError::Invalid(e) => { + write!(f, "PCZT parsing resulted in an invalid condition: {}.", e) + } + } + } +} + +impl error::Error for Error +where + DE: Debug + Display + error::Error + 'static, + TE: Debug + Display + error::Error + 'static, + SE: Debug + Display + error::Error + 'static, + FE: Debug + Display + 'static, + CE: Debug + Display + error::Error + 'static, + N: Debug + Display + 'static, +{ fn source(&self) -> Option<&(dyn error::Error + 'static)> { match &self { + Error::DataSource(e) => Some(e), + Error::CommitmentTree(e) => Some(e), + Error::NoteSelection(e) => Some(e), + Error::Proposal(e) => Some(e), Error::Builder(e) => Some(e), - Error::Protobuf(e) => Some(e), + #[cfg(feature = "pczt")] + Error::Pczt(e) => Some(e), _ => None, } } } -impl From for Error { - fn from(e: builder::Error) -> Self { +#[cfg(feature = "pczt")] +impl error::Error for PcztError {} + +impl From> for Error { + fn from(e: builder::Error) -> Self { Error::Builder(e) } } -impl From for Error { - fn from(e: protobuf::ProtobufError) -> Self { - Error::Protobuf(e) +impl From for Error { + fn from(e: ProposalError) -> Self { + Error::Proposal(e) + } +} + +impl From for Error { + fn from(e: BalanceError) -> Self { + Error::BalanceError(e) + } +} + +impl From> for Error { + fn from(value: ConversionError<&'static str>) -> Self { + Error::Address(value) + } +} + +impl From> + for Error +{ + fn from(e: InputSelectorError) -> Self { + match e { + InputSelectorError::DataSource(e) => Error::DataSource(e), + InputSelectorError::Selection(e) => Error::NoteSelection(e), + InputSelectorError::Change(e) => Error::Change(e), + InputSelectorError::Proposal(e) => Error::Proposal(e), + InputSelectorError::InsufficientFunds { + available, + required, + } => Error::InsufficientFunds { + available, + required, + }, + InputSelectorError::SyncRequired => Error::ScanRequired, + InputSelectorError::Address(e) => Error::Address(e), + } + } +} + +impl From for Error { + fn from(e: sapling::builder::Error) -> Self { + Error::Builder(builder::Error::SaplingBuild(e)) + } +} + +impl From for Error { + fn from(e: ::transparent::builder::Error) -> Self { + Error::Builder(builder::Error::TransparentBuild(e)) + } +} + +impl From> for Error { + fn from(e: ShardTreeError) -> Self { + Error::CommitmentTree(e) + } +} + +#[cfg(feature = "pczt")] +impl From for Error { + fn from(e: PcztError) -> Self { + Error::Pczt(e) + } +} + +#[cfg(feature = "pczt")] +impl From + for Error +{ + fn from(e: pczt::roles::io_finalizer::Error) -> Self { + Error::Pczt(PcztError::IoFinalization(e)) + } +} + +#[cfg(feature = "pczt")] +impl From + for Error +{ + fn from(e: pczt::roles::updater::OrchardError) -> Self { + Error::Pczt(PcztError::UpdateOrchard(e)) + } +} + +#[cfg(feature = "pczt")] +impl From + for Error +{ + fn from(e: pczt::roles::updater::SaplingError) -> Self { + Error::Pczt(PcztError::UpdateSapling(e)) + } +} + +#[cfg(feature = "pczt")] +impl From + for Error +{ + fn from(e: pczt::roles::updater::TransparentError) -> Self { + Error::Pczt(PcztError::UpdateTransparent(e)) + } +} + +#[cfg(feature = "pczt")] +impl From + for Error +{ + fn from(e: pczt::roles::spend_finalizer::Error) -> Self { + Error::Pczt(PcztError::SpendFinalization(e)) + } +} + +#[cfg(feature = "pczt")] +impl From + for Error +{ + fn from(e: pczt::roles::tx_extractor::Error) -> Self { + Error::Pczt(PcztError::Extraction(e)) } } diff --git a/zcash_client_backend/src/data_api/scanning.rs b/zcash_client_backend/src/data_api/scanning.rs new file mode 100644 index 0000000000..0ab42f2df8 --- /dev/null +++ b/zcash_client_backend/src/data_api/scanning.rs @@ -0,0 +1,193 @@ +//! Common types used for managing a queue of scanning ranges. + +use std::fmt; +use std::ops::Range; + +use zcash_protocol::consensus::BlockHeight; + +#[cfg(feature = "unstable-spanning-tree")] +pub mod spanning_tree; + +/// Scanning range priority levels. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum ScanPriority { + /// Block ranges that are ignored have lowest priority. + Ignored, + /// Block ranges that have already been scanned will not be re-scanned. + Scanned, + /// Block ranges to be scanned to advance the fully-scanned height. + Historic, + /// Block ranges adjacent to heights at which the user opened the wallet. + OpenAdjacent, + /// Blocks that must be scanned to complete note commitment tree shards adjacent to found notes. + FoundNote, + /// Blocks that must be scanned to complete the latest note commitment tree shard. + ChainTip, + /// A previously scanned range that must be verified to check it is still in the + /// main chain, has highest priority. + Verify, +} + +/// A range of blocks to be scanned, along with its associated priority. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ScanRange { + block_range: Range, + priority: ScanPriority, +} + +impl fmt::Display for ScanRange { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{:?}({}..{})", + self.priority, self.block_range.start, self.block_range.end, + ) + } +} + +impl ScanRange { + /// Constructs a scan range from its constituent parts. + pub fn from_parts(block_range: Range, priority: ScanPriority) -> Self { + assert!( + block_range.end >= block_range.start, + "{:?} is invalid for ScanRange({:?})", + block_range, + priority, + ); + ScanRange { + block_range, + priority, + } + } + + /// Returns the range of block heights to be scanned. + pub fn block_range(&self) -> &Range { + &self.block_range + } + + /// Returns the priority with which the scan range should be scanned. + pub fn priority(&self) -> ScanPriority { + self.priority + } + + /// Returns whether or not the scan range is empty. + pub fn is_empty(&self) -> bool { + self.block_range.is_empty() + } + + /// Returns the number of blocks in the scan range. + pub fn len(&self) -> usize { + usize::try_from(u32::from(self.block_range.end) - u32::from(self.block_range.start)) + .unwrap() + } + + /// Shifts the start of the block range to the right if `block_height > + /// self.block_range().start`. Returns `None` if the resulting range would + /// be empty (or the range was already empty). + pub fn truncate_start(&self, block_height: BlockHeight) -> Option { + if block_height >= self.block_range.end || self.is_empty() { + None + } else { + Some(ScanRange { + block_range: self.block_range.start.max(block_height)..self.block_range.end, + priority: self.priority, + }) + } + } + + /// Shifts the end of the block range to the left if `block_height < + /// self.block_range().end`. Returns `None` if the resulting range would + /// be empty (or the range was already empty). + pub fn truncate_end(&self, block_height: BlockHeight) -> Option { + if block_height <= self.block_range.start || self.is_empty() { + None + } else { + Some(ScanRange { + block_range: self.block_range.start..self.block_range.end.min(block_height), + priority: self.priority, + }) + } + } + + /// Splits this scan range at the specified height, such that the provided height becomes the + /// end of the first range returned and the start of the second. Returns `None` if + /// `p <= self.block_range().start || p >= self.block_range().end`. + pub fn split_at(&self, p: BlockHeight) -> Option<(Self, Self)> { + (p > self.block_range.start && p < self.block_range.end).then_some(( + ScanRange { + block_range: self.block_range.start..p, + priority: self.priority, + }, + ScanRange { + block_range: p..self.block_range.end, + priority: self.priority, + }, + )) + } +} + +#[cfg(test)] +mod tests { + use super::{ScanPriority, ScanRange}; + + fn scan_range(start: u32, end: u32) -> ScanRange { + ScanRange::from_parts((start.into())..(end.into()), ScanPriority::Scanned) + } + + #[test] + fn truncate_start() { + let r = scan_range(5, 8); + + assert_eq!(r.truncate_start(4.into()), Some(scan_range(5, 8))); + assert_eq!(r.truncate_start(5.into()), Some(scan_range(5, 8))); + assert_eq!(r.truncate_start(6.into()), Some(scan_range(6, 8))); + assert_eq!(r.truncate_start(7.into()), Some(scan_range(7, 8))); + assert_eq!(r.truncate_start(8.into()), None); + assert_eq!(r.truncate_start(9.into()), None); + + let empty = scan_range(5, 5); + assert_eq!(empty.truncate_start(4.into()), None); + assert_eq!(empty.truncate_start(5.into()), None); + assert_eq!(empty.truncate_start(6.into()), None); + } + + #[test] + fn truncate_end() { + let r = scan_range(5, 8); + + assert_eq!(r.truncate_end(9.into()), Some(scan_range(5, 8))); + assert_eq!(r.truncate_end(8.into()), Some(scan_range(5, 8))); + assert_eq!(r.truncate_end(7.into()), Some(scan_range(5, 7))); + assert_eq!(r.truncate_end(6.into()), Some(scan_range(5, 6))); + assert_eq!(r.truncate_end(5.into()), None); + assert_eq!(r.truncate_end(4.into()), None); + + let empty = scan_range(5, 5); + assert_eq!(empty.truncate_end(4.into()), None); + assert_eq!(empty.truncate_end(5.into()), None); + assert_eq!(empty.truncate_end(6.into()), None); + } + + #[test] + fn split_at() { + let r = scan_range(5, 8); + + assert_eq!(r.split_at(4.into()), None); + assert_eq!(r.split_at(5.into()), None); + assert_eq!( + r.split_at(6.into()), + Some((scan_range(5, 6), scan_range(6, 8))) + ); + assert_eq!( + r.split_at(7.into()), + Some((scan_range(5, 7), scan_range(7, 8))) + ); + assert_eq!(r.split_at(8.into()), None); + assert_eq!(r.split_at(9.into()), None); + + let empty = scan_range(5, 5); + assert_eq!(empty.split_at(4.into()), None); + assert_eq!(empty.split_at(5.into()), None); + assert_eq!(empty.split_at(6.into()), None); + } +} diff --git a/zcash_client_backend/src/data_api/scanning/spanning_tree.rs b/zcash_client_backend/src/data_api/scanning/spanning_tree.rs new file mode 100644 index 0000000000..a0dd5c826f --- /dev/null +++ b/zcash_client_backend/src/data_api/scanning/spanning_tree.rs @@ -0,0 +1,811 @@ +use std::cmp::{max, Ordering}; +use std::ops::{Not, Range}; + +use zcash_protocol::consensus::BlockHeight; + +use super::{ScanPriority, ScanRange}; + +#[derive(Debug, Clone, Copy)] +enum InsertOn { + Left, + Right, +} + +struct Insert { + on: InsertOn, + force_rescan: bool, +} + +impl Insert { + fn left(force_rescan: bool) -> Self { + Insert { + on: InsertOn::Left, + force_rescan, + } + } + + fn right(force_rescan: bool) -> Self { + Insert { + on: InsertOn::Right, + force_rescan, + } + } +} + +impl Not for Insert { + type Output = Self; + + fn not(self) -> Self::Output { + Insert { + on: match self.on { + InsertOn::Left => InsertOn::Right, + InsertOn::Right => InsertOn::Left, + }, + force_rescan: self.force_rescan, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Dominance { + Left, + Right, + Equal, +} + +impl From for Dominance { + fn from(value: Insert) -> Self { + match value.on { + InsertOn::Left => Dominance::Left, + InsertOn::Right => Dominance::Right, + } + } +} + +// This implements the dominance rule for range priority. If the inserted range's priority is +// `Verify`, this replaces any existing priority. Otherwise, if the current priority is +// `Scanned`, it remains as `Scanned`; and if the new priority is `Scanned`, it +// overrides any existing priority. +fn dominance(current: &ScanPriority, inserted: &ScanPriority, insert: Insert) -> Dominance { + match (current.cmp(inserted), (current, inserted)) { + (Ordering::Equal, _) => Dominance::Equal, + (_, (_, ScanPriority::Verify | ScanPriority::Scanned)) => Dominance::from(insert), + (_, (ScanPriority::Scanned, _)) if !insert.force_rescan => Dominance::from(!insert), + (Ordering::Less, _) => Dominance::from(insert), + (Ordering::Greater, _) => Dominance::from(!insert), + } +} + +/// In the comments for each alternative, `()` represents the left range and `[]` represents the right range. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum RangeOrdering { + /// `( ) [ ]` + LeftFirstDisjoint, + /// `( [ ) ]` + LeftFirstOverlap, + /// `[ ( ) ]` + LeftContained, + /// ```text + /// ( ) + /// [ ] + /// ``` + Equal, + /// `( [ ] )` + RightContained, + /// `[ ( ] )` + RightFirstOverlap, + /// `[ ] ( )` + RightFirstDisjoint, +} + +impl RangeOrdering { + fn cmp(a: &Range, b: &Range) -> Self { + use Ordering::*; + assert!(a.start <= a.end && b.start <= b.end); + match (a.start.cmp(&b.start), a.end.cmp(&b.end)) { + _ if a.end <= b.start => RangeOrdering::LeftFirstDisjoint, + _ if b.end <= a.start => RangeOrdering::RightFirstDisjoint, + (Less, Less) => RangeOrdering::LeftFirstOverlap, + (Equal, Less) | (Greater, Less) | (Greater, Equal) => RangeOrdering::LeftContained, + (Equal, Equal) => RangeOrdering::Equal, + (Equal, Greater) | (Less, Greater) | (Less, Equal) => RangeOrdering::RightContained, + (Greater, Greater) => RangeOrdering::RightFirstOverlap, + } + } +} + +#[derive(Debug, PartialEq, Eq)] +enum Joined { + One(ScanRange), + Two(ScanRange, ScanRange), + Three(ScanRange, ScanRange, ScanRange), +} + +fn join_nonoverlapping(left: ScanRange, right: ScanRange) -> Joined { + assert!(left.block_range().end <= right.block_range().start); + + if left.block_range().end == right.block_range().start { + if left.priority() == right.priority() { + Joined::One(ScanRange::from_parts( + left.block_range().start..right.block_range().end, + left.priority(), + )) + } else { + Joined::Two(left, right) + } + } else { + // there is a gap that will need to be filled + let gap = ScanRange::from_parts( + left.block_range().end..right.block_range().start, + ScanPriority::Historic, + ); + + match join_nonoverlapping(left, gap) { + Joined::One(merged) => join_nonoverlapping(merged, right), + Joined::Two(left, gap) => match join_nonoverlapping(gap, right) { + Joined::One(merged) => Joined::Two(left, merged), + Joined::Two(gap, right) => Joined::Three(left, gap, right), + _ => unreachable!(), + }, + _ => unreachable!(), + } + } +} + +fn insert(current: ScanRange, to_insert: ScanRange, force_rescans: bool) -> Joined { + fn join_overlapping(left: ScanRange, right: ScanRange, insert: Insert) -> Joined { + assert!( + left.block_range().start <= right.block_range().start + && left.block_range().end > right.block_range().start + ); + + // recompute the range dominance based upon the queue entry priorities + let dominance = match insert.on { + InsertOn::Left => dominance(&right.priority(), &left.priority(), insert), + InsertOn::Right => dominance(&left.priority(), &right.priority(), insert), + }; + + match dominance { + Dominance::Left => { + if let Some(right) = right.truncate_start(left.block_range().end) { + Joined::Two(left, right) + } else { + Joined::One(left) + } + } + Dominance::Equal => Joined::One(ScanRange::from_parts( + left.block_range().start..max(left.block_range().end, right.block_range().end), + left.priority(), + )), + Dominance::Right => match ( + left.truncate_end(right.block_range().start), + left.truncate_start(right.block_range().end), + ) { + (Some(before), Some(after)) => Joined::Three(before, right, after), + (Some(before), None) => Joined::Two(before, right), + (None, Some(after)) => Joined::Two(right, after), + (None, None) => Joined::One(right), + }, + } + } + + use RangeOrdering::*; + match RangeOrdering::cmp(to_insert.block_range(), current.block_range()) { + LeftFirstDisjoint => join_nonoverlapping(to_insert, current), + LeftFirstOverlap | RightContained => { + join_overlapping(to_insert, current, Insert::left(force_rescans)) + } + Equal => Joined::One(ScanRange::from_parts( + to_insert.block_range().clone(), + match dominance( + ¤t.priority(), + &to_insert.priority(), + Insert::right(force_rescans), + ) { + Dominance::Left | Dominance::Equal => current.priority(), + Dominance::Right => to_insert.priority(), + }, + )), + RightFirstOverlap | LeftContained => { + join_overlapping(current, to_insert, Insert::right(force_rescans)) + } + RightFirstDisjoint => join_nonoverlapping(current, to_insert), + } +} + +#[derive(Debug, Clone)] +#[cfg(feature = "unstable-spanning-tree")] +pub enum SpanningTree { + Leaf(ScanRange), + Parent { + span: Range, + left: Box, + right: Box, + }, +} + +#[cfg(feature = "unstable-spanning-tree")] +impl SpanningTree { + fn span(&self) -> Range { + match self { + SpanningTree::Leaf(entry) => entry.block_range().clone(), + SpanningTree::Parent { span, .. } => span.clone(), + } + } + + fn from_joined(joined: Joined) -> Self { + match joined { + Joined::One(entry) => SpanningTree::Leaf(entry), + Joined::Two(left, right) => SpanningTree::Parent { + span: left.block_range().start..right.block_range().end, + left: Box::new(SpanningTree::Leaf(left)), + right: Box::new(SpanningTree::Leaf(right)), + }, + Joined::Three(left, mid, right) => SpanningTree::Parent { + span: left.block_range().start..right.block_range().end, + left: Box::new(SpanningTree::Leaf(left)), + right: Box::new(SpanningTree::Parent { + span: mid.block_range().start..right.block_range().end, + left: Box::new(SpanningTree::Leaf(mid)), + right: Box::new(SpanningTree::Leaf(right)), + }), + }, + } + } + + fn from_insert( + left: Box, + right: Box, + to_insert: ScanRange, + insert: Insert, + ) -> Self { + let (left, right) = match insert.on { + InsertOn::Left => (Box::new(left.insert(to_insert, insert.force_rescan)), right), + InsertOn::Right => (left, Box::new(right.insert(to_insert, insert.force_rescan))), + }; + SpanningTree::Parent { + span: left.span().start..right.span().end, + left, + right, + } + } + + fn from_split( + left: Self, + right: Self, + to_insert: ScanRange, + split_point: BlockHeight, + force_rescans: bool, + ) -> Self { + let (l_insert, r_insert) = to_insert + .split_at(split_point) + .expect("Split point is within the range of to_insert"); + let left = Box::new(left.insert(l_insert, force_rescans)); + let right = Box::new(right.insert(r_insert, force_rescans)); + SpanningTree::Parent { + span: left.span().start..right.span().end, + left, + right, + } + } + + pub fn insert(self, to_insert: ScanRange, force_rescans: bool) -> Self { + match self { + SpanningTree::Leaf(cur) => Self::from_joined(insert(cur, to_insert, force_rescans)), + SpanningTree::Parent { span, left, right } => { + // This algorithm always preserves the existing partition point, and does not do + // any rebalancing or unification of ranges within the tree. This should be okay + // because `into_vec` performs such unification, and the tree being unbalanced + // should be fine given the relatively small number of ranges we should ordinarily + // be concerned with. + use RangeOrdering::*; + match RangeOrdering::cmp(&span, to_insert.block_range()) { + LeftFirstDisjoint => { + // extend the right-hand branch + Self::from_insert(left, right, to_insert, Insert::right(force_rescans)) + } + LeftFirstOverlap => { + let split_point = left.span().end; + if split_point > to_insert.block_range().start { + Self::from_split(*left, *right, to_insert, split_point, force_rescans) + } else { + // to_insert is fully contained in or equals the right child + Self::from_insert(left, right, to_insert, Insert::right(force_rescans)) + } + } + RightContained => { + // to_insert is fully contained within the current span, so we will insert + // into one or both sides + let split_point = left.span().end; + if to_insert.block_range().start >= split_point { + // to_insert is fully contained in the right + Self::from_insert(left, right, to_insert, Insert::right(force_rescans)) + } else if to_insert.block_range().end <= split_point { + // to_insert is fully contained in the left + Self::from_insert(left, right, to_insert, Insert::left(force_rescans)) + } else { + // to_insert must be split. + Self::from_split(*left, *right, to_insert, split_point, force_rescans) + } + } + Equal => { + let split_point = left.span().end; + if split_point > to_insert.block_range().start { + Self::from_split(*left, *right, to_insert, split_point, force_rescans) + } else { + // to_insert is fully contained in the right subtree + right.insert(to_insert, force_rescans) + } + } + LeftContained => { + // the current span is fully contained within to_insert, so we will extend + // or overwrite both sides + let split_point = left.span().end; + Self::from_split(*left, *right, to_insert, split_point, force_rescans) + } + RightFirstOverlap => { + let split_point = left.span().end; + if split_point < to_insert.block_range().end { + Self::from_split(*left, *right, to_insert, split_point, force_rescans) + } else { + // to_insert is fully contained in or equals the left child + Self::from_insert(left, right, to_insert, Insert::left(force_rescans)) + } + } + RightFirstDisjoint => { + // extend the left-hand branch + Self::from_insert(left, right, to_insert, Insert::left(force_rescans)) + } + } + } + } + } + + pub fn into_vec(self) -> Vec { + fn go(acc: &mut Vec, tree: SpanningTree) { + match tree { + SpanningTree::Leaf(entry) => { + if !entry.is_empty() { + if let Some(top) = acc.pop() { + match join_nonoverlapping(top, entry) { + Joined::One(merged) => acc.push(merged), + Joined::Two(l, r) => { + acc.push(l); + acc.push(r); + } + _ => unreachable!(), + } + } else { + acc.push(entry); + } + } + } + SpanningTree::Parent { left, right, .. } => { + go(acc, *left); + go(acc, *right); + } + } + } + + let mut acc = vec![]; + go(&mut acc, self); + acc + } +} + +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing { + use std::ops::Range; + + use zcash_protocol::consensus::BlockHeight; + + use crate::data_api::scanning::{ScanPriority, ScanRange}; + + pub fn scan_range(range: Range, priority: ScanPriority) -> ScanRange { + ScanRange::from_parts( + BlockHeight::from(range.start)..BlockHeight::from(range.end), + priority, + ) + } +} + +#[cfg(test)] +mod tests { + use std::ops::Range; + + use zcash_protocol::consensus::BlockHeight; + + use super::{join_nonoverlapping, testing::scan_range, Joined, RangeOrdering, SpanningTree}; + use crate::data_api::scanning::{ScanPriority, ScanRange}; + + #[test] + fn test_join_nonoverlapping() { + fn test_range(left: ScanRange, right: ScanRange, expected_joined: Joined) { + let joined = join_nonoverlapping(left, right); + + assert_eq!(joined, expected_joined); + } + + macro_rules! range { + ( $start:expr, $end:expr; $priority:ident ) => { + ScanRange::from_parts( + BlockHeight::from($start)..BlockHeight::from($end), + ScanPriority::$priority, + ) + }; + } + + macro_rules! joined { + ( + ($a_start:expr, $a_end:expr; $a_priority:ident) + ) => { + Joined::One( + range!($a_start, $a_end; $a_priority) + ) + }; + ( + ($a_start:expr, $a_end:expr; $a_priority:ident), + ($b_start:expr, $b_end:expr; $b_priority:ident) + ) => { + Joined::Two( + range!($a_start, $a_end; $a_priority), + range!($b_start, $b_end; $b_priority) + ) + }; + ( + ($a_start:expr, $a_end:expr; $a_priority:ident), + ($b_start:expr, $b_end:expr; $b_priority:ident), + ($c_start:expr, $c_end:expr; $c_priority:ident) + + ) => { + Joined::Three( + range!($a_start, $a_end; $a_priority), + range!($b_start, $b_end; $b_priority), + range!($c_start, $c_end; $c_priority) + ) + }; + } + + // Scan ranges have the same priority and + // line up. + test_range( + range!(1, 9; OpenAdjacent), + range!(9, 15; OpenAdjacent), + joined!( + (1, 15; OpenAdjacent) + ), + ); + + // Scan ranges have different priorities, + // so we cannot merge them even though they + // line up. + test_range( + range!(1, 9; OpenAdjacent), + range!(9, 15; ChainTip), + joined!( + (1, 9; OpenAdjacent), + (9, 15; ChainTip) + ), + ); + + // Scan ranges have the same priority but + // do not line up. + test_range( + range!(1, 9; OpenAdjacent), + range!(13, 15; OpenAdjacent), + joined!( + (1, 9; OpenAdjacent), + (9, 13; Historic), + (13, 15; OpenAdjacent) + ), + ); + + test_range( + range!(1, 9; Historic), + range!(13, 15; OpenAdjacent), + joined!( + (1, 13; Historic), + (13, 15; OpenAdjacent) + ), + ); + + test_range( + range!(1, 9; OpenAdjacent), + range!(13, 15; Historic), + joined!( + (1, 9; OpenAdjacent), + (9, 15; Historic) + ), + ); + } + + #[test] + fn range_ordering() { + use super::RangeOrdering::*; + // Equal + assert_eq!(RangeOrdering::cmp(&(0..1), &(0..1)), Equal); + + // Disjoint or contiguous + assert_eq!(RangeOrdering::cmp(&(0..1), &(1..2)), LeftFirstDisjoint); + assert_eq!(RangeOrdering::cmp(&(1..2), &(0..1)), RightFirstDisjoint); + assert_eq!(RangeOrdering::cmp(&(0..1), &(2..3)), LeftFirstDisjoint); + assert_eq!(RangeOrdering::cmp(&(2..3), &(0..1)), RightFirstDisjoint); + assert_eq!(RangeOrdering::cmp(&(1..2), &(2..2)), LeftFirstDisjoint); + assert_eq!(RangeOrdering::cmp(&(2..2), &(1..2)), RightFirstDisjoint); + assert_eq!(RangeOrdering::cmp(&(1..1), &(1..2)), LeftFirstDisjoint); + assert_eq!(RangeOrdering::cmp(&(1..2), &(1..1)), RightFirstDisjoint); + + // Contained + assert_eq!(RangeOrdering::cmp(&(1..2), &(0..3)), LeftContained); + assert_eq!(RangeOrdering::cmp(&(0..3), &(1..2)), RightContained); + assert_eq!(RangeOrdering::cmp(&(0..1), &(0..3)), LeftContained); + assert_eq!(RangeOrdering::cmp(&(0..3), &(0..1)), RightContained); + assert_eq!(RangeOrdering::cmp(&(2..3), &(0..3)), LeftContained); + assert_eq!(RangeOrdering::cmp(&(0..3), &(2..3)), RightContained); + + // Overlap + assert_eq!(RangeOrdering::cmp(&(0..2), &(1..3)), LeftFirstOverlap); + assert_eq!(RangeOrdering::cmp(&(1..3), &(0..2)), RightFirstOverlap); + } + + fn spanning_tree(to_insert: &[(Range, ScanPriority)]) -> Option { + to_insert.iter().fold(None, |acc, (range, priority)| { + let scan_range = scan_range(range.clone(), *priority); + match acc { + None => Some(SpanningTree::Leaf(scan_range)), + Some(t) => Some(t.insert(scan_range, false)), + } + }) + } + + #[test] + fn spanning_tree_insert_adjacent() { + use ScanPriority::*; + + let t = spanning_tree(&[ + (0..3, Historic), + (3..6, Scanned), + (6..8, ChainTip), + (8..10, ChainTip), + ]) + .unwrap(); + + assert_eq!( + t.into_vec(), + vec![ + scan_range(0..3, Historic), + scan_range(3..6, Scanned), + scan_range(6..10, ChainTip), + ] + ); + } + + #[test] + fn spanning_tree_insert_overlaps() { + use ScanPriority::*; + + let t = spanning_tree(&[ + (0..3, Historic), + (2..5, Scanned), + (6..8, ChainTip), + (7..10, Scanned), + ]) + .unwrap(); + + assert_eq!( + t.into_vec(), + vec![ + scan_range(0..2, Historic), + scan_range(2..5, Scanned), + scan_range(5..6, Historic), + scan_range(6..7, ChainTip), + scan_range(7..10, Scanned), + ] + ); + } + + #[test] + fn spanning_tree_insert_empty() { + use ScanPriority::*; + + let t = spanning_tree(&[ + (0..3, Historic), + (3..6, Scanned), + (6..6, FoundNote), + (6..8, Scanned), + (8..10, ChainTip), + ]) + .unwrap(); + + assert_eq!( + t.into_vec(), + vec![ + scan_range(0..3, Historic), + scan_range(3..8, Scanned), + scan_range(8..10, ChainTip), + ] + ); + } + + #[test] + fn spanning_tree_insert_gaps() { + use ScanPriority::*; + + let t = spanning_tree(&[(0..3, Historic), (6..8, ChainTip)]).unwrap(); + + assert_eq!( + t.into_vec(), + vec![scan_range(0..6, Historic), scan_range(6..8, ChainTip),] + ); + + let t = spanning_tree(&[(0..3, Historic), (3..4, Verify), (6..8, ChainTip)]).unwrap(); + + assert_eq!( + t.into_vec(), + vec![ + scan_range(0..3, Historic), + scan_range(3..4, Verify), + scan_range(4..6, Historic), + scan_range(6..8, ChainTip), + ] + ); + } + + #[test] + fn spanning_tree_insert_rfd_span() { + use ScanPriority::*; + + // This sequence of insertions causes a RightFirstDisjoint on the last insertion, + // which originally had a bug that caused the parent's span to only cover its left + // child. The bug was otherwise unobservable as the insertion logic was able to + // heal this specific kind of bug. + let t = spanning_tree(&[ + // 6..8 + (6..8, Scanned), + // 6..12 + // 6..8 8..12 + // 8..10 10..12 + (10..12, ChainTip), + // 3..12 + // 3..8 8..12 + // 3..6 6..8 8..10 10..12 + (3..6, Historic), + ]) + .unwrap(); + + assert_eq!(t.span(), (3.into())..(12.into())); + assert_eq!( + t.into_vec(), + vec![ + scan_range(3..6, Historic), + scan_range(6..8, Scanned), + scan_range(8..10, Historic), + scan_range(10..12, ChainTip), + ] + ); + } + + #[test] + fn spanning_tree_dominance() { + use ScanPriority::*; + + let t = spanning_tree(&[(0..3, Verify), (2..8, Scanned), (6..10, Verify)]).unwrap(); + assert_eq!( + t.into_vec(), + vec![ + scan_range(0..2, Verify), + scan_range(2..6, Scanned), + scan_range(6..10, Verify), + ] + ); + + let t = spanning_tree(&[(0..3, Verify), (2..8, Historic), (6..10, Verify)]).unwrap(); + assert_eq!( + t.into_vec(), + vec![ + scan_range(0..3, Verify), + scan_range(3..6, Historic), + scan_range(6..10, Verify), + ] + ); + + let t = spanning_tree(&[(0..3, Scanned), (2..8, Verify), (6..10, Scanned)]).unwrap(); + assert_eq!( + t.into_vec(), + vec![ + scan_range(0..2, Scanned), + scan_range(2..6, Verify), + scan_range(6..10, Scanned), + ] + ); + + let t = spanning_tree(&[(0..3, Scanned), (2..8, Historic), (6..10, Scanned)]).unwrap(); + assert_eq!( + t.into_vec(), + vec![ + scan_range(0..3, Scanned), + scan_range(3..6, Historic), + scan_range(6..10, Scanned), + ] + ); + + // a `ChainTip` insertion should not overwrite a scanned range. + let mut t = spanning_tree(&[(0..3, ChainTip), (3..5, Scanned), (5..7, ChainTip)]).unwrap(); + t = t.insert(scan_range(0..7, ChainTip), false); + assert_eq!( + t.into_vec(), + vec![ + scan_range(0..3, ChainTip), + scan_range(3..5, Scanned), + scan_range(5..7, ChainTip), + ] + ); + + let mut t = + spanning_tree(&[(280300..280310, FoundNote), (280310..280320, Scanned)]).unwrap(); + assert_eq!( + t.clone().into_vec(), + vec![ + scan_range(280300..280310, FoundNote), + scan_range(280310..280320, Scanned) + ] + ); + t = t.insert(scan_range(280300..280340, ChainTip), false); + assert_eq!( + t.into_vec(), + vec![ + scan_range(280300..280310, ChainTip), + scan_range(280310..280320, Scanned), + scan_range(280320..280340, ChainTip) + ] + ); + } + + #[test] + fn spanning_tree_insert_coalesce_scanned() { + use ScanPriority::*; + + let mut t = spanning_tree(&[ + (0..3, Historic), + (2..5, Scanned), + (6..8, ChainTip), + (7..10, Scanned), + ]) + .unwrap(); + + t = t.insert(scan_range(0..3, Scanned), false); + t = t.insert(scan_range(5..8, Scanned), false); + + assert_eq!(t.into_vec(), vec![scan_range(0..10, Scanned)]); + } + + #[test] + fn spanning_tree_force_rescans() { + use ScanPriority::*; + + let mut t = spanning_tree(&[ + (0..3, Historic), + (3..5, Scanned), + (5..7, ChainTip), + (7..10, Scanned), + ]) + .unwrap(); + + t = t.insert(scan_range(4..9, OpenAdjacent), true); + + let expected = vec![ + scan_range(0..3, Historic), + scan_range(3..4, Scanned), + scan_range(4..5, OpenAdjacent), + scan_range(5..7, ChainTip), + scan_range(7..9, OpenAdjacent), + scan_range(9..10, Scanned), + ]; + assert_eq!(t.clone().into_vec(), expected); + + // An insert of an ignored range should not override a scanned range; the existing + // priority should prevail, and so the expected state of the tree is unchanged. + t = t.insert(scan_range(2..5, Ignored), true); + assert_eq!(t.into_vec(), expected); + } +} diff --git a/zcash_client_backend/src/data_api/testing.rs b/zcash_client_backend/src/data_api/testing.rs new file mode 100644 index 0000000000..ebad21a116 --- /dev/null +++ b/zcash_client_backend/src/data_api/testing.rs @@ -0,0 +1,2827 @@ +//! Utilities for testing wallets based upon the [`crate::data_api`] traits. + +use std::{ + collections::{BTreeMap, HashMap}, + convert::Infallible, + fmt, + hash::Hash, + num::NonZeroU32, +}; + +use assert_matches::assert_matches; +use group::ff::Field; +use incrementalmerkletree::{Marking, Retention}; +use nonempty::NonEmpty; +use rand::{CryptoRng, Rng, RngCore, SeedableRng}; +use rand_chacha::ChaChaRng; +use secrecy::{ExposeSecret, Secret, SecretVec}; +use shardtree::{error::ShardTreeError, store::memory::MemoryShardStore, ShardTree}; +use subtle::ConditionallySelectable; + +use ::sapling::{ + note_encryption::{sapling_note_encryption, SaplingDomain}, + util::generate_random_rseed, + zip32::DiversifiableFullViewingKey, +}; +use zcash_address::ZcashAddress; +use zcash_keys::{ + address::{Address, UnifiedAddress}, + keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey}, +}; +use zcash_note_encryption::Domain; +use zcash_primitives::{ + block::BlockHash, + transaction::{components::sapling::zip212_enforcement, fees::FeeRule, Transaction, TxId}, +}; +use zcash_proofs::prover::LocalTxProver; +use zcash_protocol::{ + consensus::{self, BlockHeight, Network, NetworkUpgrade, Parameters as _}, + local_consensus::LocalNetwork, + memo::{Memo, MemoBytes}, + value::{ZatBalance, Zatoshis}, + ShieldedProtocol, +}; +use zip32::{fingerprint::SeedFingerprint, DiversifierIndex}; +use zip321::Payment; + +use super::{ + chain::{scan_cached_blocks, BlockSource, ChainState, CommitmentTreeRoot, ScanSummary}, + error::Error, + scanning::ScanRange, + wallet::{ + create_proposed_transactions, + input_selection::{GreedyInputSelector, InputSelector}, + propose_standard_transfer_to_address, propose_transfer, + }, + Account, AccountBalance, AccountBirthday, AccountMeta, AccountPurpose, AccountSource, + BlockMetadata, DecryptedTransaction, InputSource, NoteFilter, NullifierQuery, ScannedBlock, + SeedRelevance, SentTransaction, SpendableNotes, TransactionDataRequest, TransactionStatus, + WalletCommitmentTrees, WalletRead, WalletSummary, WalletTest, WalletWrite, + SAPLING_SHARD_HEIGHT, +}; +use crate::{ + fees::{ + standard::{self, SingleOutputChangeStrategy}, + ChangeStrategy, DustOutputPolicy, StandardFeeRule, + }, + proposal::Proposal, + proto::compact_formats::{ + self, CompactBlock, CompactSaplingOutput, CompactSaplingSpend, CompactTx, + }, + wallet::{Note, NoteId, OvkPolicy, ReceivedNote, WalletTransparentOutput}, +}; + +#[cfg(feature = "transparent-inputs")] +use { + super::wallet::input_selection::ShieldingSelector, + crate::wallet::TransparentAddressMetadata, + ::transparent::{address::TransparentAddress, keys::NonHardenedChildIndex}, + std::ops::Range, +}; + +#[cfg(feature = "orchard")] +use { + super::ORCHARD_SHARD_HEIGHT, crate::proto::compact_formats::CompactOrchardAction, + ::orchard::tree::MerkleHashOrchard, group::ff::PrimeField, pasta_curves::pallas, +}; + +#[cfg(feature = "orchard")] +pub mod orchard; +pub mod pool; +pub mod sapling; +#[cfg(feature = "transparent-inputs")] +pub mod transparent; + +/// Information about a transaction that the wallet is interested in. +pub struct TransactionSummary { + account_id: AccountId, + txid: TxId, + expiry_height: Option, + mined_height: Option, + account_value_delta: ZatBalance, + fee_paid: Option, + spent_note_count: usize, + has_change: bool, + sent_note_count: usize, + received_note_count: usize, + memo_count: usize, + expired_unmined: bool, + is_shielding: bool, +} + +impl TransactionSummary { + /// Constructs a `TransactionSummary` from its parts. + /// + /// See the documentation for each getter method below to determine how each method + /// argument should be prepared. + #[allow(clippy::too_many_arguments)] + pub fn from_parts( + account_id: AccountId, + txid: TxId, + expiry_height: Option, + mined_height: Option, + account_value_delta: ZatBalance, + fee_paid: Option, + spent_note_count: usize, + has_change: bool, + sent_note_count: usize, + received_note_count: usize, + memo_count: usize, + expired_unmined: bool, + is_shielding: bool, + ) -> Self { + Self { + account_id, + txid, + expiry_height, + mined_height, + account_value_delta, + fee_paid, + spent_note_count, + has_change, + sent_note_count, + received_note_count, + memo_count, + expired_unmined, + is_shielding, + } + } + + /// Returns the wallet-internal ID for the account that this transaction was received + /// by or sent from. + pub fn account_id(&self) -> &AccountId { + &self.account_id + } + + /// Returns the transaction's ID. + pub fn txid(&self) -> TxId { + self.txid + } + + /// Returns the expiry height of the transaction, if known. + /// + /// - `None` means that the expiry height is unknown. + /// - `Some(0)` means that the transaction does not expire. + pub fn expiry_height(&self) -> Option { + self.expiry_height + } + + /// Returns the height of the mined block containing this transaction, or `None` if + /// the wallet has not yet observed the transaction to be mined. + pub fn mined_height(&self) -> Option { + self.mined_height + } + + /// Returns the net change in balance that this transaction caused to the account. + /// + /// For example, an account-internal transaction (such as a shielding operation) would + /// show `-fee_paid` as the account value delta. + pub fn account_value_delta(&self) -> ZatBalance { + self.account_value_delta + } + + /// Returns the fee paid by this transaction, if known. + pub fn fee_paid(&self) -> Option { + self.fee_paid + } + + /// Returns the number of notes spent by the account in this transaction. + pub fn spent_note_count(&self) -> usize { + self.spent_note_count + } + + /// Returns `true` if the account received a change note as part of this transaction. + /// + /// This implies that the transaction was (at least in part) sent from the account. + pub fn has_change(&self) -> bool { + self.has_change + } + + /// Returns the number of notes created in this transaction that were sent to a + /// wallet-external address. + pub fn sent_note_count(&self) -> usize { + self.sent_note_count + } + + /// Returns the number of notes created in this transaction that were received by the + /// account. + pub fn received_note_count(&self) -> usize { + self.received_note_count + } + + /// Returns `true` if, from the wallet's current view of the chain, this transaction + /// expired before it was mined. + pub fn expired_unmined(&self) -> bool { + self.expired_unmined + } + + /// Returns the number of non-empty memos viewable by the account in this transaction. + pub fn memo_count(&self) -> usize { + self.memo_count + } + + /// Returns `true` if this is detectably a shielding transaction. + /// + /// Specifically, `true` means that at a minimum: + /// - All of the wallet-spent and wallet-received notes are consistent with a + /// shielding transaction. + /// - The transaction contains at least one wallet-spent output. + /// - The transaction contains at least one wallet-received note. + /// - We do not know about any external outputs of the transaction. + /// + /// There may be some shielding transactions for which this method returns `false`, + /// due to them not being detectable by the wallet as shielding transactions under the + /// above metrics. + pub fn is_shielding(&self) -> bool { + self.is_shielding + } +} + +/// Metadata about a block generated by [`TestState`]. +#[derive(Clone, Debug)] +pub struct CachedBlock { + chain_state: ChainState, + sapling_end_size: u32, + orchard_end_size: u32, +} + +impl CachedBlock { + /// Produces metadata for a block "before shielded time", when the Sapling and Orchard + /// trees were (by definition) empty. + /// + /// `block_height` must be a height before Sapling activation (and therefore also + /// before NU5 activation). + pub fn none(block_height: BlockHeight) -> Self { + Self { + chain_state: ChainState::empty(block_height, BlockHash([0; 32])), + sapling_end_size: 0, + orchard_end_size: 0, + } + } + + /// Produces metadata for a block as of the given chain state. + pub fn at(chain_state: ChainState, sapling_end_size: u32, orchard_end_size: u32) -> Self { + assert_eq!( + chain_state.final_sapling_tree().tree_size() as u32, + sapling_end_size + ); + #[cfg(feature = "orchard")] + assert_eq!( + chain_state.final_orchard_tree().tree_size() as u32, + orchard_end_size + ); + + Self { + chain_state, + sapling_end_size, + orchard_end_size, + } + } + + fn roll_forward(&self, cb: &CompactBlock) -> Self { + assert_eq!(self.chain_state.block_height() + 1, cb.height()); + + let sapling_final_tree = cb.vtx.iter().flat_map(|tx| tx.outputs.iter()).fold( + self.chain_state.final_sapling_tree().clone(), + |mut acc, c_out| { + acc.append(::sapling::Node::from_cmu(&c_out.cmu().unwrap())); + acc + }, + ); + let sapling_end_size = sapling_final_tree.tree_size() as u32; + + #[cfg(feature = "orchard")] + let orchard_final_tree = cb.vtx.iter().flat_map(|tx| tx.actions.iter()).fold( + self.chain_state.final_orchard_tree().clone(), + |mut acc, c_act| { + acc.append(MerkleHashOrchard::from_cmx(&c_act.cmx().unwrap())); + acc + }, + ); + #[cfg(feature = "orchard")] + let orchard_end_size = orchard_final_tree.tree_size() as u32; + #[cfg(not(feature = "orchard"))] + let orchard_end_size = cb.vtx.iter().fold(self.orchard_end_size, |sz, tx| { + sz + (tx.actions.len() as u32) + }); + + Self { + chain_state: ChainState::new( + cb.height(), + cb.hash(), + sapling_final_tree, + #[cfg(feature = "orchard")] + orchard_final_tree, + ), + sapling_end_size, + orchard_end_size, + } + } + + /// Returns the height of this block. + pub fn height(&self) -> BlockHeight { + self.chain_state.block_height() + } + + /// Returns the size of the Sapling note commitment tree as of the end of this block. + pub fn sapling_end_size(&self) -> u32 { + self.sapling_end_size + } + + /// Returns the size of the Orchard note commitment tree as of the end of this block. + pub fn orchard_end_size(&self) -> u32 { + self.orchard_end_size + } +} + +/// The test account configured for a [`TestState`]. +/// +/// Create this by calling either [`TestBuilder::with_account_from_sapling_activation`] or +/// [`TestBuilder::with_account_having_current_birthday`] while setting up a test, and +/// then access it with [`TestState::test_account`]. +#[derive(Clone)] +pub struct TestAccount { + account: A, + usk: UnifiedSpendingKey, + birthday: AccountBirthday, +} + +impl TestAccount { + /// Returns the underlying wallet account. + pub fn account(&self) -> &A { + &self.account + } + + /// Returns the account's unified spending key. + pub fn usk(&self) -> &UnifiedSpendingKey { + &self.usk + } + + /// Returns the birthday that was configured for the account. + pub fn birthday(&self) -> &AccountBirthday { + &self.birthday + } +} + +impl Account for TestAccount { + type AccountId = A::AccountId; + + fn id(&self) -> Self::AccountId { + self.account.id() + } + + fn name(&self) -> Option<&str> { + self.account.name() + } + + fn source(&self) -> &AccountSource { + self.account.source() + } + + fn ufvk(&self) -> Option<&zcash_keys::keys::UnifiedFullViewingKey> { + self.account.ufvk() + } + + fn uivk(&self) -> zcash_keys::keys::UnifiedIncomingViewingKey { + self.account.uivk() + } +} + +/// Trait method exposing the ability to reset the wallet within a test. +// TODO: Does this need to exist separately from DataStoreFactory? +pub trait Reset: WalletTest + Sized { + /// A handle that confers ownership of a specific wallet instance. + type Handle; + + /// Replaces the wallet in `st` (via [`TestState::wallet_mut`]) with a new wallet + /// database. + /// + /// This does not recreate accounts. The resulting wallet in `st` has no test account. + /// + /// Returns the old wallet. + fn reset(st: &mut TestState) -> Self::Handle; +} + +/// The state for a `zcash_client_backend` test. +pub struct TestState { + cache: Cache, + cached_blocks: BTreeMap, + latest_block_height: Option, + wallet_data: DataStore, + network: Network, + test_account: Option<(SecretVec, TestAccount)>, + rng: ChaChaRng, +} + +impl TestState { + /// Exposes an immutable reference to the test's `DataStore`. + pub fn wallet(&self) -> &DataStore { + &self.wallet_data + } + + /// Exposes a mutable reference to the test's `DataStore`. + pub fn wallet_mut(&mut self) -> &mut DataStore { + &mut self.wallet_data + } + + /// Exposes the test framework's source of randomness. + pub fn rng_mut(&mut self) -> &mut ChaChaRng { + &mut self.rng + } + + /// Exposes the network in use. + pub fn network(&self) -> &Network { + &self.network + } +} + +impl + TestState +{ + /// Convenience method for obtaining the Sapling activation height for the network under test. + pub fn sapling_activation_height(&self) -> BlockHeight { + self.network + .activation_height(NetworkUpgrade::Sapling) + .expect("Sapling activation height must be known.") + } + + /// Convenience method for obtaining the NU5 activation height for the network under test. + #[allow(dead_code)] + pub fn nu5_activation_height(&self) -> BlockHeight { + self.network + .activation_height(NetworkUpgrade::Nu5) + .expect("NU5 activation height must be known.") + } + + /// Exposes the seed for the test wallet. + pub fn test_seed(&self) -> Option<&SecretVec> { + self.test_account.as_ref().map(|(seed, _)| seed) + } + + /// Returns a reference to the test account, if one was configured. + pub fn test_account(&self) -> Option<&TestAccount<::Account>> { + self.test_account.as_ref().map(|(_, acct)| acct) + } + + /// Returns the test account's Sapling DFVK, if one was configured. + pub fn test_account_sapling(&self) -> Option<&DiversifiableFullViewingKey> { + let (_, acct) = self.test_account.as_ref()?; + let ufvk = acct.ufvk()?; + ufvk.sapling() + } + + /// Returns the test account's Orchard FVK, if one was configured. + #[cfg(feature = "orchard")] + pub fn test_account_orchard(&self) -> Option<&::orchard::keys::FullViewingKey> { + let (_, acct) = self.test_account.as_ref()?; + let ufvk = acct.ufvk()?; + ufvk.orchard() + } +} + +impl TestState +where + Network: consensus::Parameters, + DataStore: WalletTest + WalletWrite, + ::Error: fmt::Debug, +{ + /// Exposes an immutable reference to the test's [`BlockSource`]. + #[cfg(feature = "unstable")] + pub fn cache(&self) -> &Cache::BlockSource { + self.cache.block_source() + } + + /// Returns the cached chain state corresponding to the latest block generated by this + /// `TestState`. + pub fn latest_cached_block(&self) -> Option<&CachedBlock> { + self.latest_block_height + .as_ref() + .and_then(|h| self.cached_blocks.get(h)) + } + + fn latest_cached_block_below_height(&self, height: BlockHeight) -> Option<&CachedBlock> { + self.cached_blocks.range(..height).last().map(|(_, b)| b) + } + + fn cache_block( + &mut self, + prev_block: &CachedBlock, + compact_block: CompactBlock, + ) -> Cache::InsertResult { + self.cached_blocks.insert( + compact_block.height(), + prev_block.roll_forward(&compact_block), + ); + self.cache.insert(&compact_block) + } + + /// Creates a fake block at the expected next height containing a single output of the + /// given value, and inserts it into the cache. + pub fn generate_next_block( + &mut self, + fvk: &Fvk, + address_type: AddressType, + value: Zatoshis, + ) -> (BlockHeight, Cache::InsertResult, Fvk::Nullifier) { + let pre_activation_block = CachedBlock::none(self.sapling_activation_height() - 1); + let prior_cached_block = self.latest_cached_block().unwrap_or(&pre_activation_block); + let height = prior_cached_block.height() + 1; + + let (res, nfs) = self.generate_block_at( + height, + prior_cached_block.chain_state.block_hash(), + &[FakeCompactOutput::new(fvk, address_type, value)], + prior_cached_block.sapling_end_size, + prior_cached_block.orchard_end_size, + false, + ); + + (height, res, nfs[0]) + } + + /// Creates a fake block at the expected next height containing multiple outputs + /// and inserts it into the cache. + #[allow(dead_code)] + pub fn generate_next_block_multi( + &mut self, + outputs: &[FakeCompactOutput], + ) -> (BlockHeight, Cache::InsertResult, Vec) { + let pre_activation_block = CachedBlock::none(self.sapling_activation_height() - 1); + let prior_cached_block = self.latest_cached_block().unwrap_or(&pre_activation_block); + let height = prior_cached_block.height() + 1; + + let (res, nfs) = self.generate_block_at( + height, + prior_cached_block.chain_state.block_hash(), + outputs, + prior_cached_block.sapling_end_size, + prior_cached_block.orchard_end_size, + false, + ); + + (height, res, nfs) + } + + /// Adds an empty block to the cache, advancing the simulated chain height. + #[allow(dead_code)] // used only for tests that are flagged off by default + pub fn generate_empty_block(&mut self) -> (BlockHeight, Cache::InsertResult) { + let new_hash = { + let mut hash = vec![0; 32]; + self.rng.fill_bytes(&mut hash); + hash + }; + + let pre_activation_block = CachedBlock::none(self.sapling_activation_height() - 1); + let prior_cached_block = self + .latest_cached_block() + .unwrap_or(&pre_activation_block) + .clone(); + let new_height = prior_cached_block.height() + 1; + + let mut cb = CompactBlock { + hash: new_hash, + height: new_height.into(), + ..Default::default() + }; + cb.prev_hash + .extend_from_slice(&prior_cached_block.chain_state.block_hash().0); + + cb.chain_metadata = Some(compact_formats::ChainMetadata { + sapling_commitment_tree_size: prior_cached_block.sapling_end_size, + orchard_commitment_tree_size: prior_cached_block.orchard_end_size, + }); + + let res = self.cache_block(&prior_cached_block, cb); + self.latest_block_height = Some(new_height); + + (new_height, res) + } + + /// Creates a fake block with the given height and hash containing the requested outputs, and + /// inserts it into the cache. + /// + /// This generated block will be treated as the latest block, and subsequent calls to + /// [`Self::generate_next_block`] will build on it. + #[allow(clippy::too_many_arguments)] + pub fn generate_block_at( + &mut self, + height: BlockHeight, + prev_hash: BlockHash, + outputs: &[FakeCompactOutput], + initial_sapling_tree_size: u32, + initial_orchard_tree_size: u32, + allow_broken_hash_chain: bool, + ) -> (Cache::InsertResult, Vec) { + let mut prior_cached_block = self + .latest_cached_block_below_height(height) + .cloned() + .unwrap_or_else(|| CachedBlock::none(self.sapling_activation_height() - 1)); + assert!(prior_cached_block.chain_state.block_height() < height); + assert!(prior_cached_block.sapling_end_size <= initial_sapling_tree_size); + assert!(prior_cached_block.orchard_end_size <= initial_orchard_tree_size); + + // If the block height has increased or the Sapling and/or Orchard tree sizes have changed, + // we need to generate a new prior cached block that the block to be generated can + // successfully chain from, with the provided tree sizes. + if prior_cached_block.chain_state.block_height() == height - 1 { + if !allow_broken_hash_chain { + assert_eq!(prev_hash, prior_cached_block.chain_state.block_hash()); + } + } else { + let final_sapling_tree = + (prior_cached_block.sapling_end_size..initial_sapling_tree_size).fold( + prior_cached_block.chain_state.final_sapling_tree().clone(), + |mut acc, _| { + acc.append(::sapling::Node::from_scalar(bls12_381::Scalar::random( + &mut self.rng, + ))); + acc + }, + ); + + #[cfg(feature = "orchard")] + let final_orchard_tree = + (prior_cached_block.orchard_end_size..initial_orchard_tree_size).fold( + prior_cached_block.chain_state.final_orchard_tree().clone(), + |mut acc, _| { + acc.append(MerkleHashOrchard::random(&mut self.rng)); + acc + }, + ); + + prior_cached_block = CachedBlock::at( + ChainState::new( + height - 1, + prev_hash, + final_sapling_tree, + #[cfg(feature = "orchard")] + final_orchard_tree, + ), + initial_sapling_tree_size, + initial_orchard_tree_size, + ); + + self.cached_blocks + .insert(height - 1, prior_cached_block.clone()); + } + + let (cb, nfs) = fake_compact_block( + &self.network, + height, + prev_hash, + outputs, + initial_sapling_tree_size, + initial_orchard_tree_size, + &mut self.rng, + ); + assert_eq!(cb.height(), height); + + let res = self.cache_block(&prior_cached_block, cb); + self.latest_block_height = Some(height); + + (res, nfs) + } + + /// Creates a fake block at the expected next height spending the given note, and + /// inserts it into the cache. + pub fn generate_next_block_spending( + &mut self, + fvk: &Fvk, + note: (Fvk::Nullifier, Zatoshis), + to: impl Into
, + value: Zatoshis, + ) -> (BlockHeight, Cache::InsertResult) { + let prior_cached_block = self + .latest_cached_block() + .cloned() + .unwrap_or_else(|| CachedBlock::none(self.sapling_activation_height() - 1)); + let height = prior_cached_block.height() + 1; + + let cb = fake_compact_block_spending( + &self.network, + height, + prior_cached_block.chain_state.block_hash(), + note, + fvk, + to.into(), + value, + prior_cached_block.sapling_end_size, + prior_cached_block.orchard_end_size, + &mut self.rng, + ); + assert_eq!(cb.height(), height); + + let res = self.cache_block(&prior_cached_block, cb); + self.latest_block_height = Some(height); + + (height, res) + } + + /// Creates a fake block at the expected next height containing only the wallet + /// transaction with the given txid, and inserts it into the cache. + /// + /// This generated block will be treated as the latest block, and subsequent calls to + /// [`Self::generate_next_block`] (or similar) will build on it. + pub fn generate_next_block_including( + &mut self, + txid: TxId, + ) -> (BlockHeight, Cache::InsertResult) { + let tx = self + .wallet() + .get_transaction(txid) + .unwrap() + .expect("TxId should exist in the wallet"); + + // Index 0 is by definition a coinbase transaction, and the wallet doesn't + // construct coinbase transactions. So we pretend here that the block has a + // coinbase transaction that does not have shielded coinbase outputs. + self.generate_next_block_from_tx(1, &tx) + } + + /// Creates a fake block at the expected next height containing only the given + /// transaction, and inserts it into the cache. + /// + /// This generated block will be treated as the latest block, and subsequent calls to + /// [`Self::generate_next_block`] will build on it. + pub fn generate_next_block_from_tx( + &mut self, + tx_index: usize, + tx: &Transaction, + ) -> (BlockHeight, Cache::InsertResult) { + let prior_cached_block = self + .latest_cached_block() + .cloned() + .unwrap_or_else(|| CachedBlock::none(self.sapling_activation_height() - 1)); + let height = prior_cached_block.height() + 1; + + let cb = fake_compact_block_from_tx( + height, + prior_cached_block.chain_state.block_hash(), + tx_index, + tx, + prior_cached_block.sapling_end_size, + prior_cached_block.orchard_end_size, + &mut self.rng, + ); + assert_eq!(cb.height(), height); + + let res = self.cache_block(&prior_cached_block, cb); + self.latest_block_height = Some(height); + + (height, res) + } + + /// Truncates the test wallet and block cache to the specified height, discarding all data from + /// blocks at heights greater than the specified height, excluding transaction data that may + /// not be recoverable from the chain. + pub fn truncate_to_height(&mut self, height: BlockHeight) { + self.wallet_mut().truncate_to_height(height).unwrap(); + self.cache.truncate_to_height(height); + self.cached_blocks.split_off(&(height + 1)); + self.latest_block_height = Some(height); + } + + /// Truncates the test wallet to the specified height, and resets the cache's latest block + /// height but does not truncate the block cache. This is useful for circumstances when you + /// want to re-scan a set of cached blocks. + pub fn truncate_to_height_retaining_cache(&mut self, height: BlockHeight) { + self.wallet_mut().truncate_to_height(height).unwrap(); + self.latest_block_height = Some(height); + } +} + +impl TestState +where + Cache: TestCache, + ::Error: fmt::Debug, + ParamsT: consensus::Parameters + Send + 'static, + DbT: InputSource + WalletTest + WalletWrite + WalletCommitmentTrees, + ::AccountId: + std::fmt::Debug + ConditionallySelectable + Default + Send + 'static, +{ + /// Invokes [`scan_cached_blocks`] with the given arguments, expecting success. + pub fn scan_cached_blocks(&mut self, from_height: BlockHeight, limit: usize) -> ScanSummary { + let result = self.try_scan_cached_blocks(from_height, limit); + assert_matches!(result, Ok(_)); + result.unwrap() + } + + /// Invokes [`scan_cached_blocks`] with the given arguments. + pub fn try_scan_cached_blocks( + &mut self, + from_height: BlockHeight, + limit: usize, + ) -> Result< + ScanSummary, + super::chain::error::Error< + ::Error, + ::Error, + >, + > { + let prior_cached_block = self + .latest_cached_block_below_height(from_height) + .cloned() + .unwrap_or_else(|| CachedBlock::none(from_height - 1)); + + let result = scan_cached_blocks( + &self.network, + self.cache.block_source(), + &mut self.wallet_data, + from_height, + &prior_cached_block.chain_state, + limit, + ); + result + } + + /// Insert shard roots for both trees. + pub fn put_subtree_roots( + &mut self, + sapling_start_index: u64, + sapling_roots: &[CommitmentTreeRoot<::sapling::Node>], + #[cfg(feature = "orchard")] orchard_start_index: u64, + #[cfg(feature = "orchard")] orchard_roots: &[CommitmentTreeRoot], + ) -> Result<(), ShardTreeError<::Error>> { + self.wallet_mut() + .put_sapling_subtree_roots(sapling_start_index, sapling_roots)?; + + #[cfg(feature = "orchard")] + self.wallet_mut() + .put_orchard_subtree_roots(orchard_start_index, orchard_roots)?; + + Ok(()) + } +} + +impl TestState +where + ParamsT: consensus::Parameters + Send + 'static, + AccountIdT: std::fmt::Debug + std::cmp::Eq + std::hash::Hash, + ErrT: std::fmt::Debug, + DbT: InputSource + + WalletTest + + WalletWrite + + WalletCommitmentTrees, + ::AccountId: ConditionallySelectable + Default + Send + 'static, +{ + // Creates a transaction that sends the specified value from the given account to + // the provided recipient address, using a greedy input selector and the default + // mutli-output change strategy. + pub fn create_standard_transaction( + &mut self, + from_account: &TestAccount, + to: ZcashAddress, + value: Zatoshis, + ) -> Result< + NonEmpty, + super::wallet::TransferErrT< + DbT, + GreedyInputSelector, + standard::MultiOutputChangeStrategy, + >, + > { + let input_selector = GreedyInputSelector::new(); + + #[cfg(not(feature = "orchard"))] + let fallback_change_pool = ShieldedProtocol::Sapling; + #[cfg(feature = "orchard")] + let fallback_change_pool = ShieldedProtocol::Orchard; + + let change_strategy = standard::SingleOutputChangeStrategy::new( + StandardFeeRule::Zip317, + None, + fallback_change_pool, + DustOutputPolicy::default(), + ); + + let request = + zip321::TransactionRequest::new(vec![Payment::without_memo(to, value)]).unwrap(); + + self.spend( + &input_selector, + &change_strategy, + from_account.usk(), + request, + OvkPolicy::Sender, + NonZeroU32::MIN, + ) + } + + /// Prepares and executes the given [`zip321::TransactionRequest`] in a single step. + #[allow(clippy::type_complexity)] + pub fn spend( + &mut self, + input_selector: &InputsT, + change_strategy: &ChangeT, + usk: &UnifiedSpendingKey, + request: zip321::TransactionRequest, + ovk_policy: OvkPolicy, + min_confirmations: NonZeroU32, + ) -> Result, super::wallet::TransferErrT> + where + InputsT: InputSelector, + ChangeT: ChangeStrategy, + { + let prover = LocalTxProver::bundled(); + let network = self.network().clone(); + + let account = self + .wallet() + .get_account_for_ufvk(&usk.to_unified_full_viewing_key()) + .map_err(Error::DataSource)? + .ok_or(Error::KeyNotRecognized)?; + + let proposal = propose_transfer( + self.wallet_mut(), + &network, + account.id(), + input_selector, + change_strategy, + request, + min_confirmations, + )?; + + create_proposed_transactions( + self.wallet_mut(), + &network, + &prover, + &prover, + usk, + ovk_policy, + &proposal, + ) + } + + /// Invokes [`propose_transfer`] with the given arguments. + #[allow(clippy::type_complexity)] + pub fn propose_transfer( + &mut self, + spend_from_account: ::AccountId, + input_selector: &InputsT, + change_strategy: &ChangeT, + request: zip321::TransactionRequest, + min_confirmations: NonZeroU32, + ) -> Result< + Proposal::NoteRef>, + super::wallet::ProposeTransferErrT, + > + where + InputsT: InputSelector, + ChangeT: ChangeStrategy, + { + let network = self.network().clone(); + propose_transfer::<_, _, _, _, Infallible>( + self.wallet_mut(), + &network, + spend_from_account, + input_selector, + change_strategy, + request, + min_confirmations, + ) + } + + /// Invokes [`propose_standard_transfer_to_address`] with the given arguments. + #[allow(clippy::type_complexity)] + #[allow(clippy::too_many_arguments)] + pub fn propose_standard_transfer( + &mut self, + spend_from_account: ::AccountId, + fee_rule: StandardFeeRule, + min_confirmations: NonZeroU32, + to: &Address, + amount: Zatoshis, + memo: Option, + change_memo: Option, + fallback_change_pool: ShieldedProtocol, + ) -> Result< + Proposal::NoteRef>, + super::wallet::ProposeTransferErrT< + DbT, + CommitmentTreeErrT, + GreedyInputSelector, + SingleOutputChangeStrategy, + >, + > { + let network = self.network().clone(); + let result = propose_standard_transfer_to_address::<_, _, CommitmentTreeErrT>( + self.wallet_mut(), + &network, + fee_rule, + spend_from_account, + min_confirmations, + to, + amount, + memo, + change_memo, + fallback_change_pool, + ); + + if let Ok(proposal) = &result { + check_proposal_serialization_roundtrip(self.wallet(), proposal); + } + + result + } + + /// Invokes [`propose_shielding`] with the given arguments. + /// + /// [`propose_shielding`]: crate::data_api::wallet::propose_shielding + #[cfg(feature = "transparent-inputs")] + #[allow(clippy::type_complexity)] + #[allow(dead_code)] + pub fn propose_shielding( + &mut self, + input_selector: &InputsT, + change_strategy: &ChangeT, + shielding_threshold: Zatoshis, + from_addrs: &[TransparentAddress], + to_account: ::AccountId, + min_confirmations: u32, + ) -> Result< + Proposal, + super::wallet::ProposeShieldingErrT, + > + where + InputsT: ShieldingSelector, + ChangeT: ChangeStrategy, + { + use super::wallet::propose_shielding; + + let network = self.network().clone(); + propose_shielding::<_, _, _, _, Infallible>( + self.wallet_mut(), + &network, + input_selector, + change_strategy, + shielding_threshold, + from_addrs, + to_account, + min_confirmations, + ) + } + + /// Invokes [`create_proposed_transactions`] with the given arguments. + #[allow(clippy::type_complexity)] + pub fn create_proposed_transactions( + &mut self, + usk: &UnifiedSpendingKey, + ovk_policy: OvkPolicy, + proposal: &Proposal::NoteRef>, + ) -> Result< + NonEmpty, + super::wallet::CreateErrT, + > + where + FeeRuleT: FeeRule, + { + let prover = LocalTxProver::bundled(); + let network = self.network().clone(); + create_proposed_transactions( + self.wallet_mut(), + &network, + &prover, + &prover, + usk, + ovk_policy, + proposal, + ) + } + + /// Invokes [`create_pczt_from_proposal`] with the given arguments. + /// + /// [`create_pczt_from_proposal`]: super::wallet::create_pczt_from_proposal + #[cfg(feature = "pczt")] + #[allow(clippy::type_complexity)] + pub fn create_pczt_from_proposal( + &mut self, + spend_from_account: ::AccountId, + ovk_policy: OvkPolicy, + proposal: &Proposal::NoteRef>, + ) -> Result< + pczt::Pczt, + super::wallet::CreateErrT, + > + where + ::AccountId: serde::Serialize, + FeeRuleT: FeeRule, + { + use super::wallet::create_pczt_from_proposal; + + let network = self.network().clone(); + + create_pczt_from_proposal( + self.wallet_mut(), + &network, + spend_from_account, + ovk_policy, + proposal, + ) + } + + /// Invokes [`extract_and_store_transaction_from_pczt`] with the given arguments. + /// + /// [`extract_and_store_transaction_from_pczt`]: super::wallet::extract_and_store_transaction_from_pczt + #[cfg(feature = "pczt")] + #[allow(clippy::type_complexity)] + pub fn extract_and_store_transaction_from_pczt( + &mut self, + pczt: pczt::Pczt, + ) -> Result> + where + ::AccountId: serde::de::DeserializeOwned, + { + use super::wallet::extract_and_store_transaction_from_pczt; + + let prover = LocalTxProver::bundled(); + let (spend_vk, output_vk) = prover.verifying_keys(); + let orchard_vk = ::orchard::circuit::VerifyingKey::build(); + + extract_and_store_transaction_from_pczt( + self.wallet_mut(), + pczt, + &spend_vk, + &output_vk, + &orchard_vk, + ) + } + + /// Invokes [`shield_transparent_funds`] with the given arguments. + /// + /// [`shield_transparent_funds`]: crate::data_api::wallet::shield_transparent_funds + #[cfg(feature = "transparent-inputs")] + #[allow(clippy::type_complexity)] + #[allow(clippy::too_many_arguments)] + pub fn shield_transparent_funds( + &mut self, + input_selector: &InputsT, + change_strategy: &ChangeT, + shielding_threshold: Zatoshis, + usk: &UnifiedSpendingKey, + from_addrs: &[TransparentAddress], + to_account: ::AccountId, + min_confirmations: u32, + ) -> Result, super::wallet::ShieldErrT> + where + InputsT: ShieldingSelector, + ChangeT: ChangeStrategy, + { + use crate::data_api::wallet::shield_transparent_funds; + + let prover = LocalTxProver::bundled(); + let network = self.network().clone(); + shield_transparent_funds( + self.wallet_mut(), + &network, + &prover, + &prover, + input_selector, + change_strategy, + shielding_threshold, + usk, + from_addrs, + to_account, + min_confirmations, + ) + } + + fn with_account_balance T>( + &self, + account: AccountIdT, + min_confirmations: u32, + f: F, + ) -> T { + let binding = self + .wallet() + .get_wallet_summary(min_confirmations) + .unwrap() + .unwrap(); + f(binding.account_balances().get(&account).unwrap()) + } + + /// Returns the total balance in the given account at this point in the test. + pub fn get_total_balance(&self, account: AccountIdT) -> Zatoshis { + self.with_account_balance(account, 0, |balance| balance.total()) + } + + /// Returns the balance in the given account that is spendable with the given number + /// of confirmations at this point in the test. + pub fn get_spendable_balance(&self, account: AccountIdT, min_confirmations: u32) -> Zatoshis { + self.with_account_balance(account, min_confirmations, |balance| { + balance.spendable_value() + }) + } + + /// Returns the balance in the given account that is detected but not yet spendable + /// with the given number of confirmations at this point in the test. + pub fn get_pending_shielded_balance( + &self, + account: AccountIdT, + min_confirmations: u32, + ) -> Zatoshis { + self.with_account_balance(account, min_confirmations, |balance| { + balance.value_pending_spendability() + balance.change_pending_confirmation() + }) + .unwrap() + } + + /// Returns the amount of change in the given account that is not yet spendable with + /// the given number of confirmations at this point in the test. + #[allow(dead_code)] + pub fn get_pending_change(&self, account: AccountIdT, min_confirmations: u32) -> Zatoshis { + self.with_account_balance(account, min_confirmations, |balance| { + balance.change_pending_confirmation() + }) + } + + /// Returns a summary of the wallet at this point in the test. + pub fn get_wallet_summary(&self, min_confirmations: u32) -> Option> { + self.wallet().get_wallet_summary(min_confirmations).unwrap() + } +} + +impl TestState +where + ParamsT: consensus::Parameters + Send + 'static, + AccountIdT: std::cmp::Eq + std::hash::Hash, + ErrT: std::fmt::Debug, + DbT: InputSource + + WalletTest + + WalletWrite + + WalletCommitmentTrees, + ::AccountId: ConditionallySelectable + Default + Send + 'static, +{ + /// Returns a transaction from the history. + #[allow(dead_code)] + pub fn get_tx_from_history( + &self, + txid: TxId, + ) -> Result>, ErrT> { + let history = self.wallet().get_tx_history()?; + Ok(history.into_iter().find(|tx| tx.txid() == txid)) + } +} + +impl TestState { + /// Resets the wallet using a new wallet database but with the same cache of blocks, + /// and returns the old wallet database file. + /// + /// This does not recreate accounts, nor does it rescan the cached blocks. + /// The resulting wallet has no test account. + /// Before using any `generate_*` method on the reset state, call `reset_latest_cached_block()`. + pub fn reset(&mut self) -> DbT::Handle { + self.latest_block_height = None; + self.test_account = None; + DbT::reset(self) + } + + // /// Reset the latest cached block to the most recent one in the cache database. + // #[allow(dead_code)] + // pub fn reset_latest_cached_block(&mut self) { + // self.cache + // .block_source() + // .with_blocks::<_, Infallible>(None, None, |block: CompactBlock| { + // let chain_metadata = block.chain_metadata.unwrap(); + // self.latest_cached_block = Some(CachedBlock::at( + // BlockHash::from_slice(block.hash.as_slice()), + // BlockHeight::from_u32(block.height.try_into().unwrap()), + // chain_metadata.sapling_commitment_tree_size, + // chain_metadata.orchard_commitment_tree_size, + // )); + // Ok(()) + // }) + // .unwrap(); + // } +} + +pub fn single_output_change_strategy( + fee_rule: StandardFeeRule, + change_memo: Option<&str>, + fallback_change_pool: ShieldedProtocol, +) -> standard::SingleOutputChangeStrategy { + let change_memo = change_memo.map(|m| MemoBytes::from(m.parse::().unwrap())); + standard::SingleOutputChangeStrategy::new( + fee_rule, + change_memo, + fallback_change_pool, + DustOutputPolicy::default(), + ) +} + +// Checks that a protobuf proposal serialized from the provided proposal value correctly parses to +// the same proposal value. +fn check_proposal_serialization_roundtrip( + wallet_data: &DbT, + proposal: &Proposal, +) { + let proposal_proto = crate::proto::proposal::Proposal::from_standard_proposal(proposal); + let deserialized_proposal = proposal_proto.try_into_standard_proposal(wallet_data); + assert_matches!(deserialized_proposal, Ok(r) if &r == proposal); +} + +/// The initial chain state for a test. +/// +/// This is returned from the closure passed to [`TestBuilder::with_initial_chain_state`] +/// to configure the test state with a starting chain position, to which subsequent test +/// activity is applied. +pub struct InitialChainState { + /// Information about the chain's state as of the chain tip. + pub chain_state: ChainState, + /// Roots of the completed Sapling subtrees as of this chain state. + pub prior_sapling_roots: Vec>, + /// Roots of the completed Orchard subtrees as of this chain state. + #[cfg(feature = "orchard")] + pub prior_orchard_roots: Vec>, +} + +/// Trait representing the ability to construct a new data store for use in a test. +pub trait DataStoreFactory { + type Error: core::fmt::Debug; + type AccountId: std::fmt::Debug + ConditionallySelectable + Default + Hash + Eq + Send + 'static; + type Account: Account + Clone; + type DsError: core::fmt::Debug; + type DataStore: InputSource + + WalletRead + + WalletTest + + WalletWrite + + WalletCommitmentTrees; + + /// Constructs a new data store. + fn new_data_store(&self, network: LocalNetwork) -> Result; +} + +/// A [`TestState`] builder, that configures the environment for a test. +pub struct TestBuilder { + rng: ChaChaRng, + network: LocalNetwork, + cache: Cache, + ds_factory: DataStoreFactory, + initial_chain_state: Option, + account_birthday: Option, + account_index: Option, +} + +impl TestBuilder<(), ()> { + /// The default network used by [`TestBuilder::new`]. + /// + /// This is a fake network where Sapling through NU5 activate at the same height. We + /// pick height 100,000 to be large enough to handle any hard-coded test offsets. + pub const DEFAULT_NETWORK: LocalNetwork = LocalNetwork { + overwinter: Some(BlockHeight::from_u32(1)), + sapling: Some(BlockHeight::from_u32(100_000)), + blossom: Some(BlockHeight::from_u32(100_000)), + heartwood: Some(BlockHeight::from_u32(100_000)), + canopy: Some(BlockHeight::from_u32(100_000)), + nu5: Some(BlockHeight::from_u32(100_000)), + nu6: None, + #[cfg(zcash_unstable = "zfuture")] + z_future: None, + }; + + /// Constructs a new test environment builder. + pub fn new() -> Self { + TestBuilder { + rng: ChaChaRng::seed_from_u64(0), + network: Self::DEFAULT_NETWORK, + cache: (), + ds_factory: (), + initial_chain_state: None, + account_birthday: None, + account_index: None, + } + } +} + +impl Default for TestBuilder<(), ()> { + fn default() -> Self { + Self::new() + } +} + +impl TestBuilder<(), A> { + /// Adds a block cache to the test environment. + pub fn with_block_cache(self, cache: C) -> TestBuilder { + TestBuilder { + rng: self.rng, + network: self.network, + cache, + ds_factory: self.ds_factory, + initial_chain_state: self.initial_chain_state, + account_birthday: self.account_birthday, + account_index: self.account_index, + } + } +} + +impl TestBuilder { + /// Adds a wallet data store to the test environment. + pub fn with_data_store_factory( + self, + ds_factory: DsFactory, + ) -> TestBuilder { + TestBuilder { + rng: self.rng, + network: self.network, + cache: self.cache, + ds_factory, + initial_chain_state: self.initial_chain_state, + account_birthday: self.account_birthday, + account_index: self.account_index, + } + } +} + +impl TestBuilder { + /// Configures the test to start with the given initial chain state. + /// + /// # Panics + /// + /// - Must not be called twice. + /// - Must be called before [`Self::with_account_from_sapling_activation`] or + /// [`Self::with_account_having_current_birthday`]. + /// + /// # Examples + /// + /// ``` + /// use std::num::NonZeroU8; + /// + /// use incrementalmerkletree::frontier::Frontier; + /// use zcash_primitives::{block::BlockHash, consensus::Parameters}; + /// use zcash_protocol::consensus::NetworkUpgrade; + /// use zcash_client_backend::data_api::{ + /// chain::{ChainState, CommitmentTreeRoot}, + /// testing::{InitialChainState, TestBuilder}, + /// }; + /// + /// // For this test, we'll start inserting leaf notes 5 notes after the end of the + /// // third subtree, with a gap of 10 blocks. After `scan_cached_blocks`, the scan + /// // queue should have a requested scan range of 300..310 with `FoundNote` priority, + /// // 310..320 with `Scanned` priority. We set both Sapling and Orchard to the same + /// // initial tree size for simplicity. + /// let prior_block_hash = BlockHash([0; 32]); + /// let initial_sapling_tree_size: u32 = (0x1 << 16) * 3 + 5; + /// let initial_orchard_tree_size: u32 = (0x1 << 16) * 3 + 5; + /// let initial_height_offset = 310; + /// + /// let mut st = TestBuilder::new() + /// .with_initial_chain_state(|rng, network| { + /// // For simplicity, assume Sapling and NU5 activated at the same height. + /// let sapling_activation_height = + /// network.activation_height(NetworkUpgrade::Sapling).unwrap(); + /// + /// // Construct a fake chain state for the end of block 300 + /// let (prior_sapling_roots, sapling_initial_tree) = + /// Frontier::random_with_prior_subtree_roots( + /// rng, + /// initial_sapling_tree_size.into(), + /// NonZeroU8::new(16).unwrap(), + /// ); + /// let prior_sapling_roots = prior_sapling_roots + /// .into_iter() + /// .zip(1u32..) + /// .map(|(root, i)| { + /// CommitmentTreeRoot::from_parts(sapling_activation_height + (100 * i), root) + /// }) + /// .collect::>(); + /// + /// #[cfg(feature = "orchard")] + /// let (prior_orchard_roots, orchard_initial_tree) = + /// Frontier::random_with_prior_subtree_roots( + /// rng, + /// initial_orchard_tree_size.into(), + /// NonZeroU8::new(16).unwrap(), + /// ); + /// #[cfg(feature = "orchard")] + /// let prior_orchard_roots = prior_orchard_roots + /// .into_iter() + /// .zip(1u32..) + /// .map(|(root, i)| { + /// CommitmentTreeRoot::from_parts(sapling_activation_height + (100 * i), root) + /// }) + /// .collect::>(); + /// + /// InitialChainState { + /// chain_state: ChainState::new( + /// sapling_activation_height + initial_height_offset - 1, + /// prior_block_hash, + /// sapling_initial_tree, + /// #[cfg(feature = "orchard")] + /// orchard_initial_tree, + /// ), + /// prior_sapling_roots, + /// #[cfg(feature = "orchard")] + /// prior_orchard_roots, + /// } + /// }); + /// ``` + pub fn with_initial_chain_state( + mut self, + chain_state: impl FnOnce(&mut ChaChaRng, &LocalNetwork) -> InitialChainState, + ) -> Self { + assert!(self.initial_chain_state.is_none()); + assert!(self.account_birthday.is_none()); + self.initial_chain_state = Some(chain_state(&mut self.rng, &self.network)); + self + } + + /// Configures the environment with a [`TestAccount`] that has a birthday at Sapling + /// activation. + /// + /// # Panics + /// + /// - Must not be called twice. + /// - Do not call both [`Self::with_account_having_current_birthday`] and this method. + pub fn with_account_from_sapling_activation(mut self, prev_hash: BlockHash) -> Self { + assert!(self.account_birthday.is_none()); + self.account_birthday = Some(AccountBirthday::from_parts( + ChainState::empty( + self.network + .activation_height(NetworkUpgrade::Sapling) + .unwrap() + - 1, + prev_hash, + ), + None, + )); + self + } + + /// Configures the environment with a [`TestAccount`] that has a birthday one block + /// after the initial chain state. + /// + /// # Panics + /// + /// - Must not be called twice. + /// - Must call [`Self::with_initial_chain_state`] before calling this method. + /// - Do not call both [`Self::with_account_from_sapling_activation`] and this method. + pub fn with_account_having_current_birthday(mut self) -> Self { + assert!(self.account_birthday.is_none()); + assert!(self.initial_chain_state.is_some()); + self.account_birthday = Some(AccountBirthday::from_parts( + self.initial_chain_state + .as_ref() + .unwrap() + .chain_state + .clone(), + None, + )); + self + } + + /// Sets the account index for the test account. + /// + /// Does nothing unless either [`Self::with_account_from_sapling_activation`] or + /// [`Self::with_account_having_current_birthday`] is also called. + /// + /// # Panics + /// + /// - Must not be called twice. + pub fn set_account_index(mut self, index: zip32::AccountId) -> Self { + assert!(self.account_index.is_none()); + self.account_index = Some(index); + self + } +} + +impl TestBuilder { + /// Builds the state for this test. + pub fn build(self) -> TestState { + let mut cached_blocks = BTreeMap::new(); + let mut wallet_data = self.ds_factory.new_data_store(self.network).unwrap(); + + if let Some(initial_state) = &self.initial_chain_state { + wallet_data + .put_sapling_subtree_roots(0, &initial_state.prior_sapling_roots) + .unwrap(); + wallet_data + .with_sapling_tree_mut(|t| { + t.insert_frontier( + initial_state.chain_state.final_sapling_tree().clone(), + Retention::Checkpoint { + id: initial_state.chain_state.block_height(), + marking: Marking::Reference, + }, + ) + }) + .unwrap(); + + #[cfg(feature = "orchard")] + { + wallet_data + .put_orchard_subtree_roots(0, &initial_state.prior_orchard_roots) + .unwrap(); + wallet_data + .with_orchard_tree_mut(|t| { + t.insert_frontier( + initial_state.chain_state.final_orchard_tree().clone(), + Retention::Checkpoint { + id: initial_state.chain_state.block_height(), + marking: Marking::Reference, + }, + ) + }) + .unwrap(); + } + + let final_sapling_tree_size = + initial_state.chain_state.final_sapling_tree().tree_size() as u32; + let _final_orchard_tree_size = 0; + #[cfg(feature = "orchard")] + let _final_orchard_tree_size = + initial_state.chain_state.final_orchard_tree().tree_size() as u32; + + cached_blocks.insert( + initial_state.chain_state.block_height(), + CachedBlock { + chain_state: initial_state.chain_state.clone(), + sapling_end_size: final_sapling_tree_size, + orchard_end_size: _final_orchard_tree_size, + }, + ); + }; + + let test_account = self.account_birthday.map(|birthday| { + let seed = Secret::new(vec![0u8; 32]); + let (account, usk) = match self.account_index { + Some(index) => wallet_data + .import_account_hd("", &seed, index, &birthday, None) + .unwrap(), + None => { + let result = wallet_data + .create_account("", &seed, &birthday, None) + .unwrap(); + ( + wallet_data.get_account(result.0).unwrap().unwrap(), + result.1, + ) + } + }; + ( + seed, + TestAccount { + account, + usk, + birthday, + }, + ) + }); + + TestState { + cache: self.cache, + cached_blocks, + latest_block_height: self + .initial_chain_state + .map(|s| s.chain_state.block_height()), + wallet_data, + network: self.network, + test_account, + rng: self.rng, + } + } +} + +/// Trait used by tests that require a full viewing key. +pub trait TestFvk: Clone { + /// The type of nullifier corresponding to the kind of note that this full viewing key + /// can detect (and that its corresponding spending key can spend). + type Nullifier: Copy; + + /// Returns the Sapling outgoing viewing key corresponding to this full viewing key, + /// if any. + fn sapling_ovk(&self) -> Option<::sapling::keys::OutgoingViewingKey>; + + /// Returns the Orchard outgoing viewing key corresponding to this full viewing key, + /// if any. + #[cfg(feature = "orchard")] + fn orchard_ovk(&self, scope: zip32::Scope) -> Option<::orchard::keys::OutgoingViewingKey>; + + /// Adds a single spend to the given [`CompactTx`] of a note previously received by + /// this full viewing key. + fn add_spend( + &self, + ctx: &mut CompactTx, + nf: Self::Nullifier, + rng: &mut R, + ); + + /// Adds a single output to the given [`CompactTx`] that will be received by this full + /// viewing key. + /// + /// `req` allows configuring how the full viewing key will detect the output. + #[allow(clippy::too_many_arguments)] + fn add_output( + &self, + ctx: &mut CompactTx, + params: &P, + height: BlockHeight, + req: AddressType, + value: Zatoshis, + initial_sapling_tree_size: u32, + // we don't require an initial Orchard tree size because we don't need it to compute + // the nullifier. + rng: &mut R, + ) -> Self::Nullifier; + + /// Adds both a spend and an output to the given [`CompactTx`]. + /// + /// - If this is a Sapling full viewing key, the transaction will gain both a Spend + /// and an Output. + /// - If this is an Orchard full viewing key, the transaction will gain an Action. + /// + /// `req` allows configuring how the full viewing key will detect the output. + #[allow(clippy::too_many_arguments)] + fn add_logical_action( + &self, + ctx: &mut CompactTx, + params: &P, + height: BlockHeight, + nf: Self::Nullifier, + req: AddressType, + value: Zatoshis, + initial_sapling_tree_size: u32, + // we don't require an initial Orchard tree size because we don't need it to compute + // the nullifier. + rng: &mut R, + ) -> Self::Nullifier; +} + +impl TestFvk for &A { + type Nullifier = A::Nullifier; + + fn sapling_ovk(&self) -> Option<::sapling::keys::OutgoingViewingKey> { + (*self).sapling_ovk() + } + + #[cfg(feature = "orchard")] + fn orchard_ovk(&self, scope: zip32::Scope) -> Option<::orchard::keys::OutgoingViewingKey> { + (*self).orchard_ovk(scope) + } + + fn add_spend( + &self, + ctx: &mut CompactTx, + nf: Self::Nullifier, + rng: &mut R, + ) { + (*self).add_spend(ctx, nf, rng) + } + + fn add_output( + &self, + ctx: &mut CompactTx, + params: &P, + height: BlockHeight, + req: AddressType, + value: Zatoshis, + initial_sapling_tree_size: u32, + // we don't require an initial Orchard tree size because we don't need it to compute + // the nullifier. + rng: &mut R, + ) -> Self::Nullifier { + (*self).add_output( + ctx, + params, + height, + req, + value, + initial_sapling_tree_size, + rng, + ) + } + + fn add_logical_action( + &self, + ctx: &mut CompactTx, + params: &P, + height: BlockHeight, + nf: Self::Nullifier, + req: AddressType, + value: Zatoshis, + initial_sapling_tree_size: u32, + // we don't require an initial Orchard tree size because we don't need it to compute + // the nullifier. + rng: &mut R, + ) -> Self::Nullifier { + (*self).add_logical_action( + ctx, + params, + height, + nf, + req, + value, + initial_sapling_tree_size, + rng, + ) + } +} + +impl TestFvk for DiversifiableFullViewingKey { + type Nullifier = ::sapling::Nullifier; + + fn sapling_ovk(&self) -> Option<::sapling::keys::OutgoingViewingKey> { + Some(self.fvk().ovk) + } + + #[cfg(feature = "orchard")] + fn orchard_ovk(&self, _: zip32::Scope) -> Option<::orchard::keys::OutgoingViewingKey> { + None + } + + fn add_spend( + &self, + ctx: &mut CompactTx, + nf: Self::Nullifier, + _: &mut R, + ) { + let cspend = CompactSaplingSpend { nf: nf.to_vec() }; + ctx.spends.push(cspend); + } + + fn add_output( + &self, + ctx: &mut CompactTx, + params: &P, + height: BlockHeight, + req: AddressType, + value: Zatoshis, + initial_sapling_tree_size: u32, + rng: &mut R, + ) -> Self::Nullifier { + let recipient = match req { + AddressType::DefaultExternal => self.default_address().1, + AddressType::DiversifiedExternal(idx) => self.find_address(idx).unwrap().1, + AddressType::Internal => self.change_address().1, + }; + + let position = initial_sapling_tree_size + ctx.outputs.len() as u32; + + let (cout, note) = + compact_sapling_output(params, height, recipient, value, self.sapling_ovk(), rng); + ctx.outputs.push(cout); + + note.nf(&self.fvk().vk.nk, position as u64) + } + + #[allow(clippy::too_many_arguments)] + fn add_logical_action( + &self, + ctx: &mut CompactTx, + params: &P, + height: BlockHeight, + nf: Self::Nullifier, + req: AddressType, + value: Zatoshis, + initial_sapling_tree_size: u32, + rng: &mut R, + ) -> Self::Nullifier { + self.add_spend(ctx, nf, rng); + self.add_output( + ctx, + params, + height, + req, + value, + initial_sapling_tree_size, + rng, + ) + } +} + +#[cfg(feature = "orchard")] +impl TestFvk for ::orchard::keys::FullViewingKey { + type Nullifier = ::orchard::note::Nullifier; + + fn sapling_ovk(&self) -> Option<::sapling::keys::OutgoingViewingKey> { + None + } + + fn orchard_ovk(&self, scope: zip32::Scope) -> Option<::orchard::keys::OutgoingViewingKey> { + Some(self.to_ovk(scope)) + } + + fn add_spend( + &self, + ctx: &mut CompactTx, + revealed_spent_note_nullifier: Self::Nullifier, + rng: &mut R, + ) { + // Generate a dummy recipient. + let recipient = loop { + let mut bytes = [0; 32]; + rng.fill_bytes(&mut bytes); + let sk = ::orchard::keys::SpendingKey::from_bytes(bytes); + if sk.is_some().into() { + break ::orchard::keys::FullViewingKey::from(&sk.unwrap()) + .address_at(0u32, zip32::Scope::External); + } + }; + + let (cact, _) = compact_orchard_action( + revealed_spent_note_nullifier, + recipient, + Zatoshis::ZERO, + self.orchard_ovk(zip32::Scope::Internal), + rng, + ); + ctx.actions.push(cact); + } + + fn add_output( + &self, + ctx: &mut CompactTx, + _: &P, + _: BlockHeight, + req: AddressType, + value: Zatoshis, + _: u32, // the position is not required for computing the Orchard nullifier + mut rng: &mut R, + ) -> Self::Nullifier { + // Generate a dummy nullifier for the spend + let revealed_spent_note_nullifier = + ::orchard::note::Nullifier::from_bytes(&pallas::Base::random(&mut rng).to_repr()) + .unwrap(); + + let (j, scope) = match req { + AddressType::DefaultExternal => (0u32.into(), zip32::Scope::External), + AddressType::DiversifiedExternal(idx) => (idx, zip32::Scope::External), + AddressType::Internal => (0u32.into(), zip32::Scope::Internal), + }; + + let (cact, note) = compact_orchard_action( + revealed_spent_note_nullifier, + self.address_at(j, scope), + value, + self.orchard_ovk(scope), + rng, + ); + ctx.actions.push(cact); + + note.nullifier(self) + } + + // Override so we can merge the spend and output into a single action. + fn add_logical_action( + &self, + ctx: &mut CompactTx, + _: &P, + _: BlockHeight, + revealed_spent_note_nullifier: Self::Nullifier, + address_type: AddressType, + value: Zatoshis, + _: u32, // the position is not required for computing the Orchard nullifier + rng: &mut R, + ) -> Self::Nullifier { + let (j, scope) = match address_type { + AddressType::DefaultExternal => (0u32.into(), zip32::Scope::External), + AddressType::DiversifiedExternal(idx) => (idx, zip32::Scope::External), + AddressType::Internal => (0u32.into(), zip32::Scope::Internal), + }; + + let (cact, note) = compact_orchard_action( + revealed_spent_note_nullifier, + self.address_at(j, scope), + value, + self.orchard_ovk(scope), + rng, + ); + ctx.actions.push(cact); + + // Return the nullifier of the newly created output note + note.nullifier(self) + } +} + +/// Configures how a [`TestFvk`] receives a particular output. +/// +/// Used with [`TestFvk::add_output`] and [`TestFvk::add_logical_action`]. +#[derive(Clone, Copy)] +pub enum AddressType { + /// The output will be sent to the default address of the full viewing key. + DefaultExternal, + /// The output will be sent to the specified diversified address of the full viewing + /// key. + #[allow(dead_code)] + DiversifiedExternal(DiversifierIndex), + /// The output will be sent to the internal receiver of the full viewing key. + /// + /// Such outputs are treated as "wallet-internal". A "recipient address" is **NEVER** + /// exposed to users. + Internal, +} + +/// Creates a `CompactSaplingOutput` at the given height paying the given recipient. +/// +/// Returns the `CompactSaplingOutput` and the new note. +fn compact_sapling_output( + params: &P, + height: BlockHeight, + recipient: ::sapling::PaymentAddress, + value: Zatoshis, + ovk: Option<::sapling::keys::OutgoingViewingKey>, + rng: &mut R, +) -> (CompactSaplingOutput, ::sapling::Note) { + let rseed = generate_random_rseed(zip212_enforcement(params, height), rng); + let note = ::sapling::Note::from_parts( + recipient, + ::sapling::value::NoteValue::from_raw(value.into_u64()), + rseed, + ); + let encryptor = sapling_note_encryption(ovk, note.clone(), *MemoBytes::empty().as_array(), rng); + let cmu = note.cmu().to_bytes().to_vec(); + let ephemeral_key = SaplingDomain::epk_bytes(encryptor.epk()).0.to_vec(); + let enc_ciphertext = encryptor.encrypt_note_plaintext(); + + ( + CompactSaplingOutput { + cmu, + ephemeral_key, + ciphertext: enc_ciphertext[..52].to_vec(), + }, + note, + ) +} + +/// Creates a `CompactOrchardAction` at the given height paying the given recipient. +/// +/// Returns the `CompactOrchardAction` and the new note. +#[cfg(feature = "orchard")] +fn compact_orchard_action( + nf_old: ::orchard::note::Nullifier, + recipient: ::orchard::Address, + value: Zatoshis, + ovk: Option<::orchard::keys::OutgoingViewingKey>, + rng: &mut R, +) -> (CompactOrchardAction, ::orchard::Note) { + use zcash_note_encryption::ShieldedOutput; + + let (compact_action, note) = ::orchard::note_encryption::testing::fake_compact_action( + rng, + nf_old, + recipient, + ::orchard::value::NoteValue::from_raw(value.into_u64()), + ovk, + ); + + ( + CompactOrchardAction { + nullifier: compact_action.nullifier().to_bytes().to_vec(), + cmx: compact_action.cmx().to_bytes().to_vec(), + ephemeral_key: compact_action.ephemeral_key().0.to_vec(), + ciphertext: compact_action.enc_ciphertext()[..52].to_vec(), + }, + note, + ) +} + +/// Creates a fake `CompactTx` with a random transaction ID and no spends or outputs. +fn fake_compact_tx(rng: &mut R) -> CompactTx { + let mut ctx = CompactTx::default(); + let mut txid = vec![0; 32]; + rng.fill_bytes(&mut txid); + ctx.hash = txid; + + ctx +} + +/// A fake output of a [`CompactTx`]. +/// +/// Used with the following block generators: +/// - [`TestState::generate_next_block_multi`] +/// - [`TestState::generate_block_at`] +#[derive(Clone)] +pub struct FakeCompactOutput { + fvk: Fvk, + address_type: AddressType, + value: Zatoshis, +} + +impl FakeCompactOutput { + /// Constructs a new fake output with the given properties. + pub fn new(fvk: Fvk, address_type: AddressType, value: Zatoshis) -> Self { + Self { + fvk, + address_type, + value, + } + } + + /// Constructs a new random fake external output to the given FVK with a value in the range + /// 10000..1000000 ZAT. + pub fn random(rng: &mut R, fvk: Fvk) -> Self { + Self { + fvk, + address_type: AddressType::DefaultExternal, + value: Zatoshis::const_from_u64(rng.gen_range(10000..1000000)), + } + } +} + +/// Create a fake CompactBlock at the given height, containing the specified fake compact outputs. +/// +/// Returns the newly created compact block, along with the nullifier for each note created in that +/// block. +#[allow(clippy::too_many_arguments)] +fn fake_compact_block( + params: &P, + height: BlockHeight, + prev_hash: BlockHash, + outputs: &[FakeCompactOutput], + initial_sapling_tree_size: u32, + initial_orchard_tree_size: u32, + mut rng: impl RngCore + CryptoRng, +) -> (CompactBlock, Vec) { + // Create a fake CompactBlock containing the note + let mut ctx = fake_compact_tx(&mut rng); + let mut nfs = vec![]; + for output in outputs { + let nf = output.fvk.add_output( + &mut ctx, + params, + height, + output.address_type, + output.value, + initial_sapling_tree_size, + &mut rng, + ); + nfs.push(nf); + } + + let cb = fake_compact_block_from_compact_tx( + ctx, + height, + prev_hash, + initial_sapling_tree_size, + initial_orchard_tree_size, + rng, + ); + (cb, nfs) +} + +/// Create a fake CompactBlock at the given height containing only the given transaction. +fn fake_compact_block_from_tx( + height: BlockHeight, + prev_hash: BlockHash, + tx_index: usize, + tx: &Transaction, + initial_sapling_tree_size: u32, + initial_orchard_tree_size: u32, + rng: impl RngCore, +) -> CompactBlock { + // Create a fake CompactTx containing the transaction. + let mut ctx = CompactTx { + index: tx_index as u64, + hash: tx.txid().as_ref().to_vec(), + ..Default::default() + }; + + if let Some(bundle) = tx.sapling_bundle() { + for spend in bundle.shielded_spends() { + ctx.spends.push(spend.into()); + } + for output in bundle.shielded_outputs() { + ctx.outputs.push(output.into()); + } + } + + #[cfg(feature = "orchard")] + if let Some(bundle) = tx.orchard_bundle() { + for action in bundle.actions() { + ctx.actions.push(action.into()); + } + } + + fake_compact_block_from_compact_tx( + ctx, + height, + prev_hash, + initial_sapling_tree_size, + initial_orchard_tree_size, + rng, + ) +} + +/// Create a fake CompactBlock at the given height, spending a single note from the +/// given address. +#[allow(clippy::too_many_arguments)] +fn fake_compact_block_spending( + params: &P, + height: BlockHeight, + prev_hash: BlockHash, + (nf, in_value): (Fvk::Nullifier, Zatoshis), + fvk: &Fvk, + to: Address, + value: Zatoshis, + initial_sapling_tree_size: u32, + initial_orchard_tree_size: u32, + mut rng: impl RngCore + CryptoRng, +) -> CompactBlock { + let mut ctx = fake_compact_tx(&mut rng); + + // Create a fake spend and a fake Note for the change + fvk.add_logical_action( + &mut ctx, + params, + height, + nf, + AddressType::Internal, + (in_value - value).unwrap(), + initial_sapling_tree_size, + &mut rng, + ); + + // Create a fake Note for the payment + match to { + Address::Sapling(recipient) => ctx.outputs.push( + compact_sapling_output( + params, + height, + recipient, + value, + fvk.sapling_ovk(), + &mut rng, + ) + .0, + ), + Address::Transparent(_) | Address::Tex(_) => { + panic!("transparent addresses not supported in compact blocks") + } + Address::Unified(ua) => { + // This is annoying to implement, because the protocol-aware UA type has no + // concept of ZIP 316 preference order. + let mut done = false; + + #[cfg(feature = "orchard")] + if let Some(recipient) = ua.orchard() { + // Generate a dummy nullifier + let nullifier = ::orchard::note::Nullifier::from_bytes( + &pallas::Base::random(&mut rng).to_repr(), + ) + .unwrap(); + + ctx.actions.push( + compact_orchard_action( + nullifier, + *recipient, + value, + fvk.orchard_ovk(zip32::Scope::External), + &mut rng, + ) + .0, + ); + done = true; + } + + if !done { + if let Some(recipient) = ua.sapling() { + ctx.outputs.push( + compact_sapling_output( + params, + height, + *recipient, + value, + fvk.sapling_ovk(), + &mut rng, + ) + .0, + ); + done = true; + } + } + if !done { + panic!("No supported shielded receiver to send funds to"); + } + } + } + + fake_compact_block_from_compact_tx( + ctx, + height, + prev_hash, + initial_sapling_tree_size, + initial_orchard_tree_size, + rng, + ) +} + +fn fake_compact_block_from_compact_tx( + ctx: CompactTx, + height: BlockHeight, + prev_hash: BlockHash, + initial_sapling_tree_size: u32, + initial_orchard_tree_size: u32, + mut rng: impl RngCore, +) -> CompactBlock { + let mut cb = CompactBlock { + hash: { + let mut hash = vec![0; 32]; + rng.fill_bytes(&mut hash); + hash + }, + height: height.into(), + ..Default::default() + }; + cb.prev_hash.extend_from_slice(&prev_hash.0); + cb.vtx.push(ctx); + cb.chain_metadata = Some(compact_formats::ChainMetadata { + sapling_commitment_tree_size: initial_sapling_tree_size + + cb.vtx.iter().map(|tx| tx.outputs.len() as u32).sum::(), + orchard_commitment_tree_size: initial_orchard_tree_size + + cb.vtx.iter().map(|tx| tx.actions.len() as u32).sum::(), + }); + cb +} + +/// Trait used by tests that require a block cache. +pub trait TestCache { + type BsError: core::fmt::Debug; + type BlockSource: BlockSource; + type InsertResult; + + /// Exposes the block cache as a [`BlockSource`]. + fn block_source(&self) -> &Self::BlockSource; + + /// Inserts a CompactBlock into the cache DB. + fn insert(&mut self, cb: &CompactBlock) -> Self::InsertResult; + + /// Deletes block data from the cache, retaining blocks at heights less than or equal to the + /// specified height. + fn truncate_to_height(&mut self, height: BlockHeight); +} + +/// A convenience type for the note commitments contained within a [`CompactBlock`]. +/// +/// Indended for use as (part of) the [`TestCache::InsertResult`] associated type. +pub struct NoteCommitments { + sapling: Vec<::sapling::Node>, + #[cfg(feature = "orchard")] + orchard: Vec, +} + +impl NoteCommitments { + /// Extracts the note commitments from the given compact block. + pub fn from_compact_block(cb: &CompactBlock) -> Self { + NoteCommitments { + sapling: cb + .vtx + .iter() + .flat_map(|tx| { + tx.outputs + .iter() + .map(|out| ::sapling::Node::from_cmu(&out.cmu().unwrap())) + }) + .collect(), + #[cfg(feature = "orchard")] + orchard: cb + .vtx + .iter() + .flat_map(|tx| { + tx.actions + .iter() + .map(|act| MerkleHashOrchard::from_cmx(&act.cmx().unwrap())) + }) + .collect(), + } + } + + /// Returns the Sapling note commitments. + #[allow(dead_code)] + pub fn sapling(&self) -> &[::sapling::Node] { + self.sapling.as_ref() + } + + /// Returns the Orchard note commitments. + #[cfg(feature = "orchard")] + pub fn orchard(&self) -> &[MerkleHashOrchard] { + self.orchard.as_ref() + } +} + +/// A mock wallet data source that implements the bare minimum necessary to function. +pub struct MockWalletDb { + pub network: Network, + pub sapling_tree: ShardTree< + MemoryShardStore<::sapling::Node, BlockHeight>, + { SAPLING_SHARD_HEIGHT * 2 }, + SAPLING_SHARD_HEIGHT, + >, + #[cfg(feature = "orchard")] + pub orchard_tree: ShardTree< + MemoryShardStore<::orchard::tree::MerkleHashOrchard, BlockHeight>, + { ORCHARD_SHARD_HEIGHT * 2 }, + ORCHARD_SHARD_HEIGHT, + >, +} + +impl MockWalletDb { + /// Constructs a new mock wallet data source. + pub fn new(network: Network) -> Self { + Self { + network, + sapling_tree: ShardTree::new(MemoryShardStore::empty(), 100), + #[cfg(feature = "orchard")] + orchard_tree: ShardTree::new(MemoryShardStore::empty(), 100), + } + } +} + +impl InputSource for MockWalletDb { + type Error = (); + type NoteRef = u32; + type AccountId = u32; + + fn get_spendable_note( + &self, + _txid: &TxId, + _protocol: ShieldedProtocol, + _index: u32, + ) -> Result>, Self::Error> { + Ok(None) + } + + fn select_spendable_notes( + &self, + _account: Self::AccountId, + _target_value: Zatoshis, + _sources: &[ShieldedProtocol], + _anchor_height: BlockHeight, + _exclude: &[Self::NoteRef], + ) -> Result, Self::Error> { + Ok(SpendableNotes::empty()) + } + + fn get_account_metadata( + &self, + _account: Self::AccountId, + _selector: &NoteFilter, + _exclude: &[Self::NoteRef], + ) -> Result { + Err(()) + } +} + +impl WalletRead for MockWalletDb { + type Error = (); + type AccountId = u32; + type Account = (Self::AccountId, UnifiedFullViewingKey); + + fn get_account_ids(&self) -> Result, Self::Error> { + Ok(Vec::new()) + } + + fn get_account( + &self, + _account_id: Self::AccountId, + ) -> Result, Self::Error> { + Ok(None) + } + + fn get_derived_account( + &self, + _seed: &SeedFingerprint, + _account_id: zip32::AccountId, + ) -> Result, Self::Error> { + Ok(None) + } + + fn validate_seed( + &self, + _account_id: Self::AccountId, + _seed: &SecretVec, + ) -> Result { + Ok(false) + } + + fn seed_relevance_to_derived_accounts( + &self, + _seed: &SecretVec, + ) -> Result, Self::Error> { + Ok(SeedRelevance::NoAccounts) + } + + fn get_account_for_ufvk( + &self, + _ufvk: &UnifiedFullViewingKey, + ) -> Result, Self::Error> { + Ok(None) + } + + fn get_current_address( + &self, + _account: Self::AccountId, + ) -> Result, Self::Error> { + Ok(None) + } + + fn get_account_birthday(&self, _account: Self::AccountId) -> Result { + Err(()) + } + + fn get_wallet_birthday(&self) -> Result, Self::Error> { + Ok(None) + } + + fn get_wallet_summary( + &self, + _min_confirmations: u32, + ) -> Result>, Self::Error> { + Ok(None) + } + + fn chain_height(&self) -> Result, Self::Error> { + Ok(None) + } + + fn get_block_hash(&self, _block_height: BlockHeight) -> Result, Self::Error> { + Ok(None) + } + + fn block_metadata(&self, _height: BlockHeight) -> Result, Self::Error> { + Ok(None) + } + + fn block_fully_scanned(&self) -> Result, Self::Error> { + Ok(None) + } + + fn get_max_height_hash(&self) -> Result, Self::Error> { + Ok(None) + } + + fn block_max_scanned(&self) -> Result, Self::Error> { + Ok(None) + } + + fn suggest_scan_ranges(&self) -> Result, Self::Error> { + Ok(vec![]) + } + + fn get_target_and_anchor_heights( + &self, + _min_confirmations: NonZeroU32, + ) -> Result, Self::Error> { + Ok(None) + } + + fn get_tx_height(&self, _txid: TxId) -> Result, Self::Error> { + Ok(None) + } + + fn get_unified_full_viewing_keys( + &self, + ) -> Result, Self::Error> { + Ok(HashMap::new()) + } + + fn get_memo(&self, _id_note: NoteId) -> Result, Self::Error> { + Ok(None) + } + + fn get_transaction(&self, _txid: TxId) -> Result, Self::Error> { + Ok(None) + } + + fn get_sapling_nullifiers( + &self, + _query: NullifierQuery, + ) -> Result, Self::Error> { + Ok(Vec::new()) + } + + #[cfg(feature = "orchard")] + fn get_orchard_nullifiers( + &self, + _query: NullifierQuery, + ) -> Result, Self::Error> { + Ok(Vec::new()) + } + + #[cfg(feature = "transparent-inputs")] + fn get_transparent_receivers( + &self, + _account: Self::AccountId, + ) -> Result>, Self::Error> { + Ok(HashMap::new()) + } + + #[cfg(feature = "transparent-inputs")] + fn get_transparent_balances( + &self, + _account: Self::AccountId, + _max_height: BlockHeight, + ) -> Result, Self::Error> { + Ok(HashMap::new()) + } + + #[cfg(feature = "transparent-inputs")] + fn get_transparent_address_metadata( + &self, + _account: Self::AccountId, + _address: &TransparentAddress, + ) -> Result, Self::Error> { + Ok(None) + } + + #[cfg(feature = "transparent-inputs")] + fn get_known_ephemeral_addresses( + &self, + _account: Self::AccountId, + _index_range: Option>, + ) -> Result, Self::Error> { + Ok(vec![]) + } + + #[cfg(feature = "transparent-inputs")] + fn find_account_for_ephemeral_address( + &self, + _address: &TransparentAddress, + ) -> Result, Self::Error> { + Ok(None) + } + + fn transaction_data_requests(&self) -> Result, Self::Error> { + Ok(vec![]) + } +} + +impl WalletWrite for MockWalletDb { + type UtxoRef = u32; + + fn create_account( + &mut self, + _account_name: &str, + seed: &SecretVec, + _birthday: &AccountBirthday, + _key_source: Option<&str>, + ) -> Result<(Self::AccountId, UnifiedSpendingKey), Self::Error> { + let account = zip32::AccountId::ZERO; + UnifiedSpendingKey::from_seed(&self.network, seed.expose_secret(), account) + .map(|k| (u32::from(account), k)) + .map_err(|_| ()) + } + + fn import_account_hd( + &mut self, + _account_name: &str, + _seed: &SecretVec, + _account_index: zip32::AccountId, + _birthday: &AccountBirthday, + _key_source: Option<&str>, + ) -> Result<(Self::Account, UnifiedSpendingKey), Self::Error> { + todo!() + } + + fn import_account_ufvk( + &mut self, + _account_name: &str, + _unified_key: &UnifiedFullViewingKey, + _birthday: &AccountBirthday, + _purpose: AccountPurpose, + _key_source: Option<&str>, + ) -> Result { + todo!() + } + + fn get_next_available_address( + &mut self, + _account: Self::AccountId, + _request: Option, + ) -> Result, Self::Error> { + Ok(None) + } + + #[allow(clippy::type_complexity)] + fn put_blocks( + &mut self, + _from_state: &ChainState, + _blocks: Vec>, + ) -> Result<(), Self::Error> { + Ok(()) + } + + fn update_chain_tip(&mut self, _tip_height: BlockHeight) -> Result<(), Self::Error> { + Ok(()) + } + + fn store_decrypted_tx( + &mut self, + _received_tx: DecryptedTransaction, + ) -> Result<(), Self::Error> { + Ok(()) + } + + fn store_transactions_to_be_sent( + &mut self, + _transactions: &[SentTransaction], + ) -> Result<(), Self::Error> { + Ok(()) + } + + fn truncate_to_height( + &mut self, + _block_height: BlockHeight, + ) -> Result { + Err(()) + } + + /// Adds a transparent UTXO received by the wallet to the data store. + fn put_received_transparent_utxo( + &mut self, + _output: &WalletTransparentOutput, + ) -> Result { + Ok(0) + } + + #[cfg(feature = "transparent-inputs")] + fn reserve_next_n_ephemeral_addresses( + &mut self, + _account_id: Self::AccountId, + _n: usize, + ) -> Result, Self::Error> { + Err(()) + } + + fn set_transaction_status( + &mut self, + _txid: TxId, + _status: TransactionStatus, + ) -> Result<(), Self::Error> { + Ok(()) + } +} + +impl WalletCommitmentTrees for MockWalletDb { + type Error = Infallible; + type SaplingShardStore<'a> = MemoryShardStore<::sapling::Node, BlockHeight>; + + fn with_sapling_tree_mut(&mut self, mut callback: F) -> Result + where + for<'a> F: FnMut( + &'a mut ShardTree< + Self::SaplingShardStore<'a>, + { ::sapling::NOTE_COMMITMENT_TREE_DEPTH }, + SAPLING_SHARD_HEIGHT, + >, + ) -> Result, + E: From>, + { + callback(&mut self.sapling_tree) + } + + fn put_sapling_subtree_roots( + &mut self, + start_index: u64, + roots: &[CommitmentTreeRoot<::sapling::Node>], + ) -> Result<(), ShardTreeError> { + self.with_sapling_tree_mut(|t| { + for (root, i) in roots.iter().zip(0u64..) { + let root_addr = incrementalmerkletree::Address::from_parts( + SAPLING_SHARD_HEIGHT.into(), + start_index + i, + ); + t.insert(root_addr, *root.root_hash())?; + } + Ok::<_, ShardTreeError>(()) + })?; + + Ok(()) + } + + #[cfg(feature = "orchard")] + type OrchardShardStore<'a> = MemoryShardStore<::orchard::tree::MerkleHashOrchard, BlockHeight>; + + #[cfg(feature = "orchard")] + fn with_orchard_tree_mut(&mut self, mut callback: F) -> Result + where + for<'a> F: FnMut( + &'a mut ShardTree< + Self::OrchardShardStore<'a>, + { ORCHARD_SHARD_HEIGHT * 2 }, + ORCHARD_SHARD_HEIGHT, + >, + ) -> Result, + E: From>, + { + callback(&mut self.orchard_tree) + } + + /// Adds a sequence of note commitment tree subtree roots to the data store. + #[cfg(feature = "orchard")] + fn put_orchard_subtree_roots( + &mut self, + start_index: u64, + roots: &[CommitmentTreeRoot<::orchard::tree::MerkleHashOrchard>], + ) -> Result<(), ShardTreeError> { + self.with_orchard_tree_mut(|t| { + for (root, i) in roots.iter().zip(0u64..) { + let root_addr = incrementalmerkletree::Address::from_parts( + ORCHARD_SHARD_HEIGHT.into(), + start_index + i, + ); + t.insert(root_addr, *root.root_hash())?; + } + Ok::<_, ShardTreeError>(()) + })?; + + Ok(()) + } +} diff --git a/zcash_client_backend/src/data_api/testing/orchard.rs b/zcash_client_backend/src/data_api/testing/orchard.rs new file mode 100644 index 0000000000..d07f14ba4e --- /dev/null +++ b/zcash_client_backend/src/data_api/testing/orchard.rs @@ -0,0 +1,208 @@ +use std::hash::Hash; + +use ::orchard::{ + keys::{FullViewingKey, SpendingKey}, + note_encryption::OrchardDomain, + tree::MerkleHashOrchard, +}; +use incrementalmerkletree::{Hashable, Level}; +use shardtree::error::ShardTreeError; + +use zcash_keys::{ + address::{Address, UnifiedAddress}, + keys::UnifiedSpendingKey, +}; +use zcash_note_encryption::try_output_recovery_with_ovk; +use zcash_primitives::transaction::Transaction; +use zcash_protocol::{ + consensus::{self, BlockHeight}, + memo::MemoBytes, + value::Zatoshis, + ShieldedProtocol, +}; + +use crate::{ + data_api::{ + chain::{CommitmentTreeRoot, ScanSummary}, + testing::{pool::ShieldedPoolTester, TestState}, + DecryptedTransaction, InputSource, WalletCommitmentTrees, WalletSummary, WalletTest, + }, + wallet::{Note, ReceivedNote}, +}; + +/// Type for running pool-agnostic tests on the Orchard pool. +pub struct OrchardPoolTester; +impl ShieldedPoolTester for OrchardPoolTester { + const SHIELDED_PROTOCOL: ShieldedProtocol = ShieldedProtocol::Orchard; + // const MERKLE_TREE_DEPTH: u8 = {orchard::NOTE_COMMITMENT_TREE_DEPTH as u8}; + + type Sk = SpendingKey; + type Fvk = FullViewingKey; + type MerkleTreeHash = MerkleHashOrchard; + type Note = orchard::note::Note; + + fn test_account_fvk( + st: &TestState, + ) -> Self::Fvk { + st.test_account_orchard().unwrap().clone() + } + + fn usk_to_sk(usk: &UnifiedSpendingKey) -> &Self::Sk { + usk.orchard() + } + + fn sk(seed: &[u8]) -> Self::Sk { + let mut account = zip32::AccountId::ZERO; + loop { + if let Ok(sk) = SpendingKey::from_zip32_seed(seed, 1, account) { + break sk; + } + account = account.next().unwrap(); + } + } + + fn sk_to_fvk(sk: &Self::Sk) -> Self::Fvk { + sk.into() + } + + fn sk_default_address(sk: &Self::Sk) -> Address { + Self::fvk_default_address(&Self::sk_to_fvk(sk)) + } + + fn fvk_default_address(fvk: &Self::Fvk) -> Address { + UnifiedAddress::from_receivers( + Some(fvk.address_at(0u32, zip32::Scope::External)), + None, + None, + ) + .unwrap() + .into() + } + + fn fvks_equal(a: &Self::Fvk, b: &Self::Fvk) -> bool { + a == b + } + + fn empty_tree_leaf() -> Self::MerkleTreeHash { + MerkleHashOrchard::empty_leaf() + } + + fn empty_tree_root(level: Level) -> Self::MerkleTreeHash { + MerkleHashOrchard::empty_root(level) + } + + fn put_subtree_roots( + st: &mut TestState, + start_index: u64, + roots: &[CommitmentTreeRoot], + ) -> Result<(), ShardTreeError<::Error>> { + st.wallet_mut() + .put_orchard_subtree_roots(start_index, roots) + } + + fn next_subtree_index(s: &WalletSummary) -> u64 { + s.next_orchard_subtree_index() + } + + fn select_spendable_notes( + st: &TestState, + account: ::AccountId, + target_value: Zatoshis, + anchor_height: BlockHeight, + exclude: &[DbT::NoteRef], + ) -> Result>, ::Error> { + st.wallet() + .select_spendable_notes( + account, + target_value, + &[ShieldedProtocol::Orchard], + anchor_height, + exclude, + ) + .map(|n| n.take_orchard()) + } + + fn decrypted_pool_outputs_count(d_tx: &DecryptedTransaction<'_, A>) -> usize { + d_tx.orchard_outputs().len() + } + + fn with_decrypted_pool_memos( + d_tx: &DecryptedTransaction<'_, A>, + mut f: impl FnMut(&MemoBytes), + ) { + for output in d_tx.orchard_outputs() { + f(output.memo()); + } + } + + fn try_output_recovery( + _params: &P, + _: BlockHeight, + tx: &Transaction, + fvk: &Self::Fvk, + ) -> Option<(Note, Address, MemoBytes)> { + for action in tx.orchard_bundle().unwrap().actions() { + // Find the output that decrypts with the external OVK + let result = try_output_recovery_with_ovk( + &OrchardDomain::for_action(action), + &fvk.to_ovk(zip32::Scope::External), + action, + action.cv_net(), + &action.encrypted_note().out_ciphertext, + ); + + if result.is_some() { + return result.map(|(note, addr, memo)| { + ( + Note::Orchard(note), + UnifiedAddress::from_receivers(Some(addr), None, None) + .unwrap() + .into(), + MemoBytes::from_bytes(&memo).expect("correct length"), + ) + }); + } + } + + None + } + + fn received_note_count(summary: &ScanSummary) -> usize { + summary.received_orchard_note_count() + } + + #[cfg(feature = "pczt")] + fn add_proof_generation_keys( + pczt: pczt::Pczt, + _: &UnifiedSpendingKey, + ) -> Result { + // No-op; Orchard doesn't have proof generation keys. + Ok(pczt) + } + + #[cfg(feature = "pczt")] + fn apply_signatures_to_pczt( + signer: &mut pczt::roles::signer::Signer, + usk: &UnifiedSpendingKey, + ) -> Result<(), pczt::roles::signer::Error> { + let sk = Self::usk_to_sk(usk); + let ask = orchard::keys::SpendAuthorizingKey::from(sk); + + // Figuring out which one is for us is hard. Let's just try signing all of them! + for index in 0.. { + match signer.sign_orchard(index, &ask) { + // Loop termination. + Err(pczt::roles::signer::Error::InvalidIndex) => break, + // Ignore any errors due to using the wrong key. + Ok(()) + | Err(pczt::roles::signer::Error::OrchardSign( + orchard::pczt::SignerError::WrongSpendAuthorizingKey, + )) => Ok(()), + // Raise any unexpected errors. + Err(e) => Err(e), + }?; + } + + Ok(()) + } +} diff --git a/zcash_client_backend/src/data_api/testing/pool.rs b/zcash_client_backend/src/data_api/testing/pool.rs new file mode 100644 index 0000000000..a58e73b78f --- /dev/null +++ b/zcash_client_backend/src/data_api/testing/pool.rs @@ -0,0 +1,3210 @@ +use std::{ + cmp::Eq, + convert::Infallible, + hash::Hash, + num::{NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize}, +}; + +use assert_matches::assert_matches; +use incrementalmerkletree::{frontier::Frontier, Level, Position}; +use rand::{Rng, RngCore}; +use secrecy::Secret; +use shardtree::error::ShardTreeError; + +use ::transparent::address::TransparentAddress; +use zcash_keys::{address::Address, keys::UnifiedSpendingKey}; +use zcash_primitives::{ + block::BlockHash, + transaction::{ + fees::zip317::{FeeRule as Zip317FeeRule, MARGINAL_FEE, MINIMUM_FEE}, + Transaction, + }, +}; +use zcash_protocol::{ + consensus::{self, BlockHeight, NetworkUpgrade, Parameters}, + local_consensus::LocalNetwork, + memo::{Memo, MemoBytes}, + value::Zatoshis, + ShieldedProtocol, +}; +use zip32::Scope; +use zip321::{Payment, TransactionRequest}; + +use crate::{ + data_api::{ + self, + chain::{self, ChainState, CommitmentTreeRoot, ScanSummary}, + error::Error, + testing::{ + single_output_change_strategy, AddressType, FakeCompactOutput, InitialChainState, + TestBuilder, + }, + wallet::{ + decrypt_and_store_transaction, input_selection::GreedyInputSelector, TransferErrT, + }, + Account as _, AccountBirthday, BoundedU8, DecryptedTransaction, InputSource, NoteFilter, + Ratio, WalletCommitmentTrees, WalletRead, WalletSummary, WalletTest, WalletWrite, + }, + decrypt_transaction, + fees::{ + self, + standard::{self, SingleOutputChangeStrategy}, + DustOutputPolicy, SplitPolicy, StandardFeeRule, + }, + scanning::ScanError, + wallet::{Note, NoteId, OvkPolicy, ReceivedNote}, +}; + +use super::{DataStoreFactory, Reset, TestCache, TestFvk, TestState}; + +#[cfg(feature = "transparent-inputs")] +use { + crate::{ + data_api::{TransactionDataRequest, TransactionStatus}, + fees::ChangeValue, + proposal::{Proposal, ProposalError, StepOutput, StepOutputIndex}, + wallet::{TransparentAddressMetadata, WalletTransparentOutput}, + }, + ::transparent::{ + bundle::{OutPoint, TxOut}, + keys::{NonHardenedChildIndex, TransparentKeyScope}, + }, + nonempty::NonEmpty, + rand_core::OsRng, + std::{collections::HashSet, str::FromStr}, + zcash_primitives::transaction::{ + builder::{BuildConfig, Builder}, + fees::zip317, + }, + zcash_proofs::prover::LocalTxProver, + zcash_protocol::value::ZatBalance, +}; + +#[cfg(feature = "orchard")] +use zcash_protocol::PoolType; + +#[cfg(feature = "pczt")] +use pczt::roles::{prover::Prover, signer::Signer}; + +/// Trait that exposes the pool-specific types and operations necessary to run the +/// single-shielded-pool tests on a given pool. +/// +/// You should not need to implement this yourself; instead use [`SaplingPoolTester`] or +/// [`OrchardPoolTester`] as appropriate. +/// +/// [`SaplingPoolTester`]: super::sapling::SaplingPoolTester +#[cfg_attr( + feature = "orchard", + doc = "[`OrchardPoolTester`]: super::orchard::OrchardPoolTester" +)] +#[cfg_attr( + not(feature = "orchard"), + doc = "[`OrchardPoolTester`]: https://github.com/zcash/librustzcash/blob/0777cbc2def6ba6b99f96333eaf96c314c1f3a37/zcash_client_backend/src/data_api/testing/orchard.rs#L33" +)] +pub trait ShieldedPoolTester { + const SHIELDED_PROTOCOL: ShieldedProtocol; + + type Sk; + type Fvk: TestFvk; + type MerkleTreeHash; + type Note; + + fn test_account_fvk( + st: &TestState, + ) -> Self::Fvk; + fn usk_to_sk(usk: &UnifiedSpendingKey) -> &Self::Sk; + fn sk(seed: &[u8]) -> Self::Sk; + fn sk_to_fvk(sk: &Self::Sk) -> Self::Fvk; + fn sk_default_address(sk: &Self::Sk) -> Address; + fn fvk_default_address(fvk: &Self::Fvk) -> Address; + fn fvks_equal(a: &Self::Fvk, b: &Self::Fvk) -> bool; + + fn random_fvk(mut rng: impl RngCore) -> Self::Fvk { + let sk = { + let mut sk_bytes = vec![0; 32]; + rng.fill_bytes(&mut sk_bytes); + Self::sk(&sk_bytes) + }; + + Self::sk_to_fvk(&sk) + } + fn random_address(rng: impl RngCore) -> Address { + Self::fvk_default_address(&Self::random_fvk(rng)) + } + + fn empty_tree_leaf() -> Self::MerkleTreeHash; + fn empty_tree_root(level: Level) -> Self::MerkleTreeHash; + + fn put_subtree_roots( + st: &mut TestState, + start_index: u64, + roots: &[CommitmentTreeRoot], + ) -> Result<(), ShardTreeError<::Error>>; + + fn next_subtree_index(s: &WalletSummary) -> u64; + + #[allow(clippy::type_complexity)] + fn select_spendable_notes( + st: &TestState, + account: ::AccountId, + target_value: Zatoshis, + anchor_height: BlockHeight, + exclude: &[DbT::NoteRef], + ) -> Result>, ::Error>; + + fn decrypted_pool_outputs_count(d_tx: &DecryptedTransaction<'_, A>) -> usize; + + fn with_decrypted_pool_memos(d_tx: &DecryptedTransaction<'_, A>, f: impl FnMut(&MemoBytes)); + + fn try_output_recovery( + params: &P, + height: BlockHeight, + tx: &Transaction, + fvk: &Self::Fvk, + ) -> Option<(Note, Address, MemoBytes)>; + + fn received_note_count(summary: &ScanSummary) -> usize; + + #[cfg(feature = "pczt")] + fn add_proof_generation_keys( + pczt: pczt::Pczt, + usk: &UnifiedSpendingKey, + ) -> Result; + + #[cfg(feature = "pczt")] + fn apply_signatures_to_pczt( + signer: &mut Signer, + usk: &UnifiedSpendingKey, + ) -> Result<(), pczt::roles::signer::Error>; +} + +/// Tests sending funds within the given shielded pool in a single transaction. +/// +/// The test: +/// - Adds funds to the wallet in a single note. +/// - Checks that the wallet balances are correct. +/// - Constructs a request to spend part of that balance to an external address in the +/// same pool. +/// - Builds the transaction. +/// - Checks that the transaction was stored, and that the outputs are decryptable and +/// have the expected details. +pub fn send_single_step_proposed_transfer( + dsf: impl DataStoreFactory, + cache: impl TestCache, +) { + let mut st = TestBuilder::new() + .with_data_store_factory(dsf) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account = st.test_account().cloned().unwrap(); + let dfvk = T::test_account_fvk(&st); + + // Add funds to the wallet in a single note + let value = Zatoshis::const_from_u64(60000); + let (h, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); + st.scan_cached_blocks(h, 1); + + // Spendable balance matches total balance + assert_eq!(st.get_total_balance(account.id()), value); + assert_eq!(st.get_spendable_balance(account.id(), 1), value); + + assert_eq!( + st.wallet() + .block_max_scanned() + .unwrap() + .unwrap() + .block_height(), + h + ); + + let to_extsk = T::sk(&[0xf5; 32]); + let to: Address = T::sk_default_address(&to_extsk); + let request = zip321::TransactionRequest::new(vec![Payment::without_memo( + to.to_zcash_address(st.network()), + Zatoshis::const_from_u64(10000), + )]) + .unwrap(); + + let fee_rule = StandardFeeRule::Zip317; + + let change_memo = "Test change memo".parse::().unwrap(); + let change_strategy = standard::SingleOutputChangeStrategy::new( + fee_rule, + Some(change_memo.clone().into()), + T::SHIELDED_PROTOCOL, + DustOutputPolicy::default(), + ); + let input_selector = GreedyInputSelector::new(); + + let proposal = st + .propose_transfer( + account.id(), + &input_selector, + &change_strategy, + request, + NonZeroU32::new(1).unwrap(), + ) + .unwrap(); + + let create_proposed_result = st.create_proposed_transactions::( + account.usk(), + OvkPolicy::Sender, + &proposal, + ); + assert_matches!(&create_proposed_result, Ok(txids) if txids.len() == 1); + + let sent_tx_id = create_proposed_result.unwrap()[0]; + + // Verify that the sent transaction was stored and that we can decrypt the memos + let tx = st + .wallet() + .get_transaction(sent_tx_id) + .unwrap() + .expect("Created transaction was stored."); + let ufvks = [(account.id(), account.usk().to_unified_full_viewing_key())] + .into_iter() + .collect(); + let d_tx = decrypt_transaction(st.network(), h + 1, &tx, &ufvks); + assert_eq!(T::decrypted_pool_outputs_count(&d_tx), 2); + + let mut found_tx_change_memo = false; + let mut found_tx_empty_memo = false; + T::with_decrypted_pool_memos(&d_tx, |memo| { + if Memo::try_from(memo).unwrap() == change_memo { + found_tx_change_memo = true + } + if Memo::try_from(memo).unwrap() == Memo::Empty { + found_tx_empty_memo = true + } + }); + assert!(found_tx_change_memo); + assert!(found_tx_empty_memo); + + // Verify that the stored sent notes match what we're expecting + let sent_note_ids = st + .wallet() + .get_sent_note_ids(&sent_tx_id, T::SHIELDED_PROTOCOL) + .unwrap(); + assert_eq!(sent_note_ids.len(), 2); + + // The sent memo should be the empty memo for the sent output, and the + // change output's memo should be as specified. + let mut found_sent_change_memo = false; + let mut found_sent_empty_memo = false; + for sent_note_id in sent_note_ids { + match st + .wallet() + .get_memo(sent_note_id) + .expect("Note id is valid") + .as_ref() + { + Some(m) if m == &change_memo => { + found_sent_change_memo = true; + } + Some(m) if m == &Memo::Empty => { + found_sent_empty_memo = true; + } + Some(other) => panic!("Unexpected memo value: {:?}", other), + None => panic!("Memo should not be stored as NULL"), + } + } + assert!(found_sent_change_memo); + assert!(found_sent_empty_memo); + + // Check that querying for a nonexistent sent note returns None + assert_matches!( + st.wallet() + .get_memo(NoteId::new(sent_tx_id, T::SHIELDED_PROTOCOL, 12345)), + Ok(None) + ); + + let tx_history = st.wallet().get_tx_history().unwrap(); + assert_eq!(tx_history.len(), 2); + + let network = *st.network(); + assert_matches!( + decrypt_and_store_transaction(&network, st.wallet_mut(), &tx, None), + Ok(_) + ); +} + +pub fn send_with_multiple_change_outputs( + dsf: impl DataStoreFactory, + cache: impl TestCache, +) { + let mut st = TestBuilder::new() + .with_data_store_factory(dsf) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account = st.test_account().cloned().unwrap(); + let dfvk = T::test_account_fvk(&st); + + // Add funds to the wallet in a single note + let value = Zatoshis::const_from_u64(650_0000); + let (h, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); + st.scan_cached_blocks(h, 1); + + // Spendable balance matches total balance + assert_eq!(st.get_total_balance(account.id()), value); + assert_eq!(st.get_spendable_balance(account.id(), 1), value); + + assert_eq!( + st.wallet() + .block_max_scanned() + .unwrap() + .unwrap() + .block_height(), + h + ); + + let to_extsk = T::sk(&[0xf5; 32]); + let to: Address = T::sk_default_address(&to_extsk); + let request = zip321::TransactionRequest::new(vec![Payment::without_memo( + to.to_zcash_address(st.network()), + Zatoshis::const_from_u64(100_0000), + )]) + .unwrap(); + + let input_selector = GreedyInputSelector::new(); + let change_memo = "Test change memo".parse::().unwrap(); + let change_strategy = fees::zip317::MultiOutputChangeStrategy::new( + Zip317FeeRule::standard(), + Some(change_memo.clone().into()), + T::SHIELDED_PROTOCOL, + DustOutputPolicy::default(), + SplitPolicy::with_min_output_value( + NonZeroUsize::new(2).unwrap(), + Zatoshis::const_from_u64(100_0000), + ), + ); + + let proposal = st + .propose_transfer( + account.id(), + &input_selector, + &change_strategy, + request.clone(), + NonZeroU32::new(1).unwrap(), + ) + .unwrap(); + + let step = &proposal.steps().head; + assert_eq!(step.balance().proposed_change().len(), 2); + + let create_proposed_result = st.create_proposed_transactions::( + account.usk(), + OvkPolicy::Sender, + &proposal, + ); + assert_matches!(&create_proposed_result, Ok(txids) if txids.len() == 1); + + let sent_tx_id = create_proposed_result.unwrap()[0]; + + // Verify that the sent transaction was stored and that we can decrypt the memos + let tx = st + .wallet() + .get_transaction(sent_tx_id) + .unwrap() + .expect("Created transaction was stored."); + let ufvks = [(account.id(), account.usk().to_unified_full_viewing_key())] + .into_iter() + .collect(); + let d_tx = decrypt_transaction(st.network(), h + 1, &tx, &ufvks); + assert_eq!(T::decrypted_pool_outputs_count(&d_tx), 3); + + let mut found_tx_change_memo = false; + let mut found_tx_empty_memo = false; + T::with_decrypted_pool_memos(&d_tx, |memo| { + if Memo::try_from(memo).unwrap() == change_memo { + found_tx_change_memo = true + } + if Memo::try_from(memo).unwrap() == Memo::Empty { + found_tx_empty_memo = true + } + }); + assert!(found_tx_change_memo); + assert!(found_tx_empty_memo); + + // Verify that the stored sent notes match what we're expecting + let sent_note_ids = st + .wallet() + .get_sent_note_ids(&sent_tx_id, T::SHIELDED_PROTOCOL) + .unwrap(); + assert_eq!(sent_note_ids.len(), 3); + + // The sent memo should be the empty memo for the sent output, and each + // change output's memo should be as specified. + let mut change_memo_count = 0; + let mut found_sent_empty_memo = false; + for sent_note_id in sent_note_ids { + match st + .wallet() + .get_memo(sent_note_id) + .expect("Note id is valid") + .as_ref() + { + Some(m) if m == &change_memo => { + change_memo_count += 1; + } + Some(m) if m == &Memo::Empty => { + found_sent_empty_memo = true; + } + Some(other) => panic!("Unexpected memo value: {:?}", other), + None => panic!("Memo should not be stored as NULL"), + } + } + assert_eq!(change_memo_count, 2); + assert!(found_sent_empty_memo); + + let tx_history = st.wallet().get_tx_history().unwrap(); + assert_eq!(tx_history.len(), 2); + + let network = *st.network(); + assert_matches!( + decrypt_and_store_transaction(&network, st.wallet_mut(), &tx, None), + Ok(_) + ); + + let (h, _) = st.generate_next_block_including(sent_tx_id); + st.scan_cached_blocks(h, 1); + + // Now, create another proposal with more outputs requested. We have two change notes; + // we'll spend one of them, and then we'll generate 7 splits. + let change_strategy = fees::zip317::MultiOutputChangeStrategy::new( + Zip317FeeRule::standard(), + Some(change_memo.into()), + T::SHIELDED_PROTOCOL, + DustOutputPolicy::default(), + SplitPolicy::with_min_output_value( + NonZeroUsize::new(8).unwrap(), + Zatoshis::const_from_u64(10_0000), + ), + ); + + let proposal = st + .propose_transfer( + account.id(), + &input_selector, + &change_strategy, + request, + NonZeroU32::new(1).unwrap(), + ) + .unwrap(); + + let step = &proposal.steps().head; + assert_eq!(step.balance().proposed_change().len(), 7); +} + +#[cfg(feature = "transparent-inputs")] +pub fn send_multi_step_proposed_transfer( + ds_factory: DSF, + cache: impl TestCache, + is_reached_gap_limit: impl Fn(&::Error, DSF::AccountId, u32) -> bool, +) where + DSF: DataStoreFactory, + ::AccountId: std::fmt::Debug, +{ + use ::transparent::builder::TransparentSigningSet; + + use crate::data_api::{OutputOfSentTx, GAP_LIMIT}; + + let mut st = TestBuilder::new() + .with_data_store_factory(ds_factory) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account = st.test_account().cloned().unwrap(); + let account_id = account.id(); + let (default_addr, default_index) = account.usk().default_transparent_address(); + let dfvk = T::test_account_fvk(&st); + + let add_funds = |st: &mut TestState<_, DSF::DataStore, _>, value| { + let (h, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); + st.scan_cached_blocks(h, 1); + + assert_eq!( + st.wallet() + .block_max_scanned() + .unwrap() + .unwrap() + .block_height(), + h + ); + assert_eq!(st.get_spendable_balance(account_id, 1), value); + h + }; + + let value = Zatoshis::const_from_u64(100000); + let transfer_amount = Zatoshis::const_from_u64(50000); + + let run_test = |st: &mut TestState<_, DSF::DataStore, _>, expected_index| { + // Add funds to the wallet. + add_funds(st, value); + + let expected_step0_fee = (zip317::MARGINAL_FEE * 3u64).unwrap(); + let expected_step1_fee = zip317::MINIMUM_FEE; + let expected_ephemeral = (transfer_amount + expected_step1_fee).unwrap(); + let expected_step0_change = + (value - expected_ephemeral - expected_step0_fee).expect("sufficient funds"); + assert!(expected_step0_change.is_positive()); + + // Generate a ZIP 320 proposal, sending to the wallet's default transparent address + // expressed as a TEX address. + let tex_addr = match default_addr { + TransparentAddress::PublicKeyHash(data) => Address::Tex(data), + _ => unreachable!(), + }; + let change_memo = Some(Memo::from_str("change").expect("valid memo").encode()); + + // We use `st.propose_standard_transfer` here in order to also test round-trip + // serialization of the proposal. + let proposal = st + .propose_standard_transfer::( + account_id, + StandardFeeRule::Zip317, + NonZeroU32::new(1).unwrap(), + &tex_addr, + transfer_amount, + None, + change_memo.clone(), + T::SHIELDED_PROTOCOL, + ) + .unwrap(); + + let steps: Vec<_> = proposal.steps().iter().cloned().collect(); + assert_eq!(steps.len(), 2); + + assert_eq!(steps[0].balance().fee_required(), expected_step0_fee); + assert_eq!(steps[1].balance().fee_required(), expected_step1_fee); + assert_eq!( + steps[0].balance().proposed_change(), + [ + ChangeValue::shielded(T::SHIELDED_PROTOCOL, expected_step0_change, change_memo), + ChangeValue::ephemeral_transparent(expected_ephemeral), + ] + ); + assert_eq!(steps[1].balance().proposed_change(), []); + + let create_proposed_result = st.create_proposed_transactions::( + account.usk(), + OvkPolicy::Sender, + &proposal, + ); + assert_matches!(&create_proposed_result, Ok(txids) if txids.len() == 2); + let txids = create_proposed_result.unwrap(); + + // Check that there are sent outputs with the correct values. + let confirmed_sent: Vec> = txids + .iter() + .map(|sent_txid| st.wallet().get_sent_outputs(sent_txid).unwrap()) + .collect(); + + // Verify that a status request has been generated for the second transaction of + // the ZIP 320 pair. + let tx_data_requests = st.wallet().transaction_data_requests().unwrap(); + assert!(tx_data_requests.contains(&TransactionDataRequest::GetStatus(*txids.last()))); + + assert!(expected_step0_change < expected_ephemeral); + assert_eq!(confirmed_sent.len(), 2); + assert_eq!(confirmed_sent[0].len(), 2); + assert_eq!(confirmed_sent[0][0].value, expected_step0_change); + let OutputOfSentTx { + value: ephemeral_v, + external_recipient: to_addr, + ephemeral_address, + } = confirmed_sent[0][1].clone(); + assert_eq!(ephemeral_v, expected_ephemeral); + assert!(to_addr.is_some()); + assert_eq!( + ephemeral_address, + to_addr.map(|addr| (addr, expected_index)), + ); + + assert_eq!(confirmed_sent[1].len(), 1); + assert_matches!( + &confirmed_sent[1][0], + OutputOfSentTx { value: sent_v, external_recipient: sent_to_addr, ephemeral_address: None } + if sent_v == &transfer_amount && sent_to_addr == &Some(tex_addr)); + + // Check that the transaction history matches what we expect. + let tx_history = st.wallet().get_tx_history().unwrap(); + + let tx_0 = tx_history + .iter() + .find(|tx| tx.txid() == *txids.first()) + .unwrap(); + let tx_1 = tx_history + .iter() + .find(|tx| tx.txid() == *txids.last()) + .unwrap(); + + assert_eq!(tx_0.account_id(), &account_id); + assert!(!tx_0.expired_unmined()); + assert_eq!(tx_0.has_change(), expected_step0_change.is_positive()); + assert!(!tx_0.is_shielding()); + assert_eq!( + tx_0.account_value_delta(), + -ZatBalance::from(expected_step0_fee), + ); + + assert_eq!(tx_1.account_id(), &account_id); + assert!(!tx_1.expired_unmined()); + assert!(!tx_1.has_change()); + assert!(!tx_0.is_shielding()); + assert_eq!( + tx_1.account_value_delta(), + -ZatBalance::from(expected_ephemeral), + ); + + (ephemeral_address.unwrap().0, txids) + }; + + // Each transfer should use a different ephemeral address. + let (ephemeral0, txids0) = run_test(&mut st, 0); + let (ephemeral1, txids1) = run_test(&mut st, 1); + assert_ne!(ephemeral0, ephemeral1); + + let height = add_funds(&mut st, value); + + assert_matches!( + ephemeral0, + Address::Transparent(TransparentAddress::PublicKeyHash(_)) + ); + + // Attempting to pay to an ephemeral address should cause an error. + let proposal = st + .propose_standard_transfer::( + account_id, + StandardFeeRule::Zip317, + NonZeroU32::new(1).unwrap(), + &ephemeral0, + transfer_amount, + None, + None, + T::SHIELDED_PROTOCOL, + ) + .unwrap(); + + let create_proposed_result = st.create_proposed_transactions::( + account.usk(), + OvkPolicy::Sender, + &proposal, + ); + assert_matches!( + &create_proposed_result, + Err(Error::PaysEphemeralTransparentAddress(address_str)) if address_str == &ephemeral0.encode(st.network())); + + // Simulate another wallet sending to an ephemeral address with an index + // within the current gap limit. The `PaysEphemeralTransparentAddress` error + // prevents us from doing so straightforwardly, so we'll do it by building + // a transaction and calling `store_decrypted_tx` with it. + let known_addrs = st + .wallet() + .get_known_ephemeral_addresses(account_id, None) + .unwrap(); + assert_eq!(known_addrs.len(), (GAP_LIMIT as usize) + 2); + + // Check that the addresses are all distinct. + let known_set: HashSet<_> = known_addrs.iter().map(|(addr, _)| addr).collect(); + assert_eq!(known_set.len(), known_addrs.len()); + // Check that the metadata is as expected. + for (i, (_, meta)) in known_addrs.iter().enumerate() { + assert_eq!( + meta, + &TransparentAddressMetadata::new( + TransparentKeyScope::EPHEMERAL, + NonHardenedChildIndex::from_index(i.try_into().unwrap()).unwrap() + ) + ); + } + + let mut builder = Builder::new( + *st.network(), + height + 1, + BuildConfig::Standard { + sapling_anchor: None, + orchard_anchor: None, + }, + ); + let mut transparent_signing_set = TransparentSigningSet::new(); + let (colliding_addr, _) = &known_addrs[10]; + let utxo_value = (value - zip317::MINIMUM_FEE).unwrap(); + assert_matches!( + builder.add_transparent_output(colliding_addr, utxo_value), + Ok(_) + ); + let sk = account + .usk() + .transparent() + .derive_secret_key(Scope::External.into(), default_index) + .unwrap(); + let pubkey = transparent_signing_set.add_key(sk); + let outpoint = OutPoint::fake(); + let txout = TxOut { + script_pubkey: default_addr.script(), + value, + }; + // Add the fake input to our UTXO set so that we can ensure we recognize the outpoint. + st.wallet_mut() + .put_received_transparent_utxo( + &WalletTransparentOutput::from_parts(outpoint.clone(), txout.clone(), None).unwrap(), + ) + .unwrap(); + + assert_matches!( + builder.add_transparent_input(pubkey, outpoint, txout), + Ok(_) + ); + let test_prover = LocalTxProver::bundled(); + let build_result = builder + .build( + &transparent_signing_set, + &[], + &[], + OsRng, + &test_prover, + &test_prover, + &zip317::FeeRule::standard(), + ) + .unwrap(); + let txid = build_result.transaction().txid(); + st.wallet_mut() + .store_decrypted_tx(DecryptedTransaction::new( + None, + build_result.transaction(), + vec![], + #[cfg(feature = "orchard")] + vec![], + )) + .unwrap(); + + // Verify that storing the fully transparent transaction causes a transaction + // status request to be generated. + let tx_data_requests = st.wallet().transaction_data_requests().unwrap(); + assert!(tx_data_requests.contains(&TransactionDataRequest::GetStatus(txid))); + + // We call get_transparent_output with `allow_unspendable = true` to verify + // storage because the decrypted transaction has not yet been mined. + let utxo = st + .wallet() + .get_transparent_output(&OutPoint::new(txid.into(), 0), true) + .unwrap(); + assert_matches!(utxo, Some(v) if v.value() == utxo_value); + + // That should have advanced the start of the gap to index 11. + let new_known_addrs = st + .wallet() + .get_known_ephemeral_addresses(account_id, None) + .unwrap(); + assert_eq!(new_known_addrs.len(), (GAP_LIMIT as usize) + 11); + assert!(new_known_addrs.starts_with(&known_addrs)); + + let reservation_should_succeed = |st: &mut TestState<_, DSF::DataStore, _>, n| { + let reserved = st + .wallet_mut() + .reserve_next_n_ephemeral_addresses(account_id, n) + .unwrap(); + assert_eq!(reserved.len(), n); + reserved + }; + let reservation_should_fail = + |st: &mut TestState<_, DSF::DataStore, _>, n, expected_bad_index| { + assert_matches!(st + .wallet_mut() + .reserve_next_n_ephemeral_addresses(account_id, n), + Err(e) if is_reached_gap_limit(&e, account_id, expected_bad_index)); + }; + + let next_reserved = reservation_should_succeed(&mut st, 1); + assert_eq!(next_reserved[0], known_addrs[11]); + + // Calling `reserve_next_n_ephemeral_addresses(account_id, 1)` will have advanced + // the start of the gap to index 12. This also tests the `index_range` parameter. + let newer_known_addrs = st + .wallet() + .get_known_ephemeral_addresses( + account_id, + Some( + NonHardenedChildIndex::from_index(5).unwrap() + ..NonHardenedChildIndex::from_index(100).unwrap(), + ), + ) + .unwrap(); + assert_eq!(newer_known_addrs.len(), (GAP_LIMIT as usize) + 12 - 5); + assert!(newer_known_addrs.starts_with(&new_known_addrs[5..])); + + // None of the five transactions created above (two from each proposal and the + // one built manually) have been mined yet. So, the range of address indices + // that are safe to reserve is still 0..20, and we have already reserved 12 + // addresses, so trying to reserve another 9 should fail. + reservation_should_fail(&mut st, 9, 20); + reservation_should_succeed(&mut st, 8); + reservation_should_fail(&mut st, 1, 20); + + // Now mine the transaction with the ephemeral output at index 1. + // We already reserved 20 addresses, so this should allow 2 more (..22). + // It does not matter that the transaction with ephemeral output at index 0 + // remains unmined. + let (h, _) = st.generate_next_block_including(txids1.head); + st.scan_cached_blocks(h, 1); + reservation_should_succeed(&mut st, 2); + reservation_should_fail(&mut st, 1, 22); + + // Mining the transaction with the ephemeral output at index 0 at this point + // should make no difference. + let (h, _) = st.generate_next_block_including(txids0.head); + st.scan_cached_blocks(h, 1); + reservation_should_fail(&mut st, 1, 22); + + // Now mine the transaction with the ephemeral output at index 10. + let tx = build_result.transaction(); + let tx_index = 1; + let (h, _) = st.generate_next_block_from_tx(tx_index, tx); + st.scan_cached_blocks(h, 1); + + // The above `scan_cached_blocks` does not detect `tx` as interesting to the + // wallet. If a transaction is in the database with a null `mined_height`, + // as in this case, its `mined_height` will remain null unless either + // `put_tx_meta` or `set_transaction_status` is called on it. The former + // is normally called internally via `put_blocks` as a result of scanning, + // but not for the case of a fully transparent transaction. The latter is + // called by the wallet implementation in response to processing the + // `transaction_data_requests` queue. + + // The reservation should fail because `tx` is not yet seen as mined. + reservation_should_fail(&mut st, 1, 22); + + // Simulate the wallet processing the `transaction_data_requests` queue. + let tx_data_requests = st.wallet().transaction_data_requests().unwrap(); + assert!(tx_data_requests.contains(&TransactionDataRequest::GetStatus(tx.txid()))); + + // Respond to the GetStatus request. + st.wallet_mut() + .set_transaction_status(tx.txid(), TransactionStatus::Mined(h)) + .unwrap(); + + // We already reserved 22 addresses, so mining the transaction with the + // ephemeral output at index 10 should allow 9 more (..31). + reservation_should_succeed(&mut st, 9); + reservation_should_fail(&mut st, 1, 31); + + let newest_known_addrs = st + .wallet() + .get_known_ephemeral_addresses(account_id, None) + .unwrap(); + assert_eq!(newest_known_addrs.len(), (GAP_LIMIT as usize) + 31); + assert!(newest_known_addrs.starts_with(&known_addrs)); + assert!(newest_known_addrs[5..].starts_with(&newer_known_addrs)); +} + +#[cfg(feature = "transparent-inputs")] +pub fn proposal_fails_if_not_all_ephemeral_outputs_consumed( + ds_factory: DSF, + cache: impl TestCache, +) where + DSF: DataStoreFactory, +{ + let mut st = TestBuilder::new() + .with_data_store_factory(ds_factory) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account = st.test_account().cloned().unwrap(); + let account_id = account.id(); + let dfvk = T::test_account_fvk(&st); + + let add_funds = |st: &mut TestState<_, DSF::DataStore, _>, value| { + let (h, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); + st.scan_cached_blocks(h, 1); + + assert_eq!( + st.wallet() + .block_max_scanned() + .unwrap() + .unwrap() + .block_height(), + h + ); + assert_eq!(st.get_spendable_balance(account_id, 1), value); + }; + + let value = Zatoshis::const_from_u64(100000); + let transfer_amount = Zatoshis::const_from_u64(50000); + + // Add funds to the wallet. + add_funds(&mut st, value); + + // Generate a ZIP 320 proposal, sending to the wallet's default transparent address + // expressed as a TEX address. + let tex_addr = match account.usk().default_transparent_address().0 { + TransparentAddress::PublicKeyHash(data) => Address::Tex(data), + _ => unreachable!(), + }; + + let proposal = st + .propose_standard_transfer::( + account_id, + StandardFeeRule::Zip317, + NonZeroU32::new(1).unwrap(), + &tex_addr, + transfer_amount, + None, + None, + T::SHIELDED_PROTOCOL, + ) + .unwrap(); + + // This is somewhat redundant with `send_multi_step_proposed_transfer`, + // but tests the case with no change memo and ensures we haven't messed + // up the test setup. + let create_proposed_result = st.create_proposed_transactions::( + account.usk(), + OvkPolicy::Sender, + &proposal, + ); + assert_matches!(create_proposed_result, Ok(_)); + + // Frobnicate the proposal to make it invalid because it does not consume + // the ephemeral output, by truncating it to the first step. + let frobbed_proposal = Proposal::multi_step( + *proposal.fee_rule(), + proposal.min_target_height(), + NonEmpty::singleton(proposal.steps().first().clone()), + ) + .unwrap(); + + let create_proposed_result = st.create_proposed_transactions::( + account.usk(), + OvkPolicy::Sender, + &frobbed_proposal, + ); + assert_matches!( + create_proposed_result, + Err(Error::Proposal(ProposalError::EphemeralOutputLeftUnspent(so))) + if so == StepOutput::new(0, StepOutputIndex::Change(1)) + ); +} + +pub fn create_to_address_fails_on_incorrect_usk( + ds_factory: DSF, +) { + let mut st = TestBuilder::new() + .with_data_store_factory(ds_factory) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + let dfvk = T::test_account_fvk(&st); + let to = T::fvk_default_address(&dfvk); + + // Create a USK that doesn't exist in the wallet + let acct1 = zip32::AccountId::try_from(1).unwrap(); + let usk1 = UnifiedSpendingKey::from_seed(st.network(), &[1u8; 32], acct1).unwrap(); + + let input_selector = GreedyInputSelector::::new(); + let change_strategy = + single_output_change_strategy(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL); + + let req = TransactionRequest::new(vec![Payment::without_memo( + to.to_zcash_address(st.network()), + Zatoshis::const_from_u64(1), + )]) + .unwrap(); + + // Attempting to spend with a USK that is not in the wallet results in an error + assert_matches!( + st.spend( + &input_selector, + &change_strategy, + &usk1, + req, + OvkPolicy::Sender, + NonZeroU32::new(1).unwrap(), + ), + Err(data_api::error::Error::KeyNotRecognized) + ); +} + +pub fn proposal_fails_with_no_blocks(ds_factory: DSF) +where + DSF: DataStoreFactory, + ::AccountId: std::fmt::Debug, +{ + let mut st = TestBuilder::new() + .with_data_store_factory(ds_factory) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account_id = st.test_account().unwrap().id(); + let dfvk = T::test_account_fvk(&st); + let to = T::fvk_default_address(&dfvk); + + // Wallet summary is not yet available + assert_eq!(st.get_wallet_summary(0), None); + + // We cannot do anything if we aren't synchronised + assert_matches!( + st.propose_standard_transfer::( + account_id, + StandardFeeRule::Zip317, + NonZeroU32::new(1).unwrap(), + &to, + Zatoshis::const_from_u64(1), + None, + None, + T::SHIELDED_PROTOCOL, + ), + Err(data_api::error::Error::ScanRequired) + ); +} + +pub fn spend_fails_on_unverified_notes( + ds_factory: impl DataStoreFactory, + cache: impl TestCache, +) { + let mut st = TestBuilder::new() + .with_data_store_factory(ds_factory) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account = st.test_account().cloned().unwrap(); + let account_id = account.id(); + let dfvk = T::test_account_fvk(&st); + + // Add funds to the wallet in a single note + let value = Zatoshis::const_from_u64(50000); + let (h1, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); + st.scan_cached_blocks(h1, 1); + + // Spendable balance matches total balance at 1 confirmation. + assert_eq!(st.get_total_balance(account_id), value); + assert_eq!(st.get_spendable_balance(account_id, 1), value); + + // Value is considered pending at 10 confirmations. + assert_eq!(st.get_pending_shielded_balance(account_id, 10), value); + assert_eq!(st.get_spendable_balance(account_id, 10), Zatoshis::ZERO); + + // If none of the wallet's accounts have a recover-until height, then there + // is no recovery phase for the wallet, and therefore the denominator in the + // resulting ratio (the number of notes in the recovery range) is zero. + let no_recovery = Some(Ratio::new(0, 0)); + + // Wallet is fully scanned + let summary = st.get_wallet_summary(1); + assert_eq!( + summary.as_ref().and_then(|s| s.progress().recovery()), + no_recovery, + ); + assert_eq!(summary.map(|s| s.progress().scan()), Some(Ratio::new(1, 1))); + + // Add more funds to the wallet in a second note + let (h2, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); + st.scan_cached_blocks(h2, 1); + + // Verified balance does not include the second note + let total = (value + value).unwrap(); + assert_eq!(st.get_spendable_balance(account_id, 2), value); + assert_eq!(st.get_pending_shielded_balance(account_id, 2), value); + assert_eq!(st.get_total_balance(account_id), total); + + // Wallet is still fully scanned + let summary = st.get_wallet_summary(1); + assert_eq!( + summary.as_ref().and_then(|s| s.progress().recovery()), + no_recovery + ); + assert_eq!(summary.map(|s| s.progress().scan()), Some(Ratio::new(2, 2))); + + // Spend fails because there are insufficient verified notes + let extsk2 = T::sk(&[0xf5; 32]); + let to = T::sk_default_address(&extsk2); + assert_matches!( + st.propose_standard_transfer::( + account_id, + StandardFeeRule::Zip317, + NonZeroU32::new(2).unwrap(), + &to, + Zatoshis::const_from_u64(70000), + None, + None, + T::SHIELDED_PROTOCOL, + ), + Err(data_api::error::Error::InsufficientFunds { + available, + required + }) + if available == Zatoshis::const_from_u64(50000) + && required == Zatoshis::const_from_u64(80000) + ); + + // Mine blocks SAPLING_ACTIVATION_HEIGHT + 2 to 9 until just before the second + // note is verified + for _ in 2..10 { + st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); + } + st.scan_cached_blocks(h2 + 1, 8); + + // Total balance is value * number of blocks scanned (10). + assert_eq!(st.get_total_balance(account_id), (value * 10u64).unwrap()); + + // Spend still fails + assert_matches!( + st.propose_standard_transfer::( + account_id, + StandardFeeRule::Zip317, + NonZeroU32::new(10).unwrap(), + &to, + Zatoshis::const_from_u64(70000), + None, + None, + T::SHIELDED_PROTOCOL, + ), + Err(data_api::error::Error::InsufficientFunds { + available, + required + }) + if available == Zatoshis::const_from_u64(50000) + && required == Zatoshis::const_from_u64(80000) + ); + + // Mine block 11 so that the second note becomes verified + let (h11, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); + st.scan_cached_blocks(h11, 1); + + // Total balance is value * number of blocks scanned (11). + assert_eq!(st.get_total_balance(account_id), (value * 11u64).unwrap()); + // Spendable balance at 10 confirmations is value * 2. + assert_eq!( + st.get_spendable_balance(account_id, 10), + (value * 2u64).unwrap() + ); + assert_eq!( + st.get_pending_shielded_balance(account_id, 10), + (value * 9u64).unwrap() + ); + + // Should now be able to generate a proposal + let amount_sent = Zatoshis::from_u64(70000).unwrap(); + let min_confirmations = NonZeroU32::new(10).unwrap(); + let proposal = st + .propose_standard_transfer::( + account_id, + StandardFeeRule::Zip317, + min_confirmations, + &to, + amount_sent, + None, + None, + T::SHIELDED_PROTOCOL, + ) + .unwrap(); + + // Executing the proposal should succeed + let txid = st + .create_proposed_transactions::( + account.usk(), + OvkPolicy::Sender, + &proposal, + ) + .unwrap()[0]; + + let (h, _) = st.generate_next_block_including(txid); + st.scan_cached_blocks(h, 1); + + // TODO: send to an account so that we can check its balance. + assert_eq!( + st.get_total_balance(account_id), + ((value * 11u64).unwrap() - (amount_sent + Zatoshis::from_u64(10000).unwrap()).unwrap()) + .unwrap() + ); +} + +pub fn spend_fails_on_locked_notes( + ds_factory: impl DataStoreFactory, + cache: impl TestCache, +) { + let mut st = TestBuilder::new() + .with_data_store_factory(ds_factory) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account = st.test_account().cloned().unwrap(); + let account_id = account.id(); + let dfvk = T::test_account_fvk(&st); + + let fee_rule = StandardFeeRule::Zip317; + + // Add funds to the wallet in a single note + let value = Zatoshis::const_from_u64(50000); + let (h1, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); + st.scan_cached_blocks(h1, 1); + + // Spendable balance matches total balance at 1 confirmation. + assert_eq!(st.get_total_balance(account_id), value); + assert_eq!(st.get_spendable_balance(account_id, 1), value); + + // Send some of the funds to another address, but don't mine the tx. + let extsk2 = T::sk(&[0xf5; 32]); + let to = T::sk_default_address(&extsk2); + let min_confirmations = NonZeroU32::new(1).unwrap(); + let proposal = st + .propose_standard_transfer::( + account_id, + fee_rule, + min_confirmations, + &to, + Zatoshis::const_from_u64(15000), + None, + None, + T::SHIELDED_PROTOCOL, + ) + .unwrap(); + + // Executing the proposal should succeed + assert_matches!( + st.create_proposed_transactions::(account.usk(), OvkPolicy::Sender, &proposal,), + Ok(txids) if txids.len() == 1 + ); + + // A second proposal fails because there are no usable notes + assert_matches!( + st.propose_standard_transfer::( + account_id, + fee_rule, + NonZeroU32::new(1).unwrap(), + &to, + Zatoshis::const_from_u64(2000), + None, + None, + T::SHIELDED_PROTOCOL, + ), + Err(data_api::error::Error::InsufficientFunds { + available, + required + }) + if available == Zatoshis::ZERO && required == Zatoshis::const_from_u64(12000) + ); + + // Mine blocks SAPLING_ACTIVATION_HEIGHT + 1 to 41 (that don't send us funds) + // until just before the first transaction expires + for i in 1..42 { + st.generate_next_block( + &T::sk_to_fvk(&T::sk(&[i as u8; 32])), + AddressType::DefaultExternal, + value, + ); + } + st.scan_cached_blocks(h1 + 1, 40); + + // Second proposal still fails + assert_matches!( + st.propose_standard_transfer::( + account_id, + fee_rule, + NonZeroU32::new(1).unwrap(), + &to, + Zatoshis::const_from_u64(2000), + None, + None, + T::SHIELDED_PROTOCOL, + ), + Err(data_api::error::Error::InsufficientFunds { + available, + required + }) + if available == Zatoshis::ZERO && required == Zatoshis::const_from_u64(12000) + ); + + // Mine block SAPLING_ACTIVATION_HEIGHT + 42 so that the first transaction expires + let (h43, _, _) = st.generate_next_block( + &T::sk_to_fvk(&T::sk(&[42; 32])), + AddressType::DefaultExternal, + value, + ); + st.scan_cached_blocks(h43, 1); + + // Spendable balance matches total balance at 1 confirmation. + assert_eq!(st.get_total_balance(account_id), value); + assert_eq!(st.get_spendable_balance(account_id, 1), value); + + // Second spend should now succeed + let amount_sent2 = Zatoshis::const_from_u64(2000); + let min_confirmations = NonZeroU32::new(1).unwrap(); + let proposal = st + .propose_standard_transfer::( + account_id, + fee_rule, + min_confirmations, + &to, + amount_sent2, + None, + None, + T::SHIELDED_PROTOCOL, + ) + .unwrap(); + + let txid2 = st + .create_proposed_transactions::( + account.usk(), + OvkPolicy::Sender, + &proposal, + ) + .unwrap()[0]; + + let (h, _) = st.generate_next_block_including(txid2); + st.scan_cached_blocks(h, 1); + + // TODO: send to an account so that we can check its balance. + assert_eq!( + st.get_total_balance(account_id), + (value - (amount_sent2 + Zatoshis::from_u64(10000).unwrap()).unwrap()).unwrap() + ); +} + +pub fn ovk_policy_prevents_recovery_from_chain( + ds_factory: DSF, + cache: impl TestCache, +) where + DSF: DataStoreFactory, +{ + let mut st = TestBuilder::new() + .with_data_store_factory(ds_factory) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account = st.test_account().cloned().unwrap(); + let account_id = account.id(); + let dfvk = T::test_account_fvk(&st); + + // Add funds to the wallet in a single note + let value = Zatoshis::const_from_u64(50000); + let (h1, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); + st.scan_cached_blocks(h1, 1); + + // Spendable balance matches total balance at 1 confirmation. + assert_eq!(st.get_total_balance(account_id), value); + assert_eq!(st.get_spendable_balance(account_id, 1), value); + + let extsk2 = T::sk(&[0xf5; 32]); + let addr2 = T::sk_default_address(&extsk2); + + let fee_rule = StandardFeeRule::Zip317; + + #[allow(clippy::type_complexity)] + let send_and_recover_with_policy = |st: &mut TestState<_, DSF::DataStore, _>, + ovk_policy| + -> Result< + Option<(Note, Address, MemoBytes)>, + TransferErrT< + DSF::DataStore, + GreedyInputSelector, + SingleOutputChangeStrategy, + >, + > { + let min_confirmations = NonZeroU32::new(1).unwrap(); + let proposal = st.propose_standard_transfer( + account_id, + fee_rule, + min_confirmations, + &addr2, + Zatoshis::const_from_u64(15000), + None, + None, + T::SHIELDED_PROTOCOL, + )?; + + // Executing the proposal should succeed + let txid = st.create_proposed_transactions(account.usk(), ovk_policy, &proposal)?[0]; + + // Fetch the transaction from the database + let tx = st + .wallet() + .get_transaction(txid) + .map_err(Error::DataSource)? + .unwrap(); + + Ok(T::try_output_recovery(st.network(), h1, &tx, &dfvk)) + }; + + // Send some of the funds to another address, keeping history. + // The recipient output is decryptable by the sender. + assert_matches!( + send_and_recover_with_policy(&mut st, OvkPolicy::Sender), + Ok(Some((_, recovered_to, _))) if recovered_to == addr2 + ); + + // Mine blocks SAPLING_ACTIVATION_HEIGHT + 1 to 42 (that don't send us funds) + // so that the first transaction expires + for i in 1..=42 { + st.generate_next_block( + &T::sk_to_fvk(&T::sk(&[i as u8; 32])), + AddressType::DefaultExternal, + value, + ); + } + st.scan_cached_blocks(h1 + 1, 42); + + // Send the funds again, discarding history. + // Neither transaction output is decryptable by the sender. + assert_matches!( + send_and_recover_with_policy(&mut st, OvkPolicy::Discard), + Ok(None) + ); +} + +pub fn spend_succeeds_to_t_addr_zero_change( + ds_factory: impl DataStoreFactory, + cache: impl TestCache, +) { + let mut st = TestBuilder::new() + .with_data_store_factory(ds_factory) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account = st.test_account().cloned().unwrap(); + let account_id = account.id(); + let dfvk = T::test_account_fvk(&st); + + // Add funds to the wallet in a single note + let value = Zatoshis::const_from_u64(70000); + let (h, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); + st.scan_cached_blocks(h, 1); + + // Spendable balance matches total balance at 1 confirmation. + assert_eq!(st.get_total_balance(account_id), value); + assert_eq!(st.get_spendable_balance(account_id, 1), value); + + let fee_rule = StandardFeeRule::Zip317; + + // TODO: generate_next_block_from_tx does not currently support transparent outputs. + let to = TransparentAddress::PublicKeyHash([7; 20]).into(); + let min_confirmations = NonZeroU32::new(1).unwrap(); + let proposal = st + .propose_standard_transfer::( + account_id, + fee_rule, + min_confirmations, + &to, + Zatoshis::const_from_u64(50000), + None, + None, + T::SHIELDED_PROTOCOL, + ) + .unwrap(); + + // Executing the proposal should succeed + assert_matches!( + st.create_proposed_transactions::(account.usk(), OvkPolicy::Sender, &proposal), + Ok(txids) if txids.len() == 1 + ); +} + +pub fn change_note_spends_succeed( + ds_factory: impl DataStoreFactory, + cache: impl TestCache, +) { + let mut st = TestBuilder::new() + .with_data_store_factory(ds_factory) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account = st.test_account().cloned().unwrap(); + let account_id = account.id(); + let dfvk = T::test_account_fvk(&st); + + // Add funds to the wallet in a single note owned by the internal spending key + let value = Zatoshis::const_from_u64(70000); + let (h, _, _) = st.generate_next_block(&dfvk, AddressType::Internal, value); + st.scan_cached_blocks(h, 1); + + // Spendable balance matches total balance at 1 confirmation. + assert_eq!(st.get_total_balance(account_id), value); + assert_eq!(st.get_spendable_balance(account_id, 1), value); + + // Value is considered pending at 10 confirmations. + assert_eq!(st.get_pending_shielded_balance(account_id, 10), value); + assert_eq!(st.get_spendable_balance(account_id, 10), Zatoshis::ZERO); + + let change_note_scope = st + .wallet() + .get_notes(T::SHIELDED_PROTOCOL) + .unwrap() + .iter() + .find_map(|note| (note.note().value() == value).then_some(note.spending_key_scope())); + assert_matches!(change_note_scope, Some(Scope::Internal)); + + let fee_rule = StandardFeeRule::Zip317; + + // TODO: generate_next_block_from_tx does not currently support transparent outputs. + let to = TransparentAddress::PublicKeyHash([7; 20]).into(); + let min_confirmations = NonZeroU32::new(1).unwrap(); + let proposal = st + .propose_standard_transfer::( + account_id, + fee_rule, + min_confirmations, + &to, + Zatoshis::const_from_u64(50000), + None, + None, + T::SHIELDED_PROTOCOL, + ) + .unwrap(); + + // Executing the proposal should succeed + assert_matches!( + st.create_proposed_transactions::(account.usk(), OvkPolicy::Sender, &proposal), + Ok(txids) if txids.len() == 1 + ); +} + +pub fn external_address_change_spends_detected_in_restore_from_seed( + ds_factory: DSF, + cache: impl TestCache, +) where + DSF: DataStoreFactory, + ::DataStore: Reset, +{ + let mut st = TestBuilder::new() + .with_data_store_factory(ds_factory) + .with_block_cache(cache) + .build(); + + // Add two accounts to the wallet. + let seed = Secret::new([0u8; 32].to_vec()); + let birthday = AccountBirthday::from_sapling_activation(st.network(), BlockHash([0; 32])); + let (account1, usk) = st + .wallet_mut() + .create_account("account1", &seed, &birthday, None) + .unwrap(); + let dfvk = T::sk_to_fvk(T::usk_to_sk(&usk)); + + let (account2, usk2) = st + .wallet_mut() + .create_account("account2", &seed, &birthday, None) + .unwrap(); + let dfvk2 = T::sk_to_fvk(T::usk_to_sk(&usk2)); + + // Add funds to the wallet in a single note + let value = Zatoshis::from_u64(100000).unwrap(); + let (h, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); + st.scan_cached_blocks(h, 1); + + // Spendable balance matches total balance + assert_eq!(st.get_total_balance(account1), value); + assert_eq!(st.get_spendable_balance(account1, 1), value); + assert_eq!(st.get_total_balance(account2), Zatoshis::ZERO); + + let amount_sent = Zatoshis::from_u64(20000).unwrap(); + let amount_legacy_change = Zatoshis::from_u64(30000).unwrap(); + let addr = T::fvk_default_address(&dfvk); + let addr2 = T::fvk_default_address(&dfvk2); + let req = TransactionRequest::new(vec![ + // payment to an external recipient + Payment::without_memo(addr2.to_zcash_address(st.network()), amount_sent), + // payment back to the originating wallet, simulating legacy change + Payment::without_memo(addr.to_zcash_address(st.network()), amount_legacy_change), + ]) + .unwrap(); + + let change_strategy = fees::standard::SingleOutputChangeStrategy::new( + StandardFeeRule::Zip317, + None, + T::SHIELDED_PROTOCOL, + DustOutputPolicy::default(), + ); + let input_selector = GreedyInputSelector::new(); + + let txid = st + .spend( + &input_selector, + &change_strategy, + &usk, + req, + OvkPolicy::Sender, + NonZeroU32::new(1).unwrap(), + ) + .unwrap()[0]; + + let amount_left = (value - (amount_sent + MINIMUM_FEE + MARGINAL_FEE).unwrap()).unwrap(); + let pending_change = (amount_left - amount_legacy_change).unwrap(); + + // The "legacy change" is not counted by get_pending_change(). + assert_eq!(st.get_pending_change(account1, 1), pending_change); + // We spent the only note so we only have pending change. + assert_eq!(st.get_total_balance(account1), pending_change); + + let (h, _) = st.generate_next_block_including(txid); + st.scan_cached_blocks(h, 1); + + assert_eq!(st.get_total_balance(account2), amount_sent,); + assert_eq!(st.get_total_balance(account1), amount_left); + + st.reset(); + + // Account creation and DFVK derivation should be deterministic. + let (account1, restored_usk) = st + .wallet_mut() + .create_account("account1_restored", &seed, &birthday, None) + .unwrap(); + assert!(T::fvks_equal( + &T::sk_to_fvk(T::usk_to_sk(&restored_usk)), + &dfvk, + )); + + let (account2, restored_usk2) = st + .wallet_mut() + .create_account("account2_restored", &seed, &birthday, None) + .unwrap(); + assert!(T::fvks_equal( + &T::sk_to_fvk(T::usk_to_sk(&restored_usk2)), + &dfvk2, + )); + + st.scan_cached_blocks(st.sapling_activation_height(), 2); + + assert_eq!(st.get_total_balance(account2), amount_sent); + assert_eq!(st.get_total_balance(account1), amount_left); +} + +#[allow(dead_code)] +pub fn zip317_spend( + ds_factory: DSF, + cache: impl TestCache, +) { + let mut st = TestBuilder::new() + .with_data_store_factory(ds_factory) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account = st.test_account().cloned().unwrap(); + let account_id = account.id(); + let dfvk = T::test_account_fvk(&st); + + // Add funds to the wallet + let (h1, _, _) = st.generate_next_block( + &dfvk, + AddressType::Internal, + Zatoshis::const_from_u64(50000), + ); + + // Add 10 dust notes to the wallet + for _ in 1..=10 { + st.generate_next_block( + &dfvk, + AddressType::DefaultExternal, + Zatoshis::const_from_u64(1000), + ); + } + + st.scan_cached_blocks(h1, 11); + + // Spendable balance matches total balance + let total = Zatoshis::const_from_u64(60000); + assert_eq!(st.get_total_balance(account_id), total); + assert_eq!(st.get_spendable_balance(account_id, 1), total); + + let input_selector = GreedyInputSelector::::new(); + let change_strategy = + single_output_change_strategy(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL); + + // This first request will fail due to insufficient non-dust funds + let req = TransactionRequest::new(vec![Payment::without_memo( + T::fvk_default_address(&dfvk).to_zcash_address(st.network()), + Zatoshis::const_from_u64(50000), + )]) + .unwrap(); + + assert_matches!( + st.spend( + &input_selector, + &change_strategy, + account.usk(), + req, + OvkPolicy::Sender, + NonZeroU32::new(1).unwrap(), + ), + Err(Error::InsufficientFunds { available, required }) + if available == Zatoshis::const_from_u64(51000) + && required == Zatoshis::const_from_u64(60000) + ); + + // This request will succeed, spending a single dust input to pay the 10000 + // ZAT fee in addition to the 41000 ZAT output to the recipient + let req = TransactionRequest::new(vec![Payment::without_memo( + T::fvk_default_address(&dfvk).to_zcash_address(st.network()), + Zatoshis::const_from_u64(41000), + )]) + .unwrap(); + + let txid = st + .spend( + &input_selector, + &change_strategy, + account.usk(), + req, + OvkPolicy::Sender, + NonZeroU32::new(1).unwrap(), + ) + .unwrap()[0]; + + let (h, _) = st.generate_next_block_including(txid); + st.scan_cached_blocks(h, 1); + + // TODO: send to an account so that we can check its balance. + // We sent back to the same account so the amount_sent should be included + // in the total balance. + assert_eq!( + st.get_total_balance(account_id), + (total - Zatoshis::const_from_u64(10000)).unwrap() + ); +} + +#[cfg(feature = "transparent-inputs")] +pub fn shield_transparent(ds_factory: DSF, cache: impl TestCache) +where + DSF: DataStoreFactory, + <::DataStore as WalletWrite>::UtxoRef: std::fmt::Debug, +{ + let mut st = TestBuilder::new() + .with_data_store_factory(ds_factory) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account = st.test_account().cloned().unwrap(); + let dfvk = T::test_account_fvk(&st); + + let uaddr = st + .wallet() + .get_current_address(account.id()) + .unwrap() + .unwrap(); + let taddr = uaddr.transparent().unwrap(); + + // Ensure that the wallet has at least one block + let (h, _, _) = st.generate_next_block( + &dfvk, + AddressType::Internal, + Zatoshis::const_from_u64(50000), + ); + st.scan_cached_blocks(h, 1); + + let utxo = WalletTransparentOutput::from_parts( + OutPoint::fake(), + TxOut { + value: Zatoshis::const_from_u64(100000), + script_pubkey: taddr.script(), + }, + Some(h), + ) + .unwrap(); + + let res0 = st.wallet_mut().put_received_transparent_utxo(&utxo); + assert_matches!(res0, Ok(_)); + + let input_selector = GreedyInputSelector::new(); + let change_strategy = + single_output_change_strategy(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL); + + let txids = st + .shield_transparent_funds( + &input_selector, + &change_strategy, + Zatoshis::from_u64(10000).unwrap(), + account.usk(), + &[*taddr], + account.id(), + 1, + ) + .unwrap(); + assert_eq!(txids.len(), 1); + + let tx = st.get_tx_from_history(*txids.first()).unwrap().unwrap(); + assert_eq!(tx.spent_note_count(), 1); + assert!(tx.has_change()); + assert_eq!(tx.received_note_count(), 0); + assert_eq!(tx.sent_note_count(), 0); + assert!(tx.is_shielding()); + + // Generate and scan the block including the transaction + let (h, _) = st.generate_next_block_including(*txids.first()); + st.scan_cached_blocks(h, 1); + + // Ensure that the transaction metadata is still correct after the update produced by scanning. + let tx = st.get_tx_from_history(*txids.first()).unwrap().unwrap(); + assert_eq!(tx.spent_note_count(), 1); + assert!(tx.has_change()); + assert_eq!(tx.received_note_count(), 0); + assert_eq!(tx.sent_note_count(), 0); + assert!(tx.is_shielding()); +} + +// FIXME: This requires fixes to the test framework. +#[allow(dead_code)] +pub fn birthday_in_anchor_shard( + ds_factory: impl DataStoreFactory, + cache: impl TestCache, +) { + // Set up the following situation: + // + // |<------ 500 ------->|<--- 10 --->|<--- 10 --->| + // last_shard_start wallet_birthday received_tx anchor_height + // + // We set the Sapling and Orchard frontiers at the birthday block initial state to 1234 + // notes beyond the end of the first shard. + let frontier_tree_size: u32 = (0x1 << 16) + 1234; + let mut st = TestBuilder::new() + .with_data_store_factory(ds_factory) + .with_block_cache(cache) + .with_initial_chain_state(|rng, network| { + let birthday_height = network.activation_height(NetworkUpgrade::Nu5).unwrap() + 1000; + + // Construct a fake chain state for the end of the block with the given + // birthday_offset from the Nu5 birthday. + let (prior_sapling_roots, sapling_initial_tree) = + Frontier::random_with_prior_subtree_roots( + rng, + frontier_tree_size.into(), + NonZeroU8::new(16).unwrap(), + ); + // There will only be one prior root + let prior_sapling_roots = prior_sapling_roots + .into_iter() + .map(|root| CommitmentTreeRoot::from_parts(birthday_height - 500, root)) + .collect::>(); + + #[cfg(feature = "orchard")] + let (prior_orchard_roots, orchard_initial_tree) = + Frontier::random_with_prior_subtree_roots( + rng, + frontier_tree_size.into(), + NonZeroU8::new(16).unwrap(), + ); + // There will only be one prior root + #[cfg(feature = "orchard")] + let prior_orchard_roots = prior_orchard_roots + .into_iter() + .map(|root| CommitmentTreeRoot::from_parts(birthday_height - 500, root)) + .collect::>(); + + InitialChainState { + chain_state: ChainState::new( + birthday_height - 1, + BlockHash([5; 32]), + sapling_initial_tree, + #[cfg(feature = "orchard")] + orchard_initial_tree, + ), + prior_sapling_roots, + #[cfg(feature = "orchard")] + prior_orchard_roots, + } + }) + .with_account_having_current_birthday() + .build(); + + // Generate 9 blocks that have no value for us, starting at the birthday height. + let not_our_value = Zatoshis::const_from_u64(10000); + let not_our_key = T::random_fvk(st.rng_mut()); + let (initial_height, _, _) = + st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value); + for _ in 1..9 { + st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value); + } + + // Now, generate a block that belongs to our wallet + let (received_tx_height, _, _) = st.generate_next_block( + &T::test_account_fvk(&st), + AddressType::DefaultExternal, + Zatoshis::const_from_u64(500000), + ); + + // Generate some more blocks to get above our anchor height + for _ in 0..15 { + st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value); + } + + // Scan a block range that includes our received note, but skips some blocks we need to + // make it spendable. + st.scan_cached_blocks(initial_height + 5, 20); + + // Verify that the received note is not considered spendable + let account = st.test_account().unwrap(); + let account_id = account.id(); + let spendable = T::select_spendable_notes( + &st, + account_id, + Zatoshis::const_from_u64(300000), + received_tx_height + 10, + &[], + ) + .unwrap(); + + assert_eq!(spendable.len(), 0); + + // Scan the blocks we skipped + st.scan_cached_blocks(initial_height, 5); + + // Verify that the received note is now considered spendable + let spendable = T::select_spendable_notes( + &st, + account_id, + Zatoshis::const_from_u64(300000), + received_tx_height + 10, + &[], + ) + .unwrap(); + + assert_eq!(spendable.len(), 1); +} + +pub fn checkpoint_gaps( + ds_factory: DSF, + cache: impl TestCache, +) { + let mut st = TestBuilder::new() + .with_data_store_factory(ds_factory) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account = st.test_account().cloned().unwrap(); + let dfvk = T::test_account_fvk(&st); + + // Generate a block with funds belonging to our wallet. + st.generate_next_block( + &dfvk, + AddressType::DefaultExternal, + Zatoshis::const_from_u64(500000), + ); + st.scan_cached_blocks(account.birthday().height(), 1); + + // Create a gap of 10 blocks having no shielded outputs, then add a block that doesn't + // belong to us so that we can get a checkpoint in the tree. + let not_our_key = T::sk_to_fvk(&T::sk(&[0xf5; 32])); + let not_our_value = Zatoshis::const_from_u64(10000); + st.generate_block_at( + account.birthday().height() + 10, + BlockHash([0; 32]), + &[FakeCompactOutput::new( + ¬_our_key, + AddressType::DefaultExternal, + not_our_value, + )], + st.latest_cached_block().unwrap().sapling_end_size(), + st.latest_cached_block().unwrap().orchard_end_size(), + false, + ); + + // Scan the block + st.scan_cached_blocks(account.birthday().height() + 10, 1); + + // Verify that our note is considered spendable + let spendable = T::select_spendable_notes( + &st, + account.id(), + Zatoshis::const_from_u64(300000), + account.birthday().height() + 5, + &[], + ) + .unwrap(); + assert_eq!(spendable.len(), 1); + + let input_selector = GreedyInputSelector::::new(); + let change_strategy = + single_output_change_strategy(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL); + + let to = T::fvk_default_address(¬_our_key); + let req = TransactionRequest::new(vec![Payment::without_memo( + to.to_zcash_address(st.network()), + Zatoshis::const_from_u64(10000), + )]) + .unwrap(); + + // Attempt to spend the note with 5 confirmations + assert_matches!( + st.spend( + &input_selector, + &change_strategy, + account.usk(), + req, + OvkPolicy::Sender, + NonZeroU32::new(5).unwrap(), + ), + Ok(_) + ); +} + +#[cfg(feature = "orchard")] +pub fn pool_crossing_required( + ds_factory: impl DataStoreFactory, + cache: impl TestCache, +) { + let mut st = TestBuilder::new() + .with_data_store_factory(ds_factory) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) // TODO: Allow for Orchard + // activation after Sapling + .build(); + + let account = st.test_account().cloned().unwrap(); + + let p0_fvk = P0::test_account_fvk(&st); + + let p1_fvk = P1::test_account_fvk(&st); + let p1_to = P1::fvk_default_address(&p1_fvk); + + let note_value = Zatoshis::const_from_u64(350000); + st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value); + st.scan_cached_blocks(account.birthday().height(), 2); + + let initial_balance = note_value; + assert_eq!(st.get_total_balance(account.id()), initial_balance); + assert_eq!(st.get_spendable_balance(account.id(), 1), initial_balance); + + let transfer_amount = Zatoshis::const_from_u64(200000); + let p0_to_p1 = TransactionRequest::new(vec![Payment::without_memo( + p1_to.to_zcash_address(st.network()), + transfer_amount, + )]) + .unwrap(); + + let input_selector = GreedyInputSelector::new(); + let change_strategy = + single_output_change_strategy(StandardFeeRule::Zip317, None, P1::SHIELDED_PROTOCOL); + let proposal0 = st + .propose_transfer( + account.id(), + &input_selector, + &change_strategy, + p0_to_p1, + NonZeroU32::new(1).unwrap(), + ) + .unwrap(); + + let _min_target_height = proposal0.min_target_height(); + assert_eq!(proposal0.steps().len(), 1); + let step0 = &proposal0.steps().head; + + // We expect 4 logical actions, two per pool (due to padding). + let expected_fee = Zatoshis::const_from_u64(20000); + assert_eq!(step0.balance().fee_required(), expected_fee); + + let expected_change = (note_value - transfer_amount - expected_fee).unwrap(); + let proposed_change = step0.balance().proposed_change(); + assert_eq!(proposed_change.len(), 1); + let change_output = proposed_change.first().unwrap(); + // Since this is a cross-pool transfer, change will be sent to the preferred pool. + assert_eq!( + change_output.output_pool(), + PoolType::Shielded(std::cmp::max( + ShieldedProtocol::Sapling, + ShieldedProtocol::Orchard + )) + ); + assert_eq!(change_output.value(), expected_change); + + let create_proposed_result = st.create_proposed_transactions::( + account.usk(), + OvkPolicy::Sender, + &proposal0, + ); + assert_matches!(&create_proposed_result, Ok(txids) if txids.len() == 1); + + let (h, _) = st.generate_next_block_including(create_proposed_result.unwrap()[0]); + st.scan_cached_blocks(h, 1); + + assert_eq!( + st.get_total_balance(account.id()), + (initial_balance - expected_fee).unwrap() + ); + assert_eq!( + st.get_spendable_balance(account.id(), 1), + (initial_balance - expected_fee).unwrap() + ); +} + +#[cfg(feature = "orchard")] +pub fn fully_funded_fully_private( + ds_factory: impl DataStoreFactory, + cache: impl TestCache, +) { + let mut st = TestBuilder::new() + .with_data_store_factory(ds_factory) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) // TODO: Allow for Orchard + // activation after Sapling + .build(); + + let account = st.test_account().cloned().unwrap(); + + let p0_fvk = P0::test_account_fvk(&st); + + let p1_fvk = P1::test_account_fvk(&st); + let p1_to = P1::fvk_default_address(&p1_fvk); + + let note_value = Zatoshis::const_from_u64(350000); + st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value); + st.generate_next_block(&p1_fvk, AddressType::DefaultExternal, note_value); + st.scan_cached_blocks(account.birthday().height(), 2); + + let initial_balance = (note_value * 2u64).unwrap(); + assert_eq!(st.get_total_balance(account.id()), initial_balance); + assert_eq!(st.get_spendable_balance(account.id(), 1), initial_balance); + + let transfer_amount = Zatoshis::const_from_u64(200000); + let p0_to_p1 = TransactionRequest::new(vec![Payment::without_memo( + p1_to.to_zcash_address(st.network()), + transfer_amount, + )]) + .unwrap(); + + let input_selector = GreedyInputSelector::new(); + // We set the default change output pool to P0, because we want to verify later that + // change is actually sent to P1 (as the transaction is fully fundable from P1). + let change_strategy = + single_output_change_strategy(StandardFeeRule::Zip317, None, P0::SHIELDED_PROTOCOL); + let proposal0 = st + .propose_transfer( + account.id(), + &input_selector, + &change_strategy, + p0_to_p1, + NonZeroU32::new(1).unwrap(), + ) + .unwrap(); + + let _min_target_height = proposal0.min_target_height(); + assert_eq!(proposal0.steps().len(), 1); + let step0 = &proposal0.steps().head; + + // We expect 2 logical actions, since either pool can pay the full balance required + // and note selection should choose the fully-private path. + let expected_fee = Zatoshis::const_from_u64(10000); + assert_eq!(step0.balance().fee_required(), expected_fee); + + let expected_change = (note_value - transfer_amount - expected_fee).unwrap(); + let proposed_change = step0.balance().proposed_change(); + assert_eq!(proposed_change.len(), 1); + let change_output = proposed_change.first().unwrap(); + // Since there are sufficient funds in either pool, change is kept in the same pool as + // the source note (the target pool), and does not necessarily follow preference order. + assert_eq!( + change_output.output_pool(), + PoolType::Shielded(P1::SHIELDED_PROTOCOL) + ); + assert_eq!(change_output.value(), expected_change); + + let create_proposed_result = st.create_proposed_transactions::( + account.usk(), + OvkPolicy::Sender, + &proposal0, + ); + assert_matches!(&create_proposed_result, Ok(txids) if txids.len() == 1); + + let (h, _) = st.generate_next_block_including(create_proposed_result.unwrap()[0]); + st.scan_cached_blocks(h, 1); + + assert_eq!( + st.get_total_balance(account.id()), + (initial_balance - expected_fee).unwrap() + ); + assert_eq!( + st.get_spendable_balance(account.id(), 1), + (initial_balance - expected_fee).unwrap() + ); +} + +#[cfg(all(feature = "orchard", feature = "transparent-inputs"))] +pub fn fully_funded_send_to_t( + ds_factory: impl DataStoreFactory, + cache: impl TestCache, +) { + let mut st = TestBuilder::new() + .with_data_store_factory(ds_factory) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) // TODO: Allow for Orchard + // activation after Sapling + .build(); + + let account = st.test_account().cloned().unwrap(); + + let p0_fvk = P0::test_account_fvk(&st); + let p1_fvk = P1::test_account_fvk(&st); + let (p1_to, _) = account.usk().default_transparent_address(); + + let note_value = Zatoshis::const_from_u64(350000); + st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value); + st.generate_next_block(&p1_fvk, AddressType::DefaultExternal, note_value); + st.scan_cached_blocks(account.birthday().height(), 2); + + let initial_balance = (note_value * 2u64).unwrap(); + assert_eq!(st.get_total_balance(account.id()), initial_balance); + assert_eq!(st.get_spendable_balance(account.id(), 1), initial_balance); + + let transfer_amount = Zatoshis::const_from_u64(200000); + let p0_to_p1 = TransactionRequest::new(vec![Payment::without_memo( + Address::Transparent(p1_to).to_zcash_address(st.network()), + transfer_amount, + )]) + .unwrap(); + + let input_selector = GreedyInputSelector::new(); + // We set the default change output pool to P0, because we want to verify later that + // change is actually sent to P1 (as the transaction is fully fundable from P1). + let change_strategy = + single_output_change_strategy(StandardFeeRule::Zip317, None, P0::SHIELDED_PROTOCOL); + let proposal0 = st + .propose_transfer( + account.id(), + &input_selector, + &change_strategy, + p0_to_p1, + NonZeroU32::new(1).unwrap(), + ) + .unwrap(); + + let _min_target_height = proposal0.min_target_height(); + assert_eq!(proposal0.steps().len(), 1); + let step0 = &proposal0.steps().head; + + // We expect 3 logical actions, one for the transparent output and two for the source pool. + let expected_fee = Zatoshis::const_from_u64(15000); + assert_eq!(step0.balance().fee_required(), expected_fee); + + let expected_change = (note_value - transfer_amount - expected_fee).unwrap(); + let proposed_change = step0.balance().proposed_change(); + assert_eq!(proposed_change.len(), 1); + let change_output = proposed_change.first().unwrap(); + // Since there are sufficient funds in either pool, change is kept in the same pool as + // the source note (the target pool), and does not necessarily follow preference order. + // The source note will always be sapling, as we spend Sapling funds preferentially. + assert_eq!(change_output.output_pool(), PoolType::SAPLING); + assert_eq!(change_output.value(), expected_change); + + let create_proposed_result = st.create_proposed_transactions::( + account.usk(), + OvkPolicy::Sender, + &proposal0, + ); + assert_matches!(&create_proposed_result, Ok(txids) if txids.len() == 1); + + let (h, _) = st.generate_next_block_including(create_proposed_result.unwrap()[0]); + st.scan_cached_blocks(h, 1); + + assert_eq!( + st.get_total_balance(account.id()), + (initial_balance - transfer_amount - expected_fee).unwrap() + ); + assert_eq!( + st.get_spendable_balance(account.id(), 1), + (initial_balance - transfer_amount - expected_fee).unwrap() + ); +} + +#[cfg(feature = "orchard")] +pub fn multi_pool_checkpoint( + ds_factory: impl DataStoreFactory, + cache: impl TestCache, +) { + let mut st = TestBuilder::new() + .with_data_store_factory(ds_factory) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) // TODO: Allow for Orchard + // activation after Sapling + .build(); + + let account = st.test_account().cloned().unwrap(); + let acct_id = account.id(); + + let p0_fvk = P0::test_account_fvk(&st); + let p1_fvk = P1::test_account_fvk(&st); + + // Add some funds to the wallet; we add two notes to allow successive spends. Also, + // we will generate a note in the P1 pool to ensure that we have some tree state. + let note_value = Zatoshis::const_from_u64(500000); + let (start_height, _, _) = + st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value); + st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value); + st.generate_next_block(&p1_fvk, AddressType::DefaultExternal, note_value); + let scanned = st.scan_cached_blocks(start_height, 3); + + let next_to_scan = scanned.scanned_range().end; + + let initial_balance = (note_value * 3u64).unwrap(); + assert_eq!(st.get_total_balance(acct_id), initial_balance); + assert_eq!(st.get_spendable_balance(acct_id, 1), initial_balance); + + // Generate several empty blocks + for _ in 0..10 { + st.generate_empty_block(); + } + + // Scan into the middle of the empty range + let scanned = st.scan_cached_blocks(next_to_scan, 5); + let next_to_scan = scanned.scanned_range().end; + + // The initial balance should be unchanged. + assert_eq!(st.get_total_balance(acct_id), initial_balance); + assert_eq!(st.get_spendable_balance(acct_id, 1), initial_balance); + + // Set up the fee rule and input selector we'll use for all the transfers. + let input_selector = GreedyInputSelector::new(); + let change_strategy = + single_output_change_strategy(StandardFeeRule::Zip317, None, P1::SHIELDED_PROTOCOL); + + // First, send funds just to P0 + let transfer_amount = Zatoshis::const_from_u64(200000); + let p0_transfer = TransactionRequest::new(vec![Payment::without_memo( + P0::random_address(st.rng_mut()).to_zcash_address(st.network()), + transfer_amount, + )]) + .unwrap(); + let res = st + .spend( + &input_selector, + &change_strategy, + account.usk(), + p0_transfer, + OvkPolicy::Sender, + NonZeroU32::new(1).unwrap(), + ) + .unwrap(); + st.generate_next_block_including(*res.first()); + + let expected_fee = Zatoshis::const_from_u64(10000); + let expected_change = (note_value - transfer_amount - expected_fee).unwrap(); + assert_eq!( + st.get_total_balance(acct_id), + ((note_value * 2u64).unwrap() + expected_change).unwrap() + ); + assert_eq!(st.get_pending_change(acct_id, 1), expected_change); + + // In the next block, send funds to both P0 and P1 + let both_transfer = TransactionRequest::new(vec![ + Payment::without_memo( + P0::random_address(st.rng_mut()).to_zcash_address(st.network()), + transfer_amount, + ), + Payment::without_memo( + P1::random_address(st.rng_mut()).to_zcash_address(st.network()), + transfer_amount, + ), + ]) + .unwrap(); + let res = st + .spend( + &input_selector, + &change_strategy, + account.usk(), + both_transfer, + OvkPolicy::Sender, + NonZeroU32::new(1).unwrap(), + ) + .unwrap(); + st.generate_next_block_including(*res.first()); + + // Generate a few more empty blocks + for _ in 0..5 { + st.generate_empty_block(); + } + + // Generate another block with funds for us + let (max_height, _, _) = + st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value); + + // Scan everything. + st.scan_cached_blocks( + next_to_scan, + usize::try_from(u32::from(max_height) - u32::from(next_to_scan) + 1).unwrap(), + ); + + let expected_final = (initial_balance + note_value + - (transfer_amount * 3u64).unwrap() + - (expected_fee * 3u64).unwrap()) + .unwrap(); + assert_eq!(st.get_total_balance(acct_id), expected_final); + + let expected_checkpoints_p0: Vec<(BlockHeight, Option)> = [ + (99999, None), + (100000, Some(0)), + (100001, Some(1)), + (100002, Some(1)), + (100007, Some(1)), // synthetic checkpoint in empty span from scan start + (100013, Some(3)), + (100014, Some(5)), + (100020, Some(6)), + ] + .into_iter() + .map(|(h, pos)| (BlockHeight::from(h), pos.map(Position::from))) + .collect(); + + let expected_checkpoints_p1: Vec<(BlockHeight, Option)> = [ + (99999, None), + (100000, None), + (100001, None), + (100002, Some(0)), + (100007, Some(0)), // synthetic checkpoint in empty span from scan start + (100013, Some(0)), + (100014, Some(2)), + (100020, Some(2)), + ] + .into_iter() + .map(|(h, pos)| (BlockHeight::from(h), pos.map(Position::from))) + .collect(); + + let p0_checkpoints = st + .wallet() + .get_checkpoint_history(&P0::SHIELDED_PROTOCOL) + .unwrap(); + assert_eq!(p0_checkpoints.to_vec(), expected_checkpoints_p0); + + let p1_checkpoints = st + .wallet() + .get_checkpoint_history(&P1::SHIELDED_PROTOCOL) + .unwrap(); + assert_eq!(p1_checkpoints.to_vec(), expected_checkpoints_p1); +} + +#[cfg(feature = "orchard")] +pub fn multi_pool_checkpoints_with_pruning( + ds_factory: impl DataStoreFactory, + cache: impl TestCache, +) { + let mut st = TestBuilder::new() + .with_data_store_factory(ds_factory) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) // TODO: Allow for Orchard + // activation after Sapling + .build(); + + let account = st.test_account().cloned().unwrap(); + + let p0_fvk = P0::random_fvk(st.rng_mut()); + let p1_fvk = P1::random_fvk(st.rng_mut()); + + let note_value = Zatoshis::const_from_u64(10000); + // Generate 100 P0 blocks, then 100 P1 blocks, then another 100 P0 blocks. + for _ in 0..10 { + for _ in 0..10 { + st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value); + } + for _ in 0..10 { + st.generate_next_block(&p1_fvk, AddressType::DefaultExternal, note_value); + } + } + st.scan_cached_blocks(account.birthday().height(), 200); + for _ in 0..100 { + st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value); + st.generate_next_block(&p1_fvk, AddressType::DefaultExternal, note_value); + } + st.scan_cached_blocks(account.birthday().height() + 200, 200); +} + +pub fn valid_chain_states( + ds_factory: impl DataStoreFactory, + cache: impl TestCache, +) { + let mut st = TestBuilder::new() + .with_data_store_factory(ds_factory) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let dfvk = T::test_account_fvk(&st); + + // Empty chain should return None + assert_matches!(st.wallet().chain_height(), Ok(None)); + + // Create a fake CompactBlock sending value to the address + let (h1, _, _) = st.generate_next_block( + &dfvk, + AddressType::DefaultExternal, + Zatoshis::const_from_u64(5), + ); + + // Scan the cache + st.scan_cached_blocks(h1, 1); + + // Create a second fake CompactBlock sending more value to the address + let (h2, _, _) = st.generate_next_block( + &dfvk, + AddressType::DefaultExternal, + Zatoshis::const_from_u64(7), + ); + + // Scanning should detect no inconsistencies + st.scan_cached_blocks(h2, 1); +} + +// FIXME: This requires fixes to the test framework. +#[allow(dead_code)] +pub fn invalid_chain_cache_disconnected( + ds_factory: impl DataStoreFactory, + cache: impl TestCache, +) { + let mut st = TestBuilder::new() + .with_data_store_factory(ds_factory) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let dfvk = T::test_account_fvk(&st); + + // Create some fake CompactBlocks + let (h, _, _) = st.generate_next_block( + &dfvk, + AddressType::DefaultExternal, + Zatoshis::const_from_u64(5), + ); + let (last_contiguous_height, _, _) = st.generate_next_block( + &dfvk, + AddressType::DefaultExternal, + Zatoshis::const_from_u64(7), + ); + + // Scanning the cache should find no inconsistencies + st.scan_cached_blocks(h, 2); + + // Create more fake CompactBlocks that don't connect to the scanned ones + let disconnect_height = last_contiguous_height + 1; + st.generate_block_at( + disconnect_height, + BlockHash([1; 32]), + &[FakeCompactOutput::new( + &dfvk, + AddressType::DefaultExternal, + Zatoshis::const_from_u64(8), + )], + 2, + 2, + true, + ); + st.generate_next_block( + &dfvk, + AddressType::DefaultExternal, + Zatoshis::const_from_u64(3), + ); + + // Data+cache chain should be invalid at the data/cache boundary + assert_matches!( + st.try_scan_cached_blocks( + disconnect_height, + 2 + ), + Err(chain::error::Error::Scan(ScanError::PrevHashMismatch { at_height })) + if at_height == disconnect_height + ); +} + +pub fn data_db_truncation(ds_factory: DSF, cache: impl TestCache) +where + DSF: DataStoreFactory, + ::AccountId: std::fmt::Debug, +{ + let mut st = TestBuilder::new() + .with_data_store_factory(ds_factory) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account = st.test_account().cloned().unwrap(); + let dfvk = T::test_account_fvk(&st); + + // Wallet summary is not yet available + assert_eq!(st.get_wallet_summary(0), None); + + // Create fake CompactBlocks sending value to the address + let value = Zatoshis::const_from_u64(5); + let value2 = Zatoshis::const_from_u64(7); + let (h, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); + st.generate_next_block(&dfvk, AddressType::DefaultExternal, value2); + + // Scan the cache + st.scan_cached_blocks(h, 2); + + // Spendable balance should reflect both received notes + assert_eq!( + st.get_spendable_balance(account.id(), 1), + (value + value2).unwrap() + ); + + // "Rewind" to height of last scanned block (this is a no-op) + st.wallet_mut().truncate_to_height(h + 1).unwrap(); + + // Spendable balance should be unaltered + assert_eq!( + st.get_spendable_balance(account.id(), 1), + (value + value2).unwrap() + ); + + // Rewind so that one block is dropped + st.wallet_mut().truncate_to_height(h).unwrap(); + + // Spendable balance should only contain the first received note; + // the rest should be pending. + assert_eq!(st.get_spendable_balance(account.id(), 1), value); + assert_eq!(st.get_pending_shielded_balance(account.id(), 1), value2); + + // Scan the cache again + st.scan_cached_blocks(h, 2); + + // Account balance should again reflect both received notes + assert_eq!( + st.get_spendable_balance(account.id(), 1), + (value + value2).unwrap() + ); +} + +pub fn reorg_to_checkpoint(ds_factory: DSF, cache: C) +where + DSF: DataStoreFactory, + ::AccountId: std::fmt::Debug, + C: TestCache, +{ + let mut st = TestBuilder::new() + .with_data_store_factory(ds_factory) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account = st.test_account().cloned().unwrap(); + + // Create a sequence of blocks to serve as the foundation of our chain state. + let p0_fvk = T::random_fvk(st.rng_mut()); + let gen_random_block = |st: &mut TestState, + output_count: usize| { + let fake_outputs = + std::iter::repeat_with(|| FakeCompactOutput::random(st.rng_mut(), p0_fvk.clone())) + .take(output_count) + .collect::>(); + st.generate_next_block_multi(&fake_outputs[..]); + output_count + }; + + // The stable portion of the tree will contain 20 notes. + for _ in 0..10 { + gen_random_block(&mut st, 4); + } + + // We will reorg to this height. + let reorg_height = account.birthday().height() + 4; + let reorg_position = Position::from(19); + + // Scan the first 5 blocks. The last block in this sequence will be where we simulate a + // reorg. + st.scan_cached_blocks(account.birthday().height(), 5); + assert_eq!( + st.wallet() + .block_max_scanned() + .unwrap() + .unwrap() + .block_height(), + reorg_height + ); + + // There will be 6 checkpoints: one for the prior block frontier, and then one for each scanned + // block. + let checkpoints = st + .wallet() + .get_checkpoint_history(&T::SHIELDED_PROTOCOL) + .unwrap(); + assert_eq!(checkpoints.len(), 6); + assert_eq!( + checkpoints.last(), + Some(&(reorg_height, Some(reorg_position))) + ); + + // Scan another block, then simulate a reorg. + st.scan_cached_blocks(reorg_height + 1, 1); + assert_eq!( + st.wallet() + .block_max_scanned() + .unwrap() + .unwrap() + .block_height(), + reorg_height + 1 + ); + let checkpoints = st + .wallet() + .get_checkpoint_history(&T::SHIELDED_PROTOCOL) + .unwrap(); + assert_eq!(checkpoints.len(), 7); + assert_eq!( + checkpoints.last(), + Some(&(reorg_height + 1, Some(reorg_position + 4))) + ); + + // /\ /\ /\ + // .... /\/\/\/\/\/\ + // c d e + + // Truncate back to the reorg height, but retain the block cache. + st.truncate_to_height_retaining_cache(reorg_height); + + // The following error-prone tree state is generated by the a previous (buggy) truncate + // implementation: + // /\ /\ + // .... /\/\/\/\ + // c + + // We have pruned back to the original checkpoints & tree state. + let checkpoints = st + .wallet() + .get_checkpoint_history(&T::SHIELDED_PROTOCOL) + .unwrap(); + assert_eq!(checkpoints.len(), 6); + assert_eq!( + checkpoints.last(), + Some(&(reorg_height, Some(reorg_position))) + ); + + // Skip two blocks, then (re) scan the same block. + st.scan_cached_blocks(reorg_height + 2, 1); + + // Given the buggy truncation, this would result in this the following tree state: + // /\ /\ \ /\ + // .... /\/\/\/\ \/\/\ + // c e f + + let checkpoints = st + .wallet() + .get_checkpoint_history(&T::SHIELDED_PROTOCOL) + .unwrap(); + // Even though we only scanned one block, we get a checkpoint at both the start and the end of + // the block due to the insertion of the prior block frontier. + assert_eq!(checkpoints.len(), 8); + assert_eq!( + checkpoints.last(), + Some(&(reorg_height + 2, Some(reorg_position + 8))) + ); + + // Now, fully truncate back to the reorg height. This should leave the tree in a state + // where it can be added to with arbitrary notes. + st.truncate_to_height(reorg_height); + + // Generate some new random blocks + for _ in 0..10 { + let output_count = st.rng_mut().gen_range(2..10); + gen_random_block(&mut st, output_count); + } + + // The previous truncation retained the cache, so re-scanning the same blocks would have + // resulted in the same note commitment tree state, and hence no conflicts; could occur. Now + // that we have cleared the cache and generated a different sequence blocks, if truncation did + // not completely clear the tree state this would generates a note commitment tree conflict. + st.scan_cached_blocks(reorg_height + 1, 1); +} + +pub fn scan_cached_blocks_allows_blocks_out_of_order( + ds_factory: impl DataStoreFactory, + cache: impl TestCache, +) { + let mut st = TestBuilder::new() + .with_data_store_factory(ds_factory) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account = st.test_account().cloned().unwrap(); + let dfvk = T::test_account_fvk(&st); + + let value = Zatoshis::const_from_u64(50000); + let (h1, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); + st.scan_cached_blocks(h1, 1); + assert_eq!(st.get_total_balance(account.id()), value); + + // Create blocks to reach height + 2 + let (h2, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); + let (h3, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); + + // Scan the later block first + st.scan_cached_blocks(h3, 1); + + // Now scan the block of height height + 1 + st.scan_cached_blocks(h2, 1); + assert_eq!( + st.get_total_balance(account.id()), + Zatoshis::const_from_u64(150_000) + ); + + // We can spend the received notes + let req = TransactionRequest::new(vec![Payment::without_memo( + T::fvk_default_address(&dfvk).to_zcash_address(st.network()), + Zatoshis::const_from_u64(110_000), + )]) + .unwrap(); + + let input_selector = GreedyInputSelector::new(); + let change_strategy = + single_output_change_strategy(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL); + + assert_matches!( + st.spend( + &input_selector, + &change_strategy, + account.usk(), + req, + OvkPolicy::Sender, + NonZeroU32::new(1).unwrap(), + ), + Ok(_) + ); +} + +pub fn scan_cached_blocks_finds_received_notes( + ds_factory: DSF, + cache: impl TestCache, +) where + DSF: DataStoreFactory, + ::AccountId: std::fmt::Debug, +{ + let mut st = TestBuilder::new() + .with_data_store_factory(ds_factory) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account = st.test_account().cloned().unwrap(); + let dfvk = T::test_account_fvk(&st); + + // Wallet summary is not yet available + assert_eq!(st.get_wallet_summary(0), None); + + // Create a fake CompactBlock sending value to the address + let value = Zatoshis::const_from_u64(5); + let (h1, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); + + // Scan the cache + let summary = st.scan_cached_blocks(h1, 1); + assert_eq!(summary.scanned_range().start, h1); + assert_eq!(summary.scanned_range().end, h1 + 1); + assert_eq!(T::received_note_count(&summary), 1); + + // Account balance should reflect the received note + assert_eq!(st.get_total_balance(account.id()), value); + + // Create a second fake CompactBlock sending more value to the address + let value2 = Zatoshis::const_from_u64(7); + let (h2, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value2); + + // Scan the cache again + let summary = st.scan_cached_blocks(h2, 1); + assert_eq!(summary.scanned_range().start, h2); + assert_eq!(summary.scanned_range().end, h2 + 1); + assert_eq!(T::received_note_count(&summary), 1); + + // Account balance should reflect both received notes + assert_eq!( + st.get_total_balance(account.id()), + (value + value2).unwrap() + ); +} + +// TODO: This test can probably be entirely removed, as the following test duplicates it entirely. +pub fn scan_cached_blocks_finds_change_notes( + ds_factory: DSF, + cache: impl TestCache, +) where + DSF: DataStoreFactory, + ::AccountId: std::fmt::Debug, +{ + let mut st = TestBuilder::new() + .with_data_store_factory(ds_factory) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account = st.test_account().cloned().unwrap(); + let dfvk = T::test_account_fvk(&st); + + // Wallet summary is not yet available + assert_eq!(st.get_wallet_summary(0), None); + + // Create a fake CompactBlock sending value to the address + let value = Zatoshis::const_from_u64(5); + let (received_height, _, nf) = + st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); + + // Scan the cache + st.scan_cached_blocks(received_height, 1); + + // Account balance should reflect the received note + assert_eq!(st.get_total_balance(account.id()), value); + + // Create a second fake CompactBlock spending value from the address + let not_our_key = T::sk_to_fvk(&T::sk(&[0xf5; 32])); + let to2 = T::fvk_default_address(¬_our_key); + let value2 = Zatoshis::const_from_u64(2); + let (spent_height, _) = st.generate_next_block_spending(&dfvk, (nf, value), to2, value2); + + // Scan the cache again + st.scan_cached_blocks(spent_height, 1); + + // Account balance should equal the change + assert_eq!( + st.get_total_balance(account.id()), + (value - value2).unwrap() + ); +} + +pub fn scan_cached_blocks_detects_spends_out_of_order( + ds_factory: DSF, + cache: impl TestCache, +) where + DSF: DataStoreFactory, + ::AccountId: std::fmt::Debug, +{ + let mut st = TestBuilder::new() + .with_data_store_factory(ds_factory) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account = st.test_account().cloned().unwrap(); + let dfvk = T::test_account_fvk(&st); + + // Wallet summary is not yet available + assert_eq!(st.get_wallet_summary(0), None); + + // Create a fake CompactBlock sending value to the address + let value = Zatoshis::const_from_u64(5); + let (received_height, _, nf) = + st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); + + // Create a second fake CompactBlock spending value from the address + let not_our_key = T::sk_to_fvk(&T::sk(&[0xf5; 32])); + let to2 = T::fvk_default_address(¬_our_key); + let value2 = Zatoshis::const_from_u64(2); + let (spent_height, _) = st.generate_next_block_spending(&dfvk, (nf, value), to2, value2); + + // Scan the spending block first. + st.scan_cached_blocks(spent_height, 1); + + // Account balance should equal the change + assert_eq!( + st.get_total_balance(account.id()), + (value - value2).unwrap() + ); + + // Now scan the block in which we received the note that was spent. + st.scan_cached_blocks(received_height, 1); + + // Account balance should be the same. + assert_eq!( + st.get_total_balance(account.id()), + (value - value2).unwrap() + ); +} + +pub fn metadata_queries_exclude_unwanted_notes( + ds_factory: DSF, + cache: TC, +) where + DSF: DataStoreFactory, + ::AccountId: std::fmt::Debug, + TC: TestCache, +{ + let mut st = TestBuilder::new() + .with_data_store_factory(ds_factory) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account = st.test_account().cloned().unwrap(); + let dfvk = T::test_account_fvk(&st); + + // Create 10 blocks with successively increasing value + let value = Zatoshis::const_from_u64(100_0000); + let (h0, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); + let mut note_values = vec![value]; + for i in 2..=10 { + let value = Zatoshis::const_from_u64(i * 100_0000); + st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); + note_values.push(value); + } + st.scan_cached_blocks(h0, 10); + + let test_meta = |st: &TestState, query, expected_count| { + let metadata = st + .wallet() + .get_account_metadata(account.id(), &query, &[]) + .unwrap(); + + assert_eq!(metadata.note_count(T::SHIELDED_PROTOCOL), expected_count); + }; + + test_meta( + &st, + NoteFilter::ExceedsMinValue(Zatoshis::const_from_u64(1000_0000)), + Some(1), + ); + test_meta( + &st, + NoteFilter::ExceedsMinValue(Zatoshis::const_from_u64(500_0000)), + Some(6), + ); + test_meta( + &st, + NoteFilter::ExceedsBalancePercentage(BoundedU8::new_const(10)), + Some(5), + ); + + // We haven't sent any funds yet, so we can't evaluate this query + test_meta( + &st, + NoteFilter::ExceedsPriorSendPercentile(BoundedU8::new_const(50)), + None, + ); + + // Spend half of each one of our notes, so that we can get a distribution of sent note values. + // FIXME: This test is currently excessively specialized to the `zcash_client_sqlite::WalletDb` + // implmentation of the `InputSource` trait. A better approach would be to create a test input + // source that can select a set of notes directly based upon their nullifiers. + let not_our_key = T::sk_to_fvk(&T::sk(&[0xf5; 32])); + let to = T::fvk_default_address(¬_our_key).to_zcash_address(st.network()); + let nz2 = NonZeroU64::new(2).unwrap(); + + for value in ¬e_values { + let txids = st + .create_standard_transaction(&account, to.clone(), *value / nz2) + .unwrap(); + st.generate_next_block_including(txids.head); + } + st.scan_cached_blocks(h0 + 10, 10); + + // Since we've spent half our notes, our remaining notes each have approximately half their + // original value. The 50th percentile of our spends should be 250_0000 ZAT, and half of our + // remaining notes should have value greater than that. + test_meta( + &st, + NoteFilter::ExceedsPriorSendPercentile(BoundedU8::new_const(50)), + Some(5), + ); +} + +#[cfg(feature = "pczt")] +pub fn pczt_single_step( + ds_factory: DSF, + cache: impl TestCache, +) where + DSF: DataStoreFactory, + ::AccountId: serde::Serialize + serde::de::DeserializeOwned, +{ + use zcash_protocol::consensus::ZIP212_GRACE_PERIOD; + + let mut st = TestBuilder::new() + .with_data_store_factory(ds_factory) + .with_block_cache(cache) + .with_initial_chain_state(|_, network| { + // Initialize the chain state to after ZIP 212 became enforced. + let birthday_height = std::cmp::max( + network.activation_height(NetworkUpgrade::Nu5).unwrap(), + network.activation_height(NetworkUpgrade::Canopy).unwrap() + ZIP212_GRACE_PERIOD, + ); + + InitialChainState { + chain_state: ChainState::new( + birthday_height - 1, + BlockHash([5; 32]), + Frontier::empty(), + #[cfg(feature = "orchard")] + Frontier::empty(), + ), + prior_sapling_roots: vec![], + #[cfg(feature = "orchard")] + prior_orchard_roots: vec![], + } + }) + .with_account_having_current_birthday() + .build(); + + let account = st.test_account().cloned().unwrap(); + + let p0_fvk = P0::test_account_fvk(&st); + + let p1_fvk = P1::test_account_fvk(&st); + let p1_to = P1::fvk_default_address(&p1_fvk); + + // Only mine a block in P0 to ensure the transactions source is there. + let note_value = Zatoshis::const_from_u64(350000); + st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value); + st.scan_cached_blocks(account.birthday().height(), 1); + + assert_eq!(st.get_total_balance(account.id()), note_value); + assert_eq!(st.get_spendable_balance(account.id(), 1), note_value); + + let transfer_amount = Zatoshis::const_from_u64(200000); + let p0_to_p1 = TransactionRequest::new(vec![Payment::without_memo( + p1_to.to_zcash_address(st.network()), + transfer_amount, + )]) + .unwrap(); + + let input_selector = GreedyInputSelector::new(); + let change_strategy = + single_output_change_strategy(StandardFeeRule::Zip317, None, P0::SHIELDED_PROTOCOL); + let proposal0 = st + .propose_transfer( + account.id(), + &input_selector, + &change_strategy, + p0_to_p1, + NonZeroU32::new(1).unwrap(), + ) + .unwrap(); + + let _min_target_height = proposal0.min_target_height(); + assert_eq!(proposal0.steps().len(), 1); + + let create_proposed_result = st.create_pczt_from_proposal::( + account.id(), + OvkPolicy::Sender, + &proposal0, + ); + assert_matches!(&create_proposed_result, Ok(_)); + let pczt_created = create_proposed_result.unwrap(); + + // If we don't create proofs or signatures, we will fail to extract a transaction. + assert_matches!( + st.extract_and_store_transaction_from_pczt(pczt_created.clone()), + Err(Error::Pczt(data_api::error::PcztError::Extraction(_))) + ); + + // Add proof generation keys to Sapling spends. + let pczt_updated = P0::add_proof_generation_keys(pczt_created, account.usk()).unwrap(); + + // Create proofs. + let sapling_prover = LocalTxProver::bundled(); + let orchard_pk = ::orchard::circuit::ProvingKey::build(); + let pczt_proven = Prover::new(pczt_updated) + .create_orchard_proof(&orchard_pk) + .unwrap() + .create_sapling_proofs(&sapling_prover, &sapling_prover) + .unwrap() + .finish(); + + // Apply signatures. + let mut signer = Signer::new(pczt_proven).unwrap(); + P0::apply_signatures_to_pczt(&mut signer, account.usk()).unwrap(); + let pczt_authorized = signer.finish(); + + // Now we can extract the transaction. + let extract_and_store_result = st.extract_and_store_transaction_from_pczt(pczt_authorized); + assert_matches!(&extract_and_store_result, Ok(_)); + let txid = extract_and_store_result.unwrap(); + + let (h, _) = st.generate_next_block_including(txid); + st.scan_cached_blocks(h, 1); +} diff --git a/zcash_client_backend/src/data_api/testing/sapling.rs b/zcash_client_backend/src/data_api/testing/sapling.rs new file mode 100644 index 0000000000..08b737923b --- /dev/null +++ b/zcash_client_backend/src/data_api/testing/sapling.rs @@ -0,0 +1,210 @@ +use std::hash::Hash; + +use incrementalmerkletree::{Hashable, Level}; +use sapling::{ + note_encryption::try_sapling_output_recovery, + zip32::{DiversifiableFullViewingKey, ExtendedSpendingKey}, +}; +use shardtree::error::ShardTreeError; +use zcash_keys::{address::Address, keys::UnifiedSpendingKey}; +use zcash_primitives::transaction::{components::sapling::zip212_enforcement, Transaction}; +use zcash_protocol::{ + consensus::{self, BlockHeight}, + memo::MemoBytes, + value::Zatoshis, + ShieldedProtocol, +}; +use zip32::Scope; + +use crate::{ + data_api::{ + chain::{CommitmentTreeRoot, ScanSummary}, + DecryptedTransaction, InputSource, WalletCommitmentTrees, WalletSummary, WalletTest, + }, + wallet::{Note, ReceivedNote}, +}; + +use super::{pool::ShieldedPoolTester, TestState}; + +/// Type for running pool-agnostic tests on the Sapling pool. +pub struct SaplingPoolTester; +impl ShieldedPoolTester for SaplingPoolTester { + const SHIELDED_PROTOCOL: ShieldedProtocol = ShieldedProtocol::Sapling; + // const MERKLE_TREE_DEPTH: u8 = sapling::NOTE_COMMITMENT_TREE_DEPTH; + + type Sk = ExtendedSpendingKey; + type Fvk = DiversifiableFullViewingKey; + type MerkleTreeHash = sapling::Node; + type Note = sapling::Note; + + fn test_account_fvk( + st: &TestState, + ) -> Self::Fvk { + st.test_account_sapling().unwrap().clone() + } + + fn usk_to_sk(usk: &UnifiedSpendingKey) -> &Self::Sk { + usk.sapling() + } + + fn sk(seed: &[u8]) -> Self::Sk { + ExtendedSpendingKey::master(seed) + } + + fn sk_to_fvk(sk: &Self::Sk) -> Self::Fvk { + sk.to_diversifiable_full_viewing_key() + } + + fn sk_default_address(sk: &Self::Sk) -> Address { + sk.default_address().1.into() + } + + fn fvk_default_address(fvk: &Self::Fvk) -> Address { + fvk.default_address().1.into() + } + + fn fvks_equal(a: &Self::Fvk, b: &Self::Fvk) -> bool { + a.to_bytes() == b.to_bytes() + } + + fn empty_tree_leaf() -> Self::MerkleTreeHash { + ::sapling::Node::empty_leaf() + } + + fn empty_tree_root(level: Level) -> Self::MerkleTreeHash { + ::sapling::Node::empty_root(level) + } + + fn put_subtree_roots( + st: &mut TestState, + start_index: u64, + roots: &[CommitmentTreeRoot], + ) -> Result<(), ShardTreeError<::Error>> { + st.wallet_mut() + .put_sapling_subtree_roots(start_index, roots) + } + + fn next_subtree_index(s: &WalletSummary) -> u64 { + s.next_sapling_subtree_index() + } + + fn select_spendable_notes( + st: &TestState, + account: ::AccountId, + target_value: Zatoshis, + anchor_height: BlockHeight, + exclude: &[DbT::NoteRef], + ) -> Result>, ::Error> { + st.wallet() + .select_spendable_notes( + account, + target_value, + &[ShieldedProtocol::Sapling], + anchor_height, + exclude, + ) + .map(|n| n.take_sapling()) + } + + fn decrypted_pool_outputs_count(d_tx: &DecryptedTransaction<'_, A>) -> usize { + d_tx.sapling_outputs().len() + } + + fn with_decrypted_pool_memos( + d_tx: &DecryptedTransaction<'_, A>, + mut f: impl FnMut(&MemoBytes), + ) { + for output in d_tx.sapling_outputs() { + f(output.memo()); + } + } + + fn try_output_recovery( + params: &P, + height: BlockHeight, + tx: &Transaction, + fvk: &Self::Fvk, + ) -> Option<(Note, Address, MemoBytes)> { + for output in tx.sapling_bundle().unwrap().shielded_outputs() { + // Find the output that decrypts with the external OVK + let result = try_sapling_output_recovery( + &fvk.to_ovk(Scope::External), + output, + zip212_enforcement(params, height), + ); + + if result.is_some() { + return result.map(|(note, addr, memo)| { + ( + Note::Sapling(note), + addr.into(), + MemoBytes::from_bytes(&memo).expect("correct length"), + ) + }); + } + } + + None + } + + fn received_note_count(summary: &ScanSummary) -> usize { + summary.received_sapling_note_count() + } + + #[cfg(feature = "pczt")] + fn add_proof_generation_keys( + pczt: pczt::Pczt, + usk: &UnifiedSpendingKey, + ) -> Result { + let extsk = Self::usk_to_sk(usk); + + Ok(pczt::roles::updater::Updater::new(pczt) + .update_sapling_with(|mut updater| { + let non_dummy_spends = updater + .bundle() + .spends() + .iter() + .enumerate() + .filter_map(|(index, spend)| { + // Dummy spends will already have a proof generation key. + spend.proof_generation_key().is_none().then_some(index) + }) + .collect::>(); + + // Assume all non-dummy spent notes are from the same account. + for index in non_dummy_spends { + updater.update_spend_with(index, |mut spend_updater| { + spend_updater.set_proof_generation_key(extsk.expsk.proof_generation_key()) + })?; + } + + Ok(()) + })? + .finish()) + } + + #[cfg(feature = "pczt")] + fn apply_signatures_to_pczt( + signer: &mut pczt::roles::signer::Signer, + usk: &UnifiedSpendingKey, + ) -> Result<(), pczt::roles::signer::Error> { + let extsk = Self::usk_to_sk(usk); + + // Figuring out which one is for us is hard. Let's just try signing all of them! + for index in 0.. { + match signer.sign_sapling(index, &extsk.expsk.ask) { + // Loop termination. + Err(pczt::roles::signer::Error::InvalidIndex) => break, + // Ignore any errors due to using the wrong key. + Ok(()) + | Err(pczt::roles::signer::Error::SaplingSign( + sapling::pczt::SignerError::WrongSpendAuthorizingKey, + )) => Ok(()), + // Raise any unexpected errors. + Err(e) => Err(e), + }?; + } + + Ok(()) + } +} diff --git a/zcash_client_backend/src/data_api/testing/transparent.rs b/zcash_client_backend/src/data_api/testing/transparent.rs new file mode 100644 index 0000000000..5bacc88b04 --- /dev/null +++ b/zcash_client_backend/src/data_api/testing/transparent.rs @@ -0,0 +1,369 @@ +use crate::{ + data_api::{ + testing::{ + AddressType, DataStoreFactory, ShieldedProtocol, TestBuilder, TestCache, TestState, + }, + wallet::input_selection::GreedyInputSelector, + Account as _, Balance, InputSource, WalletRead, WalletWrite, + }, + fees::{standard, DustOutputPolicy, StandardFeeRule}, + wallet::WalletTransparentOutput, +}; +use assert_matches::assert_matches; + +use ::transparent::{ + address::TransparentAddress, + bundle::{OutPoint, TxOut}, +}; +use sapling::zip32::ExtendedSpendingKey; +use zcash_primitives::block::BlockHash; +use zcash_protocol::{local_consensus::LocalNetwork, value::Zatoshis}; + +use super::TestAccount; + +/// Checks whether the transparent balance of the given test `account` is as `expected` +/// considering the `min_confirmations`. It is assumed that zero or one `min_confirmations` +/// are treated the same, and so this function also checks the other case when +/// `min_confirmations` is 0 or 1. +fn check_balance( + st: &TestState::DataStore, LocalNetwork>, + account: &TestAccount<::Account>, + taddr: &TransparentAddress, + min_confirmations: u32, + expected: &Balance, +) where + DSF: DataStoreFactory, +{ + // Check the wallet summary returns the expected transparent balance. + let summary = st + .wallet() + .get_wallet_summary(min_confirmations) + .unwrap() + .unwrap(); + let balance = summary.account_balances().get(&account.id()).unwrap(); + + #[allow(deprecated)] + let old_unshielded_value = balance.unshielded(); + assert_eq!(old_unshielded_value, expected.total()); + assert_eq!(balance.unshielded_balance(), expected); + + // Check the older APIs for consistency. + let mempool_height = st.wallet().chain_height().unwrap().unwrap() + 1; + assert_eq!( + st.wallet() + .get_transparent_balances(account.id(), mempool_height) + .unwrap() + .get(taddr) + .cloned() + .unwrap_or(Zatoshis::ZERO), + expected.total(), + ); + assert_eq!( + st.wallet() + .get_spendable_transparent_outputs(taddr, mempool_height, min_confirmations) + .unwrap() + .into_iter() + .map(|utxo| utxo.value()) + .sum::>(), + Some(expected.spendable_value()), + ); + + // we currently treat min_confirmations the same regardless they are 0 (zero confirmations) + // or 1 (one block confirmation). We will check if this assumption holds until it's no + // longer made. If zero and one [`min_confirmations`] are treated differently in the future, + // this check should then be removed. + if min_confirmations == 0 || min_confirmations == 1 { + assert_eq!( + st.wallet() + .get_spendable_transparent_outputs(taddr, mempool_height, 1 - min_confirmations) + .unwrap() + .into_iter() + .map(|utxo| utxo.value()) + .sum::>(), + Some(expected.spendable_value()), + ); + } +} + +pub fn put_received_transparent_utxo(dsf: DSF) +where + DSF: DataStoreFactory, + <::DataStore as WalletWrite>::UtxoRef: std::fmt::Debug + PartialEq, +{ + let mut st = TestBuilder::new() + .with_data_store_factory(dsf) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let birthday = st.test_account().unwrap().birthday().height(); + let account_id = st.test_account().unwrap().id(); + let uaddr = st + .wallet() + .get_current_address(account_id) + .unwrap() + .unwrap(); + let taddr = uaddr.transparent().unwrap(); + + let height_1 = birthday + 12345; + st.wallet_mut().update_chain_tip(height_1).unwrap(); + + let bal_absent = st + .wallet() + .get_transparent_balances(account_id, height_1) + .unwrap(); + assert!(bal_absent.is_empty()); + + // Create a fake transparent output. + let value = Zatoshis::const_from_u64(100000); + let outpoint = OutPoint::fake(); + let txout = TxOut { + value, + script_pubkey: taddr.script(), + }; + + // Pretend the output's transaction was mined at `height_1`. + let utxo = WalletTransparentOutput::from_parts(outpoint.clone(), txout.clone(), Some(height_1)) + .unwrap(); + let res0 = st.wallet_mut().put_received_transparent_utxo(&utxo); + assert_matches!(res0, Ok(_)); + + // Confirm that we see the output unspent as of `height_1`. + assert_matches!( + st.wallet().get_spendable_transparent_outputs( + taddr, + height_1, + 0 + ).as_deref(), + Ok([ret]) if (ret.outpoint(), ret.txout(), ret.mined_height()) == (utxo.outpoint(), utxo.txout(), Some(height_1)) + ); + assert_matches!( + st.wallet().get_unspent_transparent_output(utxo.outpoint()), + Ok(Some(ret)) if (ret.outpoint(), ret.txout(), ret.mined_height()) == (utxo.outpoint(), utxo.txout(), Some(height_1)) + ); + + // Change the mined height of the UTXO and upsert; we should get back + // the same `UtxoId`. + let height_2 = birthday + 34567; + st.wallet_mut().update_chain_tip(height_2).unwrap(); + let utxo2 = WalletTransparentOutput::from_parts(outpoint, txout, Some(height_2)).unwrap(); + let res1 = st.wallet_mut().put_received_transparent_utxo(&utxo2); + assert_matches!(res1, Ok(id) if id == res0.unwrap()); + + // Confirm that we no longer see any unspent outputs as of `height_1`. + assert_matches!( + st.wallet() + .get_spendable_transparent_outputs(taddr, height_1, 0) + .as_deref(), + Ok(&[]) + ); + + // We can still look up the specific output, and it has the expected height. + assert_matches!( + st.wallet().get_unspent_transparent_output(utxo2.outpoint()), + Ok(Some(ret)) if (ret.outpoint(), ret.txout(), ret.mined_height()) == (utxo2.outpoint(), utxo2.txout(), Some(height_2)) + ); + + // If we include `height_2` then the output is returned. + assert_matches!( + st.wallet() + .get_spendable_transparent_outputs(taddr, height_2, 0) + .as_deref(), + Ok([ret]) if (ret.outpoint(), ret.txout(), ret.mined_height()) == (utxo.outpoint(), utxo.txout(), Some(height_2)) + ); + + assert_matches!( + st.wallet().get_transparent_balances(account_id, height_2), + Ok(h) if h.get(taddr) == Some(&value) + ); +} + +pub fn transparent_balance_across_shielding(dsf: DSF, cache: impl TestCache) +where + DSF: DataStoreFactory, +{ + let mut st = TestBuilder::new() + .with_data_store_factory(dsf) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account = st.test_account().cloned().unwrap(); + let uaddr = st + .wallet() + .get_current_address(account.id()) + .unwrap() + .unwrap(); + let taddr = uaddr.transparent().unwrap(); + + // Initialize the wallet with chain data that has no shielded notes for us. + let not_our_key = ExtendedSpendingKey::master(&[]).to_diversifiable_full_viewing_key(); + let not_our_value = Zatoshis::const_from_u64(10000); + let (start_height, _, _) = + st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value); + for _ in 1..10 { + st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value); + } + st.scan_cached_blocks(start_height, 10); + + // The wallet starts out with zero balance. + check_balance::(&st, &account, taddr, 0, &Balance::ZERO); + + // Create a fake transparent output. + let value = Zatoshis::from_u64(100000).unwrap(); + let txout = TxOut { + value, + script_pubkey: taddr.script(), + }; + + // Pretend the output was received in the chain tip. + let height = st.wallet().chain_height().unwrap().unwrap(); + let utxo = WalletTransparentOutput::from_parts(OutPoint::fake(), txout, Some(height)).unwrap(); + st.wallet_mut() + .put_received_transparent_utxo(&utxo) + .unwrap(); + + // The wallet should detect the balance as available + let mut zero_or_one_conf_value = Balance::ZERO; + + // add the spendable value to the expected balance + zero_or_one_conf_value.add_spendable_value(value).unwrap(); + + check_balance::(&st, &account, taddr, 0, &zero_or_one_conf_value); + + // Shield the output. + let input_selector = GreedyInputSelector::new(); + let change_strategy = standard::SingleOutputChangeStrategy::new( + StandardFeeRule::Zip317, + None, + ShieldedProtocol::Sapling, + DustOutputPolicy::default(), + ); + let txid = st + .shield_transparent_funds( + &input_selector, + &change_strategy, + value, + account.usk(), + &[*taddr], + account.id(), + 1, + ) + .unwrap()[0]; + + // The wallet should have zero transparent balance, because the shielding + // transaction can be mined. + check_balance::(&st, &account, taddr, 0, &Balance::ZERO); + + // Mine the shielding transaction. + let (mined_height, _) = st.generate_next_block_including(txid); + st.scan_cached_blocks(mined_height, 1); + + // The wallet should still have zero transparent balance. + check_balance::(&st, &account, taddr, 0, &Balance::ZERO); + + // Unmine the shielding transaction via a reorg. + st.wallet_mut() + .truncate_to_height(mined_height - 1) + .unwrap(); + assert_eq!(st.wallet().chain_height().unwrap(), Some(mined_height - 1)); + + // The wallet should still have zero transparent balance. + check_balance::(&st, &account, taddr, 0, &Balance::ZERO); + + // Expire the shielding transaction. + let expiry_height = st + .wallet() + .get_transaction(txid) + .unwrap() + .expect("Transaction exists in the wallet.") + .expiry_height(); + st.wallet_mut().update_chain_tip(expiry_height).unwrap(); + + check_balance::(&st, &account, taddr, 0, &zero_or_one_conf_value); +} + +/// This test attempts to verify that transparent funds spendability is +/// accounted for properly given the different minimum confirmations values +/// that can be set when querying for balances. +pub fn transparent_balance_spendability(dsf: DSF, cache: impl TestCache) +where + DSF: DataStoreFactory, +{ + let mut st = TestBuilder::new() + .with_data_store_factory(dsf) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account = st.test_account().cloned().unwrap(); + let uaddr = st + .wallet() + .get_current_address(account.id()) + .unwrap() + .unwrap(); + let taddr = uaddr.transparent().unwrap(); + + // Initialize the wallet with chain data that has no shielded notes for us. + let not_our_key = ExtendedSpendingKey::master(&[]).to_diversifiable_full_viewing_key(); + let not_our_value = Zatoshis::const_from_u64(10000); + let (start_height, _, _) = + st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value); + for _ in 1..10 { + st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value); + } + st.scan_cached_blocks(start_height, 10); + + // The wallet starts out with zero balance. + check_balance::( + &st as &TestState<_, DSF::DataStore, _>, + &account, + taddr, + 0, + &Balance::ZERO, + ); + + // Create a fake transparent output. + let value = Zatoshis::from_u64(100000).unwrap(); + let txout = TxOut { + value, + script_pubkey: taddr.script(), + }; + + // Pretend the output was received in the chain tip. + let height = st.wallet().chain_height().unwrap().unwrap(); + let utxo = WalletTransparentOutput::from_parts(OutPoint::fake(), txout, Some(height)).unwrap(); + st.wallet_mut() + .put_received_transparent_utxo(&utxo) + .unwrap(); + + // The wallet should detect the balance as available + let mut zero_or_one_conf_value = Balance::ZERO; + + // add the spendable value to the expected balance + zero_or_one_conf_value.add_spendable_value(value).unwrap(); + + check_balance::(&st, &account, taddr, 0, &zero_or_one_conf_value); + + // now if we increase the number of confirmations our spendable balance should + // be zero and the total balance equal to `value` + let mut not_confirmed_yet_value = Balance::ZERO; + + not_confirmed_yet_value + .add_pending_spendable_value(value) + .unwrap(); + + check_balance::(&st, &account, taddr, 2, ¬_confirmed_yet_value); + + // Add one extra block + st.generate_empty_block(); + + // Scan that block + st.scan_cached_blocks(height, 1); + + // now we generate one more block and the balance should be the same as when the + // check_balance function was called with zero or one confirmation. + st.generate_empty_block(); + st.scan_cached_blocks(height + 1, 1); + + check_balance::(&st, &account, taddr, 2, &zero_or_one_conf_value); +} diff --git a/zcash_client_backend/src/data_api/wallet.rs b/zcash_client_backend/src/data_api/wallet.rs index 82a9a9a9b5..a2b8deda26 100644 --- a/zcash_client_backend/src/data_api/wallet.rs +++ b/zcash_client_backend/src/data_api/wallet.rs @@ -1,258 +1,2154 @@ -//! Functions for scanning the chain and extracting relevant information. -use std::fmt::Debug; +//! # Functions for creating Zcash transactions that spend funds belonging to the wallet +//! +//! This module contains several different ways of creating Zcash transactions. This module is +//! designed around the idea that a Zcash wallet holds its funds in notes in either the Orchard +//! or Sapling shielded pool. In order to better preserve users' privacy, it does not provide any +//! functionality that allows users to directly spend transparent funds except by sending them to a +//! shielded internal address belonging to their wallet. +//! +//! The important high-level operations provided by this module are [`propose_transfer`], +//! and [`create_proposed_transactions`]. +//! +//! [`propose_transfer`] takes a [`TransactionRequest`] object, selects inputs notes and +//! computes the fees required to satisfy that request, and returns a [`Proposal`] object that +//! describes the transaction to be made. +//! +//! [`create_proposed_transactions`] constructs one or more Zcash [`Transaction`]s based upon a +//! provided [`Proposal`], stores them to the wallet database, and returns the [`TxId`] for each +//! constructed transaction to the caller. The caller can then use the +//! [`WalletRead::get_transaction`] method to retrieve the newly constructed transactions. It is +//! the responsibility of the caller to retrieve and serialize the transactions and submit them for +//! inclusion into the Zcash blockchain. +//! +#![cfg_attr( + feature = "transparent-inputs", + doc = " +Another important high-level operation provided by this module is [`propose_shielding`], which +takes a set of transparent source addresses, and constructs a [`Proposal`] to send those funds +to a wallet-internal shielded address, as described in [ZIP 316](https://zips.z.cash/zip-0316). -use zcash_primitives::{ - consensus::{self, BranchId, NetworkUpgrade}, - memo::MemoBytes, - sapling::prover::TxProver, - transaction::{ - builder::Builder, - components::{amount::DEFAULT_FEE, Amount}, - Transaction, - }, - zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, -}; +[`propose_shielding`]: crate::data_api::wallet::propose_shielding +" +)] +//! [`TransactionRequest`]: crate::zip321::TransactionRequest +//! [`propose_transfer`]: crate::data_api::wallet::propose_transfer + +use nonempty::NonEmpty; +use rand_core::OsRng; +use std::num::NonZeroU32; + +use shardtree::error::{QueryError, ShardTreeError}; +use super::InputSource; use crate::{ - address::RecipientAddress, - data_api::{error::Error, ReceivedTransaction, SentTransaction, WalletWrite}, + data_api::{ + error::Error, Account, SentTransaction, SentTransactionOutput, WalletCommitmentTrees, + WalletRead, WalletWrite, + }, decrypt_transaction, - wallet::{AccountId, OvkPolicy}, + fees::{ + standard::SingleOutputChangeStrategy, ChangeStrategy, DustOutputPolicy, StandardFeeRule, + }, + proposal::{Proposal, ProposalError, Step, StepOutputIndex}, + wallet::{Note, OvkPolicy, Recipient}, +}; +use ::sapling::{ + note_encryption::{try_sapling_note_decryption, PreparedIncomingViewingKey}, + prover::{OutputProver, SpendProver}, +}; +use ::transparent::{ + address::TransparentAddress, builder::TransparentSigningSet, bundle::OutPoint, }; +use zcash_address::ZcashAddress; +use zcash_keys::{ + address::Address, + keys::{UnifiedFullViewingKey, UnifiedSpendingKey}, +}; +use zcash_primitives::transaction::{ + builder::{BuildConfig, BuildResult, Builder}, + components::sapling::zip212_enforcement, + fees::FeeRule, + Transaction, TxId, +}; +use zcash_protocol::{ + consensus::{self, BlockHeight, NetworkUpgrade}, + memo::MemoBytes, + value::Zatoshis, + PoolType, ShieldedProtocol, +}; +use zip32::Scope; +use zip321::Payment; -pub const ANCHOR_OFFSET: u32 = 10; +#[cfg(feature = "transparent-inputs")] +use { + crate::{fees::ChangeValue, proposal::StepOutput, wallet::TransparentAddressMetadata}, + ::transparent::bundle::TxOut, + core::convert::Infallible, + input_selection::ShieldingSelector, + std::collections::HashMap, + zcash_keys::encoding::AddressCodec, +}; + +#[cfg(feature = "pczt")] +use { + crate::data_api::error::PcztError, + ::transparent::pczt::Bip32Derivation, + bip32::ChildNumber, + orchard::note_encryption::OrchardDomain, + pczt::roles::{ + creator::Creator, io_finalizer::IoFinalizer, spend_finalizer::SpendFinalizer, + tx_extractor::TransactionExtractor, updater::Updater, + }, + sapling::note_encryption::SaplingDomain, + serde::{Deserialize, Serialize}, + zcash_note_encryption::try_output_recovery_with_pkd_esk, + zcash_protocol::{ + consensus::NetworkConstants, + value::{BalanceError, ZatBalance}, + }, +}; + +pub mod input_selection; +use input_selection::{GreedyInputSelector, InputSelector, InputSelectorError}; + +#[cfg(feature = "pczt")] +const PROPRIETARY_PROPOSAL_INFO: &str = "zcash_client_backend:proposal_info"; +#[cfg(feature = "pczt")] +const PROPRIETARY_OUTPUT_INFO: &str = "zcash_client_backend:output_info"; + +/// Information about the proposal from which a PCZT was created. +/// +/// Stored under the proprietary field `PROPRIETARY_PROPOSAL_INFO`. +#[cfg(feature = "pczt")] +#[derive(Serialize, Deserialize)] +struct ProposalInfo { + from_account: AccountId, + target_height: u32, +} + +/// Reduced version of [`Recipient`] stored inside a PCZT. +/// +/// Stored under the proprietary field `PROPRIETARY_OUTPUT_INFO`. +#[cfg(feature = "pczt")] +#[derive(Serialize, Deserialize)] +enum PcztRecipient { + External, + #[cfg(feature = "transparent-inputs")] + EphemeralTransparent { + receiving_account: AccountId, + }, + InternalAccount { + receiving_account: AccountId, + }, +} + +#[cfg(feature = "pczt")] +impl PcztRecipient { + fn from_recipient(recipient: BuildRecipient) -> (Self, Option) { + match recipient { + BuildRecipient::External { + recipient_address, .. + } => (PcztRecipient::External, Some(recipient_address)), + #[cfg(feature = "transparent-inputs")] + BuildRecipient::EphemeralTransparent { + receiving_account, .. + } => ( + PcztRecipient::EphemeralTransparent { receiving_account }, + None, + ), + BuildRecipient::InternalAccount { + receiving_account, + external_address, + } => ( + PcztRecipient::InternalAccount { receiving_account }, + external_address, + ), + } + } +} /// Scans a [`Transaction`] for any information that can be decrypted by the accounts in /// the wallet, and saves it to the wallet. -pub fn decrypt_and_store_transaction( - params: &P, - data: &mut D, +pub fn decrypt_and_store_transaction( + params: &ParamsT, + data: &mut DbT, tx: &Transaction, -) -> Result<(), E> + mined_height: Option, +) -> Result<(), DbT::Error> where - E: From>, - P: consensus::Parameters, - D: WalletWrite, + ParamsT: consensus::Parameters, + DbT: WalletWrite, { - // Fetch the ExtendedFullViewingKeys we are tracking - let extfvks = data.get_extended_full_viewing_keys()?; + // Fetch the UnifiedFullViewingKeys we are tracking + let ufvks = data.get_unified_full_viewing_keys()?; // Height is block height for mined transactions, and the "mempool height" (chain height + 1) // for mempool transactions. - let height = data - .get_tx_height(tx.txid())? - .or(data - .block_height_extrema()? - .map(|(_, max_height)| max_height + 1)) - .or_else(|| params.activation_height(NetworkUpgrade::Sapling)) - .ok_or(Error::SaplingNotActive)?; - - let outputs = decrypt_transaction(params, height, tx, &extfvks); - if outputs.is_empty() { - Ok(()) - } else { - data.store_received_tx(&ReceivedTransaction { - tx, - outputs: &outputs, - })?; + let height = mined_height.map(Ok).unwrap_or_else(|| { + Ok(data + .get_tx_height(tx.txid())? + .or(data.chain_height()?.map(|max_height| max_height + 1)) + .or_else(|| params.activation_height(NetworkUpgrade::Sapling)) + .expect("Sapling activation height must be known.")) + })?; - Ok(()) - } + data.store_decrypted_tx(decrypt_transaction(params, height, tx, &ufvks))?; + + Ok(()) } -#[allow(clippy::needless_doctest_main)] -/// Creates a transaction paying the specified address from the given account. -/// -/// Returns the row index of the newly-created transaction in the `transactions` table -/// within the data database. The caller can read the raw transaction bytes from the `raw` -/// column in order to broadcast the transaction to the network. -/// -/// Do not call this multiple times in parallel, or you will generate transactions that -/// double-spend the same notes. -/// -/// # Transaction privacy -/// -/// `ovk_policy` specifies the desired policy for which outgoing viewing key should be -/// able to decrypt the outputs of this transaction. This is primarily relevant to -/// wallet recovery from backup; in particular, [`OvkPolicy::Discard`] will prevent the -/// recipient's address, and the contents of `memo`, from ever being recovered from the -/// block chain. (The total value sent can always be inferred by the sender from the spent -/// notes and received change.) -/// -/// Regardless of the specified policy, `create_spend_to_address` saves `to`, `value`, and -/// `memo` in `db_data`. This can be deleted independently of `ovk_policy`. -/// -/// For details on what transaction information is visible to the holder of a full or -/// outgoing viewing key, refer to [ZIP 310]. -/// -/// [ZIP 310]: https://zips.z.cash/zip-0310 -/// -/// # Examples -/// -/// ``` -/// use tempfile::NamedTempFile; -/// use zcash_primitives::{ -/// consensus::{self, Network}, -/// constants::testnet::COIN_TYPE, -/// transaction::components::Amount -/// }; -/// use zcash_proofs::prover::LocalTxProver; -/// use zcash_client_backend::{ -/// keys::spending_key, -/// data_api::wallet::create_spend_to_address, -/// wallet::{AccountId, OvkPolicy}, -/// }; -/// use zcash_client_sqlite::{ -/// WalletDb, -/// error::SqliteClientError, -/// wallet::init::init_wallet_db, -/// }; +/// Errors that may be generated in construction of proposals for shielded->shielded or +/// shielded->transparent transfers. +pub type ProposeTransferErrT = Error< + ::Error, + CommitmentTreeErrT, + ::Error, + <::FeeRule as FeeRule>::Error, + ::Error, + <::InputSource as InputSource>::NoteRef, +>; + +/// Errors that may be generated in construction of proposals for transparent->shielded +/// wallet-internal transfers. +#[cfg(feature = "transparent-inputs")] +pub type ProposeShieldingErrT = Error< + ::Error, + CommitmentTreeErrT, + ::Error, + <::FeeRule as FeeRule>::Error, + ::Error, + Infallible, +>; + +/// Errors that may be generated in combined creation and execution of transaction proposals. +pub type CreateErrT = Error< + ::Error, + ::Error, + InputsErrT, + ::Error, + ChangeErrT, + N, +>; + +/// Errors that may be generated in the execution of proposals that may send shielded inputs. +pub type TransferErrT = Error< + ::Error, + ::Error, + ::Error, + <::FeeRule as FeeRule>::Error, + ::Error, + <::InputSource as InputSource>::NoteRef, +>; + +/// Errors that may be generated in the execution of shielding proposals. +#[cfg(feature = "transparent-inputs")] +pub type ShieldErrT = Error< + ::Error, + ::Error, + ::Error, + <::FeeRule as FeeRule>::Error, + ::Error, + Infallible, +>; + +/// Errors that may be generated when extracting a transaction from a PCZT. +#[cfg(feature = "pczt")] +pub type ExtractErrT = Error< + ::Error, + ::Error, + Infallible, + Infallible, + Infallible, + N, +>; + +/// Select transaction inputs, compute fees, and construct a proposal for a transaction or series +/// of transactions that can then be authorized and made ready for submission to the network with +/// [`create_proposed_transactions`]. +#[allow(clippy::too_many_arguments)] +#[allow(clippy::type_complexity)] +pub fn propose_transfer( + wallet_db: &mut DbT, + params: &ParamsT, + spend_from_account: ::AccountId, + input_selector: &InputsT, + change_strategy: &ChangeT, + request: zip321::TransactionRequest, + min_confirmations: NonZeroU32, +) -> Result< + Proposal::NoteRef>, + ProposeTransferErrT, +> +where + DbT: WalletRead + InputSource::Error>, + ::NoteRef: Copy + Eq + Ord, + ParamsT: consensus::Parameters + Clone, + InputsT: InputSelector, + ChangeT: ChangeStrategy, +{ + let (target_height, anchor_height) = wallet_db + .get_target_and_anchor_heights(min_confirmations) + .map_err(|e| Error::from(InputSelectorError::DataSource(e)))? + .ok_or_else(|| Error::from(InputSelectorError::SyncRequired))?; + + input_selector + .propose_transaction( + params, + wallet_db, + target_height, + anchor_height, + spend_from_account, + request, + change_strategy, + ) + .map_err(Error::from) +} + +/// Proposes making a payment to the specified address from the given account. /// -/// # // doctests have a problem with sqlite IO, so we ignore errors -/// # // generated in this example code as it's not really testing anything -/// # fn main() { -/// # test(); -/// # } -/// # -/// # fn test() -> Result<(), SqliteClientError> { -/// let tx_prover = match LocalTxProver::with_default_location() { -/// Some(tx_prover) => tx_prover, -/// None => { -/// panic!("Cannot locate the Zcash parameters. Please run zcash-fetch-params or fetch-params.sh to download the parameters, and then re-run the tests."); -/// } -/// }; +/// Returns the proposal, which may then be executed using [`create_proposed_transactions`]. +/// Depending upon the recipient address, more than one transaction may be constructed +/// in the execution of the returned proposal. /// -/// let account = AccountId(0); -/// let extsk = spending_key(&[0; 32][..], COIN_TYPE, account.0); -/// let to = extsk.default_address().unwrap().1.into(); +/// This method uses the basic [`GreedyInputSelector`] for input selection. /// -/// let data_file = NamedTempFile::new().unwrap(); -/// let db_read = WalletDb::for_path(data_file, Network::TestNetwork).unwrap(); -/// init_wallet_db(&db_read)?; -/// let mut db = db_read.get_update_ops()?; +/// Parameters: +/// * `wallet_db`: A read/write reference to the wallet database. +/// * `params`: Consensus parameters. +/// * `fee_rule`: The fee rule to use in creating the transaction. +/// * `spend_from_account`: The unified account that controls the funds that will be spent +/// in the resulting transaction. This procedure will return an error if the +/// account ID does not correspond to an account known to the wallet. +/// * `min_confirmations`: The minimum number of confirmations that a previously +/// received note must have in the blockchain in order to be considered for being +/// spent. A value of 10 confirmations is recommended and 0-conf transactions are +/// not supported. +/// * `to`: The address to which `amount` will be paid. +/// * `amount`: The amount to send. +/// * `memo`: A memo to be included in the output to the recipient. +/// * `change_memo`: A memo to be included in any change output that is created. +/// * `fallback_change_pool`: The shielded pool to which change should be sent if +/// automatic change pool determination fails. +#[allow(clippy::too_many_arguments)] +#[allow(clippy::type_complexity)] +pub fn propose_standard_transfer_to_address( + wallet_db: &mut DbT, + params: &ParamsT, + fee_rule: StandardFeeRule, + spend_from_account: ::AccountId, + min_confirmations: NonZeroU32, + to: &Address, + amount: Zatoshis, + memo: Option, + change_memo: Option, + fallback_change_pool: ShieldedProtocol, +) -> Result< + Proposal, + ProposeTransferErrT< + DbT, + CommitmentTreeErrT, + GreedyInputSelector, + SingleOutputChangeStrategy, + >, +> +where + ParamsT: consensus::Parameters + Clone, + DbT: InputSource, + DbT: WalletRead< + Error = ::Error, + AccountId = ::AccountId, + >, + DbT::NoteRef: Copy + Eq + Ord, +{ + let request = zip321::TransactionRequest::new(vec![Payment::new( + to.to_zcash_address(params), + amount, + memo, + None, + None, + vec![], + ) + .ok_or(Error::MemoForbidden)?]) + .expect( + "It should not be possible for this to violate ZIP 321 request construction invariants.", + ); + + let input_selector = GreedyInputSelector::::new(); + let change_strategy = SingleOutputChangeStrategy::::new( + fee_rule, + change_memo, + fallback_change_pool, + DustOutputPolicy::default(), + ); + + propose_transfer( + wallet_db, + params, + spend_from_account, + &input_selector, + &change_strategy, + request, + min_confirmations, + ) +} + +/// Constructs a proposal to shield all of the funds belonging to the provided set of +/// addresses. +#[cfg(feature = "transparent-inputs")] +#[allow(clippy::too_many_arguments)] +#[allow(clippy::type_complexity)] +pub fn propose_shielding( + wallet_db: &mut DbT, + params: &ParamsT, + input_selector: &InputsT, + change_strategy: &ChangeT, + shielding_threshold: Zatoshis, + from_addrs: &[TransparentAddress], + to_account: ::AccountId, + min_confirmations: u32, +) -> Result< + Proposal, + ProposeShieldingErrT, +> +where + ParamsT: consensus::Parameters, + DbT: WalletRead + InputSource::Error>, + InputsT: ShieldingSelector, + ChangeT: ChangeStrategy, +{ + let chain_tip_height = wallet_db + .chain_height() + .map_err(|e| Error::from(InputSelectorError::DataSource(e)))? + .ok_or_else(|| Error::from(InputSelectorError::SyncRequired))?; + + input_selector + .propose_shielding( + params, + wallet_db, + change_strategy, + shielding_threshold, + from_addrs, + to_account, + chain_tip_height + 1, + min_confirmations, + ) + .map_err(Error::from) +} + +struct StepResult { + build_result: BuildResult, + outputs: Vec>, + fee_amount: Zatoshis, + #[cfg(feature = "transparent-inputs")] + utxos_spent: Vec, +} + +/// Construct, prove, and sign a transaction or series of transactions using the inputs supplied by +/// the given proposal, and persist it to the wallet database. /// -/// create_spend_to_address( -/// &mut db, -/// &Network::TestNetwork, -/// tx_prover, -/// account, -/// &extsk, -/// &to, -/// Amount::from_u64(1).unwrap(), -/// None, -/// OvkPolicy::Sender, -/// )?; +/// Returns the database identifier for each newly constructed transaction, or an error if +/// an error occurs in transaction construction, proving, or signing. /// -/// # Ok(()) -/// # } -/// ``` +/// When evaluating multi-step proposals, only transparent outputs of any given step may be spent +/// in later steps; attempting to spend a shielded note (including change) output by an earlier +/// step is not supported, because the ultimate positions of those notes in the global note +/// commitment tree cannot be known until the transaction that produces those notes is mined, +/// and therefore the required spend proofs for such notes cannot be constructed. #[allow(clippy::too_many_arguments)] -pub fn create_spend_to_address( - wallet_db: &mut D, - params: &P, - prover: impl TxProver, - account: AccountId, - extsk: &ExtendedSpendingKey, - to: &RecipientAddress, - value: Amount, - memo: Option, +#[allow(clippy::type_complexity)] +pub fn create_proposed_transactions( + wallet_db: &mut DbT, + params: &ParamsT, + spend_prover: &impl SpendProver, + output_prover: &impl OutputProver, + usk: &UnifiedSpendingKey, ovk_policy: OvkPolicy, -) -> Result + proposal: &Proposal, +) -> Result, CreateErrT> where - E: From>, - P: consensus::Parameters + Clone, - R: Copy + Debug, - D: WalletWrite, + DbT: WalletWrite + WalletCommitmentTrees, + ParamsT: consensus::Parameters + Clone, + FeeRuleT: FeeRule, { - // Check that the ExtendedSpendingKey we have been given corresponds to the - // ExtendedFullViewingKey for the account we are spending from. - let extfvk = ExtendedFullViewingKey::from(extsk); - if !wallet_db.is_valid_account_extfvk(account, &extfvk)? { - return Err(E::from(Error::InvalidExtSk(account))); + // The set of transparent `StepOutput`s available and unused from prior steps. + // When a transparent `StepOutput` is created, it is added to the map. When it + // is consumed, it is removed from the map. + #[cfg(feature = "transparent-inputs")] + let mut unused_transparent_outputs = HashMap::new(); + + let account_id = wallet_db + .get_account_for_ufvk(&usk.to_unified_full_viewing_key()) + .map_err(Error::DataSource)? + .ok_or(Error::KeyNotRecognized)? + .id(); + + let mut step_results = Vec::with_capacity(proposal.steps().len()); + for step in proposal.steps() { + let step_result: StepResult<_> = create_proposed_transaction( + wallet_db, + params, + spend_prover, + output_prover, + usk, + account_id, + ovk_policy.clone(), + proposal.fee_rule(), + proposal.min_target_height(), + &step_results, + step, + #[cfg(feature = "transparent-inputs")] + &mut unused_transparent_outputs, + )?; + step_results.push((step, step_result)); + } + + // Ephemeral outputs must be referenced exactly once. + #[cfg(feature = "transparent-inputs")] + for so in unused_transparent_outputs.into_keys() { + if let StepOutputIndex::Change(i) = so.output_index() { + // references have already been checked + if step_results[so.step_index()].0.balance().proposed_change()[i].is_ephemeral() { + return Err(ProposalError::EphemeralOutputLeftUnspent(so).into()); + } + } } + let created = time::OffsetDateTime::now_utc(); + + // Store the transactions only after creating all of them. This avoids undesired + // retransmissions in case a transaction is stored and the creation of a subsequent + // transaction fails. + let mut transactions = Vec::with_capacity(step_results.len()); + let mut txids = Vec::with_capacity(step_results.len()); + #[allow(unused_variables)] + for (_, step_result) in step_results.iter() { + let tx = step_result.build_result.transaction(); + transactions.push(SentTransaction::new( + tx, + created, + proposal.min_target_height(), + account_id, + &step_result.outputs, + step_result.fee_amount, + #[cfg(feature = "transparent-inputs")] + &step_result.utxos_spent, + )); + txids.push(tx.txid()); + } + + wallet_db + .store_transactions_to_be_sent(&transactions) + .map_err(Error::DataSource)?; + + Ok(NonEmpty::from_vec(txids).expect("proposal.steps is NonEmpty")) +} + +#[derive(Debug, Clone)] +enum BuildRecipient { + External { + recipient_address: ZcashAddress, + output_pool: PoolType, + }, + #[cfg(feature = "transparent-inputs")] + EphemeralTransparent { + receiving_account: AccountId, + ephemeral_address: TransparentAddress, + }, + InternalAccount { + receiving_account: AccountId, + external_address: Option, + }, +} + +impl BuildRecipient { + fn into_recipient_with_note(self, note: impl FnOnce() -> Note) -> Recipient { + match self { + BuildRecipient::External { + recipient_address, + output_pool, + } => Recipient::External { + recipient_address, + output_pool, + }, + #[cfg(feature = "transparent-inputs")] + BuildRecipient::EphemeralTransparent { .. } => unreachable!(), + BuildRecipient::InternalAccount { + receiving_account, + external_address, + } => Recipient::InternalAccount { + receiving_account, + external_address, + note: Box::new(note()), + }, + } + } + + fn into_recipient_with_outpoint( + self, + #[cfg(feature = "transparent-inputs")] outpoint: OutPoint, + ) -> Recipient { + match self { + BuildRecipient::External { + recipient_address, + output_pool, + } => Recipient::External { + recipient_address, + output_pool, + }, + #[cfg(feature = "transparent-inputs")] + BuildRecipient::EphemeralTransparent { + receiving_account, + ephemeral_address, + } => Recipient::EphemeralTransparent { + receiving_account, + ephemeral_address, + outpoint, + }, + BuildRecipient::InternalAccount { .. } => unreachable!(), + } + } +} + +#[allow(clippy::type_complexity)] +struct BuildState<'a, P, AccountId> { + #[cfg(feature = "transparent-inputs")] + step_index: usize, + builder: Builder<'a, P, ()>, + #[cfg(feature = "transparent-inputs")] + transparent_input_addresses: HashMap, + #[cfg(feature = "orchard")] + orchard_output_meta: Vec<(BuildRecipient, Zatoshis, Option)>, + sapling_output_meta: Vec<(BuildRecipient, Zatoshis, Option)>, + transparent_output_meta: Vec<( + BuildRecipient, + TransparentAddress, + Zatoshis, + StepOutputIndex, + )>, + #[cfg(feature = "transparent-inputs")] + utxos_spent: Vec, +} + +// `unused_transparent_outputs` maps `StepOutput`s for transparent outputs +// that have not been consumed so far, to the corresponding pair of +// `TransparentAddress` and `Outpoint`. +#[allow(clippy::too_many_arguments)] +#[allow(clippy::type_complexity)] +fn build_proposed_transaction( + wallet_db: &mut DbT, + params: &ParamsT, + ufvk: &UnifiedFullViewingKey, + account_id: ::AccountId, + ovk_policy: OvkPolicy, + min_target_height: BlockHeight, + prior_step_results: &[(&Step, StepResult<::AccountId>)], + proposal_step: &Step, + #[cfg(feature = "transparent-inputs")] unused_transparent_outputs: &mut HashMap< + StepOutput, + (TransparentAddress, OutPoint), + >, +) -> Result< + BuildState<'static, ParamsT, DbT::AccountId>, + CreateErrT, +> +where + DbT: WalletWrite + WalletCommitmentTrees, + ParamsT: consensus::Parameters + Clone, + FeeRuleT: FeeRule, +{ + #[cfg(feature = "transparent-inputs")] + let step_index = prior_step_results.len(); + + // We only support spending transparent payments or transparent ephemeral outputs from a + // prior step (when "transparent-inputs" is enabled). + // + // TODO: Maybe support spending prior shielded outputs at some point? Doing so would require + // a higher-level approach in the wallet that waits for transactions with shielded outputs to + // be mined and only then attempts to perform the next step. + #[allow(clippy::never_loop)] + for input_ref in proposal_step.prior_step_inputs() { + let (prior_step, _) = prior_step_results + .get(input_ref.step_index()) + .ok_or(ProposalError::ReferenceError(*input_ref))?; + + #[allow(unused_variables)] + let output_pool = match input_ref.output_index() { + StepOutputIndex::Payment(i) => prior_step.payment_pools().get(&i).cloned(), + StepOutputIndex::Change(i) => match prior_step.balance().proposed_change().get(i) { + Some(change) if !change.is_ephemeral() => { + return Err(ProposalError::SpendsChange(*input_ref).into()); + } + other => other.map(|change| change.output_pool()), + }, + } + .ok_or(ProposalError::ReferenceError(*input_ref))?; + + // Return an error on trying to spend a prior output that is not supported. + #[cfg(feature = "transparent-inputs")] + if output_pool != PoolType::TRANSPARENT { + return Err(Error::ProposalNotSupported); + } + #[cfg(not(feature = "transparent-inputs"))] + return Err(Error::ProposalNotSupported); + } + + let (sapling_anchor, sapling_inputs) = if proposal_step + .involves(PoolType::Shielded(ShieldedProtocol::Sapling)) + { + proposal_step.shielded_inputs().map_or_else( + || Ok((Some(sapling::Anchor::empty_tree()), vec![])), + |inputs| { + wallet_db.with_sapling_tree_mut::<_, _, Error<_, _, _, _, _, _>>(|sapling_tree| { + let anchor = sapling_tree + .root_at_checkpoint_id(&inputs.anchor_height())? + .ok_or(ProposalError::AnchorNotFound(inputs.anchor_height()))? + .into(); + + let sapling_inputs = inputs + .notes() + .iter() + .filter_map(|selected| match selected.note() { + Note::Sapling(note) => sapling_tree + .witness_at_checkpoint_id_caching( + selected.note_commitment_tree_position(), + &inputs.anchor_height(), + ) + .and_then(|witness| { + witness + .ok_or(ShardTreeError::Query(QueryError::CheckpointPruned)) + }) + .map(|merkle_path| { + Some((selected.spending_key_scope(), note, merkle_path)) + }) + .map_err(Error::from) + .transpose(), + #[cfg(feature = "orchard")] + Note::Orchard(_) => None, + }) + .collect::, Error<_, _, _, _, _, _>>>()?; + + Ok((Some(anchor), sapling_inputs)) + }) + }, + )? + } else { + (None, vec![]) + }; + + #[cfg(feature = "orchard")] + let (orchard_anchor, orchard_inputs) = if proposal_step + .involves(PoolType::Shielded(ShieldedProtocol::Orchard)) + { + proposal_step.shielded_inputs().map_or_else( + || Ok((Some(orchard::Anchor::empty_tree()), vec![])), + |inputs| { + wallet_db.with_orchard_tree_mut::<_, _, Error<_, _, _, _, _, _>>(|orchard_tree| { + let anchor = orchard_tree + .root_at_checkpoint_id(&inputs.anchor_height())? + .ok_or(ProposalError::AnchorNotFound(inputs.anchor_height()))? + .into(); + + let orchard_inputs = inputs + .notes() + .iter() + .filter_map(|selected| match selected.note() { + #[cfg(feature = "orchard")] + Note::Orchard(note) => orchard_tree + .witness_at_checkpoint_id_caching( + selected.note_commitment_tree_position(), + &inputs.anchor_height(), + ) + .and_then(|witness| { + witness + .ok_or(ShardTreeError::Query(QueryError::CheckpointPruned)) + }) + .map(|merkle_path| Some((note, merkle_path))) + .map_err(Error::from) + .transpose(), + Note::Sapling(_) => None, + }) + .collect::, Error<_, _, _, _, _, _>>>()?; + + Ok((Some(anchor), orchard_inputs)) + }) + }, + )? + } else { + (None, vec![]) + }; + #[cfg(not(feature = "orchard"))] + let orchard_anchor = None; + + // Create the transaction. The type of the proposal ensures that there + // are no possible transparent inputs, so we ignore those here. + let mut builder = Builder::new( + params.clone(), + min_target_height, + BuildConfig::Standard { + sapling_anchor, + orchard_anchor, + }, + ); + + #[cfg(all(feature = "transparent-inputs", not(feature = "orchard")))] + let has_shielded_inputs = !sapling_inputs.is_empty(); + #[cfg(all(feature = "transparent-inputs", feature = "orchard"))] + let has_shielded_inputs = !(sapling_inputs.is_empty() && orchard_inputs.is_empty()); + + for (_sapling_key_scope, sapling_note, merkle_path) in sapling_inputs.into_iter() { + let key = match _sapling_key_scope { + Scope::External => ufvk.sapling().map(|k| k.fvk().clone()), + Scope::Internal => ufvk.sapling().map(|k| k.to_internal_fvk()), + }; + + builder.add_sapling_spend( + key.ok_or(Error::KeyNotAvailable(PoolType::SAPLING))?, + sapling_note.clone(), + merkle_path, + )?; + } + + #[cfg(feature = "orchard")] + for (orchard_note, merkle_path) in orchard_inputs.into_iter() { + builder.add_orchard_spend( + ufvk.orchard() + .cloned() + .ok_or(Error::KeyNotAvailable(PoolType::ORCHARD))?, + *orchard_note, + merkle_path.into(), + )?; + } + + #[cfg(feature = "transparent-inputs")] + let mut cache = HashMap::::new(); + + #[cfg(feature = "transparent-inputs")] + let mut metadata_from_address = |addr: TransparentAddress| -> Result< + TransparentAddressMetadata, + CreateErrT, + > { + match cache.get(&addr) { + Some(result) => Ok(result.clone()), + None => { + // `wallet_db.get_transparent_address_metadata` includes reserved ephemeral + // addresses in its lookup. We don't need to include these in order to be + // able to construct ZIP 320 transactions, because in that case the ephemeral + // output is represented via a "change" reference to a previous step. However, + // we do need them in order to create a transaction from a proposal that + // explicitly spends an output from an ephemeral address (only for outputs + // already detected by this wallet instance). + + let result = wallet_db + .get_transparent_address_metadata(account_id, &addr) + .map_err(InputSelectorError::DataSource)? + .ok_or(Error::AddressNotRecognized(addr))?; + cache.insert(addr, result.clone()); + Ok(result) + } + } + }; + + #[cfg(feature = "transparent-inputs")] + let utxos_spent = { + let mut utxos_spent: Vec = vec![]; + let add_transparent_input = |builder: &mut Builder<_, _>, + utxos_spent: &mut Vec<_>, + address_metadata: &TransparentAddressMetadata, + outpoint: OutPoint, + txout: TxOut| + -> Result< + (), + CreateErrT, + > { + let pubkey = ufvk + .transparent() + .ok_or(Error::KeyNotAvailable(PoolType::Transparent))? + .derive_address_pubkey(address_metadata.scope(), address_metadata.address_index()) + .expect("spending key derivation should not fail"); + + utxos_spent.push(outpoint.clone()); + builder.add_transparent_input(pubkey, outpoint, txout)?; + + Ok(()) + }; + + for utxo in proposal_step.transparent_inputs() { + add_transparent_input( + &mut builder, + &mut utxos_spent, + &metadata_from_address(*utxo.recipient_address())?, + utxo.outpoint().clone(), + utxo.txout().clone(), + )?; + } + for input_ref in proposal_step.prior_step_inputs() { + // A referenced transparent step output must exist and be referenced *at most* once. + // (Exactly once in the case of ephemeral outputs.) + let (address, outpoint) = unused_transparent_outputs + .remove(input_ref) + .ok_or(Error::Proposal(ProposalError::ReferenceError(*input_ref)))?; + + let address_metadata = metadata_from_address(address)?; + + let txout = &prior_step_results[input_ref.step_index()] + .1 + .build_result + .transaction() + .transparent_bundle() + .ok_or(ProposalError::ReferenceError(*input_ref))? + .vout[outpoint.n() as usize]; + + add_transparent_input( + &mut builder, + &mut utxos_spent, + &address_metadata, + outpoint, + txout.clone(), + )?; + } + utxos_spent + }; + + #[cfg(feature = "orchard")] + let orchard_external_ovk = match &ovk_policy { + OvkPolicy::Sender => ufvk + .orchard() + .map(|fvk| fvk.to_ovk(orchard::keys::Scope::External)), + OvkPolicy::Custom { orchard, .. } => Some(orchard.clone()), + OvkPolicy::Discard => None, + }; + + #[cfg(feature = "orchard")] + let orchard_internal_ovk = || { + #[cfg(feature = "transparent-inputs")] + if proposal_step.is_shielding() { + return ufvk + .transparent() + .map(|k| orchard::keys::OutgoingViewingKey::from(k.internal_ovk().as_bytes())); + } + + ufvk.orchard().map(|k| k.to_ovk(Scope::Internal)) + }; + // Apply the outgoing viewing key policy. - let ovk = match ovk_policy { - OvkPolicy::Sender => Some(extfvk.fvk.ovk), - OvkPolicy::Custom(ovk) => Some(ovk), + let sapling_external_ovk = match &ovk_policy { + OvkPolicy::Sender => ufvk.sapling().map(|k| k.to_ovk(Scope::External)), + OvkPolicy::Custom { sapling, .. } => Some(*sapling), OvkPolicy::Discard => None, }; - // Target the next block, assuming we are up-to-date. - let (height, anchor_height) = wallet_db - .get_target_and_anchor_heights() - .and_then(|x| x.ok_or_else(|| Error::ScanRequired.into()))?; - - let target_value = value + DEFAULT_FEE; - let spendable_notes = wallet_db.select_spendable_notes(account, target_value, anchor_height)?; - - // Confirm we were able to select sufficient value - let selected_value = spendable_notes.iter().map(|n| n.note_value).sum(); - if selected_value < target_value { - return Err(E::from(Error::InsufficientBalance( - selected_value, - target_value, - ))); - } + let sapling_internal_ovk = || { + #[cfg(feature = "transparent-inputs")] + if proposal_step.is_shielding() { + return ufvk + .transparent() + .map(|k| sapling::keys::OutgoingViewingKey(k.internal_ovk().as_bytes())); + } + + ufvk.sapling().map(|k| k.to_ovk(Scope::Internal)) + }; - // Create the transaction - let mut builder = Builder::new(params.clone(), height); - for selected in spendable_notes { - let from = extfvk - .fvk - .vk - .to_payment_address(selected.diversifier) - .unwrap(); //DiversifyHash would have to unexpectedly return the zero point for this to be None + #[cfg(feature = "orchard")] + let mut orchard_output_meta: Vec<(BuildRecipient<_>, Zatoshis, Option)> = vec![]; + let mut sapling_output_meta: Vec<(BuildRecipient<_>, Zatoshis, Option)> = vec![]; + let mut transparent_output_meta: Vec<( + BuildRecipient<_>, + TransparentAddress, + Zatoshis, + StepOutputIndex, + )> = vec![]; - let note = from - .create_note(selected.note_value.into(), selected.rseed) - .unwrap(); + for (&payment_index, output_pool) in proposal_step.payment_pools() { + let payment = proposal_step + .transaction_request() + .payments() + .get(&payment_index) + .expect( + "The mapping between payment index and payment is checked in step construction", + ); + let recipient_address = payment.recipient_address(); - let merkle_path = selected.witness.path().expect("the tree is not empty"); + let add_sapling_output = |builder: &mut Builder<_, _>, + sapling_output_meta: &mut Vec<_>, + to: sapling::PaymentAddress| + -> Result< + (), + CreateErrT, + > { + let memo = payment.memo().map_or_else(MemoBytes::empty, |m| m.clone()); + builder.add_sapling_output(sapling_external_ovk, to, payment.amount(), memo.clone())?; + sapling_output_meta.push(( + BuildRecipient::External { + recipient_address: recipient_address.clone(), + output_pool: PoolType::SAPLING, + }, + payment.amount(), + Some(memo), + )); + Ok(()) + }; - builder - .add_sapling_spend(extsk.clone(), selected.diversifier, note, merkle_path) - .map_err(Error::Builder)?; + #[cfg(feature = "orchard")] + let add_orchard_output = + |builder: &mut Builder<_, _>, + orchard_output_meta: &mut Vec<_>, + to: orchard::Address| + -> Result<(), CreateErrT> { + let memo = payment.memo().map_or_else(MemoBytes::empty, |m| m.clone()); + builder.add_orchard_output( + orchard_external_ovk.clone(), + to, + payment.amount().into(), + memo.clone(), + )?; + orchard_output_meta.push(( + BuildRecipient::External { + recipient_address: recipient_address.clone(), + output_pool: PoolType::ORCHARD, + }, + payment.amount(), + Some(memo), + )); + Ok(()) + }; + + let add_transparent_output = + |builder: &mut Builder<_, _>, + transparent_output_meta: &mut Vec<_>, + to: TransparentAddress| + -> Result<(), CreateErrT> { + // Always reject sending to one of our known ephemeral addresses. + #[cfg(feature = "transparent-inputs")] + if wallet_db + .find_account_for_ephemeral_address(&to) + .map_err(Error::DataSource)? + .is_some() + { + return Err(Error::PaysEphemeralTransparentAddress(to.encode(params))); + } + if payment.memo().is_some() { + return Err(Error::MemoForbidden); + } + builder.add_transparent_output(&to, payment.amount())?; + transparent_output_meta.push(( + BuildRecipient::External { + recipient_address: recipient_address.clone(), + output_pool: PoolType::TRANSPARENT, + }, + to, + payment.amount(), + StepOutputIndex::Payment(payment_index), + )); + Ok(()) + }; + + match recipient_address + .clone() + .convert_if_network(params.network_type())? + { + Address::Unified(ua) => match output_pool { + #[cfg(not(feature = "orchard"))] + PoolType::Shielded(ShieldedProtocol::Orchard) => { + return Err(Error::ProposalNotSupported); + } + #[cfg(feature = "orchard")] + PoolType::Shielded(ShieldedProtocol::Orchard) => { + let to = *ua.orchard().expect("The mapping between payment pool and receiver is checked in step construction"); + add_orchard_output(&mut builder, &mut orchard_output_meta, to)?; + } + PoolType::Shielded(ShieldedProtocol::Sapling) => { + let to = *ua.sapling().expect("The mapping between payment pool and receiver is checked in step construction"); + add_sapling_output(&mut builder, &mut sapling_output_meta, to)?; + } + PoolType::Transparent => { + let to = *ua.transparent().expect("The mapping between payment pool and receiver is checked in step construction"); + add_transparent_output(&mut builder, &mut transparent_output_meta, to)?; + } + }, + Address::Sapling(to) => { + add_sapling_output(&mut builder, &mut sapling_output_meta, to)?; + } + Address::Transparent(to) => { + add_transparent_output(&mut builder, &mut transparent_output_meta, to)?; + } + #[cfg(not(feature = "transparent-inputs"))] + Address::Tex(_) => { + return Err(Error::ProposalNotSupported); + } + #[cfg(feature = "transparent-inputs")] + Address::Tex(data) => { + if has_shielded_inputs { + return Err(ProposalError::PaysTexFromShielded.into()); + } + let to = TransparentAddress::PublicKeyHash(data); + add_transparent_output(&mut builder, &mut transparent_output_meta, to)?; + } + } } - match to { - RecipientAddress::Shielded(to) => { - builder.add_sapling_output(ovk, to.clone(), value, memo.clone()) + for change_value in proposal_step.balance().proposed_change() { + let memo = change_value + .memo() + .map_or_else(MemoBytes::empty, |m| m.clone()); + let output_pool = change_value.output_pool(); + match output_pool { + PoolType::Shielded(ShieldedProtocol::Sapling) => { + builder.add_sapling_output( + sapling_internal_ovk(), + ufvk.sapling() + .ok_or(Error::KeyNotAvailable(PoolType::SAPLING))? + .change_address() + .1, + change_value.value(), + memo.clone(), + )?; + sapling_output_meta.push(( + BuildRecipient::InternalAccount { + receiving_account: account_id, + external_address: None, + }, + change_value.value(), + Some(memo), + )) + } + PoolType::Shielded(ShieldedProtocol::Orchard) => { + #[cfg(not(feature = "orchard"))] + return Err(Error::UnsupportedChangeType(output_pool)); + + #[cfg(feature = "orchard")] + { + builder.add_orchard_output( + orchard_internal_ovk(), + ufvk.orchard() + .ok_or(Error::KeyNotAvailable(PoolType::ORCHARD))? + .address_at(0u32, orchard::keys::Scope::Internal), + change_value.value().into(), + memo.clone(), + )?; + orchard_output_meta.push(( + BuildRecipient::InternalAccount { + receiving_account: account_id, + external_address: None, + }, + change_value.value(), + Some(memo), + )) + } + } + PoolType::Transparent => { + #[cfg(not(feature = "transparent-inputs"))] + return Err(Error::UnsupportedChangeType(output_pool)); + } } + } - RecipientAddress::Transparent(to) => builder.add_transparent_output(&to, value), + // This reserves the ephemeral addresses even if transaction construction fails. + // It is not worth the complexity of being able to unreserve them, because there + // are few failure modes after this point that would allow us to do so. + #[cfg(feature = "transparent-inputs")] + { + let ephemeral_outputs: Vec<(usize, &ChangeValue)> = proposal_step + .balance() + .proposed_change() + .iter() + .enumerate() + .filter(|(_, change_value)| { + change_value.is_ephemeral() && change_value.output_pool() == PoolType::Transparent + }) + .collect(); + + let addresses_and_metadata = wallet_db + .reserve_next_n_ephemeral_addresses(account_id, ephemeral_outputs.len()) + .map_err(Error::DataSource)?; + assert_eq!(addresses_and_metadata.len(), ephemeral_outputs.len()); + + // We don't need the TransparentAddressMetadata here; we can look it up from the data source later. + for ((change_index, change_value), (ephemeral_address, _)) in + ephemeral_outputs.iter().zip(addresses_and_metadata) + { + // This output is ephemeral; we will report an error in `create_proposed_transactions` + // if a later step does not consume it. + builder.add_transparent_output(&ephemeral_address, change_value.value())?; + transparent_output_meta.push(( + BuildRecipient::EphemeralTransparent { + receiving_account: account_id, + ephemeral_address, + }, + ephemeral_address, + change_value.value(), + StepOutputIndex::Change(*change_index), + )) + } } - .map_err(Error::Builder)?; - - let consensus_branch_id = BranchId::for_height(params, height); - let (tx, tx_metadata) = builder - .build(consensus_branch_id, &prover) - .map_err(Error::Builder)?; - - let output_index = match to { - // Sapling outputs are shuffled, so we need to look up where the output ended up. - RecipientAddress::Shielded(_) => match tx_metadata.output_index(0) { - Some(idx) => idx, - None => panic!("Output 0 should exist in the transaction"), + + Ok(BuildState { + #[cfg(feature = "transparent-inputs")] + step_index, + builder, + #[cfg(feature = "transparent-inputs")] + transparent_input_addresses: cache, + #[cfg(feature = "orchard")] + orchard_output_meta, + sapling_output_meta, + transparent_output_meta, + #[cfg(feature = "transparent-inputs")] + utxos_spent, + }) +} + +// `unused_transparent_outputs` maps `StepOutput`s for transparent outputs +// that have not been consumed so far, to the corresponding pair of +// `TransparentAddress` and `Outpoint`. +#[allow(clippy::too_many_arguments)] +#[allow(clippy::type_complexity)] +fn create_proposed_transaction( + wallet_db: &mut DbT, + params: &ParamsT, + spend_prover: &impl SpendProver, + output_prover: &impl OutputProver, + usk: &UnifiedSpendingKey, + account_id: ::AccountId, + ovk_policy: OvkPolicy, + fee_rule: &FeeRuleT, + min_target_height: BlockHeight, + prior_step_results: &[(&Step, StepResult<::AccountId>)], + proposal_step: &Step, + #[cfg(feature = "transparent-inputs")] unused_transparent_outputs: &mut HashMap< + StepOutput, + (TransparentAddress, OutPoint), + >, +) -> Result< + StepResult<::AccountId>, + CreateErrT, +> +where + DbT: WalletWrite + WalletCommitmentTrees, + ParamsT: consensus::Parameters + Clone, + FeeRuleT: FeeRule, +{ + let build_state = build_proposed_transaction::<_, _, _, FeeRuleT, _, _>( + wallet_db, + params, + &usk.to_unified_full_viewing_key(), + account_id, + ovk_policy, + min_target_height, + prior_step_results, + proposal_step, + #[cfg(feature = "transparent-inputs")] + unused_transparent_outputs, + )?; + + // Build the transaction with the specified fee rule + #[cfg_attr(not(feature = "transparent-inputs"), allow(unused_mut))] + let mut transparent_signing_set = TransparentSigningSet::new(); + #[cfg(feature = "transparent-inputs")] + for (_, address_metadata) in build_state.transparent_input_addresses { + transparent_signing_set.add_key( + usk.transparent() + .derive_secret_key(address_metadata.scope(), address_metadata.address_index()) + .expect("spending key derivation should not fail"), + ); + } + let sapling_extsks = &[usk.sapling().clone(), usk.sapling().derive_internal()]; + #[cfg(feature = "orchard")] + let orchard_saks = &[usk.orchard().into()]; + #[cfg(not(feature = "orchard"))] + let orchard_saks = &[]; + let build_result = build_state.builder.build( + &transparent_signing_set, + sapling_extsks, + orchard_saks, + OsRng, + spend_prover, + output_prover, + fee_rule, + )?; + + #[cfg(feature = "orchard")] + let orchard_fvk: orchard::keys::FullViewingKey = usk.orchard().into(); + #[cfg(feature = "orchard")] + let orchard_internal_ivk = orchard_fvk.to_ivk(orchard::keys::Scope::Internal); + #[cfg(feature = "orchard")] + let orchard_outputs = build_state.orchard_output_meta.into_iter().enumerate().map( + |(i, (recipient, value, memo))| { + let output_index = build_result + .orchard_meta() + .output_action_index(i) + .expect("An action should exist in the transaction for each Orchard output."); + + let recipient = recipient.into_recipient_with_note(|| { + build_result + .transaction() + .orchard_bundle() + .and_then(|bundle| { + bundle + .decrypt_output_with_key(output_index, &orchard_internal_ivk) + .map(|(note, _, _)| Note::Orchard(note)) + }) + .expect("Wallet-internal outputs must be decryptable with the wallet's IVK") + }); + + SentTransactionOutput::from_parts(output_index, recipient, value, memo) }, - RecipientAddress::Transparent(addr) => { - let script = addr.script(); - tx.vout + ); + + let sapling_dfvk = usk.sapling().to_diversifiable_full_viewing_key(); + let sapling_internal_ivk = + PreparedIncomingViewingKey::new(&sapling_dfvk.to_ivk(Scope::Internal)); + let sapling_outputs = build_state.sapling_output_meta.into_iter().enumerate().map( + |(i, (recipient, value, memo))| { + let output_index = build_result + .sapling_meta() + .output_index(i) + .expect("An output should exist in the transaction for each Sapling payment."); + + let recipient = recipient.into_recipient_with_note(|| { + build_result + .transaction() + .sapling_bundle() + .and_then(|bundle| { + try_sapling_note_decryption( + &sapling_internal_ivk, + &bundle.shielded_outputs()[output_index], + zip212_enforcement(params, min_target_height), + ) + .map(|(note, _, _)| Note::Sapling(note)) + }) + .expect("Wallet-internal outputs must be decryptable with the wallet's IVK") + }); + + SentTransactionOutput::from_parts(output_index, recipient, value, memo) + }, + ); + + let txid: [u8; 32] = build_result.transaction().txid().into(); + assert_eq!( + build_state.transparent_output_meta.len(), + build_result + .transaction() + .transparent_bundle() + .map_or(0, |b| b.vout.len()), + ); + + #[allow(unused_variables)] + let transparent_outputs = build_state + .transparent_output_meta + .into_iter() + .enumerate() + .map(|(n, (recipient, address, value, step_output_index))| { + // This assumes that transparent outputs are pushed onto `transparent_output_meta` + // with the same indices they have in the transaction's transparent outputs. + // We do not reorder transparent outputs; there is no reason to do so because it + // would not usefully improve privacy. + let outpoint = OutPoint::new(txid, n as u32); + + let recipient = recipient.into_recipient_with_outpoint( + #[cfg(feature = "transparent-inputs")] + outpoint.clone(), + ); + + #[cfg(feature = "transparent-inputs")] + unused_transparent_outputs.insert( + StepOutput::new(build_state.step_index, step_output_index), + (address, outpoint), + ); + SentTransactionOutput::from_parts(n, recipient, value, None) + }); + + let mut outputs: Vec> = vec![]; + #[cfg(feature = "orchard")] + outputs.extend(orchard_outputs); + outputs.extend(sapling_outputs); + outputs.extend(transparent_outputs); + + Ok(StepResult { + build_result, + outputs, + fee_amount: proposal_step.balance().fee_required(), + #[cfg(feature = "transparent-inputs")] + utxos_spent: build_state.utxos_spent, + }) +} + +/// Constructs a transaction using the inputs supplied by the given proposal. +/// +/// Only single-step proposals are currently supported. +/// +/// Returns a partially-created Zcash transaction (PCZT) that is ready to be authorized. +/// You can use the following roles for this: +/// - [`pczt::roles::prover::Prover`] +/// - [`pczt::roles::signer::Signer`] (if you have local access to the spend authorizing +/// keys) +/// - [`pczt::roles::combiner::Combiner`] (if you create proofs and apply signatures in +/// parallel) +/// +/// Once the PCZT fully authorized, call [`extract_and_store_transaction_from_pczt`] to +/// finish transaction creation. +#[allow(clippy::too_many_arguments)] +#[allow(clippy::type_complexity)] +#[cfg(feature = "pczt")] +pub fn create_pczt_from_proposal( + wallet_db: &mut DbT, + params: &ParamsT, + account_id: ::AccountId, + ovk_policy: OvkPolicy, + proposal: &Proposal, +) -> Result> +where + DbT: WalletWrite + WalletCommitmentTrees, + ParamsT: consensus::Parameters + Clone, + FeeRuleT: FeeRule, + DbT::AccountId: serde::Serialize, +{ + use std::collections::HashSet; + + let account = wallet_db + .get_account(account_id) + .map_err(Error::DataSource)? + .ok_or(Error::AccountIdNotRecognized)?; + let ufvk = account.ufvk().ok_or(Error::AccountCannotSpend)?; + let account_derivation = account.source().key_derivation(); + + // For now we only support turning single-step proposals into PCZTs. + if proposal.steps().len() > 1 { + return Err(Error::ProposalNotSupported); + } + let fee_rule = proposal.fee_rule(); + let min_target_height = proposal.min_target_height(); + let prior_step_results = &[]; + let proposal_step = proposal.steps().first(); + let unused_transparent_outputs = &mut HashMap::new(); + + let build_state = build_proposed_transaction::<_, _, _, FeeRuleT, _, _>( + wallet_db, + params, + ufvk, + account_id, + ovk_policy, + min_target_height, + prior_step_results, + proposal_step, + #[cfg(feature = "transparent-inputs")] + unused_transparent_outputs, + )?; + + // Build the transaction with the specified fee rule + let build_result = build_state.builder.build_for_pczt(OsRng, fee_rule)?; + + let created = Creator::build_from_parts(build_result.pczt_parts).ok_or(PcztError::Build)?; + + let io_finalized = IoFinalizer::new(created).finalize_io()?; + + #[cfg(feature = "orchard")] + let orchard_outputs = build_state + .orchard_output_meta + .into_iter() + .enumerate() + .map(|(i, (recipient, _, _))| { + let output_index = build_result + .orchard_meta + .output_action_index(i) + .expect("An action should exist in the transaction for each Orchard output."); + + (output_index, PcztRecipient::from_recipient(recipient)) + }) + .collect::>(); + + #[cfg(feature = "orchard")] + let orchard_spends = (0..) + .map(|i| build_result.orchard_meta.spend_action_index(i)) + .take_while(|item| item.is_some()) + .flatten() + .collect::>(); + + let sapling_outputs = build_state + .sapling_output_meta + .into_iter() + .enumerate() + .map(|(i, (recipient, _, _))| { + let output_index = build_result + .sapling_meta + .output_index(i) + .expect("An output should exist in the transaction for each Sapling output."); + + (output_index, PcztRecipient::from_recipient(recipient)) + }) + .collect::>(); + + let pczt = Updater::new(io_finalized) + .update_global_with(|mut updater| { + updater.set_proprietary( + PROPRIETARY_PROPOSAL_INFO.into(), + postcard::to_allocvec(&ProposalInfo:: { + from_account: account_id, + target_height: proposal.min_target_height().into(), + }) + .expect("postcard encoding of PCZT proposal metadata should not fail"), + ) + }) + .update_orchard_with(|mut updater| { + for index in 0..updater.bundle().actions().len() { + updater.update_action_with(index, |mut action_updater| { + // If the account has a known derivation, add the Orchard key path to the PCZT. + if let Some(derivation) = account_derivation { + // orchard_spends will only contain action indices for the real spends, and + // not the dummy inputs + if orchard_spends.contains(&index) { + // All spent notes are from the same account. + action_updater.set_spend_zip32_derivation( + orchard::pczt::Zip32Derivation::parse( + derivation.seed_fingerprint().to_bytes(), + vec![ + zip32::ChildIndex::hardened(32).index(), + zip32::ChildIndex::hardened( + params.network_type().coin_type(), + ) + .index(), + zip32::ChildIndex::hardened(u32::from( + derivation.account_index(), + )) + .index(), + ], + ) + .expect("valid"), + ); + } + } + + if let Some((pczt_recipient, external_address)) = orchard_outputs.get(&index) { + if let Some(user_address) = external_address { + action_updater.set_output_user_address(user_address.encode()); + } + action_updater.set_output_proprietary( + PROPRIETARY_OUTPUT_INFO.into(), + postcard::to_allocvec(pczt_recipient).expect( + "postcard encoding of PCZT recipient metadata should not fail", + ), + ); + } + + Ok(()) + })?; + } + Ok(()) + })? + .update_sapling_with(|mut updater| { + // If the account has a known derivation, add the Sapling key path to the PCZT. + if let Some(derivation) = account_derivation { + let non_dummy_spends = updater + .bundle() + .spends() + .iter() + .enumerate() + .filter_map(|(index, spend)| { + // Dummy spends will already have a proof generation key. + spend.proof_generation_key().is_none().then_some(index) + }) + .collect::>(); + + for index in non_dummy_spends { + updater.update_spend_with(index, |mut spend_updater| { + // All non-dummy spent notes are from the same account. + spend_updater.set_zip32_derivation( + sapling::pczt::Zip32Derivation::parse( + derivation.seed_fingerprint().to_bytes(), + vec![ + zip32::ChildIndex::hardened(32).index(), + zip32::ChildIndex::hardened(params.network_type().coin_type()) + .index(), + zip32::ChildIndex::hardened(u32::from( + derivation.account_index(), + )) + .index(), + ], + ) + .expect("valid"), + ); + Ok(()) + })?; + } + } + + for index in 0..updater.bundle().outputs().len() { + if let Some((pczt_recipient, external_address)) = sapling_outputs.get(&index) { + updater.update_output_with(index, |mut output_updater| { + if let Some(user_address) = external_address { + output_updater.set_user_address(user_address.encode()); + } + output_updater.set_proprietary( + PROPRIETARY_OUTPUT_INFO.into(), + postcard::to_allocvec(pczt_recipient).expect( + "postcard encoding of PCZT recipient metadata should not fail", + ), + ); + Ok(()) + })?; + } + } + + Ok(()) + })? + .update_transparent_with(|mut updater| { + // If the account has a known derivation, add the transparent key paths to the PCZT. + if let Some(derivation) = account_derivation { + // Match address metadata to the inputs that spend from those addresses. + let inputs_to_update = updater + .bundle() + .inputs() + .iter() + .enumerate() + .filter_map(|(index, input)| { + build_state + .transparent_input_addresses + .get( + &input + .script_pubkey() + .address() + .expect("we created this with a supported transparent address"), + ) + .map(|address_metadata| { + ( + index, + address_metadata.scope(), + address_metadata.address_index(), + ) + }) + }) + .collect::>(); + + for (index, scope, address_index) in inputs_to_update { + updater.update_input_with(index, |mut input_updater| { + let pubkey = ufvk + .transparent() + .expect("we derived this successfully in build_proposed_transaction") + .derive_address_pubkey(scope, address_index) + .expect("spending key derivation should not fail"); + + input_updater.set_bip32_derivation( + pubkey.serialize(), + Bip32Derivation::parse( + derivation.seed_fingerprint().to_bytes(), + vec![ + // Transparent uses BIP 44 derivation. + 44 | ChildNumber::HARDENED_FLAG, + params.network_type().coin_type() | ChildNumber::HARDENED_FLAG, + u32::from(derivation.account_index()) + | ChildNumber::HARDENED_FLAG, + ChildNumber::from(scope).into(), + ChildNumber::from(address_index).into(), + ], + ) + .expect("valid"), + ); + Ok(()) + })?; + } + } + + assert_eq!( + build_state.transparent_output_meta.len(), + updater.bundle().outputs().len(), + ); + for (index, (recipient, _, _, _)) in + build_state.transparent_output_meta.into_iter().enumerate() + { + updater.update_output_with(index, |mut output_updater| { + let (pczt_recipient, external_address) = + PcztRecipient::from_recipient(recipient); + if let Some(user_address) = external_address { + output_updater.set_user_address(user_address.encode()); + } + output_updater.set_proprietary( + PROPRIETARY_OUTPUT_INFO.into(), + postcard::to_allocvec(&pczt_recipient) + .expect("postcard encoding of pczt recipient metadata should not fail"), + ); + Ok(()) + })?; + } + + Ok(()) + })? + .finish(); + + Ok(pczt) +} + +/// Finalizes the given PCZT, and persists the transaction to the wallet database. +/// +/// The PCZT should have been created via [`create_pczt_from_proposal`], which adds +/// metadata necessary for the wallet backend. +/// +/// Returns the transaction ID for the resulting transaction. +#[cfg(feature = "pczt")] +pub fn extract_and_store_transaction_from_pczt( + wallet_db: &mut DbT, + pczt: pczt::Pczt, + spend_vk: &sapling::circuit::SpendVerifyingKey, + output_vk: &sapling::circuit::OutputVerifyingKey, + #[cfg(feature = "orchard")] orchard_vk: &orchard::circuit::VerifyingKey, +) -> Result> +where + DbT: WalletWrite + WalletCommitmentTrees, + DbT::AccountId: serde::de::DeserializeOwned, +{ + use std::collections::BTreeMap; + use zcash_note_encryption::{Domain, ShieldedOutput, ENC_CIPHERTEXT_SIZE}; + + let finalized = SpendFinalizer::new(pczt).finalize_spends()?; + + let proposal_info = finalized + .global() + .proprietary() + .get(PROPRIETARY_PROPOSAL_INFO) + .ok_or_else(|| PcztError::Invalid("PCZT missing proprietary proposal info field".into())) + .and_then(|v| { + postcard::from_bytes::>(v).map_err(|e| { + PcztError::Invalid(format!( + "Postcard decoding of proprietary proposal info failed: {}", + e + )) + }) + })?; + + let orchard_output_info = finalized + .orchard() + .actions() + .iter() + .map(|act| { + let note = || { + let recipient = + act.output().recipient().as_ref().and_then(|b| { + ::orchard::Address::from_raw_address_bytes(b).into_option() + })?; + let value = act + .output() + .value() + .map(orchard::value::NoteValue::from_raw)?; + let rho = orchard::note::Rho::from_bytes(act.spend().nullifier()).into_option()?; + let rseed = act.output().rseed().as_ref().and_then(|rseed| { + orchard::note::RandomSeed::from_bytes(*rseed, &rho).into_option() + })?; + + orchard::Note::from_parts(recipient, value, rho, rseed).into_option() + }; + + let external_address = act + .output() + .user_address() + .as_deref() + .map(ZcashAddress::try_from_encoded) + .transpose() + .map_err(|e| PcztError::Invalid(format!("Invalid user_address: {}", e)))?; + + let pczt_recipient = act + .output() + .proprietary() + .get(PROPRIETARY_OUTPUT_INFO) + .map(|v| postcard::from_bytes::>(v)) + .transpose() + .map_err(|e: postcard::Error| { + PcztError::Invalid(format!( + "Postcard decoding of proprietary output info failed: {}", + e + )) + })? + .map(|pczt_recipient| (pczt_recipient, external_address)); + + // If the pczt recipient is not present, this is a dummy note; if the note is not + // present, then the PCZT has been pruned to make this output unrecoverable and so we + // also ignore it. + Ok(pczt_recipient.zip(note())) + }) + .collect::, PcztError>>()?; + + let sapling_output_info = finalized + .sapling() + .outputs() + .iter() + .map(|out| { + let note = || { + let recipient = out + .recipient() + .as_ref() + .and_then(::sapling::PaymentAddress::from_bytes)?; + let value = out.value().map(::sapling::value::NoteValue::from_raw)?; + let rseed = out + .rseed() + .as_ref() + .cloned() + .map(::sapling::note::Rseed::AfterZip212)?; + + Some(::sapling::Note::from_parts(recipient, value, rseed)) + }; + + let external_address = out + .user_address() + .as_deref() + .map(ZcashAddress::try_from_encoded) + .transpose() + .map_err(|e| PcztError::Invalid(format!("Invalid user_address: {}", e)))?; + + let pczt_recipient = out + .proprietary() + .get(PROPRIETARY_OUTPUT_INFO) + .map(|v| postcard::from_bytes::>(v)) + .transpose() + .map_err(|e: postcard::Error| { + PcztError::Invalid(format!( + "Postcard decoding of proprietary output info failed: {}", + e + )) + })? + .map(|pczt_recipient| (pczt_recipient, external_address)); + + // If the pczt recipient is not present, this is a dummy note; if the note is not + // present, then the PCZT has been pruned to make this output unrecoverable and so we + // also ignore it. + Ok(pczt_recipient.zip(note())) + }) + .collect::, PcztError>>()?; + + let transparent_output_info = finalized + .transparent() + .outputs() + .iter() + .map(|out| { + let external_address = out + .user_address() + .as_deref() + .map(ZcashAddress::try_from_encoded) + .transpose() + .map_err(|e| PcztError::Invalid(format!("Invalid user_address: {}", e)))?; + + let pczt_recipient = out + .proprietary() + .get(PROPRIETARY_OUTPUT_INFO) + .map(|v| postcard::from_bytes::>(v)) + .transpose() + .map_err(|e: postcard::Error| { + PcztError::Invalid(format!( + "Postcard decoding of proprietary output info failed: {}", + e + )) + })? + .map(|pczt_recipient| (pczt_recipient, external_address)); + + Ok(pczt_recipient) + }) + .collect::, PcztError>>()?; + + let utxos_map = finalized + .transparent() + .inputs() + .iter() + .map(|input| { + ZatBalance::from_u64(*input.value()).map(|value| { + ( + OutPoint::new(*input.prevout_txid(), *input.prevout_index()), + value, + ) + }) + }) + .collect::, _>>()?; + + let transaction = TransactionExtractor::new(finalized) + .with_sapling(spend_vk, output_vk) + .with_orchard(orchard_vk) + .extract()?; + let txid = transaction.txid(); + + #[allow(clippy::too_many_arguments)] + fn to_sent_transaction_output< + AccountId: Copy, + D: Domain, + O: ShieldedOutput, + DbT: WalletRead + WalletCommitmentTrees, + N, + >( + domain: D, + note: D::Note, + output: &O, + output_pool: ShieldedProtocol, + output_index: usize, + pczt_recipient: PcztRecipient, + external_address: Option, + note_value: impl Fn(&D::Note) -> u64, + memo_bytes: impl Fn(&D::Memo) -> &[u8; 512], + wallet_note: impl Fn(D::Note) -> Note, + ) -> Result, ExtractErrT> { + let pk_d = D::get_pk_d(¬e); + let esk = D::derive_esk(¬e).expect("notes are post-ZIP 212"); + let memo = try_output_recovery_with_pkd_esk(&domain, pk_d, esk, output).map(|(_, _, m)| { + MemoBytes::from_bytes(memo_bytes(&m)).expect("Memo is the correct length.") + }); + + let note_value = Zatoshis::try_from(note_value(¬e))?; + let recipient = match (pczt_recipient, external_address) { + (PcztRecipient::External, Some(addr)) => Ok(Recipient::External { + recipient_address: addr, + output_pool: PoolType::Shielded(output_pool), + }), + (PcztRecipient::External, None) => Err(PcztError::Invalid( + "external recipient needs to have its user_address field set".into(), + )), + #[cfg(feature = "transparent-inputs")] + (PcztRecipient::EphemeralTransparent { .. }, _) => Err(PcztError::Invalid( + "shielded output cannot be EphemeralTransparent".into(), + )), + (PcztRecipient::InternalAccount { receiving_account }, external_address) => { + Ok(Recipient::InternalAccount { + receiving_account, + external_address, + note: Box::new(wallet_note(note)), + }) + } + }?; + + Ok(SentTransactionOutput::from_parts( + output_index, + recipient, + note_value, + memo, + )) + } + + #[cfg(feature = "orchard")] + let orchard_outputs = transaction + .orchard_bundle() + .map(|bundle| { + assert_eq!(bundle.actions().len(), orchard_output_info.len()); + bundle + .actions() .iter() + .zip(orchard_output_info) .enumerate() - .find(|(_, tx_out)| tx_out.script_pubkey == script) - .map(|(index, _)| index) - .expect("we sent to this address") - } - }; + .filter_map(|(output_index, (action, output_info))| { + output_info.map(|((pczt_recipient, external_address), note)| { + let domain = OrchardDomain::for_action(action); + to_sent_transaction_output::<_, _, _, DbT, _>( + domain, + note, + action, + ShieldedProtocol::Orchard, + output_index, + pczt_recipient, + external_address, + |note| note.value().inner(), + |memo| memo, + Note::Orchard, + ) + }) + }) + .collect::, _>>() + }) + .transpose()?; - wallet_db.store_sent_tx(&SentTransaction { - tx: &tx, - created: time::OffsetDateTime::now_utc(), - output_index, - account, - recipient_address: to, - value, - memo, - }) + let sapling_outputs = transaction + .sapling_bundle() + .map(|bundle| { + assert_eq!(bundle.shielded_outputs().len(), sapling_output_info.len()); + bundle + .shielded_outputs() + .iter() + .zip(sapling_output_info) + .enumerate() + .filter_map(|(output_index, (action, output_info))| { + output_info.map(|((pczt_recipient, external_address), note)| { + let domain = + SaplingDomain::new(sapling::note_encryption::Zip212Enforcement::On); + to_sent_transaction_output::<_, _, _, DbT, _>( + domain, + note, + action, + ShieldedProtocol::Sapling, + output_index, + pczt_recipient, + external_address, + |note| note.value().inner(), + |memo| memo, + Note::Sapling, + ) + }) + }) + .collect::, _>>() + }) + .transpose()?; + + #[allow(unused_variables)] + let transparent_outputs = transaction + .transparent_bundle() + .map(|bundle| { + assert_eq!(bundle.vout.len(), transparent_output_info.len()); + bundle + .vout + .iter() + .zip(transparent_output_info) + .enumerate() + .filter_map(|(output_index, (output, output_info))| { + output_info.map(|(pczt_recipient, external_address)| { + // This assumes that transparent outputs are pushed onto `transparent_output_meta` + // with the same indices they have in the transaction's transparent outputs. + // We do not reorder transparent outputs; there is no reason to do so because it + // would not usefully improve privacy. + let outpoint = OutPoint::new(txid.into(), output_index as u32); + + let recipient = match (pczt_recipient, external_address) { + (PcztRecipient::External, Some(addr)) => { + Ok(Recipient::External { + recipient_address: addr, + output_pool: PoolType::Transparent, + }) + } + (PcztRecipient::External, None) => Err(PcztError::Invalid( + "external recipient needs to have its user_address field set".into(), + )), + #[cfg(feature = "transparent-inputs")] + (PcztRecipient::EphemeralTransparent { receiving_account }, _) => output + .recipient_address() + .ok_or(PcztError::Invalid( + "Ephemeral outputs cannot have a non-standard script_pubkey" + .into(), + )) + .map(|ephemeral_address| Recipient::EphemeralTransparent { + receiving_account, + ephemeral_address, + outpoint, + }), + ( + PcztRecipient::InternalAccount { + receiving_account, + }, + _, + ) => Err(PcztError::Invalid( + "Transparent output cannot be InternalAccount".into(), + )), + }?; + + Ok(SentTransactionOutput::from_parts( + output_index, + recipient, + output.value, + None, + )) + }) + }) + .collect::, ExtractErrT>>() + }) + .transpose()?; + + let mut outputs: Vec> = vec![]; + #[cfg(feature = "orchard")] + outputs.extend(orchard_outputs.into_iter().flatten()); + outputs.extend(sapling_outputs.into_iter().flatten()); + outputs.extend(transparent_outputs.into_iter().flatten()); + + let fee_amount = Zatoshis::try_from(transaction.fee_paid(|outpoint| { + utxos_map + .get(outpoint) + .copied() + // Error doesn't matter, this can never happen because we constructed the + // UTXOs map and the transaction from the same PCZT. + .ok_or(BalanceError::Overflow) + })?)?; + + // We don't need the spent UTXOs to be in transaction order. + let utxos_spent = utxos_map.into_keys().collect::>(); + + let created = time::OffsetDateTime::now_utc(); + + let transactions = vec![SentTransaction::new( + &transaction, + created, + BlockHeight::from_u32(proposal_info.target_height), + proposal_info.from_account, + &outputs, + fee_amount, + #[cfg(feature = "transparent-inputs")] + &utxos_spent, + )]; + + wallet_db + .store_transactions_to_be_sent(&transactions) + .map_err(Error::DataSource)?; + + Ok(txid) +} + +/// Constructs a transaction that consumes available transparent UTXOs belonging to the specified +/// secret key, and sends them to the most-preferred receiver of the default internal address for +/// the provided Unified Spending Key. +/// +/// This procedure will not attempt to shield transparent funds if the total amount being shielded +/// is less than the default fee to send the transaction. Fees will be paid only from the +/// transparent UTXOs being consumed. +/// +/// Parameters: +/// * `wallet_db`: A read/write reference to the wallet database +/// * `params`: Consensus parameters +/// * `spend_prover`: The [`sapling::SpendProver`] to use in constructing the shielded +/// transaction. +/// * `output_prover`: The [`sapling::OutputProver`] to use in constructing the shielded +/// transaction. +/// * `input_selector`: The [`InputSelector`] to for note selection and change and fee +/// determination +/// * `usk`: The unified spending key that will be used to detect and spend transparent UTXOs, +/// and that will provide the shielded address to which funds will be sent. Funds will be +/// shielded to the internal (change) address associated with the most preferred shielded +/// receiver corresponding to this account, or if no shielded receiver can be used for this +/// account, this function will return an error. This procedure will return an error if the +/// USK does not correspond to an account known to the wallet. +/// * `from_addrs`: The list of transparent addresses that will be used to filter transaparent +/// UTXOs received by the wallet. Only UTXOs received at one of the provided addresses will +/// be selected to be shielded. +/// * `min_confirmations`: The minimum number of confirmations that a previously +/// received note must have in the blockchain in order to be considered for being +/// spent. A value of 10 confirmations is recommended and 0-conf transactions are +/// not supported. +/// +/// [`sapling::SpendProver`]: sapling::prover::SpendProver +/// [`sapling::OutputProver`]: sapling::prover::OutputProver +#[cfg(feature = "transparent-inputs")] +#[allow(clippy::too_many_arguments)] +#[allow(clippy::type_complexity)] +pub fn shield_transparent_funds( + wallet_db: &mut DbT, + params: &ParamsT, + spend_prover: &impl SpendProver, + output_prover: &impl OutputProver, + input_selector: &InputsT, + change_strategy: &ChangeT, + shielding_threshold: Zatoshis, + usk: &UnifiedSpendingKey, + from_addrs: &[TransparentAddress], + to_account: ::AccountId, + min_confirmations: u32, +) -> Result, ShieldErrT> +where + ParamsT: consensus::Parameters, + DbT: WalletWrite + WalletCommitmentTrees + InputSource::Error>, + InputsT: ShieldingSelector, + ChangeT: ChangeStrategy, +{ + let proposal = propose_shielding( + wallet_db, + params, + input_selector, + change_strategy, + shielding_threshold, + from_addrs, + to_account, + min_confirmations, + )?; + + create_proposed_transactions( + wallet_db, + params, + spend_prover, + output_prover, + usk, + OvkPolicy::Sender, + &proposal, + ) } diff --git a/zcash_client_backend/src/data_api/wallet/input_selection.rs b/zcash_client_backend/src/data_api/wallet/input_selection.rs new file mode 100644 index 0000000000..15b604effb --- /dev/null +++ b/zcash_client_backend/src/data_api/wallet/input_selection.rs @@ -0,0 +1,865 @@ +//! Types related to the process of selecting inputs to be spent given a transaction request. + +use core::marker::PhantomData; +use std::{ + collections::BTreeMap, + error, + fmt::{self, Debug, Display}, +}; + +use ::transparent::bundle::TxOut; +use nonempty::NonEmpty; +use zcash_address::ConversionError; +use zcash_keys::address::{Address, UnifiedAddress}; +use zcash_protocol::{ + consensus::{self, BlockHeight}, + value::{BalanceError, Zatoshis}, + PoolType, ShieldedProtocol, +}; +use zip321::TransactionRequest; + +use crate::{ + data_api::{InputSource, SimpleNoteRetention, SpendableNotes}, + fees::{sapling, ChangeError, ChangeStrategy}, + proposal::{Proposal, ProposalError, ShieldedInputs}, + wallet::WalletTransparentOutput, +}; + +#[cfg(feature = "transparent-inputs")] +use { + crate::{ + fees::EphemeralBalance, + proposal::{Step, StepOutput, StepOutputIndex}, + }, + ::transparent::{address::TransparentAddress, bundle::OutPoint}, + std::collections::BTreeSet, + std::convert::Infallible, + zip321::Payment, +}; + +#[cfg(feature = "orchard")] +use crate::fees::orchard as orchard_fees; + +/// The type of errors that may be produced in input selection. +#[derive(Debug)] +pub enum InputSelectorError { + /// An error occurred accessing the underlying data store. + DataSource(DbErrT), + /// An error occurred specific to the provided input selector's selection rules. + Selection(SelectorErrT), + /// An error occurred in computing the change or fee for the proposed transfer. + Change(ChangeError), + /// Input selection attempted to generate an invalid transaction proposal. + Proposal(ProposalError), + /// An error occurred parsing the address from a payment request. + Address(ConversionError<&'static str>), + /// Insufficient funds were available to satisfy the payment request that inputs were being + /// selected to attempt to satisfy. + InsufficientFunds { + available: Zatoshis, + required: Zatoshis, + }, + /// The data source does not have enough information to choose an expiry height + /// for the transaction. + SyncRequired, +} + +impl fmt::Display + for InputSelectorError +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self { + InputSelectorError::DataSource(e) => { + write!( + f, + "The underlying datasource produced the following error: {}", + e + ) + } + InputSelectorError::Selection(e) => { + write!(f, "Note selection encountered the following error: {}", e) + } + InputSelectorError::Change(e) => write!( + f, + "Proposal generation failed due to an error in computing change or transaction fees: {}", + e + ), + InputSelectorError::Proposal(e) => { + write!( + f, + "Input selection attempted to generate an invalid proposal: {}", + e + ) + } + InputSelectorError::Address(e) => { + write!( + f, + "An error occurred decoding the address from a payment request: {}.", + e + ) + } + InputSelectorError::InsufficientFunds { + available, + required, + } => write!( + f, + "Insufficient balance (have {}, need {} including fee)", + u64::from(*available), + u64::from(*required) + ), + InputSelectorError::SyncRequired => { + write!(f, "Insufficient chain data is available, sync required.") + } + } + } +} + +impl error::Error for InputSelectorError +where + DE: Debug + Display + error::Error + 'static, + SE: Debug + Display + error::Error + 'static, + CE: Debug + Display + error::Error + 'static, + N: Debug + Display + 'static, +{ + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match &self { + Self::DataSource(e) => Some(e), + Self::Selection(e) => Some(e), + Self::Change(e) => Some(e), + Self::Proposal(e) => Some(e), + _ => None, + } + } +} + +impl From> for InputSelectorError { + fn from(value: ConversionError<&'static str>) -> Self { + InputSelectorError::Address(value) + } +} + +impl From> for InputSelectorError { + fn from(err: ChangeError) -> Self { + InputSelectorError::Change(err) + } +} + +/// A strategy for selecting transaction inputs and proposing transaction outputs. +/// +/// Proposals should include only economically useful inputs, as determined by `Self::FeeRule`; +/// that is, do not return inputs that cause fees to increase by an amount greater than the value +/// of the input. +pub trait InputSelector { + /// The type of errors that may be generated in input selection + type Error; + + /// The type of data source that the input selector expects to access to obtain input notes. + /// This associated type permits input selectors that may use specialized knowledge of the + /// internals of a particular backing data store, if the generic API of `InputSource` does not + /// provide sufficiently fine-grained operations for a particular backing store to optimally + /// perform input selection. + type InputSource: InputSource; + + /// Performs input selection and returns a proposal for transaction construction including + /// change and fee outputs. + /// + /// Implementations of this method should return inputs sufficient to satisfy the given + /// transaction request using a best-effort strategy to preserve user privacy, as follows: + /// * If it is possible to satisfy the specified transaction request by creating + /// a fully-shielded transaction without requiring value to cross pool boundaries, + /// return the inputs necessary to construct such a transaction; otherwise + /// * If it is possible to satisfy the transaction request by creating a fully-shielded + /// transaction with some amounts crossing between shielded pools, return the inputs + /// necessary. + /// + /// If insufficient funds are available to satisfy the required outputs for the shielding + /// request, this operation must fail and return [`InputSelectorError::InsufficientFunds`]. + #[allow(clippy::type_complexity)] + #[allow(clippy::too_many_arguments)] + fn propose_transaction( + &self, + params: &ParamsT, + wallet_db: &Self::InputSource, + target_height: BlockHeight, + anchor_height: BlockHeight, + account: ::AccountId, + transaction_request: TransactionRequest, + change_strategy: &ChangeT, + ) -> Result< + Proposal<::FeeRule, ::NoteRef>, + InputSelectorError< + ::Error, + Self::Error, + ChangeT::Error, + ::NoteRef, + >, + > + where + ParamsT: consensus::Parameters, + ChangeT: ChangeStrategy; +} + +/// A strategy for selecting transaction inputs and proposing transaction outputs +/// for shielding-only transactions (transactions which spend transparent UTXOs and +/// send all transaction outputs to the wallet's shielded internal address(es)). +#[cfg(feature = "transparent-inputs")] +pub trait ShieldingSelector { + /// The type of errors that may be generated in input selection + type Error; + /// The type of data source that the input selector expects to access to obtain input + /// transparent UTXOs. This associated type permits input selectors that may use specialized + /// knowledge of the internals of a particular backing data store, if the generic API of + /// [`InputSource`] does not provide sufficiently fine-grained operations for a + /// particular backing store to optimally perform input selection. + type InputSource: InputSource; + + /// Performs input selection and returns a proposal for the construction of a shielding + /// transaction. + /// + /// Implementations should return the maximum possible number of economically useful inputs + /// required to supply at least the requested value, choosing only inputs received at the + /// specified source addresses. If insufficient funds are available to satisfy the required + /// outputs for the shielding request, this operation must fail and return + /// [`InputSelectorError::InsufficientFunds`]. + #[allow(clippy::type_complexity)] + #[allow(clippy::too_many_arguments)] + fn propose_shielding( + &self, + params: &ParamsT, + wallet_db: &Self::InputSource, + change_strategy: &ChangeT, + shielding_threshold: Zatoshis, + source_addrs: &[TransparentAddress], + to_account: ::AccountId, + target_height: BlockHeight, + min_confirmations: u32, + ) -> Result< + Proposal<::FeeRule, Infallible>, + InputSelectorError< + ::Error, + Self::Error, + ChangeT::Error, + Infallible, + >, + > + where + ParamsT: consensus::Parameters, + ChangeT: ChangeStrategy; +} + +/// Errors that can occur as a consequence of greedy input selection. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum GreedyInputSelectorError { + /// An intermediate value overflowed or underflowed the valid monetary range. + Balance(BalanceError), + /// A unified address did not contain a supported receiver. + UnsupportedAddress(Box), + /// Support for transparent-source-only (TEX) addresses requires the transparent-inputs feature. + UnsupportedTexAddress, +} + +impl fmt::Display for GreedyInputSelectorError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self { + GreedyInputSelectorError::Balance(e) => write!( + f, + "A balance calculation violated amount validity bounds: {:?}.", + e + ), + GreedyInputSelectorError::UnsupportedAddress(_) => { + // we can't encode the UA to its string representation because we + // don't have network parameters here + write!(f, "Unified address contains no supported receivers.") + } + GreedyInputSelectorError::UnsupportedTexAddress => { + write!(f, "Support for transparent-source-only (TEX) addresses requires the transparent-inputs feature.") + } + } + } +} + +impl From + for InputSelectorError +{ + fn from(err: GreedyInputSelectorError) -> Self { + InputSelectorError::Selection(err) + } +} + +impl From + for InputSelectorError +{ + fn from(err: BalanceError) -> Self { + InputSelectorError::Selection(GreedyInputSelectorError::Balance(err)) + } +} + +pub(crate) struct SaplingPayment(Zatoshis); + +#[cfg(test)] +impl SaplingPayment { + pub(crate) fn new(amount: Zatoshis) -> Self { + SaplingPayment(amount) + } +} + +impl sapling::OutputView for SaplingPayment { + fn value(&self) -> Zatoshis { + self.0 + } +} + +#[cfg(feature = "orchard")] +pub(crate) struct OrchardPayment(Zatoshis); + +#[cfg(test)] +#[cfg(feature = "orchard")] +impl OrchardPayment { + pub(crate) fn new(amount: Zatoshis) -> Self { + OrchardPayment(amount) + } +} + +#[cfg(feature = "orchard")] +impl orchard_fees::OutputView for OrchardPayment { + fn value(&self) -> Zatoshis { + self.0 + } +} + +/// An [`InputSelector`] implementation that uses a greedy strategy to select between available +/// notes. +/// +/// This implementation performs input selection using methods available via the +/// [`InputSource`] interface. +pub struct GreedyInputSelector { + _ds_type: PhantomData, +} + +impl GreedyInputSelector { + /// Constructs a new greedy input selector that uses the provided change strategy to determine + /// change values and fee amounts. + /// + /// The [`ChangeStrategy`] provided must produce exactly one ephemeral change value when + /// computing a transaction balance if an [`EphemeralBalance::Output`] value is provided for + /// its ephemeral balance, or the resulting [`GreedyInputSelector`] will return an error when + /// attempting to construct a transaction proposal that requires such an output. + /// + /// [`EphemeralBalance::Output`]: crate::fees::EphemeralBalance::Output + pub fn new() -> Self { + GreedyInputSelector { + _ds_type: PhantomData, + } + } +} + +impl Default for GreedyInputSelector { + fn default() -> Self { + Self::new() + } +} + +impl InputSelector for GreedyInputSelector { + type Error = GreedyInputSelectorError; + type InputSource = DbT; + + #[allow(clippy::type_complexity)] + fn propose_transaction( + &self, + params: &ParamsT, + wallet_db: &Self::InputSource, + target_height: BlockHeight, + anchor_height: BlockHeight, + account: ::AccountId, + transaction_request: TransactionRequest, + change_strategy: &ChangeT, + ) -> Result< + Proposal<::FeeRule, DbT::NoteRef>, + InputSelectorError<::Error, Self::Error, ChangeT::Error, DbT::NoteRef>, + > + where + ParamsT: consensus::Parameters, + Self::InputSource: InputSource, + ChangeT: ChangeStrategy, + { + let mut transparent_outputs = vec![]; + let mut sapling_outputs = vec![]; + #[cfg(feature = "orchard")] + let mut orchard_outputs = vec![]; + let mut payment_pools = BTreeMap::new(); + + // In a ZIP 320 pair, tr0 refers to the first transaction request that + // collects shielded value and sends it to an ephemeral address, and tr1 + // refers to the second transaction request that pays the TEX addresses. + #[cfg(feature = "transparent-inputs")] + let mut tr1_transparent_outputs = vec![]; + #[cfg(feature = "transparent-inputs")] + let mut tr1_payments = vec![]; + #[cfg(feature = "transparent-inputs")] + let mut tr1_payment_pools = BTreeMap::new(); + // This balance value is just used for overflow checking; the actual value of ephemeral + // outputs will be computed from the constructed `tr1_transparent_outputs` value + // constructed below. + #[cfg(feature = "transparent-inputs")] + let mut total_ephemeral = Zatoshis::ZERO; + + for (idx, payment) in transaction_request.payments() { + let recipient_address: Address = payment + .recipient_address() + .clone() + .convert_if_network(params.network_type())?; + + match recipient_address { + Address::Transparent(addr) => { + payment_pools.insert(*idx, PoolType::TRANSPARENT); + transparent_outputs.push(TxOut { + value: payment.amount(), + script_pubkey: addr.script(), + }); + } + #[cfg(feature = "transparent-inputs")] + Address::Tex(data) => { + let p2pkh_addr = TransparentAddress::PublicKeyHash(data); + + tr1_payment_pools.insert(*idx, PoolType::TRANSPARENT); + tr1_transparent_outputs.push(TxOut { + value: payment.amount(), + script_pubkey: p2pkh_addr.script(), + }); + tr1_payments.push( + Payment::new( + payment.recipient_address().clone(), + payment.amount(), + None, + payment.label().cloned(), + payment.message().cloned(), + payment.other_params().to_vec(), + ) + .expect("cannot fail because memo is None"), + ); + total_ephemeral = (total_ephemeral + payment.amount()) + .ok_or(GreedyInputSelectorError::Balance(BalanceError::Overflow))?; + } + #[cfg(not(feature = "transparent-inputs"))] + Address::Tex(_) => { + return Err(InputSelectorError::Selection( + GreedyInputSelectorError::UnsupportedTexAddress, + )); + } + Address::Sapling(_) => { + payment_pools.insert(*idx, PoolType::SAPLING); + sapling_outputs.push(SaplingPayment(payment.amount())); + } + Address::Unified(addr) => { + #[cfg(feature = "orchard")] + if addr.has_orchard() { + payment_pools.insert(*idx, PoolType::ORCHARD); + orchard_outputs.push(OrchardPayment(payment.amount())); + continue; + } + + if addr.has_sapling() { + payment_pools.insert(*idx, PoolType::SAPLING); + sapling_outputs.push(SaplingPayment(payment.amount())); + continue; + } + + if let Some(addr) = addr.transparent() { + payment_pools.insert(*idx, PoolType::TRANSPARENT); + transparent_outputs.push(TxOut { + value: payment.amount(), + script_pubkey: addr.script(), + }); + continue; + } + + return Err(InputSelectorError::Selection( + GreedyInputSelectorError::UnsupportedAddress(Box::new(addr)), + )); + } + } + } + + let mut shielded_inputs = SpendableNotes::empty(); + let mut prior_available = Zatoshis::ZERO; + let mut amount_required = Zatoshis::ZERO; + let mut exclude: Vec = vec![]; + + // This loop is guaranteed to terminate because on each iteration we check that the amount + // of funds selected is strictly increasing. The loop will either return a successful + // result or the wallet will eventually run out of funds to select. + loop { + #[cfg(not(feature = "orchard"))] + let use_sapling = true; + #[cfg(feature = "orchard")] + let (use_sapling, use_orchard) = { + let (sapling_input_total, orchard_input_total) = ( + shielded_inputs.sapling_value()?, + shielded_inputs.orchard_value()?, + ); + + // Use Sapling inputs if there are no Orchard outputs or if there are insufficient + // funds from Orchard inputs to cover the amount required. + let use_sapling = + orchard_outputs.is_empty() || amount_required > orchard_input_total; + // Use Orchard inputs if there are insufficient funds from Sapling inputs to cover + // the amount required. + let use_orchard = !use_sapling || amount_required > sapling_input_total; + + (use_sapling, use_orchard) + }; + + let sapling_inputs = if use_sapling { + shielded_inputs + .sapling() + .iter() + .map(|i| (*i.internal_note_id(), i.note().value())) + .collect() + } else { + vec![] + }; + + #[cfg(feature = "orchard")] + let orchard_inputs = if use_orchard { + shielded_inputs + .orchard() + .iter() + .map(|i| (*i.internal_note_id(), i.note().value())) + .collect() + } else { + vec![] + }; + + let selected_input_ids = sapling_inputs.iter().map(|(id, _)| id); + #[cfg(feature = "orchard")] + let selected_input_ids = + selected_input_ids.chain(orchard_inputs.iter().map(|(id, _)| id)); + + let selected_input_ids = selected_input_ids.cloned().collect::>(); + + let wallet_meta = change_strategy + .fetch_wallet_meta(wallet_db, account, &selected_input_ids) + .map_err(InputSelectorError::DataSource)?; + + #[cfg(not(feature = "transparent-inputs"))] + let ephemeral_balance = None; + + #[cfg(feature = "transparent-inputs")] + let (ephemeral_balance, tr1_balance_opt) = { + if tr1_transparent_outputs.is_empty() { + (None, None) + } else { + // The ephemeral input going into transaction 1 must be able to pay that + // transaction's fee, as well as the TEX address payments. + + // First compute the required total with an additional zero input, + // catching the `InsufficientFunds` error to obtain the required amount + // given the provided change strategy. Ignore the change memo in order + // to avoid adding a change output. + let tr1_required_input_value = match change_strategy + .compute_balance::<_, DbT::NoteRef>( + params, + target_height, + &[] as &[WalletTransparentOutput], + &tr1_transparent_outputs, + &sapling::EmptyBundleView, + #[cfg(feature = "orchard")] + &orchard_fees::EmptyBundleView, + Some(&EphemeralBalance::Input(Zatoshis::ZERO)), + &wallet_meta, + ) { + Err(ChangeError::InsufficientFunds { required, .. }) => required, + Err(ChangeError::DustInputs { .. }) => { + unreachable!("no inputs were supplied") + } + Err(other) => return Err(InputSelectorError::Change(other)), + Ok(_) => Zatoshis::ZERO, // shouldn't happen + }; + + // Now recompute to obtain the `TransactionBalance` and verify that it + // fully accounts for the required fees. + let tr1_balance = change_strategy.compute_balance::<_, DbT::NoteRef>( + params, + target_height, + &[] as &[WalletTransparentOutput], + &tr1_transparent_outputs, + &sapling::EmptyBundleView, + #[cfg(feature = "orchard")] + &orchard_fees::EmptyBundleView, + Some(&EphemeralBalance::Input(tr1_required_input_value)), + &wallet_meta, + )?; + assert_eq!(tr1_balance.total(), tr1_balance.fee_required()); + + ( + Some(EphemeralBalance::Output(tr1_required_input_value)), + Some(tr1_balance), + ) + } + }; + + // In the ZIP 320 case, this is the balance for transaction 0, taking into account + // the ephemeral output. + let balance = change_strategy.compute_balance( + params, + target_height, + &[] as &[WalletTransparentOutput], + &transparent_outputs, + &( + ::sapling::builder::BundleType::DEFAULT, + &sapling_inputs[..], + &sapling_outputs[..], + ), + #[cfg(feature = "orchard")] + &( + ::orchard::builder::BundleType::DEFAULT, + &orchard_inputs[..], + &orchard_outputs[..], + ), + ephemeral_balance.as_ref(), + &wallet_meta, + ); + + match balance { + Ok(balance) => { + // At this point, we have enough input value to pay for everything, so we will + // return at the end of this block. + + let shielded_inputs = + NonEmpty::from_vec(shielded_inputs.into_vec(&SimpleNoteRetention { + sapling: use_sapling, + #[cfg(feature = "orchard")] + orchard: use_orchard, + })) + .map(|notes| ShieldedInputs::from_parts(anchor_height, notes)); + + #[cfg(feature = "transparent-inputs")] + if let Some(tr1_balance) = tr1_balance_opt { + // Construct two new `TransactionRequest`s: + // * `tr0` excludes the TEX outputs, and in their place includes + // a single additional ephemeral output to the transparent pool. + // * `tr1` spends from that ephemeral output to each TEX output. + + // Find exactly one ephemeral change output. + let ephemeral_outputs = balance + .proposed_change() + .iter() + .enumerate() + .filter(|(_, c)| c.is_ephemeral()) + .collect::>(); + + let ephemeral_value = ephemeral_balance + .and_then(|b| b.ephemeral_output_amount()) + .expect("ephemeral output balance exists (constructed above)"); + + let ephemeral_output_index = match &ephemeral_outputs[..] { + [(i, change_value)] if change_value.value() == ephemeral_value => { + Ok(*i) + } + _ => Err(InputSelectorError::Proposal( + ProposalError::EphemeralOutputsInvalid, + )), + }?; + + let ephemeral_stepoutput = + StepOutput::new(0, StepOutputIndex::Change(ephemeral_output_index)); + + let tr0 = TransactionRequest::from_indexed( + transaction_request + .payments() + .iter() + .filter(|(idx, _payment)| !tr1_payment_pools.contains_key(idx)) + .map(|(k, v)| (*k, v.clone())) + .collect(), + ) + .expect("removing payments from a TransactionRequest preserves validity"); + + let mut steps = vec![]; + steps.push( + Step::from_parts( + &[], + tr0, + payment_pools, + vec![], + shielded_inputs, + vec![], + balance, + false, + ) + .map_err(InputSelectorError::Proposal)?, + ); + + let tr1 = + TransactionRequest::new(tr1_payments).expect("valid by construction"); + steps.push( + Step::from_parts( + &steps, + tr1, + tr1_payment_pools, + vec![], + None, + vec![ephemeral_stepoutput], + tr1_balance, + false, + ) + .map_err(InputSelectorError::Proposal)?, + ); + + return Proposal::multi_step( + change_strategy.fee_rule().clone(), + target_height, + NonEmpty::from_vec(steps).expect("steps is known to be nonempty"), + ) + .map_err(InputSelectorError::Proposal); + } + + return Proposal::single_step( + transaction_request, + payment_pools, + vec![], + shielded_inputs, + balance, + (*change_strategy.fee_rule()).clone(), + target_height, + false, + ) + .map_err(InputSelectorError::Proposal); + } + Err(ChangeError::DustInputs { + mut sapling, + #[cfg(feature = "orchard")] + mut orchard, + .. + }) => { + exclude.append(&mut sapling); + #[cfg(feature = "orchard")] + exclude.append(&mut orchard); + } + Err(ChangeError::InsufficientFunds { required, .. }) => { + amount_required = required; + } + Err(other) => return Err(InputSelectorError::Change(other)), + } + + #[cfg(not(feature = "orchard"))] + let selectable_pools = &[ShieldedProtocol::Sapling]; + #[cfg(feature = "orchard")] + let selectable_pools = &[ShieldedProtocol::Sapling, ShieldedProtocol::Orchard]; + + shielded_inputs = wallet_db + .select_spendable_notes( + account, + amount_required, + selectable_pools, + anchor_height, + &exclude, + ) + .map_err(InputSelectorError::DataSource)?; + + let new_available = shielded_inputs.total_value()?; + if new_available <= prior_available { + return Err(InputSelectorError::InsufficientFunds { + required: amount_required, + available: new_available, + }); + } else { + // If the set of selected inputs has changed after selection, we will loop again + // and see whether we now have enough funds. + prior_available = new_available; + } + } + } +} + +#[cfg(feature = "transparent-inputs")] +impl ShieldingSelector for GreedyInputSelector { + type Error = GreedyInputSelectorError; + type InputSource = DbT; + + #[allow(clippy::type_complexity)] + fn propose_shielding( + &self, + params: &ParamsT, + wallet_db: &Self::InputSource, + change_strategy: &ChangeT, + shielding_threshold: Zatoshis, + source_addrs: &[TransparentAddress], + to_account: ::AccountId, + target_height: BlockHeight, + min_confirmations: u32, + ) -> Result< + Proposal<::FeeRule, Infallible>, + InputSelectorError<::Error, Self::Error, ChangeT::Error, Infallible>, + > + where + ParamsT: consensus::Parameters, + ChangeT: ChangeStrategy, + { + let mut transparent_inputs: Vec = source_addrs + .iter() + .map(|taddr| { + wallet_db.get_spendable_transparent_outputs(taddr, target_height, min_confirmations) + }) + .collect::>, _>>() + .map_err(InputSelectorError::DataSource)? + .into_iter() + .flat_map(|v| v.into_iter()) + .collect(); + + let wallet_meta = change_strategy + .fetch_wallet_meta(wallet_db, to_account, &[]) + .map_err(InputSelectorError::DataSource)?; + + let trial_balance = change_strategy.compute_balance( + params, + target_height, + &transparent_inputs, + &[] as &[TxOut], + &sapling::EmptyBundleView, + #[cfg(feature = "orchard")] + &orchard_fees::EmptyBundleView, + None, + &wallet_meta, + ); + + let balance = match trial_balance { + Ok(balance) => balance, + Err(ChangeError::DustInputs { transparent, .. }) => { + let exclusions: BTreeSet = transparent.into_iter().collect(); + transparent_inputs.retain(|i| !exclusions.contains(i.outpoint())); + + change_strategy.compute_balance( + params, + target_height, + &transparent_inputs, + &[] as &[TxOut], + &sapling::EmptyBundleView, + #[cfg(feature = "orchard")] + &orchard_fees::EmptyBundleView, + None, + &wallet_meta, + )? + } + Err(other) => return Err(InputSelectorError::Change(other)), + }; + + if balance.total() >= shielding_threshold { + Proposal::single_step( + TransactionRequest::empty(), + BTreeMap::new(), + transparent_inputs, + None, + balance, + (*change_strategy.fee_rule()).clone(), + target_height, + true, + ) + .map_err(InputSelectorError::Proposal) + } else { + Err(InputSelectorError::InsufficientFunds { + available: balance.total(), + required: shielding_threshold, + }) + } + } +} diff --git a/zcash_client_backend/src/decrypt.rs b/zcash_client_backend/src/decrypt.rs index b304ce7a0d..d2d4ef8781 100644 --- a/zcash_client_backend/src/decrypt.rs +++ b/zcash_client_backend/src/decrypt.rs @@ -1,72 +1,226 @@ use std::collections::HashMap; +use sapling::note_encryption::{PreparedIncomingViewingKey, SaplingDomain}; +use zcash_keys::keys::UnifiedFullViewingKey; +use zcash_note_encryption::{try_note_decryption, try_output_recovery_with_ovk}; use zcash_primitives::{ + transaction::components::sapling::zip212_enforcement, transaction::Transaction, +}; +use zcash_protocol::{ consensus::{self, BlockHeight}, memo::MemoBytes, - sapling::{ - note_encryption::{try_sapling_note_decryption, try_sapling_output_recovery}, - Note, PaymentAddress, - }, - transaction::Transaction, - zip32::ExtendedFullViewingKey, + value::Zatoshis, }; +use zip32::Scope; + +use crate::data_api::DecryptedTransaction; -use crate::wallet::AccountId; +#[cfg(feature = "orchard")] +use orchard::note_encryption::OrchardDomain; + +/// An enumeration of the possible relationships a TXO can have to the wallet. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum TransferType { + /// The output was received on one of the wallet's external addresses via decryption using the + /// associated incoming viewing key, or at one of the wallet's transparent addresses. + Incoming, + /// The output was received on one of the wallet's internal-only shielded addresses via trial + /// decryption using one of the wallet's internal incoming viewing keys. + WalletInternal, + /// The output was decrypted using one of the wallet's outgoing viewing keys, or was created + /// in a transaction constructed by this wallet. + Outgoing, +} /// A decrypted shielded output. -pub struct DecryptedOutput { - /// The index of the output within [`shielded_outputs`]. - /// - /// [`shielded_outputs`]: zcash_primitives::transaction::TransactionData - pub index: usize, +pub struct DecryptedOutput { + index: usize, + note: Note, + account: AccountId, + memo: MemoBytes, + transfer_type: TransferType, +} + +impl DecryptedOutput { + pub fn new( + index: usize, + note: Note, + account: AccountId, + memo: MemoBytes, + transfer_type: TransferType, + ) -> Self { + Self { + index, + note, + account, + memo, + transfer_type, + } + } + + /// The index of the output within the shielded outputs of the Sapling bundle or the actions of + /// the Orchard bundle, depending upon the type of [`Self::note`]. + pub fn index(&self) -> usize { + self.index + } + /// The note within the output. - pub note: Note, + pub fn note(&self) -> &Note { + &self.note + } + /// The account that decrypted the note. - pub account: AccountId, - /// The address the note was sent to. - pub to: PaymentAddress, + pub fn account(&self) -> &AccountId { + &self.account + } + /// The memo bytes included with the note. - pub memo: MemoBytes, - /// True if this output was recovered using an [`OutgoingViewingKey`], meaning that - /// this is a logical output of the transaction. - /// - /// [`OutgoingViewingKey`]: zcash_primitives::sapling::keys::OutgoingViewingKey - pub outgoing: bool, + pub fn memo(&self) -> &MemoBytes { + &self.memo + } + + /// Returns a [`TransferType`] value that is determined based upon what type of key was used to + /// decrypt the transaction. + pub fn transfer_type(&self) -> TransferType { + self.transfer_type + } +} + +impl DecryptedOutput { + pub fn note_value(&self) -> Zatoshis { + Zatoshis::from_u64(self.note.value().inner()) + .expect("Sapling note value is expected to have been validated by consensus.") + } +} + +#[cfg(feature = "orchard")] +impl DecryptedOutput { + pub fn note_value(&self) -> Zatoshis { + Zatoshis::from_u64(self.note.value().inner()) + .expect("Orchard note value is expected to have been validated by consensus.") + } } /// Scans a [`Transaction`] for any information that can be decrypted by the set of -/// [`ExtendedFullViewingKey`]s. -pub fn decrypt_transaction( +/// [`UnifiedFullViewingKey`]s. +pub fn decrypt_transaction<'a, P: consensus::Parameters, AccountId: Copy>( params: &P, height: BlockHeight, - tx: &Transaction, - extfvks: &HashMap, -) -> Vec { - let mut decrypted = vec![]; - - for (account, extfvk) in extfvks.iter() { - let ivk = extfvk.fvk.vk.ivk(); - let ovk = extfvk.fvk.ovk; - - for (index, output) in tx.shielded_outputs.iter().enumerate() { - let ((note, to, memo), outgoing) = - match try_sapling_note_decryption(params, height, &ivk, output) { - Some(ret) => (ret, false), - None => match try_sapling_output_recovery(params, height, &ovk, output) { - Some(ret) => (ret, true), - None => continue, - }, - }; - decrypted.push(DecryptedOutput { - index, - note, - account: *account, - to, - memo, - outgoing, - }) - } - } + tx: &'a Transaction, + ufvks: &HashMap, +) -> DecryptedTransaction<'a, AccountId> { + let zip212_enforcement = zip212_enforcement(params, height); + let sapling_bundle = tx.sapling_bundle(); + let sapling_outputs = sapling_bundle + .iter() + .flat_map(|bundle| { + ufvks + .iter() + .flat_map(|(account, ufvk)| ufvk.sapling().into_iter().map(|dfvk| (*account, dfvk))) + .flat_map(|(account, dfvk)| { + let sapling_domain = SaplingDomain::new(zip212_enforcement); + let ivk_external = + PreparedIncomingViewingKey::new(&dfvk.to_ivk(Scope::External)); + let ivk_internal = + PreparedIncomingViewingKey::new(&dfvk.to_ivk(Scope::Internal)); + let ovk = dfvk.fvk().ovk; + + bundle + .shielded_outputs() + .iter() + .enumerate() + .flat_map(move |(index, output)| { + try_note_decryption(&sapling_domain, &ivk_external, output) + .map(|ret| (ret, TransferType::Incoming)) + .or_else(|| { + try_note_decryption(&sapling_domain, &ivk_internal, output) + .map(|ret| (ret, TransferType::WalletInternal)) + }) + .or_else(|| { + try_output_recovery_with_ovk( + &sapling_domain, + &ovk, + output, + output.cv(), + output.out_ciphertext(), + ) + .map(|ret| (ret, TransferType::Outgoing)) + }) + .into_iter() + .map(move |((note, _, memo), transfer_type)| { + DecryptedOutput::new( + index, + note, + account, + MemoBytes::from_bytes(&memo).expect("correct length"), + transfer_type, + ) + }) + }) + }) + }) + .collect(); + + #[cfg(feature = "orchard")] + let orchard_bundle = tx.orchard_bundle(); + #[cfg(feature = "orchard")] + let orchard_outputs = orchard_bundle + .iter() + .flat_map(|bundle| { + ufvks + .iter() + .flat_map(|(account, ufvk)| ufvk.orchard().into_iter().map(|fvk| (*account, fvk))) + .flat_map(|(account, fvk)| { + let ivk_external = orchard::keys::PreparedIncomingViewingKey::new( + &fvk.to_ivk(Scope::External), + ); + let ivk_internal = orchard::keys::PreparedIncomingViewingKey::new( + &fvk.to_ivk(Scope::Internal), + ); + let ovk = fvk.to_ovk(Scope::External); + + bundle + .actions() + .iter() + .enumerate() + .flat_map(move |(index, action)| { + let domain = OrchardDomain::for_action(action); + try_note_decryption(&domain, &ivk_external, action) + .map(|ret| (ret, TransferType::Incoming)) + .or_else(|| { + try_note_decryption(&domain, &ivk_internal, action) + .map(|ret| (ret, TransferType::WalletInternal)) + }) + .or_else(|| { + try_output_recovery_with_ovk( + &domain, + &ovk, + action, + action.cv_net(), + &action.encrypted_note().out_ciphertext, + ) + .map(|ret| (ret, TransferType::Outgoing)) + }) + .into_iter() + .map(move |((note, _, memo), transfer_type)| { + DecryptedOutput::new( + index, + note, + account, + MemoBytes::from_bytes(&memo).expect("correct length"), + transfer_type, + ) + }) + }) + }) + }) + .collect(); - decrypted + DecryptedTransaction::new( + Some(height), + tx, + sapling_outputs, + #[cfg(feature = "orchard")] + orchard_outputs, + ) } diff --git a/zcash_client_backend/src/encoding.rs b/zcash_client_backend/src/encoding.rs deleted file mode 100644 index 827b99bf0a..0000000000 --- a/zcash_client_backend/src/encoding.rs +++ /dev/null @@ -1,460 +0,0 @@ -//! Encoding and decoding functions for Zcash key and address structs. -//! -//! Human-Readable Prefixes (HRPs) for Bech32 encodings are located in the -//! [zcash_primitives::constants][constants] module. -//! -//! [constants]: zcash_primitives::constants - -use bech32::{self, Error, FromBase32, ToBase32, Variant}; -use bs58::{self, decode::Error as Bs58Error}; -use std::convert::TryInto; -use std::io::{self, Write}; -use zcash_primitives::{ - legacy::TransparentAddress, - sapling::PaymentAddress, - zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, -}; - -fn bech32_encode(hrp: &str, write: F) -> String -where - F: Fn(&mut dyn Write) -> io::Result<()>, -{ - let mut data: Vec = vec![]; - write(&mut data).expect("Should be able to write to a Vec"); - bech32::encode(hrp, data.to_base32(), Variant::Bech32).expect("hrp is invalid") -} - -fn bech32_decode(hrp: &str, s: &str, read: F) -> Result, Error> -where - F: Fn(Vec) -> Option, -{ - match bech32::decode(s)? { - (decoded_hrp, data, Variant::Bech32) if decoded_hrp == hrp => { - Vec::::from_base32(&data).map(|data| read(data)) - } - _ => Ok(None), - } -} - -/// Writes an [`ExtendedSpendingKey`] as a Bech32-encoded string. -/// -/// # Examples -/// -/// ``` -/// use zcash_primitives::{ -/// constants::testnet::{COIN_TYPE, HRP_SAPLING_EXTENDED_SPENDING_KEY}, -/// }; -/// use zcash_client_backend::{ -/// encoding::encode_extended_spending_key, -/// keys::spending_key, -/// }; -/// -/// let extsk = spending_key(&[0; 32][..], COIN_TYPE, 0); -/// let encoded = encode_extended_spending_key(HRP_SAPLING_EXTENDED_SPENDING_KEY, &extsk); -/// ``` -/// [`ExtendedSpendingKey`]: zcash_primitives::zip32::ExtendedSpendingKey -pub fn encode_extended_spending_key(hrp: &str, extsk: &ExtendedSpendingKey) -> String { - bech32_encode(hrp, |w| extsk.write(w)) -} - -/// Decodes an [`ExtendedSpendingKey`] from a Bech32-encoded string. -/// -/// [`ExtendedSpendingKey`]: zcash_primitives::zip32::ExtendedSpendingKey -pub fn decode_extended_spending_key( - hrp: &str, - s: &str, -) -> Result, Error> { - bech32_decode(hrp, s, |data| ExtendedSpendingKey::read(&data[..]).ok()) -} - -/// Writes an [`ExtendedFullViewingKey`] as a Bech32-encoded string. -/// -/// # Examples -/// -/// ``` -/// use zcash_primitives::{ -/// constants::testnet::{COIN_TYPE, HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY}, -/// }; -/// use zcash_client_backend::{ -/// encoding::encode_extended_full_viewing_key, -/// keys::spending_key, -/// }; -/// use zcash_primitives::zip32::ExtendedFullViewingKey; -/// -/// let extsk = spending_key(&[0; 32][..], COIN_TYPE, 0); -/// let extfvk = ExtendedFullViewingKey::from(&extsk); -/// let encoded = encode_extended_full_viewing_key(HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, &extfvk); -/// ``` -/// [`ExtendedFullViewingKey`]: zcash_primitives::zip32::ExtendedFullViewingKey -pub fn encode_extended_full_viewing_key(hrp: &str, extfvk: &ExtendedFullViewingKey) -> String { - bech32_encode(hrp, |w| extfvk.write(w)) -} - -/// Decodes an [`ExtendedFullViewingKey`] from a Bech32-encoded string. -/// -/// [`ExtendedFullViewingKey`]: zcash_primitives::zip32::ExtendedFullViewingKey -pub fn decode_extended_full_viewing_key( - hrp: &str, - s: &str, -) -> Result, Error> { - bech32_decode(hrp, s, |data| ExtendedFullViewingKey::read(&data[..]).ok()) -} - -/// Writes a [`PaymentAddress`] as a Bech32-encoded string. -/// -/// # Examples -/// -/// ``` -/// use group::Group; -/// use jubjub::SubgroupPoint; -/// use rand_core::SeedableRng; -/// use rand_xorshift::XorShiftRng; -/// use zcash_client_backend::{ -/// encoding::encode_payment_address, -/// }; -/// use zcash_primitives::{ -/// constants::testnet::HRP_SAPLING_PAYMENT_ADDRESS, -/// sapling::{Diversifier, PaymentAddress}, -/// }; -/// -/// let rng = &mut XorShiftRng::from_seed([ -/// 0x59, 0x62, 0xbe, 0x3d, 0x76, 0x3d, 0x31, 0x8d, 0x17, 0xdb, 0x37, 0x32, 0x54, 0x06, -/// 0xbc, 0xe5, -/// ]); -/// -/// let pa = PaymentAddress::from_parts( -/// Diversifier([0u8; 11]), -/// SubgroupPoint::random(rng), -/// ) -/// .unwrap(); -/// -/// assert_eq!( -/// encode_payment_address(HRP_SAPLING_PAYMENT_ADDRESS, &pa), -/// "ztestsapling1qqqqqqqqqqqqqqqqqqcguyvaw2vjk4sdyeg0lc970u659lvhqq7t0np6hlup5lusxle75ss7jnk", -/// ); -/// ``` -/// [`PaymentAddress`]: zcash_primitives::sapling::PaymentAddress -pub fn encode_payment_address(hrp: &str, addr: &PaymentAddress) -> String { - bech32_encode(hrp, |w| w.write_all(&addr.to_bytes())) -} - -/// Decodes a [`PaymentAddress`] from a Bech32-encoded string. -/// -/// # Examples -/// -/// ``` -/// use group::Group; -/// use jubjub::SubgroupPoint; -/// use rand_core::SeedableRng; -/// use rand_xorshift::XorShiftRng; -/// use zcash_client_backend::{ -/// encoding::decode_payment_address, -/// }; -/// use zcash_primitives::{ -/// constants::testnet::HRP_SAPLING_PAYMENT_ADDRESS, -/// sapling::{Diversifier, PaymentAddress}, -/// }; -/// -/// let rng = &mut XorShiftRng::from_seed([ -/// 0x59, 0x62, 0xbe, 0x3d, 0x76, 0x3d, 0x31, 0x8d, 0x17, 0xdb, 0x37, 0x32, 0x54, 0x06, -/// 0xbc, 0xe5, -/// ]); -/// -/// let pa = PaymentAddress::from_parts( -/// Diversifier([0u8; 11]), -/// SubgroupPoint::random(rng), -/// ) -/// .unwrap(); -/// -/// assert_eq!( -/// decode_payment_address( -/// HRP_SAPLING_PAYMENT_ADDRESS, -/// "ztestsapling1qqqqqqqqqqqqqqqqqqcguyvaw2vjk4sdyeg0lc970u659lvhqq7t0np6hlup5lusxle75ss7jnk", -/// ), -/// Ok(Some(pa)), -/// ); -/// ``` -/// [`PaymentAddress`]: zcash_primitives::sapling::PaymentAddress -pub fn decode_payment_address(hrp: &str, s: &str) -> Result, Error> { - bech32_decode(hrp, s, |data| { - if data.len() != 43 { - return None; - } - - let mut bytes = [0; 43]; - bytes.copy_from_slice(&data); - PaymentAddress::from_bytes(&bytes) - }) -} - -/// Writes a [`TransparentAddress`] as a Base58Check-encoded string. -/// -/// # Examples -/// -/// ``` -/// use zcash_client_backend::{ -/// encoding::encode_transparent_address, -/// }; -/// use zcash_primitives::{ -/// constants::testnet::{B58_PUBKEY_ADDRESS_PREFIX, B58_SCRIPT_ADDRESS_PREFIX}, -/// legacy::TransparentAddress, -/// }; -/// -/// assert_eq!( -/// encode_transparent_address( -/// &B58_PUBKEY_ADDRESS_PREFIX, -/// &B58_SCRIPT_ADDRESS_PREFIX, -/// &TransparentAddress::PublicKey([0; 20]), -/// ), -/// "tm9iMLAuYMzJ6jtFLcA7rzUmfreGuKvr7Ma", -/// ); -/// -/// assert_eq!( -/// encode_transparent_address( -/// &B58_PUBKEY_ADDRESS_PREFIX, -/// &B58_SCRIPT_ADDRESS_PREFIX, -/// &TransparentAddress::Script([0; 20]), -/// ), -/// "t26YoyZ1iPgiMEWL4zGUm74eVWfhyDMXzY2", -/// ); -/// ``` -/// [`TransparentAddress`]: zcash_primitives::legacy::TransparentAddress -pub fn encode_transparent_address( - pubkey_version: &[u8], - script_version: &[u8], - addr: &TransparentAddress, -) -> String { - let decoded = match addr { - TransparentAddress::PublicKey(key_id) => { - let mut decoded = vec![0; pubkey_version.len() + 20]; - decoded[..pubkey_version.len()].copy_from_slice(pubkey_version); - decoded[pubkey_version.len()..].copy_from_slice(key_id); - decoded - } - TransparentAddress::Script(script_id) => { - let mut decoded = vec![0; script_version.len() + 20]; - decoded[..script_version.len()].copy_from_slice(script_version); - decoded[script_version.len()..].copy_from_slice(script_id); - decoded - } - }; - bs58::encode(decoded).with_check().into_string() -} - -/// Decodes a [`TransparentAddress`] from a Base58Check-encoded string. -/// -/// # Examples -/// -/// ``` -/// use zcash_primitives::{ -/// constants::testnet::{B58_PUBKEY_ADDRESS_PREFIX, B58_SCRIPT_ADDRESS_PREFIX}, -/// }; -/// use zcash_client_backend::{ -/// encoding::decode_transparent_address, -/// }; -/// use zcash_primitives::legacy::TransparentAddress; -/// -/// assert_eq!( -/// decode_transparent_address( -/// &B58_PUBKEY_ADDRESS_PREFIX, -/// &B58_SCRIPT_ADDRESS_PREFIX, -/// "tm9iMLAuYMzJ6jtFLcA7rzUmfreGuKvr7Ma", -/// ), -/// Ok(Some(TransparentAddress::PublicKey([0; 20]))), -/// ); -/// -/// assert_eq!( -/// decode_transparent_address( -/// &B58_PUBKEY_ADDRESS_PREFIX, -/// &B58_SCRIPT_ADDRESS_PREFIX, -/// "t26YoyZ1iPgiMEWL4zGUm74eVWfhyDMXzY2", -/// ), -/// Ok(Some(TransparentAddress::Script([0; 20]))), -/// ); -/// ``` -/// [`TransparentAddress`]: zcash_primitives::legacy::TransparentAddress -pub fn decode_transparent_address( - pubkey_version: &[u8], - script_version: &[u8], - s: &str, -) -> Result, Bs58Error> { - bs58::decode(s).with_check(None).into_vec().map(|decoded| { - if decoded.starts_with(pubkey_version) { - decoded[pubkey_version.len()..] - .try_into() - .ok() - .map(TransparentAddress::PublicKey) - } else if decoded.starts_with(script_version) { - decoded[script_version.len()..] - .try_into() - .ok() - .map(TransparentAddress::Script) - } else { - None - } - }) -} - -#[cfg(test)] -mod tests { - use group::Group; - use rand_core::SeedableRng; - use rand_xorshift::XorShiftRng; - use zcash_primitives::{ - constants, - sapling::{Diversifier, PaymentAddress}, - zip32::ExtendedSpendingKey, - }; - - use super::{ - decode_extended_full_viewing_key, decode_extended_spending_key, decode_payment_address, - encode_extended_full_viewing_key, encode_extended_spending_key, encode_payment_address, - }; - - #[test] - fn extended_spending_key() { - let extsk = ExtendedSpendingKey::master(&[0; 32][..]); - - let encoded_main = "secret-extended-key-main1qqqqqqqqqqqqqq8n3zjjmvhhr854uy3qhpda3ml34haf0x388z5r7h4st4kpsf6qysqws3xh6qmha7gna72fs2n4clnc9zgyd22s658f65pex4exe56qjk5pqj9vfdq7dfdhjc2rs9jdwq0zl99uwycyrxzp86705rk687spn44e2uhm7h0hsagfvkk4n7n6nfer6u57v9cac84t7nl2zth0xpyfeg0w2p2wv2yn6jn923aaz0vdaml07l60ahapk6efchyxwysrvjs87qvlj"; - let encoded_test = "secret-extended-key-test1qqqqqqqqqqqqqq8n3zjjmvhhr854uy3qhpda3ml34haf0x388z5r7h4st4kpsf6qysqws3xh6qmha7gna72fs2n4clnc9zgyd22s658f65pex4exe56qjk5pqj9vfdq7dfdhjc2rs9jdwq0zl99uwycyrxzp86705rk687spn44e2uhm7h0hsagfvkk4n7n6nfer6u57v9cac84t7nl2zth0xpyfeg0w2p2wv2yn6jn923aaz0vdaml07l60ahapk6efchyxwysrvjsvzyw8j"; - - assert_eq!( - encode_extended_spending_key( - constants::mainnet::HRP_SAPLING_EXTENDED_SPENDING_KEY, - &extsk - ), - encoded_main - ); - assert_eq!( - decode_extended_spending_key( - constants::mainnet::HRP_SAPLING_EXTENDED_SPENDING_KEY, - encoded_main - ) - .unwrap(), - Some(extsk.clone()) - ); - - assert_eq!( - encode_extended_spending_key( - constants::testnet::HRP_SAPLING_EXTENDED_SPENDING_KEY, - &extsk - ), - encoded_test - ); - assert_eq!( - decode_extended_spending_key( - constants::testnet::HRP_SAPLING_EXTENDED_SPENDING_KEY, - encoded_test - ) - .unwrap(), - Some(extsk) - ); - } - - #[test] - fn extended_full_viewing_key() { - let extfvk = (&ExtendedSpendingKey::master(&[0; 32][..])).into(); - - let encoded_main = "zxviews1qqqqqqqqqqqqqq8n3zjjmvhhr854uy3qhpda3ml34haf0x388z5r7h4st4kpsf6qy3zw4wc246aw9rlfyg5ndlwvne7mwdq0qe6vxl42pqmcf8pvmmd5slmjxduqa9evgej6wa3th2505xq4nggrxdm93rxk4rpdjt5nmq2vn44e2uhm7h0hsagfvkk4n7n6nfer6u57v9cac84t7nl2zth0xpyfeg0w2p2wv2yn6jn923aaz0vdaml07l60ahapk6efchyxwysrvjsxmansf"; - let encoded_test = "zxviewtestsapling1qqqqqqqqqqqqqq8n3zjjmvhhr854uy3qhpda3ml34haf0x388z5r7h4st4kpsf6qy3zw4wc246aw9rlfyg5ndlwvne7mwdq0qe6vxl42pqmcf8pvmmd5slmjxduqa9evgej6wa3th2505xq4nggrxdm93rxk4rpdjt5nmq2vn44e2uhm7h0hsagfvkk4n7n6nfer6u57v9cac84t7nl2zth0xpyfeg0w2p2wv2yn6jn923aaz0vdaml07l60ahapk6efchyxwysrvjs8evfkz"; - - assert_eq!( - encode_extended_full_viewing_key( - constants::mainnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, - &extfvk - ), - encoded_main - ); - assert_eq!( - decode_extended_full_viewing_key( - constants::mainnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, - encoded_main - ) - .unwrap(), - Some(extfvk.clone()) - ); - - assert_eq!( - encode_extended_full_viewing_key( - constants::testnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, - &extfvk - ), - encoded_test - ); - assert_eq!( - decode_extended_full_viewing_key( - constants::testnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, - encoded_test - ) - .unwrap(), - Some(extfvk) - ); - } - - #[test] - fn payment_address() { - let rng = &mut XorShiftRng::from_seed([ - 0x59, 0x62, 0xbe, 0x3d, 0x76, 0x3d, 0x31, 0x8d, 0x17, 0xdb, 0x37, 0x32, 0x54, 0x06, - 0xbc, 0xe5, - ]); - - let addr = - PaymentAddress::from_parts(Diversifier([0u8; 11]), jubjub::SubgroupPoint::random(rng)) - .unwrap(); - - let encoded_main = - "zs1qqqqqqqqqqqqqqqqqqcguyvaw2vjk4sdyeg0lc970u659lvhqq7t0np6hlup5lusxle75c8v35z"; - let encoded_test = - "ztestsapling1qqqqqqqqqqqqqqqqqqcguyvaw2vjk4sdyeg0lc970u659lvhqq7t0np6hlup5lusxle75ss7jnk"; - - assert_eq!( - encode_payment_address(constants::mainnet::HRP_SAPLING_PAYMENT_ADDRESS, &addr), - encoded_main - ); - assert_eq!( - decode_payment_address( - constants::mainnet::HRP_SAPLING_PAYMENT_ADDRESS, - encoded_main - ) - .unwrap(), - Some(addr.clone()) - ); - - assert_eq!( - encode_payment_address(constants::testnet::HRP_SAPLING_PAYMENT_ADDRESS, &addr), - encoded_test - ); - assert_eq!( - decode_payment_address( - constants::testnet::HRP_SAPLING_PAYMENT_ADDRESS, - encoded_test - ) - .unwrap(), - Some(addr) - ); - } - - #[test] - fn invalid_diversifier() { - let rng = &mut XorShiftRng::from_seed([ - 0x59, 0x62, 0xbe, 0x3d, 0x76, 0x3d, 0x31, 0x8d, 0x17, 0xdb, 0x37, 0x32, 0x54, 0x06, - 0xbc, 0xe5, - ]); - - let addr = - PaymentAddress::from_parts(Diversifier([1u8; 11]), jubjub::SubgroupPoint::random(rng)) - .unwrap(); - - let encoded_main = - encode_payment_address(constants::mainnet::HRP_SAPLING_PAYMENT_ADDRESS, &addr); - - assert_eq!( - decode_payment_address( - constants::mainnet::HRP_SAPLING_PAYMENT_ADDRESS, - &encoded_main - ) - .unwrap(), - None - ); - } -} diff --git a/zcash_client_backend/src/fees.rs b/zcash_client_backend/src/fees.rs new file mode 100644 index 0000000000..4591ef740e --- /dev/null +++ b/zcash_client_backend/src/fees.rs @@ -0,0 +1,592 @@ +use std::{ + fmt::{self, Debug, Display}, + num::{NonZeroU64, NonZeroUsize}, +}; + +use ::transparent::bundle::OutPoint; +use zcash_primitives::transaction::fees::{ + transparent::{self, InputSize}, + zip317::{self as prim_zip317}, + FeeRule, +}; +use zcash_protocol::{ + consensus::{self, BlockHeight}, + memo::MemoBytes, + value::Zatoshis, + PoolType, ShieldedProtocol, +}; + +use crate::data_api::InputSource; + +pub mod common; +#[cfg(feature = "non-standard-fees")] +pub mod fixed; +#[cfg(feature = "orchard")] +pub mod orchard; +pub mod sapling; +pub mod standard; +pub mod zip317; + +/// An enumeration of the standard fee rules supported by the wallet backend. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum StandardFeeRule { + Zip317, +} + +impl FeeRule for StandardFeeRule { + type Error = prim_zip317::FeeError; + + fn fee_required( + &self, + params: &P, + target_height: BlockHeight, + transparent_input_sizes: impl IntoIterator, + transparent_output_sizes: impl IntoIterator, + sapling_input_count: usize, + sapling_output_count: usize, + orchard_action_count: usize, + ) -> Result { + #[allow(deprecated)] + match self { + Self::Zip317 => prim_zip317::FeeRule::standard().fee_required( + params, + target_height, + transparent_input_sizes, + transparent_output_sizes, + sapling_input_count, + sapling_output_count, + orchard_action_count, + ), + } + } +} + +/// `ChangeValue` represents either a proposed change output to a shielded pool +/// (with an optional change memo), or if the "transparent-inputs" feature is +/// enabled, an ephemeral output to the transparent pool. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ChangeValue(ChangeValueInner); + +#[derive(Clone, Debug, PartialEq, Eq)] +enum ChangeValueInner { + Shielded { + protocol: ShieldedProtocol, + value: Zatoshis, + memo: Option, + }, + #[cfg(feature = "transparent-inputs")] + EphemeralTransparent { value: Zatoshis }, +} + +impl ChangeValue { + /// Constructs a new ephemeral transparent output value. + #[cfg(feature = "transparent-inputs")] + pub fn ephemeral_transparent(value: Zatoshis) -> Self { + Self(ChangeValueInner::EphemeralTransparent { value }) + } + + /// Constructs a new change value that will be created as a shielded output. + pub fn shielded(protocol: ShieldedProtocol, value: Zatoshis, memo: Option) -> Self { + Self(ChangeValueInner::Shielded { + protocol, + value, + memo, + }) + } + + /// Constructs a new change value that will be created as a Sapling output. + pub fn sapling(value: Zatoshis, memo: Option) -> Self { + Self::shielded(ShieldedProtocol::Sapling, value, memo) + } + + /// Constructs a new change value that will be created as an Orchard output. + #[cfg(feature = "orchard")] + pub fn orchard(value: Zatoshis, memo: Option) -> Self { + Self::shielded(ShieldedProtocol::Orchard, value, memo) + } + + /// Returns the pool to which the change or ephemeral output should be sent. + pub fn output_pool(&self) -> PoolType { + match &self.0 { + ChangeValueInner::Shielded { protocol, .. } => PoolType::Shielded(*protocol), + #[cfg(feature = "transparent-inputs")] + ChangeValueInner::EphemeralTransparent { .. } => PoolType::Transparent, + } + } + + /// Returns the value of the change or ephemeral output to be created, in zatoshis. + pub fn value(&self) -> Zatoshis { + match &self.0 { + ChangeValueInner::Shielded { value, .. } => *value, + #[cfg(feature = "transparent-inputs")] + ChangeValueInner::EphemeralTransparent { value } => *value, + } + } + + /// Returns the memo to be associated with the output. + pub fn memo(&self) -> Option<&MemoBytes> { + match &self.0 { + ChangeValueInner::Shielded { memo, .. } => memo.as_ref(), + #[cfg(feature = "transparent-inputs")] + ChangeValueInner::EphemeralTransparent { .. } => None, + } + } + + /// Whether this is to be an ephemeral output. + #[cfg_attr( + not(feature = "transparent-inputs"), + doc = "This is always false because the `transparent-inputs` feature is + not enabled." + )] + pub fn is_ephemeral(&self) -> bool { + match &self.0 { + ChangeValueInner::Shielded { .. } => false, + #[cfg(feature = "transparent-inputs")] + ChangeValueInner::EphemeralTransparent { .. } => true, + } + } +} + +/// The amount of change and fees required to make a transaction's inputs and +/// outputs balance under a specific fee rule, as computed by a particular +/// [`ChangeStrategy`] that is aware of that rule. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TransactionBalance { + proposed_change: Vec, + fee_required: Zatoshis, + + // A cache for the sum of proposed change and fee; we compute it on construction anyway, so we + // cache the resulting value. + total: Zatoshis, +} + +impl TransactionBalance { + /// Constructs a new balance from its constituent parts. + pub fn new(proposed_change: Vec, fee_required: Zatoshis) -> Result { + let total = proposed_change + .iter() + .map(|c| c.value()) + .chain(Some(fee_required).into_iter()) + .sum::>() + .ok_or(())?; + + Ok(Self { + proposed_change, + fee_required, + total, + }) + } + + /// The change values proposed by the [`ChangeStrategy`] that computed this balance. + pub fn proposed_change(&self) -> &[ChangeValue] { + &self.proposed_change + } + + /// Returns the fee computed for the transaction, assuming that the suggested + /// change outputs are added to the transaction. + pub fn fee_required(&self) -> Zatoshis { + self.fee_required + } + + /// Returns the sum of the proposed change outputs and the required fee. + pub fn total(&self) -> Zatoshis { + self.total + } +} + +/// Errors that can occur in computing suggested change and/or fees. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ChangeError { + /// Insufficient inputs were provided to change selection to fund the + /// required outputs and fees. + InsufficientFunds { + /// The total of the inputs provided to change selection + available: Zatoshis, + /// The total amount of input value required to fund the requested outputs, + /// including the required fees. + required: Zatoshis, + }, + /// Some of the inputs provided to the transaction have value less than the + /// marginal fee, and could not be determined to have any economic value in + /// the context of this input selection. + /// + /// This determination is potentially conservative in the sense that inputs + /// with value less than or equal to the marginal fee might be excluded, even + /// though in practice they would not cause the fee to increase. Inputs with + /// value greater than the marginal fee will never be excluded. + /// + /// The ordering of the inputs in each list is unspecified. + DustInputs { + /// The outpoints for transparent inputs that could not be determined to + /// have economic value in the context of this input selection. + transparent: Vec, + /// The identifiers for Sapling inputs that could not be determined to + /// have economic value in the context of this input selection. + sapling: Vec, + /// The identifiers for Orchard inputs that could not be determined to + /// have economic value in the context of this input selection. + #[cfg(feature = "orchard")] + orchard: Vec, + }, + /// An error occurred that was specific to the change selection strategy in use. + StrategyError(E), + /// The proposed bundle structure would violate bundle type construction rules. + BundleError(&'static str), +} + +impl fmt::Display for ChangeError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self { + ChangeError::InsufficientFunds { + available, + required, + } => write!( + f, + "Insufficient funds: required {} zatoshis, but only {} zatoshis were available.", + u64::from(*required), + u64::from(*available) + ), + ChangeError::DustInputs { + transparent, + sapling, + #[cfg(feature = "orchard")] + orchard, + } => { + #[cfg(feature = "orchard")] + let orchard_len = orchard.len(); + #[cfg(not(feature = "orchard"))] + let orchard_len = 0; + + // we can't encode the UA to its string representation because we + // don't have network parameters here + write!( + f, + "Insufficient funds: {} dust inputs were present, but would cost more to spend than they are worth.", + transparent.len() + sapling.len() + orchard_len, + ) + } + ChangeError::StrategyError(err) => { + write!(f, "{}", err) + } + ChangeError::BundleError(err) => { + write!( + f, + "The proposed transaction structure violates bundle type constraints: {}", + err + ) + } + } + } +} + +impl std::error::Error for ChangeError +where + E: Debug + Display + std::error::Error + 'static, + N: Debug + Display + 'static, +{ + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match &self { + ChangeError::StrategyError(e) => Some(e), + _ => None, + } + } +} + +/// An enumeration of actions to take when a transaction would potentially create dust +/// outputs (outputs that are likely to be without economic value due to fee rules). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum DustAction { + /// Do not allow creation of dust outputs; instead, require that additional inputs be provided. + Reject, + /// Explicitly allow the creation of dust change amounts greater than the specified value. + AllowDustChange, + /// Allow dust amounts to be added to the transaction fee. + AddDustToFee, +} + +/// A policy describing how a [`ChangeStrategy`] should treat potentially dust-valued change +/// outputs (outputs that are likely to be without economic value due to fee rules). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct DustOutputPolicy { + action: DustAction, + dust_threshold: Option, +} + +impl DustOutputPolicy { + /// Constructs a new dust output policy. + /// + /// A dust policy created with `None` as the dust threshold will delegate determination + /// of the dust threshold to the change strategy that is evaluating the strategy; this + /// is recommended, but an explicit value (including zero) may be provided to explicitly + /// override the determination of the change strategy. + pub fn new(action: DustAction, dust_threshold: Option) -> Self { + Self { + action, + dust_threshold, + } + } + + /// Returns the action to take in the event that a dust change amount would be produced. + pub fn action(&self) -> DustAction { + self.action + } + /// Returns a value that will be used to override the dust determination logic of the + /// change policy, if any. + pub fn dust_threshold(&self) -> Option { + self.dust_threshold + } +} + +impl Default for DustOutputPolicy { + fn default() -> Self { + DustOutputPolicy::new(DustAction::Reject, None) + } +} + +/// A policy that describes how change output should be split into multiple notes for the purpose +/// of note management. +/// +/// If an account contains at least [`Self::target_output_count`] notes having at least value +/// [`Self::min_split_output_value`], this policy will recommend a single output; if the account +/// contains fewer such notes, this policy will recommend that multiple outputs be produced in +/// order to achieve the target. +#[derive(Clone, Copy, Debug)] +pub struct SplitPolicy { + target_output_count: NonZeroUsize, + min_split_output_value: Option, +} + +impl SplitPolicy { + /// In the case that no other conditions provided by the user are available to fall back on, + /// a default value of [`MARGINAL_FEE`] * 100 will be used as the "minimum usable note value" + /// when retrieving wallet metadata. + /// + /// [`MARGINAL_FEE`]: zcash_primitives::transaction::fees::zip317::MARGINAL_FEE + pub(crate) const MIN_NOTE_VALUE: Zatoshis = Zatoshis::const_from_u64(500000); + + /// Constructs a new [`SplitPolicy`] that splits change to ensure the given number of spendable + /// outputs exists within an account, each having at least the specified minimum note value. + pub fn with_min_output_value( + target_output_count: NonZeroUsize, + min_split_output_value: Zatoshis, + ) -> Self { + Self { + target_output_count, + min_split_output_value: Some(min_split_output_value), + } + } + + /// Constructs a [`SplitPolicy`] that prescribes a single output (no splitting). + pub fn single_output() -> Self { + Self { + target_output_count: NonZeroUsize::MIN, + min_split_output_value: None, + } + } + + /// Returns the number of outputs that this policy will attempt to ensure that the wallet has + /// available for spending. + pub fn target_output_count(&self) -> NonZeroUsize { + self.target_output_count + } + + /// Returns the minimum value for a note resulting from splitting of change. + pub fn min_split_output_value(&self) -> Option { + self.min_split_output_value + } + + /// Returns the number of output notes to produce from the given total change value, given the + /// total value and number of existing unspent notes in the account and this policy. + /// + /// If splitting change to produce [`Self::target_output_count`] would result in notes of value + /// less than [`Self::min_split_output_value`], then this will suggest a smaller number of + /// splits so that each resulting change note has sufficient value. + pub fn split_count( + &self, + existing_notes: Option, + existing_notes_total: Option, + total_change: Zatoshis, + ) -> NonZeroUsize { + fn to_nonzero_u64(value: usize) -> NonZeroU64 { + NonZeroU64::new(u64::try_from(value).expect("usize fits into u64")) + .expect("NonZeroU64 input derived from NonZeroUsize") + } + + let mut split_count = NonZeroUsize::new( + usize::from(self.target_output_count) + .saturating_sub(existing_notes.unwrap_or(usize::MAX)), + ) + .unwrap_or(NonZeroUsize::MIN); + + let min_split_output_value = self.min_split_output_value.or_else(|| { + // If no minimum split output size is set, we choose the minimum split size to be a + // quarter of the average value of notes in the wallet after the transaction. + (existing_notes_total + total_change).map(|total| { + *total + .div_with_remainder(to_nonzero_u64( + usize::from(self.target_output_count).saturating_mul(4), + )) + .quotient() + }) + }); + + if let Some(min_split_output_value) = min_split_output_value { + loop { + let per_output_change = + total_change.div_with_remainder(to_nonzero_u64(usize::from(split_count))); + if *per_output_change.quotient() >= min_split_output_value { + return split_count; + } else if let Some(new_count) = NonZeroUsize::new(usize::from(split_count) - 1) { + split_count = new_count; + } else { + // We always create at least one change output. + return NonZeroUsize::MIN; + } + } + } else { + NonZeroUsize::MIN + } + } +} + +/// `EphemeralBalance` describes the ephemeral input or output value for a transaction. It is used +/// in fee computation for series of transactions that use an ephemeral transparent output in an +/// intermediate step, such as when sending from a shielded pool to a [ZIP 320] "TEX" address. +/// +/// [ZIP 320]: https://zips.z.cash/zip-0320 +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum EphemeralBalance { + Input(Zatoshis), + Output(Zatoshis), +} + +impl EphemeralBalance { + pub fn is_input(&self) -> bool { + matches!(self, EphemeralBalance::Input(_)) + } + + pub fn is_output(&self) -> bool { + matches!(self, EphemeralBalance::Output(_)) + } + + pub fn ephemeral_input_amount(&self) -> Option { + match self { + EphemeralBalance::Input(v) => Some(*v), + EphemeralBalance::Output(_) => None, + } + } + + pub fn ephemeral_output_amount(&self) -> Option { + match self { + EphemeralBalance::Input(_) => None, + EphemeralBalance::Output(v) => Some(*v), + } + } +} + +/// A trait that represents the ability to compute the suggested change and fees that must be paid +/// by a transaction having a specified set of inputs and outputs. +pub trait ChangeStrategy { + type FeeRule: FeeRule + Clone; + type Error; + + /// The type of metadata source that this change strategy requires in order to be able to + /// retrieve required wallet metadata. If more capabilities are required of the backend than + /// are exposed in the [`InputSource`] trait, the implementer of this trait should define their + /// own trait that descends from [`InputSource`] and adds the required capabilities there, and + /// then implement that trait for their desired database backend. + type MetaSource: InputSource; + + /// Tye type of wallet metadata that this change strategy relies upon in order to compute + /// change. + type AccountMetaT; + + /// Returns the fee rule that this change strategy will respect when performing + /// balance computations. + fn fee_rule(&self) -> &Self::FeeRule; + + /// Uses the provided metadata source to obtain the wallet metadata required for change + /// creation determinations. + fn fetch_wallet_meta( + &self, + meta_source: &Self::MetaSource, + account: ::AccountId, + exclude: &[::NoteRef], + ) -> Result::Error>; + + /// Computes the totals of inputs, suggested change amounts, and fees given the + /// provided inputs and outputs being used to construct a transaction. + /// + /// The fee computed as part of this operation should take into account the prospective + /// change outputs recommended by this operation. If insufficient funds are available to + /// supply the requested outputs and required fees, implementations should return + /// [`ChangeError::InsufficientFunds`]. + /// + /// If the inputs include notes or UTXOs that are not economic to spend in the context + /// of this input selection, a [`ChangeError::DustInputs`] error can be returned + /// indicating inputs that should be removed from the selection (all of which will + /// have value less than or equal to the marginal fee). The caller should order the + /// inputs from most to least preferred to spend within each pool, so that the most + /// preferred ones are less likely to be indicated to remove. + /// + /// - `ephemeral_balance`: if the transaction is to be constructed with either an + /// ephemeral transparent input or an ephemeral transparent output this argument + /// may be used to provide the value of that input or output. The value of this + /// argument should be `None` in the case that there are no such items. + /// - `wallet_meta`: Additional wallet metadata that the change strategy may use + /// in determining how to construct change outputs. This wallet metadata value + /// should be computed excluding the inputs provided in the `transparent_inputs`, + /// `sapling`, and `orchard` arguments. + /// + /// [ZIP 320]: https://zips.z.cash/zip-0320 + #[allow(clippy::too_many_arguments)] + fn compute_balance( + &self, + params: &P, + target_height: BlockHeight, + transparent_inputs: &[impl transparent::InputView], + transparent_outputs: &[impl transparent::OutputView], + sapling: &impl sapling::BundleView, + #[cfg(feature = "orchard")] orchard: &impl orchard::BundleView, + ephemeral_balance: Option<&EphemeralBalance>, + wallet_meta: &Self::AccountMetaT, + ) -> Result>; +} + +#[cfg(test)] +pub(crate) mod tests { + use ::transparent::bundle::{OutPoint, TxOut}; + use zcash_primitives::transaction::fees::transparent; + use zcash_protocol::value::Zatoshis; + + use super::sapling; + + #[derive(Debug)] + pub(crate) struct TestTransparentInput { + pub outpoint: OutPoint, + pub coin: TxOut, + } + + impl transparent::InputView for TestTransparentInput { + fn outpoint(&self) -> &OutPoint { + &self.outpoint + } + fn coin(&self) -> &TxOut { + &self.coin + } + } + + pub(crate) struct TestSaplingInput { + pub note_id: u32, + pub value: Zatoshis, + } + + impl sapling::InputView for TestSaplingInput { + fn note_id(&self) -> &u32 { + &self.note_id + } + fn value(&self) -> Zatoshis { + self.value + } + } +} diff --git a/zcash_client_backend/src/fees/common.rs b/zcash_client_backend/src/fees/common.rs new file mode 100644 index 0000000000..132622f059 --- /dev/null +++ b/zcash_client_backend/src/fees/common.rs @@ -0,0 +1,783 @@ +use core::cmp::{max, min}; +use std::num::{NonZeroU64, NonZeroUsize}; + +use zcash_primitives::transaction::fees::{ + transparent, zip317::MINIMUM_FEE, zip317::P2PKH_STANDARD_OUTPUT_SIZE, FeeRule, +}; +use zcash_protocol::{ + consensus::{self, BlockHeight}, + memo::MemoBytes, + value::{BalanceError, Zatoshis}, + ShieldedProtocol, +}; + +use crate::data_api::AccountMeta; + +use super::{ + sapling as sapling_fees, ChangeError, ChangeValue, DustAction, DustOutputPolicy, + EphemeralBalance, SplitPolicy, TransactionBalance, +}; + +#[cfg(feature = "orchard")] +use super::orchard as orchard_fees; + +pub(crate) struct NetFlows { + t_in: Zatoshis, + t_out: Zatoshis, + sapling_in: Zatoshis, + sapling_out: Zatoshis, + orchard_in: Zatoshis, + orchard_out: Zatoshis, +} + +impl NetFlows { + fn total_in(&self) -> Result { + (self.t_in + self.sapling_in + self.orchard_in).ok_or(BalanceError::Overflow) + } + fn total_out(&self) -> Result { + (self.t_out + self.sapling_out + self.orchard_out).ok_or(BalanceError::Overflow) + } + /// Returns true iff the flows excluding change are fully transparent. + fn is_transparent(&self) -> bool { + !(self.sapling_in.is_positive() + || self.sapling_out.is_positive() + || self.orchard_in.is_positive() + || self.orchard_out.is_positive()) + } +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn calculate_net_flows( + transparent_inputs: &[impl transparent::InputView], + transparent_outputs: &[impl transparent::OutputView], + sapling: &impl sapling_fees::BundleView, + #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView, + ephemeral_balance: Option<&EphemeralBalance>, +) -> Result> +where + E: From + From, +{ + let overflow = || ChangeError::StrategyError(E::from(BalanceError::Overflow)); + + let t_in = transparent_inputs + .iter() + .map(|t_in| t_in.coin().value) + .chain(ephemeral_balance.and_then(|b| b.ephemeral_input_amount())) + .sum::>() + .ok_or_else(overflow)?; + let t_out = transparent_outputs + .iter() + .map(|t_out| t_out.value()) + .chain(ephemeral_balance.and_then(|b| b.ephemeral_output_amount())) + .sum::>() + .ok_or_else(overflow)?; + let sapling_in = sapling + .inputs() + .iter() + .map(sapling_fees::InputView::::value) + .sum::>() + .ok_or_else(overflow)?; + let sapling_out = sapling + .outputs() + .iter() + .map(sapling_fees::OutputView::value) + .sum::>() + .ok_or_else(overflow)?; + + #[cfg(feature = "orchard")] + let orchard_in = orchard + .inputs() + .iter() + .map(orchard_fees::InputView::::value) + .sum::>() + .ok_or_else(overflow)?; + #[cfg(not(feature = "orchard"))] + let orchard_in = Zatoshis::ZERO; + + #[cfg(feature = "orchard")] + let orchard_out = orchard + .outputs() + .iter() + .map(orchard_fees::OutputView::value) + .sum::>() + .ok_or_else(overflow)?; + #[cfg(not(feature = "orchard"))] + let orchard_out = Zatoshis::ZERO; + + Ok(NetFlows { + t_in, + t_out, + sapling_in, + sapling_out, + orchard_in, + orchard_out, + }) +} + +/// Decide which shielded pool change should go to if there is any. +pub(crate) fn select_change_pool( + _net_flows: &NetFlows, + _fallback_change_pool: ShieldedProtocol, +) -> ShieldedProtocol { + // TODO: implement a less naive strategy for selecting the pool to which change will be sent. + #[cfg(feature = "orchard")] + if _net_flows.orchard_in.is_positive() || _net_flows.orchard_out.is_positive() { + // Send change to Orchard if we're spending any Orchard inputs or creating any Orchard outputs. + ShieldedProtocol::Orchard + } else if _net_flows.sapling_in.is_positive() || _net_flows.sapling_out.is_positive() { + // Otherwise, send change to Sapling if we're spending any Sapling inputs or creating any + // Sapling outputs, so that we avoid pool-crossing. + ShieldedProtocol::Sapling + } else { + // The flows are transparent, so there may not be change. If there is, the caller + // gets to decide where to shield it. + _fallback_change_pool + } + #[cfg(not(feature = "orchard"))] + ShieldedProtocol::Sapling +} + +#[derive(Clone, Copy, Debug)] +pub(crate) struct OutputManifest { + transparent: usize, + sapling: usize, + orchard: usize, +} + +impl OutputManifest { + const ZERO: OutputManifest = OutputManifest { + transparent: 0, + sapling: 0, + orchard: 0, + }; + + pub(crate) fn sapling(&self) -> usize { + self.sapling + } + + pub(crate) fn orchard(&self) -> usize { + self.orchard + } + + pub(crate) fn total_shielded(&self) -> usize { + self.sapling + self.orchard + } +} + +pub(crate) struct SinglePoolBalanceConfig<'a, P, F> { + params: &'a P, + fee_rule: &'a F, + dust_output_policy: &'a DustOutputPolicy, + default_dust_threshold: Zatoshis, + split_policy: &'a SplitPolicy, + fallback_change_pool: ShieldedProtocol, + marginal_fee: Zatoshis, + grace_actions: usize, +} + +impl<'a, P, F> SinglePoolBalanceConfig<'a, P, F> { + #[allow(clippy::too_many_arguments)] + pub(crate) fn new( + params: &'a P, + fee_rule: &'a F, + dust_output_policy: &'a DustOutputPolicy, + default_dust_threshold: Zatoshis, + split_policy: &'a SplitPolicy, + fallback_change_pool: ShieldedProtocol, + marginal_fee: Zatoshis, + grace_actions: usize, + ) -> Self { + Self { + params, + fee_rule, + dust_output_policy, + default_dust_threshold, + split_policy, + fallback_change_pool, + marginal_fee, + grace_actions, + } + } +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn single_pool_output_balance( + cfg: SinglePoolBalanceConfig, + wallet_meta: Option<&AccountMeta>, + target_height: BlockHeight, + transparent_inputs: &[impl transparent::InputView], + transparent_outputs: &[impl transparent::OutputView], + sapling: &impl sapling_fees::BundleView, + #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView, + change_memo: Option<&MemoBytes>, + ephemeral_balance: Option<&EphemeralBalance>, +) -> Result> +where + E: From + From, +{ + // The change memo, if any, must be attached to the change in the intermediate step that + // produces the ephemeral output, and so it should be discarded in the ultimate step; this is + // distinguished by identifying that this transaction has ephemeral inputs. + let change_memo = change_memo.filter(|_| ephemeral_balance.map_or(true, |b| !b.is_input())); + + let overflow = || ChangeError::StrategyError(E::from(BalanceError::Overflow)); + let underflow = || ChangeError::StrategyError(E::from(BalanceError::Underflow)); + + let net_flows = calculate_net_flows::( + transparent_inputs, + transparent_outputs, + sapling, + #[cfg(feature = "orchard")] + orchard, + ephemeral_balance, + )?; + + let change_pool = select_change_pool(&net_flows, cfg.fallback_change_pool); + let target_change_count = wallet_meta.map_or(1, |m| { + usize::from(cfg.split_policy.target_output_count) + // If we cannot determine a total note count, fall back to a single output + .saturating_sub(m.total_note_count().unwrap_or(usize::MAX)) + .max(1) + }); + let target_change_counts = OutputManifest { + transparent: 0, + sapling: if change_pool == ShieldedProtocol::Sapling { + target_change_count + } else { + 0 + }, + orchard: if change_pool == ShieldedProtocol::Orchard { + target_change_count + } else { + 0 + }, + }; + + // We don't create a fully-transparent transaction if a change memo is used. + let transparent = net_flows.is_transparent() && change_memo.is_none(); + + // If we have a non-zero marginal fee, we need to check for uneconomic inputs. + // This is basically assuming that fee rules with non-zero marginal fee are + // "ZIP 317-like", but we can generalize later if needed. + if cfg.marginal_fee.is_positive() { + // Is it certain that there will be a change output? If it is not certain, + // we should call `check_for_uneconomic_inputs` with `possible_change` + // including both possibilities. + let possible_change = { + // These are the situations where we might not have a change output. + if transparent + || (cfg.dust_output_policy.action() == DustAction::AddDustToFee + && change_memo.is_none()) + { + vec![OutputManifest::ZERO, target_change_counts] + } else { + vec![target_change_counts] + } + }; + + check_for_uneconomic_inputs( + transparent_inputs, + transparent_outputs, + sapling, + #[cfg(feature = "orchard")] + orchard, + cfg.marginal_fee, + cfg.grace_actions, + &possible_change[..], + ephemeral_balance, + )?; + } + + let total_in = net_flows + .total_in() + .map_err(|e| ChangeError::StrategyError(E::from(e)))?; + let total_out = net_flows + .total_out() + .map_err(|e| ChangeError::StrategyError(E::from(e)))?; + + let sapling_input_count = sapling + .bundle_type() + .num_spends(sapling.inputs().len()) + .map_err(ChangeError::BundleError)?; + let sapling_output_count = |change_count| { + sapling + .bundle_type() + .num_outputs( + sapling.inputs().len(), + sapling.outputs().len() + change_count, + ) + .map_err(ChangeError::BundleError) + }; + + #[cfg(feature = "orchard")] + let orchard_action_count = |change_count| { + orchard + .bundle_type() + .num_actions( + orchard.inputs().len(), + orchard.outputs().len() + change_count, + ) + .map_err(ChangeError::BundleError) + }; + #[cfg(not(feature = "orchard"))] + let orchard_action_count = |change_count: usize| -> Result> { + if change_count != 0 { + Err(ChangeError::BundleError( + "Nonzero Orchard change requested but the `orchard` feature is not enabled.", + )) + } else { + Ok(0) + } + }; + + // Once we calculate the balance with and without change, there are five cases: + // + // 1. Insufficient funds even without change. + // 2. The fee amount without change exactly cancels out the net flow balance. + // 3. The fee amount without change is smaller than the change. + // 3a. Insufficient funds once the change output is added. + // 3b. The fee amount with change exactly cancels out the net flow balance. + // 3c. The fee amount with change leaves a non-zero change value. + // + // Case 2 happens for the second transaction of a ZIP 320 pair. In that case + // the transaction will be fully transparent, and there must be no change. + // + // If cases 2 or 3b happen for a transaction with any shielded flows, we + // want there to be a zero-value shielded change output anyway (i.e. treat + // case 2 as case 3, and case 3b as case 3c), because: + // * being able to distinguish these cases potentially leaks too much + // information (an adversary that knows the number of external recipients + // and the sum of their outputs learns the sum of the inputs if no change + // output is present); and + // * we will then always have an shielded output in which to put change_memo, + // if one is used. + // + // Note that using the `DustAction::AddDustToFee` policy inherently leaks + // more information. + + let transparent_input_sizes = transparent_inputs + .iter() + .map(|i| i.serialized_size()) + .chain( + ephemeral_balance + .and_then(|b| b.ephemeral_input_amount()) + .map(|_| transparent::InputSize::STANDARD_P2PKH), + ); + let transparent_output_sizes = transparent_outputs + .iter() + .map(|i| i.serialized_size()) + .chain( + ephemeral_balance + .and_then(|b| b.ephemeral_output_amount()) + .map(|_| P2PKH_STANDARD_OUTPUT_SIZE), + ); + + let fee_without_change = cfg + .fee_rule + .fee_required( + cfg.params, + target_height, + transparent_input_sizes.clone(), + transparent_output_sizes.clone(), + sapling_input_count, + sapling_output_count(0)?, + orchard_action_count(0)?, + ) + .map_err(|fee_error| ChangeError::StrategyError(E::from(fee_error)))?; + + let fee_with_change = max( + fee_without_change, + cfg.fee_rule + .fee_required( + cfg.params, + target_height, + transparent_input_sizes.clone(), + transparent_output_sizes.clone(), + sapling_input_count, + sapling_output_count(target_change_counts.sapling())?, + orchard_action_count(target_change_counts.orchard())?, + ) + .map_err(|fee_error| ChangeError::StrategyError(E::from(fee_error)))?, + ); + + let total_out_plus_fee_without_change = + (total_out + fee_without_change).ok_or_else(overflow)?; + let total_out_plus_fee_with_change = (total_out + fee_with_change).ok_or_else(overflow)?; + + #[allow(unused_mut)] + let (mut change, fee) = { + if transparent && total_in < total_out_plus_fee_without_change { + // Case 1 for a tx with all transparent flows. + return Err(ChangeError::InsufficientFunds { + available: total_in, + required: total_out_plus_fee_without_change, + }); + } else if transparent && total_in == total_out_plus_fee_without_change { + // Case 2 for a tx with all transparent flows. + (vec![], fee_without_change) + } else if total_in < total_out_plus_fee_with_change { + // Case 3a, or case 1 or 2 with non-transparent flows. + return Err(ChangeError::InsufficientFunds { + available: total_in, + required: total_out_plus_fee_with_change, + }); + } else { + // Case 3b or 3c. + let proposed_change = + (total_in - total_out_plus_fee_with_change).expect("checked above"); + + // We obtain a split count based on the total number of notes of sufficient size + // available in the wallet, irrespective of pool. If we don't have any wallet metadata + // available, we fall back to generating a single change output. + let split_count = wallet_meta.map_or(NonZeroUsize::MIN, |wm| { + cfg.split_policy.split_count( + wm.total_note_count(), + wm.total_value(), + proposed_change, + ) + }); + let per_output_change = proposed_change.div_with_remainder( + NonZeroU64::new( + u64::try_from(usize::from(split_count)).expect("usize fits into u64"), + ) + .unwrap(), + ); + + // If we don't have as many change outputs as we expected, recompute the fee. + let (fee_with_change, excess_fee) = + if usize::from(split_count) < target_change_counts.total_shielded() { + let new_fee_with_change = cfg + .fee_rule + .fee_required( + cfg.params, + target_height, + transparent_input_sizes, + transparent_output_sizes, + sapling_input_count, + sapling_output_count(if change_pool == ShieldedProtocol::Sapling { + usize::from(split_count) + } else { + 0 + })?, + orchard_action_count(if change_pool == ShieldedProtocol::Orchard { + usize::from(split_count) + } else { + 0 + })?, + ) + .map_err(|fee_error| ChangeError::StrategyError(E::from(fee_error)))?; + ( + new_fee_with_change, + (fee_with_change - new_fee_with_change).unwrap_or(Zatoshis::ZERO), + ) + } else { + (fee_with_change, Zatoshis::ZERO) + }; + + let simple_case = || { + ( + (0usize..split_count.into()) + .map(|i| { + ChangeValue::shielded( + change_pool, + if i == 0 { + // Add any remainder to the first output only + (*per_output_change.quotient() + + *per_output_change.remainder() + + excess_fee) + .unwrap() + } else { + // For any other output, the change value will just be the + // quotient. + *per_output_change.quotient() + }, + change_memo.cloned(), + ) + }) + .collect(), + fee_with_change, + ) + }; + + let change_dust_threshold = cfg + .dust_output_policy + .dust_threshold() + .unwrap_or(cfg.default_dust_threshold); + + if proposed_change < change_dust_threshold { + match cfg.dust_output_policy.action() { + DustAction::Reject => { + // Always allow zero-valued change even for the `Reject` policy: + // * it should be allowed in order to record change memos and to improve + // indistinguishability; + // * this case occurs in practice when sending all funds from an account; + // * zero-valued notes do not require witness tracking; + // * the effect on trial decryption overhead is small. + if proposed_change.is_zero() && excess_fee.is_zero() { + simple_case() + } else { + let shortfall = + (change_dust_threshold - proposed_change).ok_or_else(underflow)?; + + return Err(ChangeError::InsufficientFunds { + available: total_in, + required: (total_in + shortfall).ok_or_else(overflow)?, + }); + } + } + DustAction::AllowDustChange => simple_case(), + DustAction::AddDustToFee => { + // Zero-valued change is also always allowed for this policy, but when + // no change memo is given, we might omit the change output instead. + + let fee_with_dust = (total_in - total_out) + .expect("we already checked for sufficient funds"); + // We can add a change output if necessary. + assert!(fee_with_change <= fee_with_dust); + + let reasonable_fee = (fee_with_change + (MINIMUM_FEE * 10u64).unwrap()) + .ok_or_else(overflow)?; + + if fee_with_dust > reasonable_fee { + // Defend against losing money by using AddDustToFee with a too-high + // dust threshold. + simple_case() + } else if change_memo.is_some() { + ( + vec![ChangeValue::shielded( + change_pool, + Zatoshis::ZERO, + change_memo.cloned(), + )], + fee_with_dust, + ) + } else { + (vec![], fee_with_dust) + } + } + } + } else { + simple_case() + } + } + }; + #[cfg(feature = "transparent-inputs")] + change.extend( + ephemeral_balance + .and_then(|b| b.ephemeral_output_amount()) + .map(ChangeValue::ephemeral_transparent), + ); + + TransactionBalance::new(change, fee).map_err(|_| overflow()) +} + +/// Returns a `[ChangeStrategy::DustInputs]` error if some of the inputs provided +/// to the transaction have value less than the marginal fee, and could not be +/// determined to have any economic value in the context of this input selection. +/// +/// This determination is potentially conservative in the sense that outputs +/// with value less than the marginal fee might be excluded, even though in +/// practice they would not cause the fee to increase. Outputs with value +/// greater than the marginal fee will never be excluded. +/// +/// `possible_change` is a slice of `(transparent_change, sapling_change, orchard_change)` +/// tuples indicating possible combinations of how many change outputs (0 or 1) +/// might be included in the transaction for each pool. The shape of the tuple +/// does not depend on which protocol features are enabled. +#[allow(clippy::too_many_arguments)] +pub(crate) fn check_for_uneconomic_inputs( + transparent_inputs: &[impl transparent::InputView], + transparent_outputs: &[impl transparent::OutputView], + sapling: &impl sapling_fees::BundleView, + #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView, + marginal_fee: Zatoshis, + grace_actions: usize, + possible_change: &[OutputManifest], + ephemeral_balance: Option<&EphemeralBalance>, +) -> Result<(), ChangeError> { + let mut t_dust: Vec<_> = transparent_inputs + .iter() + .filter_map(|i| { + // For now, we're just assuming P2PKH inputs, so we don't check the + // size of the input script. + if i.coin().value <= marginal_fee { + Some(i.outpoint().clone()) + } else { + None + } + }) + .collect(); + + let mut s_dust: Vec<_> = sapling + .inputs() + .iter() + .filter_map(|i| { + if sapling_fees::InputView::::value(i) <= marginal_fee { + Some(sapling_fees::InputView::::note_id(i).clone()) + } else { + None + } + }) + .collect(); + + #[cfg(feature = "orchard")] + let mut o_dust: Vec = orchard + .inputs() + .iter() + .filter_map(|i| { + if orchard_fees::InputView::::value(i) <= marginal_fee { + Some(orchard_fees::InputView::::note_id(i).clone()) + } else { + None + } + }) + .collect(); + #[cfg(not(feature = "orchard"))] + let mut o_dust: Vec = vec![]; + + // If we don't have any dust inputs, there is nothing to check. + if t_dust.is_empty() && s_dust.is_empty() && o_dust.is_empty() { + return Ok(()); + } + + let (t_inputs_len, t_outputs_len) = ( + transparent_inputs.len() + usize::from(ephemeral_balance.is_some_and(|b| b.is_input())), + transparent_outputs.len() + usize::from(ephemeral_balance.is_some_and(|b| b.is_output())), + ); + let (s_inputs_len, s_outputs_len) = (sapling.inputs().len(), sapling.outputs().len()); + #[cfg(feature = "orchard")] + let (o_inputs_len, o_outputs_len) = (orchard.inputs().len(), orchard.outputs().len()); + #[cfg(not(feature = "orchard"))] + let (o_inputs_len, o_outputs_len) = (0usize, 0usize); + + let t_non_dust = t_inputs_len.checked_sub(t_dust.len()).unwrap(); + let s_non_dust = s_inputs_len.checked_sub(s_dust.len()).unwrap(); + let o_non_dust = o_inputs_len.checked_sub(o_dust.len()).unwrap(); + + // Return the number of allowed dust inputs from each pool. + let allowed_dust = |change: &OutputManifest| { + // Here we assume a "ZIP 317-like" fee model in which the existence of an output + // to a given pool implies that a corresponding input from that pool can be + // provided without increasing the fee. (This is also likely to be true for + // future fee models if we do not want to penalize use of Orchard relative to + // other pools.) + // + // Under that assumption, we want to calculate the maximum number of dust inputs + // from each pool, out of the ones we actually have, that can be economically + // spent along with the non-dust inputs. Get an initial estimate by calculating + // the number of dust inputs in each pool that will be allowed regardless of + // padding or grace. + + let t_allowed = min( + t_dust.len(), + (t_outputs_len + change.transparent).saturating_sub(t_non_dust), + ); + let s_allowed = min( + s_dust.len(), + (s_outputs_len + change.sapling).saturating_sub(s_non_dust), + ); + let o_allowed = min( + o_dust.len(), + (o_outputs_len + change.orchard).saturating_sub(o_non_dust), + ); + + // We'll be spending the non-dust and allowed dust in each pool. + let t_req_inputs = t_non_dust + t_allowed; + let s_req_inputs = s_non_dust + s_allowed; + #[cfg(feature = "orchard")] + let o_req_inputs = o_non_dust + o_allowed; + + // This calculates the hypothetical number of actions with given extra inputs, + // for ZIP 317 and the padding rules in effect. The padding rules for each + // pool are subtle (they also depend on `bundle_required` for example), so we + // must actually call them rather than try to predict their effect. To tell + // whether we can freely add an extra input from a given pool, we need to call + // them both with and without that input; if the number of actions does not + // increase, then the input is free to add. + let hypothetical_actions = |t_extra, s_extra, _o_extra| { + let s_spend_count = sapling + .bundle_type() + .num_spends(s_req_inputs + s_extra) + .map_err(ChangeError::BundleError)?; + + let s_output_count = sapling + .bundle_type() + .num_outputs(s_req_inputs + s_extra, s_outputs_len + change.sapling) + .map_err(ChangeError::BundleError)?; + + #[cfg(feature = "orchard")] + let o_action_count = orchard + .bundle_type() + .num_actions(o_req_inputs + _o_extra, o_outputs_len + change.orchard) + .map_err(ChangeError::BundleError)?; + #[cfg(not(feature = "orchard"))] + let o_action_count = 0; + + // To calculate the number of unused actions, we assume that transparent inputs + // and outputs are P2PKH. + Ok( + max(t_req_inputs + t_extra, t_outputs_len + change.transparent) + + max(s_spend_count, s_output_count) + + o_action_count, + ) + }; + + // First calculate the baseline number of logical actions with only the definitely + // allowed inputs estimated above. If it is less than `grace_actions`, try to allocate + // a grace input first to transparent dust, then to Sapling dust, then to Orchard dust. + // If the number of actions increases, it was not possible to allocate that input for + // free. This approach is sufficient because at most one such input can be allocated, + // since `grace_actions` is at most 2 for ZIP 317 and there must be at least one + // logical action. (If `grace_actions` were greater than 2 then the code would still + // be correct, it would just not find all potential extra inputs.) + + let baseline = hypothetical_actions(0, 0, 0)?; + + let (t_extra, s_extra, o_extra) = if baseline >= grace_actions { + (0, 0, 0) + } else if t_dust.len() > t_allowed && hypothetical_actions(1, 0, 0)? <= baseline { + (1, 0, 0) + } else if s_dust.len() > s_allowed && hypothetical_actions(0, 1, 0)? <= baseline { + (0, 1, 0) + } else if o_dust.len() > o_allowed && hypothetical_actions(0, 0, 1)? <= baseline { + (0, 0, 1) + } else { + (0, 0, 0) + }; + Ok(OutputManifest { + transparent: t_allowed + t_extra, + sapling: s_allowed + s_extra, + orchard: o_allowed + o_extra, + }) + }; + + // Find the least number of allowed dust inputs for each pool for any `possible_change`. + let allowed = possible_change + .iter() + .map(allowed_dust) + .collect::, _>>()? + .into_iter() + .reduce(|l, r| OutputManifest { + transparent: min(l.transparent, r.transparent), + sapling: min(l.sapling, r.sapling), + orchard: min(l.orchard, r.orchard), + }) + .expect("possible_change is nonempty"); + + // The inputs in the tail of each list after the first `*_allowed` are returned as uneconomic. + // The caller should order the inputs from most to least preferred to spend. + let t_dust = t_dust.split_off(allowed.transparent); + let s_dust = s_dust.split_off(allowed.sapling); + let o_dust = o_dust.split_off(allowed.orchard); + + if t_dust.is_empty() && s_dust.is_empty() && o_dust.is_empty() { + Ok(()) + } else { + Err(ChangeError::DustInputs { + transparent: t_dust, + sapling: s_dust, + #[cfg(feature = "orchard")] + orchard: o_dust, + }) + } +} diff --git a/zcash_client_backend/src/fees/fixed.rs b/zcash_client_backend/src/fees/fixed.rs new file mode 100644 index 0000000000..cf8d489d52 --- /dev/null +++ b/zcash_client_backend/src/fees/fixed.rs @@ -0,0 +1,224 @@ +//! Change strategies designed for use with a fixed fee. + +use core::marker::PhantomData; + +use zcash_primitives::transaction::fees::{fixed::FeeRule as FixedFeeRule, transparent}; +use zcash_protocol::{ + consensus::{self, BlockHeight}, + memo::MemoBytes, + value::{BalanceError, Zatoshis}, + ShieldedProtocol, +}; + +use crate::data_api::InputSource; + +use super::{ + common::{single_pool_output_balance, SinglePoolBalanceConfig}, + sapling as sapling_fees, ChangeError, ChangeStrategy, DustOutputPolicy, EphemeralBalance, + SplitPolicy, TransactionBalance, +}; + +#[cfg(feature = "orchard")] +use super::orchard as orchard_fees; + +/// A change strategy that proposes change as a single output. The output pool is chosen +/// as the most current pool that avoids unnecessary pool-crossing (with a specified +/// fallback when the transaction has no shielded inputs). Fee calculation is delegated +/// to the provided fee rule. +pub struct SingleOutputChangeStrategy { + fee_rule: FixedFeeRule, + change_memo: Option, + fallback_change_pool: ShieldedProtocol, + dust_output_policy: DustOutputPolicy, + meta_source: PhantomData, +} + +impl SingleOutputChangeStrategy { + /// Constructs a new [`SingleOutputChangeStrategy`] with the specified fee rule + /// and change memo. + /// + /// `fallback_change_pool` is used when more than one shielded pool is enabled via + /// feature flags, and the transaction has no shielded inputs. + pub fn new( + fee_rule: FixedFeeRule, + change_memo: Option, + fallback_change_pool: ShieldedProtocol, + dust_output_policy: DustOutputPolicy, + ) -> Self { + Self { + fee_rule, + change_memo, + fallback_change_pool, + dust_output_policy, + meta_source: PhantomData, + } + } +} + +impl ChangeStrategy for SingleOutputChangeStrategy { + type FeeRule = FixedFeeRule; + type Error = BalanceError; + type MetaSource = I; + type AccountMetaT = (); + + fn fee_rule(&self) -> &Self::FeeRule { + &self.fee_rule + } + + fn fetch_wallet_meta( + &self, + _meta_source: &Self::MetaSource, + _account: ::AccountId, + _exclude: &[::NoteRef], + ) -> Result::Error> { + Ok(()) + } + + fn compute_balance( + &self, + params: &P, + target_height: BlockHeight, + transparent_inputs: &[impl transparent::InputView], + transparent_outputs: &[impl transparent::OutputView], + sapling: &impl sapling_fees::BundleView, + #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView, + ephemeral_balance: Option<&EphemeralBalance>, + _wallet_meta: &Self::AccountMetaT, + ) -> Result> { + let split_policy = SplitPolicy::single_output(); + let cfg = SinglePoolBalanceConfig::new( + params, + &self.fee_rule, + &self.dust_output_policy, + self.fee_rule.fixed_fee(), + &split_policy, + self.fallback_change_pool, + Zatoshis::ZERO, + 0, + ); + + single_pool_output_balance( + cfg, + None, + target_height, + transparent_inputs, + transparent_outputs, + sapling, + #[cfg(feature = "orchard")] + orchard, + self.change_memo.as_ref(), + ephemeral_balance, + ) + } +} + +#[cfg(test)] +mod tests { + use ::transparent::bundle::TxOut; + use zcash_primitives::transaction::fees::{ + fixed::FeeRule as FixedFeeRule, zip317::MINIMUM_FEE, + }; + use zcash_protocol::{ + consensus::{Network, NetworkUpgrade, Parameters}, + value::Zatoshis, + ShieldedProtocol, + }; + + use super::SingleOutputChangeStrategy; + use crate::{ + data_api::{testing::MockWalletDb, wallet::input_selection::SaplingPayment}, + fees::{ + tests::{TestSaplingInput, TestTransparentInput}, + ChangeError, ChangeStrategy, ChangeValue, DustOutputPolicy, + }, + }; + + #[cfg(feature = "orchard")] + use crate::fees::orchard as orchard_fees; + + #[test] + fn change_without_dust() { + let fee_rule = FixedFeeRule::non_standard(MINIMUM_FEE); + let change_strategy = SingleOutputChangeStrategy::::new( + fee_rule, + None, + ShieldedProtocol::Sapling, + DustOutputPolicy::default(), + ); + + // spend a single Sapling note that is sufficient to pay the fee + let result = change_strategy.compute_balance( + &Network::TestNetwork, + Network::TestNetwork + .activation_height(NetworkUpgrade::Nu5) + .unwrap(), + &[] as &[TestTransparentInput], + &[] as &[TxOut], + &( + sapling::builder::BundleType::DEFAULT, + &[TestSaplingInput { + note_id: 0, + value: Zatoshis::const_from_u64(60000), + }][..], + &[SaplingPayment::new(Zatoshis::const_from_u64(40000))][..], + ), + #[cfg(feature = "orchard")] + &orchard_fees::EmptyBundleView, + None, + &(), + ); + + assert_matches!( + result, + Ok(balance) if + balance.proposed_change() == [ChangeValue::sapling(Zatoshis::const_from_u64(10000), None)] && + balance.fee_required() == MINIMUM_FEE + ); + } + + #[test] + fn dust_change() { + let fee_rule = FixedFeeRule::non_standard(MINIMUM_FEE); + let change_strategy = SingleOutputChangeStrategy::::new( + fee_rule, + None, + ShieldedProtocol::Sapling, + DustOutputPolicy::default(), + ); + + // spend a single Sapling note that is sufficient to pay the fee + let result = change_strategy.compute_balance( + &Network::TestNetwork, + Network::TestNetwork + .activation_height(NetworkUpgrade::Nu5) + .unwrap(), + &[] as &[TestTransparentInput], + &[] as &[TxOut], + &( + sapling::builder::BundleType::DEFAULT, + &[ + TestSaplingInput { + note_id: 0, + value: Zatoshis::const_from_u64(40000), + }, + // enough to pay a fee, plus dust + TestSaplingInput { + note_id: 0, + value: Zatoshis::const_from_u64(10100), + }, + ][..], + &[SaplingPayment::new(Zatoshis::const_from_u64(40000))][..], + ), + #[cfg(feature = "orchard")] + &orchard_fees::EmptyBundleView, + None, + &(), + ); + + assert_matches!( + result, + Err(ChangeError::InsufficientFunds { available, required }) + if available == Zatoshis::const_from_u64(50100) && required == Zatoshis::const_from_u64(60000) + ); + } +} diff --git a/zcash_client_backend/src/fees/orchard.rs b/zcash_client_backend/src/fees/orchard.rs new file mode 100644 index 0000000000..ec9c5d0b32 --- /dev/null +++ b/zcash_client_backend/src/fees/orchard.rs @@ -0,0 +1,93 @@ +//! Types related to computation of fees and change related to the Orchard components +//! of a transaction. + +use std::convert::Infallible; + +use orchard::builder::BundleType; +use zcash_protocol::value::Zatoshis; + +/// A trait that provides a minimized view of Orchard bundle configuration +/// suitable for use in fee and change calculation. +pub trait BundleView { + /// The type of inputs to the bundle. + type In: InputView; + /// The type of inputs of the bundle. + type Out: OutputView; + + /// Returns the type of the bundle + fn bundle_type(&self) -> BundleType; + /// Returns the inputs to the bundle. + fn inputs(&self) -> &[Self::In]; + /// Returns the outputs of the bundle. + fn outputs(&self) -> &[Self::Out]; +} + +impl<'a, NoteRef, In: InputView, Out: OutputView> BundleView + for (BundleType, &'a [In], &'a [Out]) +{ + type In = In; + type Out = Out; + + fn bundle_type(&self) -> BundleType { + self.0 + } + + fn inputs(&self) -> &[In] { + self.1 + } + + fn outputs(&self) -> &[Out] { + self.2 + } +} + +/// A [`BundleView`] for the empty bundle with [`BundleType::DEFAULT`] bundle type. +pub struct EmptyBundleView; + +impl BundleView for EmptyBundleView { + type In = Infallible; + type Out = Infallible; + + fn bundle_type(&self) -> BundleType { + BundleType::DEFAULT + } + + fn inputs(&self) -> &[Self::In] { + &[] + } + + fn outputs(&self) -> &[Self::Out] { + &[] + } +} + +/// A trait that provides a minimized view of an Orchard input suitable for use in fee and change +/// calculation. +pub trait InputView { + /// An identifier for the input being spent. + fn note_id(&self) -> &NoteRef; + /// The value of the input being spent. + fn value(&self) -> Zatoshis; +} + +impl InputView for Infallible { + fn note_id(&self) -> &N { + unreachable!() + } + fn value(&self) -> Zatoshis { + unreachable!() + } +} + +/// A trait that provides a minimized view of a Orchard output suitable for use in fee and change +/// calculation. +pub trait OutputView { + /// The value of the output being produced. + fn value(&self) -> Zatoshis; +} + +impl OutputView for Infallible { + fn value(&self) -> Zatoshis { + unreachable!() + } +} diff --git a/zcash_client_backend/src/fees/sapling.rs b/zcash_client_backend/src/fees/sapling.rs new file mode 100644 index 0000000000..f4e282e8e2 --- /dev/null +++ b/zcash_client_backend/src/fees/sapling.rs @@ -0,0 +1,113 @@ +//! Types related to computation of fees and change related to the Sapling components +//! of a transaction. + +use std::convert::Infallible; + +use sapling::builder::{BundleType, OutputInfo, SpendInfo}; +use zcash_protocol::value::Zatoshis; + +/// A trait that provides a minimized view of Sapling bundle configuration +/// suitable for use in fee and change calculation. +pub trait BundleView { + /// The type of inputs to the bundle. + type In: InputView; + /// The type of inputs of the bundle. + type Out: OutputView; + + /// Returns the type of the bundle + fn bundle_type(&self) -> BundleType; + /// Returns the inputs to the bundle. + fn inputs(&self) -> &[Self::In]; + /// Returns the outputs of the bundle. + fn outputs(&self) -> &[Self::Out]; +} + +impl<'a, NoteRef, In: InputView, Out: OutputView> BundleView + for (BundleType, &'a [In], &'a [Out]) +{ + type In = In; + type Out = Out; + + fn bundle_type(&self) -> BundleType { + self.0 + } + + fn inputs(&self) -> &[In] { + self.1 + } + + fn outputs(&self) -> &[Out] { + self.2 + } +} + +/// A [`BundleView`] for the empty bundle with [`BundleType::DEFAULT`] bundle type. +pub struct EmptyBundleView; + +impl BundleView for EmptyBundleView { + type In = Infallible; + type Out = Infallible; + + fn bundle_type(&self) -> BundleType { + BundleType::DEFAULT + } + + fn inputs(&self) -> &[Self::In] { + &[] + } + + fn outputs(&self) -> &[Self::Out] { + &[] + } +} + +/// A trait that provides a minimized view of a Sapling input suitable for use in +/// fee and change calculation. +pub trait InputView { + /// An identifier for the input being spent. + fn note_id(&self) -> &NoteRef; + /// The value of the input being spent. + fn value(&self) -> Zatoshis; +} + +impl InputView for Infallible { + fn note_id(&self) -> &N { + unreachable!() + } + fn value(&self) -> Zatoshis { + unreachable!() + } +} + +// `SpendDescriptionInfo` does not contain a note identifier, so we can only implement +// `InputView<()>` +impl InputView<()> for SpendInfo { + fn note_id(&self) -> &() { + &() + } + + fn value(&self) -> Zatoshis { + Zatoshis::try_from(self.value().inner()) + .expect("An existing note to be spent must have a valid amount value.") + } +} + +/// A trait that provides a minimized view of a Sapling output suitable for use in +/// fee and change calculation. +pub trait OutputView { + /// The value of the output being produced. + fn value(&self) -> Zatoshis; +} + +impl OutputView for OutputInfo { + fn value(&self) -> Zatoshis { + Zatoshis::try_from(self.value().inner()) + .expect("Output values should be checked at construction.") + } +} + +impl OutputView for Infallible { + fn value(&self) -> Zatoshis { + unreachable!() + } +} diff --git a/zcash_client_backend/src/fees/standard.rs b/zcash_client_backend/src/fees/standard.rs new file mode 100644 index 0000000000..f9a14a8514 --- /dev/null +++ b/zcash_client_backend/src/fees/standard.rs @@ -0,0 +1,17 @@ +//! Change strategies designed for use with a standard fee. + +use super::StandardFeeRule; + +/// A change strategy that proposes change as a single output. The output pool is chosen +/// as the most current pool that avoids unnecessary pool-crossing (with a specified +/// fallback when the transaction has no shielded inputs). Fee calculation is delegated +/// to the provided fee rule. +pub type SingleOutputChangeStrategy = + super::zip317::SingleOutputChangeStrategy; + +/// A change strategy that proposes change as potentially multiple evenly-sized outputs having at +/// least a threshold value. The output pool is chosen as the most current pool that avoids +/// unnecessary pool-crossing (with a specified fallback when the transaction has no shielded +/// inputs). Fee calculation is delegated to the provided fee rule. +pub type MultiOutputChangeStrategy = + super::zip317::MultiOutputChangeStrategy; diff --git a/zcash_client_backend/src/fees/zip317.rs b/zcash_client_backend/src/fees/zip317.rs new file mode 100644 index 0000000000..e833dada05 --- /dev/null +++ b/zcash_client_backend/src/fees/zip317.rs @@ -0,0 +1,785 @@ +//! Change strategies designed to implement the ZIP 317 fee rules. +//! +//! Change selection in ZIP 317 requires careful handling of low-valued inputs +//! to ensure that inputs added to a transaction do not cause fees to rise by +//! an amount greater than their value. + +use core::marker::PhantomData; + +use zcash_primitives::transaction::fees::{transparent, zip317 as prim_zip317, FeeRule}; +use zcash_protocol::{ + consensus::{self, BlockHeight}, + memo::MemoBytes, + value::{BalanceError, Zatoshis}, + ShieldedProtocol, +}; + +use crate::{ + data_api::{AccountMeta, InputSource, NoteFilter}, + fees::StandardFeeRule, +}; + +use super::{ + common::{single_pool_output_balance, SinglePoolBalanceConfig}, + sapling as sapling_fees, ChangeError, ChangeStrategy, DustOutputPolicy, EphemeralBalance, + SplitPolicy, TransactionBalance, +}; + +#[cfg(feature = "orchard")] +use super::orchard as orchard_fees; + +/// An extension to the [`FeeRule`] trait that exposes methods required for +/// ZIP 317 fee calculation. +pub trait Zip317FeeRule: FeeRule { + /// Returns the ZIP 317 marginal fee. + fn marginal_fee(&self) -> Zatoshis; + + /// Returns the ZIP 317 number of grace actions + fn grace_actions(&self) -> usize; +} + +impl Zip317FeeRule for prim_zip317::FeeRule { + fn marginal_fee(&self) -> Zatoshis { + self.marginal_fee() + } + + fn grace_actions(&self) -> usize { + self.grace_actions() + } +} + +impl Zip317FeeRule for StandardFeeRule { + fn marginal_fee(&self) -> Zatoshis { + prim_zip317::FeeRule::standard().marginal_fee() + } + + fn grace_actions(&self) -> usize { + prim_zip317::FeeRule::standard().grace_actions() + } +} + +/// A change strategy that proposes change as a single output. The output pool is chosen +/// as the most current pool that avoids unnecessary pool-crossing (with a specified +/// fallback when the transaction has no shielded inputs). Fee calculation is delegated +/// to the provided fee rule. +pub struct SingleOutputChangeStrategy { + fee_rule: R, + change_memo: Option, + fallback_change_pool: ShieldedProtocol, + dust_output_policy: DustOutputPolicy, + meta_source: PhantomData, +} + +impl SingleOutputChangeStrategy { + /// Constructs a new [`SingleOutputChangeStrategy`] with the specified ZIP 317 + /// fee parameters and change memo. + /// + /// `fallback_change_pool` is used when more than one shielded pool is enabled via + /// feature flags, and the transaction has no shielded inputs. + pub fn new( + fee_rule: R, + change_memo: Option, + fallback_change_pool: ShieldedProtocol, + dust_output_policy: DustOutputPolicy, + ) -> Self { + Self { + fee_rule, + change_memo, + fallback_change_pool, + dust_output_policy, + meta_source: PhantomData, + } + } +} + +impl ChangeStrategy for SingleOutputChangeStrategy +where + R: Zip317FeeRule + Clone, + I: InputSource, + ::Error: From, +{ + type FeeRule = R; + type Error = ::Error; + type MetaSource = I; + type AccountMetaT = (); + + fn fee_rule(&self) -> &Self::FeeRule { + &self.fee_rule + } + + fn fetch_wallet_meta( + &self, + _meta_source: &Self::MetaSource, + _account: ::AccountId, + _exclude: &[::NoteRef], + ) -> Result::Error> { + Ok(()) + } + + fn compute_balance( + &self, + params: &P, + target_height: BlockHeight, + transparent_inputs: &[impl transparent::InputView], + transparent_outputs: &[impl transparent::OutputView], + sapling: &impl sapling_fees::BundleView, + #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView, + ephemeral_balance: Option<&EphemeralBalance>, + _wallet_meta: &Self::AccountMetaT, + ) -> Result> { + let split_policy = SplitPolicy::single_output(); + let cfg = SinglePoolBalanceConfig::new( + params, + &self.fee_rule, + &self.dust_output_policy, + self.fee_rule.marginal_fee(), + &split_policy, + self.fallback_change_pool, + self.fee_rule.marginal_fee(), + self.fee_rule.grace_actions(), + ); + + single_pool_output_balance( + cfg, + None, + target_height, + transparent_inputs, + transparent_outputs, + sapling, + #[cfg(feature = "orchard")] + orchard, + self.change_memo.as_ref(), + ephemeral_balance, + ) + } +} + +/// A change strategy that attempts to split the change value into some number of equal-sized notes +/// as dictated by the included [`SplitPolicy`] value. +pub struct MultiOutputChangeStrategy { + fee_rule: R, + change_memo: Option, + fallback_change_pool: ShieldedProtocol, + dust_output_policy: DustOutputPolicy, + split_policy: SplitPolicy, + meta_source: PhantomData, +} + +impl MultiOutputChangeStrategy { + /// Constructs a new [`MultiOutputChangeStrategy`] with the specified ZIP 317 + /// fee parameters, change memo, and change splitting policy. + /// + /// This change strategy will fall back to creating a single change output if insufficient + /// change value is available to create notes with at least the minimum value dictated by the + /// split policy. + /// + /// - `fallback_change_pool`: the pool to which change will be sent if when more than one + /// shielded pool is enabled via feature flags, and the transaction has no shielded inputs. + /// - `split_policy`: A policy value describing how the change value should be returned as + /// multiple notes. + pub fn new( + fee_rule: R, + change_memo: Option, + fallback_change_pool: ShieldedProtocol, + dust_output_policy: DustOutputPolicy, + split_policy: SplitPolicy, + ) -> Self { + Self { + fee_rule, + change_memo, + fallback_change_pool, + dust_output_policy, + split_policy, + meta_source: PhantomData, + } + } +} + +impl ChangeStrategy for MultiOutputChangeStrategy +where + R: Zip317FeeRule + Clone, + I: InputSource, + ::Error: From, +{ + type FeeRule = R; + type Error = ::Error; + type MetaSource = I; + type AccountMetaT = AccountMeta; + + fn fee_rule(&self) -> &Self::FeeRule { + &self.fee_rule + } + + fn fetch_wallet_meta( + &self, + meta_source: &Self::MetaSource, + account: ::AccountId, + exclude: &[::NoteRef], + ) -> Result::Error> { + let note_selector = NoteFilter::ExceedsMinValue( + self.split_policy + .min_split_output_value() + .unwrap_or(SplitPolicy::MIN_NOTE_VALUE), + ); + + meta_source.get_account_metadata(account, ¬e_selector, exclude) + } + + fn compute_balance( + &self, + params: &P, + target_height: BlockHeight, + transparent_inputs: &[impl transparent::InputView], + transparent_outputs: &[impl transparent::OutputView], + sapling: &impl sapling_fees::BundleView, + #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView, + ephemeral_balance: Option<&EphemeralBalance>, + wallet_meta: &Self::AccountMetaT, + ) -> Result> { + let cfg = SinglePoolBalanceConfig::new( + params, + &self.fee_rule, + &self.dust_output_policy, + self.fee_rule.marginal_fee(), + &self.split_policy, + self.fallback_change_pool, + self.fee_rule.marginal_fee(), + self.fee_rule.grace_actions(), + ); + + single_pool_output_balance( + cfg, + Some(wallet_meta), + target_height, + transparent_inputs, + transparent_outputs, + sapling, + #[cfg(feature = "orchard")] + orchard, + self.change_memo.as_ref(), + ephemeral_balance, + ) + } +} + +#[cfg(test)] +mod tests { + use core::{convert::Infallible, num::NonZeroUsize}; + + use ::transparent::{address::Script, bundle::TxOut}; + use zcash_primitives::transaction::fees::zip317::FeeRule as Zip317FeeRule; + use zcash_protocol::{ + consensus::{Network, NetworkUpgrade, Parameters}, + value::Zatoshis, + ShieldedProtocol, + }; + + use super::SingleOutputChangeStrategy; + use crate::{ + data_api::{ + testing::MockWalletDb, wallet::input_selection::SaplingPayment, AccountMeta, PoolMeta, + }, + fees::{ + tests::{TestSaplingInput, TestTransparentInput}, + zip317::MultiOutputChangeStrategy, + ChangeError, ChangeStrategy, ChangeValue, DustAction, DustOutputPolicy, SplitPolicy, + }, + }; + + #[cfg(feature = "orchard")] + use { + crate::data_api::wallet::input_selection::OrchardPayment, + crate::fees::orchard as orchard_fees, + }; + + #[test] + fn change_without_dust() { + let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new( + Zip317FeeRule::standard(), + None, + ShieldedProtocol::Sapling, + DustOutputPolicy::default(), + ); + + // spend a single Sapling note that is sufficient to pay the fee + let result = change_strategy.compute_balance( + &Network::TestNetwork, + Network::TestNetwork + .activation_height(NetworkUpgrade::Nu5) + .unwrap(), + &[] as &[TestTransparentInput], + &[] as &[TxOut], + &( + sapling::builder::BundleType::DEFAULT, + &[TestSaplingInput { + note_id: 0, + value: Zatoshis::const_from_u64(55000), + }][..], + &[SaplingPayment::new(Zatoshis::const_from_u64(40000))][..], + ), + #[cfg(feature = "orchard")] + &orchard_fees::EmptyBundleView, + None, + &(), + ); + + assert_matches!( + result, + Ok(balance) if + balance.proposed_change() == [ChangeValue::sapling(Zatoshis::const_from_u64(5000), None)] && + balance.fee_required() == Zatoshis::const_from_u64(10000) + ); + } + + #[test] + fn change_without_dust_multi() { + let change_strategy = MultiOutputChangeStrategy::<_, MockWalletDb>::new( + Zip317FeeRule::standard(), + None, + ShieldedProtocol::Sapling, + DustOutputPolicy::default(), + SplitPolicy::with_min_output_value( + NonZeroUsize::new(5).unwrap(), + Zatoshis::const_from_u64(100_0000), + ), + ); + + { + // spend a single Sapling note and produce 5 outputs + let balance = |existing_notes, total| { + change_strategy.compute_balance( + &Network::TestNetwork, + Network::TestNetwork + .activation_height(NetworkUpgrade::Nu5) + .unwrap(), + &[] as &[TestTransparentInput], + &[] as &[TxOut], + &( + sapling::builder::BundleType::DEFAULT, + &[TestSaplingInput { + note_id: 0, + value: Zatoshis::const_from_u64(750_0000), + }][..], + &[SaplingPayment::new(Zatoshis::const_from_u64(100_0000))][..], + ), + #[cfg(feature = "orchard")] + &orchard_fees::EmptyBundleView, + None, + &AccountMeta::new(Some(PoolMeta::new(existing_notes, total)), None), + ) + }; + + assert_matches!( + balance(0, Zatoshis::ZERO), + Ok(balance) if + balance.proposed_change() == [ + ChangeValue::sapling(Zatoshis::const_from_u64(129_4000), None), + ChangeValue::sapling(Zatoshis::const_from_u64(129_4000), None), + ChangeValue::sapling(Zatoshis::const_from_u64(129_4000), None), + ChangeValue::sapling(Zatoshis::const_from_u64(129_4000), None), + ChangeValue::sapling(Zatoshis::const_from_u64(129_4000), None), + ] && + balance.fee_required() == Zatoshis::const_from_u64(30000) + ); + + assert_matches!( + balance(2, Zatoshis::const_from_u64(100_0000)), + Ok(balance) if + balance.proposed_change() == [ + ChangeValue::sapling(Zatoshis::const_from_u64(216_0000), None), + ChangeValue::sapling(Zatoshis::const_from_u64(216_0000), None), + ChangeValue::sapling(Zatoshis::const_from_u64(216_0000), None), + ] && + balance.fee_required() == Zatoshis::const_from_u64(20000) + ); + } + + { + // spend a single Sapling note and produce 4 outputs, as the value of the note isn't + // sufficient to produce 5 + let result = change_strategy.compute_balance( + &Network::TestNetwork, + Network::TestNetwork + .activation_height(NetworkUpgrade::Nu5) + .unwrap(), + &[] as &[TestTransparentInput], + &[] as &[TxOut], + &( + sapling::builder::BundleType::DEFAULT, + &[TestSaplingInput { + note_id: 0, + value: Zatoshis::const_from_u64(600_0000), + }][..], + &[SaplingPayment::new(Zatoshis::const_from_u64(100_0000))][..], + ), + #[cfg(feature = "orchard")] + &orchard_fees::EmptyBundleView, + None, + &AccountMeta::new( + Some(PoolMeta::new(0, Zatoshis::ZERO)), + Some(PoolMeta::new(0, Zatoshis::ZERO)), + ), + ); + + assert_matches!( + result, + Ok(balance) if + balance.proposed_change() == [ + ChangeValue::sapling(Zatoshis::const_from_u64(124_7500), None), + ChangeValue::sapling(Zatoshis::const_from_u64(124_2500), None), + ChangeValue::sapling(Zatoshis::const_from_u64(124_2500), None), + ChangeValue::sapling(Zatoshis::const_from_u64(124_2500), None), + ] && + balance.fee_required() == Zatoshis::const_from_u64(25000) + ); + } + } + + #[test] + #[cfg(feature = "orchard")] + fn cross_pool_change_without_dust() { + let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new( + Zip317FeeRule::standard(), + None, + ShieldedProtocol::Orchard, + DustOutputPolicy::default(), + ); + + // spend a single Sapling note that is sufficient to pay the fee + let result = change_strategy.compute_balance( + &Network::TestNetwork, + Network::TestNetwork + .activation_height(NetworkUpgrade::Nu5) + .unwrap(), + &[] as &[TestTransparentInput], + &[] as &[TxOut], + &( + sapling::builder::BundleType::DEFAULT, + &[TestSaplingInput { + note_id: 0, + value: Zatoshis::const_from_u64(55000), + }][..], + &[] as &[Infallible], + ), + &( + orchard::builder::BundleType::DEFAULT, + &[] as &[Infallible], + &[OrchardPayment::new(Zatoshis::const_from_u64(30000))][..], + ), + None, + &(), + ); + + assert_matches!( + result, + Ok(balance) if + balance.proposed_change() == [ChangeValue::orchard(Zatoshis::const_from_u64(5000), None)] && + balance.fee_required() == Zatoshis::const_from_u64(20000) + ); + } + + #[test] + fn change_with_transparent_payments_implicitly_allowing_zero_change() { + change_with_transparent_payments(DustOutputPolicy::default()) + } + + #[test] + fn change_with_transparent_payments_explicitly_allowing_zero_change() { + change_with_transparent_payments(DustOutputPolicy::new( + DustAction::AllowDustChange, + Some(Zatoshis::ZERO), + )) + } + + fn change_with_transparent_payments(dust_output_policy: DustOutputPolicy) { + let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new( + Zip317FeeRule::standard(), + None, + ShieldedProtocol::Sapling, + dust_output_policy, + ); + + // spend a single Sapling note that is sufficient to pay the fee + let result = change_strategy.compute_balance( + &Network::TestNetwork, + Network::TestNetwork + .activation_height(NetworkUpgrade::Nu5) + .unwrap(), + &[] as &[TestTransparentInput], + &[TxOut { + value: Zatoshis::const_from_u64(40000), + script_pubkey: Script(vec![]), + }], + &( + sapling::builder::BundleType::DEFAULT, + &[TestSaplingInput { + note_id: 0, + value: Zatoshis::const_from_u64(55000), + }][..], + &[] as &[Infallible], + ), + #[cfg(feature = "orchard")] + &orchard_fees::EmptyBundleView, + None, + &(), + ); + + assert_matches!( + result, + Ok(balance) if + balance.proposed_change() == [ChangeValue::sapling(Zatoshis::ZERO, None)] + && balance.fee_required() == Zatoshis::const_from_u64(15000) + ); + } + + #[test] + #[cfg(feature = "transparent-inputs")] + fn change_fully_transparent_no_change() { + use crate::fees::sapling as sapling_fees; + use ::transparent::{address::TransparentAddress, bundle::OutPoint}; + + let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new( + Zip317FeeRule::standard(), + None, + ShieldedProtocol::Sapling, + DustOutputPolicy::default(), + ); + + // Spend a single transparent UTXO that is exactly sufficient to pay the fee. + let result = change_strategy.compute_balance::<_, Infallible>( + &Network::TestNetwork, + Network::TestNetwork + .activation_height(NetworkUpgrade::Nu5) + .unwrap(), + &[TestTransparentInput { + outpoint: OutPoint::fake(), + coin: TxOut { + value: Zatoshis::const_from_u64(50000), + script_pubkey: TransparentAddress::PublicKeyHash([0u8; 20]).script(), + }, + }], + &[TxOut { + value: Zatoshis::const_from_u64(40000), + script_pubkey: Script(vec![]), + }], + &sapling_fees::EmptyBundleView, + #[cfg(feature = "orchard")] + &orchard_fees::EmptyBundleView, + None, + &(), + ); + + assert_matches!( + result, + Ok(balance) if + balance.proposed_change().is_empty() && + balance.fee_required() == Zatoshis::const_from_u64(10000) + ); + } + + #[test] + #[cfg(feature = "transparent-inputs")] + fn change_transparent_flows_with_shielded_change() { + use crate::fees::sapling as sapling_fees; + use ::transparent::{address::TransparentAddress, bundle::OutPoint}; + + let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new( + Zip317FeeRule::standard(), + None, + ShieldedProtocol::Sapling, + DustOutputPolicy::default(), + ); + + // Spend a single transparent UTXO that is sufficient to pay the fee. + let result = change_strategy.compute_balance::<_, Infallible>( + &Network::TestNetwork, + Network::TestNetwork + .activation_height(NetworkUpgrade::Nu5) + .unwrap(), + &[TestTransparentInput { + outpoint: OutPoint::fake(), + coin: TxOut { + value: Zatoshis::const_from_u64(63000), + script_pubkey: TransparentAddress::PublicKeyHash([0u8; 20]).script(), + }, + }], + &[TxOut { + value: Zatoshis::const_from_u64(40000), + script_pubkey: Script(vec![]), + }], + &sapling_fees::EmptyBundleView, + #[cfg(feature = "orchard")] + &orchard_fees::EmptyBundleView, + None, + &(), + ); + + assert_matches!( + result, + Ok(balance) if + balance.proposed_change() == [ChangeValue::sapling(Zatoshis::const_from_u64(8000), None)] && + balance.fee_required() == Zatoshis::const_from_u64(15000) + ); + } + + #[test] + #[cfg(feature = "transparent-inputs")] + fn change_transparent_flows_with_shielded_dust_change() { + use crate::fees::sapling as sapling_fees; + use ::transparent::{address::TransparentAddress, bundle::OutPoint}; + + let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new( + Zip317FeeRule::standard(), + None, + ShieldedProtocol::Sapling, + DustOutputPolicy::new( + DustAction::AllowDustChange, + Some(Zatoshis::const_from_u64(1000)), + ), + ); + + // Spend a single transparent UTXO that is sufficient to pay the fee. + // The change will go to the fallback shielded change pool even though all inputs + // and payments are transparent, and even though the change amount (1000) would + // normally be considered dust, because we set the dust policy to allow that. + let result = change_strategy.compute_balance::<_, Infallible>( + &Network::TestNetwork, + Network::TestNetwork + .activation_height(NetworkUpgrade::Nu5) + .unwrap(), + &[TestTransparentInput { + outpoint: OutPoint::fake(), + coin: TxOut { + value: Zatoshis::const_from_u64(56000), + script_pubkey: TransparentAddress::PublicKeyHash([0u8; 20]).script(), + }, + }], + &[TxOut { + value: Zatoshis::const_from_u64(40000), + script_pubkey: Script(vec![]), + }], + &sapling_fees::EmptyBundleView, + #[cfg(feature = "orchard")] + &orchard_fees::EmptyBundleView, + None, + &(), + ); + + assert_matches!( + result, + Ok(balance) if + balance.proposed_change() == [ChangeValue::sapling(Zatoshis::const_from_u64(1000), None)] && + balance.fee_required() == Zatoshis::const_from_u64(15000) + ); + } + + #[test] + fn change_with_allowable_dust_implicitly_allowing_zero_change() { + change_with_allowable_dust(DustOutputPolicy::default()) + } + + #[test] + fn change_with_allowable_dust_explicitly_allowing_zero_change() { + change_with_allowable_dust(DustOutputPolicy::new( + DustAction::AllowDustChange, + Some(Zatoshis::ZERO), + )) + } + + fn change_with_allowable_dust(dust_output_policy: DustOutputPolicy) { + let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new( + Zip317FeeRule::standard(), + None, + ShieldedProtocol::Sapling, + dust_output_policy, + ); + + // Spend two Sapling notes, one of them dust. There is sufficient to + // pay the fee: if only one note is spent then we are 1000 short, but + // if both notes are spent then the fee stays at 10000 (even with a + // zero-valued change output), so we have just enough. + let result = change_strategy.compute_balance( + &Network::TestNetwork, + Network::TestNetwork + .activation_height(NetworkUpgrade::Nu5) + .unwrap(), + &[] as &[TestTransparentInput], + &[] as &[TxOut], + &( + sapling::builder::BundleType::DEFAULT, + &[ + TestSaplingInput { + note_id: 0, + value: Zatoshis::const_from_u64(49000), + }, + TestSaplingInput { + note_id: 1, + value: Zatoshis::const_from_u64(1000), + }, + ][..], + &[SaplingPayment::new(Zatoshis::const_from_u64(40000))][..], + ), + #[cfg(feature = "orchard")] + &orchard_fees::EmptyBundleView, + None, + &(), + ); + + assert_matches!( + result, + Ok(balance) if + balance.proposed_change() == [ChangeValue::sapling(Zatoshis::ZERO, None)] && + balance.fee_required() == Zatoshis::const_from_u64(10000) + ); + } + + #[test] + fn change_with_disallowed_dust() { + let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new( + Zip317FeeRule::standard(), + None, + ShieldedProtocol::Sapling, + DustOutputPolicy::default(), + ); + + // Attempt to spend three Sapling notes, one of them dust. Adding the third + // note increases the number of actions, and so it is uneconomic to spend it. + let result = change_strategy.compute_balance( + &Network::TestNetwork, + Network::TestNetwork + .activation_height(NetworkUpgrade::Nu5) + .unwrap(), + &[] as &[TestTransparentInput], + &[] as &[TxOut], + &( + sapling::builder::BundleType::DEFAULT, + &[ + TestSaplingInput { + note_id: 0, + value: Zatoshis::const_from_u64(29000), + }, + TestSaplingInput { + note_id: 1, + value: Zatoshis::const_from_u64(20000), + }, + TestSaplingInput { + note_id: 2, + value: Zatoshis::const_from_u64(1000), + }, + ][..], + &[SaplingPayment::new(Zatoshis::const_from_u64(30000))][..], + ), + #[cfg(feature = "orchard")] + &orchard_fees::EmptyBundleView, + None, + &(), + ); + + // We will get an error here, because the dust input isn't free to add + // to the transaction. + assert_matches!( + result, + Err(ChangeError::DustInputs { sapling, .. }) if sapling == vec![2] + ); + } +} diff --git a/zcash_client_backend/src/keys.rs b/zcash_client_backend/src/keys.rs deleted file mode 100644 index c2fd72b348..0000000000 --- a/zcash_client_backend/src/keys.rs +++ /dev/null @@ -1,45 +0,0 @@ -//! Helper functions for managing light client key material. - -use zcash_primitives::zip32::{ChildIndex, ExtendedSpendingKey}; - -/// Derives the ZIP 32 [`ExtendedSpendingKey`] for a given coin type and account from the -/// given seed. -/// -/// # Panics -/// -/// Panics if `seed` is shorter than 32 bytes. -/// -/// # Examples -/// -/// ``` -/// use zcash_primitives::{constants::testnet::COIN_TYPE}; -/// use zcash_client_backend::{keys::spending_key}; -/// -/// let extsk = spending_key(&[0; 32][..], COIN_TYPE, 0); -/// ``` -/// [`ExtendedSpendingKey`]: zcash_primitives::zip32::ExtendedSpendingKey -pub fn spending_key(seed: &[u8], coin_type: u32, account: u32) -> ExtendedSpendingKey { - if seed.len() < 32 { - panic!("ZIP 32 seeds MUST be at least 32 bytes"); - } - - ExtendedSpendingKey::from_path( - &ExtendedSpendingKey::master(&seed), - &[ - ChildIndex::Hardened(32), - ChildIndex::Hardened(coin_type), - ChildIndex::Hardened(account), - ], - ) -} - -#[cfg(test)] -mod tests { - use super::spending_key; - - #[test] - #[should_panic] - fn spending_key_panics_on_short_seed() { - let _ = spending_key(&[0; 31][..], 0, 0); - } -} diff --git a/zcash_client_backend/src/lib.rs b/zcash_client_backend/src/lib.rs index 085070c134..781a54b7fe 100644 --- a/zcash_client_backend/src/lib.rs +++ b/zcash_client_backend/src/lib.rs @@ -2,20 +2,106 @@ //! //! `zcash_client_backend` contains Rust structs and traits for creating shielded Zcash //! light clients. +//! +//! # Design +//! +//! ## Wallet sync +//! +//! The APIs in the [`data_api::chain`] module can be used to implement the following +//! synchronization flow: +//! +//! ```text +//! ┌─────────────┐ ┌─────────────┐ +//! │Get required │ │ Update │ +//! │subtree root │─▶│subtree roots│ +//! │ range │ └─────────────┘ +//! └─────────────┘ │ +//! ▼ +//! ┌─────────┐ +//! │ Update │ +//! ┌────────────────────────────────▶│chain tip│◀──────┐ +//! │ └─────────┘ │ +//! │ │ │ +//! │ ▼ │ +//! ┌─────────────┐ ┌────────────┐ ┌─────────────┐ │ +//! │ Truncate │ │Split range │ │Get suggested│ │ +//! │ wallet to │ │into batches│◀─│ scan ranges │ │ +//! │rewind height│ └────────────┘ └─────────────┘ │ +//! └─────────────┘ │ │ +//! ▲ ╱│╲ │ +//! │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ +//! ┌────────┐ ┌───────────────┐ │ │ +//! │ Choose │ │ │Download blocks│ │ +//! │ rewind │ │ to cache │ │ │ +//! │ height │ │ └───────────────┘ .───────────────────. +//! └────────┘ │ │ ( Scan ranges updated ) +//! ▲ │ ▼ `───────────────────' +//! │ ┌───────────┐ │ ▲ +//! .───────────────┴─. │Scan cached│ .─────────. │ +//! ( Continuity error )◀────│ blocks │──▶( Success )───────┤ +//! `───────────────┬─' └───────────┘ `─────────' │ +//! │ │ │ +//! │ ┌──────┴───────┐ │ +//! ▼ ▼ │ ▼ +//! │┌─────────────┐┌─────────────┐ ┌──────────────────────┐ +//! │Delete blocks││ Enhance ││ │Update wallet balance │ +//! ││ from cache ││transactions │ │ and sync progress │ +//! └─────────────┘└─────────────┘│ └──────────────────────┘ +//! └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ +//! ``` +//! +//! ## Feature flags +#![doc = document_features::document_features!()] +//! +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] // Catch documentation errors caused by code changes. -#![deny(broken_intra_doc_links)] +#![deny(rustdoc::broken_intra_doc_links)] // Temporary until we have addressed all Result cases. #![allow(clippy::result_unit_err)] -pub mod address; pub mod data_api; mod decrypt; -pub mod encoding; -pub mod keys; +pub mod fees; +pub mod proposal; pub mod proto; +pub mod scan; +pub mod scanning; pub mod wallet; -pub mod welding_rig; -pub mod zip321; -pub use decrypt::{decrypt_transaction, DecryptedOutput}; +#[cfg(feature = "sync")] +pub mod sync; + +#[cfg(feature = "unstable-serialization")] +pub mod serialization; + +#[cfg(feature = "tor")] +pub mod tor; + +pub use decrypt::{decrypt_transaction, DecryptedOutput, TransferType}; + +#[deprecated(note = "This module is deprecated; use `::zcash_keys::address` instead.")] +pub mod address { + pub use zcash_keys::address::*; +} +#[deprecated(note = "This module is deprecated; use `::zcash_keys::encoding` instead.")] +pub mod encoding { + pub use zcash_keys::encoding::*; +} +#[deprecated(note = "This module is deprecated; use `::zcash_keys::keys` instead.")] +pub mod keys { + pub use zcash_keys::keys::*; +} +#[deprecated(note = "use ::zcash_protocol::PoolType instead")] +pub type PoolType = zcash_protocol::PoolType; +#[deprecated(note = "use ::zcash_protocol::ShieldedProtocol instead")] +pub type ShieldedProtocol = zcash_protocol::ShieldedProtocol; +#[deprecated(note = "This module is deprecated; use the `zip321` crate instead.")] +pub mod zip321 { + pub use zip321::*; +} + +#[cfg(test)] +#[macro_use] +extern crate assert_matches; diff --git a/zcash_client_backend/src/proposal.rs b/zcash_client_backend/src/proposal.rs new file mode 100644 index 0000000000..ff004cdf80 --- /dev/null +++ b/zcash_client_backend/src/proposal.rs @@ -0,0 +1,580 @@ +//! Types related to the construction and evaluation of transaction proposals. + +use std::{ + collections::{BTreeMap, BTreeSet}, + fmt::{self, Debug, Display}, +}; + +use nonempty::NonEmpty; +use zcash_primitives::transaction::TxId; +use zcash_protocol::{consensus::BlockHeight, value::Zatoshis, PoolType, ShieldedProtocol}; +use zip321::TransactionRequest; + +use crate::{ + fees::TransactionBalance, + wallet::{Note, ReceivedNote, WalletTransparentOutput}, +}; + +/// Errors that can occur in construction of a [`Step`]. +#[derive(Debug, Clone)] +pub enum ProposalError { + /// The total output value of the transaction request is not a valid Zcash amount. + RequestTotalInvalid, + /// The total of transaction inputs overflows the valid range of Zcash values. + Overflow, + /// The input total and output total of the payment request are not equal to one another. The + /// sum of transaction outputs, change, and fees is required to be exactly equal to the value + /// of provided inputs. + BalanceError { + input_total: Zatoshis, + output_total: Zatoshis, + }, + /// The `is_shielding` flag may only be set to `true` under the following conditions: + /// * The total of transparent inputs is nonzero + /// * There exist no Sapling inputs + /// * There provided transaction request is empty; i.e. the only output values specified + /// are change and fee amounts. + ShieldingInvalid, + /// No anchor information could be obtained for the specified block height. + AnchorNotFound(BlockHeight), + /// A reference to the output of a prior step is invalid. + ReferenceError(StepOutput), + /// An attempted double-spend of a prior step output was detected. + StepDoubleSpend(StepOutput), + /// An attempted double-spend of an output belonging to the wallet was detected. + ChainDoubleSpend(PoolType, TxId, u32), + /// There was a mismatch between the payments in the proposal's transaction request + /// and the payment pool selection values. + PaymentPoolsMismatch, + /// The proposal tried to spend a change output. Mark the `ChangeValue` as ephemeral if this is intended. + SpendsChange(StepOutput), + /// A proposal step created an ephemeral output that was not spent in any later step. + #[cfg(feature = "transparent-inputs")] + EphemeralOutputLeftUnspent(StepOutput), + /// The proposal included a payment to a TEX address and a spend from a shielded input in the same step. + #[cfg(feature = "transparent-inputs")] + PaysTexFromShielded, + /// The change strategy provided to input selection failed to correctly generate an ephemeral + /// change output when needed for sending to a TEX address. + #[cfg(feature = "transparent-inputs")] + EphemeralOutputsInvalid, +} + +impl Display for ProposalError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ProposalError::RequestTotalInvalid => write!( + f, + "The total requested output value is not a valid Zcash amount." + ), + ProposalError::Overflow => write!( + f, + "The total of transaction inputs overflows the valid range of Zcash values." + ), + ProposalError::BalanceError { + input_total, + output_total, + } => write!( + f, + "Balance error: the output total {} was not equal to the input total {}", + u64::from(*output_total), + u64::from(*input_total) + ), + ProposalError::ShieldingInvalid => write!( + f, + "The proposal violates the rules for a shielding transaction." + ), + ProposalError::AnchorNotFound(h) => { + write!(f, "Unable to compute anchor for block height {:?}", h) + } + ProposalError::ReferenceError(r) => { + write!(f, "No prior step output found for reference {:?}", r) + } + ProposalError::StepDoubleSpend(r) => write!( + f, + "The proposal uses the output of step {:?} in more than one place.", + r + ), + ProposalError::ChainDoubleSpend(pool, txid, index) => write!( + f, + "The proposal attempts to spend the same output twice: {}, {}, {}", + pool, txid, index + ), + ProposalError::PaymentPoolsMismatch => write!( + f, + "The chosen payment pools did not match the payments of the transaction request." + ), + ProposalError::SpendsChange(r) => write!( + f, + "The proposal attempts to spends the change output created at step {:?}.", + r, + ), + #[cfg(feature = "transparent-inputs")] + ProposalError::EphemeralOutputLeftUnspent(r) => write!( + f, + "The proposal created an ephemeral output at step {:?} that was not spent in any later step.", + r, + ), + #[cfg(feature = "transparent-inputs")] + ProposalError::PaysTexFromShielded => write!( + f, + "The proposal included a payment to a TEX address and a spend from a shielded input in the same step.", + ), + #[cfg(feature = "transparent-inputs")] + ProposalError::EphemeralOutputsInvalid => write!( + f, + "The proposal generator failed to correctly generate an ephemeral change output when needed for sending to a TEX address." + ), + } + } +} + +impl std::error::Error for ProposalError {} + +/// The Sapling inputs to a proposed transaction. +#[derive(Clone, PartialEq, Eq)] +pub struct ShieldedInputs { + anchor_height: BlockHeight, + notes: NonEmpty>, +} + +impl ShieldedInputs { + /// Constructs a [`ShieldedInputs`] from its constituent parts. + pub fn from_parts( + anchor_height: BlockHeight, + notes: NonEmpty>, + ) -> Self { + Self { + anchor_height, + notes, + } + } + + /// Returns the anchor height for Sapling inputs that should be used when constructing the + /// proposed transaction. + pub fn anchor_height(&self) -> BlockHeight { + self.anchor_height + } + + /// Returns the list of Sapling notes to be used as inputs to the proposed transaction. + pub fn notes(&self) -> &NonEmpty> { + &self.notes + } +} + +/// A proposal for a series of transactions to be created. +/// +/// Each step of the proposal represents a separate transaction to be created. At present, only +/// transparent outputs of earlier steps may be spent in later steps; the ability to chain shielded +/// transaction steps may be added in a future update. +#[derive(Clone, PartialEq, Eq)] +pub struct Proposal { + fee_rule: FeeRuleT, + min_target_height: BlockHeight, + steps: NonEmpty>, +} + +impl Proposal { + /// Constructs a validated multi-step [`Proposal`]. + /// + /// This operation validates the proposal for agreement between outputs and inputs + /// in the case of multi-step proposals, and ensures that no double-spends are being + /// proposed. + /// + /// Parameters: + /// * `fee_rule`: The fee rule observed by the proposed transaction. + /// * `min_target_height`: The minimum block height at which the transaction may be created. + /// * `steps`: A vector of steps that make up the proposal. + pub fn multi_step( + fee_rule: FeeRuleT, + min_target_height: BlockHeight, + steps: NonEmpty>, + ) -> Result { + let mut consumed_chain_inputs: BTreeSet<(PoolType, TxId, u32)> = BTreeSet::new(); + let mut consumed_prior_inputs: BTreeSet = BTreeSet::new(); + + for (i, step) in steps.iter().enumerate() { + for prior_ref in step.prior_step_inputs() { + // check that there are no forward references + if prior_ref.step_index() >= i { + return Err(ProposalError::ReferenceError(*prior_ref)); + } + // check that the reference is valid + let prior_step = &steps[prior_ref.step_index()]; + match prior_ref.output_index() { + StepOutputIndex::Payment(idx) => { + if prior_step.transaction_request().payments().len() <= idx { + return Err(ProposalError::ReferenceError(*prior_ref)); + } + } + StepOutputIndex::Change(idx) => { + if prior_step.balance().proposed_change().len() <= idx { + return Err(ProposalError::ReferenceError(*prior_ref)); + } + } + } + // check that there are no double-spends + if !consumed_prior_inputs.insert(*prior_ref) { + return Err(ProposalError::StepDoubleSpend(*prior_ref)); + } + } + + for t_out in step.transparent_inputs() { + let key = ( + PoolType::TRANSPARENT, + TxId::from_bytes(*t_out.outpoint().hash()), + t_out.outpoint().n(), + ); + if !consumed_chain_inputs.insert(key) { + return Err(ProposalError::ChainDoubleSpend(key.0, key.1, key.2)); + } + } + + for s_out in step.shielded_inputs().iter().flat_map(|i| i.notes().iter()) { + let key = ( + match &s_out.note() { + Note::Sapling(_) => PoolType::SAPLING, + #[cfg(feature = "orchard")] + Note::Orchard(_) => PoolType::ORCHARD, + }, + *s_out.txid(), + s_out.output_index().into(), + ); + if !consumed_chain_inputs.insert(key) { + return Err(ProposalError::ChainDoubleSpend(key.0, key.1, key.2)); + } + } + } + + Ok(Self { + fee_rule, + min_target_height, + steps, + }) + } + + /// Constructs a validated [`Proposal`] having only a single step from its constituent parts. + /// + /// This operation validates the proposal for balance consistency and agreement between + /// the `is_shielding` flag and the structure of the proposal. + /// + /// Parameters: + /// * `transaction_request`: The ZIP 321 transaction request describing the payments to be + /// made. + /// * `payment_pools`: A map from payment index to pool type. + /// * `transparent_inputs`: The set of previous transparent outputs to be spent. + /// * `shielded_inputs`: The sets of previous shielded outputs to be spent. + /// * `balance`: The change outputs to be added the transaction and the fee to be paid. + /// * `fee_rule`: The fee rule observed by the proposed transaction. + /// * `min_target_height`: The minimum block height at which the transaction may be created. + /// * `is_shielding`: A flag that identifies whether this is a wallet-internal shielding + /// transaction. + #[allow(clippy::too_many_arguments)] + pub fn single_step( + transaction_request: TransactionRequest, + payment_pools: BTreeMap, + transparent_inputs: Vec, + shielded_inputs: Option>, + balance: TransactionBalance, + fee_rule: FeeRuleT, + min_target_height: BlockHeight, + is_shielding: bool, + ) -> Result { + Ok(Self { + fee_rule, + min_target_height, + steps: NonEmpty::singleton(Step::from_parts( + &[], + transaction_request, + payment_pools, + transparent_inputs, + shielded_inputs, + vec![], + balance, + is_shielding, + )?), + }) + } + + /// Returns the fee rule to be used by the transaction builder. + pub fn fee_rule(&self) -> &FeeRuleT { + &self.fee_rule + } + + /// Returns the target height for which the proposal was prepared. + /// + /// The chain must contain at least this many blocks in order for the proposal to + /// be executed. + pub fn min_target_height(&self) -> BlockHeight { + self.min_target_height + } + + /// Returns the steps of the proposal. Each step corresponds to an independent transaction to + /// be generated as a result of this proposal. + pub fn steps(&self) -> &NonEmpty> { + &self.steps + } +} + +impl Debug for Proposal { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Proposal") + .field("fee_rule", &self.fee_rule) + .field("min_target_height", &self.min_target_height) + .field("steps", &self.steps) + .finish() + } +} + +/// A reference to either a payment or change output within a step. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum StepOutputIndex { + Payment(usize), + Change(usize), +} + +/// A reference to the output of a step in a proposal. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct StepOutput { + step_index: usize, + output_index: StepOutputIndex, +} + +impl StepOutput { + /// Constructs a new [`StepOutput`] from its constituent parts. + pub fn new(step_index: usize, output_index: StepOutputIndex) -> Self { + Self { + step_index, + output_index, + } + } + + /// Returns the step index to which this reference refers. + pub fn step_index(&self) -> usize { + self.step_index + } + + /// Returns the identifier for the payment or change output within + /// the referenced step. + pub fn output_index(&self) -> StepOutputIndex { + self.output_index + } +} + +/// The inputs to be consumed and outputs to be produced in a proposed transaction. +#[derive(Clone, PartialEq, Eq)] +pub struct Step { + transaction_request: TransactionRequest, + payment_pools: BTreeMap, + transparent_inputs: Vec, + shielded_inputs: Option>, + prior_step_inputs: Vec, + balance: TransactionBalance, + is_shielding: bool, +} + +impl Step { + /// Constructs a validated [`Step`] from its constituent parts. + /// + /// This operation validates the proposal for balance consistency and agreement between + /// the `is_shielding` flag and the structure of the proposal. + /// + /// Parameters: + /// * `transaction_request`: The ZIP 321 transaction request describing the payments + /// to be made. + /// * `payment_pools`: A map from payment index to pool type. The set of payment indices + /// provided here must exactly match the set of payment indices in the [`TransactionRequest`], + /// and the selected pool for an index must correspond to a valid receiver of the + /// address at that index (or the address itself in the case of bare transparent or Sapling + /// addresses). + /// * `transparent_inputs`: The set of previous transparent outputs to be spent. + /// * `shielded_inputs`: The sets of previous shielded outputs to be spent. + /// * `balance`: The change outputs to be added the transaction and the fee to be paid. + /// * `is_shielding`: A flag that identifies whether this is a wallet-internal shielding + /// transaction. + #[allow(clippy::too_many_arguments)] + pub fn from_parts( + prior_steps: &[Step], + transaction_request: TransactionRequest, + payment_pools: BTreeMap, + transparent_inputs: Vec, + shielded_inputs: Option>, + prior_step_inputs: Vec, + balance: TransactionBalance, + is_shielding: bool, + ) -> Result { + // Verify that the set of payment pools matches exactly a set of valid payment recipients + if transaction_request.payments().len() != payment_pools.len() { + return Err(ProposalError::PaymentPoolsMismatch); + } + for (idx, pool) in &payment_pools { + if !transaction_request + .payments() + .get(idx) + .iter() + .any(|payment| payment.recipient_address().can_receive_as(*pool)) + { + return Err(ProposalError::PaymentPoolsMismatch); + } + } + + let transparent_input_total = transparent_inputs + .iter() + .map(|out| out.txout().value) + .try_fold(Zatoshis::ZERO, |acc, a| { + (acc + a).ok_or(ProposalError::Overflow) + })?; + + let shielded_input_total = shielded_inputs + .iter() + .flat_map(|s_in| s_in.notes().iter()) + .map(|out| out.note().value()) + .try_fold(Zatoshis::ZERO, |acc, a| (acc + a)) + .ok_or(ProposalError::Overflow)?; + + let prior_step_input_total = prior_step_inputs + .iter() + .map(|s_ref| { + let step = prior_steps + .get(s_ref.step_index) + .ok_or(ProposalError::ReferenceError(*s_ref))?; + Ok(match s_ref.output_index { + StepOutputIndex::Payment(i) => step + .transaction_request + .payments() + .get(&i) + .ok_or(ProposalError::ReferenceError(*s_ref))? + .amount(), + StepOutputIndex::Change(i) => step + .balance + .proposed_change() + .get(i) + .ok_or(ProposalError::ReferenceError(*s_ref))? + .value(), + }) + }) + .collect::, _>>()? + .into_iter() + .try_fold(Zatoshis::ZERO, |acc, a| (acc + a)) + .ok_or(ProposalError::Overflow)?; + + let input_total = (transparent_input_total + shielded_input_total + prior_step_input_total) + .ok_or(ProposalError::Overflow)?; + + let request_total = transaction_request + .total() + .map_err(|_| ProposalError::RequestTotalInvalid)?; + let output_total = (request_total + balance.total()).ok_or(ProposalError::Overflow)?; + + if is_shielding + && (transparent_input_total == Zatoshis::ZERO + || shielded_input_total > Zatoshis::ZERO + || request_total > Zatoshis::ZERO) + { + return Err(ProposalError::ShieldingInvalid); + } + + if input_total == output_total { + Ok(Self { + transaction_request, + payment_pools, + transparent_inputs, + shielded_inputs, + prior_step_inputs, + balance, + is_shielding, + }) + } else { + Err(ProposalError::BalanceError { + input_total, + output_total, + }) + } + } + + /// Returns the transaction request that describes the payments to be made. + pub fn transaction_request(&self) -> &TransactionRequest { + &self.transaction_request + } + /// Returns the map from payment index to the pool that has been selected + /// for the output that will fulfill that payment. + pub fn payment_pools(&self) -> &BTreeMap { + &self.payment_pools + } + /// Returns the transparent inputs that have been selected to fund the transaction. + pub fn transparent_inputs(&self) -> &[WalletTransparentOutput] { + &self.transparent_inputs + } + /// Returns the shielded inputs that have been selected to fund the transaction. + pub fn shielded_inputs(&self) -> Option<&ShieldedInputs> { + self.shielded_inputs.as_ref() + } + /// Returns the inputs that should be obtained from the outputs of the transaction + /// created to satisfy a previous step of the proposal. + pub fn prior_step_inputs(&self) -> &[StepOutput] { + self.prior_step_inputs.as_ref() + } + /// Returns the change outputs to be added to the transaction and the fee to be paid. + pub fn balance(&self) -> &TransactionBalance { + &self.balance + } + /// Returns a flag indicating whether or not the proposed transaction + /// is exclusively wallet-internal (if it does not involve any external + /// recipients). + pub fn is_shielding(&self) -> bool { + self.is_shielding + } + + /// Returns whether or not this proposal requires interaction with the specified pool. + pub fn involves(&self, pool_type: PoolType) -> bool { + let input_in_this_pool = || match pool_type { + PoolType::Transparent => self.is_shielding || !self.transparent_inputs.is_empty(), + PoolType::Shielded(ShieldedProtocol::Sapling) => { + self.shielded_inputs.iter().any(|s_in| { + s_in.notes() + .iter() + .any(|note| matches!(note.note(), Note::Sapling(_))) + }) + } + #[cfg(feature = "orchard")] + PoolType::Shielded(ShieldedProtocol::Orchard) => { + self.shielded_inputs.iter().any(|s_in| { + s_in.notes() + .iter() + .any(|note| matches!(note.note(), Note::Orchard(_))) + }) + } + #[cfg(not(feature = "orchard"))] + PoolType::Shielded(ShieldedProtocol::Orchard) => false, + }; + let output_in_this_pool = || self.payment_pools().values().any(|pool| *pool == pool_type); + let change_in_this_pool = || { + self.balance + .proposed_change() + .iter() + .any(|c| c.output_pool() == pool_type) + }; + + input_in_this_pool() || output_in_this_pool() || change_in_this_pool() + } +} + +impl Debug for Step { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Step") + .field("transaction_request", &self.transaction_request) + .field("transparent_inputs", &self.transparent_inputs) + .field( + "shielded_inputs", + &self.shielded_inputs().map(|i| i.notes.len()), + ) + .field("prior_step_inputs", &self.prior_step_inputs) + .field( + "anchor_height", + &self.shielded_inputs().map(|i| i.anchor_height), + ) + .field("balance", &self.balance) + .field("is_shielding", &self.is_shielding) + .finish_non_exhaustive() + } +} diff --git a/zcash_client_backend/src/proto.rs b/zcash_client_backend/src/proto.rs index 9493c4c269..8811e0e372 100644 --- a/zcash_client_backend/src/proto.rs +++ b/zcash_client_backend/src/proto.rs @@ -1,20 +1,56 @@ -//! Generated code for handling light client protobuf structs. +//! This module contains generated code for handling light client protobuf structs. -use ff::PrimeField; -use group::GroupEncoding; -use std::convert::{TryFrom, TryInto}; +use incrementalmerkletree::frontier::CommitmentTree; +use nonempty::NonEmpty; +use std::{ + array::TryFromSliceError, + collections::BTreeMap, + fmt::{self, Display}, + io, +}; +use sapling::{self, note::ExtractedNoteCommitment, Node}; +use zcash_note_encryption::{EphemeralKeyBytes, COMPACT_NOTE_SIZE}; use zcash_primitives::{ block::{BlockHash, BlockHeader}, + merkle_tree::read_commitment_tree, + transaction::TxId, +}; +use zcash_protocol::{ consensus::BlockHeight, - sapling::Nullifier, - transaction::components::sapling::{CompactOutputDescription, OutputDescription}, + memo::{self, MemoBytes}, + value::Zatoshis, + PoolType, ShieldedProtocol, +}; +use zip321::{TransactionRequest, Zip321Error}; + +use crate::{ + data_api::{chain::ChainState, InputSource}, + fees::{ChangeValue, StandardFeeRule, TransactionBalance}, + proposal::{Proposal, ProposalError, ShieldedInputs, Step, StepOutput, StepOutputIndex}, }; -use zcash_note_encryption::COMPACT_NOTE_SIZE; +#[cfg(feature = "transparent-inputs")] +use transparent::bundle::OutPoint; +#[cfg(feature = "orchard")] +use orchard::tree::MerkleHashOrchard; + +#[rustfmt::skip] +#[allow(unknown_lints)] +#[allow(clippy::derive_partial_eq_without_eq)] pub mod compact_formats; +#[rustfmt::skip] +#[allow(unknown_lints)] +#[allow(clippy::derive_partial_eq_without_eq)] +pub mod proposal; + +#[rustfmt::skip] +#[allow(unknown_lints)] +#[allow(clippy::derive_partial_eq_without_eq)] +pub mod service; + impl compact_formats::CompactBlock { /// Returns the [`BlockHash`] for this block. /// @@ -46,7 +82,7 @@ impl compact_formats::CompactBlock { if let Some(header) = self.header() { header.prev_block } else { - BlockHash::from_slice(&self.prevHash) + BlockHash::from_slice(&self.prev_hash) } } @@ -74,16 +110,25 @@ impl compact_formats::CompactBlock { } } -impl compact_formats::CompactOutput { +impl compact_formats::CompactTx { + /// Returns the transaction Id + pub fn txid(&self) -> TxId { + let mut hash = [0u8; 32]; + hash.copy_from_slice(&self.hash); + TxId::from_bytes(hash) + } +} + +impl compact_formats::CompactSaplingOutput { /// Returns the note commitment for this output. /// /// A convenience method that parses [`CompactOutput.cmu`]. /// /// [`CompactOutput.cmu`]: #structfield.cmu - pub fn cmu(&self) -> Result { + pub fn cmu(&self) -> Result { let mut repr = [0; 32]; - repr.as_mut().copy_from_slice(&self.cmu[..]); - bls12_381::Scalar::from_repr(repr).ok_or(()) + repr.copy_from_slice(&self.cmu[..]); + Option::from(ExtractedNoteCommitment::from_bytes(&repr)).ok_or(()) } /// Returns the ephemeral public key for this output. @@ -91,40 +136,693 @@ impl compact_formats::CompactOutput { /// A convenience method that parses [`CompactOutput.epk`]. /// /// [`CompactOutput.epk`]: #structfield.epk - pub fn epk(&self) -> Result { - let p = jubjub::ExtendedPoint::from_bytes(&self.epk[..].try_into().map_err(|_| ())?); - if p.is_some().into() { - Ok(p.unwrap()) - } else { - Err(()) + pub fn ephemeral_key(&self) -> Result { + self.ephemeral_key[..] + .try_into() + .map(EphemeralKeyBytes) + .map_err(|_| ()) + } +} + +impl From<&sapling::bundle::OutputDescription> + for compact_formats::CompactSaplingOutput +{ + fn from( + out: &sapling::bundle::OutputDescription, + ) -> compact_formats::CompactSaplingOutput { + compact_formats::CompactSaplingOutput { + cmu: out.cmu().to_bytes().to_vec(), + ephemeral_key: out.ephemeral_key().as_ref().to_vec(), + ciphertext: out.enc_ciphertext()[..COMPACT_NOTE_SIZE].to_vec(), } } } -impl From for compact_formats::CompactOutput { - fn from(out: OutputDescription) -> compact_formats::CompactOutput { - let mut result = compact_formats::CompactOutput::new(); - result.set_cmu(out.cmu.to_repr().to_vec()); - result.set_epk(out.ephemeral_key.to_bytes().to_vec()); - result.set_ciphertext(out.enc_ciphertext[..COMPACT_NOTE_SIZE].to_vec()); - result +impl TryFrom + for sapling::note_encryption::CompactOutputDescription +{ + type Error = (); + + fn try_from(value: compact_formats::CompactSaplingOutput) -> Result { + (&value).try_into() } } -impl TryFrom for CompactOutputDescription { +impl TryFrom<&compact_formats::CompactSaplingOutput> + for sapling::note_encryption::CompactOutputDescription +{ type Error = (); - fn try_from(value: compact_formats::CompactOutput) -> Result { - Ok(CompactOutputDescription { + fn try_from(value: &compact_formats::CompactSaplingOutput) -> Result { + Ok(sapling::note_encryption::CompactOutputDescription { cmu: value.cmu()?, - epk: value.epk()?, - enc_ciphertext: value.ciphertext, + ephemeral_key: value.ephemeral_key()?, + enc_ciphertext: value.ciphertext[..].try_into().map_err(|_| ())?, }) } } -impl compact_formats::CompactSpend { - pub fn nf(&self) -> Result { - Nullifier::from_slice(&self.nf).map_err(|_| ()) +impl compact_formats::CompactSaplingSpend { + pub fn nf(&self) -> Result { + sapling::Nullifier::from_slice(&self.nf).map_err(|_| ()) + } +} + +#[cfg(feature = "orchard")] +impl TryFrom<&compact_formats::CompactOrchardAction> for orchard::note_encryption::CompactAction { + type Error = (); + + fn try_from(value: &compact_formats::CompactOrchardAction) -> Result { + Ok(orchard::note_encryption::CompactAction::from_parts( + value.nf()?, + value.cmx()?, + value.ephemeral_key()?, + value.ciphertext[..].try_into().map_err(|_| ())?, + )) + } +} + +#[cfg(feature = "orchard")] +impl compact_formats::CompactOrchardAction { + /// Returns the note commitment for the output of this action. + /// + /// A convenience method that parses [`CompactOrchardAction.cmx`]. + /// + /// [`CompactOrchardAction.cmx`]: #structfield.cmx + pub fn cmx(&self) -> Result { + Option::from(orchard::note::ExtractedNoteCommitment::from_bytes( + &self.cmx[..].try_into().map_err(|_| ())?, + )) + .ok_or(()) + } + + /// Returns the nullifier for the spend of this action. + /// + /// A convenience method that parses [`CompactOrchardAction.nullifier`]. + /// + /// [`CompactOrchardAction.nullifier`]: #structfield.nullifier + pub fn nf(&self) -> Result { + let nf_bytes: [u8; 32] = self.nullifier[..].try_into().map_err(|_| ())?; + Option::from(orchard::note::Nullifier::from_bytes(&nf_bytes)).ok_or(()) + } + + /// Returns the ephemeral public key for the output of this action. + /// + /// A convenience method that parses [`CompactOrchardAction.ephemeral_key`]. + /// + /// [`CompactOrchardAction.ephemeral_key`]: #structfield.ephemeral_key + pub fn ephemeral_key(&self) -> Result { + self.ephemeral_key[..] + .try_into() + .map(EphemeralKeyBytes) + .map_err(|_| ()) + } +} + +impl From<&sapling::bundle::SpendDescription> + for compact_formats::CompactSaplingSpend +{ + fn from(spend: &sapling::bundle::SpendDescription) -> compact_formats::CompactSaplingSpend { + compact_formats::CompactSaplingSpend { + nf: spend.nullifier().to_vec(), + } + } +} + +#[cfg(feature = "orchard")] +impl From<&orchard::Action> for compact_formats::CompactOrchardAction { + fn from(action: &orchard::Action) -> compact_formats::CompactOrchardAction { + compact_formats::CompactOrchardAction { + nullifier: action.nullifier().to_bytes().to_vec(), + cmx: action.cmx().to_bytes().to_vec(), + ephemeral_key: action.encrypted_note().epk_bytes.to_vec(), + ciphertext: action.encrypted_note().enc_ciphertext[..COMPACT_NOTE_SIZE].to_vec(), + } + } +} + +impl service::TreeState { + /// Deserializes and returns the Sapling note commitment tree field of the tree state. + pub fn sapling_tree( + &self, + ) -> io::Result> { + if self.sapling_tree.is_empty() { + Ok(CommitmentTree::empty()) + } else { + let sapling_tree_bytes = hex::decode(&self.sapling_tree).map_err(|e| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("Hex decoding of Sapling tree bytes failed: {:?}", e), + ) + })?; + read_commitment_tree::( + &sapling_tree_bytes[..], + ) + } + } + + /// Deserializes and returns the Sapling note commitment tree field of the tree state. + #[cfg(feature = "orchard")] + pub fn orchard_tree( + &self, + ) -> io::Result> + { + if self.orchard_tree.is_empty() { + Ok(CommitmentTree::empty()) + } else { + let orchard_tree_bytes = hex::decode(&self.orchard_tree).map_err(|e| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("Hex decoding of Orchard tree bytes failed: {:?}", e), + ) + })?; + read_commitment_tree::< + MerkleHashOrchard, + _, + { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }, + >(&orchard_tree_bytes[..]) + } + } + + /// Parses this tree state into a [`ChainState`] for use with [`scan_cached_blocks`]. + /// + /// [`scan_cached_blocks`]: crate::data_api::chain::scan_cached_blocks + pub fn to_chain_state(&self) -> io::Result { + let mut hash_bytes = hex::decode(&self.hash).map_err(|e| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("Block hash is not valid hex: {:?}", e), + ) + })?; + // Zcashd hex strings for block hashes are byte-reversed. + hash_bytes.reverse(); + + Ok(ChainState::new( + self.height + .try_into() + .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid block height"))?, + BlockHash::try_from_slice(&hash_bytes).ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidData, "Invalid block hash length.") + })?, + self.sapling_tree()?.to_frontier(), + #[cfg(feature = "orchard")] + self.orchard_tree()?.to_frontier(), + )) + } +} + +/// Constant for the V1 proposal serialization version. +pub const PROPOSAL_SER_V1: u32 = 1; + +/// Errors that can occur in the process of decoding a [`Proposal`] from its protobuf +/// representation. +#[derive(Debug, Clone)] +pub enum ProposalDecodingError { + /// The encoded proposal contained no steps. + NoSteps, + /// The ZIP 321 transaction request URI was invalid. + Zip321(Zip321Error), + /// A proposed input was null. + NullInput(usize), + /// A transaction identifier string did not decode to a valid transaction ID. + TxIdInvalid(TryFromSliceError), + /// An invalid value pool identifier was encountered. + ValuePoolNotSupported(i32), + /// A failure occurred trying to retrieve an unspent note or UTXO from the wallet database. + InputRetrieval(DbError), + /// The unspent note or UTXO corresponding to a proposal input was not found in the wallet + /// database. + InputNotFound(TxId, PoolType, u32), + /// The transaction balance, or a component thereof, failed to decode correctly. + BalanceInvalid, + /// Failed to decode a ZIP-302-compliant memo from the provided memo bytes. + MemoInvalid(memo::Error), + /// The serialization version returned by the protobuf was not recognized. + VersionInvalid(u32), + /// The fee rule specified by the proposal is not supported by the wallet. + FeeRuleNotSupported(proposal::FeeRule), + /// The proposal violated balance or structural constraints. + ProposalInvalid(ProposalError), + /// An inputs field for the given protocol was present, but contained no input note references. + EmptyShieldedInputs(ShieldedProtocol), + /// A memo field was provided for a transparent output. + TransparentMemo, + /// Change outputs to the specified pool are not supported. + InvalidChangeRecipient(PoolType), + /// Ephemeral outputs to the specified pool are not supported. + InvalidEphemeralRecipient(PoolType), +} + +impl From for ProposalDecodingError { + fn from(value: Zip321Error) -> Self { + Self::Zip321(value) + } +} + +impl Display for ProposalDecodingError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ProposalDecodingError::NoSteps => write!(f, "The proposal had no steps."), + ProposalDecodingError::Zip321(err) => write!(f, "Transaction request invalid: {}", err), + ProposalDecodingError::NullInput(i) => { + write!(f, "Proposed input was null at index {}", i) + } + ProposalDecodingError::TxIdInvalid(err) => { + write!(f, "Invalid transaction id: {:?}", err) + } + ProposalDecodingError::ValuePoolNotSupported(id) => { + write!(f, "Invalid value pool identifier: {:?}", id) + } + ProposalDecodingError::InputRetrieval(err) => write!( + f, + "An error occurred retrieving a transaction input: {}", + err + ), + ProposalDecodingError::InputNotFound(txid, pool, idx) => write!( + f, + "No {} input found for txid {}, index {}", + pool, txid, idx + ), + ProposalDecodingError::BalanceInvalid => { + write!(f, "An error occurred decoding the proposal balance.") + } + ProposalDecodingError::MemoInvalid(err) => { + write!(f, "An error occurred decoding a proposed memo: {}", err) + } + ProposalDecodingError::VersionInvalid(v) => { + write!(f, "Unrecognized proposal version {}", v) + } + ProposalDecodingError::FeeRuleNotSupported(r) => { + write!( + f, + "Fee calculation using the {:?} fee rule is not supported.", + r + ) + } + ProposalDecodingError::ProposalInvalid(err) => write!(f, "{}", err), + ProposalDecodingError::EmptyShieldedInputs(protocol) => write!( + f, + "An inputs field was present for {:?}, but contained no note references.", + protocol + ), + ProposalDecodingError::TransparentMemo => { + write!(f, "Transparent outputs cannot have memos.") + } + ProposalDecodingError::InvalidChangeRecipient(pool_type) => write!( + f, + "Change outputs to the {} pool are not supported.", + pool_type + ), + ProposalDecodingError::InvalidEphemeralRecipient(pool_type) => write!( + f, + "Ephemeral outputs to the {} pool are not supported.", + pool_type + ), + } + } +} + +impl std::error::Error for ProposalDecodingError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + ProposalDecodingError::Zip321(e) => Some(e), + ProposalDecodingError::InputRetrieval(e) => Some(e), + ProposalDecodingError::MemoInvalid(e) => Some(e), + _ => None, + } + } +} + +fn pool_type(pool_id: i32) -> Result> { + match proposal::ValuePool::try_from(pool_id) { + Ok(proposal::ValuePool::Transparent) => Ok(PoolType::TRANSPARENT), + Ok(proposal::ValuePool::Sapling) => Ok(PoolType::SAPLING), + Ok(proposal::ValuePool::Orchard) => Ok(PoolType::ORCHARD), + _ => Err(ProposalDecodingError::ValuePoolNotSupported(pool_id)), + } +} + +impl proposal::ReceivedOutput { + pub fn parse_txid(&self) -> Result { + Ok(TxId::from_bytes(self.txid[..].try_into()?)) + } + + pub fn pool_type(&self) -> Result> { + pool_type(self.value_pool) + } +} + +impl proposal::ChangeValue { + pub fn pool_type(&self) -> Result> { + pool_type(self.value_pool) + } +} + +impl From for proposal::ValuePool { + fn from(value: PoolType) -> Self { + match value { + PoolType::Transparent => proposal::ValuePool::Transparent, + PoolType::Shielded(p) => p.into(), + } + } +} + +impl From for proposal::ValuePool { + fn from(value: ShieldedProtocol) -> Self { + match value { + ShieldedProtocol::Sapling => proposal::ValuePool::Sapling, + ShieldedProtocol::Orchard => proposal::ValuePool::Orchard, + } + } +} + +impl proposal::Proposal { + /// Serializes a [`Proposal`] based upon a supported [`StandardFeeRule`] to its protobuf + /// representation. + pub fn from_standard_proposal(value: &Proposal) -> Self { + use proposal::proposed_input; + use proposal::{PriorStepChange, PriorStepOutput, ReceivedOutput}; + let steps = value + .steps() + .iter() + .map(|step| { + let transaction_request = step.transaction_request().to_uri(); + + let anchor_height = step + .shielded_inputs() + .map_or_else(|| 0, |i| u32::from(i.anchor_height())); + + let inputs = step + .transparent_inputs() + .iter() + .map(|utxo| proposal::ProposedInput { + value: Some(proposed_input::Value::ReceivedOutput(ReceivedOutput { + txid: utxo.outpoint().hash().to_vec(), + value_pool: proposal::ValuePool::Transparent.into(), + index: utxo.outpoint().n(), + value: utxo.txout().value.into(), + })), + }) + .chain(step.shielded_inputs().iter().flat_map(|s_in| { + s_in.notes().iter().map(|rec_note| proposal::ProposedInput { + value: Some(proposed_input::Value::ReceivedOutput(ReceivedOutput { + txid: rec_note.txid().as_ref().to_vec(), + value_pool: proposal::ValuePool::from(rec_note.note().protocol()) + .into(), + index: rec_note.output_index().into(), + value: rec_note.note().value().into(), + })), + }) + })) + .chain(step.prior_step_inputs().iter().map(|p_in| { + match p_in.output_index() { + StepOutputIndex::Payment(i) => proposal::ProposedInput { + value: Some(proposed_input::Value::PriorStepOutput( + PriorStepOutput { + step_index: p_in + .step_index() + .try_into() + .expect("Step index fits into a u32"), + payment_index: i + .try_into() + .expect("Payment index fits into a u32"), + }, + )), + }, + StepOutputIndex::Change(i) => proposal::ProposedInput { + value: Some(proposed_input::Value::PriorStepChange( + PriorStepChange { + step_index: p_in + .step_index() + .try_into() + .expect("Step index fits into a u32"), + change_index: i + .try_into() + .expect("Payment index fits into a u32"), + }, + )), + }, + } + })) + .collect(); + + let payment_output_pools = step + .payment_pools() + .iter() + .map(|(idx, pool_type)| proposal::PaymentOutputPool { + payment_index: u32::try_from(*idx).expect("Payment index fits into a u32"), + value_pool: proposal::ValuePool::from(*pool_type).into(), + }) + .collect(); + + let balance = Some(proposal::TransactionBalance { + proposed_change: step + .balance() + .proposed_change() + .iter() + .map(|change| proposal::ChangeValue { + value: change.value().into(), + value_pool: proposal::ValuePool::from(change.output_pool()).into(), + memo: change.memo().map(|memo_bytes| proposal::MemoBytes { + value: memo_bytes.as_slice().to_vec(), + }), + is_ephemeral: change.is_ephemeral(), + }) + .collect(), + fee_required: step.balance().fee_required().into(), + }); + + proposal::ProposalStep { + transaction_request, + payment_output_pools, + anchor_height, + inputs, + balance, + is_shielding: step.is_shielding(), + } + }) + .collect(); + + proposal::Proposal { + proto_version: PROPOSAL_SER_V1, + fee_rule: match value.fee_rule() { + StandardFeeRule::Zip317 => proposal::FeeRule::Zip317, + } + .into(), + min_target_height: value.min_target_height().into(), + steps, + } + } + + /// Attempts to parse a [`Proposal`] based upon a supported [`StandardFeeRule`] from its + /// protobuf representation. + pub fn try_into_standard_proposal( + &self, + wallet_db: &DbT, + ) -> Result, ProposalDecodingError> + where + DbT: InputSource, + { + use self::proposal::proposed_input::Value::*; + match self.proto_version { + PROPOSAL_SER_V1 => { + let fee_rule = match self.fee_rule() { + proposal::FeeRule::Zip317 => StandardFeeRule::Zip317, + other => { + return Err(ProposalDecodingError::FeeRuleNotSupported(other)); + } + }; + + let mut steps = Vec::with_capacity(self.steps.len()); + for step in &self.steps { + let transaction_request = + TransactionRequest::from_uri(&step.transaction_request)?; + + let payment_pools = step + .payment_output_pools + .iter() + .map(|pop| { + Ok(( + usize::try_from(pop.payment_index) + .expect("Payment index fits into a usize"), + pool_type(pop.value_pool)?, + )) + }) + .collect::, ProposalDecodingError>>()?; + + #[allow(unused_mut)] + let mut transparent_inputs = vec![]; + let mut received_notes = vec![]; + let mut prior_step_inputs = vec![]; + for (i, input) in step.inputs.iter().enumerate() { + match input + .value + .as_ref() + .ok_or(ProposalDecodingError::NullInput(i))? + { + ReceivedOutput(out) => { + let txid = out + .parse_txid() + .map_err(ProposalDecodingError::TxIdInvalid)?; + + match out.pool_type()? { + PoolType::Transparent => { + #[cfg(not(feature = "transparent-inputs"))] + return Err(ProposalDecodingError::ValuePoolNotSupported( + out.value_pool, + )); + + #[cfg(feature = "transparent-inputs")] + { + let outpoint = OutPoint::new(txid.into(), out.index); + transparent_inputs.push( + wallet_db + .get_unspent_transparent_output(&outpoint) + .map_err(ProposalDecodingError::InputRetrieval)? + .ok_or({ + ProposalDecodingError::InputNotFound( + txid, + PoolType::TRANSPARENT, + out.index, + ) + })?, + ); + } + } + PoolType::Shielded(protocol) => received_notes.push( + wallet_db + .get_spendable_note(&txid, protocol, out.index) + .map_err(ProposalDecodingError::InputRetrieval) + .and_then(|opt| { + opt.ok_or({ + ProposalDecodingError::InputNotFound( + txid, + PoolType::Shielded(protocol), + out.index, + ) + }) + })?, + ), + } + } + PriorStepOutput(s_ref) => { + prior_step_inputs.push(StepOutput::new( + s_ref + .step_index + .try_into() + .expect("Step index fits into a usize"), + StepOutputIndex::Payment( + s_ref + .payment_index + .try_into() + .expect("Payment index fits into a usize"), + ), + )); + } + PriorStepChange(s_ref) => { + prior_step_inputs.push(StepOutput::new( + s_ref + .step_index + .try_into() + .expect("Step index fits into a usize"), + StepOutputIndex::Change( + s_ref + .change_index + .try_into() + .expect("Payment index fits into a usize"), + ), + )); + } + } + } + + let shielded_inputs = NonEmpty::from_vec(received_notes) + .map(|notes| ShieldedInputs::from_parts(step.anchor_height.into(), notes)); + + let proto_balance = step + .balance + .as_ref() + .ok_or(ProposalDecodingError::BalanceInvalid)?; + let balance = TransactionBalance::new( + proto_balance + .proposed_change + .iter() + .map(|cv| -> Result> { + let value = Zatoshis::from_u64(cv.value) + .map_err(|_| ProposalDecodingError::BalanceInvalid)?; + let memo = cv + .memo + .as_ref() + .map(|bytes| { + MemoBytes::from_bytes(&bytes.value) + .map_err(ProposalDecodingError::MemoInvalid) + }) + .transpose()?; + match (cv.pool_type()?, cv.is_ephemeral) { + (PoolType::Shielded(ShieldedProtocol::Sapling), false) => { + Ok(ChangeValue::sapling(value, memo)) + } + #[cfg(feature = "orchard")] + (PoolType::Shielded(ShieldedProtocol::Orchard), false) => { + Ok(ChangeValue::orchard(value, memo)) + } + (PoolType::Transparent, _) if memo.is_some() => { + Err(ProposalDecodingError::TransparentMemo) + } + #[cfg(feature = "transparent-inputs")] + (PoolType::Transparent, true) => { + Ok(ChangeValue::ephemeral_transparent(value)) + } + (pool, false) => { + Err(ProposalDecodingError::InvalidChangeRecipient(pool)) + } + (pool, true) => { + Err(ProposalDecodingError::InvalidEphemeralRecipient(pool)) + } + } + }) + .collect::, _>>()?, + Zatoshis::from_u64(proto_balance.fee_required) + .map_err(|_| ProposalDecodingError::BalanceInvalid)?, + ) + .map_err(|_| ProposalDecodingError::BalanceInvalid)?; + + let step = Step::from_parts( + &steps, + transaction_request, + payment_pools, + transparent_inputs, + shielded_inputs, + prior_step_inputs, + balance, + step.is_shielding, + ) + .map_err(ProposalDecodingError::ProposalInvalid)?; + + steps.push(step); + } + + Proposal::multi_step( + fee_rule, + self.min_target_height.into(), + NonEmpty::from_vec(steps).ok_or(ProposalDecodingError::NoSteps)?, + ) + .map_err(ProposalDecodingError::ProposalInvalid) + } + other => Err(ProposalDecodingError::VersionInvalid(other)), + } + } +} + +#[cfg(feature = "lightwalletd-tonic-transport")] +impl service::compact_tx_streamer_client::CompactTxStreamerClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) } } diff --git a/zcash_client_backend/src/proto/.keep b/zcash_client_backend/src/proto/.keep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/zcash_client_backend/src/proto/compact_formats.rs b/zcash_client_backend/src/proto/compact_formats.rs new file mode 100644 index 0000000000..e2931b11b6 --- /dev/null +++ b/zcash_client_backend/src/proto/compact_formats.rs @@ -0,0 +1,117 @@ +// This file is @generated by prost-build. +/// Information about the state of the chain as of a given block. +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct ChainMetadata { + /// the size of the Sapling note commitment tree as of the end of this block + #[prost(uint32, tag = "1")] + pub sapling_commitment_tree_size: u32, + /// the size of the Orchard note commitment tree as of the end of this block + #[prost(uint32, tag = "2")] + pub orchard_commitment_tree_size: u32, +} +/// A compact representation of the shielded data in a Zcash block. +/// +/// CompactBlock is a packaging of ONLY the data from a block that's needed to: +/// 1. Detect a payment to your Shielded address +/// 2. Detect a spend of your Shielded notes +/// 3. Update your witnesses to generate new spend proofs. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CompactBlock { + /// the version of this wire format, for storage + #[prost(uint32, tag = "1")] + pub proto_version: u32, + /// the height of this block + #[prost(uint64, tag = "2")] + pub height: u64, + /// the ID (hash) of this block, same as in block explorers + #[prost(bytes = "vec", tag = "3")] + pub hash: ::prost::alloc::vec::Vec, + /// the ID (hash) of this block's predecessor + #[prost(bytes = "vec", tag = "4")] + pub prev_hash: ::prost::alloc::vec::Vec, + /// Unix epoch time when the block was mined + #[prost(uint32, tag = "5")] + pub time: u32, + /// (hash, prevHash, and time) OR (full header) + #[prost(bytes = "vec", tag = "6")] + pub header: ::prost::alloc::vec::Vec, + /// zero or more compact transactions from this block + #[prost(message, repeated, tag = "7")] + pub vtx: ::prost::alloc::vec::Vec, + /// information about the state of the chain as of this block + #[prost(message, optional, tag = "8")] + pub chain_metadata: ::core::option::Option, +} +/// A compact representation of the shielded data in a Zcash transaction. +/// +/// CompactTx contains the minimum information for a wallet to know if this transaction +/// is relevant to it (either pays to it or spends from it) via shielded elements +/// only. This message will not encode a transparent-to-transparent transaction. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CompactTx { + /// Index and hash will allow the receiver to call out to chain + /// explorers or other data structures to retrieve more information + /// about this transaction. + /// + /// the index within the full block + #[prost(uint64, tag = "1")] + pub index: u64, + /// the ID (hash) of this transaction, same as in block explorers + #[prost(bytes = "vec", tag = "2")] + pub hash: ::prost::alloc::vec::Vec, + /// The transaction fee: present if server can provide. In the case of a + /// stateless server and a transaction with transparent inputs, this will be + /// unset because the calculation requires reference to prior transactions. + /// If there are no transparent inputs, the fee will be calculable as: + /// valueBalanceSapling + valueBalanceOrchard + sum(vPubNew) - sum(vPubOld) - sum(tOut) + #[prost(uint32, tag = "3")] + pub fee: u32, + #[prost(message, repeated, tag = "4")] + pub spends: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag = "5")] + pub outputs: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag = "6")] + pub actions: ::prost::alloc::vec::Vec, +} +/// A compact representation of a [Sapling Spend](). +/// +/// CompactSaplingSpend is a Sapling Spend Description as described in 7.3 of the Zcash +/// protocol specification. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CompactSaplingSpend { + /// Nullifier (see the Zcash protocol specification) + #[prost(bytes = "vec", tag = "1")] + pub nf: ::prost::alloc::vec::Vec, +} +/// A compact representation of a [Sapling Output](). +/// +/// It encodes the `cmu` field, `ephemeralKey` field, and a 52-byte prefix of the +/// `encCiphertext` field of a Sapling Output Description. Total size is 116 bytes. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CompactSaplingOutput { + /// Note commitment u-coordinate. + #[prost(bytes = "vec", tag = "1")] + pub cmu: ::prost::alloc::vec::Vec, + /// Ephemeral public key. + #[prost(bytes = "vec", tag = "2")] + pub ephemeral_key: ::prost::alloc::vec::Vec, + /// First 52 bytes of ciphertext. + #[prost(bytes = "vec", tag = "3")] + pub ciphertext: ::prost::alloc::vec::Vec, +} +/// A compact representation of an [Orchard Action](). +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CompactOrchardAction { + /// \[32\] The nullifier of the input note + #[prost(bytes = "vec", tag = "1")] + pub nullifier: ::prost::alloc::vec::Vec, + /// \[32\] The x-coordinate of the note commitment for the output note + #[prost(bytes = "vec", tag = "2")] + pub cmx: ::prost::alloc::vec::Vec, + /// \[32\] An encoding of an ephemeral Pallas public key + #[prost(bytes = "vec", tag = "3")] + pub ephemeral_key: ::prost::alloc::vec::Vec, + /// \[52\] The first 52 bytes of the encCiphertext field + #[prost(bytes = "vec", tag = "4")] + pub ciphertext: ::prost::alloc::vec::Vec, +} diff --git a/zcash_client_backend/src/proto/proposal.rs b/zcash_client_backend/src/proto/proposal.rs new file mode 100644 index 0000000000..eed2b14a7d --- /dev/null +++ b/zcash_client_backend/src/proto/proposal.rs @@ -0,0 +1,225 @@ +// This file is @generated by prost-build. +/// A data structure that describes a series of transactions to be created. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Proposal { + /// The version of this serialization format. + #[prost(uint32, tag = "1")] + pub proto_version: u32, + /// The fee rule used in constructing this proposal + #[prost(enumeration = "FeeRule", tag = "2")] + pub fee_rule: i32, + /// The target height for which the proposal was constructed + /// + /// The chain must contain at least this many blocks in order for the proposal to + /// be executed. + #[prost(uint32, tag = "3")] + pub min_target_height: u32, + /// The series of transactions to be created. + #[prost(message, repeated, tag = "4")] + pub steps: ::prost::alloc::vec::Vec, +} +/// A data structure that describes the inputs to be consumed and outputs to +/// be produced in a proposed transaction. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ProposalStep { + /// ZIP 321 serialized transaction request + #[prost(string, tag = "1")] + pub transaction_request: ::prost::alloc::string::String, + /// The vector of selected payment index / output pool mappings. Payment index + /// 0 corresponds to the payment with no explicit index. + #[prost(message, repeated, tag = "2")] + pub payment_output_pools: ::prost::alloc::vec::Vec, + /// The anchor height to be used in creating the transaction, if any. + /// Setting the anchor height to zero will disallow the use of any shielded + /// inputs. + #[prost(uint32, tag = "3")] + pub anchor_height: u32, + /// The inputs to be used in creating the transaction. + #[prost(message, repeated, tag = "4")] + pub inputs: ::prost::alloc::vec::Vec, + /// The total value, fee value, and change outputs of the proposed + /// transaction + #[prost(message, optional, tag = "5")] + pub balance: ::core::option::Option, + /// A flag indicating whether the step is for a shielding transaction, + /// used for determining which OVK to select for wallet-internal outputs. + #[prost(bool, tag = "6")] + pub is_shielding: bool, +} +/// A mapping from ZIP 321 payment index to the output pool that has been chosen +/// for that payment, based upon the payment address and the selected inputs to +/// the transaction. +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct PaymentOutputPool { + #[prost(uint32, tag = "1")] + pub payment_index: u32, + #[prost(enumeration = "ValuePool", tag = "2")] + pub value_pool: i32, +} +/// The unique identifier and value for each proposed input that does not +/// require a back-reference to a prior step of the proposal. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ReceivedOutput { + #[prost(bytes = "vec", tag = "1")] + pub txid: ::prost::alloc::vec::Vec, + #[prost(enumeration = "ValuePool", tag = "2")] + pub value_pool: i32, + #[prost(uint32, tag = "3")] + pub index: u32, + #[prost(uint64, tag = "4")] + pub value: u64, +} +/// A reference to a payment in a prior step of the proposal. This payment must +/// belong to the wallet. +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct PriorStepOutput { + #[prost(uint32, tag = "1")] + pub step_index: u32, + #[prost(uint32, tag = "2")] + pub payment_index: u32, +} +/// A reference to a change or ephemeral output from a prior step of the proposal. +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct PriorStepChange { + #[prost(uint32, tag = "1")] + pub step_index: u32, + #[prost(uint32, tag = "2")] + pub change_index: u32, +} +/// The unique identifier and value for an input to be used in the transaction. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ProposedInput { + #[prost(oneof = "proposed_input::Value", tags = "1, 2, 3")] + pub value: ::core::option::Option, +} +/// Nested message and enum types in `ProposedInput`. +pub mod proposed_input { + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Value { + #[prost(message, tag = "1")] + ReceivedOutput(super::ReceivedOutput), + #[prost(message, tag = "2")] + PriorStepOutput(super::PriorStepOutput), + #[prost(message, tag = "3")] + PriorStepChange(super::PriorStepChange), + } +} +/// The proposed change outputs and fee value. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct TransactionBalance { + /// A list of change or ephemeral output values. + #[prost(message, repeated, tag = "1")] + pub proposed_change: ::prost::alloc::vec::Vec, + /// The fee to be paid by the proposed transaction, in zatoshis. + #[prost(uint64, tag = "2")] + pub fee_required: u64, +} +/// A proposed change or ephemeral output. If the transparent value pool is +/// selected, the `memo` field must be null. +/// +/// When the `isEphemeral` field of a `ChangeValue` is set, it represents +/// an ephemeral output, which must be spent by a subsequent step. This is +/// only supported for transparent outputs. Each ephemeral output will be +/// given a unique t-address. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ChangeValue { + /// The value of a change or ephemeral output to be created, in zatoshis. + #[prost(uint64, tag = "1")] + pub value: u64, + /// The value pool in which the change or ephemeral output should be created. + #[prost(enumeration = "ValuePool", tag = "2")] + pub value_pool: i32, + /// The optional memo that should be associated with the newly created output. + /// Memos must not be present for transparent outputs. + #[prost(message, optional, tag = "3")] + pub memo: ::core::option::Option, + /// Whether this is to be an ephemeral output. + #[prost(bool, tag = "4")] + pub is_ephemeral: bool, +} +/// An object wrapper for memo bytes, to facilitate representing the +/// `change_memo == None` case. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MemoBytes { + #[prost(bytes = "vec", tag = "1")] + pub value: ::prost::alloc::vec::Vec, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum ValuePool { + /// Protobuf requires that enums have a zero discriminant as the default + /// value. However, we need to require that a known value pool is selected, + /// and we do not want to fall back to any default, so sending the + /// PoolNotSpecified value will be treated as an error. + PoolNotSpecified = 0, + /// The transparent value pool (P2SH is not distinguished from P2PKH) + Transparent = 1, + /// The Sapling value pool + Sapling = 2, + /// The Orchard value pool + Orchard = 3, +} +impl ValuePool { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::PoolNotSpecified => "PoolNotSpecified", + Self::Transparent => "Transparent", + Self::Sapling => "Sapling", + Self::Orchard => "Orchard", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "PoolNotSpecified" => Some(Self::PoolNotSpecified), + "Transparent" => Some(Self::Transparent), + "Sapling" => Some(Self::Sapling), + "Orchard" => Some(Self::Orchard), + _ => None, + } + } +} +/// The fee rule used in constructing a Proposal +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum FeeRule { + /// Protobuf requires that enums have a zero discriminant as the default + /// value. However, we need to require that a known fee rule is selected, + /// and we do not want to fall back to any default, so sending the + /// FeeRuleNotSpecified value will be treated as an error. + NotSpecified = 0, + /// 10000 ZAT + PreZip313 = 1, + /// 1000 ZAT + Zip313 = 2, + /// MAX(10000, 5000 * logical_actions) ZAT + Zip317 = 3, +} +impl FeeRule { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::NotSpecified => "FeeRuleNotSpecified", + Self::PreZip313 => "PreZip313", + Self::Zip313 => "Zip313", + Self::Zip317 => "Zip317", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "FeeRuleNotSpecified" => Some(Self::NotSpecified), + "PreZip313" => Some(Self::PreZip313), + "Zip313" => Some(Self::Zip313), + "Zip317" => Some(Self::Zip317), + _ => None, + } + } +} diff --git a/zcash_client_backend/src/proto/service.rs b/zcash_client_backend/src/proto/service.rs new file mode 100644 index 0000000000..f471e81567 --- /dev/null +++ b/zcash_client_backend/src/proto/service.rs @@ -0,0 +1,896 @@ +// This file is @generated by prost-build. +/// A BlockID message contains identifiers to select a block: a height or a +/// hash. Specification by hash is not implemented, but may be in the future. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct BlockId { + #[prost(uint64, tag = "1")] + pub height: u64, + #[prost(bytes = "vec", tag = "2")] + pub hash: ::prost::alloc::vec::Vec, +} +/// BlockRange specifies a series of blocks from start to end inclusive. +/// Both BlockIDs must be heights; specification by hash is not yet supported. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct BlockRange { + #[prost(message, optional, tag = "1")] + pub start: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub end: ::core::option::Option, +} +/// A TxFilter contains the information needed to identify a particular +/// transaction: either a block and an index, or a direct transaction hash. +/// Currently, only specification by hash is supported. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct TxFilter { + /// block identifier, height or hash + #[prost(message, optional, tag = "1")] + pub block: ::core::option::Option, + /// index within the block + #[prost(uint64, tag = "2")] + pub index: u64, + /// transaction ID (hash, txid) + #[prost(bytes = "vec", tag = "3")] + pub hash: ::prost::alloc::vec::Vec, +} +/// RawTransaction contains the complete transaction data. It also optionally includes +/// the block height in which the transaction was included, or, when returned +/// by GetMempoolStream(), the latest block height. +/// +/// FIXME: the documentation here about mempool status contradicts the documentation +/// for the `height` field. See +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RawTransaction { + /// exact data returned by Zcash 'getrawtransaction' + #[prost(bytes = "vec", tag = "1")] + pub data: ::prost::alloc::vec::Vec, + /// height that the transaction was mined (or -1) + #[prost(uint64, tag = "2")] + pub height: u64, +} +/// A SendResponse encodes an error code and a string. It is currently used +/// only by SendTransaction(). If error code is zero, the operation was +/// successful; if non-zero, it and the message specify the failure. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SendResponse { + #[prost(int32, tag = "1")] + pub error_code: i32, + #[prost(string, tag = "2")] + pub error_message: ::prost::alloc::string::String, +} +/// Chainspec is a placeholder to allow specification of a particular chain fork. +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct ChainSpec {} +/// Empty is for gRPCs that take no arguments, currently only GetLightdInfo. +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct Empty {} +/// LightdInfo returns various information about this lightwalletd instance +/// and the state of the blockchain. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct LightdInfo { + #[prost(string, tag = "1")] + pub version: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub vendor: ::prost::alloc::string::String, + /// true + #[prost(bool, tag = "3")] + pub taddr_support: bool, + /// either "main" or "test" + #[prost(string, tag = "4")] + pub chain_name: ::prost::alloc::string::String, + /// depends on mainnet or testnet + #[prost(uint64, tag = "5")] + pub sapling_activation_height: u64, + /// protocol identifier, see consensus/upgrades.cpp + #[prost(string, tag = "6")] + pub consensus_branch_id: ::prost::alloc::string::String, + /// latest block on the best chain + #[prost(uint64, tag = "7")] + pub block_height: u64, + #[prost(string, tag = "8")] + pub git_commit: ::prost::alloc::string::String, + #[prost(string, tag = "9")] + pub branch: ::prost::alloc::string::String, + #[prost(string, tag = "10")] + pub build_date: ::prost::alloc::string::String, + #[prost(string, tag = "11")] + pub build_user: ::prost::alloc::string::String, + /// less than tip height if zcashd is syncing + #[prost(uint64, tag = "12")] + pub estimated_height: u64, + /// example: "v4.1.1-877212414" + #[prost(string, tag = "13")] + pub zcashd_build: ::prost::alloc::string::String, + /// example: "/MagicBean:4.1.1/" + #[prost(string, tag = "14")] + pub zcashd_subversion: ::prost::alloc::string::String, +} +/// TransparentAddressBlockFilter restricts the results to the given address +/// or block range. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct TransparentAddressBlockFilter { + /// t-address + #[prost(string, tag = "1")] + pub address: ::prost::alloc::string::String, + /// start, end heights + #[prost(message, optional, tag = "2")] + pub range: ::core::option::Option, +} +/// Duration is currently used only for testing, so that the Ping rpc +/// can simulate a delay, to create many simultaneous connections. Units +/// are microseconds. +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct Duration { + #[prost(int64, tag = "1")] + pub interval_us: i64, +} +/// PingResponse is used to indicate concurrency, how many Ping rpcs +/// are executing upon entry and upon exit (after the delay). +/// This rpc is used for testing only. +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct PingResponse { + #[prost(int64, tag = "1")] + pub entry: i64, + #[prost(int64, tag = "2")] + pub exit: i64, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Address { + #[prost(string, tag = "1")] + pub address: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AddressList { + #[prost(string, repeated, tag = "1")] + pub addresses: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct Balance { + #[prost(int64, tag = "1")] + pub value_zat: i64, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Exclude { + #[prost(bytes = "vec", repeated, tag = "1")] + pub txid: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, +} +/// The TreeState is derived from the Zcash z_gettreestate rpc. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct TreeState { + /// "main" or "test" + #[prost(string, tag = "1")] + pub network: ::prost::alloc::string::String, + /// block height + #[prost(uint64, tag = "2")] + pub height: u64, + /// block id + #[prost(string, tag = "3")] + pub hash: ::prost::alloc::string::String, + /// Unix epoch time when the block was mined + #[prost(uint32, tag = "4")] + pub time: u32, + /// sapling commitment tree state + #[prost(string, tag = "5")] + pub sapling_tree: ::prost::alloc::string::String, + /// orchard commitment tree state + #[prost(string, tag = "6")] + pub orchard_tree: ::prost::alloc::string::String, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct GetSubtreeRootsArg { + /// Index identifying where to start returning subtree roots + #[prost(uint32, tag = "1")] + pub start_index: u32, + /// Shielded protocol to return subtree roots for + #[prost(enumeration = "ShieldedProtocol", tag = "2")] + pub shielded_protocol: i32, + /// Maximum number of entries to return, or 0 for all entries. + #[prost(uint32, tag = "3")] + pub max_entries: u32, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SubtreeRoot { + /// The 32-byte Merkle root of the subtree. + #[prost(bytes = "vec", tag = "2")] + pub root_hash: ::prost::alloc::vec::Vec, + /// The hash of the block that completed this subtree. + #[prost(bytes = "vec", tag = "3")] + pub completing_block_hash: ::prost::alloc::vec::Vec, + /// The height of the block that completed this subtree in the main chain. + #[prost(uint64, tag = "4")] + pub completing_block_height: u64, +} +/// Results are sorted by height, which makes it easy to issue another +/// request that picks up from where the previous left off. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetAddressUtxosArg { + #[prost(string, repeated, tag = "1")] + pub addresses: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, + #[prost(uint64, tag = "2")] + pub start_height: u64, + /// zero means unlimited + #[prost(uint32, tag = "3")] + pub max_entries: u32, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetAddressUtxosReply { + #[prost(string, tag = "6")] + pub address: ::prost::alloc::string::String, + #[prost(bytes = "vec", tag = "1")] + pub txid: ::prost::alloc::vec::Vec, + #[prost(int32, tag = "2")] + pub index: i32, + #[prost(bytes = "vec", tag = "3")] + pub script: ::prost::alloc::vec::Vec, + #[prost(int64, tag = "4")] + pub value_zat: i64, + #[prost(uint64, tag = "5")] + pub height: u64, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetAddressUtxosReplyList { + #[prost(message, repeated, tag = "1")] + pub address_utxos: ::prost::alloc::vec::Vec, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum ShieldedProtocol { + Sapling = 0, + Orchard = 1, +} +impl ShieldedProtocol { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Sapling => "sapling", + Self::Orchard => "orchard", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "sapling" => Some(Self::Sapling), + "orchard" => Some(Self::Orchard), + _ => None, + } + } +} +/// Generated client implementations. +#[cfg(feature = "lightwalletd-tonic")] +pub mod compact_tx_streamer_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + #[derive(Debug, Clone)] + pub struct CompactTxStreamerClient { + inner: tonic::client::Grpc, + } + impl CompactTxStreamerClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> CompactTxStreamerClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + CompactTxStreamerClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + /// Return the BlockID of the block at the tip of the best chain + pub async fn get_latest_block( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetLatestBlock", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "cash.z.wallet.sdk.rpc.CompactTxStreamer", + "GetLatestBlock", + ), + ); + self.inner.unary(req, path, codec).await + } + /// Return the compact block corresponding to the given block identifier + pub async fn get_block( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetBlock", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "cash.z.wallet.sdk.rpc.CompactTxStreamer", + "GetBlock", + ), + ); + self.inner.unary(req, path, codec).await + } + /// Same as GetBlock except actions contain only nullifiers + pub async fn get_block_nullifiers( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetBlockNullifiers", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "cash.z.wallet.sdk.rpc.CompactTxStreamer", + "GetBlockNullifiers", + ), + ); + self.inner.unary(req, path, codec).await + } + /// Return a list of consecutive compact blocks + pub async fn get_block_range( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response< + tonic::codec::Streaming, + >, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetBlockRange", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "cash.z.wallet.sdk.rpc.CompactTxStreamer", + "GetBlockRange", + ), + ); + self.inner.server_streaming(req, path, codec).await + } + /// Same as GetBlockRange except actions contain only nullifiers + pub async fn get_block_range_nullifiers( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response< + tonic::codec::Streaming, + >, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetBlockRangeNullifiers", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "cash.z.wallet.sdk.rpc.CompactTxStreamer", + "GetBlockRangeNullifiers", + ), + ); + self.inner.server_streaming(req, path, codec).await + } + /// Return the requested full (not compact) transaction (as from zcashd) + pub async fn get_transaction( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetTransaction", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "cash.z.wallet.sdk.rpc.CompactTxStreamer", + "GetTransaction", + ), + ); + self.inner.unary(req, path, codec).await + } + /// Submit the given transaction to the Zcash network + pub async fn send_transaction( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/cash.z.wallet.sdk.rpc.CompactTxStreamer/SendTransaction", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "cash.z.wallet.sdk.rpc.CompactTxStreamer", + "SendTransaction", + ), + ); + self.inner.unary(req, path, codec).await + } + /// Return the txids corresponding to the given t-address within the given block range + pub async fn get_taddress_txids( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response>, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetTaddressTxids", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "cash.z.wallet.sdk.rpc.CompactTxStreamer", + "GetTaddressTxids", + ), + ); + self.inner.server_streaming(req, path, codec).await + } + pub async fn get_taddress_balance( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetTaddressBalance", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "cash.z.wallet.sdk.rpc.CompactTxStreamer", + "GetTaddressBalance", + ), + ); + self.inner.unary(req, path, codec).await + } + pub async fn get_taddress_balance_stream( + &mut self, + request: impl tonic::IntoStreamingRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetTaddressBalanceStream", + ); + let mut req = request.into_streaming_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "cash.z.wallet.sdk.rpc.CompactTxStreamer", + "GetTaddressBalanceStream", + ), + ); + self.inner.client_streaming(req, path, codec).await + } + /// Return the compact transactions currently in the mempool; the results + /// can be a few seconds out of date. If the Exclude list is empty, return + /// all transactions; otherwise return all *except* those in the Exclude list + /// (if any); this allows the client to avoid receiving transactions that it + /// already has (from an earlier call to this rpc). The transaction IDs in the + /// Exclude list can be shortened to any number of bytes to make the request + /// more bandwidth-efficient; if two or more transactions in the mempool + /// match a shortened txid, they are all sent (none is excluded). Transactions + /// in the exclude list that don't exist in the mempool are ignored. + pub async fn get_mempool_tx( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response< + tonic::codec::Streaming, + >, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetMempoolTx", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "cash.z.wallet.sdk.rpc.CompactTxStreamer", + "GetMempoolTx", + ), + ); + self.inner.server_streaming(req, path, codec).await + } + /// Return a stream of current Mempool transactions. This will keep the output stream open while + /// there are mempool transactions. It will close the returned stream when a new block is mined. + pub async fn get_mempool_stream( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response>, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetMempoolStream", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "cash.z.wallet.sdk.rpc.CompactTxStreamer", + "GetMempoolStream", + ), + ); + self.inner.server_streaming(req, path, codec).await + } + /// GetTreeState returns the note commitment tree state corresponding to the given block. + /// See section 3.7 of the Zcash protocol specification. It returns several other useful + /// values also (even though they can be obtained using GetBlock). + /// The block can be specified by either height or hash. + pub async fn get_tree_state( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetTreeState", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "cash.z.wallet.sdk.rpc.CompactTxStreamer", + "GetTreeState", + ), + ); + self.inner.unary(req, path, codec).await + } + pub async fn get_latest_tree_state( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetLatestTreeState", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "cash.z.wallet.sdk.rpc.CompactTxStreamer", + "GetLatestTreeState", + ), + ); + self.inner.unary(req, path, codec).await + } + /// Returns a stream of information about roots of subtrees of the Sapling and Orchard + /// note commitment trees. + pub async fn get_subtree_roots( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response>, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetSubtreeRoots", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "cash.z.wallet.sdk.rpc.CompactTxStreamer", + "GetSubtreeRoots", + ), + ); + self.inner.server_streaming(req, path, codec).await + } + pub async fn get_address_utxos( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetAddressUtxos", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "cash.z.wallet.sdk.rpc.CompactTxStreamer", + "GetAddressUtxos", + ), + ); + self.inner.unary(req, path, codec).await + } + pub async fn get_address_utxos_stream( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response>, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetAddressUtxosStream", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "cash.z.wallet.sdk.rpc.CompactTxStreamer", + "GetAddressUtxosStream", + ), + ); + self.inner.server_streaming(req, path, codec).await + } + /// Return information about this lightwalletd instance and the blockchain + pub async fn get_lightd_info( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetLightdInfo", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "cash.z.wallet.sdk.rpc.CompactTxStreamer", + "GetLightdInfo", + ), + ); + self.inner.unary(req, path, codec).await + } + /// Testing-only, requires lightwalletd --ping-very-insecure (do not enable in production) + pub async fn ping( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/cash.z.wallet.sdk.rpc.CompactTxStreamer/Ping", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("cash.z.wallet.sdk.rpc.CompactTxStreamer", "Ping"), + ); + self.inner.unary(req, path, codec).await + } + } +} diff --git a/zcash_client_backend/src/scan.rs b/zcash_client_backend/src/scan.rs new file mode 100644 index 0000000000..bb7b85406b --- /dev/null +++ b/zcash_client_backend/src/scan.rs @@ -0,0 +1,591 @@ +use crossbeam_channel as channel; +use std::collections::HashMap; +use std::fmt; +use std::mem; +use std::sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, +}; + +use memuse::DynamicUsage; +use zcash_note_encryption::{ + batch, BatchDomain, Domain, ShieldedOutput, COMPACT_NOTE_SIZE, ENC_CIPHERTEXT_SIZE, +}; +use zcash_primitives::{block::BlockHash, transaction::TxId}; + +/// A decrypted transaction output. +pub(crate) struct DecryptedOutput { + /// The tag corresponding to the incoming viewing key used to decrypt the note. + pub(crate) ivk_tag: IvkTag, + /// The recipient of the note. + pub(crate) recipient: D::Recipient, + /// The note! + pub(crate) note: D::Note, + /// The memo field, or `()` if this is a decrypted compact output. + pub(crate) memo: M, +} + +impl fmt::Debug for DecryptedOutput +where + IvkTag: fmt::Debug, + D::IncomingViewingKey: fmt::Debug, + D::Recipient: fmt::Debug, + D::Note: fmt::Debug, + M: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DecryptedOutput") + .field("ivk_tag", &self.ivk_tag) + .field("recipient", &self.recipient) + .field("note", &self.note) + .field("memo", &self.memo) + .finish() + } +} + +/// A decryptor of transaction outputs. +pub(crate) trait Decryptor { + type Memo; + + fn batch_decrypt( + tags: &[IvkTag], + ivks: &[D::IncomingViewingKey], + outputs: &[(D, Output)], + ) -> impl Iterator>>; +} + +/// A decryptor of outputs as encoded in transactions. +#[allow(dead_code)] +pub(crate) struct FullDecryptor; + +impl> Decryptor + for FullDecryptor +{ + type Memo = D::Memo; + + fn batch_decrypt( + tags: &[IvkTag], + ivks: &[D::IncomingViewingKey], + outputs: &[(D, Output)], + ) -> impl Iterator>> { + batch::try_note_decryption(ivks, outputs) + .into_iter() + .map(|res| { + res.map(|((note, recipient, memo), ivk_idx)| DecryptedOutput { + ivk_tag: tags[ivk_idx].clone(), + recipient, + note, + memo, + }) + }) + } +} + +/// A decryptor of outputs as encoded in compact blocks. +pub(crate) struct CompactDecryptor; + +impl> Decryptor + for CompactDecryptor +{ + type Memo = (); + + fn batch_decrypt( + tags: &[IvkTag], + ivks: &[D::IncomingViewingKey], + outputs: &[(D, Output)], + ) -> impl Iterator>> { + batch::try_compact_note_decryption(ivks, outputs) + .into_iter() + .map(|res| { + res.map(|((note, recipient), ivk_idx)| DecryptedOutput { + ivk_tag: tags[ivk_idx].clone(), + recipient, + note, + memo: (), + }) + }) + } +} + +/// A value correlated with an output index. +struct OutputIndex { + /// The index of the output within the corresponding shielded bundle. + output_index: usize, + /// The value for the output index. + value: V, +} + +type OutputItem = OutputIndex>; + +/// The sender for the result of batch scanning a specific transaction output. +struct OutputReplier(OutputIndex>>); + +impl DynamicUsage for OutputReplier { + #[inline(always)] + fn dynamic_usage(&self) -> usize { + // We count the memory usage of items in the channel on the receiver side. + 0 + } + + #[inline(always)] + fn dynamic_usage_bounds(&self) -> (usize, Option) { + (0, Some(0)) + } +} + +/// The receiver for the result of batch scanning a specific transaction. +struct BatchReceiver(channel::Receiver>); + +impl DynamicUsage for BatchReceiver { + fn dynamic_usage(&self) -> usize { + // We count the memory usage of items in the channel on the receiver side. + let num_items = self.0.len(); + + // We know we use unbounded channels, so the items in the channel are stored as a + // linked list. `crossbeam_channel` allocates memory for the linked list in blocks + // of 31 items. + const ITEMS_PER_BLOCK: usize = 31; + let num_blocks = num_items.div_ceil(ITEMS_PER_BLOCK); + + // The structure of a block is: + // - A pointer to the next block. + // - For each slot in the block: + // - Space for an item. + // - The state of the slot, stored as an AtomicUsize. + const PTR_SIZE: usize = std::mem::size_of::(); + let item_size = std::mem::size_of::>(); + const ATOMIC_USIZE_SIZE: usize = std::mem::size_of::(); + let block_size = PTR_SIZE + ITEMS_PER_BLOCK * (item_size + ATOMIC_USIZE_SIZE); + + num_blocks * block_size + } + + fn dynamic_usage_bounds(&self) -> (usize, Option) { + let usage = self.dynamic_usage(); + (usage, Some(usage)) + } +} + +/// A tracker for the batch scanning tasks that are currently running. +/// +/// This enables a [`BatchRunner`] to be optionally configured to track heap memory usage. +pub(crate) trait Tasks { + type Task: Task; + fn new() -> Self; + fn add_task(&self, item: Item) -> Self::Task; + fn run_task(&self, item: Item) { + let task = self.add_task(item); + rayon::spawn_fifo(|| task.run()); + } +} + +/// A batch scanning task. +pub(crate) trait Task: Send + 'static { + fn run(self); +} + +impl Tasks for () { + type Task = Item; + fn new() -> Self {} + fn add_task(&self, item: Item) -> Self::Task { + // Return the item itself as the task; we aren't tracking anything about it, so + // there is no need to wrap it in a newtype. + item + } +} + +/// A task tracker that measures heap usage. +/// +/// This struct implements `DynamicUsage` without any item bounds, but that works because +/// it only implements `Tasks` for items that implement `DynamicUsage`. +#[allow(dead_code)] +pub(crate) struct WithUsage { + // The current heap usage for all running tasks. + running_usage: Arc, +} + +impl DynamicUsage for WithUsage { + fn dynamic_usage(&self) -> usize { + self.running_usage.load(Ordering::Relaxed) + } + + fn dynamic_usage_bounds(&self) -> (usize, Option) { + // Tasks are relatively short-lived, so we accept the inaccuracy of treating the + // tasks's approximate usage as its bounds. + let usage = self.dynamic_usage(); + (usage, Some(usage)) + } +} + +impl Tasks for WithUsage { + type Task = WithUsageTask; + + fn new() -> Self { + Self { + running_usage: Arc::new(AtomicUsize::new(0)), + } + } + + fn add_task(&self, item: Item) -> Self::Task { + // Create the task that will move onto the heap with the batch item. + let mut task = WithUsageTask { + item, + own_usage: 0, + running_usage: self.running_usage.clone(), + }; + + // `rayon::spawn_fifo` creates a `HeapJob` holding a closure. The size of a + // closure is (to good approximation) the size of the captured environment, which + // in this case is two moved variables: + // - An `Arc`, which is a pointer to data that is amortized over the + // entire `rayon` thread pool, so we only count the pointer size here. + // - The spawned closure, which in our case moves `task` into it. + task.own_usage = + mem::size_of::>() + mem::size_of_val(&task) + task.item.dynamic_usage(); + + // Approximate now as when the heap cost of this running batch begins. In practice + // this is fine, because `Self::add_task` is called from `Self::run_task` which + // immediately moves the task to the heap. + self.running_usage + .fetch_add(task.own_usage, Ordering::SeqCst); + + task + } +} + +/// A task that will clean up its own heap usage from the overall running usage once it is +/// complete. +pub(crate) struct WithUsageTask { + /// The item being run. + item: Item, + /// Size of this task on the heap. We assume that the size of the task does not change + /// once it has been created, to avoid needing to maintain bidirectional channels + /// between [`WithUsage`] and its tasks. + own_usage: usize, + /// Pointer to the parent [`WithUsage`]'s heap usage tracker for running tasks. + running_usage: Arc, +} + +impl Task for WithUsageTask { + fn run(self) { + // Run the item. + self.item.run(); + + // Signal that the heap memory for this task has been freed. + self.running_usage + .fetch_sub(self.own_usage, Ordering::SeqCst); + } +} + +/// A batch of outputs to trial decrypt. +pub(crate) struct Batch> { + tags: Vec, + ivks: Vec, + /// We currently store outputs and repliers as parallel vectors, because + /// [`batch::try_note_decryption`] accepts a slice of domain/output pairs + /// rather than a value that implements `IntoIterator`, and therefore we + /// can't just use `map` to select the parts we need in order to perform + /// batch decryption. Ideally the domain, output, and output replier would + /// all be part of the same struct, which would also track the output index + /// (that is captured in the outer `OutputIndex` of each `OutputReplier`). + outputs: Vec<(D, Output)>, + repliers: Vec>, +} + +impl DynamicUsage for Batch +where + IvkTag: DynamicUsage, + D: BatchDomain + DynamicUsage, + D::IncomingViewingKey: DynamicUsage, + Output: DynamicUsage, + Dec: Decryptor, +{ + fn dynamic_usage(&self) -> usize { + self.tags.dynamic_usage() + + self.ivks.dynamic_usage() + + self.outputs.dynamic_usage() + + self.repliers.dynamic_usage() + } + + fn dynamic_usage_bounds(&self) -> (usize, Option) { + let (tags_lower, tags_upper) = self.tags.dynamic_usage_bounds(); + let (ivks_lower, ivks_upper) = self.ivks.dynamic_usage_bounds(); + let (outputs_lower, outputs_upper) = self.outputs.dynamic_usage_bounds(); + let (repliers_lower, repliers_upper) = self.repliers.dynamic_usage_bounds(); + + ( + tags_lower + ivks_lower + outputs_lower + repliers_lower, + tags_upper + .zip(ivks_upper) + .zip(outputs_upper) + .zip(repliers_upper) + .map(|(((a, b), c), d)| a + b + c + d), + ) + } +} + +impl Batch +where + IvkTag: Clone, + D: BatchDomain, + Dec: Decryptor, +{ + /// Constructs a new batch. + fn new(tags: Vec, ivks: Vec) -> Self { + assert_eq!(tags.len(), ivks.len()); + Self { + tags, + ivks, + outputs: vec![], + repliers: vec![], + } + } + + /// Returns `true` if the batch is currently empty. + fn is_empty(&self) -> bool { + self.outputs.is_empty() + } +} + +impl Task for Batch +where + IvkTag: Clone + Send + 'static, + D: BatchDomain + Send + 'static, + D::IncomingViewingKey: Send, + D::Memo: Send, + D::Note: Send, + D::Recipient: Send, + Output: Send + 'static, + Dec: Decryptor + 'static, + Dec::Memo: Send, +{ + /// Runs the batch of trial decryptions, and reports the results. + fn run(self) { + // Deconstruct self so we can consume the pieces individually. + let Self { + tags, + ivks, + outputs, + repliers, + } = self; + + assert_eq!(outputs.len(), repliers.len()); + + let decryption_results = Dec::batch_decrypt(&tags, &ivks, &outputs); + for (decryption_result, OutputReplier(replier)) in decryption_results.zip(repliers) { + // If `decryption_result` is `None` then we will just drop `replier`, + // indicating to the parent `BatchRunner` that this output was not for us. + if let Some(value) = decryption_result { + let result = OutputIndex { + output_index: replier.output_index, + value, + }; + + if replier.value.send(result).is_err() { + tracing::debug!("BatchRunner was dropped before batch finished"); + break; + } + } + } + } +} + +impl Batch +where + D: BatchDomain, + Output: Clone, + Dec: Decryptor, +{ + /// Adds the given outputs to this batch. + /// + /// `replier` will be called with the result of every output. + fn add_outputs( + &mut self, + domain: impl Fn(&Output) -> D, + outputs: &[Output], + replier: channel::Sender>, + ) { + self.outputs.extend( + outputs + .iter() + .cloned() + .map(|output| (domain(&output), output)), + ); + self.repliers.extend((0..outputs.len()).map(|output_index| { + OutputReplier(OutputIndex { + output_index, + value: replier.clone(), + }) + })); + } +} + +/// A `HashMap` key for looking up the result of a batch scanning a specific transaction. +#[derive(PartialEq, Eq, Hash)] +struct ResultKey(BlockHash, TxId); + +impl DynamicUsage for ResultKey { + #[inline(always)] + fn dynamic_usage(&self) -> usize { + 0 + } + + #[inline(always)] + fn dynamic_usage_bounds(&self) -> (usize, Option) { + (0, Some(0)) + } +} + +/// Logic to run batches of trial decryptions on the global threadpool. +pub(crate) struct BatchRunner +where + D: BatchDomain, + Dec: Decryptor, + T: Tasks>, +{ + batch_size_threshold: usize, + // The batch currently being accumulated. + acc: Batch, + // The running batches. + running_tasks: T, + // Receivers for the results of the running batches. + pending_results: HashMap>, +} + +impl DynamicUsage for BatchRunner +where + IvkTag: DynamicUsage, + D: BatchDomain + DynamicUsage, + D::IncomingViewingKey: DynamicUsage, + Output: DynamicUsage, + Dec: Decryptor, + T: Tasks> + DynamicUsage, +{ + fn dynamic_usage(&self) -> usize { + self.acc.dynamic_usage() + + self.running_tasks.dynamic_usage() + + self.pending_results.dynamic_usage() + } + + fn dynamic_usage_bounds(&self) -> (usize, Option) { + let running_usage = self.running_tasks.dynamic_usage(); + + let bounds = ( + self.acc.dynamic_usage_bounds(), + self.pending_results.dynamic_usage_bounds(), + ); + ( + bounds.0 .0 + running_usage + bounds.1 .0, + bounds + .0 + .1 + .zip(bounds.1 .1) + .map(|(a, b)| a + running_usage + b), + ) + } +} + +impl BatchRunner +where + IvkTag: Clone, + D: BatchDomain, + Dec: Decryptor, + T: Tasks>, +{ + /// Constructs a new batch runner for the given incoming viewing keys. + pub(crate) fn new( + batch_size_threshold: usize, + ivks: impl Iterator, + ) -> Self { + let (tags, ivks) = ivks.unzip(); + Self { + batch_size_threshold, + acc: Batch::new(tags, ivks), + running_tasks: T::new(), + pending_results: HashMap::default(), + } + } +} + +impl BatchRunner +where + IvkTag: Clone + Send + 'static, + D: BatchDomain + Send + 'static, + D::IncomingViewingKey: Clone + Send, + D::Memo: Send, + D::Note: Send, + D::Recipient: Send, + Output: Clone + Send + 'static, + Dec: Decryptor, + T: Tasks>, +{ + /// Batches the given outputs for trial decryption. + /// + /// `block_tag` is the hash of the block that triggered this txid being added to the + /// batch, or the all-zeros hash to indicate that no block triggered it (i.e. it was a + /// mempool change). + /// + /// If after adding the given outputs, the accumulated batch size is at least the size + /// threshold that was set via `Self::new`, `Self::flush` is called. Subsequent calls + /// to `Self::add_outputs` will be accumulated into a new batch. + pub(crate) fn add_outputs( + &mut self, + block_tag: BlockHash, + txid: TxId, + domain: impl Fn(&Output) -> D, + outputs: &[Output], + ) { + let (tx, rx) = channel::unbounded(); + self.acc.add_outputs(domain, outputs, tx); + self.pending_results + .insert(ResultKey(block_tag, txid), BatchReceiver(rx)); + + if self.acc.outputs.len() >= self.batch_size_threshold { + self.flush(); + } + } + + /// Runs the currently accumulated batch on the global threadpool. + /// + /// Subsequent calls to `Self::add_outputs` will be accumulated into a new batch. + pub(crate) fn flush(&mut self) { + if !self.acc.is_empty() { + let mut batch = Batch::new(self.acc.tags.clone(), self.acc.ivks.clone()); + mem::swap(&mut batch, &mut self.acc); + self.running_tasks.run_task(batch); + } + } + + /// Collects the pending decryption results for the given transaction. + /// + /// `block_tag` is the hash of the block that triggered this txid being added to the + /// batch, or the all-zeros hash to indicate that no block triggered it (i.e. it was a + /// mempool change). + pub(crate) fn collect_results( + &mut self, + block_tag: BlockHash, + txid: TxId, + ) -> HashMap<(TxId, usize), DecryptedOutput> { + self.pending_results + .remove(&ResultKey(block_tag, txid)) + // We won't have a pending result if the transaction didn't have outputs of + // this runner's kind. + .map(|BatchReceiver(rx)| { + // This iterator will end once the channel becomes empty and disconnected. + // We created one sender per output, and each sender is dropped after the + // batch it is in completes (and in the case of successful decryptions, + // after the decrypted note has been sent to the channel). Completion of + // the iterator therefore corresponds to complete knowledge of the outputs + // of this transaction that could be decrypted. + rx.into_iter() + .map( + |OutputIndex { + output_index, + value, + }| { ((txid, output_index), value) }, + ) + .collect() + }) + .unwrap_or_default() + } +} diff --git a/zcash_client_backend/src/scanning.rs b/zcash_client_backend/src/scanning.rs new file mode 100644 index 0000000000..316be9102f --- /dev/null +++ b/zcash_client_backend/src/scanning.rs @@ -0,0 +1,1539 @@ +//! Tools for scanning a compact representation of the Zcash block chain. + +use std::collections::{HashMap, HashSet}; +use std::convert::TryFrom; +use std::fmt::{self, Debug}; +use std::hash::Hash; + +use incrementalmerkletree::{Marking, Position, Retention}; +use sapling::{ + note_encryption::{CompactOutputDescription, SaplingDomain}, + SaplingIvk, +}; +use subtle::{ConditionallySelectable, ConstantTimeEq, CtOption}; + +use tracing::{debug, trace}; +use zcash_keys::keys::UnifiedFullViewingKey; +use zcash_note_encryption::{batch, BatchDomain, Domain, ShieldedOutput, COMPACT_NOTE_SIZE}; +use zcash_primitives::transaction::{components::sapling::zip212_enforcement, TxId}; +use zcash_protocol::{ + consensus::{self, BlockHeight, NetworkUpgrade}, + ShieldedProtocol, +}; +use zip32::Scope; + +use crate::{ + data_api::{BlockMetadata, ScannedBlock, ScannedBundles}, + proto::compact_formats::CompactBlock, + scan::{Batch, BatchRunner, CompactDecryptor, DecryptedOutput, Tasks}, + wallet::{WalletOutput, WalletSpend, WalletTx}, +}; + +#[cfg(feature = "orchard")] +use orchard::{ + note_encryption::{CompactAction, OrchardDomain}, + tree::MerkleHashOrchard, +}; + +#[cfg(not(feature = "orchard"))] +use std::marker::PhantomData; + +/// A key that can be used to perform trial decryption and nullifier +/// computation for a [`CompactSaplingOutput`] or [`CompactOrchardAction`]. +/// +/// The purpose of this trait is to enable [`scan_block`] +/// and related methods to be used with either incoming viewing keys +/// or full viewing keys, with the data returned from trial decryption +/// being dependent upon the type of key used. In the case that an +/// incoming viewing key is used, only the note and payment address +/// will be returned; in the case of a full viewing key, the +/// nullifier for the note can also be obtained. +/// +/// [`CompactSaplingOutput`]: crate::proto::compact_formats::CompactSaplingOutput +/// [`CompactOrchardAction`]: crate::proto::compact_formats::CompactOrchardAction +/// [`scan_block`]: crate::scanning::scan_block +pub trait ScanningKeyOps { + /// Prepare the key for use in batch trial decryption. + fn prepare(&self) -> D::IncomingViewingKey; + + /// Returns the account identifier for this key. An account identifier corresponds + /// to at most a single unified spending key's worth of spend authority, such that + /// both received notes and change spendable by that spending authority will be + /// interpreted as belonging to that account. + fn account_id(&self) -> &AccountId; + + /// Returns the [`zip32::Scope`] for which this key was derived, if known. + fn key_scope(&self) -> Option; + + /// Produces the nullifier for the specified note and witness, if possible. + /// + /// IVK-based implementations of this trait cannot successfully derive + /// nullifiers, in which this function will always return `None`. + fn nf(&self, note: &D::Note, note_position: Position) -> Option; +} + +impl> ScanningKeyOps + for &K +{ + fn prepare(&self) -> D::IncomingViewingKey { + (*self).prepare() + } + + fn account_id(&self) -> &AccountId { + (*self).account_id() + } + + fn key_scope(&self) -> Option { + (*self).key_scope() + } + + fn nf(&self, note: &D::Note, note_position: Position) -> Option { + (*self).nf(note, note_position) + } +} + +impl ScanningKeyOps + for Box> +{ + fn prepare(&self) -> D::IncomingViewingKey { + self.as_ref().prepare() + } + + fn account_id(&self) -> &AccountId { + self.as_ref().account_id() + } + + fn key_scope(&self) -> Option { + self.as_ref().key_scope() + } + + fn nf(&self, note: &D::Note, note_position: Position) -> Option { + self.as_ref().nf(note, note_position) + } +} + +/// An incoming viewing key, paired with an optional nullifier key and key source metadata. +pub struct ScanningKey { + ivk: Ivk, + nk: Option, + account_id: AccountId, + key_scope: Option, +} + +impl ScanningKeyOps + for ScanningKey +{ + fn prepare(&self) -> sapling::note_encryption::PreparedIncomingViewingKey { + sapling::note_encryption::PreparedIncomingViewingKey::new(&self.ivk) + } + + fn nf(&self, note: &sapling::Note, position: Position) -> Option { + self.nk.as_ref().map(|key| note.nf(key, position.into())) + } + + fn account_id(&self) -> &AccountId { + &self.account_id + } + + fn key_scope(&self) -> Option { + self.key_scope + } +} + +impl ScanningKeyOps + for (AccountId, SaplingIvk) +{ + fn prepare(&self) -> sapling::note_encryption::PreparedIncomingViewingKey { + sapling::note_encryption::PreparedIncomingViewingKey::new(&self.1) + } + + fn nf(&self, _note: &sapling::Note, _position: Position) -> Option { + None + } + + fn account_id(&self) -> &AccountId { + &self.0 + } + + fn key_scope(&self) -> Option { + None + } +} + +#[cfg(feature = "orchard")] +impl ScanningKeyOps + for ScanningKey +{ + fn prepare(&self) -> orchard::keys::PreparedIncomingViewingKey { + orchard::keys::PreparedIncomingViewingKey::new(&self.ivk) + } + + fn nf( + &self, + note: &orchard::note::Note, + _position: Position, + ) -> Option { + self.nk.as_ref().map(|key| note.nullifier(key)) + } + + fn account_id(&self) -> &AccountId { + &self.account_id + } + + fn key_scope(&self) -> Option { + self.key_scope + } +} + +/// A set of keys to be used in scanning for decryptable transaction outputs. +pub struct ScanningKeys { + sapling: HashMap>>, + #[cfg(feature = "orchard")] + orchard: HashMap< + IvkTag, + Box>, + >, +} + +impl ScanningKeys { + /// Constructs a new set of scanning keys. + pub fn new( + sapling: HashMap< + IvkTag, + Box>, + >, + #[cfg(feature = "orchard")] orchard: HashMap< + IvkTag, + Box>, + >, + ) -> Self { + Self { + sapling, + #[cfg(feature = "orchard")] + orchard, + } + } + + /// Constructs a new empty set of scanning keys. + pub fn empty() -> Self { + Self { + sapling: HashMap::new(), + #[cfg(feature = "orchard")] + orchard: HashMap::new(), + } + } + + /// Returns the Sapling keys to be used for incoming note detection. + pub fn sapling( + &self, + ) -> &HashMap>> + { + &self.sapling + } + + /// Returns the Orchard keys to be used for incoming note detection. + #[cfg(feature = "orchard")] + pub fn orchard( + &self, + ) -> &HashMap>> + { + &self.orchard + } +} + +impl ScanningKeys { + /// Constructs a [`ScanningKeys`] from an iterator of [`UnifiedFullViewingKey`]s, + /// along with the account identifiers corresponding to those UFVKs. + pub fn from_account_ufvks( + ufvks: impl IntoIterator, + ) -> Self { + #![allow(clippy::type_complexity)] + + let mut sapling: HashMap< + (AccountId, Scope), + Box>, + > = HashMap::new(); + #[cfg(feature = "orchard")] + let mut orchard: HashMap< + (AccountId, Scope), + Box>, + > = HashMap::new(); + + for (account_id, ufvk) in ufvks { + if let Some(dfvk) = ufvk.sapling() { + for scope in [Scope::External, Scope::Internal] { + sapling.insert( + (account_id, scope), + Box::new(ScanningKey { + ivk: dfvk.to_ivk(scope), + nk: Some(dfvk.to_nk(scope)), + account_id, + key_scope: Some(scope), + }), + ); + } + } + + #[cfg(feature = "orchard")] + if let Some(fvk) = ufvk.orchard() { + for scope in [Scope::External, Scope::Internal] { + orchard.insert( + (account_id, scope), + Box::new(ScanningKey { + ivk: fvk.to_ivk(scope), + nk: Some(fvk.clone()), + account_id, + key_scope: Some(scope), + }), + ); + } + } + } + + Self { + sapling, + #[cfg(feature = "orchard")] + orchard, + } + } +} + +/// The set of nullifiers being tracked by a wallet. +pub struct Nullifiers { + sapling: Vec<(AccountId, sapling::Nullifier)>, + #[cfg(feature = "orchard")] + orchard: Vec<(AccountId, orchard::note::Nullifier)>, +} + +impl Nullifiers { + /// Constructs a new empty set of nullifiers + pub fn empty() -> Self { + Self { + sapling: vec![], + #[cfg(feature = "orchard")] + orchard: vec![], + } + } + + /// Construct a nullifier set from its constituent parts. + pub(crate) fn new( + sapling: Vec<(AccountId, sapling::Nullifier)>, + #[cfg(feature = "orchard")] orchard: Vec<(AccountId, orchard::note::Nullifier)>, + ) -> Self { + Self { + sapling, + #[cfg(feature = "orchard")] + orchard, + } + } + + /// Returns the Sapling nullifiers for notes that the wallet is tracking. + pub fn sapling(&self) -> &[(AccountId, sapling::Nullifier)] { + self.sapling.as_ref() + } + + /// Returns the Orchard nullifiers for notes that the wallet is tracking. + #[cfg(feature = "orchard")] + pub fn orchard(&self) -> &[(AccountId, orchard::note::Nullifier)] { + self.orchard.as_ref() + } + + /// Discards Sapling nullifiers from the tracked nullifier set, retaining only those that + /// satisfy the given predicate. + pub(crate) fn retain_sapling(&mut self, f: impl Fn(&(AccountId, sapling::Nullifier)) -> bool) { + self.sapling.retain(f); + } + + /// Adds the given nullifiers to the tracked nullifier set. + pub(crate) fn extend_sapling( + &mut self, + nfs: impl IntoIterator, + ) { + self.sapling.extend(nfs); + } + + #[cfg(feature = "orchard")] + pub(crate) fn retain_orchard( + &mut self, + f: impl Fn(&(AccountId, orchard::note::Nullifier)) -> bool, + ) { + self.orchard.retain(f); + } + + #[cfg(feature = "orchard")] + pub(crate) fn extend_orchard( + &mut self, + nfs: impl IntoIterator, + ) { + self.orchard.extend(nfs); + } +} + +/// Errors that may occur in chain scanning +#[derive(Clone, Debug)] +pub enum ScanError { + /// The encoding of a compact Sapling output or compact Orchard action was invalid. + EncodingInvalid { + at_height: BlockHeight, + txid: TxId, + pool_type: ShieldedProtocol, + index: usize, + }, + + /// The hash of the parent block given by a proposed new chain tip does not match the hash of + /// the current chain tip. + PrevHashMismatch { at_height: BlockHeight }, + + /// The block height field of the proposed new block is not equal to the height of the previous + /// block + 1. + BlockHeightDiscontinuity { + prev_height: BlockHeight, + new_height: BlockHeight, + }, + + /// The note commitment tree size for the given protocol at the proposed new block is not equal + /// to the size at the previous block plus the count of this block's outputs. + TreeSizeMismatch { + protocol: ShieldedProtocol, + at_height: BlockHeight, + given: u32, + computed: u32, + }, + + /// The size of the note commitment tree for the given protocol was not provided as part of a + /// [`CompactBlock`] being scanned, making it impossible to construct the nullifier for a + /// detected note. + TreeSizeUnknown { + protocol: ShieldedProtocol, + at_height: BlockHeight, + }, + + /// We were provided chain metadata for a block containing note commitment tree metadata + /// that is invalidated by the data in the block itself. This may be caused by the presence + /// of default values in the chain metadata. + TreeSizeInvalid { + protocol: ShieldedProtocol, + at_height: BlockHeight, + }, +} + +impl ScanError { + /// Returns whether this error is the result of a failed continuity check + pub fn is_continuity_error(&self) -> bool { + use ScanError::*; + match self { + EncodingInvalid { .. } => false, + PrevHashMismatch { .. } => true, + BlockHeightDiscontinuity { .. } => true, + TreeSizeMismatch { .. } => true, + TreeSizeUnknown { .. } => false, + TreeSizeInvalid { .. } => false, + } + } + + /// Returns the block height at which the scan error occurred + pub fn at_height(&self) -> BlockHeight { + use ScanError::*; + match self { + EncodingInvalid { at_height, .. } => *at_height, + PrevHashMismatch { at_height } => *at_height, + BlockHeightDiscontinuity { new_height, .. } => *new_height, + TreeSizeMismatch { at_height, .. } => *at_height, + TreeSizeUnknown { at_height, .. } => *at_height, + TreeSizeInvalid { at_height, .. } => *at_height, + } + } +} + +impl fmt::Display for ScanError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use ScanError::*; + match &self { + EncodingInvalid { txid, pool_type, index, .. } => write!( + f, + "{:?} output {} of transaction {} was improperly encoded.", + pool_type, index, txid + ), + PrevHashMismatch { at_height } => write!( + f, + "The parent hash of proposed block does not correspond to the block hash at height {}.", + at_height + ), + BlockHeightDiscontinuity { prev_height, new_height } => { + write!(f, "Block height discontinuity at height {}; previous height was: {}", new_height, prev_height) + } + TreeSizeMismatch { protocol, at_height, given, computed } => { + write!(f, "The {:?} note commitment tree size provided by a compact block did not match the expected size at height {}; given {}, expected {}", protocol, at_height, given, computed) + } + TreeSizeUnknown { protocol, at_height } => { + write!(f, "Unable to determine {:?} note commitment tree size at height {}", protocol, at_height) + } + TreeSizeInvalid { protocol, at_height } => { + write!(f, "Received invalid (potentially default) {:?} note commitment tree size metadata at height {}", protocol, at_height) + } + } + } +} + +/// Scans a [`CompactBlock`] with a set of [`ScanningKeys`]. +/// +/// Returns a vector of [`WalletTx`]s decryptable by any of the given keys. If an output is +/// decrypted by a full viewing key, the nullifiers of that output will also be computed. +/// +/// [`CompactBlock`]: crate::proto::compact_formats::CompactBlock +/// [`WalletTx`]: crate::wallet::WalletTx +pub fn scan_block( + params: &P, + block: CompactBlock, + scanning_keys: &ScanningKeys, + nullifiers: &Nullifiers, + prior_block_metadata: Option<&BlockMetadata>, +) -> Result, ScanError> +where + P: consensus::Parameters + Send + 'static, + AccountId: Default + Eq + Hash + ConditionallySelectable + Send + 'static, + IvkTag: Copy + std::hash::Hash + Eq + Send + 'static, +{ + scan_block_with_runners::<_, _, _, (), ()>( + params, + block, + scanning_keys, + nullifiers, + prior_block_metadata, + None, + ) +} + +type TaggedSaplingBatch = Batch< + IvkTag, + SaplingDomain, + sapling::note_encryption::CompactOutputDescription, + CompactDecryptor, +>; +type TaggedSaplingBatchRunner = BatchRunner< + IvkTag, + SaplingDomain, + sapling::note_encryption::CompactOutputDescription, + CompactDecryptor, + Tasks, +>; + +#[cfg(feature = "orchard")] +type TaggedOrchardBatch = + Batch; +#[cfg(feature = "orchard")] +type TaggedOrchardBatchRunner = BatchRunner< + IvkTag, + OrchardDomain, + orchard::note_encryption::CompactAction, + CompactDecryptor, + Tasks, +>; + +pub(crate) trait SaplingTasks: Tasks> {} +impl>> SaplingTasks for T {} + +#[cfg(not(feature = "orchard"))] +pub(crate) trait OrchardTasks {} +#[cfg(not(feature = "orchard"))] +impl OrchardTasks for T {} + +#[cfg(feature = "orchard")] +pub(crate) trait OrchardTasks: Tasks> {} +#[cfg(feature = "orchard")] +impl>> OrchardTasks for T {} + +pub(crate) struct BatchRunners, TO: OrchardTasks> { + sapling: TaggedSaplingBatchRunner, + #[cfg(feature = "orchard")] + orchard: TaggedOrchardBatchRunner, + #[cfg(not(feature = "orchard"))] + orchard: PhantomData, +} + +impl BatchRunners +where + IvkTag: Clone + Send + 'static, + TS: SaplingTasks, + TO: OrchardTasks, +{ + pub(crate) fn for_keys( + batch_size_threshold: usize, + scanning_keys: &ScanningKeys, + ) -> Self { + BatchRunners { + sapling: BatchRunner::new( + batch_size_threshold, + scanning_keys + .sapling() + .iter() + .map(|(id, key)| (id.clone(), key.prepare())), + ), + #[cfg(feature = "orchard")] + orchard: BatchRunner::new( + batch_size_threshold, + scanning_keys + .orchard() + .iter() + .map(|(id, key)| (id.clone(), key.prepare())), + ), + #[cfg(not(feature = "orchard"))] + orchard: PhantomData, + } + } + + pub(crate) fn flush(&mut self) { + self.sapling.flush(); + #[cfg(feature = "orchard")] + self.orchard.flush(); + } + + #[tracing::instrument(skip_all, fields(height = block.height))] + pub(crate) fn add_block

(&mut self, params: &P, block: CompactBlock) -> Result<(), ScanError> + where + P: consensus::Parameters + Send + 'static, + IvkTag: Copy + Send + 'static, + { + let block_hash = block.hash(); + let block_height = block.height(); + let zip212_enforcement = zip212_enforcement(params, block_height); + + for tx in block.vtx.into_iter() { + let txid = tx.txid(); + + self.sapling.add_outputs( + block_hash, + txid, + |_| SaplingDomain::new(zip212_enforcement), + &tx.outputs + .iter() + .enumerate() + .map(|(i, output)| { + CompactOutputDescription::try_from(output).map_err(|_| { + ScanError::EncodingInvalid { + at_height: block_height, + txid, + pool_type: ShieldedProtocol::Sapling, + index: i, + } + }) + }) + .collect::, _>>()?, + ); + + #[cfg(feature = "orchard")] + self.orchard.add_outputs( + block_hash, + txid, + OrchardDomain::for_compact_action, + &tx.actions + .iter() + .enumerate() + .map(|(i, action)| { + CompactAction::try_from(action).map_err(|_| ScanError::EncodingInvalid { + at_height: block_height, + txid, + pool_type: ShieldedProtocol::Orchard, + index: i, + }) + }) + .collect::, _>>()?, + ); + } + + Ok(()) + } +} + +#[tracing::instrument(skip_all, fields(height = block.height))] +pub(crate) fn scan_block_with_runners( + params: &P, + block: CompactBlock, + scanning_keys: &ScanningKeys, + nullifiers: &Nullifiers, + prior_block_metadata: Option<&BlockMetadata>, + mut batch_runners: Option<&mut BatchRunners>, +) -> Result, ScanError> +where + P: consensus::Parameters + Send + 'static, + AccountId: Default + Eq + Hash + ConditionallySelectable + Send + 'static, + IvkTag: Copy + std::hash::Hash + Eq + Send + 'static, + TS: SaplingTasks + Sync, + TO: OrchardTasks + Sync, +{ + fn check_hash_continuity( + block: &CompactBlock, + prior_block_metadata: Option<&BlockMetadata>, + ) -> Option { + if let Some(prev) = prior_block_metadata { + if block.height() != prev.block_height() + 1 { + debug!( + "Block height discontinuity at {:?}, previous was {:?} ", + block.height(), + prev.block_height() + ); + return Some(ScanError::BlockHeightDiscontinuity { + prev_height: prev.block_height(), + new_height: block.height(), + }); + } + + if block.prev_hash() != prev.block_hash() { + debug!("Block hash discontinuity at {:?}", block.height()); + return Some(ScanError::PrevHashMismatch { + at_height: block.height(), + }); + } + } + + None + } + + if let Some(scan_error) = check_hash_continuity(&block, prior_block_metadata) { + return Err(scan_error); + } + + trace!("Block continuity okay at {:?}", block.height()); + + let cur_height = block.height(); + let cur_hash = block.hash(); + let zip212_enforcement = zip212_enforcement(params, cur_height); + + let mut sapling_commitment_tree_size = prior_block_metadata + .and_then(|m| m.sapling_tree_size()) + .map_or_else( + || { + block.chain_metadata.as_ref().map_or_else( + || { + // If we're below Sapling activation, or Sapling activation is not set, the tree size is zero + params + .activation_height(NetworkUpgrade::Sapling) + .map_or_else( + || Ok(0), + |sapling_activation| { + if cur_height < sapling_activation { + Ok(0) + } else { + Err(ScanError::TreeSizeUnknown { + protocol: ShieldedProtocol::Sapling, + at_height: cur_height, + }) + } + }, + ) + }, + |m| { + let sapling_output_count: u32 = block + .vtx + .iter() + .map(|tx| tx.outputs.len()) + .sum::() + .try_into() + .expect("Sapling output count cannot exceed a u32"); + + // The default for m.sapling_commitment_tree_size is zero, so we need to check + // that the subtraction will not underflow; if it would do so, we were given + // invalid chain metadata for a block with Sapling outputs. + m.sapling_commitment_tree_size + .checked_sub(sapling_output_count) + .ok_or(ScanError::TreeSizeInvalid { + protocol: ShieldedProtocol::Sapling, + at_height: cur_height, + }) + }, + ) + }, + Ok, + )?; + let sapling_final_tree_size = sapling_commitment_tree_size + + block + .vtx + .iter() + .map(|tx| u32::try_from(tx.outputs.len()).unwrap()) + .sum::(); + + #[cfg(feature = "orchard")] + let mut orchard_commitment_tree_size = prior_block_metadata + .and_then(|m| m.orchard_tree_size()) + .map_or_else( + || { + block.chain_metadata.as_ref().map_or_else( + || { + // If we're below Orchard activation, or Orchard activation is not set, the tree size is zero + params.activation_height(NetworkUpgrade::Nu5).map_or_else( + || Ok(0), + |orchard_activation| { + if cur_height < orchard_activation { + Ok(0) + } else { + Err(ScanError::TreeSizeUnknown { + protocol: ShieldedProtocol::Orchard, + at_height: cur_height, + }) + } + }, + ) + }, + |m| { + let orchard_action_count: u32 = block + .vtx + .iter() + .map(|tx| tx.actions.len()) + .sum::() + .try_into() + .expect("Orchard action count cannot exceed a u32"); + + // The default for m.orchard_commitment_tree_size is zero, so we need to check + // that the subtraction will not underflow; if it would do so, we were given + // invalid chain metadata for a block with Orchard actions. + m.orchard_commitment_tree_size + .checked_sub(orchard_action_count) + .ok_or(ScanError::TreeSizeInvalid { + protocol: ShieldedProtocol::Orchard, + at_height: cur_height, + }) + }, + ) + }, + Ok, + )?; + #[cfg(feature = "orchard")] + let orchard_final_tree_size = orchard_commitment_tree_size + + block + .vtx + .iter() + .map(|tx| u32::try_from(tx.actions.len()).unwrap()) + .sum::(); + + let mut wtxs: Vec> = vec![]; + let mut sapling_nullifier_map = Vec::with_capacity(block.vtx.len()); + let mut sapling_note_commitments: Vec<(sapling::Node, Retention)> = vec![]; + + #[cfg(feature = "orchard")] + let mut orchard_nullifier_map = Vec::with_capacity(block.vtx.len()); + #[cfg(feature = "orchard")] + let mut orchard_note_commitments: Vec<(MerkleHashOrchard, Retention)> = vec![]; + + for tx in block.vtx.into_iter() { + let txid = tx.txid(); + let tx_index = + u16::try_from(tx.index).expect("Cannot fit more than 2^16 transactions in a block"); + + let (sapling_spends, sapling_unlinked_nullifiers) = find_spent( + &tx.spends, + &nullifiers.sapling, + |spend| { + spend.nf().expect( + "Could not deserialize nullifier for spend from protobuf representation.", + ) + }, + WalletSpend::from_parts, + ); + + sapling_nullifier_map.push((txid, tx_index, sapling_unlinked_nullifiers)); + + #[cfg(feature = "orchard")] + let orchard_spends = { + let (orchard_spends, orchard_unlinked_nullifiers) = find_spent( + &tx.actions, + &nullifiers.orchard, + |spend| { + spend.nf().expect( + "Could not deserialize nullifier for spend from protobuf representation.", + ) + }, + WalletSpend::from_parts, + ); + orchard_nullifier_map.push((txid, tx_index, orchard_unlinked_nullifiers)); + orchard_spends + }; + + // Collect the set of accounts that were spent from in this transaction + let spent_from_accounts = sapling_spends.iter().map(|spend| spend.account_id()); + #[cfg(feature = "orchard")] + let spent_from_accounts = + spent_from_accounts.chain(orchard_spends.iter().map(|spend| spend.account_id())); + let spent_from_accounts = spent_from_accounts.copied().collect::>(); + + let (sapling_outputs, mut sapling_nc) = find_received( + cur_height, + sapling_final_tree_size + == sapling_commitment_tree_size + u32::try_from(tx.outputs.len()).unwrap(), + txid, + sapling_commitment_tree_size, + &scanning_keys.sapling, + &spent_from_accounts, + &tx.outputs + .iter() + .enumerate() + .map(|(i, output)| { + Ok(( + SaplingDomain::new(zip212_enforcement), + CompactOutputDescription::try_from(output).map_err(|_| { + ScanError::EncodingInvalid { + at_height: cur_height, + txid, + pool_type: ShieldedProtocol::Sapling, + index: i, + } + })?, + )) + }) + .collect::, _>>()?, + batch_runners + .as_mut() + .map(|runners| |txid| runners.sapling.collect_results(cur_hash, txid)), + |output| sapling::Node::from_cmu(&output.cmu), + ); + sapling_note_commitments.append(&mut sapling_nc); + let has_sapling = !(sapling_spends.is_empty() && sapling_outputs.is_empty()); + + #[cfg(feature = "orchard")] + let (orchard_outputs, mut orchard_nc) = find_received( + cur_height, + orchard_final_tree_size + == orchard_commitment_tree_size + u32::try_from(tx.actions.len()).unwrap(), + txid, + orchard_commitment_tree_size, + &scanning_keys.orchard, + &spent_from_accounts, + &tx.actions + .iter() + .enumerate() + .map(|(i, action)| { + let action = CompactAction::try_from(action).map_err(|_| { + ScanError::EncodingInvalid { + at_height: cur_height, + txid, + pool_type: ShieldedProtocol::Orchard, + index: i, + } + })?; + Ok((OrchardDomain::for_compact_action(&action), action)) + }) + .collect::, _>>()?, + batch_runners + .as_mut() + .map(|runners| |txid| runners.orchard.collect_results(cur_hash, txid)), + |output| MerkleHashOrchard::from_cmx(&output.cmx()), + ); + #[cfg(feature = "orchard")] + orchard_note_commitments.append(&mut orchard_nc); + + #[cfg(feature = "orchard")] + let has_orchard = !(orchard_spends.is_empty() && orchard_outputs.is_empty()); + #[cfg(not(feature = "orchard"))] + let has_orchard = false; + + if has_sapling || has_orchard { + wtxs.push(WalletTx::new( + txid, + tx_index as usize, + sapling_spends, + sapling_outputs, + #[cfg(feature = "orchard")] + orchard_spends, + #[cfg(feature = "orchard")] + orchard_outputs, + )); + } + + sapling_commitment_tree_size += + u32::try_from(tx.outputs.len()).expect("Sapling output count cannot exceed a u32"); + #[cfg(feature = "orchard")] + { + orchard_commitment_tree_size += + u32::try_from(tx.actions.len()).expect("Orchard action count cannot exceed a u32"); + } + } + + if let Some(chain_meta) = block.chain_metadata { + if chain_meta.sapling_commitment_tree_size != sapling_commitment_tree_size { + return Err(ScanError::TreeSizeMismatch { + protocol: ShieldedProtocol::Sapling, + at_height: cur_height, + given: chain_meta.sapling_commitment_tree_size, + computed: sapling_commitment_tree_size, + }); + } + + #[cfg(feature = "orchard")] + if chain_meta.orchard_commitment_tree_size != orchard_commitment_tree_size { + return Err(ScanError::TreeSizeMismatch { + protocol: ShieldedProtocol::Orchard, + at_height: cur_height, + given: chain_meta.orchard_commitment_tree_size, + computed: orchard_commitment_tree_size, + }); + } + } + + Ok(ScannedBlock::from_parts( + cur_height, + cur_hash, + block.time, + wtxs, + ScannedBundles::new( + sapling_commitment_tree_size, + sapling_note_commitments, + sapling_nullifier_map, + ), + #[cfg(feature = "orchard")] + ScannedBundles::new( + orchard_commitment_tree_size, + orchard_note_commitments, + orchard_nullifier_map, + ), + )) +} + +/// Check for spent notes. The comparison against known-unspent nullifiers is done +/// in constant time. +fn find_spent< + AccountId: ConditionallySelectable + Default, + Spend, + Nf: ConstantTimeEq + Copy, + WS, +>( + spends: &[Spend], + nullifiers: &[(AccountId, Nf)], + extract_nf: impl Fn(&Spend) -> Nf, + construct_wallet_spend: impl Fn(usize, Nf, AccountId) -> WS, +) -> (Vec, Vec) { + // TODO: this is O(|nullifiers| * |notes|); does using constant-time operations here really + // make sense? + let mut found_spent = vec![]; + let mut unlinked_nullifiers = Vec::with_capacity(spends.len()); + for (index, spend) in spends.iter().enumerate() { + let spend_nf = extract_nf(spend); + + // Find whether any tracked nullifier that matches this spend, and produce a + // WalletShieldedSpend in constant time. + let ct_spend = nullifiers + .iter() + .map(|&(account, nf)| CtOption::new(account, nf.ct_eq(&spend_nf))) + .fold( + CtOption::new(AccountId::default(), 0.into()), + |first, next| CtOption::conditional_select(&next, &first, first.is_some()), + ) + .map(|account| construct_wallet_spend(index, spend_nf, account)); + + if let Some(spend) = ct_spend.into() { + found_spent.push(spend); + } else { + // This nullifier didn't match any we are currently tracking; save it in + // case it matches an earlier block range we haven't scanned yet. + unlinked_nullifiers.push(spend_nf); + } + } + + (found_spent, unlinked_nullifiers) +} + +#[allow(clippy::too_many_arguments)] +#[allow(clippy::type_complexity)] +fn find_received< + AccountId: Copy + Eq + Hash, + D: BatchDomain, + Nf, + IvkTag: Copy + std::hash::Hash + Eq + Send + 'static, + SK: ScanningKeyOps, + Output: ShieldedOutput, + NoteCommitment, +>( + block_height: BlockHeight, + last_commitments_in_block: bool, + txid: TxId, + commitment_tree_size: u32, + keys: &HashMap, + spent_from_accounts: &HashSet, + decoded: &[(D, Output)], + batch_results: Option< + impl FnOnce(TxId) -> HashMap<(TxId, usize), DecryptedOutput>, + >, + extract_note_commitment: impl Fn(&Output) -> NoteCommitment, +) -> ( + Vec>, + Vec<(NoteCommitment, Retention)>, +) { + // Check for incoming notes while incrementing tree and witnesses + let (decrypted_opts, decrypted_len) = if let Some(collect_results) = batch_results { + let mut decrypted = collect_results(txid); + let decrypted_len = decrypted.len(); + ( + (0..decoded.len()) + .map(|i| { + decrypted + .remove(&(txid, i)) + .map(|d_out| (d_out.ivk_tag, d_out.note)) + }) + .collect::>(), + decrypted_len, + ) + } else { + let mut ivks = Vec::with_capacity(keys.len()); + let mut ivk_lookup = Vec::with_capacity(keys.len()); + for (key_id, key) in keys.iter() { + ivks.push(key.prepare()); + ivk_lookup.push(key_id); + } + + let mut decrypted_len = 0; + ( + batch::try_compact_note_decryption(&ivks, decoded) + .into_iter() + .map(|v| { + v.map(|((note, _), ivk_idx)| { + decrypted_len += 1; + (*ivk_lookup[ivk_idx], note) + }) + }) + .collect::>(), + decrypted_len, + ) + }; + + let mut shielded_outputs = Vec::with_capacity(decrypted_len); + let mut note_commitments = Vec::with_capacity(decoded.len()); + for (output_idx, ((_, output), decrypted_note)) in + decoded.iter().zip(decrypted_opts).enumerate() + { + // Collect block note commitments + let node = extract_note_commitment(output); + // If the commitment is the last in the block, ensure that is retained as a checkpoint + let is_checkpoint = output_idx + 1 == decoded.len() && last_commitments_in_block; + let retention = match (decrypted_note.is_some(), is_checkpoint) { + (is_marked, true) => Retention::Checkpoint { + id: block_height, + marking: if is_marked { + Marking::Marked + } else { + Marking::None + }, + }, + (true, false) => Retention::Marked, + (false, false) => Retention::Ephemeral, + }; + + if let Some((key_id, note)) = decrypted_note { + let key = keys + .get(&key_id) + .expect("Key is available for decrypted output"); + + // A note is marked as "change" if the account that received it + // also spent notes in the same transaction. This will catch, + // for instance: + // - Change created by spending fractions of notes. + // - Notes created by consolidation transactions. + // - Notes sent from one account to itself. + let is_change = spent_from_accounts.contains(key.account_id()); + let note_commitment_tree_position = Position::from(u64::from( + commitment_tree_size + u32::try_from(output_idx).unwrap(), + )); + let nf = key.nf(¬e, note_commitment_tree_position); + + shielded_outputs.push(WalletOutput::from_parts( + output_idx, + output.ephemeral_key(), + note, + is_change, + note_commitment_tree_position, + nf, + *key.account_id(), + key.key_scope(), + )); + } + + note_commitments.push((node, retention)) + } + + (shielded_outputs, note_commitments) +} + +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing { + use group::{ + ff::{Field, PrimeField}, + GroupEncoding, + }; + use rand_core::{OsRng, RngCore}; + use sapling::{ + constants::SPENDING_KEY_GENERATOR, + note_encryption::{sapling_note_encryption, SaplingDomain}, + util::generate_random_rseed, + value::NoteValue, + zip32::DiversifiableFullViewingKey, + Nullifier, + }; + use zcash_note_encryption::{Domain, COMPACT_NOTE_SIZE}; + use zcash_primitives::{ + block::BlockHash, transaction::components::sapling::zip212_enforcement, + }; + use zcash_protocol::{ + consensus::{BlockHeight, Network}, + memo::MemoBytes, + value::Zatoshis, + }; + + use crate::proto::compact_formats::{ + self as compact, CompactBlock, CompactSaplingOutput, CompactSaplingSpend, CompactTx, + }; + + fn random_compact_tx(mut rng: impl RngCore) -> CompactTx { + let fake_nf = { + let mut nf = vec![0; 32]; + rng.fill_bytes(&mut nf); + nf + }; + let fake_cmu = { + let fake_cmu = bls12_381::Scalar::random(&mut rng); + fake_cmu.to_repr().to_vec() + }; + let fake_epk = { + let mut buffer = [0; 64]; + rng.fill_bytes(&mut buffer); + let fake_esk = jubjub::Fr::from_bytes_wide(&buffer); + let fake_epk = SPENDING_KEY_GENERATOR * fake_esk; + fake_epk.to_bytes().to_vec() + }; + let cspend = CompactSaplingSpend { nf: fake_nf }; + let cout = CompactSaplingOutput { + cmu: fake_cmu, + ephemeral_key: fake_epk, + ciphertext: vec![0; COMPACT_NOTE_SIZE], + }; + let mut ctx = CompactTx::default(); + let mut txid = vec![0; 32]; + rng.fill_bytes(&mut txid); + ctx.hash = txid; + ctx.spends.push(cspend); + ctx.outputs.push(cout); + ctx + } + + /// Create a fake CompactBlock at the given height, with a transaction containing a + /// single spend of the given nullifier and a single output paying the given address. + /// Returns the CompactBlock. + /// + /// Set `initial_tree_sizes` to `None` to simulate a `CompactBlock` retrieved + /// from a `lightwalletd` that is not currently tracking note commitment tree sizes. + pub fn fake_compact_block( + height: BlockHeight, + prev_hash: BlockHash, + nf: Nullifier, + dfvk: &DiversifiableFullViewingKey, + value: Zatoshis, + tx_after: bool, + initial_tree_sizes: Option<(u32, u32)>, + ) -> CompactBlock { + let zip212_enforcement = zip212_enforcement(&Network::TestNetwork, height); + let to = dfvk.default_address().1; + + // Create a fake Note for the account + let mut rng = OsRng; + let rseed = generate_random_rseed(zip212_enforcement, &mut rng); + let note = sapling::Note::from_parts(to, NoteValue::from_raw(value.into()), rseed); + let encryptor = sapling_note_encryption( + Some(dfvk.fvk().ovk), + note.clone(), + *MemoBytes::empty().as_array(), + &mut rng, + ); + let cmu = note.cmu().to_bytes().to_vec(); + let ephemeral_key = SaplingDomain::epk_bytes(encryptor.epk()).0.to_vec(); + let enc_ciphertext = encryptor.encrypt_note_plaintext(); + + // Create a fake CompactBlock containing the note + let mut cb = CompactBlock { + hash: { + let mut hash = vec![0; 32]; + rng.fill_bytes(&mut hash); + hash + }, + prev_hash: prev_hash.0.to_vec(), + height: height.into(), + ..Default::default() + }; + + // Add a random Sapling tx before ours + { + let mut tx = random_compact_tx(&mut rng); + tx.index = cb.vtx.len() as u64; + cb.vtx.push(tx); + } + + let cspend = CompactSaplingSpend { nf: nf.0.to_vec() }; + let cout = CompactSaplingOutput { + cmu, + ephemeral_key, + ciphertext: enc_ciphertext[..52].to_vec(), + }; + let mut ctx = CompactTx::default(); + let mut txid = vec![0; 32]; + rng.fill_bytes(&mut txid); + ctx.hash = txid; + ctx.spends.push(cspend); + ctx.outputs.push(cout); + ctx.index = cb.vtx.len() as u64; + cb.vtx.push(ctx); + + // Optionally add another random Sapling tx after ours + if tx_after { + let mut tx = random_compact_tx(&mut rng); + tx.index = cb.vtx.len() as u64; + cb.vtx.push(tx); + } + + cb.chain_metadata = + initial_tree_sizes.map(|(initial_sapling_tree_size, initial_orchard_tree_size)| { + compact::ChainMetadata { + sapling_commitment_tree_size: initial_sapling_tree_size + + cb.vtx.iter().map(|tx| tx.outputs.len() as u32).sum::(), + orchard_commitment_tree_size: initial_orchard_tree_size + + cb.vtx.iter().map(|tx| tx.actions.len() as u32).sum::(), + } + }); + + cb + } +} + +#[cfg(test)] +mod tests { + + use std::convert::Infallible; + + use incrementalmerkletree::{Marking, Position, Retention}; + use sapling::Nullifier; + use zcash_keys::keys::UnifiedSpendingKey; + use zcash_primitives::block::BlockHash; + use zcash_protocol::{ + consensus::{BlockHeight, Network}, + value::Zatoshis, + }; + use zip32::AccountId; + + use crate::{ + data_api::BlockMetadata, + scanning::{BatchRunners, ScanningKeys}, + }; + + use super::{scan_block, scan_block_with_runners, testing::fake_compact_block, Nullifiers}; + + #[test] + fn scan_block_with_my_tx() { + fn go(scan_multithreaded: bool) { + let network = Network::TestNetwork; + let account = AccountId::ZERO; + let usk = + UnifiedSpendingKey::from_seed(&network, &[0u8; 32], account).expect("Valid USK"); + let ufvk = usk.to_unified_full_viewing_key(); + let sapling_dfvk = ufvk.sapling().expect("Sapling key is present").clone(); + let scanning_keys = ScanningKeys::from_account_ufvks([(account, ufvk)]); + + let cb = fake_compact_block( + 1u32.into(), + BlockHash([0; 32]), + Nullifier([0; 32]), + &sapling_dfvk, + Zatoshis::const_from_u64(5), + false, + None, + ); + assert_eq!(cb.vtx.len(), 2); + + let mut batch_runners = if scan_multithreaded { + let mut runners = BatchRunners::<_, (), ()>::for_keys(10, &scanning_keys); + runners + .add_block(&Network::TestNetwork, cb.clone()) + .unwrap(); + runners.flush(); + + Some(runners) + } else { + None + }; + + let scanned_block = scan_block_with_runners( + &network, + cb, + &scanning_keys, + &Nullifiers::empty(), + Some(&BlockMetadata::from_parts( + BlockHeight::from(0), + BlockHash([0u8; 32]), + Some(0), + #[cfg(feature = "orchard")] + Some(0), + )), + batch_runners.as_mut(), + ) + .unwrap(); + let txs = scanned_block.transactions(); + assert_eq!(txs.len(), 1); + + let tx = &txs[0]; + assert_eq!(tx.block_index(), 1); + assert_eq!(tx.sapling_spends().len(), 0); + assert_eq!(tx.sapling_outputs().len(), 1); + assert_eq!(tx.sapling_outputs()[0].index(), 0); + assert_eq!(tx.sapling_outputs()[0].account_id(), &account); + assert_eq!(tx.sapling_outputs()[0].note().value().inner(), 5); + assert_eq!( + tx.sapling_outputs()[0].note_commitment_tree_position(), + Position::from(1) + ); + + assert_eq!(scanned_block.sapling().final_tree_size(), 2); + assert_eq!( + scanned_block + .sapling() + .commitments() + .iter() + .map(|(_, retention)| *retention) + .collect::>(), + vec![ + Retention::Ephemeral, + Retention::Checkpoint { + id: scanned_block.height(), + marking: Marking::Marked + } + ] + ); + } + + go(false); + go(true); + } + + #[test] + fn scan_block_with_txs_after_my_tx() { + fn go(scan_multithreaded: bool) { + let network = Network::TestNetwork; + let account = AccountId::ZERO; + let usk = + UnifiedSpendingKey::from_seed(&network, &[0u8; 32], account).expect("Valid USK"); + let ufvk = usk.to_unified_full_viewing_key(); + let sapling_dfvk = ufvk.sapling().expect("Sapling key is present").clone(); + let scanning_keys = ScanningKeys::from_account_ufvks([(account, ufvk)]); + + let cb = fake_compact_block( + 1u32.into(), + BlockHash([0; 32]), + Nullifier([0; 32]), + &sapling_dfvk, + Zatoshis::const_from_u64(5), + true, + Some((0, 0)), + ); + assert_eq!(cb.vtx.len(), 3); + + let mut batch_runners = if scan_multithreaded { + let mut runners = BatchRunners::<_, (), ()>::for_keys(10, &scanning_keys); + runners + .add_block(&Network::TestNetwork, cb.clone()) + .unwrap(); + runners.flush(); + + Some(runners) + } else { + None + }; + + let scanned_block = scan_block_with_runners( + &network, + cb, + &scanning_keys, + &Nullifiers::empty(), + None, + batch_runners.as_mut(), + ) + .unwrap(); + let txs = scanned_block.transactions(); + assert_eq!(txs.len(), 1); + + let tx = &txs[0]; + assert_eq!(tx.block_index(), 1); + assert_eq!(tx.sapling_spends().len(), 0); + assert_eq!(tx.sapling_outputs().len(), 1); + assert_eq!(tx.sapling_outputs()[0].index(), 0); + assert_eq!(tx.sapling_outputs()[0].account_id(), &AccountId::ZERO); + assert_eq!(tx.sapling_outputs()[0].note().value().inner(), 5); + + assert_eq!( + scanned_block + .sapling() + .commitments() + .iter() + .map(|(_, retention)| *retention) + .collect::>(), + vec![ + Retention::Ephemeral, + Retention::Marked, + Retention::Checkpoint { + id: scanned_block.height(), + marking: Marking::None + } + ] + ); + } + + go(false); + go(true); + } + + #[test] + fn scan_block_with_my_spend() { + let network = Network::TestNetwork; + let account = AccountId::try_from(12).unwrap(); + let usk = UnifiedSpendingKey::from_seed(&network, &[0u8; 32], account).expect("Valid USK"); + let ufvk = usk.to_unified_full_viewing_key(); + let scanning_keys = ScanningKeys::::empty(); + + let nf = Nullifier([7; 32]); + let nullifiers = Nullifiers::new( + vec![(account, nf)], + #[cfg(feature = "orchard")] + vec![], + ); + + let cb = fake_compact_block( + 1u32.into(), + BlockHash([0; 32]), + nf, + ufvk.sapling().unwrap(), + Zatoshis::const_from_u64(5), + false, + Some((0, 0)), + ); + assert_eq!(cb.vtx.len(), 2); + + let scanned_block = scan_block(&network, cb, &scanning_keys, &nullifiers, None).unwrap(); + let txs = scanned_block.transactions(); + assert_eq!(txs.len(), 1); + + let tx = &txs[0]; + assert_eq!(tx.block_index(), 1); + assert_eq!(tx.sapling_spends().len(), 1); + assert_eq!(tx.sapling_outputs().len(), 0); + assert_eq!(tx.sapling_spends()[0].index(), 0); + assert_eq!(tx.sapling_spends()[0].nf(), &nf); + assert_eq!(tx.sapling_spends()[0].account_id(), &account); + + assert_eq!( + scanned_block + .sapling() + .commitments() + .iter() + .map(|(_, retention)| *retention) + .collect::>(), + vec![ + Retention::Ephemeral, + Retention::Checkpoint { + id: scanned_block.height(), + marking: Marking::None + } + ] + ); + } +} diff --git a/zcash_client_backend/src/serialization.rs b/zcash_client_backend/src/serialization.rs new file mode 100644 index 0000000000..b11d1cb24e --- /dev/null +++ b/zcash_client_backend/src/serialization.rs @@ -0,0 +1 @@ +pub mod shardtree; diff --git a/zcash_client_backend/src/serialization/shardtree.rs b/zcash_client_backend/src/serialization/shardtree.rs new file mode 100644 index 0000000000..a847d8672f --- /dev/null +++ b/zcash_client_backend/src/serialization/shardtree.rs @@ -0,0 +1,120 @@ +//! Serialization formats for data stored as SQLite BLOBs + +use byteorder::{ReadBytesExt, WriteBytesExt}; +use core::ops::Deref; +use shardtree::{Node, PrunableTree, RetentionFlags, Tree}; +use std::io::{self, Read, Write}; +use std::sync::Arc; +use zcash_encoding::Optional; +use zcash_primitives::merkle_tree::HashSer; + +const SER_V1: u8 = 1; + +const NIL_TAG: u8 = 0; +const LEAF_TAG: u8 = 1; +const PARENT_TAG: u8 = 2; + +/// Writes a [`PrunableTree`] to the provided [`Write`] instance. +/// +/// This is the primary method used for ShardTree shard persistence. It writes a version identifier +/// for the most-current serialized form, followed by the tree data. +pub fn write_shard(writer: &mut W, tree: &PrunableTree) -> io::Result<()> { + fn write_inner( + mut writer: &mut W, + tree: &PrunableTree, + ) -> io::Result<()> { + match tree.deref() { + Node::Parent { ann, left, right } => { + writer.write_u8(PARENT_TAG)?; + Optional::write(&mut writer, ann.as_ref(), |w, h| { + ::write(h, w) + })?; + write_inner(writer, left)?; + write_inner(writer, right)?; + Ok(()) + } + Node::Leaf { value } => { + writer.write_u8(LEAF_TAG)?; + value.0.write(&mut writer)?; + writer.write_u8(value.1.bits())?; + Ok(()) + } + Node::Nil => { + writer.write_u8(NIL_TAG)?; + Ok(()) + } + } + } + + writer.write_u8(SER_V1)?; + write_inner(writer, tree) +} + +fn read_shard_v1(mut reader: &mut R) -> io::Result> { + match reader.read_u8()? { + PARENT_TAG => { + let ann = Optional::read(&mut reader, ::read)?.map(Arc::new); + let left = read_shard_v1(reader)?; + let right = read_shard_v1(reader)?; + Ok(Tree::parent(ann, left, right)) + } + LEAF_TAG => { + let value = ::read(&mut reader)?; + let flags = reader.read_u8().and_then(|bits| { + RetentionFlags::from_bits(bits).ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidData, + format!( + "Byte value {} does not correspond to a valid set of retention flags", + bits + ), + ) + }) + })?; + Ok(Tree::leaf((value, flags))) + } + NIL_TAG => Ok(Tree::empty()), + other => Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("Node tag not recognized: {}", other), + )), + } +} + +/// Reads a [`PrunableTree`] from the provided [`Read`] instance. +/// +/// This function operates by first parsing a 1-byte version identifier, and then dispatching to +/// the correct deserialization function for the observed version, or returns an +/// [`io::ErrorKind::InvalidData`] error in the case that the version is not recognized. +pub fn read_shard(mut reader: R) -> io::Result> { + match reader.read_u8()? { + SER_V1 => read_shard_v1(&mut reader), + other => Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("Shard serialization version not recognized: {}", other), + )), + } +} + +#[cfg(test)] +mod tests { + use incrementalmerkletree::frontier::testing::{arb_test_node, TestNode}; + use proptest::prelude::*; + use shardtree::testing::arb_prunable_tree; + use std::io::Cursor; + + use super::{read_shard, write_shard}; + + proptest! { + #[test] + fn check_shard_roundtrip( + tree in arb_prunable_tree(arb_test_node(), 8, 32) + ) { + let mut tree_data = vec![]; + write_shard(&mut tree_data, &tree).unwrap(); + let cursor = Cursor::new(tree_data); + let tree_result = read_shard::(cursor).unwrap(); + assert_eq!(tree, tree_result); + } + } +} diff --git a/zcash_client_backend/src/sync.rs b/zcash_client_backend/src/sync.rs new file mode 100644 index 0000000000..65db2781d5 --- /dev/null +++ b/zcash_client_backend/src/sync.rs @@ -0,0 +1,626 @@ +//! Implementation of the synchronization flow described in the crate root. +//! +//! This is currently a simple implementation that does not yet implement a few features: +//! +//! - Block batches are not downloaded in parallel with scanning. +//! - Transactions are not enhanced once detected (that is, after an output is detected in +//! a transaction, the full transaction is not downloaded and scanned). +//! - There is no mechanism for notifying the caller of progress updates. +//! - There is no mechanism for interrupting the synchronization flow, other than ending +//! the process. + +use std::fmt; + +use futures_util::TryStreamExt; +use shardtree::error::ShardTreeError; +use subtle::ConditionallySelectable; +use tonic::{ + body::BoxBody, + client::GrpcService, + codegen::{Body, Bytes, StdError}, +}; +use tracing::{debug, info}; + +use zcash_keys::encoding::AddressCodec as _; +use zcash_primitives::merkle_tree::HashSer; +use zcash_protocol::consensus::{BlockHeight, Parameters}; + +use crate::{ + data_api::{ + chain::{ + error::Error as ChainError, scan_cached_blocks, BlockCache, ChainState, + CommitmentTreeRoot, + }, + scanning::{ScanPriority, ScanRange}, + WalletCommitmentTrees, WalletRead, WalletWrite, + }, + proto::service::{self, compact_tx_streamer_client::CompactTxStreamerClient, BlockId}, + scanning::ScanError, +}; + +#[cfg(feature = "orchard")] +use orchard::tree::MerkleHashOrchard; + +#[cfg(feature = "transparent-inputs")] +use { + crate::wallet::WalletTransparentOutput, + ::transparent::{ + address::Script, + bundle::{OutPoint, TxOut}, + }, + zcash_protocol::{consensus::NetworkUpgrade, value::Zatoshis}, +}; + +/// Scans the chain until the wallet is up-to-date. +pub async fn run( + client: &mut CompactTxStreamerClient, + params: &P, + db_cache: &CaT, + db_data: &mut DbT, + batch_size: u32, +) -> Result<(), Error::Error, ::Error>> +where + P: Parameters + Send + 'static, + ChT: GrpcService, + ChT::Error: Into, + ChT::ResponseBody: Body + Send + 'static, + ::Error: Into + Send, + CaT: BlockCache, + CaT::Error: std::error::Error + Send + Sync + 'static, + DbT: WalletWrite + WalletCommitmentTrees, + DbT::AccountId: ConditionallySelectable + Default + Send + 'static, + ::Error: std::error::Error + Send + Sync + 'static, + ::Error: std::error::Error + Send + Sync + 'static, +{ + #[cfg(feature = "transparent-inputs")] + let wallet_birthday = db_data + .get_wallet_birthday() + .map_err(Error::Wallet)? + .unwrap_or_else(|| params.activation_height(NetworkUpgrade::Sapling).unwrap()); + + // 1) Download note commitment tree data from lightwalletd + // 2) Pass the commitment tree data to the database. + update_subtree_roots(client, db_data).await?; + + while running( + client, + params, + db_cache, + db_data, + batch_size, + #[cfg(feature = "transparent-inputs")] + wallet_birthday, + ) + .await? + {} + + Ok(()) +} + +async fn running( + client: &mut CompactTxStreamerClient, + params: &P, + db_cache: &CaT, + db_data: &mut DbT, + batch_size: u32, + #[cfg(feature = "transparent-inputs")] wallet_birthday: BlockHeight, +) -> Result::Error, TrErr>> +where + P: Parameters + Send + 'static, + ChT: GrpcService, + ChT::Error: Into, + ChT::ResponseBody: Body + Send + 'static, + ::Error: Into + Send, + CaT: BlockCache, + CaT::Error: std::error::Error + Send + Sync + 'static, + DbT: WalletWrite, + DbT::AccountId: ConditionallySelectable + Default + Send + 'static, + DbT::Error: std::error::Error + Send + Sync + 'static, +{ + // 3) Download chain tip metadata from lightwalletd + // 4) Notify the wallet of the updated chain tip. + update_chain_tip(client, db_data).await?; + + // Refresh UTXOs for the accounts in the wallet. We do this before we perform + // any shielded scanning, to ensure that we discover any UTXOs between the old + // fully-scanned height and the current chain tip. + #[cfg(feature = "transparent-inputs")] + for account_id in db_data.get_account_ids().map_err(Error::Wallet)? { + let start_height = db_data + .block_fully_scanned() + .map_err(Error::Wallet)? + .map(|meta| meta.block_height()) + .unwrap_or(wallet_birthday); + info!( + "Refreshing UTXOs for {:?} from height {}", + account_id, start_height, + ); + refresh_utxos(params, client, db_data, account_id, start_height).await?; + } + + // 5) Get the suggested scan ranges from the wallet database + let mut scan_ranges = db_data.suggest_scan_ranges().map_err(Error::Wallet)?; + + // Store the handles to cached block deletions (which we spawn into separate + // tasks to allow us to continue downloading and scanning other ranges). + let mut block_deletions = vec![]; + + // 6) Run the following loop until the wallet's view of the chain tip as of + // the previous wallet session is valid. + loop { + // If there is a range of blocks that needs to be verified, it will always + // be returned as the first element of the vector of suggested ranges. + match scan_ranges.first() { + Some(scan_range) if scan_range.priority() == ScanPriority::Verify => { + // Download the blocks in `scan_range` into the block source, + // overwriting any existing blocks in this range. + download_blocks(client, db_cache, scan_range).await?; + + let chain_state = + download_chain_state(client, scan_range.block_range().start - 1).await?; + + // Scan the downloaded blocks and check for scanning errors that + // indicate the wallet's chain tip is out of sync with blockchain + // history. + let scan_ranges_updated = + scan_blocks(params, db_cache, db_data, &chain_state, scan_range).await?; + + // Delete the now-scanned blocks, because keeping the entire chain + // in CompactBlock files on disk is horrendous for the filesystem. + block_deletions.push(db_cache.delete(scan_range.clone())); + + if scan_ranges_updated { + // The suggested scan ranges have been updated, so we re-request. + scan_ranges = db_data.suggest_scan_ranges().map_err(Error::Wallet)?; + } else { + // At this point, the cache and scanned data are locally + // consistent (though not necessarily consistent with the + // latest chain tip - this would be discovered the next time + // this codepath is executed after new blocks are received) so + // we can break out of the loop. + break; + } + } + _ => { + // Nothing to verify; break out of the loop + break; + } + } + } + + // 7) Loop over the remaining suggested scan ranges, retrieving the requested data + // and calling `scan_cached_blocks` on each range. + let scan_ranges = db_data.suggest_scan_ranges().map_err(Error::Wallet)?; + debug!("Suggested ranges: {:?}", scan_ranges); + for scan_range in scan_ranges.into_iter().flat_map(|r| { + // Limit the number of blocks we download and scan at any one time. + (0..).scan(r, |acc, _| { + if acc.is_empty() { + None + } else if let Some((cur, next)) = acc.split_at(acc.block_range().start + batch_size) { + *acc = next; + Some(cur) + } else { + let cur = acc.clone(); + let end = acc.block_range().end; + *acc = ScanRange::from_parts(end..end, acc.priority()); + Some(cur) + } + }) + }) { + // Download the blocks in `scan_range` into the block source. + download_blocks(client, db_cache, &scan_range).await?; + + let chain_state = download_chain_state(client, scan_range.block_range().start - 1).await?; + + // Scan the downloaded blocks. + let scan_ranges_updated = + scan_blocks(params, db_cache, db_data, &chain_state, &scan_range).await?; + + // Delete the now-scanned blocks. + block_deletions.push(db_cache.delete(scan_range)); + + if scan_ranges_updated { + // The suggested scan ranges have been updated (either due to a continuity + // error or because a higher priority range has been added). + info!("Waiting for cached blocks to be deleted..."); + for deletion in block_deletions { + deletion.await.map_err(Error::Cache)?; + } + return Ok(true); + } + } + + info!("Waiting for cached blocks to be deleted..."); + for deletion in block_deletions { + deletion.await.map_err(Error::Cache)?; + } + Ok(false) +} + +async fn update_subtree_roots( + client: &mut CompactTxStreamerClient, + db_data: &mut DbT, +) -> Result<(), Error::Error>> +where + ChT: GrpcService, + ChT::Error: Into, + ChT::ResponseBody: Body + Send + 'static, + ::Error: Into + Send, + DbT: WalletCommitmentTrees, + ::Error: std::error::Error + Send + Sync + 'static, +{ + let mut request = service::GetSubtreeRootsArg::default(); + request.set_shielded_protocol(service::ShieldedProtocol::Sapling); + + let sapling_roots: Vec> = client + .get_subtree_roots(request) + .await? + .into_inner() + .and_then(|root| async move { + let root_hash = sapling::Node::read(&root.root_hash[..])?; + Ok(CommitmentTreeRoot::from_parts( + BlockHeight::from_u32(root.completing_block_height as u32), + root_hash, + )) + }) + .try_collect() + .await?; + + info!("Sapling tree has {} subtrees", sapling_roots.len()); + db_data + .put_sapling_subtree_roots(0, &sapling_roots) + .map_err(Error::WalletTrees)?; + + #[cfg(feature = "orchard")] + { + let mut request = service::GetSubtreeRootsArg::default(); + request.set_shielded_protocol(service::ShieldedProtocol::Orchard); + + let orchard_roots: Vec> = client + .get_subtree_roots(request) + .await? + .into_inner() + .and_then(|root| async move { + let root_hash = MerkleHashOrchard::read(&root.root_hash[..])?; + Ok(CommitmentTreeRoot::from_parts( + BlockHeight::from_u32(root.completing_block_height as u32), + root_hash, + )) + }) + .try_collect() + .await?; + + info!("Orchard tree has {} subtrees", orchard_roots.len()); + db_data + .put_orchard_subtree_roots(0, &orchard_roots) + .map_err(Error::WalletTrees)?; + } + + Ok(()) +} + +async fn update_chain_tip( + client: &mut CompactTxStreamerClient, + db_data: &mut DbT, +) -> Result<(), Error::Error, TrErr>> +where + ChT: GrpcService, + ChT::Error: Into, + ChT::ResponseBody: Body + Send + 'static, + ::Error: Into + Send, + DbT: WalletWrite, + DbT::Error: std::error::Error + Send + Sync + 'static, +{ + let tip_height: BlockHeight = client + .get_latest_block(service::ChainSpec::default()) + .await? + .get_ref() + .height + .try_into() + .map_err(|_| Error::MisbehavingServer)?; + + info!("Latest block height is {}", tip_height); + db_data + .update_chain_tip(tip_height) + .map_err(Error::Wallet)?; + + Ok(()) +} + +async fn download_blocks( + client: &mut CompactTxStreamerClient, + db_cache: &CaT, + scan_range: &ScanRange, +) -> Result<(), Error> +where + ChT: GrpcService, + ChT::Error: Into, + ChT::ResponseBody: Body + Send + 'static, + ::Error: Into + Send, + CaT: BlockCache, + CaT::Error: std::error::Error + Send + Sync + 'static, +{ + info!("Fetching {}", scan_range); + let mut start = service::BlockId::default(); + start.height = scan_range.block_range().start.into(); + let mut end = service::BlockId::default(); + end.height = (scan_range.block_range().end - 1).into(); + let range = service::BlockRange { + start: Some(start), + end: Some(end), + }; + let compact_blocks = client + .get_block_range(range) + .await? + .into_inner() + .try_collect::>() + .await?; + + db_cache + .insert(compact_blocks) + .await + .map_err(Error::Cache)?; + + Ok(()) +} + +async fn download_chain_state( + client: &mut CompactTxStreamerClient, + block_height: BlockHeight, +) -> Result> +where + ChT: GrpcService, + ChT::Error: Into, + ChT::ResponseBody: Body + Send + 'static, + ::Error: Into + Send, +{ + let tree_state = client + .get_tree_state(BlockId { + height: block_height.into(), + hash: vec![], + }) + .await?; + + tree_state + .into_inner() + .to_chain_state() + .map_err(|_| Error::MisbehavingServer) +} + +/// Scans the given block range and checks for scanning errors that indicate the wallet's +/// chain tip is out of sync with blockchain history. +/// +/// Returns `true` if scanning these blocks materially changed the suggested scan ranges. +async fn scan_blocks( + params: &P, + db_cache: &CaT, + db_data: &mut DbT, + initial_chain_state: &ChainState, + scan_range: &ScanRange, +) -> Result::Error, TrErr>> +where + P: Parameters + Send + 'static, + CaT: BlockCache, + CaT::Error: std::error::Error + Send + Sync + 'static, + DbT: WalletWrite, + DbT::AccountId: ConditionallySelectable + Default + Send + 'static, + DbT::Error: std::error::Error + Send + Sync + 'static, +{ + info!("Scanning {}", scan_range); + let scan_result = scan_cached_blocks( + params, + db_cache, + db_data, + scan_range.block_range().start, + initial_chain_state, + scan_range.len(), + ); + + match scan_result { + Err(ChainError::Scan(err)) if err.is_continuity_error() => { + // Pick a height to rewind to, which must be at least one block before the + // height at which the error occurred, but may be an earlier height determined + // based on heuristics such as the platform, available bandwidth, size of + // recent CompactBlocks, etc. + let rewind_height = err.at_height().saturating_sub(10); + info!( + "Chain reorg detected at {}, rewinding to {}", + err.at_height(), + rewind_height, + ); + + // Rewind to the chosen height. + db_data + .truncate_to_height(rewind_height) + .map_err(Error::Wallet)?; + + // Delete cached blocks from rewind_height onwards. + // + // This does imply that assumed-valid blocks will be re-downloaded, but it is + // also possible that in the intervening time, a chain reorg has occurred that + // orphaned some of those blocks. + db_cache + .truncate(rewind_height) + .await + .map_err(Error::Cache)?; + + // The database was truncated, invalidating prior suggested ranges. + Ok(true) + } + Ok(_) => { + // If scanning these blocks caused a suggested range to be added that has a + // higher priority than the current range, invalidate the current ranges. + let latest_ranges = db_data.suggest_scan_ranges().map_err(Error::Wallet)?; + + Ok(if let Some(range) = latest_ranges.first() { + range.priority() > scan_range.priority() + } else { + false + }) + } + Err(e) => Err(e.into()), + } +} + +/// Refreshes the given account's view of UTXOs that exist starting at the given height. +/// +/// ## Note about UTXO tracking +/// +/// (Extracted from [a comment in the Android SDK].) +/// +/// We no longer clear UTXOs here, as `WalletDb::put_received_transparent_utxo` now uses +/// an upsert instead of an insert. This means that now-spent UTXOs would previously have +/// been deleted, but now are left in the database (like shielded notes). +/// +/// Due to the fact that the `lightwalletd` query only returns _current_ UTXOs, we don't +/// learn about recently-spent UTXOs here, so the transparent balance does not get updated +/// here. +/// +/// Instead, when a received shielded note is "enhanced" by downloading the full +/// transaction, we mark any UTXOs spent in that transaction as spent in the database. +/// This relies on two current properties: +/// - UTXOs are only ever spent in shielding transactions. +/// - At least one shielded note from each shielding transaction is always enhanced. +/// +/// However, for greater reliability, we may want to alter the Data Access API to support +/// "inferring spentness" from what is _not_ returned as a UTXO, or alternatively fetch +/// TXOs from `lightwalletd` instead of just UTXOs. +/// +/// [a comment in the Android SDK]: https://github.com/Electric-Coin-Company/zcash-android-wallet-sdk/blob/855204fc8ae4057fdac939f98df4aa38c8e662f1/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/processor/CompactBlockProcessor.kt#L979-L991 +#[cfg(feature = "transparent-inputs")] +async fn refresh_utxos( + params: &P, + client: &mut CompactTxStreamerClient, + db_data: &mut DbT, + account_id: DbT::AccountId, + start_height: BlockHeight, +) -> Result<(), Error::Error, TrErr>> +where + P: Parameters + Send + 'static, + ChT: GrpcService, + ChT::Error: Into, + ChT::ResponseBody: Body + Send + 'static, + ::Error: Into + Send, + DbT: WalletWrite, + DbT::Error: std::error::Error + Send + Sync + 'static, +{ + let request = service::GetAddressUtxosArg { + addresses: db_data + .get_transparent_receivers(account_id) + .map_err(Error::Wallet)? + .into_keys() + .map(|addr| addr.encode(params)) + .collect(), + start_height: start_height.into(), + max_entries: 0, + }; + + if request.addresses.is_empty() { + info!("{:?} has no transparent receivers", account_id); + } else { + client + .get_address_utxos_stream(request) + .await? + .into_inner() + .map_err(Error::Server) + .and_then(|reply| async move { + WalletTransparentOutput::from_parts( + OutPoint::new( + reply.txid[..] + .try_into() + .map_err(|_| Error::MisbehavingServer)?, + reply + .index + .try_into() + .map_err(|_| Error::MisbehavingServer)?, + ), + TxOut { + value: Zatoshis::from_nonnegative_i64(reply.value_zat) + .map_err(|_| Error::MisbehavingServer)?, + script_pubkey: Script(reply.script), + }, + Some( + BlockHeight::try_from(reply.height) + .map_err(|_| Error::MisbehavingServer)?, + ), + ) + .ok_or(Error::MisbehavingServer) + }) + .try_for_each(|output| { + let res = db_data.put_received_transparent_utxo(&output).map(|_| ()); + async move { res.map_err(Error::Wallet) } + }) + .await?; + } + + Ok(()) +} + +/// Errors that can occur while syncing. +#[derive(Debug)] +pub enum Error { + /// An error while interacting with a [`BlockCache`]. + Cache(CaErr), + /// The lightwalletd server returned invalid information, and is misbehaving. + MisbehavingServer, + /// An error while scanning blocks. + Scan(ScanError), + /// An error while communicating with the lightwalletd server. + Server(tonic::Status), + /// An error while interacting with a wallet database via [`WalletRead`] or + /// [`WalletWrite`]. + Wallet(DbErr), + /// An error while interacting with a wallet database via [`WalletCommitmentTrees`]. + WalletTrees(ShardTreeError), +} + +impl fmt::Display for Error +where + CaErr: fmt::Display, + DbErr: fmt::Display, + TrErr: fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::Cache(e) => write!(f, "Error while interacting with block cache: {}", e), + Error::MisbehavingServer => write!(f, "lightwalletd server is misbehaving"), + Error::Scan(e) => write!(f, "Error while scanning blocks: {}", e), + Error::Server(e) => write!( + f, + "Error while communicating with lightwalletd server: {}", + e + ), + Error::Wallet(e) => write!(f, "Error while interacting with wallet database: {}", e), + Error::WalletTrees(e) => write!( + f, + "Error while interacting with wallet commitment trees: {}", + e + ), + } + } +} + +impl std::error::Error for Error +where + CaErr: std::error::Error, + DbErr: std::error::Error, + TrErr: std::error::Error, +{ +} + +impl From> for Error { + fn from(e: ChainError) -> Self { + match e { + ChainError::Wallet(e) => Error::Wallet(e), + ChainError::BlockSource(e) => Error::Cache(e), + ChainError::Scan(e) => Error::Scan(e), + } + } +} + +impl From for Error { + fn from(status: tonic::Status) -> Self { + Error::Server(status) + } +} diff --git a/zcash_client_backend/src/tor.rs b/zcash_client_backend/src/tor.rs new file mode 100644 index 0000000000..8c900ab5c7 --- /dev/null +++ b/zcash_client_backend/src/tor.rs @@ -0,0 +1,139 @@ +//! Tor support for Zcash wallets. + +use std::{fmt, io, path::Path}; + +use arti_client::{config::TorClientConfigBuilder, TorClient}; +use tor_rtcompat::PreferredRuntime; +use tracing::debug; + +#[cfg(feature = "lightwalletd-tonic-tls-webpki-roots")] +mod grpc; + +pub mod http; + +/// A Tor client that exposes capabilities designed for Zcash wallets. +#[derive(Clone)] +pub struct Client { + inner: TorClient, +} + +impl Client { + /// Creates and bootstraps a Tor client. + /// + /// The client's persistent data and cache are both stored in the given directory. + /// Preserving the contents of this directory will speed up subsequent calls to + /// `Client::create`. + /// + /// Returns an error if `tor_dir` does not exist, or if bootstrapping fails. + pub async fn create(tor_dir: &Path) -> Result { + let runtime = PreferredRuntime::current()?; + + if !tokio::fs::try_exists(tor_dir).await? { + return Err(Error::MissingTorDirectory); + } + + let config = TorClientConfigBuilder::from_directories( + tor_dir.join("arti-data"), + tor_dir.join("arti-cache"), + ) + .build() + .expect("all required fields initialized"); + + let client_builder = TorClient::with_runtime(runtime).config(config); + + debug!("Bootstrapping Tor"); + let inner = client_builder.create_bootstrapped().await?; + debug!("Tor bootstrapped"); + + Ok(Self { inner }) + } + + /// Returns a new isolated `tor::Client` handle. + /// + /// The two `tor::Client`s will share internal state and configuration, but their + /// streams will never share circuits with one another. + /// + /// Use this method when you want separate parts of your program to each have a + /// `tor::Client` handle, but where you don't want their activities to be linkable to + /// one another over the Tor network. + /// + /// Calling this method is usually preferable to creating a completely separate + /// `tor::Client` instance, since it can share its internals with the existing + /// `tor::Client`. + /// + /// (Connections made with clones of the returned `tor::Client` may share circuits + /// with each other.) + #[must_use] + pub fn isolated_client(&self) -> Self { + Self { + inner: self.inner.isolated_client(), + } + } +} + +/// Errors that can occur while creating or using a Tor [`Client`]. +#[derive(Debug)] +pub enum Error { + /// The directory passed to [`Client::create`] does not exist. + MissingTorDirectory, + #[cfg(feature = "lightwalletd-tonic-tls-webpki-roots")] + /// An error occurred while using gRPC-over-Tor. + Grpc(self::grpc::GrpcError), + /// An error occurred while using HTTP-over-Tor. + Http(self::http::HttpError), + /// An IO error occurred while interacting with the filesystem. + Io(io::Error), + /// A Tor-specific error. + Tor(arti_client::Error), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::MissingTorDirectory => write!(f, "Tor directory is missing"), + #[cfg(feature = "lightwalletd-tonic-tls-webpki-roots")] + Error::Grpc(e) => write!(f, "gRPC-over-Tor error: {}", e), + Error::Http(e) => write!(f, "HTTP-over-Tor error: {}", e), + Error::Io(e) => write!(f, "IO error: {}", e), + Error::Tor(e) => write!(f, "Tor error: {}", e), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::MissingTorDirectory => None, + #[cfg(feature = "lightwalletd-tonic-tls-webpki-roots")] + Error::Grpc(e) => Some(e), + Error::Http(e) => Some(e), + Error::Io(e) => Some(e), + Error::Tor(e) => Some(e), + } + } +} + +#[cfg(feature = "lightwalletd-tonic-tls-webpki-roots")] +impl From for Error { + fn from(e: self::grpc::GrpcError) -> Self { + Error::Grpc(e) + } +} + +impl From for Error { + fn from(e: self::http::HttpError) -> Self { + Error::Http(e) + } +} + +impl From for Error { + fn from(e: io::Error) -> Self { + Error::Io(e) + } +} + +impl From for Error { + fn from(e: arti_client::Error) -> Self { + Error::Tor(e) + } +} diff --git a/zcash_client_backend/src/tor/grpc.rs b/zcash_client_backend/src/tor/grpc.rs new file mode 100644 index 0000000000..fb8d77477c --- /dev/null +++ b/zcash_client_backend/src/tor/grpc.rs @@ -0,0 +1,106 @@ +use std::{ + fmt, + future::Future, + pin::Pin, + task::{Context, Poll}, +}; + +use arti_client::DataStream; +use hyper_util::rt::TokioIo; +use tonic::transport::{Channel, ClientTlsConfig, Endpoint, Uri}; +use tower::Service; +use tracing::debug; + +use super::{http, Client, Error}; +use crate::proto::service::compact_tx_streamer_client::CompactTxStreamerClient; + +impl Client { + /// Connects to the `lightwalletd` server at the given endpoint. + pub async fn connect_to_lightwalletd( + &self, + endpoint: Uri, + ) -> Result, Error> { + let is_https = http::url_is_https(&endpoint)?; + + let channel = Endpoint::from(endpoint); + let channel = if is_https { + channel + .tls_config(ClientTlsConfig::new().with_webpki_roots()) + .map_err(GrpcError::Tonic)? + } else { + channel + }; + + let conn = channel + .connect_with_connector(self.http_tcp_connector()) + .await + .map_err(GrpcError::Tonic)?; + + Ok(CompactTxStreamerClient::new(conn)) + } + + fn http_tcp_connector(&self) -> HttpTcpConnector { + HttpTcpConnector { + client: self.clone(), + } + } +} + +struct HttpTcpConnector { + client: Client, +} + +impl Service for HttpTcpConnector { + type Response = TokioIo; + type Error = Error; + type Future = Pin> + Send>>; + + fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, endpoint: Uri) -> Self::Future { + let parsed = http::parse_url(&endpoint); + let client = self.client.clone(); + + let fut = async move { + let (_, host, port) = parsed?; + + debug!("Connecting through Tor to {}:{}", host, port); + let stream = client.inner.connect((host.as_str(), port)).await?; + + Ok(TokioIo::new(stream)) + }; + + Box::pin(fut) + } +} + +/// Errors that can occurr while using HTTP-over-Tor. +#[derive(Debug)] +pub enum GrpcError { + /// A [`tonic`] error. + Tonic(tonic::transport::Error), +} + +impl fmt::Display for GrpcError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + GrpcError::Tonic(e) => write!(f, "Hyper error: {}", e), + } + } +} + +impl std::error::Error for GrpcError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + GrpcError::Tonic(e) => Some(e), + } + } +} + +impl From for GrpcError { + fn from(e: tonic::transport::Error) -> Self { + GrpcError::Tonic(e) + } +} diff --git a/zcash_client_backend/src/tor/http.rs b/zcash_client_backend/src/tor/http.rs new file mode 100644 index 0000000000..fb040e6ec6 --- /dev/null +++ b/zcash_client_backend/src/tor/http.rs @@ -0,0 +1,219 @@ +//! HTTP requests over Tor. + +use std::{fmt, future::Future, io, sync::Arc}; + +use futures_util::task::SpawnExt; +use http_body_util::{BodyExt, Empty}; +use hyper::{ + body::{Buf, Bytes, Incoming}, + client::conn, + http::{request::Builder, uri::Scheme}, + Request, Response, Uri, +}; +use hyper_util::rt::TokioIo; +use serde::de::DeserializeOwned; +use tokio::io::{AsyncRead, AsyncWrite}; +use tokio_rustls::{ + rustls::{ClientConfig, OwnedTrustAnchor, RootCertStore, ServerName}, + TlsConnector, +}; +use tor_rtcompat::PreferredRuntime; +use tracing::{debug, error}; + +use super::{Client, Error}; + +pub mod cryptex; + +pub(super) fn url_is_https(url: &Uri) -> Result { + Ok(url.scheme().ok_or_else(|| HttpError::NonHttpUrl)? == &Scheme::HTTPS) +} + +pub(super) fn parse_url(url: &Uri) -> Result<(bool, String, u16), Error> { + let is_https = url_is_https(url)?; + + let host = url.host().ok_or_else(|| HttpError::NonHttpUrl)?.to_string(); + + let port = match url.port_u16() { + Some(port) => port, + None if is_https => 443, + None => 80, + }; + + Ok((is_https, host, port)) +} + +impl Client { + #[tracing::instrument(skip(self, h, f))] + async fn get>>( + &self, + url: Uri, + h: impl FnOnce(Builder) -> Builder, + f: impl FnOnce(Incoming) -> F, + ) -> Result, Error> { + let (is_https, host, port) = parse_url(&url)?; + + // Connect to the server. + debug!("Connecting through Tor to {}:{}", host, port); + let stream = self.inner.connect((host.as_str(), port)).await?; + + if is_https { + // On apple-darwin targets there's an issue with the native TLS implementation + // when used over Tor circuits. We use Rustls instead. + // + // https://gitlab.torproject.org/tpo/core/arti/-/issues/715 + let mut root_store = RootCertStore::empty(); + root_store.add_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.iter().map(|root| { + OwnedTrustAnchor::from_subject_spki_name_constraints( + root.subject, + root.spki, + root.name_constraints, + ) + })); + let config = ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(root_store) + .with_no_client_auth(); + let connector = TlsConnector::from(Arc::new(config)); + let dnsname = ServerName::try_from(host.as_str()).expect("Already checked"); + let stream = connector + .connect(dnsname, stream) + .await + .map_err(HttpError::Tls)?; + make_http_request(stream, url, h, f).await + } else { + make_http_request(stream, url, h, f).await + } + } + + async fn get_json(&self, url: Uri) -> Result, Error> { + self.get( + url, + |builder| builder.header(hyper::header::ACCEPT, "application/json"), + |body| async { + Ok(serde_json::from_reader( + body.collect() + .await + .map_err(HttpError::from)? + .aggregate() + .reader(), + ) + .map_err(HttpError::from)?) + }, + ) + .await + } +} + +async fn make_http_request>>( + stream: impl AsyncRead + AsyncWrite + Unpin + Send + 'static, + url: Uri, + h: impl FnOnce(Builder) -> Builder, + f: impl FnOnce(Incoming) -> F, +) -> Result, Error> { + debug!("Making request"); + let (mut sender, connection) = conn::http1::handshake(TokioIo::new(stream)) + .await + .map_err(HttpError::from)?; + + // Spawn a task to poll the connection and drive the HTTP state. + PreferredRuntime::current()? + .spawn(async move { + if let Err(e) = connection.await { + error!("Connection failed: {}", e); + } + }) + .map_err(HttpError::from)?; + + let req = h(Request::builder() + .header( + hyper::header::HOST, + url.authority().expect("Already checked").as_str(), + ) + .uri(url)) + .body(Empty::::new()) + .map_err(HttpError::from)?; + let (parts, body) = sender + .send_request(req) + .await + .map_err(HttpError::from)? + .into_parts(); + debug!("Response status code: {}", parts.status); + + if parts.status.is_success() { + Ok(Response::from_parts(parts, f(body).await?)) + } else { + Err(Error::Http(HttpError::Unsuccessful(parts.status))) + } +} + +/// Errors that can occurr while using HTTP-over-Tor. +#[derive(Debug)] +pub enum HttpError { + /// A non-HTTP URL was encountered. + NonHttpUrl, + /// An HTTP error. + Http(hyper::http::Error), + /// A [`hyper`] error. + Hyper(hyper::Error), + /// A JSON parsing error. + Json(serde_json::Error), + /// An error occurred while spawning a background worker task for driving the HTTP + /// connection. + Spawn(futures_util::task::SpawnError), + /// A TLS-specific IO error. + Tls(io::Error), + /// The status code indicated that the request was unsuccessful. + Unsuccessful(hyper::http::StatusCode), +} + +impl fmt::Display for HttpError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + HttpError::NonHttpUrl => write!(f, "Only HTTP or HTTPS URLs are supported"), + HttpError::Http(e) => write!(f, "HTTP error: {}", e), + HttpError::Hyper(e) => write!(f, "Hyper error: {}", e), + HttpError::Json(e) => write!(f, "Failed to parse JSON: {}", e), + HttpError::Spawn(e) => write!(f, "Failed to spawn task: {}", e), + HttpError::Tls(e) => write!(f, "TLS error: {}", e), + HttpError::Unsuccessful(status) => write!(f, "Request was unsuccessful ({:?})", status), + } + } +} + +impl std::error::Error for HttpError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + HttpError::NonHttpUrl => None, + HttpError::Http(e) => Some(e), + HttpError::Hyper(e) => Some(e), + HttpError::Json(e) => Some(e), + HttpError::Spawn(e) => Some(e), + HttpError::Tls(e) => Some(e), + HttpError::Unsuccessful(_) => None, + } + } +} + +impl From for HttpError { + fn from(e: hyper::http::Error) -> Self { + HttpError::Http(e) + } +} + +impl From for HttpError { + fn from(e: hyper::Error) -> Self { + HttpError::Hyper(e) + } +} + +impl From for HttpError { + fn from(e: serde_json::Error) -> Self { + HttpError::Json(e) + } +} + +impl From for HttpError { + fn from(e: futures_util::task::SpawnError) -> Self { + HttpError::Spawn(e) + } +} diff --git a/zcash_client_backend/src/tor/http/cryptex.rs b/zcash_client_backend/src/tor/http/cryptex.rs new file mode 100644 index 0000000000..f2662418ea --- /dev/null +++ b/zcash_client_backend/src/tor/http/cryptex.rs @@ -0,0 +1,198 @@ +//! Cryptocurrency exchange rate APIs. + +use futures_util::{future::join_all, join}; +use rand::{seq::IteratorRandom, thread_rng}; +use rust_decimal::Decimal; +use tracing::{error, trace}; + +use crate::tor::{Client, Error}; + +mod binance; +mod coinbase; +mod gate_io; +mod gemini; +mod ku_coin; +mod mexc; + +/// Exchanges for which we know how to query data over Tor. +pub mod exchanges { + pub use super::binance::Binance; + pub use super::coinbase::Coinbase; + pub use super::gate_io::GateIo; + pub use super::gemini::Gemini; + pub use super::ku_coin::KuCoin; + pub use super::mexc::Mexc; +} + +/// An exchange that can be queried for ZEC data. +#[trait_variant::make(Exchange: Send)] +#[dynosaur::dynosaur(DynExchange = dyn Exchange)] +#[dynosaur::dynosaur(DynLocalExchange = dyn LocalExchange)] +pub trait LocalExchange { + /// Queries data about the USD/ZEC pair. + /// + /// The returned bid and ask data must be denominated in USD, i.e. the latest bid and + /// ask for 1 ZEC. + async fn query_zec_to_usd(&self, client: &Client) -> Result; +} + +/// Data queried from an [`Exchange`]. +#[derive(Debug)] +pub struct ExchangeData { + /// The highest current bid. + pub bid: Decimal, + + /// The lowest current ask. + pub ask: Decimal, +} + +impl ExchangeData { + /// Returns the mid-point between current best bid and current best ask, to avoid + /// manipulation by targeted trade fulfilment. + fn exchange_rate(&self) -> Decimal { + (self.bid + self.ask) / Decimal::TWO + } +} + +/// A set of [`Exchange`]s that can be queried for ZEC data. +pub struct Exchanges { + trusted: Box>, + others: Vec>>, +} + +impl Exchanges { + /// Unauthenticated connections to all known exchanges with USD/ZEC pairs. + /// + /// Gemini is treated as a "trusted" data source due to being a NYDFS-regulated + /// exchange. + pub fn unauthenticated_known_with_gemini_trusted() -> Self { + Self::builder(exchanges::Gemini::unauthenticated()) + .with(exchanges::Binance::unauthenticated()) + .with(exchanges::Coinbase::unauthenticated()) + .with(exchanges::GateIo::unauthenticated()) + .with(exchanges::KuCoin::unauthenticated()) + .with(exchanges::Mexc::unauthenticated()) + .build() + } + + /// Returns an `Exchanges` builder. + /// + /// The `trusted` exchange will always have its data used, _if_ data is successfully + /// obtained via Tor (i.e. no transient failures). + pub fn builder(trusted: impl Exchange + 'static) -> ExchangesBuilder { + ExchangesBuilder::new(trusted) + } +} + +/// Builder type for [`Exchanges`]. +/// +/// Every [`Exchanges`] is configured with a "trusted" [`Exchange`] that will always have +/// its data used, if data is successfully obtained via Tor (i.e. no transient failures). +/// Additional data sources can be provided to [`ExchangesBuilder::with`] for resiliency +/// against transient network failures or adversarial market manipulation on individual +/// sources. +/// +/// The number of times [`ExchangesBuilder::with`] is called will affect the behaviour of +/// the final [`Exchanges`]: +/// - With no additional sources, the trusted [`Exchange`] is used on its own. +/// - With one additional source, the trusted [`Exchange`] is used preferentially, +/// with the additional source as a backup if the trusted source cannot be queried. +/// - With two or more additional sources, a minimum of three successful responses are +/// required from any of the sources. +pub struct ExchangesBuilder(Exchanges); + +impl ExchangesBuilder { + /// Constructs a new [`Exchanges`] builder. + /// + /// The `trusted` exchange will always have its data used, _if_ data is successfully + /// obtained via Tor (i.e. no transient failures). + pub fn new(trusted: impl Exchange + 'static) -> Self { + Self(Exchanges { + trusted: DynExchange::boxed(trusted), + others: vec![], + }) + } + + /// Adds another [`Exchange`] as a data source. + pub fn with(mut self, other: impl Exchange + 'static) -> Self { + self.0.others.push(DynExchange::boxed(other)); + self + } + + /// Builds the [`Exchanges`]. + pub fn build(self) -> Exchanges { + self.0 + } +} + +impl Client { + /// Fetches the latest USD/ZEC exchange rate, derived from the given exchanges. + /// + /// Returns: + /// - `Ok(rate)` if at least one exchange request succeeds. + /// - `Err(_)` if none of the exchange queries succeed. + pub async fn get_latest_zec_to_usd_rate( + &self, + exchanges: &Exchanges, + ) -> Result { + // Fetch the data in parallel. + let res = join!( + exchanges.trusted.query_zec_to_usd(self), + join_all(exchanges.others.iter().map(|e| e.query_zec_to_usd(self))) + ); + trace!(?res, "Data results"); + let (trusted_res, other_res) = res; + + // Split into successful queries and errors. + let mut rates: Vec = vec![]; + let mut errors = vec![]; + for res in other_res { + match res { + Ok(d) => rates.push(d.exchange_rate()), + Err(e) => errors.push(e), + } + } + + // "Never go to sea with two chronometers; take one or three." + // Randomly drop one rate if necessary to have an odd number of rates, as long as + // we have either at least three rates, or fewer than three sources. + if exchanges.others.len() >= 2 && rates.len() + usize::from(trusted_res.is_ok()) < 3 { + error!("Too many exchange requests failed"); + return Err(errors + .into_iter() + .next() + .expect("At least one request failed")); + } + let evict_random = |s: &mut Vec| { + if let Some(index) = (0..s.len()).choose(&mut thread_rng()) { + s.remove(index); + } + }; + match trusted_res { + Ok(trusted) => { + if rates.len() % 2 != 0 { + evict_random(&mut rates); + } + rates.push(trusted.exchange_rate()); + } + Err(e) => { + if rates.len() % 2 == 0 { + evict_random(&mut rates); + } + errors.push(e); + } + } + + // If all of the requests failed, log all errors and return one of them. + if rates.is_empty() { + error!("All exchange requests failed"); + Err(errors.into_iter().next().expect("All requests failed")) + } else { + // We have an odd number of rates; take the median. + assert!(rates.len() % 2 != 0); + rates.sort(); + let median = rates.len() / 2; + Ok(rates[median]) + } + } +} diff --git a/zcash_client_backend/src/tor/http/cryptex/binance.rs b/zcash_client_backend/src/tor/http/cryptex/binance.rs new file mode 100644 index 0000000000..aa1645e82a --- /dev/null +++ b/zcash_client_backend/src/tor/http/cryptex/binance.rs @@ -0,0 +1,63 @@ +use rust_decimal::Decimal; +use serde::Deserialize; + +use super::{Exchange, ExchangeData}; +use crate::tor::{Client, Error}; + +/// Querier for the Binance exchange. +pub struct Binance { + _private: (), +} + +impl Binance { + /// Prepares for unauthenticated connections to Binance. + pub fn unauthenticated() -> Self { + Self { _private: () } + } +} + +#[derive(Clone, Debug, Deserialize)] +#[allow(dead_code)] +#[allow(non_snake_case)] +struct BinanceData { + symbol: String, + priceChange: Decimal, + priceChangePercent: Decimal, + weightedAvgPrice: Decimal, + prevClosePrice: Decimal, + lastPrice: Decimal, + lastQty: Decimal, + bidPrice: Decimal, + bidQty: Decimal, + askPrice: Decimal, + askQty: Decimal, + openPrice: Decimal, + highPrice: Decimal, + lowPrice: Decimal, + volume: Decimal, + quoteVolume: Decimal, + openTime: u64, + closeTime: u64, + firstId: u32, + lastId: u32, + count: u32, +} + +impl Exchange for Binance { + async fn query_zec_to_usd(&self, client: &Client) -> Result { + // API documentation: + // https://binance-docs.github.io/apidocs/spot/en/#24hr-ticker-price-change-statistics + let res = client + .get_json::( + "https://api.binance.com/api/v3/ticker/24hr?symbol=ZECUSDT" + .parse() + .unwrap(), + ) + .await?; + let data = res.into_body(); + Ok(ExchangeData { + bid: data.bidPrice, + ask: data.askPrice, + }) + } +} diff --git a/zcash_client_backend/src/tor/http/cryptex/coinbase.rs b/zcash_client_backend/src/tor/http/cryptex/coinbase.rs new file mode 100644 index 0000000000..190d3650eb --- /dev/null +++ b/zcash_client_backend/src/tor/http/cryptex/coinbase.rs @@ -0,0 +1,51 @@ +use rust_decimal::Decimal; +use serde::Deserialize; + +use super::{Exchange, ExchangeData}; +use crate::tor::{Client, Error}; + +/// Querier for the Coinbase exchange. +pub struct Coinbase { + _private: (), +} + +impl Coinbase { + /// Prepares for unauthenticated connections to Coinbase. + pub fn unauthenticated() -> Self { + Self { _private: () } + } +} + +#[derive(Debug, Deserialize)] +#[allow(dead_code)] +struct CoinbaseData { + ask: Decimal, + bid: Decimal, + volume: Decimal, + trade_id: u32, + price: Decimal, + size: Decimal, + time: String, + rfq_volume: Option, + conversions_volume: Option, +} + +impl Exchange for Coinbase { + #[allow(dead_code)] + async fn query_zec_to_usd(&self, client: &Client) -> Result { + // API documentation: + // https://docs.cdp.coinbase.com/exchange/reference/exchangerestapi_getproductticker + let res = client + .get_json::( + "https://api.exchange.coinbase.com/products/ZEC-USD/ticker" + .parse() + .unwrap(), + ) + .await?; + let data = res.into_body(); + Ok(ExchangeData { + bid: data.bid, + ask: data.ask, + }) + } +} diff --git a/zcash_client_backend/src/tor/http/cryptex/gate_io.rs b/zcash_client_backend/src/tor/http/cryptex/gate_io.rs new file mode 100644 index 0000000000..382517de12 --- /dev/null +++ b/zcash_client_backend/src/tor/http/cryptex/gate_io.rs @@ -0,0 +1,54 @@ +use hyper::StatusCode; +use rust_decimal::Decimal; +use serde::Deserialize; + +use super::{Exchange, ExchangeData}; +use crate::tor::{Client, Error}; + +/// Querier for the Gate.io exchange. +pub struct GateIo { + _private: (), +} + +impl GateIo { + /// Prepares for unauthenticated connections to Gate.io. + pub fn unauthenticated() -> Self { + Self { _private: () } + } +} + +#[derive(Debug, Deserialize)] +#[allow(dead_code)] +struct GateIoData { + currency_pair: String, + last: Decimal, + lowest_ask: Decimal, + highest_bid: Decimal, + change_percentage: Decimal, + base_volume: Decimal, + quote_volume: Decimal, + high_24h: Decimal, + low_24h: Decimal, +} + +impl Exchange for GateIo { + async fn query_zec_to_usd(&self, client: &Client) -> Result { + // API documentation: + // https://www.gate.io/docs/developers/apiv4/#retrieve-ticker-information + let res = client + .get_json::>( + "https://api.gateio.ws/api/v4/spot/tickers?currency_pair=ZEC_USDT" + .parse() + .unwrap(), + ) + .await?; + let data = res.into_body().into_iter().next().ok_or(Error::Http( + super::super::HttpError::Unsuccessful(StatusCode::GONE), + ))?; + + Ok(ExchangeData { + bid: data.highest_bid, + ask: data.lowest_ask, + }) + } +} diff --git a/zcash_client_backend/src/tor/http/cryptex/gemini.rs b/zcash_client_backend/src/tor/http/cryptex/gemini.rs new file mode 100644 index 0000000000..856634b21e --- /dev/null +++ b/zcash_client_backend/src/tor/http/cryptex/gemini.rs @@ -0,0 +1,45 @@ +use rust_decimal::Decimal; +use serde::Deserialize; + +use super::{Exchange, ExchangeData}; +use crate::tor::{Client, Error}; + +/// Querier for the Gemini exchange. +pub struct Gemini { + _private: (), +} + +impl Gemini { + /// Prepares for unauthenticated connections to Gemini. + pub fn unauthenticated() -> Self { + Self { _private: () } + } +} + +#[derive(Debug, Deserialize)] +#[allow(dead_code)] +struct GeminiData { + symbol: String, + open: Decimal, + high: Decimal, + low: Decimal, + close: Decimal, + changes: Vec, + bid: Decimal, + ask: Decimal, +} + +impl Exchange for Gemini { + async fn query_zec_to_usd(&self, client: &Client) -> Result { + // API documentation: + // https://docs.gemini.com/rest-api/#ticker-v2 + let res = client + .get_json::("https://api.gemini.com/v2/ticker/zecusd".parse().unwrap()) + .await?; + let data = res.into_body(); + Ok(ExchangeData { + bid: data.bid, + ask: data.ask, + }) + } +} diff --git a/zcash_client_backend/src/tor/http/cryptex/ku_coin.rs b/zcash_client_backend/src/tor/http/cryptex/ku_coin.rs new file mode 100644 index 0000000000..3301367050 --- /dev/null +++ b/zcash_client_backend/src/tor/http/cryptex/ku_coin.rs @@ -0,0 +1,65 @@ +use rust_decimal::Decimal; +use serde::Deserialize; + +use super::{Exchange, ExchangeData}; +use crate::tor::{Client, Error}; + +/// Querier for the KuCoin exchange. +pub struct KuCoin { + _private: (), +} + +impl KuCoin { + /// Prepares for unauthenticated connections to KuCoin. + pub fn unauthenticated() -> Self { + Self { _private: () } + } +} + +#[derive(Debug, Deserialize)] +#[allow(dead_code)] +#[allow(non_snake_case)] +struct KuCoinData { + time: u64, + symbol: String, + buy: Decimal, + sell: Decimal, + changeRate: Decimal, + changePrice: Decimal, + high: Decimal, + low: Decimal, + vol: Decimal, + volValue: Decimal, + last: Decimal, + averagePrice: Decimal, + takerFeeRate: Decimal, + makerFeeRate: Decimal, + takerCoefficient: Decimal, + makerCoefficient: Decimal, +} + +#[derive(Debug, Deserialize)] +#[allow(dead_code)] +struct KuCoinResponse { + code: String, + data: KuCoinData, +} + +impl Exchange for KuCoin { + async fn query_zec_to_usd(&self, client: &Client) -> Result { + // API documentation: + // https://www.kucoin.com/docs/rest/spot-trading/market-data/get-24hr-stats + let res = client + .get_json::( + "https://api.kucoin.com/api/v1/market/stats?symbol=ZEC-USDT" + .parse() + .unwrap(), + ) + .await?; + let data = res.into_body().data; + Ok(ExchangeData { + bid: data.buy, + ask: data.sell, + }) + } +} diff --git a/zcash_client_backend/src/tor/http/cryptex/mexc.rs b/zcash_client_backend/src/tor/http/cryptex/mexc.rs new file mode 100644 index 0000000000..278fec6939 --- /dev/null +++ b/zcash_client_backend/src/tor/http/cryptex/mexc.rs @@ -0,0 +1,58 @@ +use rust_decimal::Decimal; +use serde::Deserialize; + +use super::{Exchange, ExchangeData}; +use crate::tor::{Client, Error}; + +/// Querier for the MEXC exchange. +pub struct Mexc { + _private: (), +} + +impl Mexc { + /// Prepares for unauthenticated connections to MEXC. + pub fn unauthenticated() -> Self { + Self { _private: () } + } +} + +#[derive(Debug, Deserialize)] +#[allow(dead_code)] +#[allow(non_snake_case)] +struct MexcData { + symbol: String, + priceChange: Decimal, + priceChangePercent: Decimal, + prevClosePrice: Decimal, + lastPrice: Decimal, + bidPrice: Decimal, + bidQty: Decimal, + askPrice: Decimal, + askQty: Decimal, + openPrice: Decimal, + highPrice: Decimal, + lowPrice: Decimal, + volume: Decimal, + quoteVolume: Decimal, + openTime: u64, + closeTime: u64, +} + +impl Exchange for Mexc { + async fn query_zec_to_usd(&self, client: &Client) -> Result { + // API documentation: + // https://mexcdevelop.github.io/apidocs/spot_v3_en/#24hr-ticker-price-change-statistics + let res = client + .get_json::( + "https://api.mexc.com/api/v3/ticker/24hr?symbol=ZECUSDT" + .parse() + .unwrap(), + ) + .await?; + let data = res.into_body(); + Ok(ExchangeData { + bid: data.bidPrice, + ask: data.askPrice, + }) + } +} diff --git a/zcash_client_backend/src/wallet.rs b/zcash_client_backend/src/wallet.rs index 220aaf6e3d..f298b6fdb7 100644 --- a/zcash_client_backend/src/wallet.rs +++ b/zcash_client_backend/src/wallet.rs @@ -1,75 +1,511 @@ //! Structs representing transaction data scanned from the block chain by a wallet or //! light client. -use subtle::{Choice, ConditionallySelectable}; +use incrementalmerkletree::Position; -use zcash_primitives::{ - merkle_tree::IncrementalWitness, - sapling::{ - keys::OutgoingViewingKey, Diversifier, Node, Note, Nullifier, PaymentAddress, Rseed, - }, - transaction::{components::Amount, TxId}, +use ::transparent::{ + address::TransparentAddress, + bundle::{OutPoint, TxOut}, +}; +use zcash_address::ZcashAddress; +use zcash_note_encryption::EphemeralKeyBytes; +use zcash_primitives::transaction::{fees::transparent as transparent_fees, TxId}; +use zcash_protocol::{ + consensus::BlockHeight, + value::{BalanceError, Zatoshis}, + PoolType, ShieldedProtocol, }; +use zip32::Scope; -/// A type-safe wrapper for account identifiers. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub struct AccountId(pub u32); +use crate::fees::sapling as sapling_fees; -impl Default for AccountId { - fn default() -> Self { - AccountId(0) - } +#[cfg(feature = "orchard")] +use crate::fees::orchard as orchard_fees; + +#[cfg(feature = "transparent-inputs")] +use ::transparent::keys::{NonHardenedChildIndex, TransparentKeyScope}; + +/// A unique identifier for a shielded transaction output +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct NoteId { + txid: TxId, + protocol: ShieldedProtocol, + output_index: u16, } -impl ConditionallySelectable for AccountId { - fn conditional_select(a0: &Self, a1: &Self, c: Choice) -> Self { - AccountId(u32::conditional_select(&a0.0, &a1.0, c)) +impl NoteId { + /// Constructs a new `NoteId` from its parts. + pub fn new(txid: TxId, protocol: ShieldedProtocol, output_index: u16) -> Self { + Self { + txid, + protocol, + output_index, + } + } + + /// Returns the ID of the transaction containing this note. + pub fn txid(&self) -> &TxId { + &self.txid + } + + /// Returns the shielded protocol used by this note. + pub fn protocol(&self) -> ShieldedProtocol { + self.protocol + } + + /// Returns the index of this note within its transaction's corresponding list of + /// shielded outputs. + pub fn output_index(&self) -> u16 { + self.output_index } } -/// A subset of a [`Transaction`] relevant to wallets and light clients. +/// A type that represents the recipient of a transaction output: /// -/// [`Transaction`]: zcash_primitives::transaction::Transaction -pub struct WalletTx { - pub txid: TxId, - pub index: usize, - pub num_spends: usize, - pub num_outputs: usize, - pub shielded_spends: Vec, - pub shielded_outputs: Vec>, +/// * a recipient address; +/// * for external unified addresses, the pool to which the payment is sent; +/// * for wallet-internal outputs, the internal account ID and metadata about the note. +/// * if the `transparent-inputs` feature is enabled, for ephemeral transparent outputs, the +/// internal account ID and metadata about the outpoint; +#[derive(Debug, Clone)] +pub enum Recipient { + External { + recipient_address: ZcashAddress, + output_pool: PoolType, + }, + #[cfg(feature = "transparent-inputs")] + EphemeralTransparent { + receiving_account: AccountId, + ephemeral_address: TransparentAddress, + outpoint: OutPoint, + }, + InternalAccount { + receiving_account: AccountId, + external_address: Option, + note: Box, + }, } -/// A subset of a [`SpendDescription`] relevant to wallets and light clients. +/// The shielded subset of a [`Transaction`]'s data that is relevant to a particular wallet. /// -/// [`SpendDescription`]: zcash_primitives::transaction::components::SpendDescription -pub struct WalletShieldedSpend { - pub index: usize, - pub nf: Nullifier, - pub account: AccountId, +/// [`Transaction`]: zcash_primitives::transaction::Transaction +pub struct WalletTx { + txid: TxId, + block_index: usize, + sapling_spends: Vec>, + sapling_outputs: Vec>, + #[cfg(feature = "orchard")] + orchard_spends: Vec>, + #[cfg(feature = "orchard")] + orchard_outputs: Vec>, +} + +impl WalletTx { + /// Constructs a new [`WalletTx`] from its constituent parts. + pub fn new( + txid: TxId, + block_index: usize, + sapling_spends: Vec>, + sapling_outputs: Vec>, + #[cfg(feature = "orchard")] orchard_spends: Vec< + WalletSpend, + >, + #[cfg(feature = "orchard")] orchard_outputs: Vec>, + ) -> Self { + Self { + txid, + block_index, + sapling_spends, + sapling_outputs, + #[cfg(feature = "orchard")] + orchard_spends, + #[cfg(feature = "orchard")] + orchard_outputs, + } + } + + /// Returns the [`TxId`] for the corresponding [`Transaction`]. + /// + /// [`Transaction`]: zcash_primitives::transaction::Transaction + pub fn txid(&self) -> TxId { + self.txid + } + + /// Returns the index of the transaction in the containing block. + pub fn block_index(&self) -> usize { + self.block_index + } + + /// Returns a record for each Sapling note belonging to the wallet that was spent in the + /// transaction. + pub fn sapling_spends(&self) -> &[WalletSaplingSpend] { + self.sapling_spends.as_ref() + } + + /// Returns a record for each Sapling note received or produced by the wallet in the + /// transaction. + pub fn sapling_outputs(&self) -> &[WalletSaplingOutput] { + self.sapling_outputs.as_ref() + } + + /// Returns a record for each Orchard note belonging to the wallet that was spent in the + /// transaction. + #[cfg(feature = "orchard")] + pub fn orchard_spends(&self) -> &[WalletOrchardSpend] { + self.orchard_spends.as_ref() + } + + /// Returns a record for each Orchard note received or produced by the wallet in the + /// transaction. + #[cfg(feature = "orchard")] + pub fn orchard_outputs(&self) -> &[WalletOrchardOutput] { + self.orchard_outputs.as_ref() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WalletTransparentOutput { + outpoint: OutPoint, + txout: TxOut, + mined_height: Option, + recipient_address: TransparentAddress, +} + +impl WalletTransparentOutput { + pub fn from_parts( + outpoint: OutPoint, + txout: TxOut, + mined_height: Option, + ) -> Option { + txout + .recipient_address() + .map(|recipient_address| WalletTransparentOutput { + outpoint, + txout, + mined_height, + recipient_address, + }) + } + + pub fn outpoint(&self) -> &OutPoint { + &self.outpoint + } + + pub fn txout(&self) -> &TxOut { + &self.txout + } + + pub fn mined_height(&self) -> Option { + self.mined_height + } + + pub fn recipient_address(&self) -> &TransparentAddress { + &self.recipient_address + } + + pub fn value(&self) -> Zatoshis { + self.txout.value + } +} + +impl transparent_fees::InputView for WalletTransparentOutput { + fn outpoint(&self) -> &OutPoint { + &self.outpoint + } + fn coin(&self) -> &TxOut { + &self.txout + } +} + +/// A reference to a spent note belonging to the wallet within a transaction. +pub struct WalletSpend { + index: usize, + nf: Nf, + account_id: AccountId, +} + +impl WalletSpend { + /// Constructs a `WalletSpend` from its constituent parts. + pub fn from_parts(index: usize, nf: Nf, account_id: AccountId) -> Self { + Self { + index, + nf, + account_id, + } + } + + /// Returns the index of the Sapling spend or Orchard action within the transaction that + /// created this spend. + pub fn index(&self) -> usize { + self.index + } + /// Returns the nullifier of the spent note. + pub fn nf(&self) -> &Nf { + &self.nf + } + /// Returns the identifier to the account_id to which the note belonged. + pub fn account_id(&self) -> &AccountId { + &self.account_id + } +} + +/// A type alias for Sapling [`WalletSpend`]s. +pub type WalletSaplingSpend = WalletSpend; + +/// A type alias for Orchard [`WalletSpend`]s. +#[cfg(feature = "orchard")] +pub type WalletOrchardSpend = WalletSpend; + +/// An output that was successfully decrypted in the process of wallet scanning. +pub struct WalletOutput { + index: usize, + ephemeral_key: EphemeralKeyBytes, + note: Note, + is_change: bool, + note_commitment_tree_position: Position, + nf: Option, + account_id: AccountId, + recipient_key_scope: Option, +} + +impl WalletOutput { + /// Constructs a new `WalletOutput` value from its constituent parts. + #[allow(clippy::too_many_arguments)] + pub fn from_parts( + index: usize, + ephemeral_key: EphemeralKeyBytes, + note: Note, + is_change: bool, + note_commitment_tree_position: Position, + nf: Option, + account_id: AccountId, + recipient_key_scope: Option, + ) -> Self { + Self { + index, + ephemeral_key, + note, + is_change, + note_commitment_tree_position, + nf, + account_id, + recipient_key_scope, + } + } + + /// The index of the output or action in the transaction that created this output. + pub fn index(&self) -> usize { + self.index + } + /// The [`EphemeralKeyBytes`] used in the decryption of the note. + pub fn ephemeral_key(&self) -> &EphemeralKeyBytes { + &self.ephemeral_key + } + /// The note. + pub fn note(&self) -> &Note { + &self.note + } + /// A flag indicating whether the process of note decryption determined that this + /// output should be classified as change. + pub fn is_change(&self) -> bool { + self.is_change + } + /// The position of the note in the global note commitment tree. + pub fn note_commitment_tree_position(&self) -> Position { + self.note_commitment_tree_position + } + /// The nullifier for the note, if the key used to decrypt the note was able to compute it. + pub fn nf(&self) -> Option<&Nullifier> { + self.nf.as_ref() + } + /// The identifier for the account to which the output belongs. + pub fn account_id(&self) -> &AccountId { + &self.account_id + } + /// The ZIP 32 scope for which the viewing key that decrypted this output was derived, if + /// known. + pub fn recipient_key_scope(&self) -> Option { + self.recipient_key_scope + } } /// A subset of an [`OutputDescription`] relevant to wallets and light clients. /// -/// [`OutputDescription`]: zcash_primitives::transaction::components::OutputDescription -pub struct WalletShieldedOutput { - pub index: usize, - pub cmu: bls12_381::Scalar, - pub epk: jubjub::ExtendedPoint, - pub account: AccountId, - pub note: Note, - pub to: PaymentAddress, - pub is_change: bool, - pub witness: IncrementalWitness, - pub nf: N, +/// [`OutputDescription`]: sapling::bundle::OutputDescription +pub type WalletSaplingOutput = + WalletOutput; + +/// The output part of an Orchard [`Action`] that was decrypted in the process of scanning. +/// +/// [`Action`]: orchard::Action +#[cfg(feature = "orchard")] +pub type WalletOrchardOutput = + WalletOutput; + +/// An enumeration of supported shielded note types for use in [`ReceivedNote`] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Note { + Sapling(sapling::Note), + #[cfg(feature = "orchard")] + Orchard(orchard::Note), +} + +impl Note { + pub fn value(&self) -> Zatoshis { + match self { + Note::Sapling(n) => n.value().inner().try_into().expect( + "Sapling notes must have values in the range of valid non-negative ZEC values.", + ), + #[cfg(feature = "orchard")] + Note::Orchard(n) => Zatoshis::from_u64(n.value().inner()).expect( + "Orchard notes must have values in the range of valid non-negative ZEC values.", + ), + } + } + + /// Returns the shielded protocol used by this note. + pub fn protocol(&self) -> ShieldedProtocol { + match self { + Note::Sapling(_) => ShieldedProtocol::Sapling, + #[cfg(feature = "orchard")] + Note::Orchard(_) => ShieldedProtocol::Orchard, + } + } } /// Information about a note that is tracked by the wallet that is available for spending, /// with sufficient information for use in note selection. -pub struct SpendableNote { - pub diversifier: Diversifier, - pub note_value: Amount, - pub rseed: Rseed, - pub witness: IncrementalWitness, +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ReceivedNote { + note_id: NoteRef, + txid: TxId, + output_index: u16, + note: NoteT, + spending_key_scope: Scope, + note_commitment_tree_position: Position, +} + +impl ReceivedNote { + pub fn from_parts( + note_id: NoteRef, + txid: TxId, + output_index: u16, + note: NoteT, + spending_key_scope: Scope, + note_commitment_tree_position: Position, + ) -> Self { + ReceivedNote { + note_id, + txid, + output_index, + note, + spending_key_scope, + note_commitment_tree_position, + } + } + + pub fn internal_note_id(&self) -> &NoteRef { + &self.note_id + } + pub fn txid(&self) -> &TxId { + &self.txid + } + pub fn output_index(&self) -> u16 { + self.output_index + } + pub fn note(&self) -> &NoteT { + &self.note + } + pub fn spending_key_scope(&self) -> Scope { + self.spending_key_scope + } + pub fn note_commitment_tree_position(&self) -> Position { + self.note_commitment_tree_position + } + + /// Map over the `note` field of this data structure. + /// + /// Consume this value, applying the provided function to the value of its `note` field and + /// returning a new `ReceivedNote` with the result as its `note` field value. + pub fn map_note N>(self, f: F) -> ReceivedNote { + ReceivedNote { + note_id: self.note_id, + txid: self.txid, + output_index: self.output_index, + note: f(self.note), + spending_key_scope: self.spending_key_scope, + note_commitment_tree_position: self.note_commitment_tree_position, + } + } +} + +impl ReceivedNote { + pub fn note_value(&self) -> Result { + self.note.value().inner().try_into() + } +} + +#[cfg(feature = "orchard")] +impl ReceivedNote { + pub fn note_value(&self) -> Result { + self.note.value().inner().try_into() + } +} + +impl sapling_fees::InputView for (NoteRef, sapling::value::NoteValue) { + fn note_id(&self) -> &NoteRef { + &self.0 + } + + fn value(&self) -> Zatoshis { + self.1 + .inner() + .try_into() + .expect("Sapling note values are indirectly checked by consensus.") + } +} + +impl sapling_fees::InputView for ReceivedNote { + fn note_id(&self) -> &NoteRef { + &self.note_id + } + + fn value(&self) -> Zatoshis { + self.note + .value() + .inner() + .try_into() + .expect("Sapling note values are indirectly checked by consensus.") + } +} + +#[cfg(feature = "orchard")] +impl orchard_fees::InputView for (NoteRef, orchard::value::NoteValue) { + fn note_id(&self) -> &NoteRef { + &self.0 + } + + fn value(&self) -> Zatoshis { + self.1 + .inner() + .try_into() + .expect("Orchard note values are indirectly checked by consensus.") + } +} + +#[cfg(feature = "orchard")] +impl orchard_fees::InputView for ReceivedNote { + fn note_id(&self) -> &NoteRef { + &self.note_id + } + + fn value(&self) -> Zatoshis { + self.note + .value() + .inner() + .try_into() + .expect("Orchard note values are indirectly checked by consensus.") + } } /// Describes a policy for which outgoing viewing key should be able to decrypt @@ -79,23 +515,70 @@ pub struct SpendableNote { /// viewing key, refer to [ZIP 310]. /// /// [ZIP 310]: https://zips.z.cash/zip-0310 +#[derive(Debug, Clone)] pub enum OvkPolicy { - /// Use the outgoing viewing key from the sender's [`ExtendedFullViewingKey`]. + /// Use the outgoing viewing key from the sender's [`UnifiedFullViewingKey`]. /// /// Transaction outputs will be decryptable by the sender, in addition to the /// recipients. /// - /// [`ExtendedFullViewingKey`]: zcash_primitives::zip32::ExtendedFullViewingKey + /// [`UnifiedFullViewingKey`]: zcash_keys::keys::UnifiedFullViewingKey Sender, - /// Use a custom outgoing viewing key. This might for instance be derived from a - /// separate seed than the wallet's spending keys. + /// Use custom outgoing viewing keys. These might for instance be derived from a + /// different seed than the wallet's spending keys. /// /// Transaction outputs will be decryptable by the recipients, and whoever controls - /// the provided outgoing viewing key. - Custom(OutgoingViewingKey), - - /// Use no outgoing viewing key. Transaction outputs will be decryptable by their + /// the provided outgoing viewing keys. + Custom { + sapling: sapling::keys::OutgoingViewingKey, + #[cfg(feature = "orchard")] + orchard: orchard::keys::OutgoingViewingKey, + }, + /// Use no outgoing viewing keys. Transaction outputs will be decryptable by their /// recipients, but not by the sender. Discard, } + +impl OvkPolicy { + /// Constructs an [`OvkPolicy::Custom`] value from a single arbitrary 32-byte key. + /// + /// Outputs of transactions created with this OVK policy will be recoverable using + /// this key irrespective of the output pool. + pub fn custom_from_common_bytes(key: &[u8; 32]) -> Self { + OvkPolicy::Custom { + sapling: sapling::keys::OutgoingViewingKey(*key), + #[cfg(feature = "orchard")] + orchard: orchard::keys::OutgoingViewingKey::from(*key), + } + } +} + +/// Metadata related to the ZIP 32 derivation of a transparent address. +/// This is implicitly scoped to an account. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg(feature = "transparent-inputs")] +pub struct TransparentAddressMetadata { + scope: TransparentKeyScope, + address_index: NonHardenedChildIndex, +} + +#[cfg(feature = "transparent-inputs")] +impl TransparentAddressMetadata { + /// Returns a `TransparentAddressMetadata` in the given scope for the + /// given address index. + pub fn new(scope: TransparentKeyScope, address_index: NonHardenedChildIndex) -> Self { + Self { + scope, + address_index, + } + } + + pub fn scope(&self) -> TransparentKeyScope { + self.scope + } + + pub fn address_index(&self) -> NonHardenedChildIndex { + self.address_index + } +} diff --git a/zcash_client_backend/src/welding_rig.rs b/zcash_client_backend/src/welding_rig.rs deleted file mode 100644 index b48a4eec9d..0000000000 --- a/zcash_client_backend/src/welding_rig.rs +++ /dev/null @@ -1,526 +0,0 @@ -//! Tools for scanning a compact representation of the Zcash block chain. - -use ff::PrimeField; -use std::collections::HashSet; -use std::convert::TryFrom; -use subtle::{ConditionallySelectable, ConstantTimeEq, CtOption}; -use zcash_note_encryption::ShieldedOutput; -use zcash_primitives::{ - consensus::{self, BlockHeight}, - merkle_tree::{CommitmentTree, IncrementalWitness}, - sapling::{ - note_encryption::{try_sapling_compact_note_decryption, SaplingDomain}, - Node, Note, Nullifier, PaymentAddress, SaplingIvk, - }, - transaction::{components::sapling::CompactOutputDescription, TxId}, - zip32::ExtendedFullViewingKey, -}; - -use crate::proto::compact_formats::{CompactBlock, CompactOutput}; -use crate::wallet::{AccountId, WalletShieldedOutput, WalletShieldedSpend, WalletTx}; - -/// Scans a [`CompactOutput`] with a set of [`ScanningKey`]s. -/// -/// Returns a [`WalletShieldedOutput`] and corresponding [`IncrementalWitness`] if this -/// output belongs to any of the given [`ScanningKey`]s. -/// -/// The given [`CommitmentTree`] and existing [`IncrementalWitness`]es are incremented -/// with this output's commitment. -/// -/// [`ScanningKey`]: crate::welding_rig::ScanningKey -#[allow(clippy::too_many_arguments)] -fn scan_output( - params: &P, - height: BlockHeight, - (index, output): (usize, CompactOutput), - vks: &[(&AccountId, &K)], - spent_from_accounts: &HashSet, - tree: &mut CommitmentTree, - existing_witnesses: &mut [&mut IncrementalWitness], - block_witnesses: &mut [&mut IncrementalWitness], - new_witnesses: &mut [&mut IncrementalWitness], -) -> Option> { - let output = CompactOutputDescription::try_from(output).ok()?; - - // Increment tree and witnesses - let node = Node::new(output.cmu.to_repr()); - for witness in existing_witnesses { - witness.append(node).unwrap(); - } - for witness in block_witnesses { - witness.append(node).unwrap(); - } - for witness in new_witnesses { - witness.append(node).unwrap(); - } - tree.append(node).unwrap(); - - for (account, vk) in vks.iter() { - let (note, to) = match vk.try_decryption(params, height, &output) { - Some(ret) => ret, - None => continue, - }; - - // A note is marked as "change" if the account that received it - // also spent notes in the same transaction. This will catch, - // for instance: - // - Change created by spending fractions of notes. - // - Notes created by consolidation transactions. - // - Notes sent from one account to itself. - let is_change = spent_from_accounts.contains(&account); - - let witness = IncrementalWitness::from_tree(tree); - let nf = vk.nf(¬e, &witness); - - return Some(WalletShieldedOutput { - index, - cmu: output.cmu, - epk: output.epk, - account: **account, - note, - to, - is_change, - witness, - nf, - }); - } - - None -} - -/// A key that can be used to perform trial decryption and nullifier -/// computation for a Sapling [`CompactOutput`] -/// -/// The purpose of this trait is to enable [`scan_block`] -/// and related methods to be used with either incoming viewing keys -/// or full viewing keys, with the data returned from trial decryption -/// being dependent upon the type of key used. In the case that an -/// incoming viewing key is used, only the note and payment address -/// will be returned; in the case of a full viewing key, the -/// nullifier for the note can also be obtained. -/// -/// [`CompactOutput`]: crate::proto::compact_formats::CompactOutput -/// [`scan_block`]: crate::welding_rig::scan_block -pub trait ScanningKey { - /// The type of nullifier extracted when a note is successfully - /// obtained by trial decryption. - type Nf; - - /// Attempts to decrypt a Sapling note and payment address - /// from the specified ciphertext using this scanning key. - fn try_decryption>>( - &self, - params: &P, - height: BlockHeight, - output: &Output, - ) -> Option<(Note, PaymentAddress)>; - - /// Produces the nullifier for the specified note and witness, if possible. - /// - /// IVK-based implementations of this trait cannot successfully derive - /// nullifiers, in which case `Self::Nf` should be set to the unit type - /// and this function is a no-op. - fn nf(&self, note: &Note, witness: &IncrementalWitness) -> Self::Nf; -} - -/// The [`ScanningKey`] implementation for [`ExtendedFullViewingKey`]s. -/// Nullifiers may be derived when scanning with these keys. -/// -/// [`ExtendedFullViewingKey`]: zcash_primitives::zip32::ExtendedFullViewingKey -impl ScanningKey for ExtendedFullViewingKey { - type Nf = Nullifier; - - fn try_decryption>>( - &self, - params: &P, - height: BlockHeight, - output: &Output, - ) -> Option<(Note, PaymentAddress)> { - try_sapling_compact_note_decryption(params, height, &self.fvk.vk.ivk(), output) - } - - fn nf(&self, note: &Note, witness: &IncrementalWitness) -> Self::Nf { - note.nf(&self.fvk.vk, witness.position() as u64) - } -} - -/// The [`ScanningKey`] implementation for [`SaplingIvk`]s. -/// Nullifiers cannot be derived when scanning with these keys. -/// -/// [`SaplingIvk`]: zcash_primitives::sapling::SaplingIvk -impl ScanningKey for SaplingIvk { - type Nf = (); - - fn try_decryption>>( - &self, - params: &P, - height: BlockHeight, - output: &Output, - ) -> Option<(Note, PaymentAddress)> { - try_sapling_compact_note_decryption(params, height, self, output) - } - - fn nf(&self, _note: &Note, _witness: &IncrementalWitness) {} -} - -/// Scans a [`CompactBlock`] with a set of [`ScanningKey`]s. -/// -/// Returns a vector of [`WalletTx`]s belonging to any of the given -/// [`ScanningKey`]s. If scanning with a full viewing key, the nullifiers -/// of the resulting [`WalletShieldedOutput`]s will also be computed. -/// -/// The given [`CommitmentTree`] and existing [`IncrementalWitness`]es are -/// incremented appropriately. -/// -/// The implementation of [`ScanningKey`] may either support or omit the computation of -/// the nullifiers for received notes; the implementation for [`ExtendedFullViewingKey`] -/// will derive the nullifiers for received notes and return them as part of the resulting -/// [`WalletShieldedOutput`]s, whereas the implementation for [`SaplingIvk`] cannot -/// do so and will return the unit value in those outputs instead. -/// -/// [`ExtendedFullViewingKey`]: zcash_primitives::zip32::ExtendedFullViewingKey -/// [`SaplingIvk`]: zcash_primitives::sapling::SaplingIvk -/// [`CompactBlock`]: crate::proto::compact_formats::CompactBlock -/// [`ScanningKey`]: crate::welding_rig::ScanningKey -/// [`CommitmentTree`]: zcash_primitives::merkle_tree::CommitmentTree -/// [`IncrementalWitness`]: zcash_primitives::merkle_tree::IncrementalWitness -/// [`WalletShieldedOutput`]: crate::wallet::WalletShieldedOutput -/// [`WalletTx`]: crate::wallet::WalletTx -pub fn scan_block( - params: &P, - block: CompactBlock, - vks: &[(&AccountId, &K)], - nullifiers: &[(AccountId, Nullifier)], - tree: &mut CommitmentTree, - existing_witnesses: &mut [&mut IncrementalWitness], -) -> Vec> { - let mut wtxs: Vec> = vec![]; - let block_height = block.height(); - - for tx in block.vtx.into_iter() { - let num_spends = tx.spends.len(); - let num_outputs = tx.outputs.len(); - - // Check for spent notes - // The only step that is not constant-time is the filter() at the end. - let shielded_spends: Vec<_> = tx - .spends - .into_iter() - .enumerate() - .map(|(index, spend)| { - let spend_nf = spend.nf().expect( - "Could not deserialize nullifier for spend from protobuf representation.", - ); - // Find the first tracked nullifier that matches this spend, and produce - // a WalletShieldedSpend if there is a match, in constant time. - nullifiers - .iter() - .map(|&(account, nf)| CtOption::new(account, nf.ct_eq(&spend_nf))) - .fold( - CtOption::new(AccountId::default(), 0.into()), - |first, next| CtOption::conditional_select(&next, &first, first.is_some()), - ) - .map(|account| WalletShieldedSpend { - index, - nf: spend_nf, - account, - }) - }) - .filter(|spend| spend.is_some().into()) - .map(|spend| spend.unwrap()) - .collect(); - - // Collect the set of accounts that were spent from in this transaction - let spent_from_accounts: HashSet<_> = - shielded_spends.iter().map(|spend| spend.account).collect(); - - // Check for incoming notes while incrementing tree and witnesses - let mut shielded_outputs: Vec> = vec![]; - { - // Grab mutable references to new witnesses from previous transactions - // in this block so that we can update them. Scoped so we don't hold - // mutable references to wtxs for too long. - let mut block_witnesses: Vec<_> = wtxs - .iter_mut() - .flat_map(|tx| { - tx.shielded_outputs - .iter_mut() - .map(|output| &mut output.witness) - }) - .collect(); - - for to_scan in tx.outputs.into_iter().enumerate() { - // Grab mutable references to new witnesses from previous outputs - // in this transaction so that we can update them. Scoped so we - // don't hold mutable references to shielded_outputs for too long. - let mut new_witnesses: Vec<_> = shielded_outputs - .iter_mut() - .map(|output| &mut output.witness) - .collect(); - - if let Some(output) = scan_output( - params, - block_height, - to_scan, - vks, - &spent_from_accounts, - tree, - existing_witnesses, - &mut block_witnesses, - &mut new_witnesses, - ) { - shielded_outputs.push(output); - } - } - } - - if !(shielded_spends.is_empty() && shielded_outputs.is_empty()) { - let mut txid = TxId([0u8; 32]); - txid.0.copy_from_slice(&tx.hash); - wtxs.push(WalletTx { - txid, - index: tx.index as usize, - num_spends, - num_outputs, - shielded_spends, - shielded_outputs, - }); - } - } - - wtxs -} - -#[cfg(test)] -mod tests { - use ff::{Field, PrimeField}; - use group::GroupEncoding; - use rand_core::{OsRng, RngCore}; - use zcash_primitives::{ - consensus::{BlockHeight, Network}, - constants::SPENDING_KEY_GENERATOR, - memo::MemoBytes, - merkle_tree::CommitmentTree, - sapling::{ - note_encryption::sapling_note_encryption, util::generate_random_rseed, Note, Nullifier, - SaplingIvk, - }, - transaction::components::Amount, - zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, - }; - - use super::scan_block; - use crate::proto::compact_formats::{CompactBlock, CompactOutput, CompactSpend, CompactTx}; - use crate::wallet::AccountId; - - fn random_compact_tx(mut rng: impl RngCore) -> CompactTx { - let fake_nf = { - let mut nf = vec![0; 32]; - rng.fill_bytes(&mut nf); - nf - }; - let fake_cmu = { - let fake_cmu = bls12_381::Scalar::random(&mut rng); - fake_cmu.to_repr().as_ref().to_owned() - }; - let fake_epk = { - let mut buffer = [0; 64]; - rng.fill_bytes(&mut buffer); - let fake_esk = jubjub::Fr::from_bytes_wide(&buffer); - let fake_epk = SPENDING_KEY_GENERATOR * fake_esk; - fake_epk.to_bytes().to_vec() - }; - let mut cspend = CompactSpend::new(); - cspend.set_nf(fake_nf); - let mut cout = CompactOutput::new(); - cout.set_cmu(fake_cmu); - cout.set_epk(fake_epk); - cout.set_ciphertext(vec![0; 52]); - let mut ctx = CompactTx::new(); - let mut txid = vec![0; 32]; - rng.fill_bytes(&mut txid); - ctx.set_hash(txid); - ctx.spends.push(cspend); - ctx.outputs.push(cout); - ctx - } - - /// Create a fake CompactBlock at the given height, with a transaction containing a - /// single spend of the given nullifier and a single output paying the given address. - /// Returns the CompactBlock. - fn fake_compact_block( - height: BlockHeight, - nf: Nullifier, - extfvk: ExtendedFullViewingKey, - value: Amount, - tx_after: bool, - ) -> CompactBlock { - let to = extfvk.default_address().unwrap().1; - - // Create a fake Note for the account - let mut rng = OsRng; - let rseed = generate_random_rseed(&Network::TestNetwork, height, &mut rng); - let note = Note { - g_d: to.diversifier().g_d().unwrap(), - pk_d: *to.pk_d(), - value: value.into(), - rseed, - }; - let encryptor = sapling_note_encryption::<_, Network>( - Some(extfvk.fvk.ovk), - note.clone(), - to, - MemoBytes::empty(), - &mut rng, - ); - let cmu = note.cmu().to_repr().as_ref().to_owned(); - let epk = encryptor.epk().to_bytes().to_vec(); - let enc_ciphertext = encryptor.encrypt_note_plaintext(); - - // Create a fake CompactBlock containing the note - let mut cb = CompactBlock::new(); - cb.set_height(height.into()); - - // Add a random Sapling tx before ours - { - let mut tx = random_compact_tx(&mut rng); - tx.index = cb.vtx.len() as u64; - cb.vtx.push(tx); - } - - let mut cspend = CompactSpend::new(); - cspend.set_nf(nf.0.to_vec()); - let mut cout = CompactOutput::new(); - cout.set_cmu(cmu); - cout.set_epk(epk); - cout.set_ciphertext(enc_ciphertext.as_ref()[..52].to_vec()); - let mut ctx = CompactTx::new(); - let mut txid = vec![0; 32]; - rng.fill_bytes(&mut txid); - ctx.set_hash(txid); - ctx.spends.push(cspend); - ctx.outputs.push(cout); - ctx.index = cb.vtx.len() as u64; - cb.vtx.push(ctx); - - // Optionally add another random Sapling tx after ours - if tx_after { - let mut tx = random_compact_tx(&mut rng); - tx.index = cb.vtx.len() as u64; - cb.vtx.push(tx); - } - - cb - } - - #[test] - fn scan_block_with_my_tx() { - let extsk = ExtendedSpendingKey::master(&[]); - let extfvk = ExtendedFullViewingKey::from(&extsk); - - let cb = fake_compact_block( - 1u32.into(), - Nullifier([0; 32]), - extfvk.clone(), - Amount::from_u64(5).unwrap(), - false, - ); - assert_eq!(cb.vtx.len(), 2); - - let mut tree = CommitmentTree::empty(); - let txs = scan_block( - &Network::TestNetwork, - cb, - &[(&AccountId(0), &extfvk)], - &[], - &mut tree, - &mut [], - ); - assert_eq!(txs.len(), 1); - - let tx = &txs[0]; - assert_eq!(tx.index, 1); - assert_eq!(tx.num_spends, 1); - assert_eq!(tx.num_outputs, 1); - assert_eq!(tx.shielded_spends.len(), 0); - assert_eq!(tx.shielded_outputs.len(), 1); - assert_eq!(tx.shielded_outputs[0].index, 0); - assert_eq!(tx.shielded_outputs[0].account, AccountId(0)); - assert_eq!(tx.shielded_outputs[0].note.value, 5); - - // Check that the witness root matches - assert_eq!(tx.shielded_outputs[0].witness.root(), tree.root()); - } - - #[test] - fn scan_block_with_txs_after_my_tx() { - let extsk = ExtendedSpendingKey::master(&[]); - let extfvk = ExtendedFullViewingKey::from(&extsk); - - let cb = fake_compact_block( - 1u32.into(), - Nullifier([0; 32]), - extfvk.clone(), - Amount::from_u64(5).unwrap(), - true, - ); - assert_eq!(cb.vtx.len(), 3); - - let mut tree = CommitmentTree::empty(); - let txs = scan_block( - &Network::TestNetwork, - cb, - &[(&AccountId(0), &extfvk)], - &[], - &mut tree, - &mut [], - ); - assert_eq!(txs.len(), 1); - - let tx = &txs[0]; - assert_eq!(tx.index, 1); - assert_eq!(tx.num_spends, 1); - assert_eq!(tx.num_outputs, 1); - assert_eq!(tx.shielded_spends.len(), 0); - assert_eq!(tx.shielded_outputs.len(), 1); - assert_eq!(tx.shielded_outputs[0].index, 0); - assert_eq!(tx.shielded_outputs[0].account, AccountId(0)); - assert_eq!(tx.shielded_outputs[0].note.value, 5); - - // Check that the witness root matches - assert_eq!(tx.shielded_outputs[0].witness.root(), tree.root()); - } - - #[test] - fn scan_block_with_my_spend() { - let extsk = ExtendedSpendingKey::master(&[]); - let extfvk = ExtendedFullViewingKey::from(&extsk); - let nf = Nullifier([7; 32]); - let account = AccountId(12); - - let cb = fake_compact_block(1u32.into(), nf, extfvk, Amount::from_u64(5).unwrap(), false); - assert_eq!(cb.vtx.len(), 2); - let vks: Vec<(&AccountId, &SaplingIvk)> = vec![]; - - let mut tree = CommitmentTree::empty(); - let txs = scan_block( - &Network::TestNetwork, - cb, - &vks[..], - &[(account, nf)], - &mut tree, - &mut [], - ); - assert_eq!(txs.len(), 1); - - let tx = &txs[0]; - assert_eq!(tx.index, 1); - assert_eq!(tx.num_spends, 1); - assert_eq!(tx.num_outputs, 1); - assert_eq!(tx.shielded_spends.len(), 1); - assert_eq!(tx.shielded_outputs.len(), 0); - assert_eq!(tx.shielded_spends[0].index, 0); - assert_eq!(tx.shielded_spends[0].nf, nf); - assert_eq!(tx.shielded_spends[0].account, account); - } -} diff --git a/zcash_client_backend/src/zip321.rs b/zcash_client_backend/src/zip321.rs deleted file mode 100644 index bc2d8300b0..0000000000 --- a/zcash_client_backend/src/zip321.rs +++ /dev/null @@ -1,973 +0,0 @@ -//! Reference implementation of the ZIP-321 standard for payment requests. -//! -//! This module provides data structures, parsing, and rendering functions -//! for interpreting and producing valid ZIP 321 URIs. -//! -//! The specification for ZIP 321 URIs may be found at -use core::fmt::Debug; -use std::collections::HashMap; - -use nom::{ - character::complete::char, combinator::all_consuming, multi::separated_list0, - sequence::preceded, -}; -use zcash_primitives::{ - consensus, - memo::{self, MemoBytes}, - transaction::components::Amount, -}; - -#[cfg(any(test, feature = "test-dependencies"))] -use std::cmp::Ordering; - -use crate::address::RecipientAddress; - -/// Errors that may be produced in decoding of memos. -#[derive(Debug)] -pub enum MemoError { - InvalidBase64(base64::DecodeError), - MemoBytesError(memo::Error), -} - -/// Converts a [`MemoBytes`] value to a ZIP 321 compatible base64-encoded string. -/// -/// [`MemoBytes`]: zcash_primitives::memo::MemoBytes -pub fn memo_to_base64(memo: &MemoBytes) -> String { - base64::encode_config(memo.as_slice(), base64::URL_SAFE_NO_PAD) -} - -/// Parse a [`MemoBytes`] value from a ZIP 321 compatible base64-encoded string. -/// -/// [`MemoBytes`]: zcash_primitives::memo::MemoBytes -pub fn memo_from_base64(s: &str) -> Result { - base64::decode_config(s, base64::URL_SAFE_NO_PAD) - .map_err(MemoError::InvalidBase64) - .and_then(|b| MemoBytes::from_bytes(&b).map_err(MemoError::MemoBytesError)) -} - -/// A single payment being requested. -#[derive(Debug, PartialEq)] -pub struct Payment { - /// The payment address to which the payment should be sent. - pub recipient_address: RecipientAddress, - /// The amount of the payment that is being requested. - pub amount: Amount, - /// A memo that, if included, must be provided with the payment. - /// If a memo is present and [`recipient_address`] is not a shielded - /// address, the wallet should report an error. - /// - /// [`recipient_address`]: #structfield.recipient_address - pub memo: Option, - /// A human-readable label for this payment within the larger structure - /// of the transaction request. - pub label: Option, - /// A human-readable message to be displayed to the user describing the - /// purpose of this payment. - pub message: Option, - /// A list of other arbitrary key/value pairs associated with this payment. - pub other_params: Vec<(String, String)>, -} - -impl Payment { - /// A utility for use in tests to help check round-trip serialization properties. - #[cfg(any(test, feature = "test-dependencies"))] - pub(in crate::zip321) fn normalize(&mut self) { - self.other_params.sort(); - } - - /// Returns a function which compares two normalized payments, with addresses sorted by their - /// string representation given the specified network. This does not perform normalization - /// internally, so payments must be normalized prior to being passed to the comparison function - /// returned from this method. - #[cfg(any(test, feature = "test-dependencies"))] - pub(in crate::zip321) fn compare_normalized( - params: &P, - ) -> impl Fn(&Payment, &Payment) -> Ordering + '_ { - move |a: &Payment, b: &Payment| { - let a_addr = a.recipient_address.encode(params); - let b_addr = b.recipient_address.encode(params); - - a_addr - .cmp(&b_addr) - .then(a.amount.cmp(&b.amount)) - .then(a.memo.cmp(&b.memo)) - .then(a.label.cmp(&b.label)) - .then(a.message.cmp(&b.message)) - .then(a.other_params.cmp(&b.other_params)) - } - } -} - -/// A ZIP321 transaction request. -/// -/// A ZIP 321 request may include one or more such requests for payment. -/// When constructing a transaction in response to such a request, -/// a separate output should be added to the transaction for each -/// payment value in the request. -#[derive(Debug, PartialEq)] -pub struct TransactionRequest { - payments: Vec, -} - -impl TransactionRequest { - /// A utility for use in tests to help check round-trip serialization properties. - #[cfg(any(test, feature = "test-dependencies"))] - pub(in crate::zip321) fn normalize(&mut self, params: &P) { - for p in &mut self.payments { - p.normalize(); - } - - self.payments.sort_by(Payment::compare_normalized(params)); - } - - /// A utility for use in tests to help check round-trip serialization properties. - /// by comparing a two transaction requests for equality after normalization. - #[cfg(all(test, feature = "test-dependencies"))] - pub(in crate::zip321) fn normalize_and_eq( - params: &P, - a: &mut TransactionRequest, - b: &mut TransactionRequest, - ) -> bool { - a.normalize(params); - b.normalize(params); - - a == b - } - - /// Convert this request to a URI string. - /// - /// Returns None if the payment request is empty. - pub fn to_uri(&self, params: &P) -> Option { - fn payment_params( - payment: &Payment, - payment_index: Option, - ) -> impl IntoIterator + '_ { - std::iter::empty() - .chain(render::amount_param(payment.amount, payment_index)) - .chain( - payment - .memo - .as_ref() - .map(|m| render::memo_param(&m, payment_index)), - ) - .chain( - payment - .label - .as_ref() - .map(|m| render::str_param("label", &m, payment_index)), - ) - .chain( - payment - .message - .as_ref() - .map(|m| render::str_param("message", &m, payment_index)), - ) - .chain( - payment - .other_params - .iter() - .map(move |(name, value)| render::str_param(&name, &value, payment_index)), - ) - } - - match &self.payments[..] { - [] => None, - [payment] => { - let query_params = payment_params(&payment, None) - .into_iter() - .collect::>(); - - Some(format!( - "zcash:{}?{}", - payment.recipient_address.encode(params), - query_params.join("&") - )) - } - _ => { - let query_params = self - .payments - .iter() - .enumerate() - .flat_map(|(i, payment)| { - let primary_address = payment.recipient_address.clone(); - std::iter::empty() - .chain(Some(render::addr_param(params, &primary_address, Some(i)))) - .chain(payment_params(&payment, Some(i))) - }) - .collect::>(); - - Some(format!("zcash:?{}", query_params.join("&"))) - } - } - } - - /// Parse the provided URI to a payment request value. - pub fn from_uri(params: &P, uri: &str) -> Result { - // Parse the leading zcash:

- let (rest, primary_addr_param) = - parse::lead_addr(params)(uri).map_err(|e| e.to_string())?; - - // Parse the remaining parameters as an undifferentiated list - let (_, xs) = all_consuming(preceded( - char('?'), - separated_list0(char('&'), parse::zcashparam(params)), - ))(rest) - .map_err(|e| e.to_string())?; - - // Construct sets of payment parameters, keyed by the payment index. - let mut params_by_index: HashMap> = HashMap::new(); - - // Add the primary address, if any, to the index. - if let Some(p) = primary_addr_param { - params_by_index.insert(p.payment_index, vec![p.param]); - } - - // Group the remaining parameters by payment index - for p in xs { - match params_by_index.get_mut(&p.payment_index) { - None => { - params_by_index.insert(p.payment_index, vec![p.param]); - } - - Some(current) => { - if parse::has_duplicate_param(¤t, &p.param) { - return Err(format!( - "Found duplicate parameter {:?} at index {}", - p.param, p.payment_index - )); - } else { - current.push(p.param); - } - } - } - } - - // Build the actual payment values from the index. - params_by_index - .into_iter() - .map(|(i, params)| parse::to_payment(params, i)) - .collect::, _>>() - .map(|payments| TransactionRequest { payments }) - } -} - -mod render { - use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; - - use zcash_primitives::{ - consensus, transaction::components::amount::COIN, transaction::components::Amount, - }; - - use super::{memo_to_base64, MemoBytes, RecipientAddress}; - - /// The set of ASCII characters that must be percent-encoded according - /// to the definition of ZIP 321. This is the complement of the subset of - /// ASCII characters defined by `qchar` - /// - // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" - // allowed-delims = "!" / "$" / "'" / "(" / ")" / "*" / "+" / "," / ";" - // qchar = unreserved / pct-encoded / allowed-delims / ":" / "@" - pub const QCHAR_ENCODE: &AsciiSet = &CONTROLS - .add(b' ') - .add(b'"') - .add(b'#') - .add(b'%') - .add(b'&') - .add(b'/') - .add(b'<') - .add(b'=') - .add(b'>') - .add(b'?') - .add(b'[') - .add(b'\\') - .add(b']') - .add(b'^') - .add(b'`') - .add(b'{') - .add(b'|') - .add(b'}'); - - /// Converts a parameter index value to the `String` representation - /// that must be appended to a parameter name when constructing a ZIP 321 URI. - pub fn param_index(idx: Option) -> String { - match idx { - Some(i) if i > 0 => format!(".{}", i), - _otherwise => "".to_string(), - } - } - - /// Constructs an "address" key/value pair containing the encoded recipient address - /// at the specified parameter index. - pub fn addr_param( - params: &P, - addr: &RecipientAddress, - idx: Option, - ) -> String { - format!("address{}={}", param_index(idx), addr.encode(params)) - } - - /// Converts an [`Amount`] value to a correctly formatted decimal ZEC - /// value for inclusion in a ZIP 321 URI. - pub fn amount_str(amount: Amount) -> Option { - if amount.is_positive() { - let coins = i64::from(amount) / COIN; - let zats = i64::from(amount) % COIN; - Some(if zats == 0 { - format!("{}", coins) - } else { - format!("{}.{:0>8}", coins, zats) - .trim_end_matches('0') - .to_string() - }) - } else { - None - } - } - - /// Constructs an "amount" key/value pair containing the encoded ZEC amount - /// at the specified parameter index. - pub fn amount_param(amount: Amount, idx: Option) -> Option { - amount_str(amount).map(|s| format!("amount{}={}", param_index(idx), s)) - } - - /// Constructs a "memo" key/value pair containing the base64URI-encoded memo - /// at the specified parameter index. - pub fn memo_param(value: &MemoBytes, idx: Option) -> String { - format!("{}{}={}", "memo", param_index(idx), memo_to_base64(value)) - } - - /// Utility function for an arbitrary string key/value pair for inclusion in - /// a ZIP 321 URI at the specified parameter index. - pub fn str_param(label: &str, value: &str, idx: Option) -> String { - format!( - "{}{}={}", - label, - param_index(idx), - utf8_percent_encode(value, QCHAR_ENCODE) - ) - } -} - -mod parse { - use core::fmt::Debug; - - use nom::{ - bytes::complete::{tag, take_until}, - character::complete::{alpha1, char, digit0, digit1, one_of}, - combinator::{map_opt, map_res, opt, recognize}, - sequence::{preceded, separated_pair, tuple}, - AsChar, IResult, InputTakeAtPosition, - }; - use percent_encoding::percent_decode; - use zcash_primitives::{ - consensus, transaction::components::amount::COIN, transaction::components::Amount, - }; - - use crate::address::RecipientAddress; - - use super::{memo_from_base64, MemoBytes, Payment}; - - /// A data type that defines the possible parameter types which may occur within a - /// ZIP 321 URI. - #[derive(Debug, PartialEq)] - pub enum Param { - Addr(RecipientAddress), - Amount(Amount), - Memo(MemoBytes), - Label(String), - Message(String), - Other(String, String), - } - - /// A [`Param`] value with its associated index. - #[derive(Debug)] - pub struct IndexedParam { - pub param: Param, - pub payment_index: usize, - } - - /// Utility function for determining parameter uniqueness. - /// - /// Utility function for determining whether a newly parsed param is a duplicate - /// of a previous parameter. - pub fn has_duplicate_param(v: &[Param], p: &Param) -> bool { - for p0 in v { - match (p0, p) { - (Param::Addr(_), Param::Addr(_)) => return true, - (Param::Amount(_), Param::Amount(_)) => return true, - (Param::Memo(_), Param::Memo(_)) => return true, - (Param::Label(_), Param::Label(_)) => return true, - (Param::Message(_), Param::Message(_)) => return true, - (Param::Other(n, _), Param::Other(n0, _)) if (n == n0) => return true, - _otherwise => continue, - } - } - - false - } - - /// Converts an vector of [`Param`] values to a [`Payment`]. - /// - /// This function performs checks to ensure that the resulting [`Payment`] is structurally - /// valid; for example, a request for memo contents may not be associated with a - /// transparent payment address. - pub fn to_payment(vs: Vec, i: usize) -> Result { - let addr = vs.iter().find_map(|v| match v { - Param::Addr(a) => Some(a.clone()), - _otherwise => None, - }); - - let mut payment = Payment { - recipient_address: addr.ok_or(format!("Payment {} had no recipient address.", i))?, - amount: Amount::zero(), - memo: None, - label: None, - message: None, - other_params: vec![], - }; - - for v in vs { - match v { - Param::Amount(a) => payment.amount = a, - Param::Memo(m) => { - match payment.recipient_address { - RecipientAddress::Shielded(_) => payment.memo = Some(m), - RecipientAddress::Transparent(_) => return Err(format!("Payment {} attempted to associate a memo with a transparent recipient address", i)), - } - }, - - Param::Label(m) => payment.label = Some(m), - Param::Message(m) => payment.message = Some(m), - Param::Other(n, m) => payment.other_params.push((n, m)), - _otherwise => {} - } - } - - Ok(payment) - } - - /// Parses and consumes the leading "zcash:\[address\]" from a ZIP 321 URI. - pub fn lead_addr( - params: &P, - ) -> impl Fn(&str) -> IResult<&str, Option> + '_ { - move |input: &str| { - map_opt( - preceded(tag("zcash:"), take_until("?")), - |addr_str: &str| { - if addr_str.is_empty() { - Some(None) // no address is ok, so wrap in `Some` - } else { - // `decode` returns `None` on error, which we want to - // then cause `map_opt` to fail. - RecipientAddress::decode(params, addr_str).map(|a| { - Some(IndexedParam { - param: Param::Addr(a), - payment_index: 0, - }) - }) - } - }, - )(input) - } - } - - /// The primary parser for = query-string parameter pair. - pub fn zcashparam( - params: &P, - ) -> impl Fn(&str) -> IResult<&str, IndexedParam> + '_ { - move |input| { - map_res( - separated_pair(indexed_name, char('='), recognize(qchars)), - move |r| to_indexed_param(params, r), - )(input) - } - } - - /// Extension for the `alphanumeric0` parser which extends that parser - /// by also permitting the characters that are members of the `allowed` - /// string. - fn alphanum_or(allowed: &str) -> impl (Fn(&str) -> IResult<&str, &str>) + '_ { - move |input| { - input.split_at_position_complete(|item| { - let c = item.as_char(); - !(c.is_alphanum() || allowed.contains(c)) - }) - } - } - - /// Parses valid characters which may appear in parameter values. - pub fn qchars(input: &str) -> IResult<&str, &str> { - alphanum_or("-._~!$'()*+,;:@%")(input) - } - - /// Parses valid characters that may appear in parameter names. - pub fn namechars(input: &str) -> IResult<&str, &str> { - alphanum_or("+-")(input) - } - - /// Parses a parameter name and its associated index. - pub fn indexed_name(input: &str) -> IResult<&str, (&str, Option<&str>)> { - let paramname = recognize(tuple((alpha1, namechars))); - - tuple(( - paramname, - opt(preceded( - char('.'), - recognize(tuple(( - one_of("123456789"), - map_opt(digit0, |s: &str| if s.len() > 3 { None } else { Some(s) }), - ))), - )), - ))(input) - } - - /// Parses a value in decimal ZEC. - pub fn parse_amount(input: &str) -> IResult<&str, Amount> { - map_res( - tuple(( - digit1, - opt(preceded( - char('.'), - map_opt(digit0, |s: &str| if s.len() > 8 { None } else { Some(s) }), - )), - )), - |(whole_s, decimal_s): (&str, Option<&str>)| { - let coins: i64 = whole_s - .to_string() - .parse::() - .map_err(|e| e.to_string())?; - - let zats: i64 = match decimal_s { - Some(d) => format!("{:0<8}", d) - .parse::() - .map_err(|e| e.to_string())?, - None => 0, - }; - - if coins >= 21000000 && (coins > 21000000 || zats > 0) { - return Err(format!( - "{} coins exceeds the maximum possible Zcash value.", - coins - )); - } - - let amt = coins * COIN + zats; - - Amount::from_nonnegative_i64(amt) - .map_err(|_| format!("Not a valid zat amount: {}", amt)) - }, - )(input) - } - - fn to_indexed_param<'a, P: consensus::Parameters>( - params: &'a P, - ((name, iopt), value): ((&str, Option<&str>), &str), - ) -> Result { - let param = match name { - "address" => RecipientAddress::decode(params, value) - .map(Param::Addr) - .ok_or(format!( - "Could not interpret {} as a valid Zcash address.", - value - )), - - "amount" => parse_amount(value) - .map(|(_, a)| Param::Amount(a)) - .map_err(|e| e.to_string()), - - "label" => percent_decode(value.as_bytes()) - .decode_utf8() - .map(|s| Param::Label(s.into_owned())) - .map_err(|e| e.to_string()), - - "message" => percent_decode(value.as_bytes()) - .decode_utf8() - .map(|s| Param::Message(s.into_owned())) - .map_err(|e| e.to_string()), - - "memo" => memo_from_base64(value) - .map(Param::Memo) - .map_err(|e| format!("Decoded memo was invalid: {:?}", e)), - - other if other.starts_with("req-") => { - Err(format!("Required parameter {} not recognized", other)) - } - - other => percent_decode(value.as_bytes()) - .decode_utf8() - .map(|s| Param::Other(other.to_string(), s.into_owned())) - .map_err(|e| e.to_string()), - }?; - - let payment_index = match iopt { - Some(istr) => istr.parse::().map(Some).map_err(|e| e.to_string()), - None => Ok(None), - }?; - - Ok(IndexedParam { - param, - payment_index: payment_index.unwrap_or(0), - }) - } -} - -#[cfg(feature = "test-dependencies")] -pub mod testing { - use proptest::collection::btree_map; - use proptest::collection::vec; - use proptest::option; - use proptest::prelude::{any, prop_compose, prop_oneof}; - use proptest::strategy::Strategy; - use zcash_primitives::{ - consensus::TEST_NETWORK, legacy::testing::arb_transparent_addr, - sapling::keys::testing::arb_shielded_addr, - transaction::components::amount::testing::arb_nonnegative_amount, - }; - - use crate::address::RecipientAddress; - - use super::{MemoBytes, Payment, TransactionRequest}; - - pub fn arb_addr() -> impl Strategy { - prop_oneof![ - arb_shielded_addr().prop_map(RecipientAddress::Shielded), - arb_transparent_addr().prop_map(RecipientAddress::Transparent), - ] - } - - pub const VALID_PARAMNAME: &str = "[a-zA-Z][a-zA-Z0-9+-]*"; - - prop_compose! { - pub fn arb_valid_memo()(bytes in vec(any::(), 0..512)) -> MemoBytes { - MemoBytes::from_bytes(&bytes).unwrap() - } - } - - prop_compose! { - pub fn arb_zip321_payment()( - recipient_address in arb_addr(), - amount in arb_nonnegative_amount(), - memo in option::of(arb_valid_memo()), - message in option::of(any::()), - label in option::of(any::()), - // prevent duplicates by generating a set rather than a vec - other_params in btree_map(VALID_PARAMNAME, any::(), 0..3), - ) -> Payment { - - let is_sapling = match recipient_address { - RecipientAddress::Transparent(_) => false, - RecipientAddress::Shielded(_) => true, - }; - - Payment { - recipient_address, - amount, - memo: memo.filter(|_| is_sapling), - label, - message, - other_params: other_params.into_iter().collect(), - } - } - } - - prop_compose! { - pub fn arb_zip321_request()(payments in vec(arb_zip321_payment(), 1..10)) -> TransactionRequest { - let mut req = TransactionRequest { payments }; - req.normalize(&TEST_NETWORK); // just to make test comparisons easier - req - } - } - - prop_compose! { - pub fn arb_zip321_uri()(req in arb_zip321_request()) -> String { - req.to_uri(&TEST_NETWORK).unwrap() - } - } - - prop_compose! { - pub fn arb_addr_str()(addr in arb_addr()) -> String { - addr.encode(&TEST_NETWORK) - } - } -} - -#[cfg(test)] -mod tests { - use std::str::FromStr; - use zcash_primitives::{ - consensus::{Parameters, TEST_NETWORK}, - memo::Memo, - transaction::components::Amount, - }; - - use crate::address::RecipientAddress; - - use super::{ - memo_from_base64, memo_to_base64, - parse::{parse_amount, zcashparam, Param}, - render::amount_str, - MemoBytes, Payment, TransactionRequest, - }; - use crate::encoding::decode_payment_address; - - #[cfg(all(test, feature = "test-dependencies"))] - use proptest::prelude::{any, proptest}; - - #[cfg(all(test, feature = "test-dependencies"))] - use zcash_primitives::transaction::components::amount::testing::arb_nonnegative_amount; - - #[cfg(all(test, feature = "test-dependencies"))] - use super::{ - render::{memo_param, str_param}, - testing::{arb_addr, arb_addr_str, arb_valid_memo, arb_zip321_request, arb_zip321_uri}, - }; - - fn check_roundtrip(req: TransactionRequest) { - if let Some(req_uri) = req.to_uri(&TEST_NETWORK) { - let parsed = TransactionRequest::from_uri(&TEST_NETWORK, &req_uri).unwrap(); - assert_eq!(parsed, req); - } else { - panic!("Generated invalid payment request: {:?}", req); - } - } - - #[test] - fn test_zip321_roundtrip_simple_amounts() { - let amounts = vec![1u64, 1000u64, 100000u64, 100000000u64, 100000000000u64]; - - for amt_u64 in amounts { - let amt = Amount::from_u64(amt_u64).unwrap(); - let amt_str = amount_str(amt).unwrap(); - assert_eq!(amt, parse_amount(&amt_str).unwrap().1); - } - } - - #[test] - fn test_zip321_parse_empty_message() { - let fragment = "message="; - - let result = zcashparam(&TEST_NETWORK)(fragment).unwrap().1.param; - assert_eq!(result, Param::Message("".to_string())); - } - - #[test] - fn test_zip321_parse_simple() { - let uri = "zcash:ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k?amount=3768769.02796286&message="; - let parse_result = TransactionRequest::from_uri(&TEST_NETWORK, &uri).unwrap(); - - let expected = TransactionRequest { - payments: vec![ - Payment { - recipient_address: RecipientAddress::Shielded(decode_payment_address(&TEST_NETWORK.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap().unwrap()), - amount: Amount::from_u64(376876902796286).unwrap(), - memo: None, - label: None, - message: Some("".to_string()), - other_params: vec![], - } - ] - }; - - assert_eq!(parse_result, expected); - } - - #[test] - fn test_zip321_roundtrip_empty_message() { - let req = TransactionRequest { - payments: vec![ - Payment { - recipient_address: RecipientAddress::Shielded(decode_payment_address(TEST_NETWORK.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap().unwrap()), - amount: Amount::from_u64(0).unwrap(), - memo: None, - label: None, - message: Some("".to_string()), - other_params: vec![] - } - ] - }; - - check_roundtrip(req); - } - - #[test] - fn test_zip321_memos() { - let m_simple: MemoBytes = Memo::from_str("This is a simple memo.").unwrap().into(); - let m_simple_64 = memo_to_base64(&m_simple); - assert_eq!(memo_from_base64(&m_simple_64).unwrap(), m_simple); - - let m_json: MemoBytes = Memo::from_str("{ \"key\": \"This is a JSON-structured memo.\" }") - .unwrap() - .into(); - let m_json_64 = memo_to_base64(&m_json); - assert_eq!(memo_from_base64(&m_json_64).unwrap(), m_json); - - let m_unicode: MemoBytes = Memo::from_str("This is a unicode memo ✨🦄🏆🎉") - .unwrap() - .into(); - let m_unicode_64 = memo_to_base64(&m_unicode); - assert_eq!(memo_from_base64(&m_unicode_64).unwrap(), m_unicode); - } - - #[test] - fn test_zip321_spec_valid_examples() { - let valid_1 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=1&memo=VGhpcyBpcyBhIHNpbXBsZSBtZW1vLg&message=Thank%20you%20for%20your%20purchase"; - let v1r = TransactionRequest::from_uri(&TEST_NETWORK, &valid_1).unwrap(); - assert_eq!( - v1r.payments.get(0).map(|p| p.amount), - Some(Amount::from_u64(100000000).unwrap()) - ); - - let valid_2 = "zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=123.456&address.1=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.1=0.789&memo.1=VGhpcyBpcyBhIHVuaWNvZGUgbWVtbyDinKjwn6aE8J-PhvCfjok"; - let mut v2r = TransactionRequest::from_uri(&TEST_NETWORK, &valid_2).unwrap(); - v2r.normalize(&TEST_NETWORK); - assert_eq!( - v2r.payments.get(0).map(|p| p.amount), - Some(Amount::from_u64(12345600000).unwrap()) - ); - assert_eq!( - v2r.payments.get(1).map(|p| p.amount), - Some(Amount::from_u64(78900000).unwrap()) - ); - - // valid; amount just less than MAX_MONEY - // 20999999.99999999 - let valid_3 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=20999999.99999999"; - let v3r = TransactionRequest::from_uri(&TEST_NETWORK, &valid_3).unwrap(); - assert_eq!( - v3r.payments.get(0).map(|p| p.amount), - Some(Amount::from_u64(2099999999999999u64).unwrap()) - ); - - // valid; MAX_MONEY - // 21000000 - let valid_4 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=21000000"; - let v4r = TransactionRequest::from_uri(&TEST_NETWORK, &valid_4).unwrap(); - assert_eq!( - v4r.payments.get(0).map(|p| p.amount), - Some(Amount::from_u64(2100000000000000u64).unwrap()) - ); - } - - #[test] - fn test_zip321_spec_invalid_examples() { - // invalid; missing `address=` - let invalid_1 = "zcash:?amount=3491405.05201255&address.1=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.1=5740296.87793245"; - let i1r = TransactionRequest::from_uri(&TEST_NETWORK, &invalid_1); - assert!(i1r.is_err()); - - // invalid; missing `address.1=` - let invalid_2 = "zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=1&amount.1=2&address.2=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez"; - let i2r = TransactionRequest::from_uri(&TEST_NETWORK, &invalid_2); - assert!(i2r.is_err()); - - // invalid; `address.0=` and `amount.0=` are not permitted (leading 0s). - let invalid_3 = "zcash:?address.0=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.0=2"; - let i3r = TransactionRequest::from_uri(&TEST_NETWORK, &invalid_3); - assert!(i3r.is_err()); - - // invalid; duplicate `amount=` field - let invalid_4 = - "zcash:?amount=1.234&amount=2.345&address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU"; - let i4r = TransactionRequest::from_uri(&TEST_NETWORK, &invalid_4); - assert!(i4r.is_err()); - - // invalid; duplicate `amount.1=` field - let invalid_5 = - "zcash:?amount.1=1.234&amount.1=2.345&address.1=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU"; - let i5r = TransactionRequest::from_uri(&TEST_NETWORK, &invalid_5); - assert!(i5r.is_err()); - - //invalid; memo associated with t-addr - let invalid_6 = "zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=123.456&memo=eyAia2V5IjogIlRoaXMgaXMgYSBKU09OLXN0cnVjdHVyZWQgbWVtby4iIH0&address.1=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.1=0.789&memo.1=VGhpcyBpcyBhIHVuaWNvZGUgbWVtbyDinKjwn6aE8J-PhvCfjok"; - let i6r = TransactionRequest::from_uri(&TEST_NETWORK, &invalid_6); - assert!(i6r.is_err()); - - // invalid; amount component exceeds an i64 - // 9223372036854775808 = i64::MAX + 1 - let invalid_7 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=9223372036854775808"; - let i7r = TransactionRequest::from_uri(&TEST_NETWORK, &invalid_7); - assert!(i7r.is_err()); - - // invalid; amount component wraps into a valid small positive i64 - // 18446744073709551624 - let invalid_7a = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=18446744073709551624"; - let i7ar = TransactionRequest::from_uri(&TEST_NETWORK, &invalid_7a); - assert!(i7ar.is_err()); - - // invalid; amount component is MAX_MONEY - // 21000000.00000001 - let invalid_8 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=21000000.00000001"; - let i8r = TransactionRequest::from_uri(&TEST_NETWORK, &invalid_8); - assert!(i8r.is_err()); - - // invalid; negative amount - let invalid_9 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=-1"; - let i9r = TransactionRequest::from_uri(&TEST_NETWORK, &invalid_9); - assert!(i9r.is_err()); - - // invalid; parameter index too large - let invalid_10 = - "zcash:?amount.10000=1.23&address.10000=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU"; - let i10r = TransactionRequest::from_uri(&TEST_NETWORK, &invalid_10); - assert!(i10r.is_err()); - } - - #[cfg(all(test, feature = "test-dependencies"))] - proptest! { - #[test] - fn prop_zip321_roundtrip_address(addr in arb_addr()) { - let a = addr.encode(&TEST_NETWORK); - assert_eq!(RecipientAddress::decode(&TEST_NETWORK, &a), Some(addr)); - } - - #[test] - fn prop_zip321_roundtrip_address_str(a in arb_addr_str()) { - let addr = RecipientAddress::decode(&TEST_NETWORK, &a).unwrap(); - assert_eq!(addr.encode(&TEST_NETWORK), a); - } - - #[test] - fn prop_zip321_roundtrip_amount(amt in arb_nonnegative_amount()) { - let amt_str = amount_str(amt).unwrap(); - assert_eq!(amt, parse_amount(&amt_str).unwrap().1); - } - - #[test] - fn prop_zip321_roundtrip_str_param( - message in any::(), i in proptest::option::of(0usize..2000)) { - let fragment = str_param("message", &message, i); - let (rest, iparam) = zcashparam(&TEST_NETWORK)(&fragment).unwrap(); - assert_eq!(rest, ""); - assert_eq!(iparam.param, Param::Message(message)); - assert_eq!(iparam.payment_index, i.unwrap_or(0)); - } - - #[test] - fn prop_zip321_roundtrip_memo_param( - memo in arb_valid_memo(), i in proptest::option::of(0usize..2000)) { - let fragment = memo_param(&memo, i); - let (rest, iparam) = zcashparam(&TEST_NETWORK)(&fragment).unwrap(); - assert_eq!(rest, ""); - assert_eq!(iparam.param, Param::Memo(memo)); - assert_eq!(iparam.payment_index, i.unwrap_or(0)); - } - - #[test] - fn prop_zip321_roundtrip_request(mut req in arb_zip321_request()) { - if let Some(req_uri) = req.to_uri(&TEST_NETWORK) { - let mut parsed = TransactionRequest::from_uri(&TEST_NETWORK, &req_uri).unwrap(); - assert!(TransactionRequest::normalize_and_eq(&TEST_NETWORK, &mut parsed, &mut req)); - } else { - panic!("Generated invalid payment request: {:?}", req); - } - } - - #[test] - fn prop_zip321_roundtrip_uri(uri in arb_zip321_uri()) { - let mut parsed = TransactionRequest::from_uri(&TEST_NETWORK, &uri).unwrap(); - parsed.normalize(&TEST_NETWORK); - let serialized = parsed.to_uri(&TEST_NETWORK); - assert_eq!(serialized, Some(uri)) - } - } -} diff --git a/zcash_client_sqlite/CHANGELOG.md b/zcash_client_sqlite/CHANGELOG.md index 7ac5cefa88..5968438441 100644 --- a/zcash_client_sqlite/CHANGELOG.md +++ b/zcash_client_sqlite/CHANGELOG.md @@ -6,14 +6,553 @@ and this library adheres to Rust's notion of [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +### Added +- `zcash_client_sqlite::WalletDb::from_connection` + +### Changed +- MSRV is now 1.81.0. +- Migrated to `bip32 =0.6.0-pre.1`, `nonempty 0.11`.`incrementalmerkletree 0.8`, + `shardtree 0.6`. +- `zcash_client_sqlite::wallet::init::init_wallet_db` now has an additional + generic parameter, enabling it to be used with wallets constructed via + `WalletDb::from_connection`. + +## [0.14.0] - 2024-12-16 + +### Added +- `zcash_client_sqlite::AccountUuid` + +### Changed +- Migrated to `sapling-crypto 0.4`, `zcash_keys 0.6`, `zcash_primitives 0.21`, + `zcash_proofs 0.21`, `zcash_client_backend 0.16` +- The `v_transactions` view has been modified: + - The `account_id` column has been replaced with `account_uuid`. +- The `v_tx_outputs` view has been modified: + - The `from_account_id` column has been replaced with `from_account_uuid`. + - The `to_account_id` column has been replaced with `to_account_uuid`. +- The `WalletRead` and `InputSource` impls for `WalletDb` now set the `AccountId` + associated type to `AccountUuid`. +- Variants of `SqliteClientError` have changed: + - The `AccountCollision` and `ReachedGapLimit` now carry `AccountUuid` values + instead of `AccountId`s. + - `SqliteClientError::AccountIdDiscontinuity` has been removed as it is now + unused. + - `SqliteClientError::AccountIdOutOfRange` has been renamed to + `Zip32AccountIndexOutOfRange`. + +### Removed +- `zcash_client_sqlite::AccountId` (use `AccountUuid` instead). + +## [0.13.0] - 2024-11-14 + +### Added +- Exposed `AccountId::from_u32` and `AccountId::as_u32` conversions under the + `unstable` feature flag. + +### Changed +- MSRV is now 1.77.0. +- Migrated to `zcash_primitives 0.20`, `zcash_keys 0.5`, + `zcash_client_backend 0.15`. +- Migrated from `schemer` to our fork `schemerz`. +- Migrated to `rusqlite 0.32`. +- `error::SqliteClientError` has additional variant `NoteFilterInvalid` + +### Fixed +- `zcash_client_sqlite::WalletDb`'s implementation of + `zcash_client_backend::data_api::WalletRead::get_wallet_summary` has been + fixed to take account of `min_confirmations` for transparent balances. + (Previously, it would treat transparent balances as though + `min_confirmations` were `1` even if it was set to a higher value.) + Note that this implementation treats `min_confirmations == 0` the same + as `min_confirmations == 1` for both shielded and transparent TXOs. + It also does not currently distinguish between pending change and + non-change; the pending value is all counted as non-change (issue + [#1592](https://github.com/zcash/librustzcash/issues/1592)). + +## [0.12.2] - 2024-10-21 + +### Fixed +- Fixes an error in determining the minimum checkpoint height to which it's + possible to rewind in the case of a reorg, when no other truncation height + information is available. + +## [0.12.1] - 2024-10-10 + +### Fixed +- An error in scan progress computation was fixed. As part of this fix, wallet + summary information is now only returned in the case that some note + commitment tree size information can be determined, either from subtree root + download or from downloaded block data. NOTE: The recovery progress ratio may + be present as `0:0` in the case that the recovery range contains no notes; + this was not adequately documented in the previous release. + +## [0.12.0] - 2024-10-04 + +### Added +- `impl WalletTest for WalletDb` is now available under the `test-dependencies` + feature flag. + ### Changed -- MSRV is now 1.51.0. +- Migrated to `zcash_client_backend 0.14`, `orchard 0.10`, + `sapling-crypto 0.3`, `shardtree 0.5`, `zcash_address 0.6`, + `zcash_primitives 0.19`, `zcash_proofs 0.19`, `zcash_protocol 0.4`. +- `zcash_client_sqlite::error::SqliteClientError::RequestedRewindInvalid` + is now a structured variant. + +## [0.11.2] - 2024-08-21 + +### Changed +- The `v_tx_outputs` view was modified slightly to support older versions of + `sqlite`. Queries to the exposed `v_tx_outputs` and `v_transactions` views + are supported for SQLite versions back to `3.19.x`. +- `zcash_client_sqlite::wallet::init::WalletMigrationError` has an additional + variant, `DatabaseNotSupported`. The `init_wallet_db` function now checks + that the sqlite version in use is compatible with the features required by + the wallet and returns this error if not. SQLite version `3.35` or higher + is required for use with `zcash_client_sqlite`. + +## [0.11.1] - 2024-08-21 + +### Fixed +- The dependencies of the `tx_retrieval_queue` migration have been fixed to + enable migrating wallets containing certain kinds of transactions. + +## [0.11.0] - 2024-08-20 + +`zcash_client_sqlite` now provides capabilities for the management of ephemeral +transparent addresses in support of the creation of ZIP 320 transaction pairs. + +In addition, `zcash_client_sqlite` now provides improved tracking of transparent +wallet history in support of the API changes in `zcash_client_backend 0.13`, +and the `v_transactions` view has been modified to provide additional metadata +about the relationship of each transaction to the wallet, in particular whether +or not the transaction represents a wallet-internal shielding operation. + +### Changed +- MSRV is now 1.70.0. +- Updated dependencies: + - `zcash_address 0.4` + - `zcash_client_backend 0.13` + - `zcash_encoding 0.2.1` + - `zcash_keys 0.3` + - `zcash_primitives 0.16` + - `zcash_protocol 0.2` +- `zcash_client_sqlite::error::SqliteClientError` has a new `ReachedGapLimit` and + `EphemeralAddressReuse` variants when the "transparent-inputs" feature is enabled. +- `zcash_client_sqlite::error::SqliteClientError` has changed variants: + - Removed `HdwalletError`. + - Added `AccountCollision`. + - Added `TransparentDerivation`. +- The `v_transactions` view has been modified: + - The `block` column has been renamed to `mined_height`. + - A `spent_note_count` column has been added. + - An `is_shielding` column has been added, which is true for transactions where the + spends from the wallet are all transparent, and the outputs to the wallet are all + shielded. +- The `v_tx_outputs` view has been modified: + - The result can now include transparent outputs with unknown height. + +### Fixed +- The `to_address` column of the `v_tx_outputs` view is now `NULL` for + transparent outputs received by the wallet. This column is only intended to + contain addresses for outputs sent to external recipients. The fix aligns + received transparent outputs with received shielded outputs (which have always + returned `NULL`). + +## [0.10.3] - 2024-04-08 + +### Added +- Added a migration to ensure that the default address for existing wallets is + upgraded to include an Orchard receiver. + +### Fixed +- A bug in the SQL query for `WalletDb::get_account_birthday` was fixed. + +## [0.10.2] - 2024-03-27 + +### Fixed +- A bug in the SQL query for `WalletDb::get_unspent_transparent_output` was fixed. + +## [0.10.1] - 2024-03-25 + +### Fixed +- The `sent_notes` table's `received_note` constraint was excessively restrictive + after zcash/librustzcash#1306. Any databases that have migrations from + zcash_client_sqlite 0.10.0 applied should be wiped and restored from seed. + In order to ensure that the incorrect migration is not used, the migration + id for the `full_account_ids` migration has been changed from + `0x1b104345_f27e_42da_a9e3_1de22694da43` to `0x6d02ec76_8720_4cc6_b646_c4e2ce69221c` + +## [0.10.0] - 2024-03-25 + +This version was yanked, use 0.10.1 instead. + +### Added +- A new `orchard` feature flag has been added to make it possible to + build client code without `orchard` dependendencies. +- `zcash_client_sqlite::AccountId` +- `zcash_client_sqlite::wallet::Account` +- `impl From for SqliteClientError` + +### Changed +- Many places that `AccountId` appeared in the API changed from + using `zcash_primitives::zip32::AccountId` to using an opaque `zcash_client_sqlite::AccountId` + type. + - The enum variant `zcash_client_sqlite::error::SqliteClientError::AccountUnknown` + no longer has a `zcash_primitives::zip32::AccountId` data value. + - Changes to the implementation of the `WalletWrite` trait: + - `create_account` function returns a unique identifier for the new account (as before), + except that this ID no longer happens to match the ZIP-32 account index. + To get the ZIP-32 account index, use the new `WalletRead::get_account` function. + - Two columns in the `transactions` view were renamed. They refer to the primary key field in the `accounts` table, which no longer equates to a ZIP-32 account index. + - `to_account` -> `to_account_id` + - `from_account` -> `from_account_id` +- `zcash_client_sqlite::error::SqliteClientError` has changed variants: + - Added `AddressGeneration` + - Added `UnknownZip32Derivation` + - Added `BadAccountData` + - Removed `DiversifierIndexOutOfRange` + - Removed `InvalidNoteId` +- `zcash_client_sqlite::wallet`: + - `init::WalletMigrationError` has added variants: + - `WalletMigrationError::AddressGeneration` + - `WalletMigrationError::CannotRevert` + - `WalletMigrationError::SeedNotRelevant` +- The `v_transactions` and `v_tx_outputs` views now include Orchard notes. + +## [0.9.1] - 2024-03-09 + +### Fixed +- Documentation now correctly builds with all feature flags. + +## [0.9.0] - 2024-03-01 + +### Changed +- Migrated to `orchard 0.7`, `zcash_primitives 0.14`, `zcash_client_backend 0.11`. +- `zcash_client_sqlite::error::SqliteClientError` has new error variants: + - `SqliteClientError::UnsupportedPoolType` + - `SqliteClientError::BalanceError` + - The `Bech32DecodeError` variant has been replaced with a more general + `DecodingError` type. + +## [0.8.1] - 2023-10-18 + +### Fixed +- Fixed a bug in `v_transactions` that was omitting value from identically-valued notes + +## [0.8.0] - 2023-09-25 + +### Notable Changes +- The `v_transactions` and `v_tx_outputs` views have changed in terms of what + columns are returned, and which result columns may be null. Please see the + `Changed` section below for additional details. + +### Added +- `zcash_client_sqlite::commitment_tree` Types related to management of note + commitment trees using the `shardtree` crate. +- A new default-enabled feature flag `multicore`. This allows users to disable + multicore support by setting `default_features = false` on their + `zcash_primitives`, `zcash_proofs`, and `zcash_client_sqlite` dependencies. +- `zcash_client_sqlite::ReceivedNoteId` +- `zcash_client_sqlite::wallet::commitment_tree` A new module containing a + sqlite-backed implementation of `shardtree::store::ShardStore`. +- `impl zcash_client_backend::data_api::WalletCommitmentTrees for WalletDb` + +### Changed +- MSRV is now 1.65.0. +- Bumped dependencies to `hdwallet 0.4`, `incrementalmerkletree 0.5`, `bs58 0.5`, + `prost 0.12`, `rusqlite 0.29`, `schemer-rusqlite 0.2.2`, `time 0.3.22`, + `tempfile 3.5`, `zcash_address 0.3`, `zcash_note_encryption 0.4`, + `zcash_primitives 0.13`, `zcash_client_backend 0.10`. +- Added dependencies on `shardtree 0.0`, `zcash_encoding 0.2`, `byteorder 1` +- A `CommitmentTree` variant has been added to `zcash_client_sqlite::wallet::init::WalletMigrationError` +- `min_confirmations` parameter values are now more strongly enforced. Previously, + a note could be spent with fewer than `min_confirmations` confirmations if the + wallet did not contain enough observed blocks to satisfy the `min_confirmations` + value specified; this situation is now treated as an error. +- `zcash_client_sqlite::error::SqliteClientError` has new error variants: + - `SqliteClientError::AccountUnknown` + - `SqliteClientError::BlockConflict` + - `SqliteClientError::CacheMiss` + - `SqliteClientError::ChainHeightUnknown` + - `SqliteClientError::CommitmentTree` + - `SqliteClientError::NonSequentialBlocks` +- `zcash_client_backend::FsBlockDbError` has a new error variant: + - `FsBlockDbError::CacheMiss` +- `zcash_client_sqlite::FsBlockDb::write_block_metadata` now overwrites any + existing metadata entries that have the same height as a new entry. +- The `v_transactions` and `v_tx_outputs` views no longer return the + internal database identifier for the transaction. The `txid` column should + be used instead. The `tx_index`, `expiry_height`, `raw`, `fee_paid`, and + `expired_unmined` columns will be null for received transparent + transactions, in addition to the other columns that were previously + permitted to be null. + +### Removed +- The empty `wallet::transact` module has been removed. +- `zcash_client_sqlite::NoteId` has been replaced with `zcash_client_sqlite::ReceivedNoteId` + as the `SentNoteId` variant is now unused following changes to + `zcash_client_backend::data_api::WalletRead`. +- `zcash_client_sqlite::wallet::init::{init_blocks_table, init_accounts_table}` + have been removed. `zcash_client_backend::data_api::WalletWrite::create_account` + should be used instead; the initialization of the note commitment tree + previously performed by `init_blocks_table` is now handled by passing an + `AccountBirthday` containing the note commitment tree frontier as of the + end of the birthday height block to `create_account` instead. +- `zcash_client_sqlite::DataConnStmtCache` has been removed in favor of using + `rusqlite` caching for prepared statements. +- `zcash_client_sqlite::prepared` has been entirely removed. + +### Fixed +- Fixed an off-by-one error in the `BlockSource` implementation for the SQLite-backed + `BlockDb` block database which could result in blocks being skipped at the start of + scan ranges. +- `zcash_client_sqlite::{BlockDb, FsBlockDb}::with_blocks` now return an error + if `from_height` is set to a block height that does not exist in the cache. +- `WalletDb::get_transaction` no longer returns an error when called on a transaction + that has not yet been mined, unless the transaction's consensus branch ID cannot be + determined by other means. +- Fixed an error in `v_transactions` wherein received transparent outputs did not + result in a transaction entry appearing in the transaction history. + +## [0.7.1] - 2023-05-17 + +### Fixed +- Fixes a potential crash that could occur when attempting to read a memo from + sqlite when the memo value is `NULL`. At present, we return the empty memo + in this case; in the future, the `get_memo` API will be updated to reflect + the potential absence of memo data. + +## [0.7.0] - 2023-04-28 +### Changed +- Bumped dependencies to `zcash_client_backend 0.9`. + +### Removed +- The following deprecated types and methods have been removed from the public API: + - `wallet::ShieldedOutput` + - `wallet::block_height_extrema` + - `wallet::get_address` + - `wallet::get_all_nullifiers` + - `wallet::get_balance` + - `wallet::get_balance_at` + - `wallet::get_block_hash` + - `wallet::get_commitment_tree` + - `wallet::get_nullifiers` + - `wallet::get_received_memo` + - `wallet::get_rewind_height` + - `wallet::get_sent_memo` + - `wallet::get_spendable_sapling_notes` + - `wallet::get_transaction` + - `wallet::get_tx_height` + - `wallet::get_unified_full_viewing_keys` + - `wallet::get_witnesses` + - `wallet::insert_block` + - `wallet::insert_witnesses` + - `wallet::is_valid_account_extfvk` + - `wallet::mark_sapling_note_spent` + - `wallet::put_tx_data` + - `wallet::put_tx_meta` + - `wallet::prune_witnesses` + - `wallet::select_spendable_sapling_notes` + - `wallet::update_expired_notes` + - `wallet::transact::get_spendable_sapling_notes` + - `wallet::transact::select_spendable_sapling_notes` + +## [0.6.0] - 2023-04-15 +### Added +- SQLite view `v_tx_outputs`, exposing the history of transaction outputs sent + from and received by the wallet. See `zcash_client_sqlite::wallet` for view + documentation. + +### Fixed +- In a previous crate release, `WalletDb` was modified to start tracking Sapling + change notes in both the `sent_notes` and `received_notes` tables, as a form + of double-entry accounting. This broke assumptions in the `v_transactions` + SQLite view, and also left the `sent_notes` table in an inconsistent state. A + migration has been added to this release which fixes the `sent_notes` table to + consistently store Sapling change notes. +- The SQLite view `v_transactions` had several bugs independently from the above + issue, and has been rewritten. See `zcash_client_sqlite::wallet` for view + documentation. + +### Changed +- Bumped dependencies to `group 0.13`, `jubjub 0.10`, `zcash_primitives 0.11`, + `zcash_client_backend 0.8`. +- The dependency on `zcash_primitives` no longer enables the `multicore` feature + by default in order to support compilation under `wasm32-wasi`. Users of other + platforms may need to include an explicit dependency on `zcash_primitives` + without `default-features = false` or otherwise explicitly enable the + `zcash_primitives/multicore` feature if they did not already depend + upon `zcash_primitives` with default features enabled. + +### Removed +- SQLite views `v_tx_received` and `v_tx_sent` (use `v_tx_outputs` instead). + +## [0.5.0] - 2023-02-01 +### Added +- `zcash_client_sqlite::FsBlockDb::rewind_to_height` rewinds the BlockMeta Db + to the specified height following the same logic as homonymous functions on + `WalletDb`. This function does not delete the files referenced by the rows + that might be present and are deleted by this function call. +- `zcash_client_sqlite::FsBlockDb::find_block` +- `zcash_client_sqlite::chain`: + - `impl {Clone, Copy, Debug, PartialEq, Eq} for BlockMeta` + +### Changed +- MSRV is now 1.60.0. +- Bumped dependencies to `zcash_primitives 0.10`, `zcash_client_backend 0.7`. +- `zcash_client_backend::FsBlockDbError`: + - Renamed `FsBlockDbError::{DbError, FsError}` to `FsBlockDbError::{Db, Fs}`. + - Added `FsBlockDbError::MissingBlockPath`. + - `impl fmt::Display for FsBlockDbError` + +## [0.4.2] - 2022-12-13 +### Fixed +- `zcash_client_sqlite::WalletDb::get_transparent_balances` no longer returns an + error if the wallet has no UTXOs. + +## [0.4.1] - 2022-12-06 +### Added +- `zcash_client_sqlite::DataConnStmtCache::advance_by_block` now generates a + `tracing` span, which can be used for profiling. + +## [0.4.0] - 2022-11-12 +### Added +- Implementations of `zcash_client_backend::data_api::WalletReadTransparent` + and `WalletWriteTransparent` have been added. These implementations + are available only when the `transparent-inputs` feature flag is + enabled. +- New error variants: + - `SqliteClientError::TransparentAddress`, to support handling of errors in + transparent address decoding. + - `SqliteClientError::RequestedRewindInvalid`, to report when requested + rewinds exceed supported bounds. + - `SqliteClientError::DiversifierIndexOutOfRange`, to report when the space + of available diversifier indices has been exhausted. + - `SqliteClientError::AccountIdDiscontinuity`, to report when a user attempts + to initialize the accounts table with a noncontiguous set of account identifiers. + - `SqliteClientError::AccountIdOutOfRange`, to report when the maximum account + identifier has been reached. + - `SqliteClientError::Protobuf`, to support handling of errors in serialized + protobuf data decoding. +- An `unstable` feature flag; this is added to parts of the API that may change + in any release. It enables `zcash_client_backend`'s `unstable` feature flag. +- New summary views that may be directly accessed in the sqlite database. + The structure of these views should be considered unstable; they may + be replaced by accessors provided by the data access API at some point + in the future: + - `v_transactions` + - `v_tx_received` + - `v_tx_sent` +- `zcash_client_sqlite::wallet::init::WalletMigrationError` +- A filesystem-backed `BlockSource` implementation + `zcash_client_sqlite::FsBlockDb`. This block source expects blocks to be + stored on disk in individual files named following the pattern + `/blocks/--compactblock`. A SQLite + database stored at `/blockmeta.sqlite`stores metadata for + this block source. + - `zcash_client_sqlite::chain::init::init_blockmeta_db` creates the required + metadata cache database. +- Implementations of `PartialEq`, `Eq`, `PartialOrd`, and `Ord` for `NoteId` + +### Changed +- Various **BREAKING CHANGES** have been made to the database tables. These will + require migrations, which may need to be performed in multiple steps. Migrations + will now be automatically performed for any user using + `zcash_client_sqlite::wallet::init_wallet_db` and it is recommended to use this + method to maintain the state of the database going forward. + - The `extfvk` column in the `accounts` table has been replaced by a `ufvk` + column. Values for this column should be derived from the wallet's seed and + the account number; the Sapling component of the resulting Unified Full + Viewing Key should match the old value in the `extfvk` column. + - The `address` and `transparent_address` columns of the `accounts` table have + been removed. + - A new `addresses` table stores Unified Addresses, keyed on their `account` + and `diversifier_index`, to enable storing diversifed Unified Addresses. + - Transparent addresses for an account should be obtained by extracting the + transparent receiver of a Unified Address for the account. + - A new non-null column, `output_pool` has been added to the `sent_notes` + table to enable distinguishing between Sapling and transparent outputs + (and in the future, outputs to other pools). Values for this column should + be assigned by inference from the address type in the stored data. +- MSRV is now 1.56.1. +- Bumped dependencies to `ff 0.12`, `group 0.12`, `jubjub 0.9`, + `zcash_primitives 0.9`, `zcash_client_backend 0.6`. - Renamed the following to use lower-case abbreviations (matching Rust naming conventions): - `zcash_client_sqlite::BlockDB` to `BlockDb` - `zcash_client_sqlite::WalletDB` to `WalletDb` - `zcash_client_sqlite::error::SqliteClientError::IncorrectHRPExtFVK` to `IncorrectHrpExtFvk`. +- The SQLite implementations of `zcash_client_backend::data_api::WalletRead` + and `WalletWrite` have been updated to reflect the changes to those + traits. +- `zcash_client_sqlite::wallet`: + - `get_spendable_notes` has been renamed to `get_spendable_sapling_notes`. + - `select_spendable_notes` has been renamed to `select_spendable_sapling_notes`. + - `get_spendable_sapling_notes` and `select_spendable_sapling_notes` have also + been changed to take a parameter that permits the caller to specify a set of + notes to exclude from consideration. + - `init_wallet_db` has been modified to take the wallet seed as an argument so + that it can correctly perform migrations that require re-deriving key + material. In particular for this upgrade, the seed is used to derive UFVKs + to replace the currently stored Sapling ExtFVKs (without losing information) + as part of the migration process. + +### Removed +- The following functions have been removed from the public interface of + `zcash_client_sqlite::wallet`. Prefer methods defined on + `zcash_client_backend::data_api::{WalletRead, WalletWrite}` instead. + - `get_extended_full_viewing_keys` (use `WalletRead::get_unified_full_viewing_keys` instead). + - `insert_sent_note` (use `WalletWrite::store_sent_tx` instead). + - `insert_sent_utxo` (use `WalletWrite::store_sent_tx` instead). + - `put_sent_note` (use `WalletWrite::store_decrypted_tx` instead). + - `put_sent_utxo` (use `WalletWrite::store_decrypted_tx` instead). + - `delete_utxos_above` (use `WalletWrite::rewind_to_height` instead). +- `zcash_client_sqlite::with_blocks` (use + `zcash_client_backend::data_api::BlockSource::with_blocks` instead). +- `zcash_client_sqlite::error::SqliteClientError` variants: + - `SqliteClientError::IncorrectHrpExtFvk` + - `SqliteClientError::Base58` + - `SqliteClientError::BackendError` + +### Fixed +- The `zcash_client_backend::data_api::WalletRead::get_address` implementation + for `zcash_client_sqlite::WalletDb` now correctly returns `Ok(None)` if the + account identifier does not correspond to a known account. + +### Deprecated +- A number of public API methods that are used internally to support the + `zcash_client_backend::data_api::{WalletRead, WalletWrite}` interfaces have + been deprecated, and will be removed from the public API in a future release. + Users should depend upon the versions of these methods exposed via the + `zcash_client_backend::data_api` traits mentioned above instead. + - Deprecated in `zcash_client_sqlite::wallet`: + - `get_address` + - `is_valid_account_extfvk` + - `get_balance` + - `get_balance_at` + - `get_sent_memo` + - `block_height_extrema` + - `get_tx_height` + - `get_block_hash` + - `get_rewind_height` + - `get_commitment_tree` + - `get_witnesses` + - `get_nullifiers` + - `insert_block` + - `put_tx_meta` + - `put_tx_data` + - `mark_sapling_note_spent` + - `put_receiverd_note` + - `insert_witness` + - `prune_witnesses` + - `update_expired_notes` + - `get_address` + - Deprecated in `zcash_client_sqlite::wallet::transact`: + - `get_spendable_sapling_notes` + - `select_spendable_sapling_notes` ## [0.3.0] - 2021-03-26 This release contains a major refactor of the APIs to leverage the new Data diff --git a/zcash_client_sqlite/Cargo.toml b/zcash_client_sqlite/Cargo.toml index 5e1efe0f63..fbc83d562b 100644 --- a/zcash_client_sqlite/Cargo.toml +++ b/zcash_client_sqlite/Cargo.toml @@ -1,35 +1,150 @@ [package] name = "zcash_client_sqlite" description = "An SQLite-based Zcash light client" -version = "0.3.0" +version = "0.14.0" authors = [ "Jack Grigg ", "Kris Nuttycombe " ] homepage = "https://github.com/zcash/librustzcash" -repository = "https://github.com/zcash/librustzcash" +repository.workspace = true readme = "README.md" -license = "MIT OR Apache-2.0" -edition = "2018" +license.workspace = true +edition.workspace = true +rust-version.workspace = true +categories.workspace = true + +[package.metadata.docs.rs] +# Manually specify features while `orchard` is not in the public API. +#all-features = true +features = [ + "multicore", + "test-dependencies", + "transparent-inputs", + "unstable", +] +rustdoc-args = ["--cfg", "docsrs"] [dependencies] -bech32 = "0.8" -bs58 = { version = "0.4", features = ["check"] } -ff = "0.8" -group = "0.8" -jubjub = "0.5.1" -protobuf = "2.20" -rand_core = "0.5.1" -rusqlite = { version = "0.24", features = ["bundled", "time"] } -time = "0.2" -zcash_client_backend = { version = "0.5", path = "../zcash_client_backend" } -zcash_primitives = { version = "0.5", path = "../zcash_primitives" } +zcash_address.workspace = true +zcash_client_backend = { workspace = true, features = ["unstable-serialization", "unstable-spanning-tree"] } +zcash_encoding.workspace = true +zcash_keys = { workspace = true, features = ["sapling"] } +zcash_primitives.workspace = true +zcash_protocol.workspace = true +zip32.workspace = true +transparent.workspace = true + +# Dependencies exposed in a public API: +# (Breaking upgrades to these require a breaking upgrade to this crate.) +# - Errors +bip32 = { workspace = true, optional = true } +bs58.workspace = true + +# - Logging and metrics +tracing.workspace = true + +# - Serialization +byteorder.workspace = true +nonempty.workspace = true +prost.workspace = true +group.workspace = true +jubjub.workspace = true +serde = { workspace = true, optional = true } + +# - Secret management +secrecy.workspace = true +subtle.workspace = true + +# - Static assertions +static_assertions.workspace = true + +# - Shielded protocols +orchard = { workspace = true, optional = true } +sapling.workspace = true + +# - Note commitment trees +incrementalmerkletree.workspace = true +shardtree = { workspace = true, features = ["legacy-api"] } + +# - SQLite databases +rusqlite = { workspace = true, features = ["time", "array", "uuid"] } +schemerz.workspace = true +schemerz-rusqlite.workspace = true +time.workspace = true +uuid = { workspace = true, features = ["v4"] } +regex = "1.4" + +# Dependencies used internally: +# (Breaking upgrades to these are usually backwards-compatible, but check MSRVs.) +document-features.workspace = true +maybe-rayon.workspace = true [dev-dependencies] -rand_core = "0.5.1" -tempfile = "3" -zcash_proofs = { version = "0.5", path = "../zcash_proofs" } +ambassador.workspace = true +assert_matches.workspace = true +bls12_381.workspace = true +incrementalmerkletree = { workspace = true, features = ["test-dependencies"] } +incrementalmerkletree-testing.workspace = true +pasta_curves.workspace = true +shardtree = { workspace = true, features = ["legacy-api", "test-dependencies"] } +orchard = { workspace = true, features = ["test-dependencies"] } +proptest.workspace = true +rand_chacha.workspace = true +rand_core.workspace = true +tempfile = "3.5.0" +zcash_keys = { workspace = true, features = ["test-dependencies"] } +zcash_note_encryption.workspace = true +zcash_proofs = { workspace = true, features = ["bundled-prover"] } +zcash_primitives = { workspace = true, features = ["test-dependencies", "non-standard-fees"] } +zcash_protocol = { workspace = true, features = ["local-consensus"] } +zcash_client_backend = { workspace = true, features = ["test-dependencies", "unstable-serialization", "unstable-spanning-tree"] } +zcash_address = { workspace = true, features = ["test-dependencies"] } +zip321 = { workspace = true } [features] -mainnet = [] -test-dependencies = ["zcash_client_backend/test-dependencies"] +default = ["multicore"] + +## Enables multithreading support for creating proofs and building subtrees. +multicore = ["maybe-rayon/threads", "zcash_primitives/multicore"] + +## Enables support for storing data related to the sending and receiving of +## Orchard funds. +orchard = ["dep:orchard", "zcash_client_backend/orchard", "zcash_keys/orchard"] + +## Exposes APIs that are useful for testing, such as `proptest` strategies. +test-dependencies = [ + "incrementalmerkletree/test-dependencies", + "zcash_primitives/test-dependencies", + "zcash_client_backend/test-dependencies", + "incrementalmerkletree/test-dependencies", +] + +## Enables receiving transparent funds and sending to transparent recipients +transparent-inputs = [ + "dep:bip32", + "transparent/transparent-inputs", + "zcash_keys/transparent-inputs", + "zcash_client_backend/transparent-inputs" +] + +## Enables `serde` derives for certain types. +serde = ["dep:serde", "uuid/serde"] + +#! ### Experimental features + +## Exposes unstable APIs. Their behaviour may change at any time. +unstable = ["zcash_client_backend/unstable"] + +## A feature used to isolate tests that are expensive to run. Test-only. +expensive-tests = [] + +## A feature used to enable PCZT-specific tests without interfering with the +## protocol-specific flags. Test-only. +pczt-tests = ["serde", "zcash_client_backend/pczt"] + +[lib] +bench = false + +[lints] +workspace = true diff --git a/zcash_client_sqlite/src/chain.rs b/zcash_client_sqlite/src/chain.rs index b416ebe889..a8835e2324 100644 --- a/zcash_client_sqlite/src/chain.rs +++ b/zcash_client_sqlite/src/chain.rs @@ -1,64 +1,319 @@ //! Functions for enforcing chain validity and handling chain reorgs. -use protobuf::Message; +use prost::Message; use rusqlite::params; -use zcash_primitives::consensus::BlockHeight; +use zcash_protocol::consensus::BlockHeight; -use zcash_client_backend::{data_api::error::Error, proto::compact_formats::CompactBlock}; +use zcash_client_backend::{data_api::chain::error::Error, proto::compact_formats::CompactBlock}; use crate::{error::SqliteClientError, BlockDb}; -pub mod init; +#[cfg(feature = "unstable")] +use { + crate::{BlockHash, FsBlockDb, FsBlockDbError}, + rusqlite::Connection, + std::fs::File, + std::io::Read, + std::path::{Path, PathBuf}, +}; -struct CompactBlockRow { - height: BlockHeight, - data: Vec, -} +pub mod init; +pub mod migrations; /// Implements a traversal of `limit` blocks of the block cache database. /// -/// Starting at `from_height`, the `with_row` callback is invoked -/// with each block retrieved from the backing store. If the `limit` -/// value provided is `None`, all blocks are traversed up to the +/// Starting at `from_height`, the `with_row` callback is invoked with each block retrieved from +/// the backing store. If the `limit` value provided is `None`, all blocks are traversed up to the /// maximum height. -pub fn with_blocks( - cache: &BlockDb, - from_height: BlockHeight, - limit: Option, +pub(crate) fn blockdb_with_blocks( + block_source: &BlockDb, + from_height: Option, + limit: Option, mut with_row: F, -) -> Result<(), SqliteClientError> +) -> Result<(), Error> where - F: FnMut(CompactBlock) -> Result<(), SqliteClientError>, + F: FnMut(CompactBlock) -> Result<(), Error>, { + fn to_chain_error>(err: E) -> Error { + Error::BlockSource(err.into()) + } + // Fetch the CompactBlocks we need to scan - let mut stmt_blocks = cache.0.prepare( - "SELECT height, data FROM compactblocks WHERE height > ? ORDER BY height ASC LIMIT ?", + let mut stmt_blocks = block_source + .0 + .prepare( + "SELECT height, data FROM compactblocks + WHERE height >= ? + ORDER BY height ASC LIMIT ?", + ) + .map_err(to_chain_error)?; + + let mut rows = stmt_blocks + .query(params![ + from_height.map_or(0u32, u32::from), + limit + .and_then(|l| u32::try_from(l).ok()) + .unwrap_or(u32::MAX) + ]) + .map_err(to_chain_error)?; + + // Only look for the `from_height` in the scanned blocks if it is set. + let mut from_height_found = from_height.is_none(); + while let Some(row) = rows.next().map_err(to_chain_error)? { + let height = BlockHeight::from_u32(row.get(0).map_err(to_chain_error)?); + if !from_height_found { + // We will only perform this check on the first row. + let from_height = from_height.expect("can only reach here if set"); + if from_height != height { + return Err(to_chain_error(SqliteClientError::CacheMiss(from_height))); + } else { + from_height_found = true; + } + } + + let data: Vec = row.get(1).map_err(to_chain_error)?; + let block = CompactBlock::decode(&data[..]).map_err(to_chain_error)?; + if block.height() != height { + return Err(to_chain_error(SqliteClientError::CorruptedData(format!( + "Block height {} did not match row's height field value {}", + block.height(), + height + )))); + } + + with_row(block)?; + } + + if !from_height_found { + let from_height = from_height.expect("can only reach here if set"); + return Err(to_chain_error(SqliteClientError::CacheMiss(from_height))); + } + + Ok(()) +} + +/// Data structure representing a row in the block metadata database. +#[cfg(feature = "unstable")] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct BlockMeta { + pub height: BlockHeight, + pub block_hash: BlockHash, + pub block_time: u32, + pub sapling_outputs_count: u32, + pub orchard_actions_count: u32, +} + +#[cfg(feature = "unstable")] +impl BlockMeta { + pub fn block_file_path>(&self, blocks_dir: &P) -> PathBuf { + blocks_dir.as_ref().join(Path::new(&format!( + "{}-{}-compactblock", + self.height, self.block_hash + ))) + } +} + +/// Inserts a batch of rows into the block metadata database. +#[cfg(feature = "unstable")] +pub(crate) fn blockmetadb_insert( + conn: &Connection, + block_meta: &[BlockMeta], +) -> Result<(), rusqlite::Error> { + use rusqlite::named_params; + + let mut stmt_insert = conn.prepare( + "INSERT INTO compactblocks_meta ( + height, + blockhash, + time, + sapling_outputs_count, + orchard_actions_count + ) + VALUES ( + :height, + :blockhash, + :time, + :sapling_outputs_count, + :orchard_actions_count + ) + ON CONFLICT (height) DO UPDATE + SET blockhash = :blockhash, + time = :time, + sapling_outputs_count = :sapling_outputs_count, + orchard_actions_count = :orchard_actions_count", )?; - let rows = stmt_blocks.query_map( - params![u32::from(from_height), limit.unwrap_or(u32::max_value()),], + conn.execute("BEGIN IMMEDIATE", [])?; + let result = block_meta + .iter() + .map(|m| { + stmt_insert.execute(named_params![ + ":height": u32::from(m.height), + ":blockhash": &m.block_hash.0[..], + ":time": m.block_time, + ":sapling_outputs_count": m.sapling_outputs_count, + ":orchard_actions_count": m.orchard_actions_count, + ]) + }) + .collect::, _>>(); + match result { + Ok(_) => { + conn.execute("COMMIT", [])?; + Ok(()) + } + Err(error) => { + match conn.execute("ROLLBACK", []) { + Ok(_) => Err(error), + Err(e) => + // Panicking here is probably the right thing to do, because it + // means the database is corrupt. + panic!( + "Rollback failed with error {} while attempting to recover from error {}; database is likely corrupt.", + e, + error + ) + } + } + } +} + +#[cfg(feature = "unstable")] +pub(crate) fn blockmetadb_truncate_to_height( + conn: &Connection, + block_height: BlockHeight, +) -> Result<(), rusqlite::Error> { + conn.prepare("DELETE FROM compactblocks_meta WHERE height > ?")? + .execute(params![u32::from(block_height)])?; + Ok(()) +} + +#[cfg(feature = "unstable")] +pub(crate) fn blockmetadb_get_max_cached_height( + conn: &Connection, +) -> Result, rusqlite::Error> { + conn.query_row("SELECT MAX(height) FROM compactblocks_meta", [], |row| { + // `SELECT MAX(_)` will always return a row, but it will return `null` if the + // table is empty, which has no integer type. We handle the optionality here. + let h: Option = row.get(0)?; + Ok(h.map(BlockHeight::from)) + }) +} + +/// Returns the metadata for the block with the given height, if it exists in the database. +#[cfg(feature = "unstable")] +pub(crate) fn blockmetadb_find_block( + conn: &Connection, + height: BlockHeight, +) -> Result, rusqlite::Error> { + use rusqlite::OptionalExtension; + + conn.query_row( + "SELECT blockhash, time, sapling_outputs_count, orchard_actions_count + FROM compactblocks_meta + WHERE height = ?", + [u32::from(height)], |row| { - Ok(CompactBlockRow { - height: BlockHeight::from_u32(row.get(0)?), - data: row.get(1)?, + Ok(BlockMeta { + height, + block_hash: BlockHash::from_slice(&row.get::<_, Vec<_>>(0)?), + block_time: row.get(1)?, + sapling_outputs_count: row.get(2)?, + orchard_actions_count: row.get(3)?, }) }, - )?; + ) + .optional() +} + +/// Implements a traversal of `limit` blocks of the filesystem-backed +/// block cache. +/// +/// Starting at `from_height`, the `with_row` callback is invoked with each block retrieved from +/// the backing store. If the `limit` value provided is `None`, all blocks are traversed up to the +/// maximum height for which metadata is available. +#[cfg(feature = "unstable")] +pub(crate) fn fsblockdb_with_blocks( + cache: &FsBlockDb, + from_height: Option, + limit: Option, + mut with_block: F, +) -> Result<(), Error> +where + F: FnMut(CompactBlock) -> Result<(), Error>, +{ + fn to_chain_error>(err: E) -> Error { + Error::BlockSource(err.into()) + } + + // Fetch the CompactBlocks we need to scan + let mut stmt_blocks = cache + .conn + .prepare( + "SELECT height, blockhash, time, sapling_outputs_count, orchard_actions_count + FROM compactblocks_meta + WHERE height >= ? + ORDER BY height ASC LIMIT ?", + ) + .map_err(to_chain_error)?; + + let rows = stmt_blocks + .query_map( + params![ + from_height.map_or(0u32, u32::from), + limit + .and_then(|l| u32::try_from(l).ok()) + .unwrap_or(u32::MAX) + ], + |row| { + Ok(BlockMeta { + height: BlockHeight::from_u32(row.get(0)?), + block_hash: BlockHash::from_slice(&row.get::<_, Vec<_>>(1)?), + block_time: row.get(2)?, + sapling_outputs_count: row.get(3)?, + orchard_actions_count: row.get(4)?, + }) + }, + ) + .map_err(to_chain_error)?; + // Only look for the `from_height` in the scanned blocks if it is set. + let mut from_height_found = from_height.is_none(); for row_result in rows { - let cbr = row_result?; - let block: CompactBlock = Message::parse_from_bytes(&cbr.data).map_err(Error::from)?; + let cbr = row_result.map_err(to_chain_error)?; + if !from_height_found { + // We will only perform this check on the first row. + let from_height = from_height.expect("can only reach here if set"); + if from_height != cbr.height { + return Err(to_chain_error(FsBlockDbError::CacheMiss(from_height))); + } else { + from_height_found = true; + } + } + + let mut block_file = + File::open(cbr.block_file_path(&cache.blocks_dir)).map_err(to_chain_error)?; + let mut block_data = vec![]; + block_file + .read_to_end(&mut block_data) + .map_err(to_chain_error)?; + + let block = CompactBlock::decode(&block_data[..]).map_err(to_chain_error)?; if block.height() != cbr.height { - return Err(SqliteClientError::CorruptedData(format!( + return Err(to_chain_error(FsBlockDbError::CorruptedData(format!( "Block height {} did not match row's height field value {}", block.height(), cbr.height - ))); + )))); } - with_row(block)?; + with_block(block)?; + } + + if !from_height_found { + let from_height = from_height.expect("can only reach here if set"); + return Err(to_chain_error(FsBlockDbError::CacheMiss(from_height))); } Ok(()) @@ -66,483 +321,99 @@ where #[cfg(test)] mod tests { - use tempfile::NamedTempFile; - - use zcash_primitives::{ - block::BlockHash, - transaction::components::Amount, - zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, - }; - - use zcash_client_backend::data_api::WalletRead; - use zcash_client_backend::data_api::{ - chain::{scan_cached_blocks, validate_chain}, - error::{ChainInvalid, Error}, - }; - - use crate::{ - chain::init::init_cache_database, - error::SqliteClientError, - tests::{ - self, fake_compact_block, fake_compact_block_spending, insert_into_cache, - sapling_activation_height, - }, - wallet::{ - get_balance, - init::{init_accounts_table, init_wallet_db}, - rewind_to_height, - }, - AccountId, BlockDb, NoteId, WalletDb, - }; + use zcash_client_backend::data_api::testing::sapling::SaplingPoolTester; + + use crate::testing; + + #[cfg(feature = "orchard")] + use zcash_client_backend::data_api::testing::orchard::OrchardPoolTester; #[test] - fn valid_chain_states() { - let cache_file = NamedTempFile::new().unwrap(); - let db_cache = BlockDb::for_path(cache_file.path()).unwrap(); - init_cache_database(&db_cache).unwrap(); - - let data_file = NamedTempFile::new().unwrap(); - let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&db_data).unwrap(); - - // Add an account to the wallet - let extsk = ExtendedSpendingKey::master(&[]); - let extfvk = ExtendedFullViewingKey::from(&extsk); - init_accounts_table(&db_data, &[extfvk.clone()]).unwrap(); - - // Empty chain should be valid - validate_chain( - &tests::network(), - &db_cache, - (&db_data).get_max_height_hash().unwrap(), - ) - .unwrap(); - - // Create a fake CompactBlock sending value to the address - let (cb, _) = fake_compact_block( - sapling_activation_height(), - BlockHash([0; 32]), - extfvk.clone(), - Amount::from_u64(5).unwrap(), - ); - insert_into_cache(&db_cache, &cb); - - // Cache-only chain should be valid - validate_chain( - &tests::network(), - &db_cache, - (&db_data).get_max_height_hash().unwrap(), - ) - .unwrap(); + fn valid_chain_states_sapling() { + testing::pool::valid_chain_states::() + } + + #[test] + #[cfg(feature = "orchard")] + fn valid_chain_states_orchard() { + testing::pool::valid_chain_states::() + } - // Scan the cache - let mut db_write = db_data.get_update_ops().unwrap(); - scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); + #[test] + #[cfg(feature = "orchard")] + fn invalid_chain_cache_disconnected_sapling() { + testing::pool::invalid_chain_cache_disconnected::() + } - // Data-only chain should be valid - validate_chain( - &tests::network(), - &db_cache, - (&db_data).get_max_height_hash().unwrap(), - ) - .unwrap(); - - // Create a second fake CompactBlock sending more value to the address - let (cb2, _) = fake_compact_block( - sapling_activation_height() + 1, - cb.hash(), - extfvk, - Amount::from_u64(7).unwrap(), - ); - insert_into_cache(&db_cache, &cb2); - - // Data+cache chain should be valid - validate_chain( - &tests::network(), - &db_cache, - (&db_data).get_max_height_hash().unwrap(), - ) - .unwrap(); + #[test] + #[cfg(feature = "orchard")] + fn invalid_chain_cache_disconnected_orchard() { + testing::pool::invalid_chain_cache_disconnected::() + } - // Scan the cache again - scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); + #[test] + fn data_db_truncation_sapling() { + testing::pool::data_db_truncation::() + } - // Data-only chain should be valid - validate_chain( - &tests::network(), - &db_cache, - (&db_data).get_max_height_hash().unwrap(), - ) - .unwrap(); + #[test] + #[cfg(feature = "orchard")] + fn data_db_truncation_orchard() { + testing::pool::data_db_truncation::() } #[test] - fn invalid_chain_cache_disconnected() { - let cache_file = NamedTempFile::new().unwrap(); - let db_cache = BlockDb::for_path(cache_file.path()).unwrap(); - init_cache_database(&db_cache).unwrap(); - - let data_file = NamedTempFile::new().unwrap(); - let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&db_data).unwrap(); - - // Add an account to the wallet - let extsk = ExtendedSpendingKey::master(&[]); - let extfvk = ExtendedFullViewingKey::from(&extsk); - init_accounts_table(&db_data, &[extfvk.clone()]).unwrap(); - - // Create some fake CompactBlocks - let (cb, _) = fake_compact_block( - sapling_activation_height(), - BlockHash([0; 32]), - extfvk.clone(), - Amount::from_u64(5).unwrap(), - ); - let (cb2, _) = fake_compact_block( - sapling_activation_height() + 1, - cb.hash(), - extfvk.clone(), - Amount::from_u64(7).unwrap(), - ); - insert_into_cache(&db_cache, &cb); - insert_into_cache(&db_cache, &cb2); - - // Scan the cache - let mut db_write = db_data.get_update_ops().unwrap(); - scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); - - // Data-only chain should be valid - validate_chain( - &tests::network(), - &db_cache, - (&db_data).get_max_height_hash().unwrap(), - ) - .unwrap(); - - // Create more fake CompactBlocks that don't connect to the scanned ones - let (cb3, _) = fake_compact_block( - sapling_activation_height() + 2, - BlockHash([1; 32]), - extfvk.clone(), - Amount::from_u64(8).unwrap(), - ); - let (cb4, _) = fake_compact_block( - sapling_activation_height() + 3, - cb3.hash(), - extfvk, - Amount::from_u64(3).unwrap(), - ); - insert_into_cache(&db_cache, &cb3); - insert_into_cache(&db_cache, &cb4); - - // Data+cache chain should be invalid at the data/cache boundary - match validate_chain( - &tests::network(), - &db_cache, - (&db_data).get_max_height_hash().unwrap(), - ) { - Err(SqliteClientError::BackendError(Error::InvalidChain(lower_bound, _))) => { - assert_eq!(lower_bound, sapling_activation_height() + 2) - } - _ => panic!(), - } + fn reorg_to_checkpoint_sapling() { + testing::pool::reorg_to_checkpoint::() } #[test] - fn invalid_chain_cache_reorg() { - let cache_file = NamedTempFile::new().unwrap(); - let db_cache = BlockDb::for_path(cache_file.path()).unwrap(); - init_cache_database(&db_cache).unwrap(); - - let data_file = NamedTempFile::new().unwrap(); - let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&db_data).unwrap(); - - // Add an account to the wallet - let extsk = ExtendedSpendingKey::master(&[]); - let extfvk = ExtendedFullViewingKey::from(&extsk); - init_accounts_table(&db_data, &[extfvk.clone()]).unwrap(); - - // Create some fake CompactBlocks - let (cb, _) = fake_compact_block( - sapling_activation_height(), - BlockHash([0; 32]), - extfvk.clone(), - Amount::from_u64(5).unwrap(), - ); - let (cb2, _) = fake_compact_block( - sapling_activation_height() + 1, - cb.hash(), - extfvk.clone(), - Amount::from_u64(7).unwrap(), - ); - insert_into_cache(&db_cache, &cb); - insert_into_cache(&db_cache, &cb2); - - // Scan the cache - let mut db_write = db_data.get_update_ops().unwrap(); - scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); - - // Data-only chain should be valid - validate_chain( - &tests::network(), - &db_cache, - (&db_data).get_max_height_hash().unwrap(), - ) - .unwrap(); - - // Create more fake CompactBlocks that contain a reorg - let (cb3, _) = fake_compact_block( - sapling_activation_height() + 2, - cb2.hash(), - extfvk.clone(), - Amount::from_u64(8).unwrap(), - ); - let (cb4, _) = fake_compact_block( - sapling_activation_height() + 3, - BlockHash([1; 32]), - extfvk, - Amount::from_u64(3).unwrap(), - ); - insert_into_cache(&db_cache, &cb3); - insert_into_cache(&db_cache, &cb4); - - // Data+cache chain should be invalid inside the cache - match validate_chain( - &tests::network(), - &db_cache, - (&db_data).get_max_height_hash().unwrap(), - ) { - Err(SqliteClientError::BackendError(Error::InvalidChain(lower_bound, _))) => { - assert_eq!(lower_bound, sapling_activation_height() + 3) - } - _ => panic!(), - } + #[cfg(feature = "orchard")] + fn reorg_to_checkpoint_orchard() { + testing::pool::reorg_to_checkpoint::() } #[test] - fn data_db_rewinding() { - let cache_file = NamedTempFile::new().unwrap(); - let db_cache = BlockDb::for_path(cache_file.path()).unwrap(); - init_cache_database(&db_cache).unwrap(); - - let data_file = NamedTempFile::new().unwrap(); - let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&db_data).unwrap(); - - // Add an account to the wallet - let extsk = ExtendedSpendingKey::master(&[]); - let extfvk = ExtendedFullViewingKey::from(&extsk); - init_accounts_table(&db_data, &[extfvk.clone()]).unwrap(); - - // Account balance should be zero - assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), Amount::zero()); - - // Create fake CompactBlocks sending value to the address - let value = Amount::from_u64(5).unwrap(); - let value2 = Amount::from_u64(7).unwrap(); - let (cb, _) = fake_compact_block( - sapling_activation_height(), - BlockHash([0; 32]), - extfvk.clone(), - value, - ); - - let (cb2, _) = - fake_compact_block(sapling_activation_height() + 1, cb.hash(), extfvk, value2); - insert_into_cache(&db_cache, &cb); - insert_into_cache(&db_cache, &cb2); - - // Scan the cache - let mut db_write = db_data.get_update_ops().unwrap(); - scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); - - // Account balance should reflect both received notes - assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value + value2); - - // "Rewind" to height of last scanned block - rewind_to_height(&db_data, sapling_activation_height() + 1).unwrap(); - - // Account balance should be unaltered - assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value + value2); - - // Rewind so that one block is dropped - rewind_to_height(&db_data, sapling_activation_height()).unwrap(); - - // Account balance should only contain the first received note - assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value); - - // Scan the cache again - scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); - - // Account balance should again reflect both received notes - assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value + value2); + fn scan_cached_blocks_allows_blocks_out_of_order_sapling() { + testing::pool::scan_cached_blocks_allows_blocks_out_of_order::() } #[test] - fn scan_cached_blocks_requires_sequential_blocks() { - let cache_file = NamedTempFile::new().unwrap(); - let db_cache = BlockDb::for_path(cache_file.path()).unwrap(); - init_cache_database(&db_cache).unwrap(); - - let data_file = NamedTempFile::new().unwrap(); - let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&db_data).unwrap(); - - // Add an account to the wallet - let extsk = ExtendedSpendingKey::master(&[]); - let extfvk = ExtendedFullViewingKey::from(&extsk); - init_accounts_table(&db_data, &[extfvk.clone()]).unwrap(); - - // Create a block with height SAPLING_ACTIVATION_HEIGHT - let value = Amount::from_u64(50000).unwrap(); - let (cb1, _) = fake_compact_block( - sapling_activation_height(), - BlockHash([0; 32]), - extfvk.clone(), - value, - ); - insert_into_cache(&db_cache, &cb1); - let mut db_write = db_data.get_update_ops().unwrap(); - scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); - assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value); - - // We cannot scan a block of height SAPLING_ACTIVATION_HEIGHT + 2 next - let (cb2, _) = fake_compact_block( - sapling_activation_height() + 1, - cb1.hash(), - extfvk.clone(), - value, - ); - let (cb3, _) = - fake_compact_block(sapling_activation_height() + 2, cb2.hash(), extfvk, value); - insert_into_cache(&db_cache, &cb3); - match scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None) { - Err(SqliteClientError::BackendError(e)) => { - assert_eq!( - e.to_string(), - ChainInvalid::block_height_discontinuity::( - sapling_activation_height() + 1, - sapling_activation_height() + 2 - ) - .to_string() - ); - } - Ok(_) | Err(_) => panic!("Should have failed"), - } + #[cfg(feature = "orchard")] + fn scan_cached_blocks_allows_blocks_out_of_order_orchard() { + testing::pool::scan_cached_blocks_allows_blocks_out_of_order::() + } + + #[test] + fn scan_cached_blocks_finds_received_notes_sapling() { + testing::pool::scan_cached_blocks_finds_received_notes::() + } + + #[test] + #[cfg(feature = "orchard")] + fn scan_cached_blocks_finds_received_notes_orchard() { + testing::pool::scan_cached_blocks_finds_received_notes::() + } + + #[test] + fn scan_cached_blocks_finds_change_notes_sapling() { + testing::pool::scan_cached_blocks_finds_change_notes::() + } - // If we add a block of height SAPLING_ACTIVATION_HEIGHT + 1, we can now scan both - insert_into_cache(&db_cache, &cb2); - scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); - assert_eq!( - get_balance(&db_data, AccountId(0)).unwrap(), - Amount::from_u64(150_000).unwrap() - ); + #[test] + #[cfg(feature = "orchard")] + fn scan_cached_blocks_finds_change_notes_orchard() { + testing::pool::scan_cached_blocks_finds_change_notes::() } #[test] - fn scan_cached_blocks_finds_received_notes() { - let cache_file = NamedTempFile::new().unwrap(); - let db_cache = BlockDb::for_path(cache_file.path()).unwrap(); - init_cache_database(&db_cache).unwrap(); - - let data_file = NamedTempFile::new().unwrap(); - let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&db_data).unwrap(); - - // Add an account to the wallet - let extsk = ExtendedSpendingKey::master(&[]); - let extfvk = ExtendedFullViewingKey::from(&extsk); - init_accounts_table(&db_data, &[extfvk.clone()]).unwrap(); - - // Account balance should be zero - assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), Amount::zero()); - - // Create a fake CompactBlock sending value to the address - let value = Amount::from_u64(5).unwrap(); - let (cb, _) = fake_compact_block( - sapling_activation_height(), - BlockHash([0; 32]), - extfvk.clone(), - value, - ); - insert_into_cache(&db_cache, &cb); - - // Scan the cache - let mut db_write = db_data.get_update_ops().unwrap(); - scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); - - // Account balance should reflect the received note - assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value); - - // Create a second fake CompactBlock sending more value to the address - let value2 = Amount::from_u64(7).unwrap(); - let (cb2, _) = - fake_compact_block(sapling_activation_height() + 1, cb.hash(), extfvk, value2); - insert_into_cache(&db_cache, &cb2); - - // Scan the cache again - scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); - - // Account balance should reflect both received notes - assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value + value2); + fn scan_cached_blocks_detects_spends_out_of_order_sapling() { + testing::pool::scan_cached_blocks_detects_spends_out_of_order::() } #[test] - fn scan_cached_blocks_finds_change_notes() { - let cache_file = NamedTempFile::new().unwrap(); - let db_cache = BlockDb::for_path(cache_file.path()).unwrap(); - init_cache_database(&db_cache).unwrap(); - - let data_file = NamedTempFile::new().unwrap(); - let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&db_data).unwrap(); - - // Add an account to the wallet - let extsk = ExtendedSpendingKey::master(&[]); - let extfvk = ExtendedFullViewingKey::from(&extsk); - init_accounts_table(&db_data, &[extfvk.clone()]).unwrap(); - - // Account balance should be zero - assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), Amount::zero()); - - // Create a fake CompactBlock sending value to the address - let value = Amount::from_u64(5).unwrap(); - let (cb, nf) = fake_compact_block( - sapling_activation_height(), - BlockHash([0; 32]), - extfvk.clone(), - value, - ); - insert_into_cache(&db_cache, &cb); - - // Scan the cache - let mut db_write = db_data.get_update_ops().unwrap(); - scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); - - // Account balance should reflect the received note - assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value); - - // Create a second fake CompactBlock spending value from the address - let extsk2 = ExtendedSpendingKey::master(&[0]); - let to2 = extsk2.default_address().unwrap().1; - let value2 = Amount::from_u64(2).unwrap(); - insert_into_cache( - &db_cache, - &fake_compact_block_spending( - sapling_activation_height() + 1, - cb.hash(), - (nf, value), - extfvk, - to2, - value2, - ), - ); - - // Scan the cache again - scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); - - // Account balance should equal the change - assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value - value2); + #[cfg(feature = "orchard")] + fn scan_cached_blocks_detects_spends_out_of_order_orchard() { + testing::pool::scan_cached_blocks_detects_spends_out_of_order::() } } diff --git a/zcash_client_sqlite/src/chain/init.rs b/zcash_client_sqlite/src/chain/init.rs index ed60b452e4..944d724fa0 100644 --- a/zcash_client_sqlite/src/chain/init.rs +++ b/zcash_client_sqlite/src/chain/init.rs @@ -1,9 +1,14 @@ //! Functions for initializing the various databases. - -use rusqlite::NO_PARAMS; - use crate::BlockDb; +#[cfg(feature = "unstable")] +use { + super::migrations, + crate::FsBlockDb, + schemerz::{Migrator, MigratorError}, + schemerz_rusqlite::RusqliteAdapter, +}; + /// Sets up the internal structure of the cache database. /// /// # Examples @@ -25,7 +30,41 @@ pub fn init_cache_database(db_cache: &BlockDb) -> Result<(), rusqlite::Error> { height INTEGER PRIMARY KEY, data BLOB NOT NULL )", - NO_PARAMS, + [], )?; Ok(()) } + +/// Sets up the internal structure of the metadata cache database. +/// +/// This will automatically apply any available migrations that have not yet been applied to the +/// database as part of its operation. +/// +/// # Examples +/// +/// ``` +/// use tempfile::{tempdir, NamedTempFile}; +/// use zcash_client_sqlite::{ +/// FsBlockDb, +/// chain::init::init_blockmeta_db, +/// }; +/// +/// let cache_file = NamedTempFile::new().unwrap(); +/// let blocks_dir = tempdir().unwrap(); +/// let mut db = FsBlockDb::for_path(blocks_dir.path()).unwrap(); +/// init_blockmeta_db(&mut db).unwrap(); +/// ``` +#[cfg(feature = "unstable")] +pub fn init_blockmeta_db( + db: &mut FsBlockDb, +) -> Result<(), MigratorError> { + let adapter = RusqliteAdapter::new(&mut db.conn, Some("schemer_migrations".to_string())); + adapter.init().expect("Migrations table setup succeeds."); + + let mut migrator = Migrator::new(adapter); + migrator + .register_multiple(migrations::blockmeta::all_migrations().into_iter()) + .expect("Migration registration should have been successful."); + migrator.up(None)?; + Ok(()) +} diff --git a/zcash_client_sqlite/src/chain/migrations.rs b/zcash_client_sqlite/src/chain/migrations.rs new file mode 100644 index 0000000000..0bc8f92a33 --- /dev/null +++ b/zcash_client_sqlite/src/chain/migrations.rs @@ -0,0 +1 @@ +pub mod blockmeta; diff --git a/zcash_client_sqlite/src/chain/migrations/blockmeta.rs b/zcash_client_sqlite/src/chain/migrations/blockmeta.rs new file mode 100644 index 0000000000..6ab2387c6f --- /dev/null +++ b/zcash_client_sqlite/src/chain/migrations/blockmeta.rs @@ -0,0 +1,53 @@ +use schemerz_rusqlite::RusqliteMigration; + +pub fn all_migrations() -> Vec>> { + vec![Box::new(init::Migration {})] +} + +pub mod init { + use rusqlite::{self}; + use schemerz::{self, migration}; + use schemerz_rusqlite::RusqliteMigration; + use uuid::Uuid; + + pub struct Migration; + + /// The migration that added the `compactblocks_meta` table. + /// + /// 68525b40-36e5-46aa-a765-720f8389b99d + pub const MIGRATION_ID: Uuid = Uuid::from_fields( + 0x68525b40, + 0x36e5, + 0x46aa, + b"\xa7\x65\x72\x0f\x83\x89\xb9\x9d", + ); + + migration!( + Migration, + MIGRATION_ID, + [], + "Initialize the cachemeta database." + ); + + impl RusqliteMigration for Migration { + type Error = rusqlite::Error; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), Self::Error> { + transaction.execute_batch( + "CREATE TABLE compactblocks_meta ( + height INTEGER PRIMARY KEY, + blockhash BLOB NOT NULL, + time INTEGER NOT NULL, + sapling_outputs_count INTEGER NOT NULL, + orchard_actions_count INTEGER NOT NULL + )", + )?; + Ok(()) + } + + fn down(&self, transaction: &rusqlite::Transaction) -> Result<(), Self::Error> { + transaction.execute_batch("DROP TABLE compactblocks_meta;")?; + Ok(()) + } + } +} diff --git a/zcash_client_sqlite/src/error.rs b/zcash_client_sqlite/src/error.rs index e83c20e5dc..7c9e48cc39 100644 --- a/zcash_client_sqlite/src/error.rs +++ b/zcash_client_sqlite/src/error.rs @@ -3,9 +3,19 @@ use std::error; use std::fmt; -use zcash_client_backend::data_api; +use shardtree::error::ShardTreeError; +use zcash_address::ParseError; +use zcash_client_backend::data_api::NoteFilter; +use zcash_keys::keys::AddressGenerationError; +use zcash_protocol::{consensus::BlockHeight, value::BalanceError, PoolType}; -use crate::NoteId; +use crate::{wallet::commitment_tree, AccountUuid}; + +#[cfg(feature = "transparent-inputs")] +use { + ::transparent::address::TransparentAddress, zcash_keys::encoding::TransparentCodecError, + zcash_primitives::transaction::TxId, +}; /// The primary error type for the SQLite wallet backend. #[derive(Debug)] @@ -13,24 +23,26 @@ pub enum SqliteClientError { /// Decoding of a stored value from its serialized form has failed. CorruptedData(String), - /// Decoding of the extended full viewing key has failed (for the specified network) - IncorrectHrpExtFvk, + /// An error occurred decoding a protobuf message. + Protobuf(prost::DecodeError), /// The rcm value for a note cannot be decoded to a valid JubJub point. InvalidNote, - /// The note id associated with a witness being stored corresponds to a - /// sent note, not a received note. - InvalidNoteId, - /// Illegal attempt to reinitialize an already-initialized wallet database. TableNotEmpty, - /// Bech32 decoding error - Bech32(bech32::Error), + /// A Zcash key or address decoding error + DecodingError(ParseError), - /// Base58 decoding error - Base58(bs58::decode::Error), + /// An error produced in legacy transparent address derivation + #[cfg(feature = "transparent-inputs")] + TransparentDerivation(bip32::Error), + + /// An error encountered in decoding a transparent address from its + /// serialized form. + #[cfg(feature = "transparent-inputs")] + TransparentAddress(TransparentCodecError), /// Wrapper for rusqlite errors. DbError(rusqlite::Error), @@ -39,19 +51,94 @@ pub enum SqliteClientError { Io(std::io::Error), /// A received memo cannot be interpreted as a UTF-8 string. - InvalidMemo(zcash_primitives::memo::Error), + InvalidMemo(zcash_protocol::memo::Error), + + /// An attempt to update block data would overwrite the current hash for a block with a + /// different hash. This indicates that a required rewind was not performed. + BlockConflict(BlockHeight), + + /// A range of blocks provided to the database as a unit was non-sequential + NonSequentialBlocks, + + /// A requested rewind would violate invariants of the storage layer. The payload returned with + /// this error is (safe rewind height, requested height). If no safe rewind height can be + /// determined, the safe rewind height member will be `None`. + RequestedRewindInvalid { + safe_rewind_height: Option, + requested_height: BlockHeight, + }, + + /// An error occurred in generating a Zcash address. + AddressGeneration(AddressGenerationError), + + /// The account for which information was requested does not belong to the wallet. + AccountUnknown, + + /// The account being added collides with an existing account in the wallet with the given ID. + /// The collision can be on the seed and ZIP-32 account index, or a shared FVK component. + AccountCollision(AccountUuid), + + /// The account was imported, and ZIP-32 derivation information is not known for it. + UnknownZip32Derivation, + + /// An error occurred deriving a spending key from a seed and a ZIP-32 account index. + KeyDerivationError(zip32::AccountId), + + /// An error occurred while processing an account due to a failure in deriving the account's keys. + BadAccountData(String), + + /// A caller attempted to construct a new account with an invalid ZIP 32 account identifier. + Zip32AccountIndexOutOfRange, + + /// The address associated with a record being inserted was not recognized as + /// belonging to the wallet. + #[cfg(feature = "transparent-inputs")] + AddressNotRecognized(TransparentAddress), + + /// An error occurred in inserting data into or accessing data from one of the wallet's note + /// commitment trees. + CommitmentTree(ShardTreeError), + + /// The block at the specified height was not available from the block cache. + CacheMiss(BlockHeight), - /// Wrapper for errors from zcash_client_backend - BackendError(data_api::error::Error), + /// The height of the chain was not available; a call to [`WalletWrite::update_chain_tip`] is + /// required before the requested operation can succeed. + /// + /// [`WalletWrite::update_chain_tip`]: + /// zcash_client_backend::data_api::WalletWrite::update_chain_tip + ChainHeightUnknown, + + /// Unsupported pool type + UnsupportedPoolType(PoolType), + + /// An error occurred in computing wallet balance + BalanceError(BalanceError), + + /// A note selection query contained an invalid constant or was otherwise not supported. + NoteFilterInvalid(NoteFilter), + + /// The proposal cannot be constructed until transactions with previously reserved + /// ephemeral address outputs have been mined. The parameters are the account UUID and + /// the index that could not safely be reserved. + #[cfg(feature = "transparent-inputs")] + ReachedGapLimit(AccountUuid, u32), + + /// An ephemeral address would be reused. The parameters are the address in string + /// form, and the txid of the earliest transaction in which it is known to have been + /// used. + #[cfg(feature = "transparent-inputs")] + EphemeralAddressReuse(String, TxId), } impl error::Error for SqliteClientError { fn source(&self) -> Option<&(dyn error::Error + 'static)> { match &self { SqliteClientError::InvalidMemo(e) => Some(e), - SqliteClientError::Bech32(e) => Some(e), SqliteClientError::DbError(e) => Some(e), SqliteClientError::Io(e) => Some(e), + SqliteClientError::BalanceError(e) => Some(e), + SqliteClientError::AddressGeneration(e) => Some(e), _ => None, } } @@ -63,16 +150,47 @@ impl fmt::Display for SqliteClientError { SqliteClientError::CorruptedData(reason) => { write!(f, "Data DB is corrupted: {}", reason) } - SqliteClientError::IncorrectHrpExtFvk => write!(f, "Incorrect HRP for extfvk"), + SqliteClientError::Protobuf(e) => write!(f, "Failed to parse protobuf-encoded record: {}", e), SqliteClientError::InvalidNote => write!(f, "Invalid note"), - SqliteClientError::InvalidNoteId => write!(f, "The note ID associated with an inserted witness must correspond to a received note."), - SqliteClientError::Bech32(e) => write!(f, "{}", e), - SqliteClientError::Base58(e) => write!(f, "{}", e), + SqliteClientError::RequestedRewindInvalid { safe_rewind_height, requested_height } => write!( + f, + "A rewind for your wallet may only target height {} or greater; the requested height was {}.", + safe_rewind_height.map_or("".to_owned(), |h0| format!("{}", h0)), + requested_height + ), + SqliteClientError::DecodingError(e) => write!(f, "{}", e), + #[cfg(feature = "transparent-inputs")] + SqliteClientError::TransparentDerivation(e) => write!(f, "{:?}", e), + #[cfg(feature = "transparent-inputs")] + SqliteClientError::TransparentAddress(e) => write!(f, "{}", e), SqliteClientError::TableNotEmpty => write!(f, "Table is not empty"), SqliteClientError::DbError(e) => write!(f, "{}", e), SqliteClientError::Io(e) => write!(f, "{}", e), SqliteClientError::InvalidMemo(e) => write!(f, "{}", e), - SqliteClientError::BackendError(e) => write!(f, "{}", e), + SqliteClientError::BlockConflict(h) => write!(f, "A block hash conflict occurred at height {}; rewind required.", u32::from(*h)), + SqliteClientError::NonSequentialBlocks => write!(f, "`put_blocks` requires that the provided block range be sequential"), + SqliteClientError::AddressGeneration(e) => write!(f, "{}", e), + SqliteClientError::AccountUnknown => write!(f, "The account with the given ID does not belong to this wallet."), + SqliteClientError::UnknownZip32Derivation => write!(f, "ZIP-32 derivation information is not known for this account."), + SqliteClientError::KeyDerivationError(zip32_index) => write!(f, "Key derivation failed for ZIP 32 account index {}", u32::from(*zip32_index)), + SqliteClientError::BadAccountData(e) => write!(f, "Failed to add account: {}", e), + SqliteClientError::Zip32AccountIndexOutOfRange => write!(f, "ZIP 32 account identifiers must be less than 0x7FFFFFFF."), + SqliteClientError::AccountCollision(account_uuid) => write!(f, "An account corresponding to the data provided already exists in the wallet with UUID {account_uuid:?}."), + #[cfg(feature = "transparent-inputs")] + SqliteClientError::AddressNotRecognized(_) => write!(f, "The address associated with a received txo is not identifiable as belonging to the wallet."), + SqliteClientError::CommitmentTree(err) => write!(f, "An error occurred accessing or updating note commitment tree data: {}.", err), + SqliteClientError::CacheMiss(height) => write!(f, "Requested height {} does not exist in the block cache.", height), + SqliteClientError::ChainHeightUnknown => write!(f, "Chain height unknown; please call `update_chain_tip`"), + SqliteClientError::UnsupportedPoolType(t) => write!(f, "Pool type is not currently supported: {}", t), + SqliteClientError::BalanceError(e) => write!(f, "Balance error: {}", e), + SqliteClientError::NoteFilterInvalid(s) => write!(f, "Could not evaluate filter query: {:?}", s), + #[cfg(feature = "transparent-inputs")] + SqliteClientError::ReachedGapLimit(account_id, bad_index) => write!(f, + "The proposal cannot be constructed until transactions with previously reserved ephemeral address outputs have been mined. \ + The ephemeral address in account {account_id:?} at index {bad_index} could not be safely reserved.", + ), + #[cfg(feature = "transparent-inputs")] + SqliteClientError::EphemeralAddressReuse(address_str, txid) => write!(f, "The ephemeral address {address_str} previously used in txid {txid} would be reused."), } } } @@ -88,27 +206,52 @@ impl From for SqliteClientError { SqliteClientError::Io(e) } } +impl From for SqliteClientError { + fn from(e: ParseError) -> Self { + SqliteClientError::DecodingError(e) + } +} + +impl From for SqliteClientError { + fn from(e: prost::DecodeError) -> Self { + SqliteClientError::Protobuf(e) + } +} -impl From for SqliteClientError { - fn from(e: bech32::Error) -> Self { - SqliteClientError::Bech32(e) +#[cfg(feature = "transparent-inputs")] +impl From for SqliteClientError { + fn from(e: bip32::Error) -> Self { + SqliteClientError::TransparentDerivation(e) } } -impl From for SqliteClientError { - fn from(e: bs58::decode::Error) -> Self { - SqliteClientError::Base58(e) +#[cfg(feature = "transparent-inputs")] +impl From for SqliteClientError { + fn from(e: TransparentCodecError) -> Self { + SqliteClientError::TransparentAddress(e) } } -impl From for SqliteClientError { - fn from(e: zcash_primitives::memo::Error) -> Self { +impl From for SqliteClientError { + fn from(e: zcash_protocol::memo::Error) -> Self { SqliteClientError::InvalidMemo(e) } } -impl From> for SqliteClientError { - fn from(e: data_api::error::Error) -> Self { - SqliteClientError::BackendError(e) +impl From> for SqliteClientError { + fn from(e: ShardTreeError) -> Self { + SqliteClientError::CommitmentTree(e) + } +} + +impl From for SqliteClientError { + fn from(e: BalanceError) -> Self { + SqliteClientError::BalanceError(e) + } +} + +impl From for SqliteClientError { + fn from(e: AddressGenerationError) -> Self { + SqliteClientError::AddressGeneration(e) } } diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index 0d9047574b..46d4c9f568 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -18,502 +18,1681 @@ //! **MUST NOT** write to the database without using these APIs. Callers **MAY** read //! the database directly in order to extract information for display to users. //! -//! # Features -//! -//! The `mainnet` feature configures the light client for use with the Zcash mainnet. By -//! default, the light client is configured for use with the Zcash testnet. +//! ## Feature flags +#![doc = document_features::document_features!()] //! //! [`WalletRead`]: zcash_client_backend::data_api::WalletRead //! [`WalletWrite`]: zcash_client_backend::data_api::WalletWrite -//! [`BlockSource`]: zcash_client_backend::data_api::BlockSource +//! [`BlockSource`]: zcash_client_backend::data_api::chain::BlockSource //! [`CompactBlock`]: zcash_client_backend::proto::compact_formats::CompactBlock //! [`init_cache_database`]: crate::chain::init::init_cache_database +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] // Catch documentation errors caused by code changes. -#![deny(broken_intra_doc_links)] - -use std::collections::HashMap; -use std::fmt; -use std::path::Path; - -use rusqlite::{Connection, Statement, NO_PARAMS}; +#![deny(rustdoc::broken_intra_doc_links)] + +use incrementalmerkletree::{Marking, Position, Retention}; +use nonempty::NonEmpty; +use rusqlite::{self, Connection}; +use secrecy::{ExposeSecret, SecretVec}; +use shardtree::{error::ShardTreeError, ShardTree}; +use std::{ + borrow::{Borrow, BorrowMut}, + collections::HashMap, + convert::AsRef, + fmt, + num::NonZeroU32, + ops::Range, + path::Path, +}; +use subtle::ConditionallySelectable; +use tracing::{debug, trace, warn}; +use uuid::Uuid; +use zcash_client_backend::{ + data_api::{ + self, + chain::{BlockSource, ChainState, CommitmentTreeRoot}, + scanning::{ScanPriority, ScanRange}, + Account, AccountBirthday, AccountMeta, AccountPurpose, AccountSource, BlockMetadata, + DecryptedTransaction, InputSource, NoteFilter, NullifierQuery, ScannedBlock, SeedRelevance, + SentTransaction, SpendableNotes, TransactionDataRequest, WalletCommitmentTrees, WalletRead, + WalletSummary, WalletWrite, Zip32Derivation, SAPLING_SHARD_HEIGHT, + }, + proto::compact_formats::CompactBlock, + wallet::{Note, NoteId, ReceivedNote, WalletTransparentOutput}, + TransferType, +}; +use zcash_keys::{ + address::UnifiedAddress, + keys::{ + AddressGenerationError, ReceiverRequirement, UnifiedAddressRequest, UnifiedFullViewingKey, + UnifiedSpendingKey, + }, +}; use zcash_primitives::{ block::BlockHash, + transaction::{Transaction, TxId}, +}; +use zcash_protocol::{ consensus::{self, BlockHeight}, memo::Memo, - merkle_tree::{CommitmentTree, IncrementalWitness}, - sapling::{Node, Nullifier, PaymentAddress}, - transaction::{components::Amount, TxId}, - zip32::ExtendedFullViewingKey, + value::Zatoshis, + ShieldedProtocol, }; +use zip32::{fingerprint::SeedFingerprint, DiversifierIndex}; -use zcash_client_backend::{ - data_api::{ - BlockSource, PrunedBlock, ReceivedTransaction, SentTransaction, WalletRead, WalletWrite, - }, - encoding::encode_payment_address, - proto::compact_formats::CompactBlock, - wallet::{AccountId, SpendableNote}, +use crate::{error::SqliteClientError, wallet::commitment_tree::SqliteShardStore}; + +#[cfg(any(test, feature = "test-dependencies", not(feature = "orchard")))] +use zcash_protocol::PoolType; + +#[cfg(feature = "orchard")] +use { + incrementalmerkletree::frontier::Frontier, + shardtree::store::{Checkpoint, ShardStore}, + std::collections::BTreeMap, + zcash_client_backend::data_api::ORCHARD_SHARD_HEIGHT, +}; + +#[cfg(feature = "transparent-inputs")] +use { + ::transparent::{address::TransparentAddress, bundle::OutPoint, keys::NonHardenedChildIndex}, + zcash_client_backend::wallet::TransparentAddressMetadata, + zcash_keys::encoding::AddressCodec, +}; + +#[cfg(feature = "multicore")] +use maybe_rayon::{ + prelude::{IndexedParallelIterator, ParallelIterator}, + slice::ParallelSliceMut, }; -use crate::error::SqliteClientError; +#[cfg(any(test, feature = "test-dependencies"))] +use { + zcash_client_backend::data_api::{testing::TransactionSummary, OutputOfSentTx, WalletTest}, + zcash_keys::address::Address, +}; + +/// `maybe-rayon` doesn't provide this as a fallback, so we have to. +#[cfg(not(feature = "multicore"))] +trait ParallelSliceMut { + fn par_chunks_mut(&mut self, chunk_size: usize) -> std::slice::ChunksMut<'_, T>; +} +#[cfg(not(feature = "multicore"))] +impl ParallelSliceMut for [T] { + fn par_chunks_mut(&mut self, chunk_size: usize) -> std::slice::ChunksMut<'_, T> { + self.chunks_mut(chunk_size) + } +} + +#[cfg(feature = "unstable")] +use { + crate::chain::{fsblockdb_with_blocks, BlockMeta}, + std::path::PathBuf, + std::{fs, io}, +}; pub mod chain; pub mod error; pub mod wallet; +use wallet::{ + commitment_tree::{self, put_shard_roots}, + common::spendable_notes_meta, + SubtreeProgressEstimator, +}; + +#[cfg(test)] +mod testing; + +/// The maximum number of blocks the wallet is allowed to rewind. This is +/// consistent with the bound in zcashd, and allows block data deeper than +/// this delta from the chain tip to be pruned. +pub(crate) const PRUNING_DEPTH: u32 = 100; + +/// The number of blocks to verify ahead when the chain tip is updated. +pub(crate) const VERIFY_LOOKAHEAD: u32 = 10; + +pub(crate) const SAPLING_TABLES_PREFIX: &str = "sapling"; + +#[cfg(feature = "orchard")] +pub(crate) const ORCHARD_TABLES_PREFIX: &str = "orchard"; + +#[cfg(not(feature = "orchard"))] +pub(crate) const UA_ORCHARD: ReceiverRequirement = ReceiverRequirement::Omit; +#[cfg(feature = "orchard")] +pub(crate) const UA_ORCHARD: ReceiverRequirement = ReceiverRequirement::Require; + +#[cfg(not(feature = "transparent-inputs"))] +pub(crate) const UA_TRANSPARENT: ReceiverRequirement = ReceiverRequirement::Omit; +#[cfg(feature = "transparent-inputs")] +pub(crate) const UA_TRANSPARENT: ReceiverRequirement = ReceiverRequirement::Require; + +/// Unique identifier for a specific account tracked by a [`WalletDb`]. +/// +/// Account identifiers are "one-way stable": a given identifier always points to a +/// specific viewing key within a specific [`WalletDb`] instance, but the same viewing key +/// may have multiple account identifiers over time. In particular, this crate upholds the +/// following properties: +/// +/// - When an account starts being tracked within a [`WalletDb`] instance (via APIs like +/// [`WalletWrite::create_account`], [`WalletWrite::import_account_hd`], or +/// [`WalletWrite::import_account_ufvk`]), a new `AccountUuid` is generated. +/// - If an `AccountUuid` is present within a [`WalletDb`], it always points to the same +/// account. +/// +/// What this means is that account identifiers are not stable across "wallet recreation +/// events". Examples of these include: +/// - Restoring a wallet from a backed-up seed. +/// - Importing the same viewing key into two different wallet instances. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct AccountUuid(#[cfg_attr(feature = "serde", serde(with = "uuid::serde::compact"))] Uuid); + +impl ConditionallySelectable for AccountUuid { + fn conditional_select(a: &Self, b: &Self, choice: subtle::Choice) -> Self { + AccountUuid(Uuid::from_u128( + ConditionallySelectable::conditional_select(&a.0.as_u128(), &b.0.as_u128(), choice), + )) + } +} + +impl AccountUuid { + /// Constructs an `AccountUuid` from a bare [`Uuid`] value. + /// + /// The resulting identifier is not guaranteed to correspond to any account stored in + /// a [`WalletDb`]. + pub fn from_uuid(value: Uuid) -> Self { + AccountUuid(value) + } + + /// Exposes the opaque account identifier from its typesafe wrapper. + pub fn expose_uuid(&self) -> Uuid { + self.0 + } +} -/// A newtype wrapper for sqlite primary key values for the notes -/// table. -#[derive(Debug, Copy, Clone)] -pub enum NoteId { - SentNoteId(i64), - ReceivedNoteId(i64), +/// A typesafe wrapper for the primary key identifier for a row in the `accounts` table. +/// +/// This is an ephemeral value for efficiently and generically working with accounts in a +/// [`WalletDb`]. To reference accounts in external contexts, use [`AccountUuid`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)] +pub(crate) struct AccountRef(u32); + +/// This implementation is retained under `#[cfg(test)]` for pre-AccountUuid testing. +#[cfg(test)] +impl ConditionallySelectable for AccountRef { + fn conditional_select(a: &Self, b: &Self, choice: subtle::Choice) -> Self { + AccountRef(ConditionallySelectable::conditional_select( + &a.0, &b.0, choice, + )) + } } -impl fmt::Display for NoteId { +/// An opaque type for received note identifiers. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct ReceivedNoteId(pub(crate) ShieldedProtocol, pub(crate) i64); + +impl fmt::Display for ReceivedNoteId { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - NoteId::SentNoteId(id) => write!(f, "Sent Note {}", id), - NoteId::ReceivedNoteId(id) => write!(f, "Received Note {}", id), + ReceivedNoteId(protocol, id) => write!(f, "Received {:?} Note: {}", protocol, id), } } } +/// A newtype wrapper for sqlite primary key values for the utxos table. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct UtxoId(pub i64); + +/// A newtype wrapper for sqlite primary key values for the transactions table. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +struct TxRef(pub i64); + /// A wrapper for the SQLite connection to the wallet database. -pub struct WalletDb

{ - conn: Connection, +pub struct WalletDb { + conn: C, params: P, } -impl WalletDb

{ +/// A wrapper for a SQLite transaction affecting the wallet database. +pub struct SqlTransaction<'conn>(pub(crate) &'conn rusqlite::Transaction<'conn>); + +impl Borrow for SqlTransaction<'_> { + fn borrow(&self) -> &rusqlite::Connection { + self.0 + } +} + +impl WalletDb { /// Construct a connection to the wallet database stored at the specified path. pub fn for_path>(path: F, params: P) -> Result { - Connection::open(path).map(move |conn| WalletDb { conn, params }) + Connection::open(path).and_then(move |conn| { + rusqlite::vtab::array::load_module(&conn)?; + Ok(WalletDb { conn, params }) + }) } +} - /// Given a wallet database connection, obtain a handle for the write operations - /// for that database. This operation may eagerly initialize and cache sqlite - /// prepared statements that are used in write operations. - pub fn get_update_ops(&self) -> Result, SqliteClientError> { - Ok( - DataConnStmtCache { - wallet_db: self, - stmt_insert_block: self.conn.prepare( - "INSERT INTO blocks (height, hash, time, sapling_tree) - VALUES (?, ?, ?, ?)", - )?, - stmt_insert_tx_meta: self.conn.prepare( - "INSERT INTO transactions (txid, block, tx_index) - VALUES (?, ?, ?)", - )?, - stmt_update_tx_meta: self.conn.prepare( - "UPDATE transactions - SET block = ?, tx_index = ? WHERE txid = ?", - )?, - stmt_insert_tx_data: self.conn.prepare( - "INSERT INTO transactions (txid, created, expiry_height, raw) - VALUES (?, ?, ?, ?)", - )?, - stmt_update_tx_data: self.conn.prepare( - "UPDATE transactions - SET expiry_height = ?, raw = ? WHERE txid = ?", - )?, - stmt_select_tx_ref: self.conn.prepare( - "SELECT id_tx FROM transactions WHERE txid = ?", - )?, - stmt_mark_recived_note_spent: self.conn.prepare( - "UPDATE received_notes SET spent = ? WHERE nf = ?" - )?, - stmt_insert_received_note: self.conn.prepare( - "INSERT INTO received_notes (tx, output_index, account, diversifier, value, rcm, memo, nf, is_change) - VALUES (:tx, :output_index, :account, :diversifier, :value, :rcm, :memo, :nf, :is_change)", - )?, - stmt_update_received_note: self.conn.prepare( - "UPDATE received_notes - SET account = :account, - diversifier = :diversifier, - value = :value, - rcm = :rcm, - nf = IFNULL(:nf, nf), - memo = IFNULL(:memo, memo), - is_change = IFNULL(:is_change, is_change) - WHERE tx = :tx AND output_index = :output_index", - )?, - stmt_select_received_note: self.conn.prepare( - "SELECT id_note FROM received_notes WHERE tx = ? AND output_index = ?" - )?, - stmt_update_sent_note: self.conn.prepare( - "UPDATE sent_notes - SET from_account = ?, address = ?, value = ?, memo = ? - WHERE tx = ? AND output_index = ?", - )?, - stmt_insert_sent_note: self.conn.prepare( - "INSERT INTO sent_notes (tx, output_index, from_account, address, value, memo) - VALUES (?, ?, ?, ?, ?, ?)", - )?, - stmt_insert_witness: self.conn.prepare( - "INSERT INTO sapling_witnesses (note, block, witness) - VALUES (?, ?, ?)", - )?, - stmt_prune_witnesses: self.conn.prepare( - "DELETE FROM sapling_witnesses WHERE block < ?" - )?, - stmt_update_expired: self.conn.prepare( - "UPDATE received_notes SET spent = NULL WHERE EXISTS ( - SELECT id_tx FROM transactions - WHERE id_tx = received_notes.spent AND block IS NULL AND expiry_height < ? - )", - )?, - } - ) +impl, P: consensus::Parameters + Clone> WalletDb { + /// Constructs a new wrapper around the given connection. + /// + /// This is provided for use cases such as connection pooling, where `conn` may be an + /// `&mut rusqlite::Connection`. + /// + /// The caller must ensure that [`rusqlite::vtab::array::load_module`] has been called + /// on the connection. + pub fn from_connection(conn: C, params: P) -> Self { + WalletDb { conn, params } + } +} + +impl, P: consensus::Parameters + Clone> WalletDb { + pub fn transactionally>(&mut self, f: F) -> Result + where + F: FnOnce(&mut WalletDb, P>) -> Result, + { + let tx = self.conn.borrow_mut().transaction()?; + let mut wdb = WalletDb { + conn: SqlTransaction(&tx), + params: self.params.clone(), + }; + let result = f(&mut wdb)?; + tx.commit()?; + Ok(result) } } -impl WalletRead for WalletDb

{ +impl, P: consensus::Parameters> InputSource for WalletDb { type Error = SqliteClientError; - type NoteRef = NoteId; - type TxRef = i64; + type NoteRef = ReceivedNoteId; + type AccountId = AccountUuid; - fn block_height_extrema(&self) -> Result, Self::Error> { - wallet::block_height_extrema(self).map_err(SqliteClientError::from) + fn get_spendable_note( + &self, + txid: &TxId, + protocol: ShieldedProtocol, + index: u32, + ) -> Result>, Self::Error> { + match protocol { + ShieldedProtocol::Sapling => wallet::sapling::get_spendable_sapling_note( + self.conn.borrow(), + &self.params, + txid, + index, + ) + .map(|opt| opt.map(|n| n.map_note(Note::Sapling))), + ShieldedProtocol::Orchard => { + #[cfg(feature = "orchard")] + return wallet::orchard::get_spendable_orchard_note( + self.conn.borrow(), + &self.params, + txid, + index, + ) + .map(|opt| opt.map(|n| n.map_note(Note::Orchard))); + + #[cfg(not(feature = "orchard"))] + return Err(SqliteClientError::UnsupportedPoolType(PoolType::ORCHARD)); + } + } } - fn get_block_hash(&self, block_height: BlockHeight) -> Result, Self::Error> { - wallet::get_block_hash(self, block_height).map_err(SqliteClientError::from) + fn select_spendable_notes( + &self, + account: Self::AccountId, + target_value: Zatoshis, + sources: &[ShieldedProtocol], + anchor_height: BlockHeight, + exclude: &[Self::NoteRef], + ) -> Result, Self::Error> { + Ok(SpendableNotes::new( + if sources.contains(&ShieldedProtocol::Sapling) { + wallet::sapling::select_spendable_sapling_notes( + self.conn.borrow(), + &self.params, + account, + target_value, + anchor_height, + exclude, + )? + } else { + vec![] + }, + #[cfg(feature = "orchard")] + if sources.contains(&ShieldedProtocol::Orchard) { + wallet::orchard::select_spendable_orchard_notes( + self.conn.borrow(), + &self.params, + account, + target_value, + anchor_height, + exclude, + )? + } else { + vec![] + }, + )) } - fn get_tx_height(&self, txid: TxId) -> Result, Self::Error> { - wallet::get_tx_height(self, txid).map_err(SqliteClientError::from) + #[cfg(feature = "transparent-inputs")] + fn get_unspent_transparent_output( + &self, + outpoint: &OutPoint, + ) -> Result, Self::Error> { + wallet::transparent::get_wallet_transparent_output(self.conn.borrow(), outpoint, false) } - fn get_extended_full_viewing_keys( + #[cfg(feature = "transparent-inputs")] + fn get_spendable_transparent_outputs( &self, - ) -> Result, Self::Error> { - wallet::get_extended_full_viewing_keys(self) + address: &TransparentAddress, + target_height: BlockHeight, + min_confirmations: u32, + ) -> Result, Self::Error> { + wallet::transparent::get_spendable_transparent_outputs( + self.conn.borrow(), + &self.params, + address, + target_height, + min_confirmations, + ) } - fn get_address(&self, account: AccountId) -> Result, Self::Error> { - wallet::get_address(self, account) + /// Returns metadata for the spendable notes in the wallet. + fn get_account_metadata( + &self, + account_id: Self::AccountId, + selector: &NoteFilter, + exclude: &[Self::NoteRef], + ) -> Result { + let chain_tip_height = wallet::chain_tip_height(self.conn.borrow())? + .ok_or(SqliteClientError::ChainHeightUnknown)?; + + let sapling_pool_meta = spendable_notes_meta( + self.conn.borrow(), + ShieldedProtocol::Sapling, + chain_tip_height, + account_id, + selector, + exclude, + )?; + + #[cfg(feature = "orchard")] + let orchard_pool_meta = spendable_notes_meta( + self.conn.borrow(), + ShieldedProtocol::Orchard, + chain_tip_height, + account_id, + selector, + exclude, + )?; + #[cfg(not(feature = "orchard"))] + let orchard_pool_meta = None; + + Ok(AccountMeta::new(sapling_pool_meta, orchard_pool_meta)) } +} - fn is_valid_account_extfvk( - &self, - account: AccountId, - extfvk: &ExtendedFullViewingKey, - ) -> Result { - wallet::is_valid_account_extfvk(self, account, extfvk) +impl, P: consensus::Parameters> WalletRead for WalletDb { + type Error = SqliteClientError; + type AccountId = AccountUuid; + type Account = wallet::Account; + + fn get_account_ids(&self) -> Result, Self::Error> { + Ok(wallet::get_account_ids(self.conn.borrow())?) } - fn get_balance_at( + fn get_account( &self, - account: AccountId, - anchor_height: BlockHeight, - ) -> Result { - wallet::get_balance_at(self, account, anchor_height) + account_id: Self::AccountId, + ) -> Result, Self::Error> { + wallet::get_account(self.conn.borrow(), &self.params, account_id) } - fn get_memo(&self, id_note: Self::NoteRef) -> Result { - match id_note { - NoteId::SentNoteId(id_note) => wallet::get_sent_memo(self, id_note), - NoteId::ReceivedNoteId(id_note) => wallet::get_received_memo(self, id_note), - } + fn get_derived_account( + &self, + seed: &SeedFingerprint, + account_id: zip32::AccountId, + ) -> Result, Self::Error> { + wallet::get_derived_account(self.conn.borrow(), &self.params, seed, account_id) } - fn get_commitment_tree( + fn validate_seed( &self, - block_height: BlockHeight, - ) -> Result>, Self::Error> { - wallet::get_commitment_tree(self, block_height) + account_id: Self::AccountId, + seed: &SecretVec, + ) -> Result { + if let Some(account) = self.get_account(account_id)? { + if let AccountSource::Derived { derivation, .. } = account.source() { + wallet::seed_matches_derived_account( + &self.params, + seed, + derivation.seed_fingerprint(), + derivation.account_index(), + &account.uivk(), + ) + } else { + Err(SqliteClientError::UnknownZip32Derivation) + } + } else { + // Missing account is documented to return false. + Ok(false) + } } - #[allow(clippy::type_complexity)] - fn get_witnesses( + fn seed_relevance_to_derived_accounts( &self, - block_height: BlockHeight, - ) -> Result)>, Self::Error> { - wallet::get_witnesses(self, block_height) - } + seed: &SecretVec, + ) -> Result, Self::Error> { + let mut has_accounts = false; + let mut has_derived = false; + let mut relevant_account_ids = vec![]; + + for account_id in self.get_account_ids()? { + has_accounts = true; + let account = self.get_account(account_id)?.expect("account ID exists"); + + // If the account is imported, the seed _might_ be relevant, but the only + // way we could determine that is by brute-forcing the ZIP 32 account + // index space, which we're not going to do. The method name indicates to + // the caller that we only check derived accounts. + if let AccountSource::Derived { derivation, .. } = account.source() { + has_derived = true; + + if wallet::seed_matches_derived_account( + &self.params, + seed, + derivation.seed_fingerprint(), + derivation.account_index(), + &account.uivk(), + )? { + // The seed is relevant to this account. + relevant_account_ids.push(account_id); + } + } + } - fn get_nullifiers(&self) -> Result, Self::Error> { - wallet::get_nullifiers(self) + Ok( + if let Some(account_ids) = NonEmpty::from_vec(relevant_account_ids) { + SeedRelevance::Relevant { account_ids } + } else if has_derived { + SeedRelevance::NotRelevant + } else if has_accounts { + SeedRelevance::NoDerivedAccounts + } else { + SeedRelevance::NoAccounts + }, + ) } - fn get_spendable_notes( + fn get_account_for_ufvk( &self, - account: AccountId, - anchor_height: BlockHeight, - ) -> Result, Self::Error> { - wallet::transact::get_spendable_notes(self, account, anchor_height) + ufvk: &UnifiedFullViewingKey, + ) -> Result, Self::Error> { + wallet::get_account_for_ufvk(self.conn.borrow(), &self.params, ufvk) } - fn select_spendable_notes( + fn get_current_address( &self, - account: AccountId, - target_value: Amount, - anchor_height: BlockHeight, - ) -> Result, Self::Error> { - wallet::transact::select_spendable_notes(self, account, target_value, anchor_height) + account: Self::AccountId, + ) -> Result, Self::Error> { + wallet::get_current_address(self.conn.borrow(), &self.params, account) + .map(|res| res.map(|(addr, _)| addr)) } -} -/// The primary type used to implement [`WalletWrite`] for the SQLite database. -/// -/// A data structure that stores the SQLite prepared statements that are -/// required for the implementation of [`WalletWrite`] against the backing -/// store. -/// -/// [`WalletWrite`]: zcash_client_backend::data_api::WalletWrite -pub struct DataConnStmtCache<'a, P> { - wallet_db: &'a WalletDb

, - stmt_insert_block: Statement<'a>, + fn get_account_birthday(&self, account: Self::AccountId) -> Result { + wallet::account_birthday(self.conn.borrow(), account) + } - stmt_insert_tx_meta: Statement<'a>, - stmt_update_tx_meta: Statement<'a>, + fn get_wallet_birthday(&self) -> Result, Self::Error> { + wallet::wallet_birthday(self.conn.borrow()).map_err(SqliteClientError::from) + } - stmt_insert_tx_data: Statement<'a>, - stmt_update_tx_data: Statement<'a>, - stmt_select_tx_ref: Statement<'a>, + fn get_wallet_summary( + &self, + min_confirmations: u32, + ) -> Result>, Self::Error> { + // This will return a runtime error if we call `get_wallet_summary` from two + // threads at the same time, as transactions cannot nest. + wallet::get_wallet_summary( + &self.conn.borrow().unchecked_transaction()?, + &self.params, + min_confirmations, + &SubtreeProgressEstimator, + ) + } - stmt_mark_recived_note_spent: Statement<'a>, + fn chain_height(&self) -> Result, Self::Error> { + wallet::chain_tip_height(self.conn.borrow()).map_err(SqliteClientError::from) + } - stmt_insert_received_note: Statement<'a>, - stmt_update_received_note: Statement<'a>, - stmt_select_received_note: Statement<'a>, + fn get_block_hash(&self, block_height: BlockHeight) -> Result, Self::Error> { + wallet::get_block_hash(self.conn.borrow(), block_height).map_err(SqliteClientError::from) + } - stmt_insert_sent_note: Statement<'a>, - stmt_update_sent_note: Statement<'a>, + fn block_metadata(&self, height: BlockHeight) -> Result, Self::Error> { + wallet::block_metadata(self.conn.borrow(), &self.params, height) + } - stmt_insert_witness: Statement<'a>, - stmt_prune_witnesses: Statement<'a>, - stmt_update_expired: Statement<'a>, -} + fn block_fully_scanned(&self) -> Result, Self::Error> { + wallet::block_fully_scanned(self.conn.borrow(), &self.params) + } -impl<'a, P: consensus::Parameters> WalletRead for DataConnStmtCache<'a, P> { - type Error = SqliteClientError; - type NoteRef = NoteId; - type TxRef = i64; + fn get_max_height_hash(&self) -> Result, Self::Error> { + wallet::get_max_height_hash(self.conn.borrow()).map_err(SqliteClientError::from) + } - fn block_height_extrema(&self) -> Result, Self::Error> { - self.wallet_db.block_height_extrema() + fn block_max_scanned(&self) -> Result, Self::Error> { + wallet::block_max_scanned(self.conn.borrow(), &self.params) } - fn get_block_hash(&self, block_height: BlockHeight) -> Result, Self::Error> { - self.wallet_db.get_block_hash(block_height) + fn suggest_scan_ranges(&self) -> Result, Self::Error> { + wallet::scanning::suggest_scan_ranges(self.conn.borrow(), ScanPriority::Historic) + } + + fn get_target_and_anchor_heights( + &self, + min_confirmations: NonZeroU32, + ) -> Result, Self::Error> { + wallet::get_target_and_anchor_heights(self.conn.borrow(), min_confirmations) + .map_err(SqliteClientError::from) } fn get_tx_height(&self, txid: TxId) -> Result, Self::Error> { - self.wallet_db.get_tx_height(txid) + wallet::get_tx_height(self.conn.borrow(), txid).map_err(SqliteClientError::from) } - fn get_extended_full_viewing_keys( + fn get_unified_full_viewing_keys( &self, - ) -> Result, Self::Error> { - self.wallet_db.get_extended_full_viewing_keys() + ) -> Result, Self::Error> { + wallet::get_unified_full_viewing_keys(self.conn.borrow(), &self.params) } - fn get_address(&self, account: AccountId) -> Result, Self::Error> { - self.wallet_db.get_address(account) + fn get_memo(&self, note_id: NoteId) -> Result, Self::Error> { + let sent_memo = wallet::get_sent_memo(self.conn.borrow(), note_id)?; + if sent_memo.is_some() { + Ok(sent_memo) + } else { + wallet::get_received_memo(self.conn.borrow(), note_id) + } } - fn is_valid_account_extfvk( - &self, - account: AccountId, - extfvk: &ExtendedFullViewingKey, - ) -> Result { - self.wallet_db.is_valid_account_extfvk(account, extfvk) + fn get_transaction(&self, txid: TxId) -> Result, Self::Error> { + wallet::get_transaction(self.conn.borrow(), &self.params, txid) + .map(|res| res.map(|(_, tx)| tx)) } - fn get_balance_at( + fn get_sapling_nullifiers( &self, - account: AccountId, - anchor_height: BlockHeight, - ) -> Result { - self.wallet_db.get_balance_at(account, anchor_height) + query: NullifierQuery, + ) -> Result, Self::Error> { + wallet::sapling::get_sapling_nullifiers(self.conn.borrow(), query) } - fn get_memo(&self, id_note: Self::NoteRef) -> Result { - self.wallet_db.get_memo(id_note) + #[cfg(feature = "orchard")] + fn get_orchard_nullifiers( + &self, + query: NullifierQuery, + ) -> Result, Self::Error> { + wallet::orchard::get_orchard_nullifiers(self.conn.borrow(), query) } - fn get_commitment_tree( + #[cfg(feature = "transparent-inputs")] + fn get_transparent_receivers( &self, - block_height: BlockHeight, - ) -> Result>, Self::Error> { - self.wallet_db.get_commitment_tree(block_height) + account: Self::AccountId, + ) -> Result>, Self::Error> { + wallet::transparent::get_transparent_receivers(self.conn.borrow(), &self.params, account) } - #[allow(clippy::type_complexity)] - fn get_witnesses( + #[cfg(feature = "transparent-inputs")] + fn get_transparent_balances( &self, - block_height: BlockHeight, - ) -> Result)>, Self::Error> { - self.wallet_db.get_witnesses(block_height) + account: Self::AccountId, + max_height: BlockHeight, + ) -> Result, Self::Error> { + wallet::transparent::get_transparent_balances( + self.conn.borrow(), + &self.params, + account, + max_height, + ) } - fn get_nullifiers(&self) -> Result, Self::Error> { - self.wallet_db.get_nullifiers() + #[cfg(feature = "transparent-inputs")] + fn get_transparent_address_metadata( + &self, + account: Self::AccountId, + address: &TransparentAddress, + ) -> Result, Self::Error> { + wallet::transparent::get_transparent_address_metadata( + self.conn.borrow(), + &self.params, + account, + address, + ) } - fn get_spendable_notes( + #[cfg(feature = "transparent-inputs")] + fn get_known_ephemeral_addresses( &self, - account: AccountId, - anchor_height: BlockHeight, - ) -> Result, Self::Error> { - self.wallet_db.get_spendable_notes(account, anchor_height) + account: Self::AccountId, + index_range: Option>, + ) -> Result, Self::Error> { + let account_id = wallet::get_account_ref(self.conn.borrow(), account)?; + wallet::transparent::ephemeral::get_known_ephemeral_addresses( + self.conn.borrow(), + &self.params, + account_id, + index_range.map(|i| i.start.index()..i.end.index()), + ) } - fn select_spendable_notes( + #[cfg(feature = "transparent-inputs")] + fn find_account_for_ephemeral_address( &self, - account: AccountId, - target_value: Amount, - anchor_height: BlockHeight, - ) -> Result, Self::Error> { - self.wallet_db - .select_spendable_notes(account, target_value, anchor_height) + address: &TransparentAddress, + ) -> Result, Self::Error> { + wallet::transparent::ephemeral::find_account_for_ephemeral_address_str( + self.conn.borrow(), + &address.encode(&self.params), + ) + } + + fn transaction_data_requests(&self) -> Result, Self::Error> { + let iter = wallet::transaction_data_requests(self.conn.borrow())?.into_iter(); + + #[cfg(feature = "transparent-inputs")] + let iter = iter.chain(wallet::transparent::transaction_data_requests( + self.conn.borrow(), + &self.params, + )?); + + Ok(iter.collect()) } } -impl<'a, P: consensus::Parameters> DataConnStmtCache<'a, P> { - fn transactionally(&mut self, f: F) -> Result - where - F: FnOnce(&mut Self) -> Result, +#[cfg(any(test, feature = "test-dependencies"))] +impl, P: consensus::Parameters> WalletTest for WalletDb { + fn get_tx_history( + &self, + ) -> Result::AccountId>>, ::Error> { - self.wallet_db.conn.execute("BEGIN IMMEDIATE", NO_PARAMS)?; - match f(self) { - Ok(result) => { - self.wallet_db.conn.execute("COMMIT", NO_PARAMS)?; - Ok(result) - } - Err(error) => { - match self.wallet_db.conn.execute("ROLLBACK", NO_PARAMS) { - Ok(_) => Err(error), - Err(e) => - // Panicking here is probably the right thing to do, because it - // means the database is corrupt. - panic!( - "Rollback failed with error {} while attempting to recover from error {}; database is likely corrupt.", - e, - error - ) - } - } - } + wallet::testing::get_tx_history(self.conn.borrow()) + } + + fn get_sent_note_ids( + &self, + txid: &TxId, + protocol: ShieldedProtocol, + ) -> Result, ::Error> { + use crate::wallet::pool_code; + use rusqlite::named_params; + + let mut stmt_sent_notes = self.conn.borrow().prepare( + "SELECT output_index + FROM sent_notes + JOIN transactions ON transactions.id_tx = sent_notes.tx + WHERE transactions.txid = :txid + AND sent_notes.output_pool = :pool_code", + )?; + + let note_ids = stmt_sent_notes + .query_map( + named_params! { + ":txid": txid.as_ref(), + ":pool_code": pool_code(PoolType::Shielded(protocol)), + }, + |row| Ok(NoteId::new(*txid, protocol, row.get(0)?)), + )? + .collect::>()?; + + Ok(note_ids) + } + + fn get_sent_outputs( + &self, + txid: &TxId, + ) -> Result, ::Error> { + let mut stmt_sent = self + .conn.borrow() + .prepare( + "SELECT value, to_address, ephemeral_addresses.address, ephemeral_addresses.address_index + FROM sent_notes + JOIN transactions ON transactions.id_tx = sent_notes.tx + LEFT JOIN ephemeral_addresses ON ephemeral_addresses.used_in_tx = sent_notes.tx + WHERE transactions.txid = ? + ORDER BY value", + )?; + + let sends = stmt_sent + .query_map(rusqlite::params![txid.as_ref()], |row| { + let v = row.get(0)?; + let to_address = row + .get::<_, Option>(1)? + .and_then(|s| Address::decode(&self.params, &s)); + let ephemeral_address = row + .get::<_, Option>(2)? + .and_then(|s| Address::decode(&self.params, &s)); + let address_index: Option = row.get(3)?; + Ok((v, to_address, ephemeral_address.zip(address_index))) + })? + .map(|res| { + let (amount, external_recipient, ephemeral_address) = res?; + Ok::<_, ::Error>(OutputOfSentTx::from_parts( + Zatoshis::from_u64(amount)?, + external_recipient, + ephemeral_address, + )) + }) + .collect::>()?; + + Ok(sends) + } + + fn get_checkpoint_history( + &self, + protocol: &ShieldedProtocol, + ) -> Result< + Vec<(BlockHeight, Option)>, + ::Error, + > { + wallet::testing::get_checkpoint_history(self.conn.borrow(), protocol) + } + + #[cfg(feature = "transparent-inputs")] + fn get_transparent_output( + &self, + outpoint: &OutPoint, + allow_unspendable: bool, + ) -> Result, ::Error> { + wallet::transparent::get_wallet_transparent_output( + self.conn.borrow(), + outpoint, + allow_unspendable, + ) + } + + fn get_notes( + &self, + protocol: ShieldedProtocol, + ) -> Result>, ::Error> { + let (table_prefix, index_col, _) = wallet::common::per_protocol_names(protocol); + let mut stmt_received_notes = self.conn.borrow().prepare(&format!( + "SELECT txid, {index_col} + FROM {table_prefix}_received_notes rn + INNER JOIN transactions ON transactions.id_tx = rn.tx + WHERE transactions.block IS NOT NULL + AND recipient_key_scope IS NOT NULL + AND nf IS NOT NULL + AND commitment_tree_position IS NOT NULL" + ))?; + + let result = stmt_received_notes + .query_map([], |row| { + let txid: [u8; 32] = row.get(0)?; + let output_index: u32 = row.get(1)?; + let note = self + .get_spendable_note(&TxId::from_bytes(txid), protocol, output_index) + .unwrap() + .unwrap(); + Ok(note) + })? + .collect::, _>>()?; + + Ok(result) } } -impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> { - #[allow(clippy::type_complexity)] - fn advance_by_block( +impl, P: consensus::Parameters> WalletWrite for WalletDb { + type UtxoRef = UtxoId; + + fn create_account( + &mut self, + account_name: &str, + seed: &SecretVec, + birthday: &AccountBirthday, + key_source: Option<&str>, + ) -> Result<(Self::AccountId, UnifiedSpendingKey), Self::Error> { + self.borrow_mut().transactionally(|wdb| { + let seed_fingerprint = + SeedFingerprint::from_seed(seed.expose_secret()).ok_or_else(|| { + SqliteClientError::BadAccountData( + "Seed must be between 32 and 252 bytes in length.".to_owned(), + ) + })?; + let zip32_account_index = + wallet::max_zip32_account_index(wdb.conn.0, &seed_fingerprint)? + .map(|a| { + a.next() + .ok_or(SqliteClientError::Zip32AccountIndexOutOfRange) + }) + .transpose()? + .unwrap_or(zip32::AccountId::ZERO); + + let usk = UnifiedSpendingKey::from_seed( + &wdb.params, + seed.expose_secret(), + zip32_account_index, + ) + .map_err(|_| SqliteClientError::KeyDerivationError(zip32_account_index))?; + let ufvk = usk.to_unified_full_viewing_key(); + + let account = wallet::add_account( + wdb.conn.0, + &wdb.params, + account_name, + &AccountSource::Derived { + derivation: Zip32Derivation::new(seed_fingerprint, zip32_account_index), + key_source: key_source.map(|s| s.to_owned()), + }, + wallet::ViewingKey::Full(Box::new(ufvk)), + birthday, + )?; + + Ok((account.id(), usk)) + }) + } + + fn import_account_hd( &mut self, - block: &PrunedBlock, - updated_witnesses: &[(Self::NoteRef, IncrementalWitness)], - ) -> Result)>, Self::Error> { - // database updates for each block are transactional - self.transactionally(|up| { - // Insert the block into the database. - wallet::insert_block( - up, - block.block_height, - block.block_hash, - block.block_time, - &block.commitment_tree, + account_name: &str, + seed: &SecretVec, + account_index: zip32::AccountId, + birthday: &AccountBirthday, + key_source: Option<&str>, + ) -> Result<(Self::Account, UnifiedSpendingKey), Self::Error> { + self.transactionally(|wdb| { + let seed_fingerprint = + SeedFingerprint::from_seed(seed.expose_secret()).ok_or_else(|| { + SqliteClientError::BadAccountData( + "Seed must be between 32 and 252 bytes in length.".to_owned(), + ) + })?; + + let usk = + UnifiedSpendingKey::from_seed(&wdb.params, seed.expose_secret(), account_index) + .map_err(|_| SqliteClientError::KeyDerivationError(account_index))?; + let ufvk = usk.to_unified_full_viewing_key(); + + let account = wallet::add_account( + wdb.conn.0, + &wdb.params, + account_name, + &AccountSource::Derived { + derivation: Zip32Derivation::new(seed_fingerprint, account_index), + key_source: key_source.map(|s| s.to_owned()), + }, + wallet::ViewingKey::Full(Box::new(ufvk)), + birthday, )?; - let mut new_witnesses = vec![]; - for tx in block.transactions { - let tx_row = wallet::put_tx_meta(up, &tx, block.block_height)?; + Ok((account, usk)) + }) + } + + fn import_account_ufvk( + &mut self, + account_name: &str, + ufvk: &UnifiedFullViewingKey, + birthday: &AccountBirthday, + purpose: AccountPurpose, + key_source: Option<&str>, + ) -> Result { + self.transactionally(|wdb| { + wallet::add_account( + wdb.conn.0, + &wdb.params, + account_name, + &AccountSource::Imported { + purpose, + key_source: key_source.map(|s| s.to_owned()), + }, + wallet::ViewingKey::Full(Box::new(ufvk.to_owned())), + birthday, + ) + }) + } - // Mark notes as spent and remove them from the scanning cache - for spend in &tx.shielded_spends { - wallet::mark_spent(up, tx_row, &spend.nf)?; + fn get_next_available_address( + &mut self, + account_uuid: Self::AccountId, + request: Option, + ) -> Result, Self::Error> { + self.transactionally( + |wdb| match wdb.get_unified_full_viewing_keys()?.get(&account_uuid) { + Some(ufvk) => { + let search_from = + match wallet::get_current_address(wdb.conn.0, &wdb.params, account_uuid)? { + Some((_, mut last_diversifier_index)) => { + last_diversifier_index.increment().map_err(|_| { + AddressGenerationError::DiversifierSpaceExhausted + })?; + last_diversifier_index + } + None => DiversifierIndex::default(), + }; + + let (addr, diversifier_index) = ufvk.find_address(search_from, request)?; + + let account_id = wallet::get_account_ref(wdb.conn.0, account_uuid)?; + wallet::insert_address( + wdb.conn.0, + &wdb.params, + account_id, + diversifier_index, + &addr, + )?; + + Ok(Some(addr)) } + None => Ok(None), + }, + ) + } + + fn update_chain_tip(&mut self, tip_height: BlockHeight) -> Result<(), Self::Error> { + let tx = self.conn.borrow_mut().transaction()?; + wallet::scanning::update_chain_tip(&tx, &self.params, tip_height)?; + tx.commit()?; + Ok(()) + } + + #[tracing::instrument(skip_all, fields(height = blocks.first().map(|b| u32::from(b.height())), count = blocks.len()))] + #[allow(clippy::type_complexity)] + fn put_blocks( + &mut self, + from_state: &ChainState, + blocks: Vec>, + ) -> Result<(), Self::Error> { + struct BlockPositions { + height: BlockHeight, + sapling_start_position: Position, + #[cfg(feature = "orchard")] + orchard_start_position: Position, + } + + if blocks.is_empty() { + return Ok(()); + } - for output in &tx.shielded_outputs { - let received_note_id = wallet::put_received_note(up, output, tx_row)?; + self.transactionally(|wdb| { + let initial_block = blocks.first().expect("blocks is known to be nonempty"); + assert!(from_state.block_height() + 1 == initial_block.height()); + + let start_positions = BlockPositions { + height: initial_block.height(), + sapling_start_position: Position::from( + u64::from(initial_block.sapling().final_tree_size()) + - u64::try_from(initial_block.sapling().commitments().len()).unwrap(), + ), + #[cfg(feature = "orchard")] + orchard_start_position: Position::from( + u64::from(initial_block.orchard().final_tree_size()) + - u64::try_from(initial_block.orchard().commitments().len()).unwrap(), + ), + }; - // Save witness for note. - new_witnesses.push((received_note_id, output.witness.clone())); + let mut sapling_commitments = vec![]; + #[cfg(feature = "orchard")] + let mut orchard_commitments = vec![]; + let mut last_scanned_height = None; + let mut note_positions = vec![]; + for block in blocks.into_iter() { + if last_scanned_height + .iter() + .any(|prev| block.height() != *prev + 1) + { + return Err(SqliteClientError::NonSequentialBlocks); } - } - // Insert current new_witnesses into the database. - for (received_note_id, witness) in updated_witnesses.iter().chain(new_witnesses.iter()) - { - if let NoteId::ReceivedNoteId(rnid) = *received_note_id { - wallet::insert_witness(up, rnid, witness, block.block_height)?; - } else { - return Err(SqliteClientError::InvalidNoteId); + // Insert the block into the database. + wallet::put_block( + wdb.conn.0, + block.height(), + block.block_hash(), + block.block_time(), + block.sapling().final_tree_size(), + block.sapling().commitments().len().try_into().unwrap(), + #[cfg(feature = "orchard")] + block.orchard().final_tree_size(), + #[cfg(feature = "orchard")] + block.orchard().commitments().len().try_into().unwrap(), + )?; + + for tx in block.transactions() { + let tx_row = wallet::put_tx_meta(wdb.conn.0, tx, block.height())?; + wallet::queue_tx_retrieval(wdb.conn.0, std::iter::once(tx.txid()), None)?; + + // Mark notes as spent and remove them from the scanning cache + for spend in tx.sapling_spends() { + wallet::sapling::mark_sapling_note_spent(wdb.conn.0, tx_row, spend.nf())?; + } + #[cfg(feature = "orchard")] + for spend in tx.orchard_spends() { + wallet::orchard::mark_orchard_note_spent(wdb.conn.0, tx_row, spend.nf())?; + } + + for output in tx.sapling_outputs() { + // Check whether this note was spent in a later block range that + // we previously scanned. + let spent_in = output + .nf() + .map(|nf| { + wallet::query_nullifier_map( + wdb.conn.0, + ShieldedProtocol::Sapling, + nf, + ) + }) + .transpose()? + .flatten(); + + wallet::sapling::put_received_note(wdb.conn.0, output, tx_row, spent_in)?; + } + #[cfg(feature = "orchard")] + for output in tx.orchard_outputs() { + // Check whether this note was spent in a later block range that + // we previously scanned. + let spent_in = output + .nf() + .map(|nf| { + wallet::query_nullifier_map( + wdb.conn.0, + ShieldedProtocol::Orchard, + &nf.to_bytes(), + ) + }) + .transpose()? + .flatten(); + + wallet::orchard::put_received_note(wdb.conn.0, output, tx_row, spent_in)?; + } } + + // Insert the new nullifiers from this block into the nullifier map. + wallet::insert_nullifier_map( + wdb.conn.0, + block.height(), + ShieldedProtocol::Sapling, + block.sapling().nullifier_map(), + )?; + #[cfg(feature = "orchard")] + wallet::insert_nullifier_map( + wdb.conn.0, + block.height(), + ShieldedProtocol::Orchard, + &block + .orchard() + .nullifier_map() + .iter() + .map(|(txid, idx, nfs)| { + (*txid, *idx, nfs.iter().map(|nf| nf.to_bytes()).collect()) + }) + .collect::>(), + )?; + + note_positions.extend(block.transactions().iter().flat_map(|wtx| { + let iter = wtx.sapling_outputs().iter().map(|out| { + ( + ShieldedProtocol::Sapling, + out.note_commitment_tree_position(), + ) + }); + #[cfg(feature = "orchard")] + let iter = iter.chain(wtx.orchard_outputs().iter().map(|out| { + ( + ShieldedProtocol::Orchard, + out.note_commitment_tree_position(), + ) + })); + + iter + })); + + last_scanned_height = Some(block.height()); + let block_commitments = block.into_commitments(); + trace!( + "Sapling commitments for {:?}: {:?}", + last_scanned_height, + block_commitments + .sapling + .iter() + .map(|(_, r)| *r) + .collect::>() + ); + #[cfg(feature = "orchard")] + trace!( + "Orchard commitments for {:?}: {:?}", + last_scanned_height, + block_commitments + .orchard + .iter() + .map(|(_, r)| *r) + .collect::>() + ); + + sapling_commitments.extend(block_commitments.sapling.into_iter().map(Some)); + #[cfg(feature = "orchard")] + orchard_commitments.extend(block_commitments.orchard.into_iter().map(Some)); } - // Prune the stored witnesses (we only expect rollbacks of at most 100 blocks). - wallet::prune_witnesses(up, block.block_height - 100)?; + // Prune the nullifier map of entries we no longer need. + if let Some(meta) = wdb.block_fully_scanned()? { + wallet::prune_nullifier_map( + wdb.conn.0, + meta.block_height().saturating_sub(PRUNING_DEPTH), + )?; + } - // Update now-expired transactions that didn't get mined. - wallet::update_expired_notes(up, block.block_height)?; + // We will have a start position and a last scanned height in all cases where + // `blocks` is non-empty. + if let Some(last_scanned_height) = last_scanned_height { + // Create subtrees from the note commitments in parallel. + const CHUNK_SIZE: usize = 1024; + let sapling_subtrees = sapling_commitments + .par_chunks_mut(CHUNK_SIZE) + .enumerate() + .filter_map(|(i, chunk)| { + let start = + start_positions.sapling_start_position + (i * CHUNK_SIZE) as u64; + let end = start + chunk.len() as u64; + + shardtree::LocatedTree::from_iter( + start..end, + SAPLING_SHARD_HEIGHT.into(), + chunk.iter_mut().map(|n| n.take().expect("always Some")), + ) + }) + .map(|res| (res.subtree, res.checkpoints)) + .collect::>(); + + #[cfg(feature = "orchard")] + let orchard_subtrees = orchard_commitments + .par_chunks_mut(CHUNK_SIZE) + .enumerate() + .filter_map(|(i, chunk)| { + let start = + start_positions.orchard_start_position + (i * CHUNK_SIZE) as u64; + let end = start + chunk.len() as u64; + + shardtree::LocatedTree::from_iter( + start..end, + ORCHARD_SHARD_HEIGHT.into(), + chunk.iter_mut().map(|n| n.take().expect("always Some")), + ) + }) + .map(|res| (res.subtree, res.checkpoints)) + .collect::>(); + + // Collect the complete set of Sapling checkpoints + #[cfg(feature = "orchard")] + let sapling_checkpoint_positions: BTreeMap = + sapling_subtrees + .iter() + .flat_map(|(_, checkpoints)| checkpoints.iter()) + .map(|(k, v)| (*k, *v)) + .collect(); + + #[cfg(feature = "orchard")] + let orchard_checkpoint_positions: BTreeMap = + orchard_subtrees + .iter() + .flat_map(|(_, checkpoints)| checkpoints.iter()) + .map(|(k, v)| (*k, *v)) + .collect(); + + #[cfg(feature = "orchard")] + fn ensure_checkpoints< + 'a, + H, + I: Iterator, + const DEPTH: u8, + >( + // An iterator of checkpoints heights for which we wish to ensure that + // checkpoints exists. + ensure_heights: I, + // The map of checkpoint positions from which we will draw note commitment tree + // position information for the newly created checkpoints. + existing_checkpoint_positions: &BTreeMap, + // The frontier whose position will be used for an inserted checkpoint when + // there is no preceding checkpoint in existing_checkpoint_positions. + state_final_tree: &Frontier, + ) -> Vec<(BlockHeight, Checkpoint)> { + ensure_heights + .flat_map(|ensure_height| { + existing_checkpoint_positions + .range::(..=*ensure_height) + .last() + .map_or_else( + || { + Some(( + *ensure_height, + state_final_tree + .value() + .map_or_else(Checkpoint::tree_empty, |t| { + Checkpoint::at_position(t.position()) + }), + )) + }, + |(existing_checkpoint_height, position)| { + if *existing_checkpoint_height < *ensure_height { + Some(( + *ensure_height, + Checkpoint::at_position(*position), + )) + } else { + // The checkpoint already exists, so we don't need to + // do anything. + None + } + }, + ) + .into_iter() + }) + .collect::>() + } - Ok(new_witnesses) - }) - } + #[cfg(feature = "orchard")] + let (missing_sapling_checkpoints, missing_orchard_checkpoints) = ( + ensure_checkpoints( + orchard_checkpoint_positions.keys(), + &sapling_checkpoint_positions, + from_state.final_sapling_tree(), + ), + ensure_checkpoints( + sapling_checkpoint_positions.keys(), + &orchard_checkpoint_positions, + from_state.final_orchard_tree(), + ), + ); + + // Update the Sapling note commitment tree with all newly read note commitments + { + let mut sapling_subtrees_iter = sapling_subtrees.into_iter(); + wdb.with_sapling_tree_mut::<_, _, Self::Error>(|sapling_tree| { + debug!( + "Sapling initial tree size at {:?}: {:?}", + from_state.block_height(), + from_state.final_sapling_tree().tree_size() + ); + // We insert the frontier with `Checkpoint` retention because we need to be + // able to truncate the tree back to this point. + sapling_tree.insert_frontier( + from_state.final_sapling_tree().clone(), + Retention::Checkpoint { + id: from_state.block_height(), + marking: Marking::Reference, + }, + )?; + + for (tree, checkpoints) in &mut sapling_subtrees_iter { + sapling_tree.insert_tree(tree, checkpoints)?; + } + + // Ensure we have a Sapling checkpoint for each checkpointed Orchard block height. + // We skip all checkpoints below the minimum retained checkpoint in the + // Sapling tree, because branches below this height may be pruned. + #[cfg(feature = "orchard")] + { + let min_checkpoint_height = sapling_tree + .store() + .min_checkpoint_id() + .map_err(ShardTreeError::Storage)? + .expect( + "At least one checkpoint was inserted (by insert_frontier)", + ); + + for (height, checkpoint) in &missing_sapling_checkpoints { + if *height > min_checkpoint_height { + sapling_tree + .store_mut() + .add_checkpoint(*height, checkpoint.clone()) + .map_err(ShardTreeError::Storage)?; + } + } + } + + Ok(()) + })?; + } - fn store_received_tx( - &mut self, - received_tx: &ReceivedTransaction, - ) -> Result { - self.transactionally(|up| { - let tx_ref = wallet::put_tx_data(up, received_tx.tx, None)?; - - for output in received_tx.outputs { - if output.outgoing { - wallet::put_sent_note(up, output, tx_ref)?; - } else { - wallet::put_received_note(up, output, tx_ref)?; + // Update the Orchard note commitment tree with all newly read note commitments + #[cfg(feature = "orchard")] + { + let mut orchard_subtrees = orchard_subtrees.into_iter(); + wdb.with_orchard_tree_mut::<_, _, Self::Error>(|orchard_tree| { + debug!( + "Orchard initial tree size at {:?}: {:?}", + from_state.block_height(), + from_state.final_orchard_tree().tree_size() + ); + // We insert the frontier with `Checkpoint` retention because we need to be + // able to truncate the tree back to this point. + orchard_tree.insert_frontier( + from_state.final_orchard_tree().clone(), + Retention::Checkpoint { + id: from_state.block_height(), + marking: Marking::Reference, + }, + )?; + + for (tree, checkpoints) in &mut orchard_subtrees { + orchard_tree.insert_tree(tree, checkpoints)?; + } + + // Ensure we have an Orchard checkpoint for each checkpointed Sapling block height. + // We skip all checkpoints below the minimum retained checkpoint in the + // Orchard tree, because branches below this height may be pruned. + { + let min_checkpoint_height = orchard_tree + .store() + .min_checkpoint_id() + .map_err(ShardTreeError::Storage)? + .expect( + "At least one checkpoint was inserted (by insert_frontier)", + ); + + for (height, checkpoint) in &missing_orchard_checkpoints { + if *height > min_checkpoint_height { + debug!( + "Adding missing Orchard checkpoint for height: {:?}: {:?}", + height, + checkpoint.position() + ); + orchard_tree + .store_mut() + .add_checkpoint(*height, checkpoint.clone()) + .map_err(ShardTreeError::Storage)?; + } + } + } + Ok(()) + })?; } + + wallet::scanning::scan_complete( + wdb.conn.0, + &wdb.params, + Range { + start: start_positions.height, + end: last_scanned_height + 1, + }, + ¬e_positions, + )?; } - Ok(tx_ref) + Ok(()) }) } - fn store_sent_tx(&mut self, sent_tx: &SentTransaction) -> Result { - // Update the database atomically, to ensure the result is internally consistent. - self.transactionally(|up| { - let tx_ref = wallet::put_tx_data(up, &sent_tx.tx, Some(sent_tx.created))?; - - // Mark notes as spent. - // - // This locks the notes so they aren't selected again by a subsequent call to - // create_spend_to_address() before this transaction has been mined (at which point the notes - // get re-marked as spent). - // - // Assumes that create_spend_to_address() will never be called in parallel, which is a - // reasonable assumption for a light client such as a mobile phone. - for spend in &sent_tx.tx.shielded_spends { - wallet::mark_spent(up, tx_ref, &spend.nullifier)?; + fn put_received_transparent_utxo( + &mut self, + _output: &WalletTransparentOutput, + ) -> Result { + #[cfg(feature = "transparent-inputs")] + return wallet::transparent::put_received_transparent_utxo( + self.conn.borrow(), + &self.params, + _output, + ); + + #[cfg(not(feature = "transparent-inputs"))] + panic!( + "The wallet must be compiled with the transparent-inputs feature to use this method." + ); + } + + fn store_decrypted_tx( + &mut self, + d_tx: DecryptedTransaction, + ) -> Result<(), Self::Error> { + self.transactionally(|wdb| wallet::store_decrypted_tx(wdb.conn.0, &wdb.params, d_tx)) + } + + fn store_transactions_to_be_sent( + &mut self, + transactions: &[SentTransaction], + ) -> Result<(), Self::Error> { + self.transactionally(|wdb| { + for sent_tx in transactions { + wallet::store_transaction_to_be_sent(wdb, sent_tx)?; } + Ok(()) + }) + } - wallet::insert_sent_note( - up, - tx_ref, - sent_tx.output_index, - sent_tx.account, - sent_tx.recipient_address, - sent_tx.value, - sent_tx.memo.as_ref(), - )?; + fn truncate_to_height(&mut self, max_height: BlockHeight) -> Result { + self.transactionally(|wdb| wallet::truncate_to_height(wdb.conn.0, &wdb.params, max_height)) + } - // Return the row number of the transaction, so the caller can fetch it for sending. - Ok(tx_ref) + #[cfg(feature = "transparent-inputs")] + fn reserve_next_n_ephemeral_addresses( + &mut self, + account_id: Self::AccountId, + n: usize, + ) -> Result, Self::Error> { + self.transactionally(|wdb| { + let account_id = wallet::get_account_ref(wdb.conn.0, account_id)?; + wallet::transparent::ephemeral::reserve_next_n_ephemeral_addresses( + wdb.conn.0, + &wdb.params, + account_id, + n, + ) }) } - fn rewind_to_height(&mut self, block_height: BlockHeight) -> Result<(), Self::Error> { - wallet::rewind_to_height(self.wallet_db, block_height) + fn set_transaction_status( + &mut self, + txid: TxId, + status: data_api::TransactionStatus, + ) -> Result<(), Self::Error> { + self.transactionally(|wdb| wallet::set_transaction_status(wdb.conn.0, txid, status)) + } +} + +impl, P: consensus::Parameters> WalletCommitmentTrees + for WalletDb +{ + type Error = commitment_tree::Error; + type SaplingShardStore<'a> = + SqliteShardStore<&'a rusqlite::Transaction<'a>, sapling::Node, SAPLING_SHARD_HEIGHT>; + + fn with_sapling_tree_mut(&mut self, mut callback: F) -> Result + where + for<'a> F: FnMut( + &'a mut ShardTree< + Self::SaplingShardStore<'a>, + { sapling::NOTE_COMMITMENT_TREE_DEPTH }, + SAPLING_SHARD_HEIGHT, + >, + ) -> Result, + E: From>, + { + let tx = self + .conn + .borrow_mut() + .transaction() + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?; + let shard_store = SqliteShardStore::from_connection(&tx, SAPLING_TABLES_PREFIX) + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?; + let result = { + let mut shardtree = ShardTree::new(shard_store, PRUNING_DEPTH.try_into().unwrap()); + callback(&mut shardtree)? + }; + + tx.commit() + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?; + Ok(result) + } + + fn put_sapling_subtree_roots( + &mut self, + start_index: u64, + roots: &[CommitmentTreeRoot], + ) -> Result<(), ShardTreeError> { + let tx = self + .conn + .borrow_mut() + .transaction() + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?; + put_shard_roots::<_, { sapling::NOTE_COMMITMENT_TREE_DEPTH }, SAPLING_SHARD_HEIGHT>( + &tx, + SAPLING_TABLES_PREFIX, + start_index, + roots, + )?; + tx.commit() + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?; + Ok(()) + } + + #[cfg(feature = "orchard")] + type OrchardShardStore<'a> = SqliteShardStore< + &'a rusqlite::Transaction<'a>, + orchard::tree::MerkleHashOrchard, + ORCHARD_SHARD_HEIGHT, + >; + + #[cfg(feature = "orchard")] + fn with_orchard_tree_mut(&mut self, mut callback: F) -> Result + where + for<'a> F: FnMut( + &'a mut ShardTree< + Self::OrchardShardStore<'a>, + { ORCHARD_SHARD_HEIGHT * 2 }, + ORCHARD_SHARD_HEIGHT, + >, + ) -> Result, + E: From>, + { + let tx = self + .conn + .borrow_mut() + .transaction() + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?; + let shard_store = SqliteShardStore::from_connection(&tx, ORCHARD_TABLES_PREFIX) + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?; + let result = { + let mut shardtree = ShardTree::new(shard_store, PRUNING_DEPTH.try_into().unwrap()); + callback(&mut shardtree)? + }; + + tx.commit() + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?; + Ok(result) + } + + #[cfg(feature = "orchard")] + fn put_orchard_subtree_roots( + &mut self, + start_index: u64, + roots: &[CommitmentTreeRoot], + ) -> Result<(), ShardTreeError> { + let tx = self + .conn + .borrow_mut() + .transaction() + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?; + put_shard_roots::<_, { ORCHARD_SHARD_HEIGHT * 2 }, ORCHARD_SHARD_HEIGHT>( + &tx, + ORCHARD_TABLES_PREFIX, + start_index, + roots, + )?; + tx.commit() + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?; + Ok(()) } } -/// A wrapper for the SQLite connection to the block cache database. +impl WalletCommitmentTrees for WalletDb, P> { + type Error = commitment_tree::Error; + type SaplingShardStore<'a> = + SqliteShardStore<&'a rusqlite::Transaction<'a>, sapling::Node, SAPLING_SHARD_HEIGHT>; + + fn with_sapling_tree_mut(&mut self, mut callback: F) -> Result + where + for<'a> F: FnMut( + &'a mut ShardTree< + Self::SaplingShardStore<'a>, + { sapling::NOTE_COMMITMENT_TREE_DEPTH }, + SAPLING_SHARD_HEIGHT, + >, + ) -> Result, + E: From>, + { + let mut shardtree = ShardTree::new( + SqliteShardStore::from_connection(self.conn.0, SAPLING_TABLES_PREFIX) + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?, + PRUNING_DEPTH.try_into().unwrap(), + ); + let result = callback(&mut shardtree)?; + + Ok(result) + } + + fn put_sapling_subtree_roots( + &mut self, + start_index: u64, + roots: &[CommitmentTreeRoot], + ) -> Result<(), ShardTreeError> { + put_shard_roots::<_, { sapling::NOTE_COMMITMENT_TREE_DEPTH }, SAPLING_SHARD_HEIGHT>( + self.conn.0, + SAPLING_TABLES_PREFIX, + start_index, + roots, + ) + } + + #[cfg(feature = "orchard")] + type OrchardShardStore<'a> = SqliteShardStore< + &'a rusqlite::Transaction<'a>, + orchard::tree::MerkleHashOrchard, + ORCHARD_SHARD_HEIGHT, + >; + + #[cfg(feature = "orchard")] + fn with_orchard_tree_mut(&mut self, mut callback: F) -> Result + where + for<'a> F: FnMut( + &'a mut ShardTree< + Self::OrchardShardStore<'a>, + { ORCHARD_SHARD_HEIGHT * 2 }, + ORCHARD_SHARD_HEIGHT, + >, + ) -> Result, + E: From>, + { + let mut shardtree = ShardTree::new( + SqliteShardStore::from_connection(self.conn.0, ORCHARD_TABLES_PREFIX) + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?, + PRUNING_DEPTH.try_into().unwrap(), + ); + let result = callback(&mut shardtree)?; + + Ok(result) + } + + #[cfg(feature = "orchard")] + fn put_orchard_subtree_roots( + &mut self, + start_index: u64, + roots: &[CommitmentTreeRoot], + ) -> Result<(), ShardTreeError> { + put_shard_roots::<_, { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }, ORCHARD_SHARD_HEIGHT>( + self.conn.0, + ORCHARD_TABLES_PREFIX, + start_index, + roots, + ) + } +} + +/// A handle for the SQLite block source. pub struct BlockDb(Connection); impl BlockDb { @@ -526,219 +1705,628 @@ impl BlockDb { impl BlockSource for BlockDb { type Error = SqliteClientError; - fn with_blocks( + fn with_blocks( + &self, + from_height: Option, + limit: Option, + with_row: F, + ) -> Result<(), data_api::chain::error::Error> + where + F: FnMut(CompactBlock) -> Result<(), data_api::chain::error::Error>, + { + chain::blockdb_with_blocks(self, from_height, limit, with_row) + } +} + +/// A block source that reads block data from disk and block metadata from a SQLite database. +/// +/// This block source expects each compact block to be stored on disk in the `blocks` subdirectory +/// of the `blockstore_root` path provided at the time of construction. Each block should be +/// written, as the serialized bytes of its protobuf representation, where the path for each block +/// has the pattern: +/// +/// `/blocks/--compactblock` +/// +/// where `` is the decimal value of the height at which the block was mined, and +/// `` is the hexadecimal representation of the block hash, as produced by the +/// [`fmt::Display`] implementation for [`zcash_primitives::block::BlockHash`]. +/// +/// This block source is intended to be used with the following data flow: +/// * When the cache is being filled: +/// * The caller requests the current maximum height at which cached data is available +/// using [`FsBlockDb::get_max_cached_height`]. If no cached data is available, the caller +/// can use the wallet's synced-to height for the following operations instead. +/// * (recommended for privacy) the caller should round the returned height down to some 100- / +/// 1000-block boundary. +/// * The caller uses the lightwalletd's `getblock` gRPC method to obtain a stream of blocks. +/// For each block returned, the caller writes the compact block to `blocks_dir` using the +/// path format specified above. It is fine to overwrite an existing block, since block hashes +/// are immutable and collision-resistant. +/// * Once a caller-determined number of blocks have been successfully written to disk, the +/// caller should invoke [`FsBlockDb::write_block_metadata`] with the metadata for each block +/// written to disk. +/// * The cache can then be scanned using the [`BlockSource`] implementation, providing the +/// wallet's synced-to-height as a starting point. +/// * When part of the cache is no longer needed: +/// * The caller determines some height `H` that is the earliest block data it needs to preserve. +/// This might be determined based on where the wallet is fully-synced to, or other heuristics. +/// * The caller searches the defined filesystem folder for all files beginning in `HEIGHT-*` where +/// `HEIGHT < H`, and deletes those files. +/// +/// Note: This API is unstable, and may change in the future. In particular, the [`BlockSource`] +/// API and the above description currently assume that scanning is performed in linear block +/// order; this assumption is likely to be weakened and/or removed in a future update. +#[cfg(feature = "unstable")] +pub struct FsBlockDb { + conn: Connection, + blocks_dir: PathBuf, +} + +/// Errors that can be generated by the filesystem/sqlite-backed +/// block source. +#[derive(Debug)] +#[cfg(feature = "unstable")] +pub enum FsBlockDbError { + Fs(io::Error), + Db(rusqlite::Error), + Protobuf(prost::DecodeError), + MissingBlockPath(PathBuf), + InvalidBlockstoreRoot(PathBuf), + InvalidBlockPath(PathBuf), + CorruptedData(String), + CacheMiss(BlockHeight), +} + +#[cfg(feature = "unstable")] +impl From for FsBlockDbError { + fn from(err: io::Error) -> Self { + FsBlockDbError::Fs(err) + } +} + +#[cfg(feature = "unstable")] +impl From for FsBlockDbError { + fn from(err: rusqlite::Error) -> Self { + FsBlockDbError::Db(err) + } +} + +#[cfg(feature = "unstable")] +impl From for FsBlockDbError { + fn from(e: prost::DecodeError) -> Self { + FsBlockDbError::Protobuf(e) + } +} + +#[cfg(feature = "unstable")] +impl FsBlockDb { + /// Creates a filesystem-backed block store at the given path. + /// + /// This will construct or open a SQLite database at the path + /// `/blockmeta.sqlite` and will ensure that a directory exists at + /// `/blocks` where this block store will expect to find serialized block + /// files as described for [`FsBlockDb`]. + /// + /// An application using this constructor should ensure that they call + /// [`crate::chain::init::init_blockmeta_db`] at application startup to ensure + /// that the resulting metadata database is properly initialized and has had all required + /// migrations applied before use. + pub fn for_path>(fsblockdb_root: P) -> Result { + let meta = fs::metadata(&fsblockdb_root).map_err(FsBlockDbError::Fs)?; + if meta.is_dir() { + let db_path = fsblockdb_root.as_ref().join("blockmeta.sqlite"); + let blocks_dir = fsblockdb_root.as_ref().join("blocks"); + fs::create_dir_all(&blocks_dir)?; + Ok(FsBlockDb { + conn: Connection::open(db_path).map_err(FsBlockDbError::Db)?, + blocks_dir, + }) + } else { + Err(FsBlockDbError::InvalidBlockstoreRoot( + fsblockdb_root.as_ref().to_path_buf(), + )) + } + } + + /// Returns the maximum height of blocks known to the block metadata database. + pub fn get_max_cached_height(&self) -> Result, FsBlockDbError> { + Ok(chain::blockmetadb_get_max_cached_height(&self.conn)?) + } + + /// Adds a set of block metadata entries to the metadata database, overwriting any + /// existing entries at the given block heights. + /// + /// This will return an error if any block file corresponding to one of these metadata records + /// is absent from the blocks directory. + pub fn write_block_metadata(&self, block_meta: &[BlockMeta]) -> Result<(), FsBlockDbError> { + for m in block_meta { + let block_path = m.block_file_path(&self.blocks_dir); + match fs::metadata(&block_path) { + Err(e) => { + return Err(match e.kind() { + io::ErrorKind::NotFound => FsBlockDbError::MissingBlockPath(block_path), + _ => FsBlockDbError::Fs(e), + }); + } + Ok(meta) => { + if !meta.is_file() { + return Err(FsBlockDbError::InvalidBlockPath(block_path)); + } + } + } + } + + Ok(chain::blockmetadb_insert(&self.conn, block_meta)?) + } + + /// Returns the metadata for the block with the given height, if it exists in the + /// database. + pub fn find_block(&self, height: BlockHeight) -> Result, FsBlockDbError> { + Ok(chain::blockmetadb_find_block(&self.conn, height)?) + } + + /// Rewinds the BlockMeta Db to the `block_height` provided. + /// + /// This doesn't delete any files referenced by the records + /// stored in BlockMeta. + /// + /// If the requested height is greater than or equal to the height + /// of the last scanned block, or if the DB is empty, this function + /// does nothing. + pub fn truncate_to_height(&self, block_height: BlockHeight) -> Result<(), FsBlockDbError> { + Ok(chain::blockmetadb_truncate_to_height( + &self.conn, + block_height, + )?) + } +} + +#[cfg(feature = "unstable")] +impl BlockSource for FsBlockDb { + type Error = FsBlockDbError; + + fn with_blocks( &self, - from_height: BlockHeight, - limit: Option, + from_height: Option, + limit: Option, with_row: F, - ) -> Result<(), Self::Error> + ) -> Result<(), data_api::chain::error::Error> where - F: FnMut(CompactBlock) -> Result<(), Self::Error>, + F: FnMut(CompactBlock) -> Result<(), data_api::chain::error::Error>, { - chain::with_blocks(self, from_height, limit, with_row) + fsblockdb_with_blocks(self, from_height, limit, with_row) } } -fn address_from_extfvk( - params: &P, - extfvk: &ExtendedFullViewingKey, -) -> String { - let addr = extfvk.default_address().unwrap().1; - encode_payment_address(params.hrp_sapling_payment_address(), &addr) +#[cfg(feature = "unstable")] +impl std::fmt::Display for FsBlockDbError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + FsBlockDbError::Fs(io_error) => { + write!(f, "Failed to access the file system: {}", io_error) + } + FsBlockDbError::Db(e) => { + write!(f, "There was a problem with the sqlite db: {}", e) + } + FsBlockDbError::Protobuf(e) => { + write!(f, "Failed to parse protobuf-encoded record: {}", e) + } + FsBlockDbError::MissingBlockPath(block_path) => { + write!( + f, + "CompactBlock file expected but not found at {}", + block_path.display(), + ) + } + FsBlockDbError::InvalidBlockstoreRoot(fsblockdb_root) => { + write!( + f, + "The block storage root {} is not a directory", + fsblockdb_root.display(), + ) + } + FsBlockDbError::InvalidBlockPath(block_path) => { + write!( + f, + "CompactBlock path {} is not a file", + block_path.display(), + ) + } + FsBlockDbError::CorruptedData(e) => { + write!( + f, + "The block cache has corrupted data and this caused an error: {}", + e, + ) + } + FsBlockDbError::CacheMiss(height) => { + write!( + f, + "Requested height {} does not exist in the block cache", + height + ) + } + } + } } #[cfg(test)] -mod tests { - use ff::PrimeField; - use group::GroupEncoding; - use protobuf::Message; - use rand_core::{OsRng, RngCore}; - use rusqlite::params; - - use zcash_client_backend::proto::compact_formats::{ - CompactBlock, CompactOutput, CompactSpend, CompactTx, - }; +#[macro_use] +extern crate assert_matches; - use zcash_primitives::{ - block::BlockHash, - consensus::{BlockHeight, Network, NetworkUpgrade, Parameters}, - memo::MemoBytes, - sapling::{ - note_encryption::sapling_note_encryption, util::generate_random_rseed, Note, Nullifier, - PaymentAddress, - }, - transaction::components::Amount, - zip32::ExtendedFullViewingKey, +#[cfg(test)] +mod tests { + use secrecy::{ExposeSecret, Secret, SecretVec}; + use uuid::Uuid; + use zcash_client_backend::data_api::{ + chain::ChainState, + testing::{TestBuilder, TestState}, + Account, AccountBirthday, AccountPurpose, AccountSource, WalletRead, WalletTest, + WalletWrite, }; + use zcash_keys::keys::{UnifiedFullViewingKey, UnifiedSpendingKey}; + use zcash_primitives::block::BlockHash; + use zcash_protocol::consensus; + + use crate::{error::SqliteClientError, testing::db::TestDbFactory, AccountUuid}; + + #[cfg(feature = "unstable")] + use zcash_keys::keys::sapling; + + #[test] + fn validate_seed() { + let st = TestBuilder::new() + .with_data_store_factory(TestDbFactory::default()) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + let account = st.test_account().unwrap(); + + assert!({ + st.wallet() + .validate_seed(account.id(), st.test_seed().unwrap()) + .unwrap() + }); - use super::BlockDb; + // check that passing an invalid account results in a failure + assert!({ + let wrong_account_uuid = AccountUuid(Uuid::nil()); + !st.wallet() + .validate_seed(wrong_account_uuid, st.test_seed().unwrap()) + .unwrap() + }); - #[cfg(feature = "mainnet")] - pub(crate) fn network() -> Network { - Network::MainNetwork + // check that passing an invalid seed results in a failure + assert!({ + !st.wallet() + .validate_seed(account.id(), &SecretVec::new(vec![1u8; 32])) + .unwrap() + }); } - #[cfg(not(feature = "mainnet"))] - pub(crate) fn network() -> Network { - Network::TestNetwork - } + #[test] + pub(crate) fn get_next_available_address() { + let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory::default()) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + let account = st.test_account().cloned().unwrap(); - #[cfg(feature = "mainnet")] - pub(crate) fn sapling_activation_height() -> BlockHeight { - Network::MainNetwork - .activation_height(NetworkUpgrade::Sapling) - .unwrap() + let current_addr = st.wallet().get_current_address(account.id()).unwrap(); + assert!(current_addr.is_some()); + + let addr2 = st + .wallet_mut() + .get_next_available_address(account.id(), None) + .unwrap(); + assert!(addr2.is_some()); + assert_ne!(current_addr, addr2); + + let addr2_cur = st.wallet().get_current_address(account.id()).unwrap(); + assert_eq!(addr2, addr2_cur); } - #[cfg(not(feature = "mainnet"))] - pub(crate) fn sapling_activation_height() -> BlockHeight { - Network::TestNetwork - .activation_height(NetworkUpgrade::Sapling) - .unwrap() + #[test] + pub(crate) fn import_account_hd_0() { + let st = TestBuilder::new() + .with_data_store_factory(TestDbFactory::default()) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .set_account_index(zip32::AccountId::ZERO) + .build(); + assert_matches!( + st.test_account().unwrap().account().source(), + AccountSource::Derived { derivation, .. } if derivation.account_index() == zip32::AccountId::ZERO); } - /// Create a fake CompactBlock at the given height, containing a single output paying - /// the given address. Returns the CompactBlock and the nullifier for the new note. - pub(crate) fn fake_compact_block( - height: BlockHeight, - prev_hash: BlockHash, - extfvk: ExtendedFullViewingKey, - value: Amount, - ) -> (CompactBlock, Nullifier) { - let to = extfvk.default_address().unwrap().1; + #[test] + pub(crate) fn import_account_hd_1_then_2() { + let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory::default()) + .build(); - // Create a fake Note for the account - let mut rng = OsRng; - let rseed = generate_random_rseed(&network(), height, &mut rng); - let note = Note { - g_d: to.diversifier().g_d().unwrap(), - pk_d: *to.pk_d(), - value: value.into(), - rseed, - }; - let encryptor = sapling_note_encryption::<_, Network>( - Some(extfvk.fvk.ovk), - note.clone(), - to, - MemoBytes::empty(), - &mut rng, + let birthday = AccountBirthday::from_parts( + ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])), + None, ); - let cmu = note.cmu().to_repr().as_ref().to_vec(); - let epk = encryptor.epk().to_bytes().to_vec(); - let enc_ciphertext = encryptor.encrypt_note_plaintext(); - - // Create a fake CompactBlock containing the note - let mut cout = CompactOutput::new(); - cout.set_cmu(cmu); - cout.set_epk(epk); - cout.set_ciphertext(enc_ciphertext.as_ref()[..52].to_vec()); - let mut ctx = CompactTx::new(); - let mut txid = vec![0; 32]; - rng.fill_bytes(&mut txid); - ctx.set_hash(txid); - ctx.outputs.push(cout); - let mut cb = CompactBlock::new(); - cb.set_height(u64::from(height)); - cb.hash.resize(32, 0); - rng.fill_bytes(&mut cb.hash); - cb.prevHash.extend_from_slice(&prev_hash.0); - cb.vtx.push(ctx); - (cb, note.nf(&extfvk.fvk.vk, 0)) - } - - /// Create a fake CompactBlock at the given height, spending a single note from the - /// given address. - pub(crate) fn fake_compact_block_spending( - height: BlockHeight, - prev_hash: BlockHash, - (nf, in_value): (Nullifier, Amount), - extfvk: ExtendedFullViewingKey, - to: PaymentAddress, - value: Amount, - ) -> CompactBlock { - let mut rng = OsRng; - let rseed = generate_random_rseed(&network(), height, &mut rng); - - // Create a fake CompactBlock containing the note - let mut cspend = CompactSpend::new(); - cspend.set_nf(nf.to_vec()); - let mut ctx = CompactTx::new(); - let mut txid = vec![0; 32]; - rng.fill_bytes(&mut txid); - ctx.set_hash(txid); - ctx.spends.push(cspend); - - // Create a fake Note for the payment - ctx.outputs.push({ - let note = Note { - g_d: to.diversifier().g_d().unwrap(), - pk_d: *to.pk_d(), - value: value.into(), - rseed, - }; - let encryptor = sapling_note_encryption::<_, Network>( - Some(extfvk.fvk.ovk), - note.clone(), - to, - MemoBytes::empty(), - &mut rng, + + let seed = Secret::new(vec![0u8; 32]); + let zip32_index_1 = zip32::AccountId::ZERO.next().unwrap(); + + let first = st + .wallet_mut() + .import_account_hd("", &seed, zip32_index_1, &birthday, None) + .unwrap(); + assert_matches!( + first.0.source(), + AccountSource::Derived { derivation, .. } if derivation.account_index() == zip32_index_1); + + let zip32_index_2 = zip32_index_1.next().unwrap(); + let second = st + .wallet_mut() + .import_account_hd("", &seed, zip32_index_2, &birthday, None) + .unwrap(); + assert_matches!( + second.0.source(), + AccountSource::Derived { derivation, .. } if derivation.account_index() == zip32_index_2); + } + + fn check_collisions( + st: &mut TestState, + ufvk: &UnifiedFullViewingKey, + birthday: &AccountBirthday, + is_account_collision: impl Fn(&::Error) -> bool, + ) where + DbT::Account: core::fmt::Debug, + { + assert_matches!( + st.wallet_mut() + .import_account_ufvk("", ufvk, birthday, AccountPurpose::Spending { derivation: None }, None), + Err(e) if is_account_collision(&e) + ); + + // Remove the transparent component so that we don't have a match on the full UFVK. + // That should still produce an AccountCollision error. + #[cfg(feature = "transparent-inputs")] + { + assert!(ufvk.transparent().is_some()); + let subset_ufvk = UnifiedFullViewingKey::new( + None, + ufvk.sapling().cloned(), + #[cfg(feature = "orchard")] + ufvk.orchard().cloned(), + ) + .unwrap(); + assert_matches!( + st.wallet_mut().import_account_ufvk( + "", + &subset_ufvk, + birthday, + AccountPurpose::Spending { derivation: None }, + None, + ), + Err(e) if is_account_collision(&e) ); - let cmu = note.cmu().to_repr().as_ref().to_vec(); - let epk = encryptor.epk().to_bytes().to_vec(); - let enc_ciphertext = encryptor.encrypt_note_plaintext(); - - let mut cout = CompactOutput::new(); - cout.set_cmu(cmu); - cout.set_epk(epk); - cout.set_ciphertext(enc_ciphertext.as_ref()[..52].to_vec()); - cout - }); + } - // Create a fake Note for the change - ctx.outputs.push({ - let change_addr = extfvk.default_address().unwrap().1; - let rseed = generate_random_rseed(&network(), height, &mut rng); - let note = Note { - g_d: change_addr.diversifier().g_d().unwrap(), - pk_d: *change_addr.pk_d(), - value: (in_value - value).into(), - rseed, - }; - let encryptor = sapling_note_encryption::<_, Network>( - Some(extfvk.fvk.ovk), - note.clone(), - change_addr, - MemoBytes::empty(), - &mut rng, + // Remove the Orchard component so that we don't have a match on the full UFVK. + // That should still produce an AccountCollision error. + #[cfg(feature = "orchard")] + { + assert!(ufvk.orchard().is_some()); + let subset_ufvk = UnifiedFullViewingKey::new( + #[cfg(feature = "transparent-inputs")] + ufvk.transparent().cloned(), + ufvk.sapling().cloned(), + None, + ) + .unwrap(); + assert_matches!( + st.wallet_mut().import_account_ufvk( + "", + &subset_ufvk, + birthday, + AccountPurpose::Spending { derivation: None }, + None, + ), + Err(e) if is_account_collision(&e) ); - let cmu = note.cmu().to_repr().as_ref().to_vec(); - let epk = encryptor.epk().to_bytes().to_vec(); - let enc_ciphertext = encryptor.encrypt_note_plaintext(); - - let mut cout = CompactOutput::new(); - cout.set_cmu(cmu); - cout.set_epk(epk); - cout.set_ciphertext(enc_ciphertext.as_ref()[..52].to_vec()); - cout - }); + } + } + + #[test] + pub(crate) fn import_account_hd_1_then_conflicts() { + let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory::default()) + .build(); + + let birthday = AccountBirthday::from_parts( + ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])), + None, + ); + + let seed = Secret::new(vec![0u8; 32]); + let zip32_index_1 = zip32::AccountId::ZERO.next().unwrap(); + + let (first_account, _) = st + .wallet_mut() + .import_account_hd("", &seed, zip32_index_1, &birthday, None) + .unwrap(); + let ufvk = first_account.ufvk().unwrap(); + + assert_matches!( + st.wallet_mut().import_account_hd("", &seed, zip32_index_1, &birthday, None), + Err(SqliteClientError::AccountCollision(id)) if id == first_account.id()); + + check_collisions( + &mut st, + ufvk, + &birthday, + |e| matches!(e, SqliteClientError::AccountCollision(id) if *id == first_account.id()), + ); + } + + #[test] + pub(crate) fn import_account_ufvk_then_conflicts() { + let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory::default()) + .build(); + + let birthday = AccountBirthday::from_parts( + ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])), + None, + ); + + let seed = Secret::new(vec![0u8; 32]); + let zip32_index_0 = zip32::AccountId::ZERO; + let usk = UnifiedSpendingKey::from_seed(st.network(), seed.expose_secret(), zip32_index_0) + .unwrap(); + let ufvk = usk.to_unified_full_viewing_key(); + + let account = st + .wallet_mut() + .import_account_ufvk( + "", + &ufvk, + &birthday, + AccountPurpose::Spending { derivation: None }, + None, + ) + .unwrap(); + assert_eq!( + ufvk.encode(st.network()), + account.ufvk().unwrap().encode(st.network()) + ); + + assert_matches!( + account.source(), + AccountSource::Imported { + purpose: AccountPurpose::Spending { .. }, + .. + } + ); + + assert_matches!( + st.wallet_mut().import_account_hd("", &seed, zip32_index_0, &birthday, None), + Err(SqliteClientError::AccountCollision(id)) if id == account.id()); + + check_collisions( + &mut st, + &ufvk, + &birthday, + |e| matches!(e, SqliteClientError::AccountCollision(id) if *id == account.id()), + ); + } + + #[test] + pub(crate) fn create_account_then_conflicts() { + let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory::default()) + .build(); + + let birthday = AccountBirthday::from_parts( + ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])), + None, + ); - let mut cb = CompactBlock::new(); - cb.set_height(u64::from(height)); - cb.hash.resize(32, 0); - rng.fill_bytes(&mut cb.hash); - cb.prevHash.extend_from_slice(&prev_hash.0); - cb.vtx.push(ctx); - cb - } - - /// Insert a fake CompactBlock into the cache DB. - pub(crate) fn insert_into_cache(db_cache: &BlockDb, cb: &CompactBlock) { - let cb_bytes = cb.write_to_bytes().unwrap(); - db_cache - .0 - .prepare("INSERT INTO compactblocks (height, data) VALUES (?, ?)") - .unwrap() - .execute(params![u32::from(cb.height()), cb_bytes,]) + let seed = Secret::new(vec![0u8; 32]); + let zip32_index_0 = zip32::AccountId::ZERO; + let seed_based = st + .wallet_mut() + .create_account("", &seed, &birthday, None) .unwrap(); + let seed_based_account = st.wallet().get_account(seed_based.0).unwrap().unwrap(); + let ufvk = seed_based_account.ufvk().unwrap(); + + assert_matches!( + st.wallet_mut().import_account_hd("", &seed, zip32_index_0, &birthday, None), + Err(SqliteClientError::AccountCollision(id)) if id == seed_based.0); + + check_collisions( + &mut st, + ufvk, + &birthday, + |e| matches!(e, SqliteClientError::AccountCollision(id) if *id == seed_based.0), + ); + } + + #[cfg(feature = "transparent-inputs")] + #[test] + fn transparent_receivers() { + // Add an account to the wallet. + + use crate::testing::BlockCache; + let st = TestBuilder::new() + .with_data_store_factory(TestDbFactory::default()) + .with_block_cache(BlockCache::new()) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + let account = st.test_account().unwrap(); + let ufvk = account.usk().to_unified_full_viewing_key(); + let (taddr, _) = account.usk().default_transparent_address(); + + let receivers = st.wallet().get_transparent_receivers(account.id()).unwrap(); + + // The receiver for the default UA should be in the set. + assert!(receivers.contains_key( + ufvk.default_address(None) + .expect("A valid default address exists for the UFVK") + .0 + .transparent() + .unwrap() + )); + + // The default t-addr should be in the set. + assert!(receivers.contains_key(&taddr)); + } + + #[cfg(feature = "unstable")] + #[test] + pub(crate) fn fsblockdb_api() { + use zcash_client_backend::data_api::testing::AddressType; + use zcash_protocol::{consensus::NetworkConstants, value::Zatoshis}; + + use crate::testing::FsBlockCache; + + let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory::default()) + .with_block_cache(FsBlockCache::new()) + .build(); + + // The BlockMeta DB starts off empty. + assert_eq!(st.cache().get_max_cached_height().unwrap(), None); + + // Generate some fake CompactBlocks. + let seed = [0u8; 32]; + let hd_account_index = zip32::AccountId::ZERO; + let extsk = sapling::spending_key(&seed, st.network().coin_type(), hd_account_index); + let dfvk = extsk.to_diversifiable_full_viewing_key(); + let (h1, meta1, _) = st.generate_next_block( + &dfvk, + AddressType::DefaultExternal, + Zatoshis::const_from_u64(5), + ); + let (h2, meta2, _) = st.generate_next_block( + &dfvk, + AddressType::DefaultExternal, + Zatoshis::const_from_u64(10), + ); + + // The BlockMeta DB is not updated until we do so explicitly. + assert_eq!(st.cache().get_max_cached_height().unwrap(), None); + + // Inform the BlockMeta DB about the newly-persisted CompactBlocks. + st.cache().write_block_metadata(&[meta1, meta2]).unwrap(); + + // The BlockMeta DB now sees blocks up to height 2. + assert_eq!(st.cache().get_max_cached_height().unwrap(), Some(h2),); + assert_eq!(st.cache().find_block(h1).unwrap(), Some(meta1)); + assert_eq!(st.cache().find_block(h2).unwrap(), Some(meta2)); + assert_eq!(st.cache().find_block(h2 + 1).unwrap(), None); + + // Rewinding to height 1 should cause the metadata for height 2 to be deleted. + st.cache().truncate_to_height(h1).unwrap(); + assert_eq!(st.cache().get_max_cached_height().unwrap(), Some(h1)); + assert_eq!(st.cache().find_block(h1).unwrap(), Some(meta1)); + assert_eq!(st.cache().find_block(h2).unwrap(), None); + assert_eq!(st.cache().find_block(h2 + 1).unwrap(), None); } } diff --git a/zcash_client_sqlite/src/testing.rs b/zcash_client_sqlite/src/testing.rs new file mode 100644 index 0000000000..ab043e3d43 --- /dev/null +++ b/zcash_client_sqlite/src/testing.rs @@ -0,0 +1,133 @@ +use prost::Message; +use rusqlite::params; +use tempfile::NamedTempFile; + +use zcash_client_backend::{ + data_api::testing::{NoteCommitments, TestCache}, + proto::compact_formats::CompactBlock, +}; + +use crate::{chain::init::init_cache_database, error::SqliteClientError}; + +use super::BlockDb; + +#[cfg(feature = "unstable")] +use { + crate::{ + chain::{init::init_blockmeta_db, BlockMeta}, + FsBlockDb, FsBlockDbError, + }, + std::fs::File, + tempfile::TempDir, +}; + +pub(crate) mod db; +pub(crate) mod pool; + +pub(crate) struct BlockCache { + _cache_file: NamedTempFile, + db_cache: BlockDb, +} + +impl BlockCache { + pub(crate) fn new() -> Self { + let cache_file = NamedTempFile::new().unwrap(); + let db_cache = BlockDb::for_path(cache_file.path()).unwrap(); + init_cache_database(&db_cache).unwrap(); + + BlockCache { + _cache_file: cache_file, + db_cache, + } + } +} + +impl TestCache for BlockCache { + type BsError = SqliteClientError; + type BlockSource = BlockDb; + type InsertResult = NoteCommitments; + + fn block_source(&self) -> &Self::BlockSource { + &self.db_cache + } + + fn insert(&mut self, cb: &CompactBlock) -> Self::InsertResult { + let cb_bytes = cb.encode_to_vec(); + let res = NoteCommitments::from_compact_block(cb); + self.db_cache + .0 + .execute( + "INSERT INTO compactblocks (height, data) VALUES (?, ?)", + params![u32::from(cb.height()), cb_bytes,], + ) + .unwrap(); + res + } + + fn truncate_to_height(&mut self, height: zcash_protocol::consensus::BlockHeight) { + self.db_cache + .0 + .execute( + "DELETE FROM compactblocks WHERE height > ?", + params![u32::from(height)], + ) + .unwrap(); + } +} + +#[cfg(feature = "unstable")] +pub(crate) struct FsBlockCache { + fsblockdb_root: TempDir, + db_meta: FsBlockDb, +} + +#[cfg(feature = "unstable")] +impl FsBlockCache { + pub(crate) fn new() -> Self { + let fsblockdb_root = tempfile::tempdir().unwrap(); + let mut db_meta = FsBlockDb::for_path(&fsblockdb_root).unwrap(); + init_blockmeta_db(&mut db_meta).unwrap(); + + FsBlockCache { + fsblockdb_root, + db_meta, + } + } +} + +#[cfg(feature = "unstable")] +impl TestCache for FsBlockCache { + type BsError = FsBlockDbError; + type BlockSource = FsBlockDb; + type InsertResult = BlockMeta; + + fn block_source(&self) -> &Self::BlockSource { + &self.db_meta + } + + fn insert(&mut self, cb: &CompactBlock) -> Self::InsertResult { + use std::io::Write; + + let meta = BlockMeta { + height: cb.height(), + block_hash: cb.hash(), + block_time: cb.time, + sapling_outputs_count: cb.vtx.iter().map(|tx| tx.outputs.len() as u32).sum(), + orchard_actions_count: cb.vtx.iter().map(|tx| tx.actions.len() as u32).sum(), + }; + + let blocks_dir = self.fsblockdb_root.as_ref().join("blocks"); + let block_path = meta.block_file_path(&blocks_dir); + + File::create(block_path) + .unwrap() + .write_all(&cb.encode_to_vec()) + .unwrap(); + + meta + } + + fn truncate_to_height(&mut self, height: zcash_protocol::consensus::BlockHeight) { + self.db_meta.truncate_to_height(height).unwrap() + } +} diff --git a/zcash_client_sqlite/src/testing/db.rs b/zcash_client_sqlite/src/testing/db.rs new file mode 100644 index 0000000000..9ee8da6a8a --- /dev/null +++ b/zcash_client_sqlite/src/testing/db.rs @@ -0,0 +1,196 @@ +use ambassador::Delegate; +use rusqlite::Connection; +use std::collections::HashMap; +use std::num::NonZeroU32; +use uuid::Uuid; + +use tempfile::NamedTempFile; + +use rusqlite::{self}; +use secrecy::SecretVec; +use shardtree::{error::ShardTreeError, ShardTree}; +use zip32::fingerprint::SeedFingerprint; + +use zcash_client_backend::{ + data_api::{ + chain::{ChainState, CommitmentTreeRoot}, + scanning::ScanRange, + testing::{DataStoreFactory, Reset, TestState}, + *, + }, + wallet::{Note, NoteId, ReceivedNote, WalletTransparentOutput}, +}; +use zcash_keys::{ + address::UnifiedAddress, + keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey}, +}; +use zcash_primitives::{ + block::BlockHash, + transaction::{Transaction, TxId}, +}; +use zcash_protocol::{ + consensus::BlockHeight, local_consensus::LocalNetwork, memo::Memo, value::Zatoshis, + ShieldedProtocol, +}; + +use crate::{ + error::SqliteClientError, + wallet::init::{init_wallet_db, init_wallet_db_internal}, + AccountUuid, WalletDb, +}; + +#[cfg(feature = "transparent-inputs")] +use { + crate::TransparentAddressMetadata, + ::transparent::{address::TransparentAddress, bundle::OutPoint, keys::NonHardenedChildIndex}, + core::ops::Range, +}; + +#[allow(clippy::duplicated_attributes, reason = "False positive")] +#[derive(Delegate)] +#[delegate(InputSource, target = "wallet_db")] +#[delegate(WalletRead, target = "wallet_db")] +#[delegate(WalletTest, target = "wallet_db")] +#[delegate(WalletWrite, target = "wallet_db")] +#[delegate(WalletCommitmentTrees, target = "wallet_db")] +pub(crate) struct TestDb { + wallet_db: WalletDb, + data_file: NamedTempFile, +} + +impl TestDb { + fn from_parts(wallet_db: WalletDb, data_file: NamedTempFile) -> Self { + Self { + wallet_db, + data_file, + } + } + + pub(crate) fn db(&self) -> &WalletDb { + &self.wallet_db + } + + pub(crate) fn db_mut(&mut self) -> &mut WalletDb { + &mut self.wallet_db + } + + pub(crate) fn conn(&self) -> &Connection { + &self.wallet_db.conn + } + + pub(crate) fn conn_mut(&mut self) -> &mut Connection { + &mut self.wallet_db.conn + } + + pub(crate) fn take_data_file(self) -> NamedTempFile { + self.data_file + } + + /// Dump the schema and contents of the given database table, in + /// sqlite3 ".dump" format. The name of the table must be a static + /// string. This assumes that `sqlite3` is on your path and that it + /// invokes a compatible version of sqlite3. + /// + /// # Panics + /// + /// Panics if `name` contains characters outside `[a-zA-Z_]`. + #[allow(dead_code)] + #[cfg(feature = "unstable")] + pub(crate) fn dump_table(&self, name: &'static str) { + assert!(name.chars().all(|c| c.is_ascii_alphabetic() || c == '_')); + unsafe { + run_sqlite3(self.data_file.path(), &format!(r#".dump "{name}""#)); + } + } + + /// Print the results of an arbitrary sqlite3 command (with "-safe" + /// and "-readonly" flags) to stderr. This is completely insecure and + /// should not be exposed in production. Use of the "-safe" and + /// "-readonly" flags is intended only to limit *accidental* misuse. + /// The output is unfiltered, and control codes could mess up your + /// terminal. This assumes that `sqlite3` is on your path and that it + /// invokes a compatible version of sqlite3. + #[allow(dead_code)] + #[cfg(feature = "unstable")] + pub(crate) unsafe fn run_sqlite3(&self, command: &str) { + run_sqlite3(self.data_file.path(), command) + } +} + +#[cfg(feature = "unstable")] +use std::{ffi::OsStr, process::Command}; + +// See the doc comment for `TestState::run_sqlite3` above. +// +// - `db_path` is the path to the database file. +// - `command` may contain newlines. +#[allow(dead_code)] +#[cfg(feature = "unstable")] +unsafe fn run_sqlite3>(db_path: S, command: &str) { + let output = Command::new("sqlite3") + .arg(db_path) + .arg("-safe") + .arg("-readonly") + .arg(command) + .output() + .expect("failed to execute sqlite3 process"); + + eprintln!( + "{}\n------\n{}", + command, + String::from_utf8_lossy(&output.stdout) + ); + if !output.stderr.is_empty() { + eprintln!( + "------ stderr:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + } + eprintln!("------"); +} + +#[derive(Default)] +pub(crate) struct TestDbFactory { + target_migrations: Option>, +} + +impl TestDbFactory { + #[allow(dead_code)] + pub(crate) fn new(target_migrations: Vec) -> Self { + Self { + target_migrations: Some(target_migrations), + } + } +} + +impl DataStoreFactory for TestDbFactory { + type Error = (); + type AccountId = AccountUuid; + type Account = crate::wallet::Account; + type DsError = SqliteClientError; + type DataStore = TestDb; + + fn new_data_store(&self, network: LocalNetwork) -> Result { + let data_file = NamedTempFile::new().unwrap(); + let mut db_data = WalletDb::for_path(data_file.path(), network).unwrap(); + if let Some(migrations) = &self.target_migrations { + init_wallet_db_internal(&mut db_data, None, migrations, true).unwrap(); + } else { + init_wallet_db(&mut db_data, None).unwrap(); + } + Ok(TestDb::from_parts(db_data, data_file)) + } +} + +impl Reset for TestDb { + type Handle = NamedTempFile; + + fn reset(st: &mut TestState) -> NamedTempFile { + let network = *st.network(); + let old_db = std::mem::replace( + st.wallet_mut(), + TestDbFactory::default().new_data_store(network).unwrap(), + ); + old_db.take_data_file() + } +} diff --git a/zcash_client_sqlite/src/testing/pool.rs b/zcash_client_sqlite/src/testing/pool.rs new file mode 100644 index 0000000000..1b5132a36f --- /dev/null +++ b/zcash_client_sqlite/src/testing/pool.rs @@ -0,0 +1,271 @@ +//! Test logic involving a single shielded pool. +//! +//! Generalised for sharing across the Sapling and Orchard implementations. + +use crate::{ + testing::{db::TestDbFactory, BlockCache}, + SAPLING_TABLES_PREFIX, +}; +use zcash_client_backend::data_api::testing::{ + pool::ShieldedPoolTester, sapling::SaplingPoolTester, +}; + +#[cfg(feature = "orchard")] +use { + crate::ORCHARD_TABLES_PREFIX, + zcash_client_backend::data_api::testing::orchard::OrchardPoolTester, +}; + +pub(crate) trait ShieldedPoolPersistence { + const TABLES_PREFIX: &'static str; +} + +impl ShieldedPoolPersistence for SaplingPoolTester { + const TABLES_PREFIX: &'static str = SAPLING_TABLES_PREFIX; +} + +#[cfg(feature = "orchard")] +impl ShieldedPoolPersistence for OrchardPoolTester { + const TABLES_PREFIX: &'static str = ORCHARD_TABLES_PREFIX; +} + +pub(crate) fn send_single_step_proposed_transfer() { + zcash_client_backend::data_api::testing::pool::send_single_step_proposed_transfer::( + TestDbFactory::default(), + BlockCache::new(), + ) +} + +pub(crate) fn send_with_multiple_change_outputs() { + zcash_client_backend::data_api::testing::pool::send_with_multiple_change_outputs::( + TestDbFactory::default(), + BlockCache::new(), + ) +} + +#[cfg(feature = "transparent-inputs")] +pub(crate) fn send_multi_step_proposed_transfer() { + zcash_client_backend::data_api::testing::pool::send_multi_step_proposed_transfer::( + TestDbFactory::default(), + BlockCache::new(), + |e, account_id, expected_bad_index| { + matches!( + e, + crate::error::SqliteClientError::ReachedGapLimit(acct, bad_index) + if acct == &account_id && bad_index == &expected_bad_index) + }, + ) +} + +#[cfg(feature = "transparent-inputs")] +pub(crate) fn proposal_fails_if_not_all_ephemeral_outputs_consumed() { + zcash_client_backend::data_api::testing::pool::proposal_fails_if_not_all_ephemeral_outputs_consumed::( + TestDbFactory::default(), + BlockCache::new(), + ) +} + +pub(crate) fn create_to_address_fails_on_incorrect_usk() { + zcash_client_backend::data_api::testing::pool::create_to_address_fails_on_incorrect_usk::( + TestDbFactory::default(), + ) +} + +pub(crate) fn proposal_fails_with_no_blocks() { + zcash_client_backend::data_api::testing::pool::proposal_fails_with_no_blocks::( + TestDbFactory::default(), + ) +} + +pub(crate) fn spend_fails_on_unverified_notes() { + zcash_client_backend::data_api::testing::pool::spend_fails_on_unverified_notes::( + TestDbFactory::default(), + BlockCache::new(), + ) +} + +pub(crate) fn spend_fails_on_locked_notes() { + zcash_client_backend::data_api::testing::pool::spend_fails_on_locked_notes::( + TestDbFactory::default(), + BlockCache::new(), + ) +} + +pub(crate) fn ovk_policy_prevents_recovery_from_chain() { + zcash_client_backend::data_api::testing::pool::ovk_policy_prevents_recovery_from_chain::( + TestDbFactory::default(), + BlockCache::new(), + ) +} + +pub(crate) fn spend_succeeds_to_t_addr_zero_change() { + zcash_client_backend::data_api::testing::pool::spend_succeeds_to_t_addr_zero_change::( + TestDbFactory::default(), + BlockCache::new(), + ) +} + +pub(crate) fn change_note_spends_succeed() { + zcash_client_backend::data_api::testing::pool::change_note_spends_succeed::( + TestDbFactory::default(), + BlockCache::new(), + ) +} + +pub(crate) fn external_address_change_spends_detected_in_restore_from_seed< + T: ShieldedPoolTester, +>() { + zcash_client_backend::data_api::testing::pool::external_address_change_spends_detected_in_restore_from_seed::( + TestDbFactory::default(), + BlockCache::new(), + ) +} + +#[allow(dead_code)] +pub(crate) fn zip317_spend() { + zcash_client_backend::data_api::testing::pool::zip317_spend::( + TestDbFactory::default(), + BlockCache::new(), + ) +} + +#[cfg(feature = "transparent-inputs")] +pub(crate) fn shield_transparent() { + zcash_client_backend::data_api::testing::pool::shield_transparent::( + TestDbFactory::default(), + BlockCache::new(), + ) +} + +// FIXME: This requires fixes to the test framework. +#[allow(dead_code)] +pub(crate) fn birthday_in_anchor_shard() { + zcash_client_backend::data_api::testing::pool::birthday_in_anchor_shard::( + TestDbFactory::default(), + BlockCache::new(), + ) +} + +pub(crate) fn checkpoint_gaps() { + zcash_client_backend::data_api::testing::pool::checkpoint_gaps::( + TestDbFactory::default(), + BlockCache::new(), + ) +} + +#[cfg(feature = "orchard")] +pub(crate) fn pool_crossing_required() { + zcash_client_backend::data_api::testing::pool::pool_crossing_required::( + TestDbFactory::default(), + BlockCache::new(), + ) +} + +#[cfg(feature = "orchard")] +pub(crate) fn fully_funded_fully_private() { + zcash_client_backend::data_api::testing::pool::fully_funded_fully_private::( + TestDbFactory::default(), + BlockCache::new(), + ) +} + +#[cfg(all(feature = "orchard", feature = "transparent-inputs"))] +pub(crate) fn fully_funded_send_to_t() { + zcash_client_backend::data_api::testing::pool::fully_funded_send_to_t::( + TestDbFactory::default(), + BlockCache::new(), + ) +} + +#[cfg(feature = "orchard")] +pub(crate) fn multi_pool_checkpoint() { + zcash_client_backend::data_api::testing::pool::multi_pool_checkpoint::( + TestDbFactory::default(), + BlockCache::new(), + ) +} + +#[cfg(feature = "orchard")] +pub(crate) fn multi_pool_checkpoints_with_pruning< + P0: ShieldedPoolTester, + P1: ShieldedPoolTester, +>() { + zcash_client_backend::data_api::testing::pool::multi_pool_checkpoints_with_pruning::( + TestDbFactory::default(), + BlockCache::new(), + ) +} + +pub(crate) fn valid_chain_states() { + zcash_client_backend::data_api::testing::pool::valid_chain_states::( + TestDbFactory::default(), + BlockCache::new(), + ) +} + +// FIXME: This requires fixes to the test framework. +#[allow(dead_code)] +pub(crate) fn invalid_chain_cache_disconnected() { + zcash_client_backend::data_api::testing::pool::invalid_chain_cache_disconnected::( + TestDbFactory::default(), + BlockCache::new(), + ) +} + +pub(crate) fn data_db_truncation() { + zcash_client_backend::data_api::testing::pool::data_db_truncation::( + TestDbFactory::default(), + BlockCache::new(), + ) +} + +pub(crate) fn reorg_to_checkpoint() { + zcash_client_backend::data_api::testing::pool::reorg_to_checkpoint::( + TestDbFactory::default(), + BlockCache::new(), + ) +} + +pub(crate) fn scan_cached_blocks_allows_blocks_out_of_order() { + zcash_client_backend::data_api::testing::pool::scan_cached_blocks_allows_blocks_out_of_order::( + TestDbFactory::default(), + BlockCache::new(), + ) +} + +pub(crate) fn scan_cached_blocks_finds_received_notes() { + zcash_client_backend::data_api::testing::pool::scan_cached_blocks_finds_received_notes::( + TestDbFactory::default(), + BlockCache::new(), + ) +} + +// TODO: This test can probably be entirely removed, as the following test duplicates it entirely. +pub(crate) fn scan_cached_blocks_finds_change_notes() { + zcash_client_backend::data_api::testing::pool::scan_cached_blocks_finds_change_notes::( + TestDbFactory::default(), + BlockCache::new(), + ) +} + +pub(crate) fn scan_cached_blocks_detects_spends_out_of_order() { + zcash_client_backend::data_api::testing::pool::scan_cached_blocks_detects_spends_out_of_order::< + T, + _, + >(TestDbFactory::default(), BlockCache::new()) +} + +pub(crate) fn metadata_queries_exclude_unwanted_notes() { + zcash_client_backend::data_api::testing::pool::metadata_queries_exclude_unwanted_notes::( + TestDbFactory::default(), + BlockCache::new(), + ) +} + +#[cfg(feature = "pczt-tests")] +pub(crate) fn pczt_single_step() { + zcash_client_backend::data_api::testing::pool::pczt_single_step::( + TestDbFactory::default(), + BlockCache::new(), + ) +} diff --git a/zcash_client_sqlite/src/wallet.rs b/zcash_client_sqlite/src/wallet.rs index 9fbc41185f..99db16e477 100644 --- a/zcash_client_sqlite/src/wallet.rs +++ b/zcash_client_sqlite/src/wallet.rs @@ -1,4 +1,4 @@ -//! Functions for querying information in the wdb database. +//! Functions for querying information in the wallet database. //! //! These functions should generally not be used directly; instead, //! their functionality is available via the [`WalletRead`] and @@ -6,886 +6,3879 @@ //! //! [`WalletRead`]: zcash_client_backend::data_api::WalletRead //! [`WalletWrite`]: zcash_client_backend::data_api::WalletWrite +//! +//! # Views +//! +//! The wallet database exposes the following views as part of its public API: +//! +//! ## `v_transactions` +//! +//! This view exposes the history of transactions that affect the balance of each account in the +//! wallet. A transaction may be represented by multiple rows in this view, one for each account in +//! the wallet that contributes funds to or receives funds from the transaction in question. Each +//! row of the view contains: +//! - `account_balance_delta`: the net effect of the transaction on the associated account's +//! balance. This value is positive when funds are received by the account, and negative when the +//! balance of the account decreases due to a spend. +//! - `fee_paid`: the total fee paid to send the transaction, as a positive value. This fee is +//! associated with the transaction (similar to e.g. `txid` or `mined_height`), and not with any +//! specific account involved with that transaction. ` If multiple rows exist for a single +//! transaction, this fee amount will be repeated for each such row. Therefore, if more than one +//! of the wallet's accounts is involved with the transaction, this fee should be considered only +//! once in determining the total value sent from the wallet as a whole. +//! +//! ### Seed Phrase with Single Account +//! +//! In the case that the seed phrase for in this wallet has only been used to create a single +//! account, this view will contain one row per transaction, in the case that +//! `account_balance_delta` is negative, it is usually safe to add `fee_paid` back to the +//! `account_balance_delta` value to determine the amount sent to addresses outside the wallet. +//! +//! ### Seed Phrase with Multiple Accounts +//! +//! In the case that the seed phrase for in this wallet has been used to create multiple accounts, +//! this view may contain multiple rows per transaction, one for each account involved. In this +//! case, the total amount sent to addresses outside the wallet can usually be calculated by +//! grouping rows by `id_tx` and then using `SUM(account_balance_delta) + MAX(fee_paid)`. +//! +//! ### Imported Seed Phrases +//! +//! If a seed phrase is imported, and not every account associated with it is loaded into the +//! wallet, this view may show partial information about some transactions. In particular, any +//! computation that involves both `account_balance_delta` and `fee_paid` is likely to be +//! inaccurate. +//! +//! ## `v_tx_outputs` +//! +//! This view exposes the history of transaction outputs received by and sent from the wallet, +//! keyed by transaction ID, pool type, and output index. The contents of this view are useful for +//! producing a detailed report of the effects of a transaction. Each row of this view contains: +//! - `from_account_id` for sent outputs, the account from which the value was sent. +//! - `to_account_id` in the case that the output was received by an account in the wallet, the +//! identifier for the account receiving the funds. +//! - `to_address` the address to which an output was sent, or the address at which value was +//! received in the case of received transparent funds. +//! - `value` the value of the output. This is always a positive number, for both sent and received +//! outputs. +//! - `is_change` a boolean flag indicating whether this is a change output belonging to the +//! wallet. +//! - `memo` the shielded memo associated with the output, if any. + +use incrementalmerkletree::{Marking, Retention}; + +use rusqlite::{self, named_params, params, Connection, OptionalExtension}; +use secrecy::{ExposeSecret, SecretVec}; +use shardtree::{error::ShardTreeError, store::ShardStore, ShardTree}; +use uuid::Uuid; +use zcash_client_backend::data_api::{ + AccountPurpose, DecryptedTransaction, Progress, TransactionDataRequest, TransactionStatus, + Zip32Derivation, +}; +use zip32::fingerprint::SeedFingerprint; -use ff::PrimeField; -use rusqlite::{params, OptionalExtension, ToSql, NO_PARAMS}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::convert::TryFrom; +use std::io::{self, Cursor}; +use std::num::NonZeroU32; +use std::ops::RangeInclusive; -use zcash_primitives::{ - block::BlockHash, - consensus::{self, BlockHeight, NetworkUpgrade}, - memo::{Memo, MemoBytes}, - merkle_tree::{CommitmentTree, IncrementalWitness}, - sapling::{Node, Note, Nullifier, PaymentAddress}, - transaction::{components::Amount, Transaction, TxId}, - zip32::ExtendedFullViewingKey, -}; +use tracing::{debug, warn}; +use zcash_address::ZcashAddress; use zcash_client_backend::{ - address::RecipientAddress, - data_api::error::Error, - encoding::{ - decode_extended_full_viewing_key, decode_payment_address, encode_extended_full_viewing_key, - encode_payment_address, + data_api::{ + scanning::{ScanPriority, ScanRange}, + Account as _, AccountBalance, AccountBirthday, AccountSource, BlockMetadata, Ratio, + SentTransaction, SentTransactionOutput, WalletSummary, SAPLING_SHARD_HEIGHT, }, - wallet::{AccountId, WalletShieldedOutput, WalletTx}, + wallet::{Note, NoteId, Recipient, WalletTx}, DecryptedOutput, }; +use zcash_keys::{ + address::{Address, Receiver, UnifiedAddress}, + encoding::AddressCodec, + keys::{ + AddressGenerationError, UnifiedAddressRequest, UnifiedFullViewingKey, + UnifiedIncomingViewingKey, UnifiedSpendingKey, + }, +}; +use zcash_primitives::{ + block::BlockHash, + merkle_tree::read_commitment_tree, + transaction::{Transaction, TransactionData, TxId}, +}; +use zcash_protocol::{ + consensus::{self, BlockHeight, BranchId, NetworkUpgrade, Parameters}, + memo::{Memo, MemoBytes}, + value::{ZatBalance, Zatoshis}, + PoolType, ShieldedProtocol, +}; +use zip32::{DiversifierIndex, Scope}; + +use self::scanning::{parse_priority_code, priority_code, replace_queue_entries}; +use crate::{ + error::SqliteClientError, + wallet::commitment_tree::{get_max_checkpointed_height, SqliteShardStore}, + AccountRef, SqlTransaction, TransferType, WalletCommitmentTrees, WalletDb, PRUNING_DEPTH, + SAPLING_TABLES_PREFIX, +}; +use crate::{AccountUuid, TxRef, VERIFY_LOOKAHEAD}; + +#[cfg(feature = "transparent-inputs")] +use ::transparent::bundle::{OutPoint, TxOut}; -use crate::{error::SqliteClientError, DataConnStmtCache, NoteId, WalletDb}; +#[cfg(feature = "orchard")] +use {crate::ORCHARD_TABLES_PREFIX, zcash_client_backend::data_api::ORCHARD_SHARD_HEIGHT}; +pub mod commitment_tree; +pub(crate) mod common; +mod db; pub mod init; -pub mod transact; +#[cfg(feature = "orchard")] +pub(crate) mod orchard; +pub(crate) mod sapling; +pub(crate) mod scanning; +#[cfg(feature = "transparent-inputs")] +pub(crate) mod transparent; + +pub(crate) const BLOCK_SAPLING_FRONTIER_ABSENT: &[u8] = &[0x0]; + +fn parse_account_source( + account_kind: u32, + hd_seed_fingerprint: Option<[u8; 32]>, + hd_account_index: Option, + spending_key_available: bool, + key_source: Option, +) -> Result { + let derivation = hd_seed_fingerprint + .zip(hd_account_index) + .map(|(seed_fp, idx)| { + zip32::AccountId::try_from(idx) + .map_err(|_| { + SqliteClientError::CorruptedData( + "ZIP-32 account ID from wallet DB is out of range.".to_string(), + ) + }) + .map(|idx| Zip32Derivation::new(SeedFingerprint::from_bytes(seed_fp), idx)) + }) + .transpose()?; + + match (account_kind, derivation) { + (0, Some(derivation)) => Ok(AccountSource::Derived { + derivation, + key_source, + }), + (1, derivation) => Ok(AccountSource::Imported { + purpose: if spending_key_available { + AccountPurpose::Spending { derivation } + } else { + AccountPurpose::ViewOnly + }, + key_source, + }), + (0, None) => Err(SqliteClientError::CorruptedData( + "Wallet DB account_kind constraint violated".to_string(), + )), + (_, _) => Err(SqliteClientError::CorruptedData( + "Unrecognized account_kind".to_string(), + )), + } +} + +fn account_kind_code(value: &AccountSource) -> u32 { + match value { + AccountSource::Derived { .. } => 0, + AccountSource::Imported { .. } => 1, + } +} + +/// The viewing key that an [`Account`] has available to it. +#[derive(Debug, Clone)] +pub(crate) enum ViewingKey { + /// A full viewing key. + /// + /// This is available to derived accounts, as well as accounts directly imported as + /// full viewing keys. + Full(Box), + + /// An incoming viewing key. + /// + /// Accounts that have this kind of viewing key cannot be used in wallet contexts, + /// because they are unable to maintain an accurate balance. + Incoming(Box), +} -/// This trait provides a generalization over shielded output representations. -pub trait ShieldedOutput { - fn index(&self) -> usize; - fn account(&self) -> AccountId; - fn to(&self) -> &PaymentAddress; - fn note(&self) -> &Note; - fn memo(&self) -> Option<&MemoBytes>; - fn is_change(&self) -> Option; - fn nullifier(&self) -> Option; +/// An account stored in a `zcash_client_sqlite` database. +#[derive(Debug, Clone)] +pub struct Account { + uuid: AccountUuid, + name: Option, + kind: AccountSource, + viewing_key: ViewingKey, } -impl ShieldedOutput for WalletShieldedOutput { - fn index(&self) -> usize { - self.index +impl Account { + /// Returns the default Unified Address for the account, along with the diversifier index that + /// generated it. + /// + /// The diversifier index may be non-zero if the Unified Address includes a Sapling + /// receiver, and there was no valid Sapling receiver at diversifier index zero. + pub(crate) fn default_address( + &self, + request: Option, + ) -> Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError> { + self.uivk().default_address(request) } - fn account(&self) -> AccountId { - self.account +} + +impl zcash_client_backend::data_api::Account for Account { + type AccountId = AccountUuid; + + fn id(&self) -> AccountUuid { + self.uuid } - fn to(&self) -> &PaymentAddress { - &self.to + + fn name(&self) -> Option<&str> { + self.name.as_deref() } - fn note(&self) -> &Note { - &self.note + + fn source(&self) -> &AccountSource { + &self.kind } - fn memo(&self) -> Option<&MemoBytes> { - None + + fn ufvk(&self) -> Option<&UnifiedFullViewingKey> { + self.viewing_key.ufvk() } - fn is_change(&self) -> Option { - Some(self.is_change) + + fn uivk(&self) -> UnifiedIncomingViewingKey { + self.viewing_key.uivk() } +} - fn nullifier(&self) -> Option { - Some(self.nf) +impl ViewingKey { + fn ufvk(&self) -> Option<&UnifiedFullViewingKey> { + match self { + ViewingKey::Full(ufvk) => Some(ufvk), + ViewingKey::Incoming(_) => None, + } + } + + fn uivk(&self) -> UnifiedIncomingViewingKey { + match self { + ViewingKey::Full(ufvk) => ufvk.as_ref().to_unified_incoming_viewing_key(), + ViewingKey::Incoming(uivk) => uivk.as_ref().clone(), + } } } -impl ShieldedOutput for DecryptedOutput { - fn index(&self) -> usize { - self.index +pub(crate) fn seed_matches_derived_account( + params: &P, + seed: &SecretVec, + seed_fingerprint: &SeedFingerprint, + account_index: zip32::AccountId, + uivk: &UnifiedIncomingViewingKey, +) -> Result { + let seed_fingerprint_match = + &SeedFingerprint::from_seed(seed.expose_secret()).ok_or_else(|| { + SqliteClientError::BadAccountData( + "Seed must be between 32 and 252 bytes in length.".to_owned(), + ) + })? == seed_fingerprint; + + // `UnifiedIncomingViewingKey`s are not comparable with `Eq`, but Unified Address + // components are, so we derive corresponding addresses for each key and use + // those to check whether any components match. + let uivk_match = { + let usk = UnifiedSpendingKey::from_seed(params, &seed.expose_secret()[..], account_index) + .map_err(|_| SqliteClientError::KeyDerivationError(account_index))?; + + let (seed_addr, _) = usk.to_unified_full_viewing_key().default_address(None)?; + let (uivk_addr, _) = uivk.default_address(None)?; + + #[cfg(not(feature = "orchard"))] + let orchard_match = false; + #[cfg(feature = "orchard")] + let orchard_match = seed_addr + .orchard() + .zip(uivk_addr.orchard()) + .map(|(a, b)| a == b) + == Some(true); + + let sapling_match = seed_addr + .sapling() + .zip(uivk_addr.sapling()) + .map(|(a, b)| a == b) + == Some(true); + + let p2pkh_match = seed_addr + .transparent() + .zip(uivk_addr.transparent()) + .map(|(a, b)| a == b) + == Some(true); + + orchard_match || sapling_match || p2pkh_match + }; + + if seed_fingerprint_match != uivk_match { + // If these mismatch, it suggests database corruption. + Err(SqliteClientError::CorruptedData(format!( + "Seed fingerprint match: {seed_fingerprint_match}, uivk match: {uivk_match}" + ))) + } else { + Ok(seed_fingerprint_match && uivk_match) } - fn account(&self) -> AccountId { - self.account +} + +pub(crate) fn pool_code(pool_type: PoolType) -> i64 { + // These constants are *incidentally* shared with the typecodes + // for unified addresses, but this is exclusively an internal + // implementation detail. + match pool_type { + PoolType::Transparent => 0i64, + PoolType::Shielded(ShieldedProtocol::Sapling) => 2i64, + PoolType::Shielded(ShieldedProtocol::Orchard) => 3i64, } - fn to(&self) -> &PaymentAddress { - &self.to +} + +pub(crate) fn scope_code(scope: Scope) -> i64 { + match scope { + Scope::External => 0i64, + Scope::Internal => 1i64, } - fn note(&self) -> &Note { - &self.note +} + +pub(crate) fn parse_scope(code: i64) -> Option { + match code { + 0i64 => Some(Scope::External), + 1i64 => Some(Scope::Internal), + _ => None, } - fn memo(&self) -> Option<&MemoBytes> { - Some(&self.memo) +} + +pub(crate) fn memo_repr(memo: Option<&MemoBytes>) -> Option<&[u8]> { + memo.map(|m| { + if m == &MemoBytes::empty() { + // we store the empty memo as a single 0xf6 byte + &[0xf6] + } else { + m.as_slice() + } + }) +} + +// Returns the highest used account index for a given seed. +pub(crate) fn max_zip32_account_index( + conn: &rusqlite::Connection, + seed_id: &SeedFingerprint, +) -> Result, SqliteClientError> { + conn.query_row_and_then( + "SELECT MAX(hd_account_index) FROM accounts WHERE hd_seed_fingerprint = :hd_seed", + [seed_id.to_bytes()], + |row| { + row.get::<_, Option>(0)? + .map(zip32::AccountId::try_from) + .transpose() + .map_err(|_| SqliteClientError::Zip32AccountIndexOutOfRange) + }, + ) +} + +pub(crate) fn add_account( + conn: &rusqlite::Transaction, + params: &P, + account_name: &str, + kind: &AccountSource, + viewing_key: ViewingKey, + birthday: &AccountBirthday, +) -> Result { + if let Some(ufvk) = viewing_key.ufvk() { + // Check whether any component of this UFVK collides with an existing imported or derived FVK. + if let Some(existing_account) = get_account_for_ufvk(conn, params, ufvk)? { + return Err(SqliteClientError::AccountCollision(existing_account.id())); + } } - fn is_change(&self) -> Option { - None + // TODO(#1490): check for IVK collisions. + + let account_uuid = AccountUuid(Uuid::new_v4()); + + let (derivation, spending_key_available, key_source) = match kind { + AccountSource::Derived { + derivation, + key_source, + } => (Some(derivation), true, key_source), + AccountSource::Imported { + purpose: AccountPurpose::Spending { derivation }, + key_source, + } => (derivation.as_ref(), true, key_source), + AccountSource::Imported { + purpose: AccountPurpose::ViewOnly, + key_source, + } => (None, false, key_source), + }; + + #[cfg(feature = "orchard")] + let orchard_item = viewing_key + .ufvk() + .and_then(|ufvk| ufvk.orchard().map(|k| k.to_bytes())); + #[cfg(not(feature = "orchard"))] + let orchard_item: Option> = None; + + let sapling_item = viewing_key + .ufvk() + .and_then(|ufvk| ufvk.sapling().map(|k| k.to_bytes())); + + #[cfg(feature = "transparent-inputs")] + let transparent_item = viewing_key + .ufvk() + .and_then(|ufvk| ufvk.transparent().map(|k| k.serialize())); + #[cfg(not(feature = "transparent-inputs"))] + let transparent_item: Option> = None; + + let birthday_sapling_tree_size = Some(birthday.sapling_frontier().tree_size()); + #[cfg(feature = "orchard")] + let birthday_orchard_tree_size = Some(birthday.orchard_frontier().tree_size()); + #[cfg(not(feature = "orchard"))] + let birthday_orchard_tree_size: Option = None; + + let ufvk_encoded = viewing_key.ufvk().map(|ufvk| ufvk.encode(params)); + let account_id = conn + .query_row( + r#" + INSERT INTO accounts ( + name, + uuid, + account_kind, hd_seed_fingerprint, hd_account_index, key_source, + ufvk, uivk, + orchard_fvk_item_cache, sapling_fvk_item_cache, p2pkh_fvk_item_cache, + birthday_height, birthday_sapling_tree_size, birthday_orchard_tree_size, + recover_until_height, + has_spend_key + ) + VALUES ( + :account_name, + :uuid, + :account_kind, :hd_seed_fingerprint, :hd_account_index, :key_source, + :ufvk, :uivk, + :orchard_fvk_item_cache, :sapling_fvk_item_cache, :p2pkh_fvk_item_cache, + :birthday_height, :birthday_sapling_tree_size, :birthday_orchard_tree_size, + :recover_until_height, + :has_spend_key + ) + RETURNING id + "#, + named_params![ + ":account_name": account_name, + ":uuid": account_uuid.0, + ":account_kind": account_kind_code(kind), + ":hd_seed_fingerprint": derivation.map(|d| d.seed_fingerprint().to_bytes()), + ":hd_account_index": derivation.map(|d| u32::from(d.account_index())), + ":key_source": key_source, + ":ufvk": ufvk_encoded, + ":uivk": viewing_key.uivk().encode(params), + ":orchard_fvk_item_cache": orchard_item, + ":sapling_fvk_item_cache": sapling_item, + ":p2pkh_fvk_item_cache": transparent_item, + ":birthday_height": u32::from(birthday.height()), + ":birthday_sapling_tree_size": birthday_sapling_tree_size, + ":birthday_orchard_tree_size": birthday_orchard_tree_size, + ":recover_until_height": birthday.recover_until().map(u32::from), + ":has_spend_key": spending_key_available as i64, + ], + |row| row.get(0).map(AccountRef), + ) + .map_err(|e| match e { + rusqlite::Error::SqliteFailure(f, s) + if f.code == rusqlite::ErrorCode::ConstraintViolation => + { + // An account conflict occurred. This should already have been caught by + // the check using `get_account_for_ufvk` above, but in case it wasn't, + // make a best effort to determine the AccountRef of the pre-existing row + // and provide that to our caller. + if let Ok(colliding_uuid) = conn.query_row( + "SELECT uuid FROM accounts WHERE ufvk = ?", + params![ufvk_encoded], + |row| Ok(AccountUuid(row.get(0)?)), + ) { + return SqliteClientError::AccountCollision(colliding_uuid); + } + + SqliteClientError::from(rusqlite::Error::SqliteFailure(f, s)) + } + _ => SqliteClientError::from(e), + })?; + + let account = Account { + name: Some(account_name.to_owned()), + uuid: account_uuid, + kind: kind.clone(), + viewing_key, + }; + + // If a birthday frontier is available, insert it into the note commitment tree. If the + // birthday frontier is the empty frontier, we don't need to do anything. + if let Some(frontier) = birthday.sapling_frontier().value() { + debug!("Inserting Sapling frontier into ShardTree: {:?}", frontier); + let shard_store = + SqliteShardStore::<_, ::sapling::Node, SAPLING_SHARD_HEIGHT>::from_connection( + conn, + SAPLING_TABLES_PREFIX, + )?; + let mut shard_tree: ShardTree< + _, + { ::sapling::NOTE_COMMITMENT_TREE_DEPTH }, + SAPLING_SHARD_HEIGHT, + > = ShardTree::new(shard_store, PRUNING_DEPTH.try_into().unwrap()); + shard_tree.insert_frontier_nodes( + frontier.clone(), + Retention::Checkpoint { + // This subtraction is safe, because all leaves in the tree appear in blocks, and + // the invariant that birthday.height() always corresponds to the block for which + // `frontier` is the tree state at the start of the block. Together, this means + // there exists a prior block for which frontier is the tree state at the end of + // the block. + id: birthday.height() - 1, + marking: Marking::Reference, + }, + )?; } - fn nullifier(&self) -> Option { - None + + #[cfg(feature = "orchard")] + if let Some(frontier) = birthday.orchard_frontier().value() { + debug!("Inserting Orchard frontier into ShardTree: {:?}", frontier); + let shard_store = SqliteShardStore::< + _, + ::orchard::tree::MerkleHashOrchard, + ORCHARD_SHARD_HEIGHT, + >::from_connection(conn, ORCHARD_TABLES_PREFIX)?; + let mut shard_tree: ShardTree< + _, + { ::orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }, + ORCHARD_SHARD_HEIGHT, + > = ShardTree::new(shard_store, PRUNING_DEPTH.try_into().unwrap()); + shard_tree.insert_frontier_nodes( + frontier.clone(), + Retention::Checkpoint { + // This subtraction is safe, because all leaves in the tree appear in blocks, and + // the invariant that birthday.height() always corresponds to the block for which + // `frontier` is the tree state at the start of the block. Together, this means + // there exists a prior block for which frontier is the tree state at the end of + // the block. + id: birthday.height() - 1, + marking: Marking::Reference, + }, + )?; + } + + // The ignored range always starts at Sapling activation + let sapling_activation_height = params + .activation_height(NetworkUpgrade::Sapling) + .expect("Sapling activation height must be available."); + + // Add the ignored range up to the birthday height. + if sapling_activation_height < birthday.height() { + let ignored_range = sapling_activation_height..birthday.height(); + + replace_queue_entries::( + conn, + &ignored_range, + Some(ScanRange::from_parts( + ignored_range.clone(), + ScanPriority::Ignored, + )) + .into_iter(), + false, + )?; + }; + + // Rewrite the scan ranges from the birthday height up to the chain tip so that we'll ensure we + // re-scan to find any notes that might belong to the newly added account. + if let Some(t) = chain_tip_height(conn)? { + let rescan_range = birthday.height()..(t + 1); + + replace_queue_entries::( + conn, + &rescan_range, + Some(ScanRange::from_parts( + rescan_range.clone(), + ScanPriority::Historic, + )) + .into_iter(), + true, // force rescan + )?; } + + // Always derive the default Unified Address for the account. If the account's viewing + // key has fewer components than the wallet supports (most likely due to this being an + // imported viewing key), derive an address containing the common subset of receivers. + let (address, d_idx) = account.default_address(None)?; + insert_address(conn, params, account_id, d_idx, &address)?; + + // Initialize the `ephemeral_addresses` table. + #[cfg(feature = "transparent-inputs")] + transparent::ephemeral::init_account(conn, params, account_id)?; + + Ok(account) } -/// Returns the address for the account. -/// -/// # Examples -/// -/// ``` -/// use tempfile::NamedTempFile; -/// use zcash_primitives::{ -/// consensus::{self, Network}, -/// }; -/// use zcash_client_backend::wallet::AccountId; -/// use zcash_client_sqlite::{ -/// WalletDb, -/// wallet::get_address, -/// }; -/// -/// let data_file = NamedTempFile::new().unwrap(); -/// let db = WalletDb::for_path(data_file, Network::TestNetwork).unwrap(); -/// let addr = get_address(&db, AccountId(0)); -/// ``` -pub fn get_address( - wdb: &WalletDb

, - account: AccountId, -) -> Result, SqliteClientError> { - let addr: String = wdb.conn.query_row( - "SELECT address FROM accounts - WHERE account = ?", - &[account.0], - |row| row.get(0), - )?; +pub(crate) fn get_current_address( + conn: &rusqlite::Connection, + params: &P, + account_uuid: AccountUuid, +) -> Result, SqliteClientError> { + // This returns the most recently generated address. + let addr: Option<(String, Vec)> = conn + .query_row( + "SELECT address, diversifier_index_be + FROM addresses + JOIN accounts ON addresses.account_id = accounts.id + WHERE accounts.uuid = :account_uuid + ORDER BY diversifier_index_be DESC + LIMIT 1", + named_params![":account_uuid": account_uuid.0], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .optional()?; - decode_payment_address(wdb.params.hrp_sapling_payment_address(), &addr) - .map_err(SqliteClientError::Bech32) + addr.map(|(addr_str, di_vec)| { + let mut di_be: [u8; 11] = di_vec.try_into().map_err(|_| { + SqliteClientError::CorruptedData("Diversifier index is not an 11-byte value".to_owned()) + })?; + di_be.reverse(); + + Address::decode(params, &addr_str) + .ok_or_else(|| { + SqliteClientError::CorruptedData("Not a valid Zcash recipient address".to_owned()) + }) + .and_then(|addr| match addr { + Address::Unified(ua) => Ok(ua), + _ => Err(SqliteClientError::CorruptedData(format!( + "Addresses table contains {} which is not a unified address", + addr_str, + ))), + }) + .map(|addr| (addr, DiversifierIndex::from(di_be))) + }) + .transpose() } -/// Returns the [`ExtendedFullViewingKey`]s for the wallet. +/// Adds the given address and diversifier index to the addresses table. /// -/// [`ExtendedFullViewingKey`]: zcash_primitives::zip32::ExtendedFullViewingKey -pub fn get_extended_full_viewing_keys( - wdb: &WalletDb

, -) -> Result, SqliteClientError> { - // Fetch the ExtendedFullViewingKeys we are tracking - let mut stmt_fetch_accounts = wdb - .conn - .prepare("SELECT account, extfvk FROM accounts ORDER BY account ASC")?; - - let rows = stmt_fetch_accounts - .query_map(NO_PARAMS, |row| { - let acct = row.get(0).map(AccountId)?; - let extfvk = row.get(1).map(|extfvk: String| { - decode_extended_full_viewing_key( - wdb.params.hrp_sapling_extended_full_viewing_key(), - &extfvk, - ) - .map_err(SqliteClientError::Bech32) - .and_then(|k| k.ok_or(SqliteClientError::IncorrectHrpExtFvk)) - })?; +/// Returns the database row for the newly-inserted address. +pub(crate) fn insert_address( + conn: &rusqlite::Connection, + params: &P, + account_id: AccountRef, + diversifier_index: DiversifierIndex, + address: &UnifiedAddress, +) -> Result<(), SqliteClientError> { + let mut stmt = conn.prepare_cached( + "INSERT INTO addresses ( + account_id, + diversifier_index_be, + address, + cached_transparent_receiver_address + ) + VALUES ( + :account_id, + :diversifier_index_be, + :address, + :cached_transparent_receiver_address + )", + )?; - Ok((acct, extfvk)) - }) - .map_err(SqliteClientError::from)?; + // the diversifier index is stored in big-endian order to allow sorting + let mut di_be = *diversifier_index.as_bytes(); + di_be.reverse(); + stmt.execute(named_params![ + ":account_id": account_id.0, + ":diversifier_index_be": &di_be[..], + ":address": &address.encode(params), + ":cached_transparent_receiver_address": &address.transparent().map(|r| r.encode(params)), + ])?; - let mut res: HashMap = HashMap::new(); + Ok(()) +} + +/// Returns the [`UnifiedFullViewingKey`]s for the wallet. +pub(crate) fn get_unified_full_viewing_keys( + conn: &rusqlite::Connection, + params: &P, +) -> Result, SqliteClientError> { + // Fetch the UnifiedFullViewingKeys we are tracking + let mut stmt_fetch_accounts = conn.prepare("SELECT uuid, ufvk FROM accounts")?; + + let rows = stmt_fetch_accounts.query_map([], |row| { + let ufvk_str: Option = row.get(1)?; + if let Some(ufvk_str) = ufvk_str { + let ufvk = UnifiedFullViewingKey::decode(params, &ufvk_str) + .map_err(SqliteClientError::CorruptedData); + Ok(Some((AccountUuid(row.get(0)?), ufvk))) + } else { + Ok(None) + } + })?; + + let mut res: HashMap = HashMap::new(); for row in rows { - let (account_id, efvkr) = row?; - res.insert(account_id, efvkr?); + if let Some((account_id, ufvkr)) = row? { + res.insert(account_id, ufvkr?); + } } Ok(res) } -/// Checks whether the specified [`ExtendedFullViewingKey`] is valid and corresponds to the -/// specified account. -/// -/// [`ExtendedFullViewingKey`]: zcash_primitives::zip32::ExtendedFullViewingKey -pub fn is_valid_account_extfvk( - wdb: &WalletDb

, - account: AccountId, - extfvk: &ExtendedFullViewingKey, -) -> Result { - wdb.conn - .prepare("SELECT * FROM accounts WHERE account = ? AND extfvk = ?")? - .exists(&[ - account.0.to_sql()?, - encode_extended_full_viewing_key( - wdb.params.hrp_sapling_extended_full_viewing_key(), - extfvk, - ) - .to_sql()?, - ]) - .map_err(SqliteClientError::from) +fn parse_account_row( + row: &rusqlite::Row<'_>, + params: &P, +) -> Result { + let account_name = row.get("name")?; + let account_uuid = AccountUuid(row.get("uuid")?); + let kind = parse_account_source( + row.get("account_kind")?, + row.get("hd_seed_fingerprint")?, + row.get("hd_account_index")?, + row.get("has_spend_key")?, + row.get("key_source")?, + )?; + + let ufvk_str: Option = row.get("ufvk")?; + let viewing_key = if let Some(ufvk_str) = ufvk_str { + ViewingKey::Full(Box::new( + UnifiedFullViewingKey::decode(params, &ufvk_str).map_err(|e| { + SqliteClientError::CorruptedData(format!( + "Could not decode unified full viewing key for account {}: {}", + account_uuid.0, e + )) + })?, + )) + } else { + let uivk_str: String = row.get("uivk")?; + ViewingKey::Incoming(Box::new( + UnifiedIncomingViewingKey::decode(params, &uivk_str).map_err(|e| { + SqliteClientError::CorruptedData(format!( + "Could not decode unified incoming viewing key for account {}: {}", + account_uuid.0, e + )) + })?, + )) + }; + + Ok(Account { + name: account_name, + uuid: account_uuid, + kind, + viewing_key, + }) } -/// Returns the balance for the account, including all mined unspent notes that we know -/// about. -/// -/// WARNING: This balance is potentially unreliable, as mined notes may become unmined due -/// to chain reorgs. You should generally not show this balance to users without some -/// caveat. Use [`get_balance_at`] where you need a more reliable indication of the -/// wallet balance. -/// -/// # Examples -/// -/// ``` -/// use tempfile::NamedTempFile; -/// use zcash_primitives::consensus::Network; -/// use zcash_client_backend::wallet::AccountId; -/// use zcash_client_sqlite::{ -/// WalletDb, -/// wallet::get_balance, -/// }; -/// -/// let data_file = NamedTempFile::new().unwrap(); -/// let db = WalletDb::for_path(data_file, Network::TestNetwork).unwrap(); -/// let addr = get_balance(&db, AccountId(0)); -/// ``` -pub fn get_balance

(wdb: &WalletDb

, account: AccountId) -> Result { - let balance = wdb.conn.query_row( - "SELECT SUM(value) FROM received_notes - INNER JOIN transactions ON transactions.id_tx = received_notes.tx - WHERE account = ? AND spent IS NULL AND transactions.block IS NOT NULL", - &[account.0], - |row| row.get(0).or(Ok(0)), +pub(crate) fn get_account( + conn: &rusqlite::Connection, + params: &P, + account_uuid: AccountUuid, +) -> Result, SqliteClientError> { + let mut stmt = conn.prepare_cached( + r#" + SELECT name, uuid, account_kind, + hd_seed_fingerprint, hd_account_index, key_source, + ufvk, uivk, has_spend_key + FROM accounts + WHERE uuid = :account_uuid + "#, )?; - match Amount::from_i64(balance) { - Ok(amount) if !amount.is_negative() => Ok(amount), - _ => Err(SqliteClientError::CorruptedData( - "Sum of values in received_notes is out of range".to_string(), - )), + let mut rows = stmt.query_and_then::<_, SqliteClientError, _, _>( + named_params![":account_uuid": account_uuid.0], + |row| parse_account_row(row, params), + )?; + + rows.next().transpose() +} + +/// Returns the account id corresponding to a given [`UnifiedFullViewingKey`], +/// if any. +pub(crate) fn get_account_for_ufvk( + conn: &rusqlite::Connection, + params: &P, + ufvk: &UnifiedFullViewingKey, +) -> Result, SqliteClientError> { + #[cfg(feature = "orchard")] + let orchard_item = ufvk.orchard().map(|k| k.to_bytes()); + #[cfg(not(feature = "orchard"))] + let orchard_item: Option> = None; + + let sapling_item = ufvk.sapling().map(|k| k.to_bytes()); + + #[cfg(feature = "transparent-inputs")] + let transparent_item = ufvk.transparent().map(|k| k.serialize()); + #[cfg(not(feature = "transparent-inputs"))] + let transparent_item: Option> = None; + + let mut stmt = conn.prepare( + "SELECT name, uuid, account_kind, + hd_seed_fingerprint, hd_account_index, key_source, + ufvk, uivk, has_spend_key + FROM accounts + WHERE orchard_fvk_item_cache = :orchard_fvk_item_cache + OR sapling_fvk_item_cache = :sapling_fvk_item_cache + OR p2pkh_fvk_item_cache = :p2pkh_fvk_item_cache", + )?; + + let accounts = stmt + .query_and_then::<_, SqliteClientError, _, _>( + named_params![ + ":orchard_fvk_item_cache": orchard_item, + ":sapling_fvk_item_cache": sapling_item, + ":p2pkh_fvk_item_cache": transparent_item, + ], + |row| parse_account_row(row, params), + )? + .collect::, _>>()?; + + if accounts.len() > 1 { + Err(SqliteClientError::CorruptedData( + "Mutiple account records matched the provided UFVK".to_owned(), + )) + } else { + Ok(accounts.into_iter().next()) } } -/// Returns the verified balance for the account at the specified height, -/// This may be used to obtain a balance that ignores notes that have been -/// received so recently that they are not yet deemed spendable. -/// -/// # Examples -/// -/// ``` -/// use tempfile::NamedTempFile; -/// use zcash_primitives::consensus::{BlockHeight, Network}; -/// use zcash_client_backend::wallet::AccountId; -/// use zcash_client_sqlite::{ -/// WalletDb, -/// wallet::get_balance_at, -/// }; -/// -/// let data_file = NamedTempFile::new().unwrap(); -/// let db = WalletDb::for_path(data_file, Network::TestNetwork).unwrap(); -/// let addr = get_balance_at(&db, AccountId(0), BlockHeight::from_u32(0)); -/// ``` -pub fn get_balance_at

( - wdb: &WalletDb

, - account: AccountId, - anchor_height: BlockHeight, -) -> Result { - let balance = wdb.conn.query_row( - "SELECT SUM(value) FROM received_notes - INNER JOIN transactions ON transactions.id_tx = received_notes.tx - WHERE account = ? AND spent IS NULL AND transactions.block <= ?", - &[account.0, u32::from(anchor_height)], - |row| row.get(0).or(Ok(0)), +/// Returns the account id corresponding to a given [`SeedFingerprint`] +/// and [`zip32::AccountId`], if any. +pub(crate) fn get_derived_account( + conn: &rusqlite::Connection, + params: &P, + seed_fp: &SeedFingerprint, + account_index: zip32::AccountId, +) -> Result, SqliteClientError> { + let mut stmt = conn.prepare( + "SELECT name, key_source, uuid, ufvk + FROM accounts + WHERE hd_seed_fingerprint = :hd_seed_fingerprint + AND hd_account_index = :hd_account_index", + )?; + + let mut accounts = stmt.query_and_then::<_, SqliteClientError, _, _>( + named_params![ + ":hd_seed_fingerprint": seed_fp.to_bytes(), + ":hd_account_index": u32::from(account_index), + ], + |row| { + let account_name = row.get("name")?; + let key_source = row.get("key_source")?; + let account_uuid = AccountUuid(row.get("uuid")?); + let ufvk = match row.get::<_, Option>("ufvk")? { + None => Err(SqliteClientError::CorruptedData(format!( + "Missing unified full viewing key for derived account {}", + account_uuid.0, + ))), + Some(ufvk_str) => UnifiedFullViewingKey::decode(params, &ufvk_str).map_err(|e| { + SqliteClientError::CorruptedData(format!( + "Could not decode unified full viewing key for account {}: {}", + account_uuid.0, e + )) + }), + }?; + Ok(Account { + name: account_name, + uuid: account_uuid, + kind: AccountSource::Derived { + derivation: Zip32Derivation::new(*seed_fp, account_index), + key_source, + }, + viewing_key: ViewingKey::Full(Box::new(ufvk)), + }) + }, )?; - match Amount::from_i64(balance) { - Ok(amount) if !amount.is_negative() => Ok(amount), - _ => Err(SqliteClientError::CorruptedData( - "Sum of values in received_notes is out of range".to_string(), + accounts.next().transpose() +} + +pub(crate) trait ProgressEstimator { + fn sapling_scan_progress( + &self, + conn: &rusqlite::Connection, + params: &P, + birthday_height: BlockHeight, + recover_until_height: Option, + fully_scanned_height: Option, + chain_tip_height: BlockHeight, + ) -> Result, SqliteClientError>; + + #[cfg(feature = "orchard")] + fn orchard_scan_progress( + &self, + conn: &rusqlite::Connection, + params: &P, + birthday_height: BlockHeight, + recover_until_height: Option, + fully_scanned_height: Option, + chain_tip_height: BlockHeight, + ) -> Result, SqliteClientError>; +} + +#[derive(Debug)] +pub(crate) struct SubtreeProgressEstimator; + +fn table_constants( + shielded_protocol: ShieldedProtocol, +) -> Result<(&'static str, &'static str, u8), SqliteClientError> { + match shielded_protocol { + ShieldedProtocol::Sapling => Ok(( + SAPLING_TABLES_PREFIX, + "sapling_output_count", + SAPLING_SHARD_HEIGHT, )), + #[cfg(feature = "orchard")] + ShieldedProtocol::Orchard => Ok(( + ORCHARD_TABLES_PREFIX, + "orchard_action_count", + ORCHARD_SHARD_HEIGHT, + )), + #[cfg(not(feature = "orchard"))] + ShieldedProtocol::Orchard => Err(SqliteClientError::UnsupportedPoolType(PoolType::ORCHARD)), } } -/// Returns the memo for a received note. -/// -/// The note is identified by its row index in the `received_notes` table within the wdb -/// database. -/// -/// # Examples -/// -/// ``` -/// use tempfile::NamedTempFile; -/// use zcash_primitives::consensus::Network; -/// use zcash_client_sqlite::{ -/// NoteId, -/// WalletDb, -/// wallet::get_received_memo, -/// }; -/// -/// let data_file = NamedTempFile::new().unwrap(); -/// let db = WalletDb::for_path(data_file, Network::TestNetwork).unwrap(); -/// let memo = get_received_memo(&db, 27); -/// ``` -pub fn get_received_memo

(wdb: &WalletDb

, id_note: i64) -> Result { - let memo_bytes: Vec<_> = wdb.conn.query_row( - "SELECT memo FROM received_notes - WHERE id_note = ?", - &[id_note], - |row| row.get(0), - )?; +fn estimate_tree_size( + conn: &rusqlite::Connection, + params: &P, + shielded_protocol: ShieldedProtocol, + pool_activation_height: BlockHeight, + chain_tip_height: BlockHeight, +) -> Result, SqliteClientError> { + let (table_prefix, _, shard_height) = table_constants(shielded_protocol)?; + + // Estimate the size of the tree by linear extrapolation from available + // data closest to the chain tip. + // + // - If we have scanned blocks within the incomplete subtree, and we know + // the tree size for the end of the most recent scanned range, then we + // extrapolate from the start of the incomplete subtree: + // + // subtree + // / \ + // / \ + // / \ + // / \ + // |<--------->| | + // | scanned | tip + // last_scanned + // + // + // subtree + // / \ + // / \ + // / \ + // / \ + // |<------->| | + // | scanned | tip + // last_scanned + // + // - If we don't have scanned blocks within the incomplete subtree, or we + // don't know the tree size, then we extrapolate from the block-width of + // the last complete subtree. + // + // This avoids having a sharp discontinuity in the progress percentages + // shown to users, and gets more accurate the closer to the chain tip we + // have scanned. + // + // TODO: it would be nice to be able to reliably have the size of the + // commitment tree at the chain tip without having to have scanned that + // block. + + // Get the tree size at the last scanned height, if known. + let last_scanned = block_max_scanned(conn, params)?.and_then(|last_scanned| { + match shielded_protocol { + ShieldedProtocol::Sapling => last_scanned.sapling_tree_size(), + #[cfg(feature = "orchard")] + ShieldedProtocol::Orchard => last_scanned.orchard_tree_size(), + #[cfg(not(feature = "orchard"))] + ShieldedProtocol::Orchard => None, + } + .map(|tree_size| (last_scanned.block_height(), u64::from(tree_size))) + }); + + // Get the last completed subtree. + let last_completed_subtree = conn + .query_row( + &format!( + "SELECT shard_index, subtree_end_height + FROM {table_prefix}_tree_shards + WHERE subtree_end_height IS NOT NULL + ORDER BY shard_index DESC + LIMIT 1" + ), + [], + |row| { + Ok(( + incrementalmerkletree::Address::from_parts( + incrementalmerkletree::Level::new(shard_height), + row.get(0)?, + ), + BlockHeight::from_u32(row.get(1)?), + )) + }, + ) + // `None` if we have no subtree roots yet. + .optional()?; - MemoBytes::from_bytes(&memo_bytes) - .and_then(Memo::try_from) - .map_err(SqliteClientError::from) + let result = if let Some((last_completed_subtree, last_completed_subtree_end)) = + last_completed_subtree + { + // If we know the tree size at the last scanned height, and that + // height is within the incomplete subtree, extrapolate. + let tip_tree_size = last_scanned.and_then(|(last_scanned, last_scanned_tree_size)| { + (last_scanned > last_completed_subtree_end) + .then(|| { + let scanned_notes = last_scanned_tree_size + - u64::from(last_completed_subtree.position_range_end()); + let scanned_range = u64::from(last_scanned - last_completed_subtree_end); + let unscanned_range = u64::from(chain_tip_height - last_scanned); + + (scanned_notes * unscanned_range) + .checked_div(scanned_range) + .map(|extrapolated_unscanned_notes| { + last_scanned_tree_size + extrapolated_unscanned_notes + }) + }) + .flatten() + }); + + if let Some(tree_size) = tip_tree_size { + Some(tree_size) + } else if let Some(second_to_last_completed_subtree_end) = last_completed_subtree + .index() + .checked_sub(1) + .and_then(|subtree_index| { + conn.query_row( + &format!( + "SELECT subtree_end_height + FROM {table_prefix}_tree_shards + WHERE shard_index = :shard_index" + ), + named_params! {":shard_index": subtree_index}, + |row| Ok(row.get::<_, Option<_>>(0)?.map(BlockHeight::from_u32)), + ) + .transpose() + }) + .transpose()? + { + let notes_in_complete_subtrees = u64::from(last_completed_subtree.position_range_end()); + + let subtree_notes = 1 << shard_height; + let subtree_range = + u64::from(last_completed_subtree_end - second_to_last_completed_subtree_end); + let unscanned_range = u64::from(chain_tip_height - last_completed_subtree_end); + + (subtree_notes * unscanned_range) + .checked_div(subtree_range) + .map(|extrapolated_incomplete_subtree_notes| { + notes_in_complete_subtrees + extrapolated_incomplete_subtree_notes + }) + } else { + // There's only one completed subtree; its start height must + // be the activation height for this shielded protocol. + let subtree_notes = 1 << shard_height; + + let subtree_range = u64::from(last_completed_subtree_end - pool_activation_height); + let unscanned_range = u64::from(chain_tip_height - last_completed_subtree_end); + + (subtree_notes * unscanned_range) + .checked_div(subtree_range) + .map(|extrapolated_incomplete_subtree_notes| { + subtree_notes + extrapolated_incomplete_subtree_notes + }) + } + } else { + // If there are no completed subtrees, but we have scanned some blocks, we can still + // interpolate based upon the tree size as of the last scanned block. Here, since we + // don't have any subtree data to draw on, we will interpolate based on the number of + // blocks since the pool activation height + last_scanned.and_then(|(last_scanned_height, last_scanned_tree_size)| { + let subtree_range = u64::from(last_scanned_height - pool_activation_height); + let unscanned_range = u64::from(chain_tip_height - last_scanned_height); + + (last_scanned_tree_size * unscanned_range) + .checked_div(subtree_range) + .map(|extrapolated_incomplete_subtree_notes| { + last_scanned_tree_size + extrapolated_incomplete_subtree_notes + }) + }) + }; + + Ok(result) } -/// Returns the memo for a sent note. -/// -/// The note is identified by its row index in the `sent_notes` table within the wdb -/// database. -/// -/// # Examples +#[allow(clippy::too_many_arguments)] +fn subtree_scan_progress( + conn: &rusqlite::Connection, + params: &P, + shielded_protocol: ShieldedProtocol, + pool_activation_height: BlockHeight, + birthday_height: BlockHeight, + recover_until_height: Option, + fully_scanned_height: Option, + chain_tip_height: BlockHeight, +) -> Result, SqliteClientError> { + let (table_prefix, output_count_col, shard_height) = table_constants(shielded_protocol)?; + + let mut stmt_scanned_count_until = conn.prepare_cached(&format!( + "SELECT SUM({output_count_col}) + FROM blocks + WHERE :start_height <= height AND height < :end_height", + ))?; + let mut stmt_scanned_count_from = conn.prepare_cached(&format!( + "SELECT SUM({output_count_col}) + FROM blocks + WHERE :start_height <= height", + ))?; + let mut stmt_start_tree_size = conn.prepare_cached(&format!( + "SELECT MAX({table_prefix}_commitment_tree_size - {output_count_col}) + FROM blocks + WHERE height <= :start_height", + ))?; + let mut stmt_end_tree_size_at = conn.prepare_cached(&format!( + "SELECT {table_prefix}_commitment_tree_size + FROM blocks + WHERE height = :height", + ))?; + + if fully_scanned_height == Some(chain_tip_height) { + // Compute the total blocks scanned since the wallet birthday on either side of + // the recover-until height. + let recover = match recover_until_height { + Some(end_height) => stmt_scanned_count_until.query_row( + named_params! { + ":start_height": u32::from(birthday_height), + ":end_height": u32::from(end_height), + }, + |row| { + let recovered = row.get::<_, Option>(0)?; + Ok(recovered.map(|n| Ratio::new(n, n))) + }, + )?, + None => { + // If none of the wallet's accounts have a recover-until height, then there + // is no recovery phase for the wallet, and therefore the denominator in the + // resulting ratio (the number of notes in the recovery range) is zero. + Some(Ratio::new(0, 0)) + } + }; + + let scan = stmt_scanned_count_from.query_row( + named_params! { + ":start_height": u32::from( + recover_until_height.unwrap_or(birthday_height) + ), + }, + |row| { + let scanned = row.get::<_, Option>(0)?; + Ok(scanned.map(|n| Ratio::new(n, n))) + }, + )?; + + Ok(scan.map(|scan| Progress::new(scan, recover))) + } else { + // In case we didn't have information about the tree size at the recover-until + // height, get the tree size from a nearby subtree. It's fine for this to be + // approximate; it just shifts the boundary between scan and recover progress. + let mut get_tree_size_near = |as_of: BlockHeight| { + let size_from_blocks = stmt_start_tree_size + .query_row(named_params![":start_height": u32::from(as_of)], |row| { + row.get::<_, Option>(0) + }) + .optional()? + .flatten(); + + let size_from_subtree_roots = || { + conn.query_row( + &format!( + "SELECT MIN(shard_index) + FROM {table_prefix}_tree_shards + WHERE subtree_end_height >= :start_height + OR subtree_end_height IS NULL", + ), + named_params! { + ":start_height": u32::from(as_of), + }, + |row| { + let min_tree_size = row + .get::<_, Option>(0)? + .map(|min_idx| min_idx << shard_height); + Ok(min_tree_size) + }, + ) + .optional() + .map(|opt| opt.flatten()) + }; + + match size_from_blocks { + Some(size) => Ok(Some(size)), + None => size_from_subtree_roots(), + } + }; + + // Get the starting note commitment tree size from the wallet birthday, or failing that + // from the blocks table. + let birthday_size = match conn + .query_row( + &format!( + "SELECT birthday_{table_prefix}_tree_size + FROM accounts + WHERE birthday_height = :birthday_height", + ), + named_params![":birthday_height": u32::from(birthday_height)], + |row| row.get::<_, Option>(0), + ) + .optional()? + .flatten() + { + Some(tree_size) => Some(tree_size), + // If we don't have an explicit birthday tree size, find something nearby. + None => get_tree_size_near(birthday_height)?, + }; + + // Get the note commitment tree size as of the start of the recover-until height. + // The outer option indicates whether or not we have recover-until height information; + // the inner option indicates whether or not we were able to obtain a tree size given + // the recover-until height. + let recover_until_size: Option> = recover_until_height + // Find a tree size near to the recover-until height + .map(get_tree_size_near) + .transpose()?; + + // Count the total outputs scanned so far on the birthday side of the recover-until height. + let recovered_count = recover_until_height + .map(|end_height| { + stmt_scanned_count_until.query_row( + named_params! { + ":start_height": u32::from(birthday_height), + ":end_height": u32::from(end_height), + }, + |row| row.get::<_, Option>(0), + ) + }) + .transpose()?; + + // If we've scanned the block at the chain tip, we know how many notes are currently in the + // tree. + let tip_tree_size = match stmt_end_tree_size_at + .query_row( + named_params! {":height": u32::from(chain_tip_height)}, + |row| row.get::<_, Option>(0), + ) + .optional()? + .flatten() + { + Some(tree_size) => Some(tree_size), + None => estimate_tree_size( + conn, + params, + shielded_protocol, + pool_activation_height, + chain_tip_height, + )?, + }; + + let recover = recovered_count + .zip(recover_until_size) + .map(|(recovered, end_size)| { + birthday_size.zip(end_size).map(|(start_size, end_size)| { + Ratio::new(recovered.unwrap_or(0), end_size - start_size) + }) + }) + // If none of the wallet's accounts have a recover-until height, then there + // is no recovery phase for the wallet, and therefore the denominator in the + // resulting ratio (the number of notes in the recovery range) is zero. + .unwrap_or_else(|| Some(Ratio::new(0, 0))); + + let scan = { + // Count the total outputs scanned so far on the chain tip side of the + // recover-until height. + let scanned_count = stmt_scanned_count_from.query_row( + named_params![":start_height": u32::from(recover_until_height.unwrap_or(birthday_height))], + |row| row.get::<_, Option>(0), + )?; + + recover_until_size + .unwrap_or(birthday_size) + .zip(tip_tree_size) + .map(|(start_size, tip_tree_size)| { + Ratio::new(scanned_count.unwrap_or(0), tip_tree_size - start_size) + }) + }; + + Ok(scan.map(|scan| Progress::new(scan, recover))) + } +} + +impl ProgressEstimator for SubtreeProgressEstimator { + #[tracing::instrument(skip(conn, params))] + fn sapling_scan_progress( + &self, + conn: &rusqlite::Connection, + params: &P, + birthday_height: BlockHeight, + recover_until_height: Option, + fully_scanned_height: Option, + chain_tip_height: BlockHeight, + ) -> Result, SqliteClientError> { + subtree_scan_progress( + conn, + params, + ShieldedProtocol::Sapling, + params + .activation_height(NetworkUpgrade::Sapling) + .expect("Sapling activation height must be available."), + birthday_height, + recover_until_height, + fully_scanned_height, + chain_tip_height, + ) + } + + #[cfg(feature = "orchard")] + #[tracing::instrument(skip(conn, params))] + fn orchard_scan_progress( + &self, + conn: &rusqlite::Connection, + params: &P, + birthday_height: BlockHeight, + recover_until_height: Option, + fully_scanned_height: Option, + chain_tip_height: BlockHeight, + ) -> Result, SqliteClientError> { + subtree_scan_progress( + conn, + params, + ShieldedProtocol::Orchard, + params + .activation_height(NetworkUpgrade::Nu5) + .expect("NU5 activation height must be available."), + birthday_height, + recover_until_height, + fully_scanned_height, + chain_tip_height, + ) + } +} + +/// Returns the spendable balance for the account at the specified height. /// -/// ``` -/// use tempfile::NamedTempFile; -/// use zcash_primitives::consensus::Network; -/// use zcash_client_sqlite::{ -/// NoteId, -/// WalletDb, -/// wallet::get_sent_memo, -/// }; +/// This may be used to obtain a balance that ignores notes that have been detected so recently +/// that they are not yet spendable, or for which it is not yet possible to construct witnesses. /// -/// let data_file = NamedTempFile::new().unwrap(); -/// let db = WalletDb::for_path(data_file, Network::TestNetwork).unwrap(); -/// let memo = get_sent_memo(&db, 12); -/// ``` -pub fn get_sent_memo

(wdb: &WalletDb

, id_note: i64) -> Result { - let memo_bytes: Vec<_> = wdb.conn.query_row( - "SELECT memo FROM sent_notes - WHERE id_note = ?", - &[id_note], - |row| row.get(0), +/// `min_confirmations` can be 0, but that case is currently treated identically to +/// `min_confirmations == 1` for both shielded and transparent TXOs. This behaviour +/// may change in the future. +#[tracing::instrument(skip(tx, params, progress))] +pub(crate) fn get_wallet_summary( + tx: &rusqlite::Transaction, + params: &P, + min_confirmations: u32, + progress: &impl ProgressEstimator, +) -> Result>, SqliteClientError> { + let chain_tip_height = match chain_tip_height(tx)? { + Some(h) => h, + None => { + return Ok(None); + } + }; + + let birthday_height = match wallet_birthday(tx)? { + Some(h) => h, + None => { + return Ok(None); + } + }; + + let recover_until_height = recover_until_height(tx)?; + + let fully_scanned_height = block_fully_scanned(tx, params)?.map(|m| m.block_height()); + let summary_height = (chain_tip_height + 1).saturating_sub(std::cmp::max(min_confirmations, 1)); + + let sapling_progress = progress.sapling_scan_progress( + tx, + params, + birthday_height, + recover_until_height, + fully_scanned_height, + chain_tip_height, )?; - MemoBytes::from_bytes(&memo_bytes) - .and_then(Memo::try_from) - .map_err(SqliteClientError::from) + #[cfg(feature = "orchard")] + let orchard_progress = progress.orchard_scan_progress( + tx, + params, + birthday_height, + recover_until_height, + fully_scanned_height, + chain_tip_height, + )?; + #[cfg(not(feature = "orchard"))] + let orchard_progress: Option = None; + + // Treat Sapling and Orchard outputs as having the same cost to scan. + let progress = sapling_progress + .as_ref() + .zip(orchard_progress.as_ref()) + .map(|(s, o)| { + Progress::new( + Ratio::new( + s.scan().numerator() + o.scan().numerator(), + s.scan().denominator() + o.scan().denominator(), + ), + s.recovery() + .zip(o.recovery()) + .map(|(s, o)| { + Ratio::new( + s.numerator() + o.numerator(), + s.denominator() + o.denominator(), + ) + }) + .or_else(|| s.recovery()) + .or_else(|| o.recovery()), + ) + }) + .or(sapling_progress) + .or(orchard_progress); + + let progress = match progress { + Some(p) => p, + None => return Ok(None), + }; + + let mut stmt_accounts = tx.prepare_cached("SELECT uuid FROM accounts")?; + let mut account_balances = stmt_accounts + .query([])? + .and_then(|row| { + Ok::<_, SqliteClientError>((AccountUuid(row.get::<_, Uuid>(0)?), AccountBalance::ZERO)) + }) + .collect::, _>>()?; + + fn count_notes( + tx: &rusqlite::Transaction, + summary_height: BlockHeight, + account_balances: &mut HashMap, + table_prefix: &'static str, + with_pool_balance: F, + ) -> Result<(), SqliteClientError> + where + F: Fn(&mut AccountBalance, Zatoshis, Zatoshis, Zatoshis) -> Result<(), SqliteClientError>, + { + // If the shard containing the summary height contains any unscanned ranges that start below or + // including that height, none of our shielded balance is currently spendable. + #[tracing::instrument(skip_all)] + fn is_any_spendable( + conn: &rusqlite::Connection, + summary_height: BlockHeight, + table_prefix: &'static str, + ) -> Result { + conn.query_row( + &format!( + "SELECT NOT EXISTS( + SELECT 1 FROM v_{table_prefix}_shard_unscanned_ranges + WHERE :summary_height + BETWEEN subtree_start_height + AND IFNULL(subtree_end_height, :summary_height) + AND block_range_start <= :summary_height + )" + ), + named_params![":summary_height": u32::from(summary_height)], + |row| row.get::<_, bool>(0), + ) + .map_err(|e| e.into()) + } + + let any_spendable = is_any_spendable(tx, summary_height, table_prefix)?; + let mut stmt_select_notes = tx.prepare_cached(&format!( + "SELECT a.uuid, n.value, n.is_change, scan_state.max_priority, t.block + FROM {table_prefix}_received_notes n + JOIN accounts a ON a.id = n.account_id + JOIN transactions t ON t.id_tx = n.tx + LEFT OUTER JOIN v_{table_prefix}_shards_scan_state scan_state + ON n.commitment_tree_position >= scan_state.start_position + AND n.commitment_tree_position < scan_state.end_position_exclusive + WHERE ( + t.block IS NOT NULL -- the receiving tx is mined + OR t.expiry_height IS NULL -- the receiving tx will not expire + OR t.expiry_height >= :summary_height -- the receiving tx is unexpired + ) + -- and the received note is unspent + AND n.id NOT IN ( + SELECT {table_prefix}_received_note_id + FROM {table_prefix}_received_note_spends + JOIN transactions t ON t.id_tx = transaction_id + WHERE t.block IS NOT NULL -- the spending transaction is mined + OR t.expiry_height IS NULL -- the spending tx will not expire + OR t.expiry_height > :summary_height -- the spending tx is unexpired + )" + ))?; + + let mut rows = + stmt_select_notes.query(named_params![":summary_height": u32::from(summary_height)])?; + while let Some(row) = rows.next()? { + let account = AccountUuid(row.get::<_, Uuid>(0)?); + + let value_raw = row.get::<_, i64>(1)?; + let value = Zatoshis::from_nonnegative_i64(value_raw).map_err(|_| { + SqliteClientError::CorruptedData(format!( + "Negative received note value: {}", + value_raw + )) + })?; + + let is_change = row.get::<_, bool>(2)?; + + // If `max_priority` is null, this means that the note is not positioned; the note + // will not be spendable, so we assign the scan priority to `ChainTip` as a priority + // that is greater than `Scanned` + let max_priority_raw = row.get::<_, Option>(3)?; + let max_priority = max_priority_raw.map_or_else( + || Ok(ScanPriority::ChainTip), + |raw| { + parse_priority_code(raw).ok_or_else(|| { + SqliteClientError::CorruptedData(format!( + "Priority code {} not recognized.", + raw + )) + }) + }, + )?; + + let received_height = row.get::<_, Option>(4)?.map(BlockHeight::from); + + let is_spendable = any_spendable + && received_height.iter().any(|h| h <= &summary_height) + && max_priority <= ScanPriority::Scanned; + + let is_pending_change = + is_change && received_height.iter().all(|h| h > &summary_height); + + let (spendable_value, change_pending_confirmation, value_pending_spendability) = { + let zero = Zatoshis::ZERO; + if is_spendable { + (value, zero, zero) + } else if is_pending_change { + (zero, value, zero) + } else { + (zero, zero, value) + } + }; + + if let Some(balances) = account_balances.get_mut(&account) { + with_pool_balance( + balances, + spendable_value, + change_pending_confirmation, + value_pending_spendability, + )?; + } + } + Ok(()) + } + + #[cfg(feature = "orchard")] + { + let orchard_trace = tracing::info_span!("orchard_balances").entered(); + count_notes( + tx, + summary_height, + &mut account_balances, + ORCHARD_TABLES_PREFIX, + |balances, spendable_value, change_pending_confirmation, value_pending_spendability| { + balances.with_orchard_balance_mut::<_, SqliteClientError>(|bal| { + bal.add_spendable_value(spendable_value)?; + bal.add_pending_change_value(change_pending_confirmation)?; + bal.add_pending_spendable_value(value_pending_spendability)?; + Ok(()) + }) + }, + )?; + drop(orchard_trace); + } + + let sapling_trace = tracing::info_span!("sapling_balances").entered(); + count_notes( + tx, + summary_height, + &mut account_balances, + SAPLING_TABLES_PREFIX, + |balances, spendable_value, change_pending_confirmation, value_pending_spendability| { + balances.with_sapling_balance_mut::<_, SqliteClientError>(|bal| { + bal.add_spendable_value(spendable_value)?; + bal.add_pending_change_value(change_pending_confirmation)?; + bal.add_pending_spendable_value(value_pending_spendability)?; + Ok(()) + }) + }, + )?; + drop(sapling_trace); + + #[cfg(feature = "transparent-inputs")] + transparent::add_transparent_account_balances( + tx, + chain_tip_height + 1, + min_confirmations, + &mut account_balances, + )?; + + // The approach used here for Sapling and Orchard subtree indexing was a quick hack + // that has not yet been replaced. TODO: Make less hacky. + // https://github.com/zcash/librustzcash/issues/1249 + let next_sapling_subtree_index = { + let shard_store = + SqliteShardStore::<_, ::sapling::Node, SAPLING_SHARD_HEIGHT>::from_connection( + tx, + SAPLING_TABLES_PREFIX, + )?; + + // The last shard will be incomplete, and we want the next range to overlap with + // the last complete shard, so return the index of the second-to-last shard root. + shard_store + .get_shard_roots() + .map_err(ShardTreeError::Storage)? + .iter() + .rev() + .nth(1) + .map(|addr| addr.index()) + .unwrap_or(0) + }; + + #[cfg(feature = "orchard")] + let next_orchard_subtree_index = { + let shard_store = SqliteShardStore::< + _, + ::orchard::tree::MerkleHashOrchard, + ORCHARD_SHARD_HEIGHT, + >::from_connection(tx, ORCHARD_TABLES_PREFIX)?; + + // The last shard will be incomplete, and we want the next range to overlap with + // the last complete shard, so return the index of the second-to-last shard root. + shard_store + .get_shard_roots() + .map_err(ShardTreeError::Storage)? + .iter() + .rev() + .nth(1) + .map(|addr| addr.index()) + .unwrap_or(0) + }; + + let summary = WalletSummary::new( + account_balances, + chain_tip_height, + fully_scanned_height.unwrap_or(birthday_height - 1), + progress, + next_sapling_subtree_index, + #[cfg(feature = "orchard")] + next_orchard_subtree_index, + ); + + Ok(Some(summary)) } -/// Returns the minimum and maximum heights for blocks stored in the wallet database. -/// -/// # Examples -/// -/// ``` -/// use tempfile::NamedTempFile; -/// use zcash_primitives::consensus::Network; -/// use zcash_client_sqlite::{ -/// WalletDb, -/// wallet::block_height_extrema, -/// }; +/// Returns the memo for a received note, if the note is known to the wallet. +pub(crate) fn get_received_memo( + conn: &rusqlite::Connection, + note_id: NoteId, +) -> Result, SqliteClientError> { + let fetch_memo = |table_prefix: &'static str, output_col: &'static str| { + conn.query_row( + &format!( + "SELECT memo FROM {table_prefix}_received_notes + JOIN transactions ON {table_prefix}_received_notes.tx = transactions.id_tx + WHERE transactions.txid = :txid + AND {table_prefix}_received_notes.{output_col} = :output_index" + ), + named_params![ + ":txid": note_id.txid().as_ref(), + ":output_index": note_id.output_index() + ], + |row| row.get(0), + ) + .optional() + }; + + let memo_bytes: Option> = match note_id.protocol() { + ShieldedProtocol::Sapling => fetch_memo(SAPLING_TABLES_PREFIX, "output_index")?.flatten(), + #[cfg(feature = "orchard")] + ShieldedProtocol::Orchard => fetch_memo(ORCHARD_TABLES_PREFIX, "action_index")?.flatten(), + #[cfg(not(feature = "orchard"))] + ShieldedProtocol::Orchard => { + return Err(SqliteClientError::UnsupportedPoolType(PoolType::ORCHARD)) + } + }; + + memo_bytes + .map(|b| { + MemoBytes::from_bytes(&b) + .and_then(Memo::try_from) + .map_err(SqliteClientError::from) + }) + .transpose() +} + +/// Looks up a transaction by its [`TxId`]. /// -/// let data_file = NamedTempFile::new().unwrap(); -/// let db = WalletDb::for_path(data_file, Network::TestNetwork).unwrap(); -/// let bounds = block_height_extrema(&db); -/// ``` -pub fn block_height_extrema

( - wdb: &WalletDb

, -) -> Result, rusqlite::Error> { - wdb.conn +/// Returns the decoded transaction, along with the block height that was used in its decoding. +/// This is either the block height at which the transaction was mined, or the expiry height if the +/// wallet created the transaction but the transaction has not yet been mined from the perspective +/// of the wallet. +pub(crate) fn get_transaction( + conn: &rusqlite::Connection, + params: &P, + txid: TxId, +) -> Result, SqliteClientError> { + conn.query_row( + "SELECT raw, block, expiry_height FROM transactions + WHERE txid = ?", + [txid.as_ref()], + |row| { + let h: Option = row.get(1)?; + let expiry: Option = row.get(2)?; + Ok(( + row.get::<_, Vec>(0)?, + h.map(BlockHeight::from), + expiry.map(BlockHeight::from), + )) + }, + ) + .optional()? + .map(|(tx_bytes, block_height, expiry_height)| { + // We need to provide a consensus branch ID so that pre-v5 `Transaction` structs + // (which don't commit directly to one) can store it internally. + // - If the transaction is mined, we use the block height to get the correct one. + // - If the transaction is unmined and has a cached non-zero expiry height, we use + // that (relying on the invariant that a transaction can't be mined across a network + // upgrade boundary, so the expiry height must be in the same epoch). + // - Otherwise, we use a placeholder for the initial transaction parse (as the + // consensus branch ID is not used there), and then either use its non-zero expiry + // height or return an error. + if let Some(height) = + block_height.or_else(|| expiry_height.filter(|h| h > &BlockHeight::from(0))) + { + Transaction::read(&tx_bytes[..], BranchId::for_height(params, height)) + .map(|t| (height, t)) + .map_err(SqliteClientError::from) + } else { + let tx_data = Transaction::read(&tx_bytes[..], BranchId::Sprout) + .map_err(SqliteClientError::from)? + .into_data(); + + let expiry_height = tx_data.expiry_height(); + if expiry_height > BlockHeight::from(0) { + TransactionData::from_parts( + tx_data.version(), + BranchId::for_height(params, expiry_height), + tx_data.lock_time(), + expiry_height, + tx_data.transparent_bundle().cloned(), + tx_data.sprout_bundle().cloned(), + tx_data.sapling_bundle().cloned(), + tx_data.orchard_bundle().cloned(), + ) + .freeze() + .map(|t| (expiry_height, t)) + .map_err(SqliteClientError::from) + } else { + Err(SqliteClientError::CorruptedData( + "Consensus branch ID not known, cannot parse this transaction until it is mined" + .to_string(), + )) + } + } + }) + .transpose() +} + +pub(crate) fn get_funding_accounts( + conn: &rusqlite::Connection, + tx: &Transaction, +) -> Result, rusqlite::Error> { + let mut funding_accounts = HashSet::new(); + #[cfg(feature = "transparent-inputs")] + funding_accounts.extend(transparent::detect_spending_accounts( + conn, + tx.transparent_bundle() + .iter() + .flat_map(|bundle| bundle.vin.iter().map(|txin| &txin.prevout)), + )?); + + funding_accounts.extend(sapling::detect_spending_accounts( + conn, + tx.sapling_bundle().iter().flat_map(|bundle| { + bundle + .shielded_spends() + .iter() + .map(|spend| spend.nullifier()) + }), + )?); + + #[cfg(feature = "orchard")] + funding_accounts.extend(orchard::detect_spending_accounts( + conn, + tx.orchard_bundle() + .iter() + .flat_map(|bundle| bundle.actions().iter().map(|action| action.nullifier())), + )?); + + Ok(funding_accounts) +} + +/// Returns the memo for a sent note, if the sent note is known to the wallet. +pub(crate) fn get_sent_memo( + conn: &rusqlite::Connection, + note_id: NoteId, +) -> Result, SqliteClientError> { + let memo_bytes: Option> = conn .query_row( - "SELECT MIN(height), MAX(height) FROM blocks", - NO_PARAMS, - |row| { - let min_height: u32 = row.get(0)?; - let max_height: u32 = row.get(1)?; - Ok(Some(( - BlockHeight::from(min_height), - BlockHeight::from(max_height), - ))) - }, + "SELECT memo FROM sent_notes + JOIN transactions ON sent_notes.tx = transactions.id_tx + WHERE transactions.txid = :txid + AND sent_notes.output_pool = :pool_code + AND sent_notes.output_index = :output_index", + named_params![ + ":txid": note_id.txid().as_ref(), + ":pool_code": pool_code(PoolType::Shielded(note_id.protocol())), + ":output_index": note_id.output_index() + ], + |row| row.get(0), ) - //.optional() doesn't work here because a failed aggregate function - //produces a runtime error, not an empty set of rows. - .or(Ok(None)) + .optional()? + .flatten(); + + memo_bytes + .map(|b| { + MemoBytes::from_bytes(&b) + .and_then(Memo::try_from) + .map_err(SqliteClientError::from) + }) + .transpose() +} + +/// Returns the minimum birthday height for accounts in the wallet. +// +// TODO ORCHARD: we should consider whether we want to permit protocol-restricted accounts; if so, +// we would then want this method to take a protocol identifier to be able to learn the wallet's +// "Orchard birthday" which might be different from the overall wallet birthday. +pub(crate) fn wallet_birthday( + conn: &rusqlite::Connection, +) -> Result, rusqlite::Error> { + conn.query_row( + "SELECT MIN(birthday_height) AS wallet_birthday FROM accounts", + [], + |row| { + row.get::<_, Option>(0) + .map(|opt| opt.map(BlockHeight::from)) + }, + ) +} + +pub(crate) fn account_birthday( + conn: &rusqlite::Connection, + account_uuid: AccountUuid, +) -> Result { + conn.query_row( + "SELECT birthday_height + FROM accounts + WHERE uuid = :account_uuid", + named_params![":account_uuid": account_uuid.0], + |row| row.get::<_, u32>(0).map(BlockHeight::from), + ) + .optional() + .map_err(SqliteClientError::from) + .and_then(|opt| opt.ok_or(SqliteClientError::AccountUnknown)) +} + +/// Returns the maximum recover-until height for accounts in the wallet. +pub(crate) fn recover_until_height( + conn: &rusqlite::Connection, +) -> Result, rusqlite::Error> { + conn.query_row( + "SELECT MAX(recover_until_height) FROM accounts", + [], + |row| { + row.get::<_, Option>(0) + .map(|opt| opt.map(BlockHeight::from)) + }, + ) +} + +/// Returns the minimum and maximum heights for blocks stored in the wallet database. +pub(crate) fn block_height_extrema( + conn: &rusqlite::Connection, +) -> Result>, rusqlite::Error> { + conn.query_row("SELECT MIN(height), MAX(height) FROM blocks", [], |row| { + let min_height: Option = row.get(0)?; + let max_height: Option = row.get(1)?; + Ok(min_height + .zip(max_height) + .map(|(min, max)| RangeInclusive::new(min.into(), max.into()))) + }) +} + +pub(crate) fn get_account_ref( + conn: &rusqlite::Connection, + account_uuid: AccountUuid, +) -> Result { + conn.query_row( + "SELECT id FROM accounts WHERE uuid = :account_uuid", + named_params! {":account_uuid": account_uuid.0}, + |row| row.get("id").map(AccountRef), + ) + .optional()? + .ok_or(SqliteClientError::AccountUnknown) +} + +#[cfg(feature = "transparent-inputs")] +pub(crate) fn get_account_uuid( + conn: &rusqlite::Connection, + account_id: AccountRef, +) -> Result { + conn.query_row( + "SELECT uuid FROM accounts WHERE id = :account_id", + named_params! {":account_id": account_id.0}, + |row| row.get("uuid").map(AccountUuid), + ) + .optional()? + .ok_or(SqliteClientError::AccountUnknown) +} + +/// Returns the minimum and maximum heights of blocks in the chain which may be scanned. +pub(crate) fn chain_tip_height( + conn: &rusqlite::Connection, +) -> Result, rusqlite::Error> { + conn.query_row("SELECT MAX(block_range_end) FROM scan_queue", [], |row| { + let max_height: Option = row.get(0)?; + + // Scan ranges are end-exclusive, so we subtract 1 from `max_height` to obtain the + // height of the last known chain tip; + Ok(max_height.map(|h| BlockHeight::from(h.saturating_sub(1)))) + }) +} + +pub(crate) fn get_target_and_anchor_heights( + conn: &rusqlite::Connection, + min_confirmations: NonZeroU32, +) -> Result, rusqlite::Error> { + match chain_tip_height(conn)? { + Some(chain_tip_height) => { + let sapling_anchor_height = get_max_checkpointed_height( + conn, + SAPLING_TABLES_PREFIX, + chain_tip_height, + min_confirmations, + )?; + + #[cfg(feature = "orchard")] + let orchard_anchor_height = get_max_checkpointed_height( + conn, + ORCHARD_TABLES_PREFIX, + chain_tip_height, + min_confirmations, + )?; + + #[cfg(not(feature = "orchard"))] + let orchard_anchor_height: Option = None; + + let anchor_height = sapling_anchor_height + .zip(orchard_anchor_height) + .map(|(s, o)| std::cmp::min(s, o)) + .or(sapling_anchor_height) + .or(orchard_anchor_height); + + Ok(anchor_height.map(|h| (chain_tip_height + 1, h))) + } + None => Ok(None), + } +} + +fn parse_block_metadata( + _params: &P, + row: (BlockHeight, Vec, Option, Vec, Option), +) -> Result { + let (block_height, hash_data, sapling_tree_size_opt, sapling_tree, _orchard_tree_size_opt) = + row; + let sapling_tree_size = sapling_tree_size_opt.map_or_else(|| { + if sapling_tree == BLOCK_SAPLING_FRONTIER_ABSENT { + Err(SqliteClientError::CorruptedData("One of either the Sapling tree size or the legacy Sapling commitment tree must be present.".to_owned())) + } else { + // parse the legacy commitment tree data + read_commitment_tree::< + ::sapling::Node, + _, + { ::sapling::NOTE_COMMITMENT_TREE_DEPTH }, + >(Cursor::new(sapling_tree)) + .map(|tree| tree.size().try_into().unwrap()) + .map_err(SqliteClientError::from) + } + }, Ok)?; + + let block_hash = BlockHash::try_from_slice(&hash_data).ok_or_else(|| { + SqliteClientError::from(io::Error::new( + io::ErrorKind::InvalidData, + format!("Invalid block hash length: {}", hash_data.len()), + )) + })?; + + Ok(BlockMetadata::from_parts( + block_height, + block_hash, + Some(sapling_tree_size), + #[cfg(feature = "orchard")] + if _params + .activation_height(NetworkUpgrade::Nu5) + .iter() + .any(|nu5_activation| &block_height >= nu5_activation) + { + _orchard_tree_size_opt + } else { + Some(0) + }, + )) +} + +#[tracing::instrument(skip(conn, params))] +pub(crate) fn block_metadata( + conn: &rusqlite::Connection, + params: &P, + block_height: BlockHeight, +) -> Result, SqliteClientError> { + conn.query_row( + "SELECT height, hash, sapling_commitment_tree_size, sapling_tree, orchard_commitment_tree_size + FROM blocks + WHERE height = :block_height", + named_params![":block_height": u32::from(block_height)], + |row| { + let height: u32 = row.get(0)?; + let block_hash: Vec = row.get(1)?; + let sapling_tree_size: Option = row.get(2)?; + let sapling_tree: Vec = row.get(3)?; + let orchard_tree_size: Option = row.get(4)?; + Ok(( + BlockHeight::from(height), + block_hash, + sapling_tree_size, + sapling_tree, + orchard_tree_size, + )) + }, + ) + .optional() + .map_err(SqliteClientError::from) + .and_then(|meta_row| meta_row.map(|r| parse_block_metadata(params, r)).transpose()) +} + +#[tracing::instrument(skip_all)] +pub(crate) fn block_fully_scanned( + conn: &rusqlite::Connection, + params: &P, +) -> Result, SqliteClientError> { + if let Some(birthday_height) = wallet_birthday(conn)? { + // We assume that the only way we get a contiguous range of block heights in the `blocks` table + // starting with the birthday block, is if all scanning operations have been performed on those + // blocks. This holds because the `blocks` table is only altered by `WalletDb::put_blocks` via + // `put_block`, and the effective combination of intra-range linear scanning and the nullifier + // map ensures that we discover all wallet-related information within the contiguous range. + // + // We also assume that every contiguous range of block heights in the `blocks` table has a + // single matching entry in the `scan_queue` table with priority "Scanned". This requires no + // bugs in the scan queue update logic, which we have had before. However, a bug here would + // mean that we return a more conservative fully-scanned height, which likely just causes a + // performance regression. + // + // The fully-scanned height is therefore the last height that falls within the first range in + // the scan queue with priority "Scanned". + let calc_fully_scanned_height = |row: &rusqlite::Row| { + let block_range_start = BlockHeight::from_u32(row.get(0)?); + let block_range_end = BlockHeight::from_u32(row.get(1)?); + + // If the start of the earliest scanned range is greater than + // the birthday height, then there is an unscanned range between + // the wallet birthday and that range, so there is no fully + // scanned height. + Ok(if block_range_start <= birthday_height { + // Scan ranges are end-exclusive. + Some(block_range_end - 1) + } else { + None + }) + }; + let fully_scanned_height = match conn + .query_row( + "SELECT block_range_start, block_range_end + FROM scan_queue + WHERE priority = :priority + ORDER BY block_range_start ASC + LIMIT 1", + named_params![":priority": priority_code(&ScanPriority::Scanned)], + calc_fully_scanned_height, + ) + .optional()? + { + Some(Some(h)) => h, + _ => return Ok(None), + }; + + block_metadata(conn, params, fully_scanned_height) + } else { + Ok(None) + } +} + +pub(crate) fn block_max_scanned( + conn: &rusqlite::Connection, + params: &P, +) -> Result, SqliteClientError> { + conn.query_row( + "SELECT blocks.height, hash, sapling_commitment_tree_size, sapling_tree, orchard_commitment_tree_size + FROM blocks + JOIN (SELECT MAX(height) AS height FROM blocks) blocks_max + ON blocks.height = blocks_max.height", + [], + |row| { + let height: u32 = row.get(0)?; + let block_hash: Vec = row.get(1)?; + let sapling_tree_size: Option = row.get(2)?; + let sapling_tree: Vec = row.get(3)?; + let orchard_tree_size: Option = row.get(4)?; + Ok(( + BlockHeight::from(height), + block_hash, + sapling_tree_size, + sapling_tree, + orchard_tree_size + )) + }, + ) + .optional() + .map_err(SqliteClientError::from) + .and_then(|meta_row| meta_row.map(|r| parse_block_metadata(params, r)).transpose()) } /// Returns the block height at which the specified transaction was mined, /// if any. -/// -/// # Examples -/// -/// ``` -/// use tempfile::NamedTempFile; -/// use zcash_primitives::consensus::Network; -/// use zcash_primitives::transaction::TxId; -/// use zcash_client_sqlite::{ -/// WalletDb, -/// wallet::get_tx_height, -/// }; -/// -/// let data_file = NamedTempFile::new().unwrap(); -/// let db = WalletDb::for_path(data_file, Network::TestNetwork).unwrap(); -/// let height = get_tx_height(&db, TxId([0u8; 32])); -/// ``` -pub fn get_tx_height

( - wdb: &WalletDb

, +pub(crate) fn get_tx_height( + conn: &rusqlite::Connection, txid: TxId, ) -> Result, rusqlite::Error> { - wdb.conn - .query_row( - "SELECT block FROM transactions WHERE txid = ?", - &[txid.0.to_vec()], - |row| row.get(0).map(u32::into), - ) - .optional() + conn.query_row( + "SELECT block FROM transactions WHERE txid = ?", + [txid.as_ref()], + |row| Ok(row.get::<_, Option>(0)?.map(BlockHeight::from)), + ) + .optional() + .map(|opt| opt.flatten()) } /// Returns the block hash for the block at the specified height, /// if any. -/// -/// # Examples -/// -/// ``` -/// use tempfile::NamedTempFile; -/// use zcash_primitives::consensus::{H0, Network}; -/// use zcash_client_sqlite::{ -/// WalletDb, -/// wallet::get_block_hash, -/// }; -/// -/// let data_file = NamedTempFile::new().unwrap(); -/// let db = WalletDb::for_path(data_file, Network::TestNetwork).unwrap(); -/// let hash = get_block_hash(&db, H0); -/// ``` -pub fn get_block_hash

( - wdb: &WalletDb

, +pub(crate) fn get_block_hash( + conn: &rusqlite::Connection, block_height: BlockHeight, ) -> Result, rusqlite::Error> { - wdb.conn - .query_row( - "SELECT hash FROM blocks WHERE height = ?", - &[u32::from(block_height)], - |row| { - let row_data = row.get::<_, Vec<_>>(0)?; - Ok(BlockHash::from_slice(&row_data)) - }, - ) - .optional() + conn.query_row( + "SELECT hash FROM blocks WHERE height = ?", + [u32::from(block_height)], + |row| { + let row_data = row.get::<_, Vec<_>>(0)?; + Ok(BlockHash::from_slice(&row_data)) + }, + ) + .optional() } -/// Rewinds the database to the given height. -/// -/// If the requested height is greater than or equal to the height of the last scanned -/// block, this function does nothing. -/// -/// This should only be executed inside a transactional context. -pub fn rewind_to_height( - wdb: &WalletDb

, - block_height: BlockHeight, +pub(crate) fn get_max_height_hash( + conn: &rusqlite::Connection, +) -> Result, rusqlite::Error> { + conn.query_row( + "SELECT height, hash FROM blocks ORDER BY height DESC LIMIT 1", + [], + |row| { + let height = row.get::<_, u32>(0).map(BlockHeight::from)?; + let row_data = row.get::<_, Vec<_>>(1)?; + Ok((height, BlockHash::from_slice(&row_data))) + }, + ) + .optional() +} + +pub(crate) fn store_transaction_to_be_sent( + wdb: &mut WalletDb, P>, + sent_tx: &SentTransaction, ) -> Result<(), SqliteClientError> { - let sapling_activation_height = wdb - .params - .activation_height(NetworkUpgrade::Sapling) - .ok_or(SqliteClientError::BackendError(Error::SaplingNotActive))?; - - // Recall where we synced up to previously. - let last_scanned_height = - wdb.conn - .query_row("SELECT MAX(height) FROM blocks", NO_PARAMS, |row| { - row.get(0) - .map(|h: u32| h.into()) - .or(Ok(sapling_activation_height - 1)) - })?; + let tx_ref = put_tx_data( + wdb.conn.0, + sent_tx.tx(), + Some(sent_tx.fee_amount()), + Some(sent_tx.created()), + Some(sent_tx.target_height()), + )?; - // nothing to do if we're deleting back down to the max height - if block_height >= last_scanned_height { - Ok(()) - } else { - // Decrement witnesses. - wdb.conn.execute( - "DELETE FROM sapling_witnesses WHERE block > ?", - &[u32::from(block_height)], - )?; + let mut detectable_via_scanning = false; + + // Mark notes as spent. + // + // This locks the notes so they aren't selected again by a subsequent call to + // create_spend_to_address() before this transaction has been mined (at which point the notes + // get re-marked as spent). + // + // Assumes that create_spend_to_address() will never be called in parallel, which is a + // reasonable assumption for a light client such as a mobile phone. + if let Some(bundle) = sent_tx.tx().sapling_bundle() { + detectable_via_scanning = true; + for spend in bundle.shielded_spends() { + sapling::mark_sapling_note_spent(wdb.conn.0, tx_ref, spend.nullifier())?; + } + } + if let Some(_bundle) = sent_tx.tx().orchard_bundle() { + #[cfg(feature = "orchard")] + { + detectable_via_scanning = true; + for action in _bundle.actions() { + orchard::mark_orchard_note_spent(wdb.conn.0, tx_ref, action.nullifier())?; + } + } + + #[cfg(not(feature = "orchard"))] + panic!("Sent a transaction with Orchard Actions without `orchard` enabled?"); + } - // Un-mine transactions. - wdb.conn.execute( - "UPDATE transactions SET block = NULL, tx_index = NULL WHERE block > ?", - &[u32::from(block_height)], - )?; + #[cfg(feature = "transparent-inputs")] + for utxo_outpoint in sent_tx.utxos_spent() { + transparent::mark_transparent_utxo_spent(wdb.conn.0, tx_ref, utxo_outpoint)?; + } - // Now that they aren't depended on, delete scanned blocks. - wdb.conn.execute( - "DELETE FROM blocks WHERE height > ?", - &[u32::from(block_height)], + for output in sent_tx.outputs() { + insert_sent_output( + wdb.conn.0, + &wdb.params, + tx_ref, + *sent_tx.account_id(), + output, )?; - Ok(()) + match output.recipient() { + Recipient::InternalAccount { + receiving_account, + note, + .. + } => match note.as_ref() { + Note::Sapling(note) => { + sapling::put_received_note( + wdb.conn.0, + &DecryptedOutput::new( + output.output_index(), + note.clone(), + *receiving_account, + output + .memo() + .map_or_else(MemoBytes::empty, |memo| memo.clone()), + TransferType::WalletInternal, + ), + tx_ref, + None, + )?; + } + #[cfg(feature = "orchard")] + Note::Orchard(note) => { + orchard::put_received_note( + wdb.conn.0, + &DecryptedOutput::new( + output.output_index(), + *note, + *receiving_account, + output + .memo() + .map_or_else(MemoBytes::empty, |memo| memo.clone()), + TransferType::WalletInternal, + ), + tx_ref, + None, + )?; + } + }, + #[cfg(feature = "transparent-inputs")] + Recipient::EphemeralTransparent { + receiving_account, + ephemeral_address, + outpoint, + } => { + transparent::put_transparent_output( + wdb.conn.0, + &wdb.params, + outpoint, + &TxOut { + value: output.value(), + script_pubkey: ephemeral_address.script(), + }, + None, + ephemeral_address, + *receiving_account, + true, + )?; + transparent::ephemeral::mark_ephemeral_address_as_used( + wdb.conn.0, + &wdb.params, + ephemeral_address, + tx_ref, + )?; + } + _ => {} + } + } + + // Add the transaction to the set to be queried for transaction status. This is only necessary + // at present for fully transparent transactions, because any transaction with a shielded + // component will be detected via ordinary chain scanning and/or nullifier checking. + if !detectable_via_scanning { + queue_tx_retrieval(wdb.conn.0, std::iter::once(sent_tx.tx().txid()), None)?; } + + Ok(()) } -/// Returns the commitment tree for the block at the specified height, -/// if any. +pub(crate) fn set_transaction_status( + conn: &rusqlite::Transaction, + txid: TxId, + status: TransactionStatus, +) -> Result<(), SqliteClientError> { + // It is safe to unconditionally delete the request from `tx_retrieval_queue` below (both in + // the expired case and the case where it has been mined), because we already have all the data + // we need about this transaction: + // * if the status is being set in response to a `GetStatus` request, we know that we already + // have the transaction data (`GetStatus` requests are only generated if we already have that + // data) + // * if it is being set in response to an `Enhancement` request, we know that the status must + // be `TxidNotRecognized` because otherwise the transaction data should have been provided to + // the backend directly instead of calling `set_transaction_status` + // + // In general `Enhancement` requests are only generated in response to situations where a + // transaction has already been mined - either the transaction was detected by scanning the + // chain of `CompactBlock` values, or was discovered by walking backward from the inputs of a + // transparent transaction; in the case that a transaction was read from the mempool, complete + // transaction data will have been available and the only question that we are concerned with + // is whether that transaction ends up being mined or expires. + match status { + TransactionStatus::TxidNotRecognized | TransactionStatus::NotInMainChain => { + // If the transaction is now expired, remove it from the retrieval queue. + if let Some(chain_tip) = chain_tip_height(conn)? { + conn.execute( + "DELETE FROM tx_retrieval_queue WHERE txid IN ( + SELECT txid FROM transactions + WHERE txid = :txid AND expiry_height < :chain_tip_minus_lookahead + )", + named_params![ + ":txid": txid.as_ref(), + ":chain_tip_minus_lookahead": u32::from(chain_tip).saturating_sub(VERIFY_LOOKAHEAD) + ], + )?; + } + } + TransactionStatus::Mined(height) => { + // The transaction has been mined, so we can set its mined height, associate it with + // the appropriate block, and remove it from the retrieval queue. + let sql_args = named_params![ + ":txid": txid.as_ref(), + ":height": u32::from(height) + ]; + + conn.execute( + "UPDATE transactions + SET mined_height = :height + WHERE txid = :txid", + sql_args, + )?; + + conn.execute( + "UPDATE transactions + SET block = blocks.height + FROM blocks + WHERE txid = :txid + AND blocks.height = :height", + sql_args, + )?; + + notify_tx_retrieved(conn, txid)?; + } + } + + Ok(()) +} + +/// Truncates the database to at most the given height. /// -/// # Examples +/// If the requested height is greater than or equal to the height of the last scanned +/// block, this function does nothing. /// -/// ``` -/// use tempfile::NamedTempFile; -/// use zcash_primitives::consensus::{Network, H0}; -/// use zcash_client_sqlite::{ -/// WalletDb, -/// wallet::get_commitment_tree, -/// }; +/// This should only be executed inside a transactional context. /// -/// let data_file = NamedTempFile::new().unwrap(); -/// let db = WalletDb::for_path(data_file, Network::TestNetwork).unwrap(); -/// let tree = get_commitment_tree(&db, H0); -/// ``` -pub fn get_commitment_tree

( - wdb: &WalletDb

, - block_height: BlockHeight, -) -> Result>, SqliteClientError> { - wdb.conn - .query_row_and_then( - "SELECT sapling_tree FROM blocks WHERE height = ?", - &[u32::from(block_height)], - |row| { - let row_data: Vec = row.get(0)?; - CommitmentTree::read(&row_data[..]).map_err(|e| { - rusqlite::Error::FromSqlConversionFailure( - row_data.len(), - rusqlite::types::Type::Blob, - Box::new(e), - ) +/// Returns the block height to which the database was truncated. +pub(crate) fn truncate_to_height( + conn: &rusqlite::Transaction, + params: &P, + max_height: BlockHeight, +) -> Result { + // Determine a checkpoint to which we can rewind, if any. + #[cfg(not(feature = "orchard"))] + let truncation_height_query = r#" + SELECT MAX(height) FROM blocks + JOIN sapling_tree_checkpoints ON checkpoint_id = blocks.height + WHERE blocks.height <= :block_height + "#; + + #[cfg(feature = "orchard")] + let truncation_height_query = r#" + SELECT MAX(height) FROM blocks + JOIN sapling_tree_checkpoints sc ON sc.checkpoint_id = blocks.height + JOIN orchard_tree_checkpoints oc ON oc.checkpoint_id = blocks.height + WHERE blocks.height <= :block_height + "#; + + let truncation_height = conn + .query_row( + truncation_height_query, + named_params! {":block_height": u32::from(max_height)}, + |row| row.get::<_, Option>(0), + ) + .optional()? + .flatten() + .map_or_else( + || { + // If we don't have a checkpoint at a height less than or equal to the requested + // truncation height, query for the minimum height to which it's possible for us to + // truncate so that we can report it to the caller. + #[cfg(not(feature = "orchard"))] + let min_checkpoint_height_query = + "SELECT MIN(checkpoint_id) FROM sapling_tree_checkpoints"; + #[cfg(feature = "orchard")] + let min_checkpoint_height_query = "SELECT MIN(sc.checkpoint_id) + FROM sapling_tree_checkpoints sc + JOIN orchard_tree_checkpoints oc + ON oc.checkpoint_id = sc.checkpoint_id"; + + let min_truncation_height = conn + .query_row(min_checkpoint_height_query, [], |row| { + row.get::<_, Option>(0) + }) + .optional()? + .flatten() + .map(BlockHeight::from); + + Err(SqliteClientError::RequestedRewindInvalid { + safe_rewind_height: min_truncation_height, + requested_height: max_height, }) }, - ) - .optional() - .map_err(SqliteClientError::from) -} + |h| Ok(BlockHeight::from(h)), + )?; -/// Returns the incremental witnesses for the block at the specified height, -/// if any. -/// -/// # Examples -/// -/// ``` -/// use tempfile::NamedTempFile; -/// use zcash_primitives::consensus::{Network, H0}; -/// use zcash_client_sqlite::{ -/// WalletDb, -/// wallet::get_witnesses, -/// }; -/// -/// let data_file = NamedTempFile::new().unwrap(); -/// let db = WalletDb::for_path(data_file, Network::TestNetwork).unwrap(); -/// let witnesses = get_witnesses(&db, H0); -/// ``` -pub fn get_witnesses

( - wdb: &WalletDb

, - block_height: BlockHeight, -) -> Result)>, SqliteClientError> { - let mut stmt_fetch_witnesses = wdb - .conn - .prepare("SELECT note, witness FROM sapling_witnesses WHERE block = ?")?; - let witnesses = stmt_fetch_witnesses - .query_map(&[u32::from(block_height)], |row| { - let id_note = NoteId::ReceivedNoteId(row.get(0)?); - let wdb: Vec = row.get(1)?; - Ok(IncrementalWitness::read(&wdb[..]).map(|witness| (id_note, witness))) - }) - .map_err(SqliteClientError::from)?; + let last_scanned_height = conn.query_row("SELECT MAX(height) FROM blocks", [], |row| { + let h = row.get::<_, Option>(0)?; - // unwrap database error & IO error from IncrementalWitness::read - let res: Vec<_> = witnesses.collect::, _>>()??; - Ok(res) -} + Ok(h.map_or_else( + || { + params + .activation_height(NetworkUpgrade::Sapling) + .expect("Sapling activation height must be available.") + - 1 + }, + BlockHeight::from, + )) + })?; -/// Retrieve the nullifiers for notes that the wallet is tracking -/// that have not yet been confirmed as a consequence of the spending -/// transaction being included in a block. -pub fn get_nullifiers

( - wdb: &WalletDb

, -) -> Result, SqliteClientError> { - // Get the nullifiers for the notes we are tracking - let mut stmt_fetch_nullifiers = wdb.conn.prepare( - "SELECT rn.id_note, rn.account, rn.nf, tx.block as block - FROM received_notes rn - LEFT OUTER JOIN transactions tx - ON tx.id_tx = rn.spent - WHERE block IS NULL", + // Delete from the scanning queue any range with a start height greater than the + // truncation height, and then truncate any remaining range by setting the end + // equal to the truncation height + 1. This sets our view of the chain tip back + // to the retained height. + conn.execute( + "DELETE FROM scan_queue + WHERE block_range_start >= :new_end_height", + named_params![":new_end_height": u32::from(truncation_height + 1)], + )?; + conn.execute( + "UPDATE scan_queue + SET block_range_end = :new_end_height + WHERE block_range_end > :new_end_height", + named_params![":new_end_height": u32::from(truncation_height + 1)], )?; - let nullifiers = stmt_fetch_nullifiers.query_map(NO_PARAMS, |row| { - let account = AccountId(row.get(1)?); - let nf_bytes: Vec = row.get(2)?; - Ok((account, Nullifier::from_slice(&nf_bytes).unwrap())) - })?; - let res: Vec<_> = nullifiers.collect::>()?; - Ok(res) + // Mark transparent utxos as un-mined. Since the TXO is now not mined, it would ideally be + // considered to have been returned to the mempool; it _might_ be spendable in this state, but + // we must also set its max_observed_unspent_height field to NULL because the transaction may + // be rendered entirely invalid by a reorg that alters anchor(s) used in constructing shielded + // spends in the transaction. + conn.execute( + "UPDATE transparent_received_outputs + SET max_observed_unspent_height = CASE WHEN tx.mined_height <= :height THEN :height ELSE NULL END + FROM transactions tx + WHERE tx.id_tx = transaction_id + AND max_observed_unspent_height > :height", + named_params![":height": u32::from(truncation_height)], + )?; + + // Un-mine transactions. This must be done outside of the last_scanned_height check because + // transaction entries may be created as a consequence of receiving transparent TXOs. + conn.execute( + "UPDATE transactions + SET block = NULL, mined_height = NULL, tx_index = NULL + WHERE mined_height > :height", + named_params![":height": u32::from(truncation_height)], + )?; + + // If we're removing scanned blocks, we need to truncate the note commitment tree and remove + // affected block records from the database. + if truncation_height < last_scanned_height { + // Truncate the note commitment trees + let mut wdb = WalletDb { + conn: SqlTransaction(conn), + params: params.clone(), + }; + wdb.with_sapling_tree_mut(|tree| { + tree.truncate_to_checkpoint(&truncation_height)?; + Ok::<_, SqliteClientError>(()) + })?; + #[cfg(feature = "orchard")] + wdb.with_orchard_tree_mut(|tree| { + tree.truncate_to_checkpoint(&truncation_height)?; + Ok::<_, SqliteClientError>(()) + })?; + + // Do not delete sent notes; this can contain data that is not recoverable + // from the chain. Wallets must continue to operate correctly in the + // presence of stale sent notes that link to unmined transactions. + // Also, do not delete received notes; they may contain memo data that is + // not recoverable; balance APIs must ensure that un-mined received notes + // do not count towards spendability or transaction balalnce. + + // Now that they aren't depended on, delete un-mined blocks. + conn.execute( + "DELETE FROM blocks WHERE height > ?", + [u32::from(truncation_height)], + )?; + + // Delete from the nullifier map any entries with a locator referencing a block + // height greater than the truncation height. + conn.execute( + "DELETE FROM tx_locator_map + WHERE block_height > :block_height", + named_params![":block_height": u32::from(truncation_height)], + )?; + } + + Ok(truncation_height) +} + +/// Returns a vector with the IDs of all accounts known to this wallet. +/// +/// Note that this is called from db migration code. +pub(crate) fn get_account_ids( + conn: &rusqlite::Connection, +) -> Result, rusqlite::Error> { + let mut stmt = conn.prepare("SELECT uuid FROM accounts")?; + let mut rows = stmt.query([])?; + let mut result = Vec::new(); + while let Some(row) = rows.next()? { + let id = AccountUuid(row.get(0)?); + result.push(id); + } + Ok(result) } /// Inserts information about a scanned block into the database. -pub fn insert_block<'a, P>( - stmts: &mut DataConnStmtCache<'a, P>, +#[allow(clippy::too_many_arguments)] +pub(crate) fn put_block( + conn: &rusqlite::Transaction<'_>, block_height: BlockHeight, block_hash: BlockHash, block_time: u32, - commitment_tree: &CommitmentTree, + sapling_commitment_tree_size: u32, + sapling_output_count: u32, + #[cfg(feature = "orchard")] orchard_commitment_tree_size: u32, + #[cfg(feature = "orchard")] orchard_action_count: u32, ) -> Result<(), SqliteClientError> { - let mut encoded_tree = Vec::new(); - commitment_tree.write(&mut encoded_tree).unwrap(); - - stmts.stmt_insert_block.execute(params![ - u32::from(block_height), - &block_hash.0[..], - block_time, - encoded_tree + let block_hash_data = conn + .query_row( + "SELECT hash FROM blocks WHERE height = ?", + [u32::from(block_height)], + |row| row.get::<_, Vec>(0), + ) + .optional()?; + + // Ensure that in the case of an upsert, we don't overwrite block data + // with information for a block with a different hash. + if let Some(bytes) = block_hash_data { + let expected_hash = BlockHash::try_from_slice(&bytes).ok_or_else(|| { + SqliteClientError::CorruptedData(format!( + "Invalid block hash at height {}", + u32::from(block_height) + )) + })?; + if expected_hash != block_hash { + return Err(SqliteClientError::BlockConflict(block_height)); + } + } + + let mut stmt_upsert_block = conn.prepare_cached( + "INSERT INTO blocks ( + height, + hash, + time, + sapling_commitment_tree_size, + sapling_output_count, + sapling_tree, + orchard_commitment_tree_size, + orchard_action_count + ) + VALUES ( + :height, + :hash, + :block_time, + :sapling_commitment_tree_size, + :sapling_output_count, + x'00', + :orchard_commitment_tree_size, + :orchard_action_count + ) + ON CONFLICT (height) DO UPDATE + SET hash = :hash, + time = :block_time, + sapling_commitment_tree_size = :sapling_commitment_tree_size, + sapling_output_count = :sapling_output_count, + orchard_commitment_tree_size = :orchard_commitment_tree_size, + orchard_action_count = :orchard_action_count", + )?; + + #[cfg(not(feature = "orchard"))] + let orchard_commitment_tree_size: Option = None; + #[cfg(not(feature = "orchard"))] + let orchard_action_count: Option = None; + + stmt_upsert_block.execute(named_params![ + ":height": u32::from(block_height), + ":hash": &block_hash.0[..], + ":block_time": block_time, + ":sapling_commitment_tree_size": sapling_commitment_tree_size, + ":sapling_output_count": sapling_output_count, + ":orchard_commitment_tree_size": orchard_commitment_tree_size, + ":orchard_action_count": orchard_action_count, ])?; + // If we now have a block corresponding to a received transparent output that had not been + // scanned at the time the UTXO was discovered, update the associated transaction record to + // refer to that block. + // + // NOTE: There's a small data corruption hazard here, in that we're relying exclusively upon + // the block height to associate the transaction to the block. This is because CompactBlock + // values only contain CompactTx entries for transactions that contain shielded inputs or + // outputs, and the GetAddressUtxosReply data does not contain the block hash. As such, it's + // necessary to ensure that any chain rollback to below the received height causes that height + // to be set to NULL. + let mut stmt_update_transaction_block_reference = conn.prepare_cached( + "UPDATE transactions + SET block = :height + WHERE mined_height = :height", + )?; + + stmt_update_transaction_block_reference + .execute(named_params![":height": u32::from(block_height),])?; + + Ok(()) +} + +pub(crate) fn store_decrypted_tx( + conn: &rusqlite::Transaction, + params: &P, + d_tx: DecryptedTransaction, +) -> Result<(), SqliteClientError> { + let tx_ref = put_tx_data(conn, d_tx.tx(), None, None, None)?; + if let Some(height) = d_tx.mined_height() { + set_transaction_status(conn, d_tx.tx().txid(), TransactionStatus::Mined(height))?; + } + + let funding_accounts = get_funding_accounts(conn, d_tx.tx())?; + + // TODO(#1305): Correctly track accounts that fund each transaction output. + let funding_account = funding_accounts.iter().next().copied(); + if funding_accounts.len() > 1 { + warn!( + "More than one wallet account detected as funding transaction {:?}, selecting {:?}", + d_tx.tx().txid(), + funding_account.unwrap() + ) + } + + // A flag used to determine whether it is necessary to query for transactions that + // provided transparent inputs to this transaction, in order to be able to correctly + // recover transparent transaction history. + #[cfg(feature = "transparent-inputs")] + let mut tx_has_wallet_outputs = false; + + for output in d_tx.sapling_outputs() { + #[cfg(feature = "transparent-inputs")] + { + tx_has_wallet_outputs = true; + } + match output.transfer_type() { + TransferType::Outgoing => { + let recipient = { + let receiver = Receiver::Sapling(output.note().recipient()); + let recipient_address = + select_receiving_address(params, conn, *output.account(), &receiver)? + .unwrap_or_else(|| receiver.to_zcash_address(params.network_type())); + + Recipient::External { + recipient_address, + output_pool: PoolType::SAPLING, + } + }; + + put_sent_output( + conn, + params, + *output.account(), + tx_ref, + output.index(), + &recipient, + output.note_value(), + Some(output.memo()), + )?; + } + TransferType::WalletInternal => { + sapling::put_received_note(conn, output, tx_ref, None)?; + + let recipient = Recipient::InternalAccount { + receiving_account: *output.account(), + external_address: None, + note: Box::new(Note::Sapling(output.note().clone())), + }; + + put_sent_output( + conn, + params, + *output.account(), + tx_ref, + output.index(), + &recipient, + output.note_value(), + Some(output.memo()), + )?; + } + TransferType::Incoming => { + sapling::put_received_note(conn, output, tx_ref, None)?; + + if let Some(account_id) = funding_account { + let recipient = Recipient::InternalAccount { + receiving_account: *output.account(), + external_address: { + let receiver = Receiver::Sapling(output.note().recipient()); + Some( + select_receiving_address( + params, + conn, + *output.account(), + &receiver, + )? + .unwrap_or_else(|| { + receiver.to_zcash_address(params.network_type()) + }), + ) + }, + note: Box::new(Note::Sapling(output.note().clone())), + }; + + put_sent_output( + conn, + params, + account_id, + tx_ref, + output.index(), + &recipient, + output.note_value(), + Some(output.memo()), + )?; + } + } + } + } + + #[cfg(feature = "orchard")] + for output in d_tx.orchard_outputs() { + #[cfg(feature = "transparent-inputs")] + { + tx_has_wallet_outputs = true; + } + match output.transfer_type() { + TransferType::Outgoing => { + let recipient = { + let receiver = Receiver::Orchard(output.note().recipient()); + let recipient_address = + select_receiving_address(params, conn, *output.account(), &receiver)? + .unwrap_or_else(|| receiver.to_zcash_address(params.network_type())); + + Recipient::External { + recipient_address, + output_pool: PoolType::ORCHARD, + } + }; + + put_sent_output( + conn, + params, + *output.account(), + tx_ref, + output.index(), + &recipient, + output.note_value(), + Some(output.memo()), + )?; + } + TransferType::WalletInternal => { + orchard::put_received_note(conn, output, tx_ref, None)?; + + let recipient = Recipient::InternalAccount { + receiving_account: *output.account(), + external_address: None, + note: Box::new(Note::Orchard(*output.note())), + }; + + put_sent_output( + conn, + params, + *output.account(), + tx_ref, + output.index(), + &recipient, + output.note_value(), + Some(output.memo()), + )?; + } + TransferType::Incoming => { + orchard::put_received_note(conn, output, tx_ref, None)?; + + if let Some(account_id) = funding_account { + // Even if the recipient address is external, record the send as internal. + let recipient = Recipient::InternalAccount { + receiving_account: *output.account(), + external_address: { + let receiver = Receiver::Orchard(output.note().recipient()); + Some( + select_receiving_address( + params, + conn, + *output.account(), + &receiver, + )? + .unwrap_or_else(|| { + receiver.to_zcash_address(params.network_type()) + }), + ) + }, + note: Box::new(Note::Orchard(*output.note())), + }; + + put_sent_output( + conn, + params, + account_id, + tx_ref, + output.index(), + &recipient, + output.note_value(), + Some(output.memo()), + )?; + } + } + } + } + + // If any of the utxos spent in the transaction are ours, mark them as spent. + #[cfg(feature = "transparent-inputs")] + for txin in d_tx + .tx() + .transparent_bundle() + .iter() + .flat_map(|b| b.vin.iter()) + { + transparent::mark_transparent_utxo_spent(conn, tx_ref, &txin.prevout)?; + } + + // This `if` is just an optimization for cases where we would do nothing in the loop. + if funding_account.is_some() || cfg!(feature = "transparent-inputs") { + for (output_index, txout) in d_tx + .tx() + .transparent_bundle() + .iter() + .flat_map(|b| b.vout.iter()) + .enumerate() + { + if let Some(address) = txout.recipient_address() { + debug!( + "{:?} output {} has recipient {}", + d_tx.tx().txid(), + output_index, + address.encode(params) + ); + + // The transaction is not necessarily mined yet, but we want to record + // that an output to the address was seen in this tx anyway. This will + // advance the gap regardless of whether it is mined, but an output in + // an unmined transaction won't advance the range of safe indices. + #[cfg(feature = "transparent-inputs")] + transparent::ephemeral::mark_ephemeral_address_as_seen( + conn, params, &address, tx_ref, + )?; + + // If the output belongs to the wallet, add it to `transparent_received_outputs`. + #[cfg(feature = "transparent-inputs")] + if let Some(account_uuid) = + transparent::find_account_uuid_for_transparent_address(conn, params, &address)? + { + debug!( + "{:?} output {} belongs to account {:?}", + d_tx.tx().txid(), + output_index, + account_uuid + ); + transparent::put_transparent_output( + conn, + params, + &OutPoint::new( + d_tx.tx().txid().into(), + u32::try_from(output_index).unwrap(), + ), + txout, + d_tx.mined_height(), + &address, + account_uuid, + false, + )?; + + // Since the wallet created the transparent output, we need to ensure + // that any transparent inputs belonging to the wallet will be + // discovered. + tx_has_wallet_outputs = true; + + // When we receive transparent funds (particularly as ephemeral outputs + // in transaction pairs sending to a ZIP 320 address) it becomes + // possible that the spend of these outputs is not then later detected + // if the transaction that spends them is purely transparent. This is + // especially a problem in wallet recovery. + transparent::queue_transparent_spend_detection( + conn, + params, + address, + tx_ref, + output_index.try_into().unwrap(), + )?; + } else { + debug!( + "Address {} is not recognized as belonging to any of our accounts.", + address.encode(params) + ); + } + + // If a transaction we observe contains spends from our wallet, we will + // store its transparent outputs in the same way they would be stored by + // create_spend_to_address. + if let Some(account_uuid) = funding_account { + let receiver = Receiver::Transparent(address); + + #[cfg(feature = "transparent-inputs")] + let recipient_address = + select_receiving_address(params, conn, account_uuid, &receiver)? + .unwrap_or_else(|| receiver.to_zcash_address(params.network_type())); + + #[cfg(not(feature = "transparent-inputs"))] + let recipient_address = receiver.to_zcash_address(params.network_type()); + + let recipient = Recipient::External { + recipient_address, + output_pool: PoolType::TRANSPARENT, + }; + + put_sent_output( + conn, + params, + account_uuid, + tx_ref, + output_index, + &recipient, + txout.value, + None, + )?; + + // Even though we know the funding account, we don't know that we have + // information for all of the transparent inputs to the transaction. + #[cfg(feature = "transparent-inputs")] + { + tx_has_wallet_outputs = true; + } + } + } else { + warn!( + "Unable to determine recipient address for tx {:?} output {}", + d_tx.tx().txid(), + output_index + ); + } + } + } + + // If the transaction has outputs that belong to the wallet as well as transparent + // inputs, we may need to download the transactions corresponding to the transparent + // prevout references to determine whether the transaction was created (at least in + // part) by this wallet. + #[cfg(feature = "transparent-inputs")] + if tx_has_wallet_outputs { + queue_transparent_input_retrieval(conn, tx_ref, &d_tx)? + } + + notify_tx_retrieved(conn, d_tx.tx().txid())?; + + // If the decrypted transaction is unmined and has no shielded components, add it to + // the queue for status retrieval. + #[cfg(feature = "transparent-inputs")] + queue_unmined_tx_retrieval(conn, &d_tx)?; + Ok(()) } /// Inserts information about a mined transaction that was observed to /// contain a note related to this wallet into the database. -pub fn put_tx_meta<'a, P, N>( - stmts: &mut DataConnStmtCache<'a, P>, - tx: &WalletTx, +pub(crate) fn put_tx_meta( + conn: &rusqlite::Connection, + tx: &WalletTx, height: BlockHeight, -) -> Result { - let txid = tx.txid.0.to_vec(); - if stmts - .stmt_update_tx_meta - .execute(params![u32::from(height), (tx.index as i64), txid])? - == 0 - { - // It isn't there, so insert our transaction into the database. - stmts - .stmt_insert_tx_meta - .execute(params![txid, u32::from(height), (tx.index as i64),])?; +) -> Result { + // It isn't there, so insert our transaction into the database. + let mut stmt_upsert_tx_meta = conn.prepare_cached( + "INSERT INTO transactions (txid, block, mined_height, tx_index) + VALUES (:txid, :block, :block, :tx_index) + ON CONFLICT (txid) DO UPDATE + SET block = :block, + mined_height = :block, + tx_index = :tx_index + RETURNING id_tx", + )?; - Ok(stmts.wallet_db.conn.last_insert_rowid()) - } else { - // It was there, so grab its row number. - stmts - .stmt_select_tx_ref - .query_row(&[txid], |row| row.get(0)) - .map_err(SqliteClientError::from) + let txid_bytes = tx.txid(); + let tx_params = named_params![ + ":txid": &txid_bytes.as_ref()[..], + ":block": u32::from(height), + ":tx_index": i64::try_from(tx.block_index()).expect("transaction indices are representable as i64"), + ]; + + stmt_upsert_tx_meta + .query_row(tx_params, |row| row.get::<_, i64>(0).map(TxRef)) + .map_err(SqliteClientError::from) +} + +/// Returns the most likely wallet address that corresponds to the protocol-level receiver of a +/// note or UTXO. +pub(crate) fn select_receiving_address( + _params: &P, + conn: &rusqlite::Connection, + account: AccountUuid, + receiver: &Receiver, +) -> Result, SqliteClientError> { + match receiver { + #[cfg(feature = "transparent-inputs")] + Receiver::Transparent(taddr) => conn + .query_row( + "SELECT address + FROM addresses + WHERE cached_transparent_receiver_address = :taddr", + named_params! { + ":taddr": Address::Transparent(*taddr).encode(_params) + }, + |row| row.get::<_, String>(0), + ) + .optional()? + .map(|addr_str| addr_str.parse::()) + .transpose() + .map_err(SqliteClientError::from), + receiver => { + let mut stmt = conn.prepare_cached( + "SELECT address + FROM addresses + JOIN accounts ON accounts.id = addresses.account_id + WHERE accounts.uuid = :account_uuid", + )?; + + let mut result = stmt.query(named_params! { ":account_uuid": account.0 })?; + while let Some(row) = result.next()? { + let addr_str = row.get::<_, String>(0)?; + let decoded = addr_str.parse::()?; + if receiver.corresponds(&decoded) { + return Ok(Some(decoded)); + } + } + + Ok(None) + } } } /// Inserts full transaction data into the database. -pub fn put_tx_data<'a, P>( - stmts: &mut DataConnStmtCache<'a, P>, +pub(crate) fn put_tx_data( + conn: &rusqlite::Connection, tx: &Transaction, + fee: Option, created_at: Option, -) -> Result { - let txid = tx.txid().0.to_vec(); + target_height: Option, +) -> Result { + let mut stmt_upsert_tx_data = conn.prepare_cached( + "INSERT INTO transactions (txid, created, expiry_height, raw, fee, target_height) + VALUES (:txid, :created_at, :expiry_height, :raw, :fee, :target_height) + ON CONFLICT (txid) DO UPDATE + SET expiry_height = :expiry_height, + raw = :raw, + fee = IFNULL(:fee, fee) + RETURNING id_tx", + )?; + let txid = tx.txid(); let mut raw_tx = vec![]; tx.write(&mut raw_tx)?; - if stmts - .stmt_update_tx_data - .execute(params![u32::from(tx.expiry_height), raw_tx, txid,])? - == 0 - { - // It isn't there, so insert our transaction into the database. - stmts.stmt_insert_tx_data.execute(params![ - txid, - created_at, - u32::from(tx.expiry_height), - raw_tx - ])?; + let tx_params = named_params![ + ":txid": &txid.as_ref()[..], + ":created_at": created_at, + ":expiry_height": u32::from(tx.expiry_height()), + ":raw": raw_tx, + ":fee": fee.map(u64::from), + ":target_height": target_height.map(u32::from), + ]; - Ok(stmts.wallet_db.conn.last_insert_rowid()) - } else { - // It was there, so grab its row number. - stmts - .stmt_select_tx_ref - .query_row(&[txid], |row| row.get(0)) - .map_err(SqliteClientError::from) + stmt_upsert_tx_data + .query_row(tx_params, |row| row.get::<_, i64>(0).map(TxRef)) + .map_err(SqliteClientError::from) +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum TxQueryType { + Status, + Enhancement, +} + +impl TxQueryType { + pub(crate) fn code(&self) -> i64 { + match self { + TxQueryType::Status => 0, + TxQueryType::Enhancement => 1, + } + } + + pub(crate) fn from_code(code: i64) -> Option { + match code { + 0 => Some(TxQueryType::Status), + 1 => Some(TxQueryType::Enhancement), + _ => None, + } } } -/// Marks a given nullifier as having been revealed in the construction -/// of the specified transaction. -/// -/// Marking a note spent in this fashion does NOT imply that the -/// spending transaction has been mined. -pub fn mark_spent<'a, P>( - stmts: &mut DataConnStmtCache<'a, P>, - tx_ref: i64, - nf: &Nullifier, +#[cfg(feature = "transparent-inputs")] +pub(crate) fn queue_transparent_input_retrieval( + conn: &rusqlite::Transaction<'_>, + tx_ref: TxRef, + d_tx: &DecryptedTransaction<'_, AccountId>, ) -> Result<(), SqliteClientError> { - stmts - .stmt_mark_recived_note_spent - .execute(&[tx_ref.to_sql()?, nf.0.to_sql()?])?; + if let Some(b) = d_tx.tx().transparent_bundle() { + // queue the transparent inputs for enhancement + queue_tx_retrieval( + conn, + b.vin.iter().map(|txin| *txin.prevout.txid()), + Some(tx_ref), + )?; + } + Ok(()) } -/// Records the specified shielded output as having been received. -// Assumptions: -// - A transaction will not contain more than 2^63 shielded outputs. -// - A note value will never exceed 2^63 zatoshis. -pub fn put_received_note<'a, P, T: ShieldedOutput>( - stmts: &mut DataConnStmtCache<'a, P>, - output: &T, - tx_ref: i64, -) -> Result { - let rcm = output.note().rcm().to_repr(); - let account = output.account().0 as i64; - let diversifier = output.to().diversifier().0.to_vec(); - let value = output.note().value as i64; - let rcm = rcm.as_ref(); - let memo = output.memo().map(|m| m.as_slice()); - let is_change = output.is_change(); - let tx = tx_ref; - let output_index = output.index() as i64; - let nf_bytes = output.nullifier().map(|nf| nf.0.to_vec()); - - let sql_args: &[(&str, &dyn ToSql)] = &[ - (&":account", &account), - (&":diversifier", &diversifier), - (&":value", &value), - (&":rcm", &rcm), - (&":nf", &nf_bytes), - (&":memo", &memo), - (&":is_change", &is_change), - (&":tx", &tx), - (&":output_index", &output_index), - ]; - - // First try updating an existing received note into the database. - if stmts.stmt_update_received_note.execute_named(&sql_args)? == 0 { - // It isn't there, so insert our note into the database. - stmts.stmt_insert_received_note.execute_named(&sql_args)?; +#[cfg(feature = "transparent-inputs")] +pub(crate) fn queue_unmined_tx_retrieval( + conn: &rusqlite::Transaction<'_>, + d_tx: &DecryptedTransaction<'_, AccountId>, +) -> Result<(), SqliteClientError> { + let detectable_via_scanning = d_tx.tx().sapling_bundle().is_some(); + #[cfg(feature = "orchard")] + let detectable_via_scanning = detectable_via_scanning | d_tx.tx().orchard_bundle().is_some(); - Ok(NoteId::ReceivedNoteId( - stmts.wallet_db.conn.last_insert_rowid(), - )) - } else { - // It was there, so grab its row number. - stmts - .stmt_select_received_note - .query_row(params![tx_ref, (output.index() as i64)], |row| { - row.get(0).map(NoteId::ReceivedNoteId) - }) - .map_err(SqliteClientError::from) + if d_tx.mined_height().is_none() && !detectable_via_scanning { + queue_tx_retrieval(conn, std::iter::once(d_tx.tx().txid()), None)? } + + Ok(()) } -/// Records the incremental witness for the specified note, -/// as of the given block height. -pub fn insert_witness<'a, P>( - stmts: &mut DataConnStmtCache<'a, P>, - note_id: i64, - witness: &IncrementalWitness, - height: BlockHeight, +pub(crate) fn queue_tx_retrieval( + conn: &rusqlite::Transaction<'_>, + txids: impl Iterator, + dependent_tx_ref: Option, ) -> Result<(), SqliteClientError> { - let mut encoded = Vec::new(); - witness.write(&mut encoded).unwrap(); + // Add an entry to the transaction retrieval queue if it would not be redundant. + let mut stmt_insert_tx = conn.prepare_cached( + "INSERT INTO tx_retrieval_queue (txid, query_type, dependent_transaction_id) + SELECT + :txid, + IIF( + EXISTS (SELECT 1 FROM transactions WHERE txid = :txid AND raw IS NOT NULL), + :status_type, + :enhancement_type + ), + :dependent_transaction_id + ON CONFLICT (txid) DO UPDATE + SET query_type = + IIF( + EXISTS (SELECT 1 FROM transactions WHERE txid = :txid AND raw IS NOT NULL), + :status_type, + :enhancement_type + ), + dependent_transaction_id = IFNULL(:dependent_transaction_id, dependent_transaction_id)", + )?; - stmts - .stmt_insert_witness - .execute(params![note_id, u32::from(height), encoded])?; + for txid in txids { + stmt_insert_tx.execute(named_params! { + ":txid": txid.as_ref(), + ":status_type": TxQueryType::Status.code(), + ":enhancement_type": TxQueryType::Enhancement.code(), + ":dependent_transaction_id": dependent_tx_ref.map(|r| r.0), + })?; + } Ok(()) } -/// Removes old incremental witnesses up to the given block height. -pub fn prune_witnesses

( - stmts: &mut DataConnStmtCache<'_, P>, - below_height: BlockHeight, +/// Returns the vector of [`TransactionDataRequest`]s that represents the information needed by the +/// wallet backend in order to be able to present a complete view of wallet history and memo data. +pub(crate) fn transaction_data_requests( + conn: &rusqlite::Connection, +) -> Result, SqliteClientError> { + let mut tx_retrieval_stmt = + conn.prepare_cached("SELECT txid, query_type FROM tx_retrieval_queue")?; + + let result = tx_retrieval_stmt + .query_and_then([], |row| { + let txid = row.get(0).map(TxId::from_bytes)?; + let query_type = row.get(1).map(TxQueryType::from_code)?.ok_or_else(|| { + SqliteClientError::CorruptedData( + "Unrecognized transaction data request type.".to_owned(), + ) + })?; + + Ok::(match query_type { + TxQueryType::Status => TransactionDataRequest::GetStatus(txid), + TxQueryType::Enhancement => TransactionDataRequest::Enhancement(txid), + }) + })? + .collect::, _>>()?; + + Ok(result) +} + +pub(crate) fn notify_tx_retrieved( + conn: &rusqlite::Transaction<'_>, + txid: TxId, ) -> Result<(), SqliteClientError> { - stmts - .stmt_prune_witnesses - .execute(&[u32::from(below_height)])?; + conn.execute( + "DELETE FROM tx_retrieval_queue WHERE txid = :txid", + named_params![":txid": &txid.as_ref()[..]], + )?; + Ok(()) } -/// Marks notes that have not been mined in transactions -/// as expired, up to the given block height. -pub fn update_expired_notes

( - stmts: &mut DataConnStmtCache<'_, P>, - height: BlockHeight, +// A utility function for creation of parameters for use in `insert_sent_output` +// and `put_sent_output` +fn recipient_params( + conn: &Connection, + _params: &P, + from: AccountUuid, + to: &Recipient, +) -> Result<(AccountRef, Option, Option, PoolType), SqliteClientError> { + let from_account_id = get_account_ref(conn, from)?; + match to { + Recipient::External { + recipient_address, + output_pool, + .. + } => Ok(( + from_account_id, + Some(recipient_address.encode()), + None, + *output_pool, + )), + #[cfg(feature = "transparent-inputs")] + Recipient::EphemeralTransparent { + receiving_account, + ephemeral_address, + .. + } => { + let to_account = get_account_ref(conn, *receiving_account)?; + Ok(( + from_account_id, + Some(ephemeral_address.encode(_params)), + Some(to_account), + PoolType::TRANSPARENT, + )) + } + Recipient::InternalAccount { + receiving_account, + external_address, + note, + } => { + let to_account = get_account_ref(conn, *receiving_account)?; + Ok(( + from_account_id, + external_address.as_ref().map(|a| a.encode()), + Some(to_account), + PoolType::Shielded(note.protocol()), + )) + } + } +} + +fn flag_previously_received_change( + conn: &rusqlite::Transaction, + tx_ref: TxRef, ) -> Result<(), SqliteClientError> { - stmts.stmt_update_expired.execute(&[u32::from(height)])?; + let flag_received_change = |table_prefix| { + conn.execute( + &format!( + "UPDATE {table_prefix}_received_notes + SET is_change = 1 + FROM sent_notes sn + WHERE sn.tx = {table_prefix}_received_notes.tx + AND sn.tx = :tx + AND sn.from_account_id = {table_prefix}_received_notes.account_id + AND {table_prefix}_received_notes.recipient_key_scope = :internal_scope" + ), + named_params! { + ":tx": tx_ref.0, + ":internal_scope": scope_code(Scope::Internal) + }, + ) + }; + + flag_received_change(SAPLING_TABLES_PREFIX)?; + #[cfg(feature = "orchard")] + flag_received_change(ORCHARD_TABLES_PREFIX)?; + Ok(()) } -/// Records information about a note that your wallet created. -pub fn put_sent_note<'a, P: consensus::Parameters>( - stmts: &mut DataConnStmtCache<'a, P>, - output: &DecryptedOutput, - tx_ref: i64, +/// Records information about a transaction output that your wallet created. +pub(crate) fn insert_sent_output( + conn: &rusqlite::Transaction, + params: &P, + tx_ref: TxRef, + from_account_uuid: AccountUuid, + output: &SentTransactionOutput, ) -> Result<(), SqliteClientError> { - let output_index = output.index as i64; - let account = output.account.0 as i64; - let value = output.note.value as i64; - let to_str = encode_payment_address( - stmts.wallet_db.params.hrp_sapling_payment_address(), - &output.to, - ); + let mut stmt_insert_sent_output = conn.prepare_cached( + "INSERT INTO sent_notes ( + tx, output_pool, output_index, from_account_id, + to_address, to_account_id, value, memo) + VALUES ( + :tx, :output_pool, :output_index, :from_account_id, + :to_address, :to_account_id, :value, :memo)", + )?; - // Try updating an existing sent note. - if stmts.stmt_update_sent_note.execute(params![ - account, - to_str, - value, - &output.memo.as_slice(), - tx_ref, - output_index - ])? == 0 - { - // It isn't there, so insert. - insert_sent_note( - stmts, - tx_ref, - output.index, - output.account, - &RecipientAddress::Shielded(output.to.clone()), - Amount::from_u64(output.note.value) - .map_err(|_| SqliteClientError::CorruptedData("Note value invalid.".to_string()))?, - Some(&output.memo), - )? - } + let (from_account_id, to_address, to_account_id, pool_type) = + recipient_params(conn, params, from_account_uuid, output.recipient())?; + let sql_args = named_params![ + ":tx": tx_ref.0, + ":output_pool": &pool_code(pool_type), + ":output_index": &i64::try_from(output.output_index()).unwrap(), + ":from_account_id": from_account_id.0, + ":to_address": &to_address, + ":to_account_id": to_account_id.map(|a| a.0), + ":value": &i64::from(ZatBalance::from(output.value())), + ":memo": memo_repr(output.memo()) + ]; + + stmt_insert_sent_output.execute(sql_args)?; + flag_previously_received_change(conn, tx_ref)?; Ok(()) } -/// Inserts a sent note into the wallet database. -/// -/// `output_index` is the index within the transaction that contains the recipient output: +/// Records information about a transaction output that your wallet created, from the constituent +/// properties of that output. /// -/// - If `to` is a Sapling address, this is an index into the Sapling outputs of the -/// transaction. -/// - If `to` is a transparent address, this is an index into the transparent outputs of +/// - If `recipient` is a Unified address, `output_index` is an index into the outputs of the +/// transaction within the bundle associated with the recipient's output pool. +/// - If `recipient` is a Sapling address, `output_index` is an index into the Sapling outputs of /// the transaction. -pub fn insert_sent_note<'a, P: consensus::Parameters>( - stmts: &mut DataConnStmtCache<'a, P>, - tx_ref: i64, +/// - If `recipient` is a transparent address, `output_index` is an index into the transparent +/// outputs of the transaction. +/// - If `recipient` is an internal account, `output_index` is an index into the Sapling outputs of +/// the transaction. +#[allow(clippy::too_many_arguments)] +pub(crate) fn put_sent_output( + conn: &rusqlite::Transaction, + params: &P, + from_account_uuid: AccountUuid, + tx_ref: TxRef, output_index: usize, - account: AccountId, - to: &RecipientAddress, - value: Amount, + recipient: &Recipient, + value: Zatoshis, memo: Option<&MemoBytes>, ) -> Result<(), SqliteClientError> { - let to_str = to.encode(&stmts.wallet_db.params); - let ivalue: i64 = value.into(); - stmts.stmt_insert_sent_note.execute(params![ - tx_ref, - (output_index as i64), - account.0, - to_str, - ivalue, - memo.map(|m| m.as_slice().to_vec()), - ])?; + let mut stmt_upsert_sent_output = conn.prepare_cached( + "INSERT INTO sent_notes ( + tx, output_pool, output_index, from_account_id, + to_address, to_account_id, value, memo) + VALUES ( + :tx, :output_pool, :output_index, :from_account_id, + :to_address, :to_account_id, :value, :memo) + ON CONFLICT (tx, output_pool, output_index) DO UPDATE + SET from_account_id = :from_account_id, + to_address = IFNULL(to_address, :to_address), + to_account_id = IFNULL(to_account_id, :to_account_id), + value = :value, + memo = IFNULL(:memo, memo)", + )?; + + let (from_account_id, to_address, to_account_id, pool_type) = + recipient_params(conn, params, from_account_uuid, recipient)?; + let sql_args = named_params![ + ":tx": tx_ref.0, + ":output_pool": &pool_code(pool_type), + ":output_index": &i64::try_from(output_index).unwrap(), + ":from_account_id": from_account_id.0, + ":to_address": &to_address, + ":to_account_id": &to_account_id.map(|a| a.0), + ":value": &i64::from(ZatBalance::from(value)), + ":memo": memo_repr(memo) + ]; + + stmt_upsert_sent_output.execute(sql_args)?; + flag_previously_received_change(conn, tx_ref)?; Ok(()) } -#[cfg(test)] -mod tests { - use tempfile::NamedTempFile; +/// Inserts the given entries into the nullifier map. +/// +/// Returns an error if the new entries conflict with existing ones. This indicates either +/// corrupted data, or that a reorg has occurred and the caller needs to repair the wallet +/// state with [`truncate_to_height`]. +pub(crate) fn insert_nullifier_map>( + conn: &rusqlite::Transaction<'_>, + block_height: BlockHeight, + spend_pool: ShieldedProtocol, + new_entries: &[(TxId, u16, Vec)], +) -> Result<(), SqliteClientError> { + let mut stmt_select_tx_locators = conn.prepare_cached( + "SELECT block_height, tx_index, txid + FROM tx_locator_map + WHERE (block_height = :block_height AND tx_index = :tx_index) OR txid = :txid", + )?; + let mut stmt_insert_tx_locator = conn.prepare_cached( + "INSERT INTO tx_locator_map + (block_height, tx_index, txid) + VALUES (:block_height, :tx_index, :txid)", + )?; + let mut stmt_insert_nullifier_mapping = conn.prepare_cached( + "INSERT INTO nullifier_map + (spend_pool, nf, block_height, tx_index) + VALUES (:spend_pool, :nf, :block_height, :tx_index) + ON CONFLICT (spend_pool, nf) DO UPDATE + SET block_height = :block_height, + tx_index = :tx_index", + )?; + + for (txid, tx_index, nullifiers) in new_entries { + let tx_args = named_params![ + ":block_height": u32::from(block_height), + ":tx_index": tx_index, + ":txid": txid.as_ref(), + ]; + + // We cannot use an upsert here, because we use the tx locator as the foreign key + // in `nullifier_map` instead of `txid` for database size efficiency. If an insert + // into `tx_locator_map` were to conflict, we would need the resulting update to + // cascade into `nullifier_map` as either: + // - an update (if a transaction moved within a block), or + // - a deletion (if the locator now points to a different transaction). + // + // `ON UPDATE` has `CASCADE` to always update, but has no deletion option. So we + // instead set `ON UPDATE RESTRICT` on the foreign key relation, and require the + // caller to manually rewind the database in this situation. + let locator = stmt_select_tx_locators + .query_map(tx_args, |row| { + Ok(( + BlockHeight::from_u32(row.get(0)?), + row.get::<_, u16>(1)?, + TxId::from_bytes(row.get(2)?), + )) + })? + .try_fold(None, |acc, row| -> Result<_, SqliteClientError> { + match (acc, row?) { + (None, rhs) => Ok(Some(Some(rhs))), + // If there was more than one row, then due to the uniqueness + // constraints on the `tx_locator_map` table, all of the rows conflict + // with the locator being inserted. + (Some(_), _) => Ok(Some(None)), + } + })?; + + match locator { + // If the locator in the table matches the one being inserted, do nothing. + Some(Some(loc)) if loc == (block_height, *tx_index, *txid) => (), + // If the locator being inserted would conflict, report it. + Some(_) => Err(SqliteClientError::DbError(rusqlite::Error::SqliteFailure( + rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_CONSTRAINT), + Some("UNIQUE constraint failed: tx_locator_map.block_height, tx_locator_map.tx_index".into()), + )))?, + // If the locator doesn't exist, insert it. + None => stmt_insert_tx_locator.execute(tx_args).map(|_| ())?, + } + + for nf in nullifiers { + // Here it is okay to use an upsert, because per above we've confirmed that + // the locator points to the same transaction. + let nf_args = named_params![ + ":spend_pool": pool_code(PoolType::Shielded(spend_pool)), + ":nf": nf.as_ref(), + ":block_height": u32::from(block_height), + ":tx_index": tx_index, + ]; + stmt_insert_nullifier_mapping.execute(nf_args)?; + } + } + + Ok(()) +} + +/// Returns the row of the `transactions` table corresponding to the transaction in which +/// this nullifier is revealed, if any. +pub(crate) fn query_nullifier_map>( + conn: &rusqlite::Transaction<'_>, + spend_pool: ShieldedProtocol, + nf: &N, +) -> Result, SqliteClientError> { + let mut stmt_select_locator = conn.prepare_cached( + "SELECT block_height, tx_index, txid + FROM nullifier_map + LEFT JOIN tx_locator_map USING (block_height, tx_index) + WHERE spend_pool = :spend_pool AND nf = :nf", + )?; + + let sql_args = named_params![ + ":spend_pool": pool_code(PoolType::Shielded(spend_pool)), + ":nf": nf.as_ref(), + ]; + + // Find the locator corresponding to this nullifier, if any. + let locator = stmt_select_locator + .query_row(sql_args, |row| { + Ok(( + BlockHeight::from_u32(row.get(0)?), + row.get(1)?, + TxId::from_bytes(row.get(2)?), + )) + }) + .optional()?; + let (height, index, txid) = match locator { + Some(res) => res, + None => return Ok(None), + }; + + // Find or create a corresponding row in the `transactions` table. Usually a row will + // have been created during the same scan that the locator was added to the nullifier + // map, but it would not happen if the transaction in question spent the note with no + // change or explicit in-wallet recipient. + put_tx_meta( + conn, + &WalletTx::new( + txid, + index, + vec![], + vec![], + #[cfg(feature = "orchard")] + vec![], + #[cfg(feature = "orchard")] + vec![], + ), + height, + ) + .map(Some) +} + +/// Deletes from the nullifier map any entries with a locator referencing a block height +/// lower than the pruning height. +pub(crate) fn prune_nullifier_map( + conn: &rusqlite::Transaction<'_>, + block_height: BlockHeight, +) -> Result<(), SqliteClientError> { + let mut stmt_delete_locators = conn.prepare_cached( + "DELETE FROM tx_locator_map + WHERE block_height < :block_height", + )?; + + stmt_delete_locators.execute(named_params![":block_height": u32::from(block_height)])?; + + Ok(()) +} - use zcash_primitives::{ - transaction::components::Amount, - zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing { + use incrementalmerkletree::Position; + use zcash_client_backend::data_api::testing::TransactionSummary; + use zcash_primitives::transaction::TxId; + use zcash_protocol::{ + consensus::BlockHeight, + value::{ZatBalance, Zatoshis}, + ShieldedProtocol, }; - use zcash_client_backend::data_api::WalletRead; + use crate::{error::SqliteClientError, AccountUuid, SAPLING_TABLES_PREFIX}; + + #[cfg(feature = "orchard")] + use crate::ORCHARD_TABLES_PREFIX; + + pub(crate) fn get_tx_history( + conn: &rusqlite::Connection, + ) -> Result>, SqliteClientError> { + let mut stmt = conn.prepare_cached( + "SELECT accounts.uuid as account_uuid, v_transactions.* + FROM v_transactions + JOIN accounts ON accounts.uuid = v_transactions.account_uuid + ORDER BY mined_height DESC, tx_index DESC", + )?; + + let results = stmt + .query_and_then::<_, SqliteClientError, _, _>([], |row| { + Ok(TransactionSummary::from_parts( + AccountUuid(row.get("account_uuid")?), + TxId::from_bytes(row.get("txid")?), + row.get::<_, Option>("expiry_height")? + .map(BlockHeight::from), + row.get::<_, Option>("mined_height")? + .map(BlockHeight::from), + ZatBalance::from_i64(row.get("account_balance_delta")?)?, + row.get::<_, Option>("fee_paid")? + .map(Zatoshis::from_nonnegative_i64) + .transpose()?, + row.get("spent_note_count")?, + row.get("has_change")?, + row.get("sent_note_count")?, + row.get("received_note_count")?, + row.get("memo_count")?, + row.get("expired_unmined")?, + row.get("is_shielding")?, + )) + })? + .collect::, _>>()?; + + Ok(results) + } + + /// Returns a vector of transaction summaries + #[allow(dead_code)] // used only for tests that are flagged off by default + pub(crate) fn get_checkpoint_history( + conn: &rusqlite::Connection, + protocol: &ShieldedProtocol, + ) -> Result)>, SqliteClientError> { + let table_prefix = match protocol { + ShieldedProtocol::Sapling => SAPLING_TABLES_PREFIX, + #[cfg(feature = "orchard")] + ShieldedProtocol::Orchard => ORCHARD_TABLES_PREFIX, + #[cfg(not(feature = "orchard"))] + ShieldedProtocol::Orchard => { + return Err(SqliteClientError::UnsupportedPoolType( + zcash_protocol::PoolType::ORCHARD, + )); + } + }; + + let mut stmt = conn.prepare_cached(&format!( + "SELECT checkpoint_id, position FROM {}_tree_checkpoints + ORDER BY checkpoint_id", + table_prefix + ))?; + + let results = stmt + .query_and_then::<_, SqliteClientError, _, _>([], |row| { + Ok(( + BlockHeight::from(row.get::<_, u32>(0)?), + row.get::<_, Option>(1)?.map(Position::from), + )) + })? + .collect::, _>>()?; + + Ok(results) + } +} + +#[cfg(test)] +mod tests { + use std::num::NonZeroU32; + + use sapling::zip32::ExtendedSpendingKey; + use secrecy::{ExposeSecret, SecretVec}; + use uuid::Uuid; + use zcash_client_backend::data_api::{ + testing::{AddressType, DataStoreFactory, FakeCompactOutput, TestBuilder, TestState}, + Account as _, AccountSource, WalletRead, WalletWrite, + }; + use zcash_primitives::block::BlockHash; + use zcash_protocol::value::Zatoshis; use crate::{ - tests, - wallet::init::{init_accounts_table, init_wallet_db}, - AccountId, WalletDb, + testing::{db::TestDbFactory, BlockCache}, + AccountUuid, }; - use super::{get_address, get_balance}; + use super::account_birthday; #[test] fn empty_database_has_no_balance() { - let data_file = NamedTempFile::new().unwrap(); - let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&db_data).unwrap(); - - // Add an account to the wallet - let extsk = ExtendedSpendingKey::master(&[]); - let extfvks = [ExtendedFullViewingKey::from(&extsk)]; - init_accounts_table(&db_data, &extfvks).unwrap(); + let st = TestBuilder::new() + .with_data_store_factory(TestDbFactory::default()) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + let account = st.test_account().unwrap(); - // The account should be empty - assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), Amount::zero()); + // The account should have no summary information + assert_eq!(st.get_wallet_summary(0), None); // We can't get an anchor height, as we have not scanned any blocks. - assert_eq!((&db_data).get_target_and_anchor_heights().unwrap(), None); + assert_eq!( + st.wallet() + .get_target_and_anchor_heights(NonZeroU32::new(10).unwrap()) + .unwrap(), + None + ); + + // The default address is set for the test account + assert_matches!(st.wallet().get_current_address(account.id()), Ok(Some(_))); + + // No default address is set for an un-initialized account + assert_matches!( + st.wallet().get_current_address(AccountUuid(Uuid::nil())), + Ok(None) + ); + } + + #[test] + fn get_default_account_index() { + let st = TestBuilder::new() + .with_data_store_factory(TestDbFactory::default()) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + let account_id = st.test_account().unwrap().id(); + let account_parameters = st.wallet().get_account(account_id).unwrap().unwrap(); + + let expected_account_index = zip32::AccountId::try_from(0).unwrap(); + assert_matches!( + account_parameters.kind, + AccountSource::Derived{derivation, ..} if derivation.account_index() == expected_account_index + ); + } + + #[test] + fn get_account_ids() { + let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory::default()) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let seed = SecretVec::new(st.test_seed().unwrap().expose_secret().clone()); + let birthday = st.test_account().unwrap().birthday().clone(); + + st.wallet_mut() + .create_account("", &seed, &birthday, None) + .unwrap(); + + for acct_id in st.wallet().get_account_ids().unwrap() { + assert_matches!(st.wallet().get_account(acct_id), Ok(Some(_))) + } + } + + #[test] + fn block_fully_scanned() { + check_block_fully_scanned(TestDbFactory::default()) + } - // An invalid account has zero balance - assert!(get_address(&db_data, AccountId(1)).is_err()); - assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), Amount::zero()); + fn check_block_fully_scanned(dsf: DsF) { + let mut st = TestBuilder::new() + .with_data_store_factory(dsf) + .with_block_cache(BlockCache::new()) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let block_fully_scanned = |st: &TestState<_, DsF::DataStore, _>| { + st.wallet() + .block_fully_scanned() + .unwrap() + .map(|meta| meta.block_height()) + }; + + // A fresh wallet should have no fully-scanned block. + assert_eq!(block_fully_scanned(&st), None); + + // Scan a block above the wallet's birthday height. + let not_our_key = ExtendedSpendingKey::master(&[]).to_diversifiable_full_viewing_key(); + let not_our_value = Zatoshis::const_from_u64(10000); + let start_height = st.sapling_activation_height(); + let _ = st.generate_block_at( + start_height, + BlockHash([0; 32]), + &[FakeCompactOutput::new( + ¬_our_key, + AddressType::DefaultExternal, + not_our_value, + )], + 0, + 0, + false, + ); + let (mid_height, _, _) = + st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value); + let (end_height, _, _) = + st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value); + + // Scan the last block first + st.scan_cached_blocks(end_height, 1); + + // The wallet should still have no fully-scanned block, as no scanned block range + // overlaps the wallet's birthday. + assert_eq!(block_fully_scanned(&st), None); + + // Scan the block at the wallet's birthday height. + st.scan_cached_blocks(start_height, 1); + + // The fully-scanned height should now be that of the scanned block. + assert_eq!(block_fully_scanned(&st), Some(start_height)); + + // Scan the block in between the two previous blocks. + st.scan_cached_blocks(mid_height, 1); + + // The fully-scanned height should now be the latest block, as the two disjoint + // ranges have been connected. + assert_eq!(block_fully_scanned(&st), Some(end_height)); + } + + #[test] + fn test_account_birthday() { + let st = TestBuilder::new() + .with_data_store_factory(TestDbFactory::default()) + .with_block_cache(BlockCache::new()) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account_id = st.test_account().unwrap().id(); + assert_matches!( + account_birthday(st.wallet().conn(), account_id), + Ok(birthday) if birthday == st.sapling_activation_height() + ) } } diff --git a/zcash_client_sqlite/src/wallet/commitment_tree.rs b/zcash_client_sqlite/src/wallet/commitment_tree.rs new file mode 100644 index 0000000000..6d123a6f45 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/commitment_tree.rs @@ -0,0 +1,1298 @@ +use rusqlite::{self, named_params, OptionalExtension}; +use std::{ + collections::BTreeSet, + error, fmt, + io::{self, Cursor}, + marker::PhantomData, + num::NonZeroU32, + ops::Range, + sync::Arc, +}; + +use incrementalmerkletree::{Address, Hashable, Level, Position, Retention}; +use shardtree::{ + error::ShardTreeError, + store::{Checkpoint, ShardStore, TreeState}, + LocatedPrunableTree, LocatedTree, PrunableTree, RetentionFlags, +}; + +use zcash_client_backend::{ + data_api::chain::CommitmentTreeRoot, + serialization::shardtree::{read_shard, write_shard}, +}; +use zcash_primitives::merkle_tree::HashSer; +use zcash_protocol::consensus::BlockHeight; + +/// Errors that can appear in SQLite-back [`ShardStore`] implementation operations. +#[derive(Debug)] +pub enum Error { + /// Errors in deserializing stored shard data + Serialization(io::Error), + /// Errors encountered querying stored shard data + Query(rusqlite::Error), + /// Raised when the caller attempts to add a checkpoint at a block height where a checkpoint + /// already exists, but the tree state being checkpointed or the marks removed at that + /// checkpoint conflict with the existing tree state. + CheckpointConflict { + checkpoint_id: BlockHeight, + checkpoint: Checkpoint, + extant_tree_state: TreeState, + extant_marks_removed: Option>, + }, + /// Raised when attempting to add shard roots to the database that + /// are discontinuous with the existing roots in the database. + SubtreeDiscontinuity { + attempted_insertion_range: Range, + existing_range: Range, + }, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self { + Error::Serialization(err) => write!(f, "Commitment tree serialization error: {}", err), + Error::Query(err) => write!(f, "Commitment tree query or update error: {}", err), + Error::CheckpointConflict { + checkpoint_id, + checkpoint, + extant_tree_state, + extant_marks_removed, + } => { + write!( + f, + "Conflict at checkpoint id {}, tried to insert {:?}, which is incompatible with existing state ({:?}, {:?})", + checkpoint_id, checkpoint, extant_tree_state, extant_marks_removed + ) + } + Error::SubtreeDiscontinuity { + attempted_insertion_range, + existing_range, + } => { + write!( + f, + "Attempted to write subtree roots with indices {:?} which is discontinuous with existing subtree range {:?}", + attempted_insertion_range, existing_range, + ) + } + } + } +} + +impl error::Error for Error { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match &self { + Error::Serialization(e) => Some(e), + Error::Query(e) => Some(e), + Error::CheckpointConflict { .. } => None, + Error::SubtreeDiscontinuity { .. } => None, + } + } +} + +pub struct SqliteShardStore { + pub(crate) conn: C, + table_prefix: &'static str, + _hash_type: PhantomData, +} + +impl SqliteShardStore { + const SHARD_ROOT_LEVEL: Level = Level::new(SHARD_HEIGHT); + + pub(crate) fn from_connection( + conn: C, + table_prefix: &'static str, + ) -> Result { + Ok(SqliteShardStore { + conn, + table_prefix, + _hash_type: PhantomData, + }) + } +} + +impl<'conn, 'a: 'conn, H: HashSer, const SHARD_HEIGHT: u8> ShardStore + for SqliteShardStore<&'a rusqlite::Transaction<'conn>, H, SHARD_HEIGHT> +{ + type H = H; + type CheckpointId = BlockHeight; + type Error = Error; + + fn get_shard( + &self, + shard_root: Address, + ) -> Result>, Self::Error> { + get_shard(self.conn, self.table_prefix, shard_root) + } + + fn last_shard(&self) -> Result>, Self::Error> { + last_shard(self.conn, self.table_prefix, Self::SHARD_ROOT_LEVEL) + } + + fn put_shard(&mut self, subtree: LocatedPrunableTree) -> Result<(), Self::Error> { + put_shard(self.conn, self.table_prefix, subtree) + } + + fn get_shard_roots(&self) -> Result, Self::Error> { + get_shard_roots(self.conn, self.table_prefix, Self::SHARD_ROOT_LEVEL) + } + + fn truncate_shards(&mut self, shard_index: u64) -> Result<(), Self::Error> { + truncate_shards(self.conn, self.table_prefix, shard_index) + } + + fn get_cap(&self) -> Result, Self::Error> { + get_cap(self.conn, self.table_prefix) + } + + fn put_cap(&mut self, cap: PrunableTree) -> Result<(), Self::Error> { + put_cap(self.conn, self.table_prefix, cap) + } + + fn min_checkpoint_id(&self) -> Result, Self::Error> { + min_checkpoint_id(self.conn, self.table_prefix) + } + + fn max_checkpoint_id(&self) -> Result, Self::Error> { + max_checkpoint_id(self.conn, self.table_prefix) + } + + fn add_checkpoint( + &mut self, + checkpoint_id: Self::CheckpointId, + checkpoint: Checkpoint, + ) -> Result<(), Self::Error> { + add_checkpoint(self.conn, self.table_prefix, checkpoint_id, checkpoint) + } + + fn checkpoint_count(&self) -> Result { + checkpoint_count(self.conn, self.table_prefix) + } + + fn get_checkpoint_at_depth( + &self, + checkpoint_depth: usize, + ) -> Result, Self::Error> { + get_checkpoint_at_depth(self.conn, self.table_prefix, checkpoint_depth) + .map_err(Error::Query) + } + + fn get_checkpoint( + &self, + checkpoint_id: &Self::CheckpointId, + ) -> Result, Self::Error> { + get_checkpoint(self.conn, self.table_prefix, *checkpoint_id) + } + + fn with_checkpoints(&mut self, limit: usize, callback: F) -> Result<(), Self::Error> + where + F: FnMut(&Self::CheckpointId, &Checkpoint) -> Result<(), Self::Error>, + { + with_checkpoints(self.conn, self.table_prefix, limit, callback) + } + + fn for_each_checkpoint(&self, limit: usize, callback: F) -> Result<(), Self::Error> + where + F: FnMut(&Self::CheckpointId, &Checkpoint) -> Result<(), Self::Error>, + { + with_checkpoints(self.conn, self.table_prefix, limit, callback) + } + + fn update_checkpoint_with( + &mut self, + checkpoint_id: &Self::CheckpointId, + update: F, + ) -> Result + where + F: Fn(&mut Checkpoint) -> Result<(), Self::Error>, + { + update_checkpoint_with(self.conn, self.table_prefix, *checkpoint_id, update) + } + + fn remove_checkpoint(&mut self, checkpoint_id: &Self::CheckpointId) -> Result<(), Self::Error> { + remove_checkpoint(self.conn, self.table_prefix, *checkpoint_id) + } + + fn truncate_checkpoints_retaining( + &mut self, + checkpoint_id: &Self::CheckpointId, + ) -> Result<(), Self::Error> { + truncate_checkpoints_retaining(self.conn, self.table_prefix, *checkpoint_id) + } +} + +impl ShardStore + for SqliteShardStore +{ + type H = H; + type CheckpointId = BlockHeight; + type Error = Error; + + fn get_shard( + &self, + shard_root: Address, + ) -> Result>, Self::Error> { + get_shard(&self.conn, self.table_prefix, shard_root) + } + + fn last_shard(&self) -> Result>, Self::Error> { + last_shard(&self.conn, self.table_prefix, Self::SHARD_ROOT_LEVEL) + } + + fn put_shard(&mut self, subtree: LocatedPrunableTree) -> Result<(), Self::Error> { + let tx = self.conn.transaction().map_err(Error::Query)?; + put_shard(&tx, self.table_prefix, subtree)?; + tx.commit().map_err(Error::Query)?; + Ok(()) + } + + fn get_shard_roots(&self) -> Result, Self::Error> { + get_shard_roots(&self.conn, self.table_prefix, Self::SHARD_ROOT_LEVEL) + } + + fn truncate_shards(&mut self, shard_index: u64) -> Result<(), Self::Error> { + truncate_shards(&self.conn, self.table_prefix, shard_index) + } + + fn get_cap(&self) -> Result, Self::Error> { + get_cap(&self.conn, self.table_prefix) + } + + fn put_cap(&mut self, cap: PrunableTree) -> Result<(), Self::Error> { + put_cap(&self.conn, self.table_prefix, cap) + } + + fn min_checkpoint_id(&self) -> Result, Self::Error> { + min_checkpoint_id(&self.conn, self.table_prefix) + } + + fn max_checkpoint_id(&self) -> Result, Self::Error> { + max_checkpoint_id(&self.conn, self.table_prefix) + } + + fn add_checkpoint( + &mut self, + checkpoint_id: Self::CheckpointId, + checkpoint: Checkpoint, + ) -> Result<(), Self::Error> { + let tx = self.conn.transaction().map_err(Error::Query)?; + add_checkpoint(&tx, self.table_prefix, checkpoint_id, checkpoint)?; + tx.commit().map_err(Error::Query) + } + + fn checkpoint_count(&self) -> Result { + checkpoint_count(&self.conn, self.table_prefix) + } + + fn get_checkpoint_at_depth( + &self, + checkpoint_depth: usize, + ) -> Result, Self::Error> { + get_checkpoint_at_depth(&self.conn, self.table_prefix, checkpoint_depth) + .map_err(Error::Query) + } + + fn get_checkpoint( + &self, + checkpoint_id: &Self::CheckpointId, + ) -> Result, Self::Error> { + get_checkpoint(&self.conn, self.table_prefix, *checkpoint_id) + } + + fn with_checkpoints(&mut self, limit: usize, callback: F) -> Result<(), Self::Error> + where + F: FnMut(&Self::CheckpointId, &Checkpoint) -> Result<(), Self::Error>, + { + let tx = self.conn.transaction().map_err(Error::Query)?; + with_checkpoints(&tx, self.table_prefix, limit, callback)?; + tx.commit().map_err(Error::Query) + } + + fn for_each_checkpoint(&self, limit: usize, callback: F) -> Result<(), Self::Error> + where + F: FnMut(&Self::CheckpointId, &Checkpoint) -> Result<(), Self::Error>, + { + let tx = self.conn.unchecked_transaction().map_err(Error::Query)?; + with_checkpoints(&tx, self.table_prefix, limit, callback)?; + // Here, we use `tx.rollback` as the semantics of this method is that the callback must + // not mutate the data store. + tx.rollback().map_err(Error::Query) + } + + fn update_checkpoint_with( + &mut self, + checkpoint_id: &Self::CheckpointId, + update: F, + ) -> Result + where + F: Fn(&mut Checkpoint) -> Result<(), Self::Error>, + { + let tx = self.conn.transaction().map_err(Error::Query)?; + let result = update_checkpoint_with(&tx, self.table_prefix, *checkpoint_id, update)?; + tx.commit().map_err(Error::Query)?; + Ok(result) + } + + fn remove_checkpoint(&mut self, checkpoint_id: &Self::CheckpointId) -> Result<(), Self::Error> { + let tx = self.conn.transaction().map_err(Error::Query)?; + remove_checkpoint(&tx, self.table_prefix, *checkpoint_id)?; + tx.commit().map_err(Error::Query) + } + + fn truncate_checkpoints_retaining( + &mut self, + checkpoint_id: &Self::CheckpointId, + ) -> Result<(), Self::Error> { + let tx = self.conn.transaction().map_err(Error::Query)?; + truncate_checkpoints_retaining(&tx, self.table_prefix, *checkpoint_id)?; + tx.commit().map_err(Error::Query) + } +} + +pub(crate) fn get_shard( + conn: &rusqlite::Connection, + table_prefix: &'static str, + shard_root_addr: Address, +) -> Result>, Error> { + conn.query_row( + &format!( + "SELECT shard_data, root_hash + FROM {}_tree_shards + WHERE shard_index = :shard_index", + table_prefix + ), + named_params![":shard_index": shard_root_addr.index()], + |row| Ok((row.get::<_, Vec>(0)?, row.get::<_, Option>>(1)?)), + ) + .optional() + .map_err(Error::Query)? + .map(|(shard_data, root_hash)| { + let shard_tree = read_shard(&mut Cursor::new(shard_data)).map_err(Error::Serialization)?; + let located_tree = + LocatedPrunableTree::from_parts(shard_root_addr, shard_tree).map_err(|e| { + Error::Serialization(io::Error::new( + io::ErrorKind::InvalidData, + format!("Tree contained invalid data at address {:?}", e), + )) + })?; + if let Some(root_hash_data) = root_hash { + let root_hash = H::read(Cursor::new(root_hash_data)).map_err(Error::Serialization)?; + Ok(located_tree.reannotate_root(Some(Arc::new(root_hash)))) + } else { + Ok(located_tree) + } + }) + .transpose() +} + +pub(crate) fn last_shard( + conn: &rusqlite::Connection, + table_prefix: &'static str, + shard_root_level: Level, +) -> Result>, Error> { + conn.query_row( + &format!( + "SELECT shard_index, shard_data + FROM {}_tree_shards + ORDER BY shard_index DESC + LIMIT 1", + table_prefix + ), + [], + |row| { + let shard_index: u64 = row.get(0)?; + let shard_data: Vec = row.get(1)?; + Ok((shard_index, shard_data)) + }, + ) + .optional() + .map_err(Error::Query)? + .map(|(shard_index, shard_data)| { + let shard_root = Address::from_parts(shard_root_level, shard_index); + let shard_tree = read_shard(&mut Cursor::new(shard_data)).map_err(Error::Serialization)?; + LocatedPrunableTree::from_parts(shard_root, shard_tree).map_err(|e| { + Error::Serialization(io::Error::new( + io::ErrorKind::InvalidData, + format!("Tree contained invalid data at address {:?}", e), + )) + }) + }) + .transpose() +} + +/// Returns an error iff the proposed insertion range +/// for the tree shards would create a discontinuity +/// in the database. +#[tracing::instrument(skip(conn))] +fn check_shard_discontinuity( + conn: &rusqlite::Connection, + table_prefix: &'static str, + proposed_insertion_range: Range, +) -> Result<(), Error> { + if let Ok((Some(stored_min), Some(stored_max))) = conn + .query_row( + &format!( + "SELECT MIN(shard_index), MAX(shard_index) FROM {}_tree_shards", + table_prefix + ), + [], + |row| { + let min = row.get::<_, Option>(0)?; + let max = row.get::<_, Option>(1)?; + Ok((min, max)) + }, + ) + .map_err(Error::Query) + { + // If the ranges overlap, or are directly adjacent, then we aren't creating a + // discontinuity. We can check this by comparing their start-inclusive, + // end-exclusive bounds: + // - If `cur_start == ins_end` then the proposed insertion range is immediately + // before the current shards. If `cur_start > ins_end` then there is a gap. + // - If `ins_start == cur_end` then the proposed insertion range is immediately + // after the current shards. If `ins_start > cur_end` then there is a gap. + let (cur_start, cur_end) = (stored_min, stored_max + 1); + let (ins_start, ins_end) = (proposed_insertion_range.start, proposed_insertion_range.end); + if cur_start > ins_end || ins_start > cur_end { + return Err(Error::SubtreeDiscontinuity { + attempted_insertion_range: proposed_insertion_range, + existing_range: cur_start..cur_end, + }); + } + } + + Ok(()) +} + +pub(crate) fn put_shard( + conn: &rusqlite::Transaction<'_>, + table_prefix: &'static str, + subtree: LocatedPrunableTree, +) -> Result<(), Error> { + let subtree_root_hash = subtree + .root() + .annotation() + .and_then(|ann| { + ann.as_ref().map(|rc| { + let mut root_hash = vec![]; + rc.write(&mut root_hash)?; + Ok(root_hash) + }) + }) + .transpose() + .map_err(Error::Serialization)?; + + let mut subtree_data = vec![]; + write_shard(&mut subtree_data, subtree.root()).map_err(Error::Serialization)?; + + let shard_index = subtree.root_addr().index(); + + check_shard_discontinuity(conn, table_prefix, shard_index..shard_index + 1)?; + + let mut stmt_put_shard = conn + .prepare_cached(&format!( + "INSERT INTO {}_tree_shards (shard_index, root_hash, shard_data) + VALUES (:shard_index, :root_hash, :shard_data) + ON CONFLICT (shard_index) DO UPDATE + SET root_hash = :root_hash, + shard_data = :shard_data", + table_prefix + )) + .map_err(Error::Query)?; + + stmt_put_shard + .execute(named_params![ + ":shard_index": shard_index, + ":root_hash": subtree_root_hash, + ":shard_data": subtree_data + ]) + .map_err(Error::Query)?; + + Ok(()) +} + +pub(crate) fn get_shard_roots( + conn: &rusqlite::Connection, + table_prefix: &'static str, + shard_root_level: Level, +) -> Result, Error> { + let mut stmt = conn + .prepare(&format!( + "SELECT shard_index FROM {}_tree_shards ORDER BY shard_index", + table_prefix + )) + .map_err(Error::Query)?; + let mut rows = stmt.query([]).map_err(Error::Query)?; + + let mut res = vec![]; + while let Some(row) = rows.next().map_err(Error::Query)? { + res.push(Address::from_parts( + shard_root_level, + row.get(0).map_err(Error::Query)?, + )); + } + Ok(res) +} + +pub(crate) fn truncate_shards( + conn: &rusqlite::Connection, + table_prefix: &'static str, + shard_index: u64, +) -> Result<(), Error> { + conn.execute( + &format!( + "DELETE FROM {}_tree_shards WHERE shard_index >= ?", + table_prefix + ), + [shard_index], + ) + .map_err(Error::Query) + .map(|_| ()) +} + +#[tracing::instrument(skip(conn))] +pub(crate) fn get_cap( + conn: &rusqlite::Connection, + table_prefix: &'static str, +) -> Result, Error> { + conn.query_row( + &format!("SELECT cap_data FROM {}_tree_cap", table_prefix), + [], + |row| row.get::<_, Vec>(0), + ) + .optional() + .map_err(Error::Query)? + .map_or_else( + || Ok(PrunableTree::empty()), + |cap_data| read_shard(&mut Cursor::new(cap_data)).map_err(Error::Serialization), + ) +} + +#[tracing::instrument(skip(conn, cap))] +pub(crate) fn put_cap( + conn: &rusqlite::Connection, + table_prefix: &'static str, + cap: PrunableTree, +) -> Result<(), Error> { + let mut stmt = conn + .prepare_cached(&format!( + "INSERT INTO {}_tree_cap (cap_id, cap_data) + VALUES (0, :cap_data) + ON CONFLICT (cap_id) DO UPDATE + SET cap_data = :cap_data", + table_prefix + )) + .map_err(Error::Query)?; + + let mut cap_data = vec![]; + write_shard(&mut cap_data, &cap).map_err(Error::Serialization)?; + stmt.execute([cap_data]).map_err(Error::Query)?; + + Ok(()) +} + +pub(crate) fn min_checkpoint_id( + conn: &rusqlite::Connection, + table_prefix: &'static str, +) -> Result, Error> { + conn.query_row( + &format!( + "SELECT MIN(checkpoint_id) FROM {}_tree_checkpoints", + table_prefix + ), + [], + |row| { + row.get::<_, Option>(0) + .map(|opt| opt.map(BlockHeight::from)) + }, + ) + .map_err(Error::Query) +} + +pub(crate) fn max_checkpoint_id( + conn: &rusqlite::Connection, + table_prefix: &'static str, +) -> Result, Error> { + conn.query_row( + &format!( + "SELECT MAX(checkpoint_id) FROM {}_tree_checkpoints", + table_prefix + ), + [], + |row| { + row.get::<_, Option>(0) + .map(|opt| opt.map(BlockHeight::from)) + }, + ) + .map_err(Error::Query) +} + +pub(crate) fn add_checkpoint( + conn: &rusqlite::Transaction<'_>, + table_prefix: &'static str, + checkpoint_id: BlockHeight, + checkpoint: Checkpoint, +) -> Result<(), Error> { + let extant_tree_state = conn + .query_row( + &format!( + "SELECT position FROM {}_tree_checkpoints WHERE checkpoint_id = :checkpoint_id", + table_prefix + ), + named_params![":checkpoint_id": u32::from(checkpoint_id),], + |row| { + row.get::<_, Option>(0).map(|opt| { + opt.map_or_else( + || TreeState::Empty, + |pos| TreeState::AtPosition(Position::from(pos)), + ) + }) + }, + ) + .optional() + .map_err(Error::Query)?; + + match extant_tree_state { + Some(current) => { + if current != checkpoint.tree_state() { + // If the checkpoint position for a given checkpoint identifier has changed, we treat + // this as an error because the wallet should have detected a chain reorg and truncated + // the tree. + Err(Error::CheckpointConflict { + checkpoint_id, + checkpoint, + extant_tree_state: current, + extant_marks_removed: None, + }) + } else { + // if the existing spends are the same, we can skip the insert; if the + // existing spends have changed, this is also a conflict. + let marks_removed = get_marks_removed(conn, table_prefix, checkpoint_id)?; + if &marks_removed == checkpoint.marks_removed() { + Ok(()) + } else { + Err(Error::CheckpointConflict { + checkpoint_id, + checkpoint, + extant_tree_state: current, + extant_marks_removed: Some(marks_removed), + }) + } + } + } + None => { + let mut stmt_insert_checkpoint = conn + .prepare_cached(&format!( + "INSERT INTO {}_tree_checkpoints (checkpoint_id, position) + VALUES (:checkpoint_id, :position)", + table_prefix + )) + .map_err(Error::Query)?; + + stmt_insert_checkpoint + .execute(named_params![ + ":checkpoint_id": u32::from(checkpoint_id), + ":position": checkpoint.position().map(u64::from) + ]) + .map_err(Error::Query)?; + + let mut stmt_insert_mark_removed = conn + .prepare_cached(&format!( + "INSERT INTO {}_tree_checkpoint_marks_removed (checkpoint_id, mark_removed_position) + VALUES (:checkpoint_id, :position)", + table_prefix + )) + .map_err(Error::Query)?; + + for pos in checkpoint.marks_removed() { + stmt_insert_mark_removed + .execute(named_params![ + ":checkpoint_id": u32::from(checkpoint_id), + ":position": u64::from(*pos) + ]) + .map_err(Error::Query)?; + } + + Ok(()) + } + } +} + +pub(crate) fn checkpoint_count( + conn: &rusqlite::Connection, + table_prefix: &'static str, +) -> Result { + conn.query_row( + &format!("SELECT COUNT(*) FROM {}_tree_checkpoints", table_prefix), + [], + |row| row.get::<_, usize>(0), + ) + .map_err(Error::Query) +} + +fn get_marks_removed( + conn: &rusqlite::Connection, + table_prefix: &'static str, + checkpoint_id: BlockHeight, +) -> Result, Error> { + let mut stmt = conn + .prepare_cached(&format!( + "SELECT mark_removed_position + FROM {}_tree_checkpoint_marks_removed + WHERE checkpoint_id = ?", + table_prefix + )) + .map_err(Error::Query)?; + let mark_removed_rows = stmt + .query([u32::from(checkpoint_id)]) + .map_err(Error::Query)?; + + mark_removed_rows + .mapped(|row| row.get::<_, u64>(0).map(Position::from)) + .collect::, _>>() + .map_err(Error::Query) +} + +pub(crate) fn get_checkpoint( + conn: &rusqlite::Connection, + table_prefix: &'static str, + checkpoint_id: BlockHeight, +) -> Result, Error> { + let checkpoint_position = conn + .query_row( + &format!( + "SELECT position + FROM {}_tree_checkpoints + WHERE checkpoint_id = ?", + table_prefix + ), + [u32::from(checkpoint_id)], + |row| { + row.get::<_, Option>(0) + .map(|opt| opt.map(Position::from)) + }, + ) + .optional() + .map_err(Error::Query)?; + + checkpoint_position + .map(|pos_opt| { + Ok(Checkpoint::from_parts( + pos_opt.map_or(TreeState::Empty, TreeState::AtPosition), + get_marks_removed(conn, table_prefix, checkpoint_id)?, + )) + }) + .transpose() +} + +pub(crate) fn get_max_checkpointed_height( + conn: &rusqlite::Connection, + table_prefix: &'static str, + chain_tip_height: BlockHeight, + min_confirmations: NonZeroU32, +) -> Result, rusqlite::Error> { + let max_checkpoint_height = + u32::from(chain_tip_height).saturating_sub(u32::from(min_confirmations) - 1); + + // We exclude from consideration all checkpoints having heights greater than the maximum + // checkpoint height. The checkpoint depth is the number of excluded checkpoints + 1. + conn.query_row( + &format!( + "SELECT checkpoint_id + FROM {}_tree_checkpoints + WHERE checkpoint_id <= :max_checkpoint_height + ORDER BY checkpoint_id DESC + LIMIT 1", + table_prefix + ), + named_params![":max_checkpoint_height": max_checkpoint_height], + |row| row.get::<_, u32>(0).map(BlockHeight::from), + ) + .optional() +} + +pub(crate) fn get_checkpoint_at_depth( + conn: &rusqlite::Connection, + table_prefix: &'static str, + checkpoint_depth: usize, +) -> Result, rusqlite::Error> { + let checkpoint_parts = conn + .query_row( + &format!( + "SELECT checkpoint_id, position + FROM {}_tree_checkpoints + ORDER BY checkpoint_id DESC + LIMIT 1 + OFFSET :offset", + table_prefix + ), + named_params![":offset": checkpoint_depth], + |row| { + let checkpoint_id: u32 = row.get(0)?; + let position: Option = row.get(1)?; + Ok(( + BlockHeight::from(checkpoint_id), + position.map(Position::from), + )) + }, + ) + .optional()?; + + checkpoint_parts + .map(|(checkpoint_id, pos_opt)| { + let mut stmt = conn.prepare_cached(&format!( + "SELECT mark_removed_position + FROM {}_tree_checkpoint_marks_removed + WHERE checkpoint_id = ?", + table_prefix + ))?; + let mark_removed_rows = stmt.query([u32::from(checkpoint_id)])?; + + let marks_removed = mark_removed_rows + .mapped(|row| row.get::<_, u64>(0).map(Position::from)) + .collect::, _>>()?; + + Ok(( + checkpoint_id, + Checkpoint::from_parts( + pos_opt.map_or(TreeState::Empty, TreeState::AtPosition), + marks_removed, + ), + )) + }) + .transpose() +} + +pub(crate) fn with_checkpoints( + conn: &rusqlite::Transaction<'_>, + table_prefix: &'static str, + limit: usize, + mut callback: F, +) -> Result<(), Error> +where + F: FnMut(&BlockHeight, &Checkpoint) -> Result<(), Error>, +{ + let mut stmt_get_checkpoints = conn + .prepare_cached(&format!( + "SELECT checkpoint_id, position + FROM {}_tree_checkpoints + ORDER BY position + LIMIT :limit", + table_prefix + )) + .map_err(Error::Query)?; + + let mut stmt_get_checkpoint_marks_removed = conn + .prepare_cached(&format!( + "SELECT mark_removed_position + FROM {}_tree_checkpoint_marks_removed + WHERE checkpoint_id = :checkpoint_id", + table_prefix + )) + .map_err(Error::Query)?; + + let mut rows = stmt_get_checkpoints + .query(named_params![":limit": limit]) + .map_err(Error::Query)?; + + while let Some(row) = rows.next().map_err(Error::Query)? { + let checkpoint_id = row.get::<_, u32>(0).map_err(Error::Query)?; + let tree_state = row + .get::<_, Option>(1) + .map(|opt| opt.map_or_else(|| TreeState::Empty, |p| TreeState::AtPosition(p.into()))) + .map_err(Error::Query)?; + + let mark_removed_rows = stmt_get_checkpoint_marks_removed + .query(named_params![":checkpoint_id": checkpoint_id]) + .map_err(Error::Query)?; + + let marks_removed = mark_removed_rows + .mapped(|row| row.get::<_, u64>(0).map(Position::from)) + .collect::, _>>() + .map_err(Error::Query)?; + + callback( + &BlockHeight::from(checkpoint_id), + &Checkpoint::from_parts(tree_state, marks_removed), + )? + } + + Ok(()) +} + +pub(crate) fn update_checkpoint_with( + conn: &rusqlite::Transaction<'_>, + table_prefix: &'static str, + checkpoint_id: BlockHeight, + update: F, +) -> Result +where + F: Fn(&mut Checkpoint) -> Result<(), Error>, +{ + if let Some(mut c) = get_checkpoint(conn, table_prefix, checkpoint_id)? { + update(&mut c)?; + remove_checkpoint(conn, table_prefix, checkpoint_id)?; + add_checkpoint(conn, table_prefix, checkpoint_id, c)?; + Ok(true) + } else { + Ok(false) + } +} + +pub(crate) fn remove_checkpoint( + conn: &rusqlite::Transaction<'_>, + table_prefix: &'static str, + checkpoint_id: BlockHeight, +) -> Result<(), Error> { + // cascading delete here obviates the need to manually delete from + // `tree_checkpoint_marks_removed` + let mut stmt_delete_checkpoint = conn + .prepare_cached(&format!( + "DELETE FROM {}_tree_checkpoints + WHERE checkpoint_id = :checkpoint_id", + table_prefix + )) + .map_err(Error::Query)?; + + stmt_delete_checkpoint + .execute(named_params![":checkpoint_id": u32::from(checkpoint_id),]) + .map_err(Error::Query)?; + + Ok(()) +} + +pub(crate) fn truncate_checkpoints_retaining( + conn: &rusqlite::Transaction<'_>, + table_prefix: &'static str, + checkpoint_id: BlockHeight, +) -> Result<(), Error> { + // cascading delete here obviates the need to manually delete from + // `_tree_checkpoint_marks_removed` + conn.execute( + &format!( + "DELETE FROM {}_tree_checkpoints WHERE checkpoint_id > ?", + table_prefix + ), + [u32::from(checkpoint_id)], + ) + .map_err(Error::Query)?; + + // we do however need to manually delete any marks associated with the retained checkpoint + conn.execute( + &format!( + "DELETE FROM {}_tree_checkpoint_marks_removed WHERE checkpoint_id = ?", + table_prefix + ), + [u32::from(checkpoint_id)], + ) + .map_err(Error::Query)?; + + Ok(()) +} + +#[tracing::instrument(skip(conn, roots))] +pub(crate) fn put_shard_roots< + H: Hashable + HashSer + Clone + Eq, + const DEPTH: u8, + const SHARD_HEIGHT: u8, +>( + conn: &rusqlite::Transaction<'_>, + table_prefix: &'static str, + start_index: u64, + roots: &[CommitmentTreeRoot], +) -> Result<(), ShardTreeError> { + if roots.is_empty() { + // nothing to do + return Ok(()); + } + + // We treat the cap as a tree with `DEPTH - SHARD_HEIGHT` levels, so that we can make a + // batch insertion of root data using `Position::from(start_index)` as the starting position + // and treating the roots as level-0 leaves. + #[derive(Clone, Debug, PartialEq, Eq)] + struct LevelShifter(H); + impl Hashable for LevelShifter { + fn empty_leaf() -> Self { + Self(H::empty_root(SHARD_HEIGHT.into())) + } + + fn combine(level: Level, a: &Self, b: &Self) -> Self { + Self(H::combine(level + SHARD_HEIGHT, &a.0, &b.0)) + } + + fn empty_root(level: Level) -> Self + where + Self: Sized, + { + Self(H::empty_root(level + SHARD_HEIGHT)) + } + } + impl HashSer for LevelShifter { + fn read(reader: R) -> io::Result + where + Self: Sized, + { + H::read(reader).map(Self) + } + + fn write(&self, writer: W) -> io::Result<()> { + self.0.write(writer) + } + } + + let cap = LocatedTree::from_parts( + Address::from_parts((DEPTH - SHARD_HEIGHT).into(), 0), + get_cap::>(conn, table_prefix) + .map_err(ShardTreeError::Storage)?, + ) + .map_err(|e| { + ShardTreeError::Storage(Error::Serialization(io::Error::new( + io::ErrorKind::InvalidData, + format!("Note commitment tree cap was invalid at address {:?}", e), + ))) + })?; + + let insert_into_cap = tracing::info_span!("insert_into_cap").entered(); + let cap_result = cap + .batch_insert::<(), _>( + Position::from(start_index), + roots + .iter() + .map(|r| (LevelShifter(r.root_hash().clone()), Retention::Reference)), + ) + .map_err(ShardTreeError::Insert)? + .expect("slice of inserted roots was verified to be nonempty"); + drop(insert_into_cap); + + put_cap(conn, table_prefix, cap_result.subtree.take_root()).map_err(ShardTreeError::Storage)?; + + check_shard_discontinuity( + conn, + table_prefix, + start_index..start_index + (roots.len() as u64), + ) + .map_err(ShardTreeError::Storage)?; + + // We want to avoid deserializing the subtree just to annotate its root node, so we simply + // cache the downloaded root alongside of any already-persisted subtree. We will update the + // subtree data itself by reannotating the root node of the tree, handling conflicts, at + // the time that we deserialize the tree. + let mut stmt = conn + .prepare_cached(&format!( + "INSERT INTO {}_tree_shards (shard_index, subtree_end_height, root_hash, shard_data) + VALUES (:shard_index, :subtree_end_height, :root_hash, :shard_data) + ON CONFLICT (shard_index) DO UPDATE + SET subtree_end_height = :subtree_end_height, root_hash = :root_hash", + table_prefix + )) + .map_err(|e| ShardTreeError::Storage(Error::Query(e)))?; + + let put_roots = tracing::info_span!("write_shards").entered(); + for (root, i) in roots.iter().zip(0u64..) { + // The `shard_data` value will only be used in the case that no tree already exists. + let mut shard_data: Vec = vec![]; + let tree = PrunableTree::leaf((root.root_hash().clone(), RetentionFlags::EPHEMERAL)); + write_shard(&mut shard_data, &tree) + .map_err(|e| ShardTreeError::Storage(Error::Serialization(e)))?; + + let mut root_hash_data: Vec = vec![]; + root.root_hash() + .write(&mut root_hash_data) + .map_err(|e| ShardTreeError::Storage(Error::Serialization(e)))?; + + stmt.execute(named_params![ + ":shard_index": start_index + i, + ":subtree_end_height": u32::from(root.subtree_end_height()), + ":root_hash": root_hash_data, + ":shard_data": shard_data, + ]) + .map_err(|e| ShardTreeError::Storage(Error::Query(e)))?; + } + drop(put_roots); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use tempfile::NamedTempFile; + + use incrementalmerkletree::{Marking, Position, Retention}; + use incrementalmerkletree_testing::{ + check_append, check_checkpoint_rewind, check_remove_mark, check_rewind_remove_mark, + check_root_hashes, check_witness_consistency, check_witnesses, + }; + use shardtree::ShardTree; + use zcash_client_backend::data_api::{ + chain::CommitmentTreeRoot, + testing::{pool::ShieldedPoolTester, sapling::SaplingPoolTester}, + }; + use zcash_protocol::consensus::{BlockHeight, Network}; + + use super::SqliteShardStore; + use crate::{testing::pool::ShieldedPoolPersistence, wallet::init::init_wallet_db, WalletDb}; + + fn new_tree( + m: usize, + ) -> ShardTree, 4, 3> { + let data_file = NamedTempFile::new().unwrap(); + let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap(); + data_file.keep().unwrap(); + + init_wallet_db(&mut db_data, None).unwrap(); + let store = + SqliteShardStore::<_, String, 3>::from_connection(db_data.conn, T::TABLES_PREFIX) + .unwrap(); + ShardTree::new(store, m) + } + + #[cfg(feature = "orchard")] + mod orchard { + use super::new_tree; + use zcash_client_backend::data_api::testing::orchard::OrchardPoolTester; + + #[test] + fn append() { + super::check_append(new_tree::); + } + + #[test] + fn root_hashes() { + super::check_root_hashes(new_tree::); + } + + #[test] + fn witnesses() { + super::check_witnesses(new_tree::); + } + + #[test] + fn witness_consistency() { + super::check_witness_consistency(new_tree::); + } + + #[test] + fn checkpoint_rewind() { + super::check_checkpoint_rewind(new_tree::); + } + + #[test] + fn remove_mark() { + super::check_remove_mark(new_tree::); + } + + #[test] + fn rewind_remove_mark() { + super::check_rewind_remove_mark(new_tree::); + } + + #[test] + fn put_shard_roots() { + super::put_shard_roots::() + } + } + + #[test] + fn sapling_append() { + check_append(new_tree::); + } + + #[test] + fn sapling_root_hashes() { + check_root_hashes(new_tree::); + } + + #[test] + fn sapling_witnesses() { + check_witnesses(new_tree::); + } + + #[test] + fn sapling_witness_consistency() { + check_witness_consistency(new_tree::); + } + + #[test] + fn sapling_checkpoint_rewind() { + check_checkpoint_rewind(new_tree::); + } + + #[test] + fn sapling_remove_mark() { + check_remove_mark(new_tree::); + } + + #[test] + fn sapling_rewind_remove_mark() { + check_rewind_remove_mark(new_tree::); + } + + #[test] + fn sapling_put_shard_roots() { + put_shard_roots::() + } + + fn put_shard_roots() { + let data_file = NamedTempFile::new().unwrap(); + let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap(); + data_file.keep().unwrap(); + + init_wallet_db(&mut db_data, None).unwrap(); + let tx = db_data.conn.transaction().unwrap(); + let store = + SqliteShardStore::<_, String, 3>::from_connection(&tx, T::TABLES_PREFIX).unwrap(); + + // introduce some roots + let roots = (0u32..4) + .map(|idx| { + CommitmentTreeRoot::from_parts( + BlockHeight::from((idx + 1) * 3), + if idx == 3 { + "abcdefgh".to_string() + } else { + idx.to_string() + }, + ) + }) + .collect::>(); + super::put_shard_roots::<_, 6, 3>(store.conn, T::TABLES_PREFIX, 0, &roots).unwrap(); + + // simulate discovery of a note + let mut tree = ShardTree::<_, 6, 3>::new(store, 10); + let checkpoint_height = BlockHeight::from(3); + tree.batch_insert( + Position::from(24), + ('a'..='h').map(|c| { + ( + c.to_string(), + match c { + 'c' => Retention::Marked, + 'h' => Retention::Checkpoint { + id: checkpoint_height, + marking: Marking::None, + }, + _ => Retention::Ephemeral, + }, + ) + }), + ) + .unwrap(); + + // construct a witness for the note + let witness = tree + .witness_at_checkpoint_id(Position::from(26), &checkpoint_height) + .unwrap(); + assert_eq!( + witness + .expect("an anchor exists at the expected checkpoint height") + .path_elems(), + &[ + "d", + "ab", + "efgh", + "2", + "01", + "________________________________" + ] + ); + } +} diff --git a/zcash_client_sqlite/src/wallet/common.rs b/zcash_client_sqlite/src/wallet/common.rs new file mode 100644 index 0000000000..b3fdd569ce --- /dev/null +++ b/zcash_client_sqlite/src/wallet/common.rs @@ -0,0 +1,423 @@ +//! Functions common to Sapling and Orchard support in the wallet. + +use rusqlite::{named_params, types::Value, Connection, Row}; +use std::{num::NonZeroU64, rc::Rc}; + +use zcash_client_backend::{ + data_api::{NoteFilter, PoolMeta}, + wallet::ReceivedNote, +}; +use zcash_primitives::transaction::TxId; +use zcash_protocol::{ + consensus::{self, BlockHeight}, + value::{BalanceError, Zatoshis}, + PoolType, ShieldedProtocol, +}; + +use super::wallet_birthday; +use crate::{ + error::SqliteClientError, wallet::pool_code, AccountUuid, ReceivedNoteId, SAPLING_TABLES_PREFIX, +}; + +#[cfg(feature = "orchard")] +use crate::ORCHARD_TABLES_PREFIX; + +pub(crate) fn per_protocol_names( + protocol: ShieldedProtocol, +) -> (&'static str, &'static str, &'static str) { + match protocol { + ShieldedProtocol::Sapling => (SAPLING_TABLES_PREFIX, "output_index", "rcm"), + #[cfg(feature = "orchard")] + ShieldedProtocol::Orchard => (ORCHARD_TABLES_PREFIX, "action_index", "rho, rseed"), + #[cfg(not(feature = "orchard"))] + ShieldedProtocol::Orchard => { + unreachable!("Should never be called unless the `orchard` feature is enabled") + } + } +} + +fn unscanned_tip_exists( + conn: &Connection, + anchor_height: BlockHeight, + table_prefix: &'static str, +) -> Result { + // v_sapling_shard_unscanned_ranges only returns ranges ending on or after wallet birthday, so + // we don't need to refer to the birthday in this query. + conn.query_row( + &format!( + "SELECT EXISTS ( + SELECT 1 FROM v_{table_prefix}_shard_unscanned_ranges range + WHERE range.block_range_start <= :anchor_height + AND :anchor_height BETWEEN + range.subtree_start_height + AND IFNULL(range.subtree_end_height, :anchor_height) + )" + ), + named_params![":anchor_height": u32::from(anchor_height),], + |row| row.get::<_, bool>(0), + ) +} + +// The `clippy::let_and_return` lint is explicitly allowed here because a bug in Clippy +// (https://github.com/rust-lang/rust-clippy/issues/11308) means it fails to identify that the `result` temporary +// is required in order to resolve the borrows involved in the `query_and_then` call. +#[allow(clippy::let_and_return)] +pub(crate) fn get_spendable_note( + conn: &Connection, + params: &P, + txid: &TxId, + index: u32, + protocol: ShieldedProtocol, + to_spendable_note: F, +) -> Result>, SqliteClientError> +where + F: Fn(&P, &Row) -> Result>, SqliteClientError>, +{ + let (table_prefix, index_col, note_reconstruction_cols) = per_protocol_names(protocol); + let result = conn.query_row_and_then( + &format!( + "SELECT rn.id, txid, {index_col}, + diversifier, value, {note_reconstruction_cols}, commitment_tree_position, + accounts.ufvk, recipient_key_scope + FROM {table_prefix}_received_notes rn + INNER JOIN accounts ON accounts.id = rn.account_id + INNER JOIN transactions ON transactions.id_tx = rn.tx + WHERE txid = :txid + AND transactions.block IS NOT NULL + AND {index_col} = :output_index + AND accounts.ufvk IS NOT NULL + AND recipient_key_scope IS NOT NULL + AND nf IS NOT NULL + AND commitment_tree_position IS NOT NULL + AND rn.id NOT IN ( + SELECT {table_prefix}_received_note_id + FROM {table_prefix}_received_note_spends + JOIN transactions stx ON stx.id_tx = transaction_id + WHERE stx.block IS NOT NULL -- the spending tx is mined + OR stx.expiry_height IS NULL -- the spending tx will not expire + )" + ), + named_params![ + ":txid": txid.as_ref(), + ":output_index": index, + ], + |row| to_spendable_note(params, row), + ); + + // `OptionalExtension` doesn't work here because the error type of `Result` is already + // `SqliteClientError` + match result { + Ok(r) => Ok(r), + Err(SqliteClientError::DbError(rusqlite::Error::QueryReturnedNoRows)) => Ok(None), + Err(e) => Err(e), + } +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn select_spendable_notes( + conn: &Connection, + params: &P, + account: AccountUuid, + target_value: Zatoshis, + anchor_height: BlockHeight, + exclude: &[ReceivedNoteId], + protocol: ShieldedProtocol, + to_spendable_note: F, +) -> Result>, SqliteClientError> +where + F: Fn(&P, &Row) -> Result>, SqliteClientError>, +{ + let birthday_height = match wallet_birthday(conn)? { + Some(birthday) => birthday, + None => { + // the wallet birthday can only be unknown if there are no accounts in the wallet; in + // such a case, the wallet has no notes to spend. + return Ok(vec![]); + } + }; + + let (table_prefix, index_col, note_reconstruction_cols) = per_protocol_names(protocol); + if unscanned_tip_exists(conn, anchor_height, table_prefix)? { + return Ok(vec![]); + } + + // The goal of this SQL statement is to select the oldest notes until the required + // value has been reached. + // 1) Use a window function to create a view of all notes, ordered from oldest to + // newest, with an additional column containing a running sum: + // - Unspent notes accumulate the values of all unspent notes in that note's + // account, up to itself. + // - Spent notes accumulate the values of all notes in the transaction they were + // spent in, up to itself. + // + // 2) Select all unspent notes in the desired account, along with their running sum. + // + // 3) Select all notes for which the running sum was less than the required value, as + // well as a single note for which the sum was greater than or equal to the + // required value, bringing the sum of all selected notes across the threshold. + let mut stmt_select_notes = conn.prepare_cached( + &format!( + "WITH eligible AS ( + SELECT + {table_prefix}_received_notes.id AS id, txid, {index_col}, + diversifier, value, {note_reconstruction_cols}, commitment_tree_position, + SUM(value) OVER (ROWS UNBOUNDED PRECEDING) AS so_far, + accounts.ufvk as ufvk, recipient_key_scope + FROM {table_prefix}_received_notes + INNER JOIN accounts + ON accounts.id = {table_prefix}_received_notes.account_id + INNER JOIN transactions + ON transactions.id_tx = {table_prefix}_received_notes.tx + WHERE accounts.uuid = :account_uuid + AND {table_prefix}_received_notes.account_id = accounts.id + AND value > 5000 -- FIXME #1316, allow selection of dust inputs + AND accounts.ufvk IS NOT NULL + AND recipient_key_scope IS NOT NULL + AND nf IS NOT NULL + AND commitment_tree_position IS NOT NULL + AND transactions.block <= :anchor_height + AND {table_prefix}_received_notes.id NOT IN rarray(:exclude) + AND {table_prefix}_received_notes.id NOT IN ( + SELECT {table_prefix}_received_note_id + FROM {table_prefix}_received_note_spends + JOIN transactions stx ON stx.id_tx = transaction_id + WHERE stx.block IS NOT NULL -- the spending tx is mined + OR stx.expiry_height IS NULL -- the spending tx will not expire + OR stx.expiry_height > :anchor_height -- the spending tx is unexpired + ) + AND NOT EXISTS ( + SELECT 1 FROM v_{table_prefix}_shard_unscanned_ranges unscanned + -- select all the unscanned ranges involving the shard containing this note + WHERE {table_prefix}_received_notes.commitment_tree_position >= unscanned.start_position + AND {table_prefix}_received_notes.commitment_tree_position < unscanned.end_position_exclusive + -- exclude unscanned ranges that start above the anchor height (they don't affect spendability) + AND unscanned.block_range_start <= :anchor_height + -- exclude unscanned ranges that end below the wallet birthday + AND unscanned.block_range_end > :wallet_birthday + ) + ) + SELECT id, txid, {index_col}, + diversifier, value, {note_reconstruction_cols}, commitment_tree_position, + ufvk, recipient_key_scope + FROM eligible WHERE so_far < :target_value + UNION + SELECT id, txid, {index_col}, + diversifier, value, {note_reconstruction_cols}, commitment_tree_position, + ufvk, recipient_key_scope + FROM (SELECT * from eligible WHERE so_far >= :target_value LIMIT 1)", + ) + )?; + + let excluded: Vec = exclude + .iter() + .filter_map(|ReceivedNoteId(p, n)| { + if *p == protocol { + Some(Value::from(*n)) + } else { + None + } + }) + .collect(); + let excluded_ptr = Rc::new(excluded); + + let notes = stmt_select_notes.query_and_then( + named_params![ + ":account_uuid": account.0, + ":anchor_height": &u32::from(anchor_height), + ":target_value": &u64::from(target_value), + ":exclude": &excluded_ptr, + ":wallet_birthday": u32::from(birthday_height) + ], + |r| to_spendable_note(params, r), + )?; + + notes + .filter_map(|r| r.transpose()) + .collect::>() +} + +pub(crate) fn spendable_notes_meta( + conn: &rusqlite::Connection, + protocol: ShieldedProtocol, + chain_tip_height: BlockHeight, + account: AccountUuid, + filter: &NoteFilter, + exclude: &[ReceivedNoteId], +) -> Result, SqliteClientError> { + let (table_prefix, _, _) = per_protocol_names(protocol); + + let excluded: Vec = exclude + .iter() + .filter_map(|ReceivedNoteId(p, n)| { + if *p == protocol { + Some(Value::from(*n)) + } else { + None + } + }) + .collect(); + let excluded_ptr = Rc::new(excluded); + + fn zatoshis(value: i64) -> Result { + Zatoshis::from_nonnegative_i64(value).map_err(|_| { + SqliteClientError::CorruptedData(format!("Negative received note value: {}", value)) + }) + } + + let run_selection = |min_value| { + conn.query_row_and_then::<_, SqliteClientError, _, _>( + &format!( + "SELECT COUNT(*), SUM(rn.value) + FROM {table_prefix}_received_notes rn + INNER JOIN accounts a ON a.id = rn.account_id + INNER JOIN transactions ON transactions.id_tx = rn.tx + WHERE a.uuid = :account_uuid + AND a.ufvk IS NOT NULL + AND rn.value >= :min_value + AND transactions.mined_height IS NOT NULL + AND rn.id NOT IN rarray(:exclude) + AND rn.id NOT IN ( + SELECT {table_prefix}_received_note_id + FROM {table_prefix}_received_note_spends rns + JOIN transactions stx ON stx.id_tx = rns.transaction_id + WHERE stx.block IS NOT NULL -- the spending tx is mined + OR stx.expiry_height IS NULL -- the spending tx will not expire + OR stx.expiry_height > :chain_tip_height -- the spending tx is unexpired + )" + ), + named_params![ + ":account_uuid": account.0, + ":min_value": u64::from(min_value), + ":exclude": &excluded_ptr, + ":chain_tip_height": u32::from(chain_tip_height) + ], + |row| { + Ok(( + row.get::<_, usize>(0)?, + row.get::<_, Option>(1)?.map(zatoshis).transpose()?, + )) + }, + ) + }; + + // Evaluates the provided note filter conditions against the wallet database in order to + // determine the minimum value of notes to be produced by note splitting. + fn min_note_value( + conn: &rusqlite::Connection, + account: AccountUuid, + filter: &NoteFilter, + chain_tip_height: BlockHeight, + ) -> Result, SqliteClientError> { + match filter { + NoteFilter::ExceedsMinValue(v) => Ok(Some(*v)), + NoteFilter::ExceedsPriorSendPercentile(n) => { + let mut bucket_query = conn.prepare( + "WITH bucketed AS ( + SELECT s.value, NTILE(10) OVER (ORDER BY s.value) AS bucket_index + FROM sent_notes s + JOIN transactions t ON s.tx = t.id_tx + JOIN accounts a on a.id = s.from_account_id + WHERE a.uuid = :account_uuid + -- only count mined transactions + AND t.mined_height IS NOT NULL + -- exclude change and account-internal sends + AND (s.to_account_id IS NULL OR s.from_account_id != s.to_account_id) + ) + SELECT MAX(value) as value + FROM bucketed + GROUP BY bucket_index + ORDER BY bucket_index", + )?; + + let bucket_maxima = bucket_query + .query_and_then::<_, SqliteClientError, _, _>( + named_params![":account_uuid": account.0], + |row| { + Zatoshis::from_nonnegative_i64(row.get::<_, i64>(0)?).map_err(|_| { + SqliteClientError::CorruptedData(format!( + "Negative received note value: {}", + n.value() + )) + }) + }, + )? + .collect::, _>>()?; + + // Pick a bucket index by scaling the requested percentile to the number of buckets + let i = (bucket_maxima.len() * usize::from(*n) / 100).saturating_sub(1); + Ok(bucket_maxima.get(i).copied()) + } + NoteFilter::ExceedsBalancePercentage(p) => { + let balance = conn.query_row_and_then::<_, SqliteClientError, _, _>( + "SELECT SUM(rn.value) + FROM v_received_outputs rn + INNER JOIN accounts a ON a.id = rn.account_id + INNER JOIN transactions ON transactions.id_tx = rn.transaction_id + WHERE a.uuid = :account_uuid + AND a.ufvk IS NOT NULL + AND transactions.mined_height IS NOT NULL + AND rn.pool != :transparent_pool + AND (rn.pool, rn.id_within_pool_table) NOT IN ( + SELECT rns.pool, rns.received_output_id + FROM v_received_output_spends rns + JOIN transactions stx ON stx.id_tx = rns.transaction_id + WHERE ( + stx.block IS NOT NULL -- the spending tx is mined + OR stx.expiry_height IS NULL -- the spending tx will not expire + OR stx.expiry_height > :chain_tip_height -- the spending tx is unexpired + ) + )", + named_params![ + ":account_uuid": account.0, + ":chain_tip_height": u32::from(chain_tip_height), + ":transparent_pool": pool_code(PoolType::Transparent) + ], + |row| row.get::<_, Option>(0)?.map(zatoshis).transpose(), + )?; + + Ok(match balance { + None => None, + Some(b) => { + let numerator = (b * u64::from(p.value())).ok_or(BalanceError::Overflow)?; + Some(numerator / NonZeroU64::new(100).expect("Constant is nonzero.")) + } + }) + } + NoteFilter::Combine(a, b) => { + // All the existing note selectors set lower bounds on note value, so the "and" + // operation is just taking the maximum of the two lower bounds. + let a_min_value = min_note_value(conn, account, a.as_ref(), chain_tip_height)?; + let b_min_value = min_note_value(conn, account, b.as_ref(), chain_tip_height)?; + Ok(a_min_value + .zip(b_min_value) + .map(|(av, bv)| std::cmp::max(av, bv)) + .or(a_min_value) + .or(b_min_value)) + } + NoteFilter::Attempt { + condition, + fallback, + } => { + let cond = min_note_value(conn, account, condition.as_ref(), chain_tip_height)?; + if cond.is_none() { + min_note_value(conn, account, fallback, chain_tip_height) + } else { + Ok(cond) + } + } + } + } + + // TODO: Simplify the query before executing it. Not worrying about this now because queries + // will be developer-configured, not end-user defined. + if let Some(min_value) = min_note_value(conn, account, filter, chain_tip_height)? { + let (note_count, total_value) = run_selection(min_value)?; + + Ok(Some(PoolMeta::new( + note_count, + total_value.unwrap_or(Zatoshis::ZERO), + ))) + } else { + Ok(None) + } +} diff --git a/zcash_client_sqlite/src/wallet/db.rs b/zcash_client_sqlite/src/wallet/db.rs new file mode 100644 index 0000000000..6520ac1834 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/db.rs @@ -0,0 +1,1074 @@ +//! Documentation about the wallet database structure. +//! +//! The database structure is managed by [`crate::wallet::init::init_wallet_db`], which +//! applies migrations (defined in `crate::wallet::init::migrations`) that produce the +//! current structure. +//! +//! The SQL code in this module's constants encodes the current database structure, as +//! represented internally by SQLite. We do not use these constants at runtime; instead we +//! check the output of the migrations in `crate::wallet::init::tests::verify_schema`, to +//! pin the expected database structure. + +// The constants in this module are only used in tests, but `#[cfg(test)]` prevents them +// from showing up in `cargo doc --document-private-items`. +#![allow(dead_code)] + +use static_assertions::const_assert_eq; + +use zcash_client_backend::data_api::{scanning::ScanPriority, GAP_LIMIT}; +use zcash_protocol::consensus::{NetworkUpgrade, Parameters}; + +use crate::wallet::scanning::priority_code; + +/// Stores information about the accounts that the wallet is tracking. +pub(super) const TABLE_ACCOUNTS: &str = r#" +CREATE TABLE "accounts" ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + name TEXT, + uuid BLOB NOT NULL, + account_kind INTEGER NOT NULL DEFAULT 0, + key_source TEXT, + hd_seed_fingerprint BLOB, + hd_account_index INTEGER, + ufvk TEXT, + uivk TEXT NOT NULL, + orchard_fvk_item_cache BLOB, + sapling_fvk_item_cache BLOB, + p2pkh_fvk_item_cache BLOB, + birthday_height INTEGER NOT NULL, + birthday_sapling_tree_size INTEGER, + birthday_orchard_tree_size INTEGER, + recover_until_height INTEGER, + has_spend_key INTEGER NOT NULL DEFAULT 1, + CHECK ( + ( + account_kind = 0 + AND hd_seed_fingerprint IS NOT NULL + AND hd_account_index IS NOT NULL + AND ufvk IS NOT NULL + ) + OR + ( + account_kind = 1 + AND (hd_seed_fingerprint IS NULL) = (hd_account_index IS NULL) + ) + ) +)"#; +pub(super) const INDEX_ACCOUNTS_UUID: &str = + r#"CREATE UNIQUE INDEX accounts_uuid ON accounts (uuid)"#; +pub(super) const INDEX_ACCOUNTS_UFVK: &str = + r#"CREATE UNIQUE INDEX accounts_ufvk ON accounts (ufvk)"#; +pub(super) const INDEX_ACCOUNTS_UIVK: &str = + r#"CREATE UNIQUE INDEX accounts_uivk ON accounts (uivk)"#; +pub(super) const INDEX_HD_ACCOUNT: &str = + r#"CREATE UNIQUE INDEX hd_account ON accounts (hd_seed_fingerprint, hd_account_index)"#; + +/// Stores diversified Unified Addresses that have been generated from accounts in the +/// wallet. +/// +/// - The `cached_transparent_receiver_address` column contains the transparent receiver component +/// of the UA. It is cached directly in the table to make account lookups for transparent outputs +/// more efficient, enabling joins to [`TABLE_TRANSPARENT_RECEIVED_OUTPUTS`]. +pub(super) const TABLE_ADDRESSES: &str = r#" +CREATE TABLE "addresses" ( + account_id INTEGER NOT NULL, + diversifier_index_be BLOB NOT NULL, + address TEXT NOT NULL, + cached_transparent_receiver_address TEXT, + FOREIGN KEY (account_id) REFERENCES accounts(id), + CONSTRAINT diversification UNIQUE (account_id, diversifier_index_be) +)"#; +pub(super) const INDEX_ADDRESSES_ACCOUNTS: &str = r#" +CREATE INDEX "addresses_accounts" ON "addresses" ( + "account_id" ASC +)"#; + +/// Stores ephemeral transparent addresses used for ZIP 320. +/// +/// For each account, these addresses are allocated sequentially by address index under scope 2 +/// (`TransparentKeyScope::EPHEMERAL`) at the "change" level of the BIP 32 address hierarchy. +/// The ephemeral addresses stored in the table are exactly the "reserved" ephemeral addresses +/// (that is addresses that have been allocated for use in a ZIP 320 transaction proposal), plus +/// the addresses at the next [`GAP_LIMIT`] indices. +/// +/// Addresses are never removed. New ones should only be reserved via the +/// `WalletWrite::reserve_next_n_ephemeral_addresses` API. All of the addresses in the table +/// should be scanned for incoming funds. +/// +/// ### Columns +/// - `address` contains the string (Base58Check) encoding of a transparent P2PKH address. +/// - `used_in_tx` indicates that the address has been used by this wallet in a transaction (which +/// has not necessarily been mined yet). This should only be set once, when the txid is known. +/// - `seen_in_tx` is non-null iff an output to the address has been seed in a transaction observed +/// on the network and passed to `store_decrypted_tx`. The transaction may have been sent by this +// wallet or another one using the same seed, or by a TEX address recipient sending back the +/// funds. This is used to advance the "gap", as well as to heuristically reduce the chance of +/// address reuse collisions with another wallet using the same seed. +/// +/// It is an external invariant that within each account: +/// - the address indices are contiguous and start from 0; +/// - the last [`GAP_LIMIT`] addresses have `used_in_tx` and `seen_in_tx` both NULL. +/// +/// All but the last [`GAP_LIMIT`] addresses are defined to be "reserved" addresses. Since the next +/// index to reserve is determined by dead reckoning from the last stored address, we use dummy +/// entries having `NULL` for the value of the `address` column after the maximum valid index in +/// order to allow the last [`GAP_LIMIT`] addresses at the end of the index range to be used. +/// +/// Note that the fact that `used_in_tx` references a specific transaction is just a debugging aid. +/// The same is mostly true of `seen_in_tx`, but we also take into account whether the referenced +/// transaction is unmined in order to determine the last index that is safe to reserve. +pub(super) const TABLE_EPHEMERAL_ADDRESSES: &str = r#" +CREATE TABLE ephemeral_addresses ( + account_id INTEGER NOT NULL, + address_index INTEGER NOT NULL, + -- nullability of this column is controlled by the index_range_and_address_nullity check + address TEXT, + used_in_tx INTEGER, + seen_in_tx INTEGER, + FOREIGN KEY (account_id) REFERENCES accounts(id), + FOREIGN KEY (used_in_tx) REFERENCES transactions(id_tx), + FOREIGN KEY (seen_in_tx) REFERENCES transactions(id_tx), + PRIMARY KEY (account_id, address_index), + CONSTRAINT ephemeral_addr_uniq UNIQUE (address), + CONSTRAINT used_implies_seen CHECK ( + used_in_tx IS NULL OR seen_in_tx IS NOT NULL + ), + CONSTRAINT index_range_and_address_nullity CHECK ( + (address_index BETWEEN 0 AND 0x7FFFFFFF AND address IS NOT NULL) OR + (address_index BETWEEN 0x80000000 AND 0x7FFFFFFF + 20 AND address IS NULL AND used_in_tx IS NULL AND seen_in_tx IS NULL) + ) +) WITHOUT ROWID"#; +// Hexadecimal integer literals were added in SQLite version 3.8.6 (2014-08-15). +// libsqlite3-sys requires at least version 3.14.0. +// "WITHOUT ROWID" tells SQLite to use a clustered index on the (composite) primary key. +const_assert_eq!(GAP_LIMIT, 20); + +/// Stores information about every block that the wallet has scanned. +/// +/// Note that this table does not contain any rows for blocks that the wallet might have +/// observed partial information about (for example, a transparent output fetched and +/// stored in [`TABLE_TRANSPARENT_RECEIVED_OUTPUTS`]). This may change in future. +pub(super) const TABLE_BLOCKS: &str = " +CREATE TABLE blocks ( + height INTEGER PRIMARY KEY, + hash BLOB NOT NULL, + time INTEGER NOT NULL, + sapling_tree BLOB NOT NULL , + sapling_commitment_tree_size INTEGER, + orchard_commitment_tree_size INTEGER, + sapling_output_count INTEGER, + orchard_action_count INTEGER)"; + +/// Stores the wallet's transactions. +/// +/// Any transactions that the wallet observes as "belonging to" one of the accounts in +/// [`TABLE_ACCOUNTS`] may be tracked in this table. As a result, this table may contain +/// data that is not recoverable from the chain (for example, transactions created by the +/// wallet that expired before being mined). +/// +/// ### Columns +/// - `created`: The time at which the transaction was created as a string in the format +/// `yyyy-MM-dd HH:mm:ss.fffffffzzz`. +/// - `block`: stores the height (in the wallet's chain view) of the mined block containing the +/// transaction. It is `NULL` for transactions that have not yet been observed in scanned blocks, +/// including transactions in the mempool or that have expired. +/// - `mined_height`: stores the height (in the wallet's chain view) of the mined block containing +/// the transaction. It is present to allow the block height for a retrieved transaction to be +/// stored without requiring that the entire block containing the transaction be scanned; the +/// foreign key constraint on `block` prevents that column from being populated prior to complete +/// scanning of the block. This is constrained to be equal to the `block` column if `block` is +/// non-null. +/// - `target_height`: stores the target height for which the transaction was constructed, if +/// known. This will ordinarily be null for transactions discovered via chain scanning; it +/// will only be set for transactions created using this wallet specifically, and not any +/// other wallet that uses the same seed (including previous installations of the same +/// wallet application.) +pub(super) const TABLE_TRANSACTIONS: &str = r#" +CREATE TABLE "transactions" ( + id_tx INTEGER PRIMARY KEY, + txid BLOB NOT NULL UNIQUE, + created TEXT, + block INTEGER, + mined_height INTEGER, + tx_index INTEGER, + expiry_height INTEGER, + raw BLOB, + fee INTEGER, + target_height INTEGER, + FOREIGN KEY (block) REFERENCES blocks(height), + CONSTRAINT height_consistency CHECK (block IS NULL OR mined_height = block) +)"#; + +/// Stores the Sapling notes received by the wallet. +/// +/// Note spentness is tracked in [`TABLE_SAPLING_RECEIVED_NOTE_SPENDS`]. +pub(super) const TABLE_SAPLING_RECEIVED_NOTES: &str = r#" +CREATE TABLE "sapling_received_notes" ( + id INTEGER PRIMARY KEY, + tx INTEGER NOT NULL, + output_index INTEGER NOT NULL, + account_id INTEGER NOT NULL, + diversifier BLOB NOT NULL, + value INTEGER NOT NULL, + rcm BLOB NOT NULL, + nf BLOB UNIQUE, + is_change INTEGER NOT NULL, + memo BLOB, + commitment_tree_position INTEGER, + recipient_key_scope INTEGER, + FOREIGN KEY (tx) REFERENCES transactions(id_tx), + FOREIGN KEY (account_id) REFERENCES accounts(id), + CONSTRAINT tx_output UNIQUE (tx, output_index) +)"#; +pub(super) const INDEX_SAPLING_RECEIVED_NOTES_ACCOUNT: &str = r#" +CREATE INDEX "sapling_received_notes_account" ON "sapling_received_notes" ( + "account_id" ASC +)"#; +pub(super) const INDEX_SAPLING_RECEIVED_NOTES_TX: &str = r#" +CREATE INDEX "sapling_received_notes_tx" ON "sapling_received_notes" ( + "tx" ASC +)"#; + +/// A junction table between received Sapling notes and the transactions that spend them. +/// +/// Only one mined transaction can spend a note. However, transactions created by the +/// wallet may expire before being mined, and the wallet still tracks the fact that the +/// user created the transaction. The junction table enables the "spent-in" relationship +/// between notes and expired transactions to be preserved; note spent-ness is determined +/// by joining this table with [`TABLE_TRANSACTIONS`] and then filtering out transactions +/// where either `transactions.block` is non-null, or `transactions.expiry_height` is not +/// greater than the wallet's view of the chain tip. +pub(super) const TABLE_SAPLING_RECEIVED_NOTE_SPENDS: &str = " +CREATE TABLE sapling_received_note_spends ( + sapling_received_note_id INTEGER NOT NULL, + transaction_id INTEGER NOT NULL, + FOREIGN KEY (sapling_received_note_id) + REFERENCES sapling_received_notes(id) + ON DELETE CASCADE, + FOREIGN KEY (transaction_id) + -- We do not delete transactions, so this does not cascade + REFERENCES transactions(id_tx), + UNIQUE (sapling_received_note_id, transaction_id) +)"; + +/// Stores the Orchard notes received by the wallet. +/// +/// Note spentness is tracked in [`TABLE_ORCHARD_RECEIVED_NOTE_SPENDS`]. +pub(super) const TABLE_ORCHARD_RECEIVED_NOTES: &str = " +CREATE TABLE orchard_received_notes ( + id INTEGER PRIMARY KEY, + tx INTEGER NOT NULL, + action_index INTEGER NOT NULL, + account_id INTEGER NOT NULL, + diversifier BLOB NOT NULL, + value INTEGER NOT NULL, + rho BLOB NOT NULL, + rseed BLOB NOT NULL, + nf BLOB UNIQUE, + is_change INTEGER NOT NULL, + memo BLOB, + commitment_tree_position INTEGER, + recipient_key_scope INTEGER, + FOREIGN KEY (tx) REFERENCES transactions(id_tx), + FOREIGN KEY (account_id) REFERENCES accounts(id), + CONSTRAINT tx_output UNIQUE (tx, action_index) +)"; +pub(super) const INDEX_ORCHARD_RECEIVED_NOTES_ACCOUNT: &str = r#" +CREATE INDEX orchard_received_notes_account ON orchard_received_notes ( + account_id ASC +)"#; +pub(super) const INDEX_ORCHARD_RECEIVED_NOTES_TX: &str = r#" +CREATE INDEX orchard_received_notes_tx ON orchard_received_notes ( + tx ASC +)"#; + +/// A junction table between received Orchard notes and the transactions that spend them. +/// +/// Thie plays the same role for Orchard notes as does [`TABLE_SAPLING_RECEIVED_NOTE_SPENDS`] for +/// Sapling notes; see its documentation for details. +pub(super) const TABLE_ORCHARD_RECEIVED_NOTE_SPENDS: &str = " +CREATE TABLE orchard_received_note_spends ( + orchard_received_note_id INTEGER NOT NULL, + transaction_id INTEGER NOT NULL, + FOREIGN KEY (orchard_received_note_id) + REFERENCES orchard_received_notes(id) + ON DELETE CASCADE, + FOREIGN KEY (transaction_id) + -- We do not delete transactions, so this does not cascade + REFERENCES transactions(id_tx), + UNIQUE (orchard_received_note_id, transaction_id) +)"; + +/// Stores the transparent outputs received by the wallet. +/// +/// Originally this table only stored the current UTXO set (as of latest refresh), and the +/// table was cleared prior to loading in the latest UTXO set. We now upsert instead of +/// insert into the database, meaning that spent outputs are left in the database. This +/// makes it similar to the `*_received_notes` tables in that it can store history. +/// Depending upon how transparent TXOs for the wallet are discovered, the following +/// may be true: +/// - The table may have incomplete contents for recovered-from-seed wallets. +/// - The table may have inconsistent contents for seeds loaded into multiple wallets +/// simultaneously. +/// - The wallet's transparent balance may be incorrect prior to "transaction enhancement" +/// (downloading the full transaction containing the transparent output spend). +/// +/// ### Columns: +/// - `id`: Primary key +/// - `transaction_id`: Reference to the transaction in which this TXO was created +/// - `output_index`: The output index of this TXO in the transaction referred to by `transaction_id` +/// - `account_id`: The account that controls spend authority for this TXO +/// - `address`: The address to which this TXO was sent. We store this address to make querying +/// for UTXOs for a single address easier, because when shielding we always select UTXOs +/// for only a single address at a time to prevent linking addresses in the shielding +/// transaction. +/// - `script`: The full txout script +/// - `value_zat`: The value of the TXO in zatoshis +/// - `max_observed_unspent_height`: The maximum block height at which this TXO was either +/// observed to be a member of the UTXO set at the start of the block, or observed +/// to be an output of a transaction mined in the block. This is intended to be used to +/// determine when the TXO is no longer a part of the UTXO set, in the case that the +/// transaction that spends it is not detected by the wallet. +pub(super) const TABLE_TRANSPARENT_RECEIVED_OUTPUTS: &str = r#" +CREATE TABLE transparent_received_outputs ( + id INTEGER PRIMARY KEY, + transaction_id INTEGER NOT NULL, + output_index INTEGER NOT NULL, + account_id INTEGER NOT NULL, + address TEXT NOT NULL, + script BLOB NOT NULL, + value_zat INTEGER NOT NULL, + max_observed_unspent_height INTEGER, + FOREIGN KEY (transaction_id) REFERENCES transactions(id_tx), + FOREIGN KEY (account_id) REFERENCES accounts(id), + CONSTRAINT transparent_output_unique UNIQUE (transaction_id, output_index) +)"#; +pub(super) const INDEX_TRANSPARENT_RECEIVED_OUTPUTS_ACCOUNT_ID: &str = r#" +CREATE INDEX idx_transparent_received_outputs_account_id +ON "transparent_received_outputs" (account_id)"#; + +/// A junction table between received transparent outputs and the transactions that spend them. +/// +/// This plays the same role for transparent TXOs as does [`TABLE_SAPLING_RECEIVED_NOTE_SPENDS`] +/// for Sapling notes. However, [`TABLE_TRANSPARENT_RECEIVED_OUTPUTS`] differs from +/// [`TABLE_SAPLING_RECEIVED_NOTES`] and [`TABLE_ORCHARD_RECEIVED_NOTES`] in that an +/// associated `transactions` record may have its `mined_height` set without there existing a +/// corresponding record in the `blocks` table for a block at that height, due to the asymmetries +/// between scanning for shielded notes and retrieving transparent TXOs currently implemented +/// in [`zcash_client_backend`]. +pub(super) const TABLE_TRANSPARENT_RECEIVED_OUTPUT_SPENDS: &str = r#" +CREATE TABLE "transparent_received_output_spends" ( + transparent_received_output_id INTEGER NOT NULL, + transaction_id INTEGER NOT NULL, + FOREIGN KEY (transparent_received_output_id) + REFERENCES transparent_received_outputs(id) + ON DELETE CASCADE, + FOREIGN KEY (transaction_id) + -- We do not delete transactions, so this does not cascade + REFERENCES transactions(id_tx), + UNIQUE (transparent_received_output_id, transaction_id) +)"#; + +/// A cache of the relationship between a transaction and the prevout data of its +/// transparent inputs. +/// +/// This table is used in out-of-order wallet recovery to cache the information about +/// what transaction(s) spend each transparent outpoint, so that if an output belonging +/// to the wallet is detected after the transaction that spends it has been processed, +/// the spend can also be recorded as part of the process of adding the output to +/// [`TABLE_TRANSPARENT_RECEIVED_OUTPUTS`]. +pub(super) const TABLE_TRANSPARENT_SPEND_MAP: &str = r#" +CREATE TABLE transparent_spend_map ( + spending_transaction_id INTEGER NOT NULL, + prevout_txid BLOB NOT NULL, + prevout_output_index INTEGER NOT NULL, + FOREIGN KEY (spending_transaction_id) REFERENCES transactions(id_tx) + -- NOTE: We can't create a unique constraint on just (prevout_txid, prevout_output_index) + -- because the same output may be attempted to be spent in multiple transactions, even + -- though only one will ever be mined. + CONSTRAINT transparent_spend_map_unique UNIQUE ( + spending_transaction_id, prevout_txid, prevout_output_index + ) +)"#; + +/// Stores the outputs of transactions created by the wallet. +/// +/// Unlike with outputs received by the wallet, we store sent outputs for all pools in +/// this table, distinguished by the `output_pool` column. The information we want to +/// record for sent outputs is the same across all pools, whereas for received outputs we +/// want to cache pool-specific data. +/// +/// ### Columns +/// - `(tx, output_pool, output_index)` collectively identify a transaction output. +/// - `from_account_id`: the ID of the account that created the transaction. +/// - On recover-from-seed or when scanning by UFVK, this will be either the account +/// that decrypted the output, or one of the accounts that funded the transaction. +/// - `to_address`: the address of the external recipient of this output, or `NULL` if the +/// output was received by the wallet. +/// - `to_account_id`: the ID of the account that received this output, or `NULL` if the +/// output was for an external recipient. +/// - `value`: the value of the output in zatoshis. +/// - `memo`: the memo bytes associated with this output, if known. +/// - This is always `NULL` for transparent outputs. +/// - This will be set for all shielded outputs of transactions created by the wallet. +/// - On recover-from-seed or when scanning by UFVK, this will only be set for shielded +/// outputs after post-scanning transaction enhancement. For shielded notes sent to +/// external recipients, the transaction needs to have been created with an +/// [`OvkPolicy`] using a known OVK. +/// +/// [`OvkPolicy`]: zcash_client_backend::wallet::OvkPolicy +pub(super) const TABLE_SENT_NOTES: &str = r#" +CREATE TABLE "sent_notes" ( + id INTEGER PRIMARY KEY, + tx INTEGER NOT NULL, + output_pool INTEGER NOT NULL, + output_index INTEGER NOT NULL, + from_account_id INTEGER NOT NULL, + to_address TEXT, + to_account_id INTEGER, + value INTEGER NOT NULL, + memo BLOB, + FOREIGN KEY (tx) REFERENCES transactions(id_tx), + FOREIGN KEY (from_account_id) REFERENCES accounts(id), + FOREIGN KEY (to_account_id) REFERENCES accounts(id), + CONSTRAINT tx_output UNIQUE (tx, output_pool, output_index), + CONSTRAINT note_recipient CHECK ( + (to_address IS NOT NULL) OR (to_account_id IS NOT NULL) + ) +)"#; +pub(super) const INDEX_SENT_NOTES_FROM_ACCOUNT: &str = + r#"CREATE INDEX sent_notes_from_account ON "sent_notes" (from_account_id)"#; +pub(super) const INDEX_SENT_NOTES_TO_ACCOUNT: &str = + r#"CREATE INDEX sent_notes_to_account ON "sent_notes" (to_account_id)"#; +pub(super) const INDEX_SENT_NOTES_TX: &str = r#"CREATE INDEX sent_notes_tx ON "sent_notes" (tx)"#; + +/// Stores the set of transaction ids for which the backend required additional data. +/// +/// ### Columns: +/// - `txid`: The transaction identifier for the transaction to retrieve state information for. +/// - `query_type`: +/// - `0` for raw transaction (enhancement) data, +/// - `1` for transaction mined-ness information. +/// - `dependent_transaction_id`: If the transaction data request is searching for information +/// about transparent inputs to a transaction, this is a reference to that transaction record. +/// NULL for transactions where the request for enhancement data is based on discovery due +/// to blockchain scanning. +pub(super) const TABLE_TX_RETRIEVAL_QUEUE: &str = r#" +CREATE TABLE tx_retrieval_queue ( + txid BLOB NOT NULL UNIQUE, + query_type INTEGER NOT NULL, + dependent_transaction_id INTEGER, + FOREIGN KEY (dependent_transaction_id) REFERENCES transactions(id_tx) +)"#; + +/// Stores the set of transaction outputs received by the wallet for which spend information +/// (if any) should be retrieved. +/// +/// This table is populated in the process of wallet recovery when a deshielding transaction +/// with transparent outputs belonging to the wallet (e.g., the deshielding half of a ZIP 320 +/// transaction pair) is discovered. It is expected that such a transparent output will be +/// spent soon after it is received in a purely transparent transaction, which the wallet +/// currently has no means of detecting otherwise. +pub(super) const TABLE_TRANSPARENT_SPEND_SEARCH_QUEUE: &str = r#" +CREATE TABLE transparent_spend_search_queue ( + address TEXT NOT NULL, + transaction_id INTEGER NOT NULL, + output_index INTEGER NOT NULL, + FOREIGN KEY (transaction_id) REFERENCES transactions(id_tx), + CONSTRAINT value_received_height UNIQUE (transaction_id, output_index) +)"#; + +// +// State for shard trees +// + +/// Stores the shards of a [`ShardTree`] for the Sapling commitment tree. +/// +/// This table contains a row for each 2^16 subtree of the Sapling note commitment tree, +/// keyed by the index of the shard. The `shard_data` column contains the subtree's data +/// as serialized by [`zcash_client_backend::serialization::shardtree::write_shard`]. +/// +/// [`ShardTree`]: shardtree::ShardTree +pub(super) const TABLE_SAPLING_TREE_SHARDS: &str = " +CREATE TABLE sapling_tree_shards ( + shard_index INTEGER PRIMARY KEY, + subtree_end_height INTEGER, + root_hash BLOB, + shard_data BLOB, + contains_marked INTEGER, + CONSTRAINT root_unique UNIQUE (root_hash) +)"; + +/// Stores the "cap" of the Sapling [`ShardTree`]. +/// +/// This table will only ever have a single row, in which is serialized the 2^16 "cap" +/// of the Sapling note commitment tree, The `cap_data` column contains the cap data +/// as serialized by [`zcash_client_backend::serialization::shardtree::write_shard`]. +/// +/// [`ShardTree`]: shardtree::ShardTree +pub(super) const TABLE_SAPLING_TREE_CAP: &str = " +CREATE TABLE sapling_tree_cap ( + -- cap_id exists only to be able to take advantage of `ON CONFLICT` + -- upsert functionality; the table will only ever contain one row + cap_id INTEGER PRIMARY KEY, + cap_data BLOB NOT NULL +)"; + +/// Stores the checkpointed positions in the Sapling [`ShardTree`]. +/// +/// Each row in this table stores the note commitment tree position of the last Sapling +/// output in the block having height `checkpoint_id`. +/// +/// [`ShardTree`]: shardtree::ShardTree +pub(super) const TABLE_SAPLING_TREE_CHECKPOINTS: &str = " +CREATE TABLE sapling_tree_checkpoints ( + checkpoint_id INTEGER PRIMARY KEY, + position INTEGER +)"; + +/// Stores metadata about the positions of Sapling notes that have been spent but for +/// which witness information has not yet been removed from the note commitment tree. +/// +/// In the process of updating the note commitment tree in response to the addition of +/// a block, it is necessary to temporarily continue to store witness information for +/// each note so that a spent note can be made spendable again after a rollback of the +/// spending block. This table caches the metadata needed for that restoration. +pub(super) const TABLE_SAPLING_TREE_CHECKPOINT_MARKS_REMOVED: &str = " +CREATE TABLE sapling_tree_checkpoint_marks_removed ( + checkpoint_id INTEGER NOT NULL, + mark_removed_position INTEGER NOT NULL, + FOREIGN KEY (checkpoint_id) REFERENCES sapling_tree_checkpoints(checkpoint_id) + ON DELETE CASCADE, + CONSTRAINT spend_position_unique UNIQUE (checkpoint_id, mark_removed_position) +)"; + +/// Stores the shards of a [`ShardTree`] for the Orchard commitment tree. +/// +/// This is identical to [`TABLE_SAPLING_TREE_SHARDS`]; see its documentation for details. +/// +/// [`ShardTree`]: shardtree::ShardTree +pub(super) const TABLE_ORCHARD_TREE_SHARDS: &str = " +CREATE TABLE orchard_tree_shards ( + shard_index INTEGER PRIMARY KEY, + subtree_end_height INTEGER, + root_hash BLOB, + shard_data BLOB, + contains_marked INTEGER, + CONSTRAINT root_unique UNIQUE (root_hash) +)"; + +/// Stores the "cap" of the Orchard [`ShardTree`]. +/// +/// This is identical to [`TABLE_SAPLING_TREE_CAP`]; see its documentation for details. +/// +/// [`ShardTree`]: shardtree::ShardTree +pub(super) const TABLE_ORCHARD_TREE_CAP: &str = " +CREATE TABLE orchard_tree_cap ( + -- cap_id exists only to be able to take advantage of `ON CONFLICT` + -- upsert functionality; the table will only ever contain one row + cap_id INTEGER PRIMARY KEY, + cap_data BLOB NOT NULL +)"; + +/// Stores the checkpointed positions in the Orchard [`ShardTree`]. +/// +/// This is identical to [`TABLE_SAPLING_TREE_CHECKPOINTS`]; see its documentation for +/// details. +/// +/// [`ShardTree`]: shardtree::ShardTree +pub(super) const TABLE_ORCHARD_TREE_CHECKPOINTS: &str = " +CREATE TABLE orchard_tree_checkpoints ( + checkpoint_id INTEGER PRIMARY KEY, + position INTEGER +)"; + +/// Stores metadata about the positions of Orchard notes that have been spent but for +/// which witness information has not yet been removed from the note commitment tree. +/// +/// This is identical to [`TABLE_SAPLING_TREE_CHECKPOINT_MARKS_REMOVED`]; see its +/// documentation for details. +pub(super) const TABLE_ORCHARD_TREE_CHECKPOINT_MARKS_REMOVED: &str = " +CREATE TABLE orchard_tree_checkpoint_marks_removed ( + checkpoint_id INTEGER NOT NULL, + mark_removed_position INTEGER NOT NULL, + FOREIGN KEY (checkpoint_id) REFERENCES orchard_tree_checkpoints(checkpoint_id) + ON DELETE CASCADE, + CONSTRAINT spend_position_unique UNIQUE (checkpoint_id, mark_removed_position) +)"; + +// +// Scanning +// + +/// Stores the [`ScanPriority`] for all block ranges in the wallet's view of the chain. +/// +/// [`ScanPriority`]: zcash_client_backend::data_api::scanning::ScanPriority +pub(super) const TABLE_SCAN_QUEUE: &str = " +CREATE TABLE scan_queue ( + block_range_start INTEGER NOT NULL, + block_range_end INTEGER NOT NULL, + priority INTEGER NOT NULL, + CONSTRAINT range_start_uniq UNIQUE (block_range_start), + CONSTRAINT range_end_uniq UNIQUE (block_range_end), + CONSTRAINT range_bounds_order CHECK ( + block_range_start < block_range_end + ) +)"; + +/// A map from "transaction locators" to transaction IDs for the current chain state. +/// +/// `(block_height, tx_index)` is a "transaction locator"; `tx_index` is an index into the +/// list of transactions for the block at height `block_height` in the chain as currently +/// known to the wallet. +/// +/// No foreign key constraint is enforced for `block_height` to [`TABLE_BLOCKS`], to allow +/// loading the nullifier map separately from block scanning. +pub(super) const TABLE_TX_LOCATOR_MAP: &str = " +CREATE TABLE tx_locator_map ( + block_height INTEGER NOT NULL, + tx_index INTEGER NOT NULL, + txid BLOB NOT NULL UNIQUE, + PRIMARY KEY (block_height, tx_index) +)"; + +/// A map from nullifiers to the transaction they were observed in. +/// +/// The purpose of this map is to allow non-linear scanning. If the wallet scans a block +/// range `Y..Z` that leaves a gap between the wallet's birthday height and `Y`, then the +/// wallet must assume that any nullifier observed in `Y..Z` might be spending one of its +/// notes (that it has not yet observed), otherwise it will fail to detect those spends +/// and report a too-large balance. Once the wallet has scanned every block between its +/// birthday height and `Y`, the nullifier map contents up to `Z` is no longer necessary +/// and can be dropped. +/// +/// The map stores transaction locators instead of transaction IDs for efficiency. SQLite +/// will represent the transaction locator in at most 6 bytes, so a transaction that only +/// spends one shielded note will incur a 12-byte overhead (across both this table and +/// [`TABLE_TX_LOCATOR_MAP`]), but each additional spent note in a transaction saves 26 +/// bytes. +pub(super) const TABLE_NULLIFIER_MAP: &str = " +CREATE TABLE nullifier_map ( + spend_pool INTEGER NOT NULL, + nf BLOB NOT NULL, + block_height INTEGER NOT NULL, + tx_index INTEGER NOT NULL, + CONSTRAINT tx_locator + FOREIGN KEY (block_height, tx_index) + REFERENCES tx_locator_map(block_height, tx_index) + ON DELETE CASCADE + ON UPDATE RESTRICT, + CONSTRAINT nf_uniq UNIQUE (spend_pool, nf) +)"; +pub(super) const INDEX_NF_MAP_LOCATOR_IDX: &str = + r#"CREATE INDEX nf_map_locator_idx ON nullifier_map(block_height, tx_index)"#; + +// +// Internal tables +// + +/// Internal table used by [`schemerz`] to manage migrations. +pub(super) const TABLE_SCHEMERZ_MIGRATIONS: &str = " +CREATE TABLE schemer_migrations ( + id blob PRIMARY KEY +)"; + +/// Internal table created by SQLite when we started using `AUTOINCREMENT`. +pub(super) const TABLE_SQLITE_SEQUENCE: &str = "CREATE TABLE sqlite_sequence(name,seq)"; + +// +// Views +// + +pub(super) const VIEW_RECEIVED_OUTPUTS: &str = " +CREATE VIEW v_received_outputs AS + SELECT + sapling_received_notes.id AS id_within_pool_table, + sapling_received_notes.tx AS transaction_id, + 2 AS pool, + sapling_received_notes.output_index, + account_id, + sapling_received_notes.value, + is_change, + sapling_received_notes.memo, + sent_notes.id AS sent_note_id + FROM sapling_received_notes + LEFT JOIN sent_notes + ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) = + (sapling_received_notes.tx, 2, sapling_received_notes.output_index) +UNION + SELECT + orchard_received_notes.id AS id_within_pool_table, + orchard_received_notes.tx AS transaction_id, + 3 AS pool, + orchard_received_notes.action_index AS output_index, + account_id, + orchard_received_notes.value, + is_change, + orchard_received_notes.memo, + sent_notes.id AS sent_note_id + FROM orchard_received_notes + LEFT JOIN sent_notes + ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) = + (orchard_received_notes.tx, 3, orchard_received_notes.action_index) +UNION + SELECT + u.id AS id_within_pool_table, + u.transaction_id, + 0 AS pool, + u.output_index, + u.account_id, + u.value_zat AS value, + 0 AS is_change, + NULL AS memo, + sent_notes.id AS sent_note_id + FROM transparent_received_outputs u + LEFT JOIN sent_notes + ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) = + (u.transaction_id, 0, u.output_index)"; + +pub(super) const VIEW_RECEIVED_OUTPUT_SPENDS: &str = " +CREATE VIEW v_received_output_spends AS +SELECT + 2 AS pool, + sapling_received_note_id AS received_output_id, + transaction_id +FROM sapling_received_note_spends +UNION +SELECT + 3 AS pool, + orchard_received_note_id AS received_output_id, + transaction_id +FROM orchard_received_note_spends +UNION +SELECT + 0 AS pool, + transparent_received_output_id AS received_output_id, + transaction_id +FROM transparent_received_output_spends"; + +pub(super) const VIEW_TRANSACTIONS: &str = " +CREATE VIEW v_transactions AS +WITH +notes AS ( + -- Outputs received in this transaction + SELECT ro.account_id AS account_id, + transactions.mined_height AS mined_height, + transactions.txid AS txid, + ro.pool AS pool, + id_within_pool_table, + ro.value AS value, + 0 AS spent_note_count, + CASE + WHEN ro.is_change THEN 1 + ELSE 0 + END AS change_note_count, + CASE + WHEN ro.is_change THEN 0 + ELSE 1 + END AS received_count, + CASE + WHEN (ro.memo IS NULL OR ro.memo = X'F6') + THEN 0 + ELSE 1 + END AS memo_present, + -- The wallet cannot receive transparent outputs in shielding transactions. + CASE + WHEN ro.pool = 0 + THEN 1 + ELSE 0 + END AS does_not_match_shielding + FROM v_received_outputs ro + JOIN transactions + ON transactions.id_tx = ro.transaction_id + UNION + -- Outputs spent in this transaction + SELECT ro.account_id AS account_id, + transactions.mined_height AS mined_height, + transactions.txid AS txid, + ro.pool AS pool, + id_within_pool_table, + -ro.value AS value, + 1 AS spent_note_count, + 0 AS change_note_count, + 0 AS received_count, + 0 AS memo_present, + -- The wallet cannot spend shielded outputs in shielding transactions. + CASE + WHEN ro.pool != 0 + THEN 1 + ELSE 0 + END AS does_not_match_shielding + FROM v_received_outputs ro + JOIN v_received_output_spends ros + ON ros.pool = ro.pool + AND ros.received_output_id = ro.id_within_pool_table + JOIN transactions + ON transactions.id_tx = ros.transaction_id +), +-- Obtain a count of the notes that the wallet created in each transaction, +-- not counting change notes. +sent_note_counts AS ( + SELECT sent_notes.from_account_id AS account_id, + transactions.txid AS txid, + COUNT(DISTINCT sent_notes.id) AS sent_notes, + SUM( + CASE + WHEN (sent_notes.memo IS NULL OR sent_notes.memo = X'F6' OR ro.transaction_id IS NOT NULL) + THEN 0 + ELSE 1 + END + ) AS memo_count + FROM sent_notes + JOIN transactions + ON transactions.id_tx = sent_notes.tx + LEFT JOIN v_received_outputs ro + ON sent_notes.id = ro.sent_note_id + WHERE COALESCE(ro.is_change, 0) = 0 + GROUP BY account_id, txid +), +blocks_max_height AS ( + SELECT MAX(blocks.height) AS max_height FROM blocks +) +SELECT accounts.uuid AS account_uuid, + notes.mined_height AS mined_height, + notes.txid AS txid, + transactions.tx_index AS tx_index, + transactions.expiry_height AS expiry_height, + transactions.raw AS raw, + SUM(notes.value) AS account_balance_delta, + transactions.fee AS fee_paid, + SUM(notes.change_note_count) > 0 AS has_change, + MAX(COALESCE(sent_note_counts.sent_notes, 0)) AS sent_note_count, + SUM(notes.received_count) AS received_note_count, + SUM(notes.memo_present) + MAX(COALESCE(sent_note_counts.memo_count, 0)) AS memo_count, + blocks.time AS block_time, + ( + blocks.height IS NULL + AND transactions.expiry_height BETWEEN 1 AND blocks_max_height.max_height + ) AS expired_unmined, + SUM(notes.spent_note_count) AS spent_note_count, + ( + -- All of the wallet-spent and wallet-received notes are consistent with a + -- shielding transaction. + SUM(notes.does_not_match_shielding) = 0 + -- The transaction contains at least one wallet-spent output. + AND SUM(notes.spent_note_count) > 0 + -- The transaction contains at least one wallet-received note. + AND (SUM(notes.received_count) + SUM(notes.change_note_count)) > 0 + -- We do not know about any external outputs of the transaction. + AND MAX(COALESCE(sent_note_counts.sent_notes, 0)) = 0 + ) AS is_shielding +FROM notes +LEFT JOIN accounts ON accounts.id = notes.account_id +LEFT JOIN transactions + ON notes.txid = transactions.txid +JOIN blocks_max_height +LEFT JOIN blocks ON blocks.height = notes.mined_height +LEFT JOIN sent_note_counts + ON sent_note_counts.account_id = notes.account_id + AND sent_note_counts.txid = notes.txid +GROUP BY notes.account_id, notes.txid"; + +/// Selects all outputs received by the wallet, plus any outputs sent from the wallet to +/// external recipients. +/// +/// This will contain: +/// * Outputs received from external recipients +/// * Outputs sent to external recipients +/// * Outputs received as part of a wallet-internal operation, including +/// both outputs received as a consequence of wallet-internal transfers +/// and as change. +/// +/// The `to_address` column will only contain an address when the recipient is +/// external. In all other cases, the recipient account id indicates the account +/// that controls the output. +pub(super) const VIEW_TX_OUTPUTS: &str = " +CREATE VIEW v_tx_outputs AS +WITH unioned AS ( + -- select all outputs received by the wallet + SELECT transactions.txid AS txid, + ro.pool AS output_pool, + ro.output_index AS output_index, + from_account.uuid AS from_account_uuid, + to_account.uuid AS to_account_uuid, + NULL AS to_address, + ro.value AS value, + ro.is_change AS is_change, + ro.memo AS memo + FROM v_received_outputs ro + JOIN transactions + ON transactions.id_tx = ro.transaction_id + -- join to the sent_notes table to obtain `from_account_id` + LEFT JOIN sent_notes ON sent_notes.id = ro.sent_note_id + -- join on the accounts table to obtain account UUIDs + LEFT JOIN accounts from_account ON from_account.id = sent_notes.from_account_id + LEFT JOIN accounts to_account ON to_account.id = ro.account_id + UNION ALL + -- select all outputs sent from the wallet to external recipients + SELECT transactions.txid AS txid, + sent_notes.output_pool AS output_pool, + sent_notes.output_index AS output_index, + from_account.uuid AS from_account_uuid, + NULL AS to_account_uuid, + sent_notes.to_address AS to_address, + sent_notes.value AS value, + 0 AS is_change, + sent_notes.memo AS memo + FROM sent_notes + JOIN transactions + ON transactions.id_tx = sent_notes.tx + LEFT JOIN v_received_outputs ro ON ro.sent_note_id = sent_notes.id + -- join on the accounts table to obtain account UUIDs + LEFT JOIN accounts from_account ON from_account.id = sent_notes.from_account_id +) +-- merge duplicate rows while retaining maximum information +SELECT + txid, + output_pool, + output_index, + max(from_account_uuid) AS from_account_uuid, + max(to_account_uuid) AS to_account_uuid, + max(to_address) AS to_address, + max(value) AS value, + max(is_change) AS is_change, + max(memo) AS memo +FROM unioned +GROUP BY txid, output_pool, output_index"; + +pub(super) fn view_sapling_shard_scan_ranges(params: &P) -> String { + format!( + "CREATE VIEW v_sapling_shard_scan_ranges AS + SELECT + shard.shard_index, + shard.shard_index << 16 AS start_position, + (shard.shard_index + 1) << 16 AS end_position_exclusive, + IFNULL(prev_shard.subtree_end_height, {}) AS subtree_start_height, + shard.subtree_end_height, + shard.contains_marked, + scan_queue.block_range_start, + scan_queue.block_range_end, + scan_queue.priority + FROM sapling_tree_shards shard + LEFT OUTER JOIN sapling_tree_shards prev_shard + ON shard.shard_index = prev_shard.shard_index + 1 + -- Join with scan ranges that overlap with the subtree's involved blocks. + INNER JOIN scan_queue ON ( + subtree_start_height < scan_queue.block_range_end AND + ( + scan_queue.block_range_start <= shard.subtree_end_height OR + shard.subtree_end_height IS NULL + ) + )", + u32::from(params.activation_height(NetworkUpgrade::Sapling).unwrap()), + ) +} + +pub(super) fn view_sapling_shard_unscanned_ranges() -> String { + format!( + "CREATE VIEW v_sapling_shard_unscanned_ranges AS + WITH wallet_birthday AS (SELECT MIN(birthday_height) AS height FROM accounts) + SELECT + shard_index, + start_position, + end_position_exclusive, + subtree_start_height, + subtree_end_height, + contains_marked, + block_range_start, + block_range_end, + priority + FROM v_sapling_shard_scan_ranges + INNER JOIN wallet_birthday + WHERE priority > {} + AND block_range_end > wallet_birthday.height", + priority_code(&ScanPriority::Scanned) + ) +} + +pub(super) const VIEW_SAPLING_SHARDS_SCAN_STATE: &str = " +CREATE VIEW v_sapling_shards_scan_state AS +SELECT + shard_index, + start_position, + end_position_exclusive, + subtree_start_height, + subtree_end_height, + contains_marked, + MAX(priority) AS max_priority +FROM v_sapling_shard_scan_ranges +GROUP BY + shard_index, + start_position, + end_position_exclusive, + subtree_start_height, + subtree_end_height, + contains_marked"; + +pub(super) fn view_orchard_shard_scan_ranges(params: &P) -> String { + format!( + "CREATE VIEW v_orchard_shard_scan_ranges AS + SELECT + shard.shard_index, + shard.shard_index << 16 AS start_position, + (shard.shard_index + 1) << 16 AS end_position_exclusive, + IFNULL(prev_shard.subtree_end_height, {}) AS subtree_start_height, + shard.subtree_end_height, + shard.contains_marked, + scan_queue.block_range_start, + scan_queue.block_range_end, + scan_queue.priority + FROM orchard_tree_shards shard + LEFT OUTER JOIN orchard_tree_shards prev_shard + ON shard.shard_index = prev_shard.shard_index + 1 + -- Join with scan ranges that overlap with the subtree's involved blocks. + INNER JOIN scan_queue ON ( + subtree_start_height < scan_queue.block_range_end AND + ( + scan_queue.block_range_start <= shard.subtree_end_height OR + shard.subtree_end_height IS NULL + ) + )", + u32::from(params.activation_height(NetworkUpgrade::Nu5).unwrap()), + ) +} + +pub(super) fn view_orchard_shard_unscanned_ranges() -> String { + format!( + "CREATE VIEW v_orchard_shard_unscanned_ranges AS + WITH wallet_birthday AS (SELECT MIN(birthday_height) AS height FROM accounts) + SELECT + shard_index, + start_position, + end_position_exclusive, + subtree_start_height, + subtree_end_height, + contains_marked, + block_range_start, + block_range_end, + priority + FROM v_orchard_shard_scan_ranges + INNER JOIN wallet_birthday + WHERE priority > {} + AND block_range_end > wallet_birthday.height", + priority_code(&ScanPriority::Scanned), + ) +} + +pub(super) const VIEW_ORCHARD_SHARDS_SCAN_STATE: &str = " +CREATE VIEW v_orchard_shards_scan_state AS +SELECT + shard_index, + start_position, + end_position_exclusive, + subtree_start_height, + subtree_end_height, + contains_marked, + MAX(priority) AS max_priority +FROM v_orchard_shard_scan_ranges +GROUP BY + shard_index, + start_position, + end_position_exclusive, + subtree_start_height, + subtree_end_height, + contains_marked"; diff --git a/zcash_client_sqlite/src/wallet/init.rs b/zcash_client_sqlite/src/wallet/init.rs index f21f53182c..91fbe38322 100644 --- a/zcash_client_sqlite/src/wallet/init.rs +++ b/zcash_client_sqlite/src/wallet/init.rs @@ -1,315 +1,1136 @@ //! Functions for initializing the various databases. -use rusqlite::{types::ToSql, NO_PARAMS}; +use std::borrow::BorrowMut; +use std::fmt; +use std::rc::Rc; -use zcash_primitives::{ - block::BlockHash, - consensus::{self, BlockHeight}, - zip32::ExtendedFullViewingKey, -}; +use regex::Regex; +use schemerz::{Migrator, MigratorError}; +use schemerz_rusqlite::RusqliteAdapter; +use secrecy::SecretVec; +use shardtree::error::ShardTreeError; +use uuid::Uuid; -use zcash_client_backend::encoding::encode_extended_full_viewing_key; +use zcash_client_backend::data_api::{SeedRelevance, WalletRead}; +use zcash_keys::keys::AddressGenerationError; +use zcash_protocol::{consensus, value::BalanceError}; -use crate::{address_from_extfvk, error::SqliteClientError, WalletDb}; +use self::migrations::verify_network_compatibility; + +use super::commitment_tree; +use crate::{error::SqliteClientError, WalletDb}; + +mod migrations; + +const SQLITE_MAJOR_VERSION: u32 = 3; +const MIN_SQLITE_MINOR_VERSION: u32 = 35; + +const MIGRATIONS_TABLE: &str = "schemer_migrations"; + +#[derive(Debug)] +pub enum WalletMigrationError { + /// A feature required by the wallet database is not supported by the version of + /// SQLite that the migration is running against. + DatabaseNotSupported(String), + + /// The seed is required for the migration. + SeedRequired, + + /// A seed was provided that is not relevant to any of the accounts within the wallet. + /// + /// Specifically, it is not relevant to any account for which [`Account::source`] is + /// [`AccountSource::Derived`]. We do not check whether the seed is relevant to any + /// imported account, because that would require brute-forcing the ZIP 32 account + /// index space. + /// + /// [`Account::source`]: zcash_client_backend::data_api::Account::source + /// [`AccountSource::Derived`]: zcash_client_backend::data_api::AccountSource::Derived + SeedNotRelevant, + + /// Decoding of an existing value from its serialized form has failed. + CorruptedData(String), + + /// An error occurred in migrating a Zcash address or key. + AddressGeneration(AddressGenerationError), + + /// Wrapper for rusqlite errors. + DbError(rusqlite::Error), + + /// Wrapper for amount balance violations + BalanceError(BalanceError), + + /// Wrapper for commitment tree invariant violations + CommitmentTree(ShardTreeError), + + /// Reverting the specified migration is not supported. + CannotRevert(Uuid), + + /// Some other unexpected violation of database business rules occurred + Other(SqliteClientError), +} + +impl From for WalletMigrationError { + fn from(e: rusqlite::Error) -> Self { + WalletMigrationError::DbError(e) + } +} + +impl From for WalletMigrationError { + fn from(e: BalanceError) -> Self { + WalletMigrationError::BalanceError(e) + } +} + +impl From> for WalletMigrationError { + fn from(e: ShardTreeError) -> Self { + WalletMigrationError::CommitmentTree(e) + } +} + +impl From for WalletMigrationError { + fn from(e: AddressGenerationError) -> Self { + WalletMigrationError::AddressGeneration(e) + } +} + +impl From for WalletMigrationError { + fn from(value: SqliteClientError) -> Self { + match value { + SqliteClientError::CorruptedData(err) => WalletMigrationError::CorruptedData(err), + SqliteClientError::DbError(err) => WalletMigrationError::DbError(err), + SqliteClientError::CommitmentTree(err) => WalletMigrationError::CommitmentTree(err), + SqliteClientError::BalanceError(err) => WalletMigrationError::BalanceError(err), + SqliteClientError::AddressGeneration(err) => { + WalletMigrationError::AddressGeneration(err) + } + other => WalletMigrationError::Other(other), + } + } +} + +impl fmt::Display for WalletMigrationError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self { + WalletMigrationError::DatabaseNotSupported(version) => { + write!( + f, + "The installed SQLite version {} does not support operations required by the wallet.", + version + ) + } + WalletMigrationError::SeedRequired => { + write!( + f, + "The wallet seed is required in order to update the database." + ) + } + WalletMigrationError::SeedNotRelevant => { + write!( + f, + "The provided seed is not relevant to any derived accounts in the database." + ) + } + WalletMigrationError::CorruptedData(reason) => { + write!(f, "Wallet database is corrupted: {}", reason) + } + WalletMigrationError::DbError(e) => write!(f, "{}", e), + WalletMigrationError::BalanceError(e) => write!(f, "Balance error: {:?}", e), + WalletMigrationError::CommitmentTree(e) => write!(f, "Commitment tree error: {:?}", e), + WalletMigrationError::AddressGeneration(e) => { + write!(f, "Address generation error: {:?}", e) + } + WalletMigrationError::CannotRevert(uuid) => { + write!(f, "Reverting migration {} is not supported", uuid) + } + WalletMigrationError::Other(err) => { + write!( + f, + "Unexpected violation of database business rules: {}", + err + ) + } + } + } +} + +impl std::error::Error for WalletMigrationError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match &self { + WalletMigrationError::DbError(e) => Some(e), + WalletMigrationError::BalanceError(e) => Some(e), + WalletMigrationError::CommitmentTree(e) => Some(e), + WalletMigrationError::AddressGeneration(e) => Some(e), + WalletMigrationError::Other(e) => Some(e), + _ => None, + } + } +} + +/// Helper to enable calling regular `WalletDb` methods inside the migration code. +/// +/// In this context we can know the full set of errors that are generated by any call we +/// make, so we mark errors as unreachable instead of adding new `WalletMigrationError` +/// variants. +fn sqlite_client_error_to_wallet_migration_error(e: SqliteClientError) -> WalletMigrationError { + match e { + SqliteClientError::CorruptedData(e) => WalletMigrationError::CorruptedData(e), + SqliteClientError::Protobuf(e) => WalletMigrationError::CorruptedData(e.to_string()), + SqliteClientError::InvalidNote => { + WalletMigrationError::CorruptedData("invalid note".into()) + } + SqliteClientError::DecodingError(e) => WalletMigrationError::CorruptedData(e.to_string()), + #[cfg(feature = "transparent-inputs")] + SqliteClientError::TransparentDerivation(e) => { + WalletMigrationError::CorruptedData(e.to_string()) + } + #[cfg(feature = "transparent-inputs")] + SqliteClientError::TransparentAddress(e) => { + WalletMigrationError::CorruptedData(e.to_string()) + } + SqliteClientError::DbError(e) => WalletMigrationError::DbError(e), + SqliteClientError::Io(e) => WalletMigrationError::CorruptedData(e.to_string()), + SqliteClientError::InvalidMemo(e) => WalletMigrationError::CorruptedData(e.to_string()), + SqliteClientError::AddressGeneration(e) => WalletMigrationError::AddressGeneration(e), + SqliteClientError::BadAccountData(e) => WalletMigrationError::CorruptedData(e), + SqliteClientError::CommitmentTree(e) => WalletMigrationError::CommitmentTree(e), + SqliteClientError::UnsupportedPoolType(pool) => WalletMigrationError::CorruptedData( + format!("Wallet DB contains unsupported pool type {}", pool), + ), + SqliteClientError::BalanceError(e) => WalletMigrationError::BalanceError(e), + SqliteClientError::TableNotEmpty => unreachable!("wallet already initialized"), + SqliteClientError::BlockConflict(_) + | SqliteClientError::NonSequentialBlocks + | SqliteClientError::RequestedRewindInvalid { .. } + | SqliteClientError::KeyDerivationError(_) + | SqliteClientError::Zip32AccountIndexOutOfRange + | SqliteClientError::AccountCollision(_) + | SqliteClientError::CacheMiss(_) => { + unreachable!("we only call WalletRead methods; mutations can't occur") + } + #[cfg(feature = "transparent-inputs")] + SqliteClientError::AddressNotRecognized(_) => { + unreachable!("we only call WalletRead methods; mutations can't occur") + } + SqliteClientError::AccountUnknown => { + unreachable!("all accounts are known in migration context") + } + SqliteClientError::UnknownZip32Derivation => { + unreachable!("we don't call methods that require operating on imported accounts") + } + SqliteClientError::ChainHeightUnknown => { + unreachable!("we don't call methods that require a known chain height") + } + #[cfg(feature = "transparent-inputs")] + SqliteClientError::ReachedGapLimit(_, _) => { + unreachable!("we don't do ephemeral address tracking") + } + #[cfg(feature = "transparent-inputs")] + SqliteClientError::EphemeralAddressReuse(_, _) => { + unreachable!("we don't do ephemeral address tracking") + } + SqliteClientError::NoteFilterInvalid(_) => { + unreachable!("we don't do note selection in migrations") + } + } +} /// Sets up the internal structure of the data database. /// -/// # Examples +/// This procedure will automatically perform migration operations to update the wallet database to +/// the database structure required by the current version of this library, and should be invoked +/// at least once any time a client program upgrades to a new version of this library. The +/// operation of this procedure is idempotent, so it is safe (though not required) to invoke this +/// operation every time the wallet is opened. /// -/// ``` -/// use tempfile::NamedTempFile; -/// use zcash_primitives::consensus::Network; -/// use zcash_client_sqlite::{ -/// WalletDb, -/// wallet::init::init_wallet_db, -/// }; +/// In order to correctly apply migrations to accounts derived from a seed, sometimes the +/// optional `seed` argument is required. This function should first be invoked with +/// `seed` set to `None`; if a pending migration requires the seed, the function returns +/// `Err(schemerz::MigratorError::Migration { error: WalletMigrationError::SeedRequired, .. })`. +/// The caller can then re-call this function with the necessary seed. /// -/// let data_file = NamedTempFile::new().unwrap(); -/// let db = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap(); -/// init_wallet_db(&db).unwrap(); -/// ``` -pub fn init_wallet_db

(wdb: &WalletDb

) -> Result<(), rusqlite::Error> { - wdb.conn.execute( - "CREATE TABLE IF NOT EXISTS accounts ( - account INTEGER PRIMARY KEY, - extfvk TEXT NOT NULL, - address TEXT NOT NULL - )", - NO_PARAMS, - )?; - wdb.conn.execute( - "CREATE TABLE IF NOT EXISTS blocks ( - height INTEGER PRIMARY KEY, - hash BLOB NOT NULL, - time INTEGER NOT NULL, - sapling_tree BLOB NOT NULL - )", - NO_PARAMS, - )?; - wdb.conn.execute( - "CREATE TABLE IF NOT EXISTS transactions ( - id_tx INTEGER PRIMARY KEY, - txid BLOB NOT NULL UNIQUE, - created TEXT, - block INTEGER, - tx_index INTEGER, - expiry_height INTEGER, - raw BLOB, - FOREIGN KEY (block) REFERENCES blocks(height) - )", - NO_PARAMS, - )?; - wdb.conn.execute( - "CREATE TABLE IF NOT EXISTS received_notes ( - id_note INTEGER PRIMARY KEY, - tx INTEGER NOT NULL, - output_index INTEGER NOT NULL, - account INTEGER NOT NULL, - diversifier BLOB NOT NULL, - value INTEGER NOT NULL, - rcm BLOB NOT NULL, - nf BLOB NOT NULL UNIQUE, - is_change INTEGER NOT NULL, - memo BLOB, - spent INTEGER, - FOREIGN KEY (tx) REFERENCES transactions(id_tx), - FOREIGN KEY (account) REFERENCES accounts(account), - FOREIGN KEY (spent) REFERENCES transactions(id_tx), - CONSTRAINT tx_output UNIQUE (tx, output_index) - )", - NO_PARAMS, - )?; - wdb.conn.execute( - "CREATE TABLE IF NOT EXISTS sapling_witnesses ( - id_witness INTEGER PRIMARY KEY, - note INTEGER NOT NULL, - block INTEGER NOT NULL, - witness BLOB NOT NULL, - FOREIGN KEY (note) REFERENCES received_notes(id_note), - FOREIGN KEY (block) REFERENCES blocks(height), - CONSTRAINT witness_height UNIQUE (note, block) - )", - NO_PARAMS, - )?; - wdb.conn.execute( - "CREATE TABLE IF NOT EXISTS sent_notes ( - id_note INTEGER PRIMARY KEY, - tx INTEGER NOT NULL, - output_index INTEGER NOT NULL, - from_account INTEGER NOT NULL, - address TEXT NOT NULL, - value INTEGER NOT NULL, - memo BLOB, - FOREIGN KEY (tx) REFERENCES transactions(id_tx), - FOREIGN KEY (from_account) REFERENCES accounts(account), - CONSTRAINT tx_output UNIQUE (tx, output_index) - )", - NO_PARAMS, - )?; - Ok(()) -} - -/// Initialises the data database with the given [`ExtendedFullViewingKey`]s. +/// > Note that currently only one seed can be provided; as such, wallets containing +/// > accounts derived from several different seeds are unsupported, and will result in an +/// > error. Support for multi-seed wallets is being tracked in [zcash/librustzcash#1284]. /// -/// The [`ExtendedFullViewingKey`]s are stored internally and used by other APIs such as -/// [`get_address`], [`scan_cached_blocks`], and [`create_spend_to_address`]. `extfvks` **MUST** -/// be arranged in account-order; that is, the [`ExtendedFullViewingKey`] for ZIP 32 -/// account `i` **MUST** be at `extfvks[i]`. +/// When the `seed` argument is provided, the seed is checked against the database for +/// _relevance_: if any account in the wallet for which [`Account::source`] is +/// [`AccountSource::Derived`] can be derived from the given seed, the seed is relevant to +/// the wallet. If the given seed is not relevant, the function returns +/// `Err(schemerz::MigratorError::Migration { error: WalletMigrationError::SeedNotRelevant, .. })` +/// or `Err(schemerz::MigratorError::Adapter(WalletMigrationError::SeedNotRelevant))`. /// -/// # Examples +/// We do not check whether the seed is relevant to any imported account, because that +/// would require brute-forcing the ZIP 32 account index space. Consequentially, imported +/// accounts are not migrated. /// -/// ``` -/// use tempfile::NamedTempFile; +/// It is safe to use a wallet database previously created without the ability to create +/// transparent spends with a build that enables transparent spends (via use of the +/// `transparent-inputs` feature flag.) The reverse is unsafe, as wallet balance calculations would +/// ignore the transparent UTXOs already controlled by the wallet. /// -/// use zcash_primitives::{ -/// consensus::Network, -/// zip32::{ExtendedFullViewingKey, ExtendedSpendingKey} -/// }; +/// [zcash/librustzcash#1284]: https://github.com/zcash/librustzcash/issues/1284 +/// [`Account::source`]: zcash_client_backend::data_api::Account::source +/// [`AccountSource::Derived`]: zcash_client_backend::data_api::AccountSource::Derived +/// +/// # Examples /// +/// ``` +/// # use std::error::Error; +/// # use secrecy::SecretVec; +/// # use tempfile::NamedTempFile; +/// use zcash_protocol::consensus::Network; /// use zcash_client_sqlite::{ /// WalletDb, -/// wallet::init::{init_accounts_table, init_wallet_db} +/// wallet::init::{WalletMigrationError, init_wallet_db}, /// }; /// -/// let data_file = NamedTempFile::new().unwrap(); -/// let db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap(); -/// init_wallet_db(&db_data).unwrap(); -/// -/// let extsk = ExtendedSpendingKey::master(&[]); -/// let extfvks = [ExtendedFullViewingKey::from(&extsk)]; -/// init_accounts_table(&db_data, &extfvks).unwrap(); +/// # fn main() -> Result<(), Box> { +/// # let data_file = NamedTempFile::new().unwrap(); +/// # let get_data_db_path = || data_file.path(); +/// # let load_seed = || -> Result<_, String> { Ok(SecretVec::new(vec![])) }; +/// let mut db = WalletDb::for_path(get_data_db_path(), Network::TestNetwork)?; +/// match init_wallet_db(&mut db, None) { +/// Err(e) +/// if matches!( +/// e.source().and_then(|e| e.downcast_ref()), +/// Some(&WalletMigrationError::SeedRequired) +/// ) => +/// { +/// let seed = load_seed()?; +/// init_wallet_db(&mut db, Some(seed)) +/// } +/// res => res, +/// }?; +/// # Ok(()) +/// # } /// ``` -/// -/// [`get_address`]: crate::wallet::get_address -/// [`scan_cached_blocks`]: zcash_client_backend::data_api::chain::scan_cached_blocks -/// [`create_spend_to_address`]: zcash_client_backend::data_api::wallet::create_spend_to_address -pub fn init_accounts_table( - wdb: &WalletDb

, - extfvks: &[ExtendedFullViewingKey], -) -> Result<(), SqliteClientError> { - let mut empty_check = wdb.conn.prepare("SELECT * FROM accounts LIMIT 1")?; - if empty_check.exists(NO_PARAMS)? { - return Err(SqliteClientError::TableNotEmpty); - } +// TODO: It would be possible to make the transition from providing transparent support to no +// longer providing transparent support safe, by including a migration that verifies that no +// unspent transparent outputs exist in the wallet at the time of upgrading to a version of +// the library that does not support transparent use. It might be a good idea to add an explicit +// check for unspent transparent outputs whenever running initialization with a version of the +// library *not* compiled with the `transparent-inputs` feature flag, and fail if any are present. +pub fn init_wallet_db, P: consensus::Parameters + 'static>( + wdb: &mut WalletDb, + seed: Option>, +) -> Result<(), MigratorError> { + init_wallet_db_internal(wdb, seed, &[], true) +} + +pub(crate) fn init_wallet_db_internal< + C: BorrowMut, + P: consensus::Parameters + 'static, +>( + wdb: &mut WalletDb, + seed: Option>, + target_migrations: &[Uuid], + verify_seed_relevance: bool, +) -> Result<(), MigratorError> { + let seed = seed.map(Rc::new); - // Insert accounts atomically - wdb.conn.execute("BEGIN IMMEDIATE", NO_PARAMS)?; - for (account, extfvk) in extfvks.iter().enumerate() { - let extfvk_str = encode_extended_full_viewing_key( - wdb.params.hrp_sapling_extended_full_viewing_key(), - extfvk, + verify_sqlite_version_compatibility(wdb.conn.borrow()).map_err(MigratorError::Adapter)?; + + // Turn off foreign key enforcement, to ensure that table replacement does not break foreign + // key references in table definitions. + // + // It is necessary to perform this operation globally using the outer connection because this + // pragma has no effect when set or unset within a transaction. + wdb.conn + .borrow() + .execute_batch("PRAGMA foreign_keys = OFF;") + .map_err(|e| MigratorError::Adapter(WalletMigrationError::from(e)))?; + + // Temporarily take ownership of the connection in a wrapper to perform the initial migration + // table setup. This extra adapter creation could be omitted if `RusqliteAdapter` provided an + // accessor for the connection that it wraps, or if it provided a mechanism to query to + // determine whether a given migration has been applied. (see + // https://github.com/zcash/schemerz/issues/6) + { + let adapter = RusqliteAdapter::<'_, WalletMigrationError>::new( + wdb.conn.borrow_mut(), + Some(MIGRATIONS_TABLE.to_string()), ); + adapter.init().expect("Migrations table setup succeeds."); + } + + // Now that we are certain that the migrations table exists, verify that if the database + // already contains account data, any stored UFVKs correspond to the same network that the + // migrations are being run for. + verify_network_compatibility(wdb.conn.borrow(), &wdb.params).map_err(MigratorError::Adapter)?; - let address_str = address_from_extfvk(&wdb.params, extfvk); - - wdb.conn.execute( - "INSERT INTO accounts (account, extfvk, address) - VALUES (?, ?, ?)", - &[ - (account as u32).to_sql()?, - extfvk_str.to_sql()?, - address_str.to_sql()?, - ], - )?; + // Now create the adapter that we're actually going to use to perform the migrations, and + // proceed. + let adapter = RusqliteAdapter::new(wdb.conn.borrow_mut(), Some(MIGRATIONS_TABLE.to_string())); + let mut migrator = Migrator::new(adapter); + migrator + .register_multiple(migrations::all_migrations(&wdb.params, seed.clone()).into_iter()) + .expect("Wallet migration registration should have been successful."); + if target_migrations.is_empty() { + migrator.up(None)?; + } else { + for target_migration in target_migrations { + migrator.up(Some(*target_migration))?; + } + } + wdb.conn + .borrow() + .execute("PRAGMA foreign_keys = ON", []) + .map_err(|e| MigratorError::Adapter(WalletMigrationError::from(e)))?; + + // Now that the migration succeeded, check whether the seed is relevant to the wallet. + // We can only check this if we have migrated as far as `full_account_ids::MIGRATION_ID`, + // but unfortunately `schemer` does not currently expose its DAG of migrations. As a + // consequence, the caller has to choose whether or not this check should be performed + // based upon which migrations they're asking to apply. + if verify_seed_relevance { + if let Some(seed) = seed { + match wdb + .seed_relevance_to_derived_accounts(&seed) + .map_err(sqlite_client_error_to_wallet_migration_error)? + { + SeedRelevance::Relevant { .. } => (), + // Every seed is relevant to a wallet with no accounts; this is most likely a + // new wallet database being initialized for the first time. + SeedRelevance::NoAccounts => (), + // No seed is relevant to a wallet that only has imported accounts. + SeedRelevance::NotRelevant | SeedRelevance::NoDerivedAccounts => { + return Err(WalletMigrationError::SeedNotRelevant.into()) + } + } + } } - wdb.conn.execute("COMMIT", NO_PARAMS)?; Ok(()) } -/// Initialises the data database with the given block. -/// -/// This enables a newly-created database to be immediately-usable, without needing to -/// synchronise historic blocks. -/// -/// # Examples -/// -/// ``` -/// use tempfile::NamedTempFile; -/// use zcash_primitives::{ -/// block::BlockHash, -/// consensus::{BlockHeight, Network}, -/// }; -/// use zcash_client_sqlite::{ -/// WalletDb, -/// wallet::init::init_blocks_table, -/// }; -/// -/// // The block height. -/// let height = BlockHeight::from_u32(500_000); -/// // The hash of the block header. -/// let hash = BlockHash([0; 32]); -/// // The nTime field from the block header. -/// let time = 12_3456_7890; -/// // The serialized Sapling commitment tree as of this block. -/// // Pre-compute and hard-code, or obtain from a service. -/// let sapling_tree = &[]; -/// -/// let data_file = NamedTempFile::new().unwrap(); -/// let db = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap(); -/// init_blocks_table(&db, height, hash, time, sapling_tree); -/// ``` -pub fn init_blocks_table

( - wdb: &WalletDb

, - height: BlockHeight, - hash: BlockHash, - time: u32, - sapling_tree: &[u8], -) -> Result<(), SqliteClientError> { - let mut empty_check = wdb.conn.prepare("SELECT * FROM blocks LIMIT 1")?; - if empty_check.exists(NO_PARAMS)? { - return Err(SqliteClientError::TableNotEmpty); - } +/// Verify that the sqlite version in use supports the features required by this library. +/// Note that the version of sqlite available to the database backend may be different +/// from what is used to query the views that are part of the public API. +fn verify_sqlite_version_compatibility( + conn: &rusqlite::Connection, +) -> Result<(), WalletMigrationError> { + let sqlite_version = + conn.query_row("SELECT sqlite_version()", [], |row| row.get::<_, String>(0))?; - wdb.conn.execute( - "INSERT INTO blocks (height, hash, time, sapling_tree) - VALUES (?, ?, ?, ?)", - &[ - u32::from(height).to_sql()?, - hash.0.to_sql()?, - time.to_sql()?, - sapling_tree.to_sql()?, - ], - )?; + let version_re = Regex::new(r"^(?[0-9]+)\.(?[0-9]+).*$").unwrap(); + let captures = + version_re + .captures(&sqlite_version) + .ok_or(WalletMigrationError::DatabaseNotSupported( + "Unknown".to_owned(), + ))?; + let parse_version_part = |part: &str| { + captures[part].parse::().map_err(|_| { + WalletMigrationError::CorruptedData(format!( + "Cannot decode SQLite {} version component {}", + part, &captures[part] + )) + }) + }; + let major = parse_version_part("major")?; + let minor = parse_version_part("minor")?; - Ok(()) + if major != SQLITE_MAJOR_VERSION || minor < MIN_SQLITE_MINOR_VERSION { + Err(WalletMigrationError::DatabaseNotSupported(sqlite_version)) + } else { + Ok(()) + } } #[cfg(test)] mod tests { + use rusqlite::{self, named_params, Connection, ToSql}; + use secrecy::Secret; + use tempfile::NamedTempFile; - use zcash_primitives::{ - block::BlockHash, - consensus::BlockHeight, - zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, + use ::sapling::zip32::ExtendedFullViewingKey; + use zcash_client_backend::data_api::testing::TestBuilder; + use zcash_keys::{ + address::Address, + encoding::{encode_extended_full_viewing_key, encode_payment_address}, + keys::{ + sapling, ReceiverRequirement::*, UnifiedAddressRequest, UnifiedFullViewingKey, + UnifiedSpendingKey, + }, + }; + use zcash_primitives::transaction::{TransactionData, TxVersion}; + use zcash_protocol::consensus::{self, BlockHeight, BranchId, Network, NetworkConstants}; + use zip32::AccountId; + + use crate::{testing::db::TestDbFactory, wallet::db, WalletDb, UA_TRANSPARENT}; + + use super::init_wallet_db; + + #[cfg(feature = "transparent-inputs")] + use { + super::WalletMigrationError, + crate::wallet::{self, pool_code, PoolType}, + zcash_address::test_vectors, + zcash_client_backend::data_api::WalletWrite, + zip32::DiversifierIndex, }; - use crate::{tests, wallet::get_address, AccountId, WalletDb}; + pub(crate) fn describe_tables(conn: &Connection) -> Result, rusqlite::Error> { + let result = conn + .prepare("SELECT sql FROM sqlite_schema WHERE type = 'table' ORDER BY tbl_name")? + .query_and_then([], |row| row.get::<_, String>(0))? + .collect::, _>>()?; - use super::{init_accounts_table, init_blocks_table, init_wallet_db}; + Ok(result) + } #[test] - fn init_accounts_table_only_works_once() { + fn verify_schema() { + let st = TestBuilder::new() + .with_data_store_factory(TestDbFactory::default()) + .build(); + + use regex::Regex; + let re = Regex::new(r"\s+").unwrap(); + + let expected_tables = vec![ + db::TABLE_ACCOUNTS, + db::TABLE_ADDRESSES, + db::TABLE_BLOCKS, + db::TABLE_EPHEMERAL_ADDRESSES, + db::TABLE_NULLIFIER_MAP, + db::TABLE_ORCHARD_RECEIVED_NOTE_SPENDS, + db::TABLE_ORCHARD_RECEIVED_NOTES, + db::TABLE_ORCHARD_TREE_CAP, + db::TABLE_ORCHARD_TREE_CHECKPOINT_MARKS_REMOVED, + db::TABLE_ORCHARD_TREE_CHECKPOINTS, + db::TABLE_ORCHARD_TREE_SHARDS, + db::TABLE_SAPLING_RECEIVED_NOTE_SPENDS, + db::TABLE_SAPLING_RECEIVED_NOTES, + db::TABLE_SAPLING_TREE_CAP, + db::TABLE_SAPLING_TREE_CHECKPOINT_MARKS_REMOVED, + db::TABLE_SAPLING_TREE_CHECKPOINTS, + db::TABLE_SAPLING_TREE_SHARDS, + db::TABLE_SCAN_QUEUE, + db::TABLE_SCHEMERZ_MIGRATIONS, + db::TABLE_SENT_NOTES, + db::TABLE_SQLITE_SEQUENCE, + db::TABLE_TRANSACTIONS, + db::TABLE_TRANSPARENT_RECEIVED_OUTPUT_SPENDS, + db::TABLE_TRANSPARENT_RECEIVED_OUTPUTS, + db::TABLE_TRANSPARENT_SPEND_MAP, + db::TABLE_TRANSPARENT_SPEND_SEARCH_QUEUE, + db::TABLE_TX_LOCATOR_MAP, + db::TABLE_TX_RETRIEVAL_QUEUE, + ]; + + let rows = describe_tables(&st.wallet().db().conn).unwrap(); + assert_eq!(rows.len(), expected_tables.len()); + for (actual, expected) in rows.iter().zip(expected_tables.iter()) { + assert_eq!( + re.replace_all(actual, " "), + re.replace_all(expected, " ").trim(), + ); + } + + let expected_indices = vec![ + db::INDEX_ACCOUNTS_UFVK, + db::INDEX_ACCOUNTS_UIVK, + db::INDEX_ACCOUNTS_UUID, + db::INDEX_HD_ACCOUNT, + db::INDEX_ADDRESSES_ACCOUNTS, + db::INDEX_NF_MAP_LOCATOR_IDX, + db::INDEX_ORCHARD_RECEIVED_NOTES_ACCOUNT, + db::INDEX_ORCHARD_RECEIVED_NOTES_TX, + db::INDEX_SAPLING_RECEIVED_NOTES_ACCOUNT, + db::INDEX_SAPLING_RECEIVED_NOTES_TX, + db::INDEX_SENT_NOTES_FROM_ACCOUNT, + db::INDEX_SENT_NOTES_TO_ACCOUNT, + db::INDEX_SENT_NOTES_TX, + db::INDEX_TRANSPARENT_RECEIVED_OUTPUTS_ACCOUNT_ID, + ]; + let mut indices_query = st + .wallet() + .db() + .conn + .prepare("SELECT sql FROM sqlite_master WHERE type = 'index' AND sql != '' ORDER BY tbl_name, name") + .unwrap(); + let mut rows = indices_query.query([]).unwrap(); + let mut expected_idx = 0; + while let Some(row) = rows.next().unwrap() { + let sql: String = row.get(0).unwrap(); + assert_eq!( + re.replace_all(&sql, " "), + re.replace_all(expected_indices[expected_idx], " ").trim(), + ); + expected_idx += 1; + } + + let expected_views = vec![ + db::view_orchard_shard_scan_ranges(st.network()), + db::view_orchard_shard_unscanned_ranges(), + db::VIEW_ORCHARD_SHARDS_SCAN_STATE.to_owned(), + db::VIEW_RECEIVED_OUTPUT_SPENDS.to_owned(), + db::VIEW_RECEIVED_OUTPUTS.to_owned(), + db::view_sapling_shard_scan_ranges(st.network()), + db::view_sapling_shard_unscanned_ranges(), + db::VIEW_SAPLING_SHARDS_SCAN_STATE.to_owned(), + db::VIEW_TRANSACTIONS.to_owned(), + db::VIEW_TX_OUTPUTS.to_owned(), + ]; + + let mut views_query = st + .wallet() + .db() + .conn + .prepare("SELECT sql FROM sqlite_schema WHERE type = 'view' ORDER BY tbl_name") + .unwrap(); + let mut rows = views_query.query([]).unwrap(); + let mut expected_idx = 0; + while let Some(row) = rows.next().unwrap() { + let sql: String = row.get(0).unwrap(); + assert_eq!( + re.replace_all(&sql, " "), + re.replace_all(&expected_views[expected_idx], " ").trim(), + ); + expected_idx += 1; + } + } + + #[test] + fn init_migrate_from_0_3_0() { + fn init_0_3_0( + wdb: &mut WalletDb, + extfvk: &ExtendedFullViewingKey, + account: AccountId, + ) -> Result<(), rusqlite::Error> { + wdb.conn.execute( + "CREATE TABLE accounts ( + account INTEGER PRIMARY KEY, + extfvk TEXT NOT NULL, + address TEXT NOT NULL + )", + [], + )?; + wdb.conn.execute( + "CREATE TABLE blocks ( + height INTEGER PRIMARY KEY, + hash BLOB NOT NULL, + time INTEGER NOT NULL, + sapling_tree BLOB NOT NULL + )", + [], + )?; + wdb.conn.execute( + "CREATE TABLE transactions ( + id_tx INTEGER PRIMARY KEY, + txid BLOB NOT NULL UNIQUE, + created TEXT, + block INTEGER, + tx_index INTEGER, + expiry_height INTEGER, + raw BLOB, + FOREIGN KEY (block) REFERENCES blocks(height) + )", + [], + )?; + wdb.conn.execute( + "CREATE TABLE received_notes ( + id_note INTEGER PRIMARY KEY, + tx INTEGER NOT NULL, + output_index INTEGER NOT NULL, + account INTEGER NOT NULL, + diversifier BLOB NOT NULL, + value INTEGER NOT NULL, + rcm BLOB NOT NULL, + nf BLOB NOT NULL UNIQUE, + is_change INTEGER NOT NULL, + memo BLOB, + spent INTEGER, + FOREIGN KEY (tx) REFERENCES transactions(id_tx), + FOREIGN KEY (account) REFERENCES accounts(account), + FOREIGN KEY (spent) REFERENCES transactions(id_tx), + CONSTRAINT tx_output UNIQUE (tx, output_index) + )", + [], + )?; + wdb.conn.execute( + "CREATE TABLE sapling_witnesses ( + id_witness INTEGER PRIMARY KEY, + note INTEGER NOT NULL, + block INTEGER NOT NULL, + witness BLOB NOT NULL, + FOREIGN KEY (note) REFERENCES received_notes(id_note), + FOREIGN KEY (block) REFERENCES blocks(height), + CONSTRAINT witness_height UNIQUE (note, block) + )", + [], + )?; + wdb.conn.execute( + "CREATE TABLE sent_notes ( + id_note INTEGER PRIMARY KEY, + tx INTEGER NOT NULL, + output_index INTEGER NOT NULL, + from_account INTEGER NOT NULL, + address TEXT NOT NULL, + value INTEGER NOT NULL, + memo BLOB, + FOREIGN KEY (tx) REFERENCES transactions(id_tx), + FOREIGN KEY (from_account) REFERENCES accounts(account), + CONSTRAINT tx_output UNIQUE (tx, output_index) + )", + [], + )?; + + let address = encode_payment_address( + wdb.params.hrp_sapling_payment_address(), + &extfvk.default_address().1, + ); + let extfvk = encode_extended_full_viewing_key( + wdb.params.hrp_sapling_extended_full_viewing_key(), + extfvk, + ); + wdb.conn.execute( + "INSERT INTO accounts (account, extfvk, address) + VALUES (?, ?, ?)", + [ + u32::from(account).to_sql()?, + extfvk.to_sql()?, + address.to_sql()?, + ], + )?; + + Ok(()) + } + let data_file = NamedTempFile::new().unwrap(); - let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&db_data).unwrap(); - - // We can call the function as many times as we want with no data - init_accounts_table(&db_data, &[]).unwrap(); - init_accounts_table(&db_data, &[]).unwrap(); - - // First call with data should initialise the accounts table - let extfvks = [ExtendedFullViewingKey::from(&ExtendedSpendingKey::master( - &[], - ))]; - init_accounts_table(&db_data, &extfvks).unwrap(); - - // Subsequent calls should return an error - init_accounts_table(&db_data, &[]).unwrap_err(); - init_accounts_table(&db_data, &extfvks).unwrap_err(); + let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap(); + + let seed = [0xab; 32]; + let account = AccountId::ZERO; + let secret_key = sapling::spending_key(&seed, db_data.params.coin_type(), account); + #[allow(deprecated)] + let extfvk = secret_key.to_extended_full_viewing_key(); + + init_0_3_0(&mut db_data, &extfvk, account).unwrap(); + assert_matches!( + init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))), + Ok(_) + ); } #[test] - fn init_blocks_table_only_works_once() { + fn init_migrate_from_autoshielding_poc() { + fn init_autoshielding( + wdb: &mut WalletDb, + extfvk: &ExtendedFullViewingKey, + account: AccountId, + ) -> Result<(), rusqlite::Error> { + wdb.conn.execute( + "CREATE TABLE accounts ( + account INTEGER PRIMARY KEY, + extfvk TEXT NOT NULL, + address TEXT NOT NULL, + transparent_address TEXT NOT NULL + )", + [], + )?; + wdb.conn.execute( + "CREATE TABLE blocks ( + height INTEGER PRIMARY KEY, + hash BLOB NOT NULL, + time INTEGER NOT NULL, + sapling_tree BLOB NOT NULL + )", + [], + )?; + wdb.conn.execute( + "CREATE TABLE transactions ( + id_tx INTEGER PRIMARY KEY, + txid BLOB NOT NULL UNIQUE, + created TEXT, + block INTEGER, + tx_index INTEGER, + expiry_height INTEGER, + raw BLOB, + FOREIGN KEY (block) REFERENCES blocks(height) + )", + [], + )?; + wdb.conn.execute( + "CREATE TABLE received_notes ( + id_note INTEGER PRIMARY KEY, + tx INTEGER NOT NULL, + output_index INTEGER NOT NULL, + account INTEGER NOT NULL, + diversifier BLOB NOT NULL, + value INTEGER NOT NULL, + rcm BLOB NOT NULL, + nf BLOB NOT NULL UNIQUE, + is_change INTEGER NOT NULL, + memo BLOB, + spent INTEGER, + FOREIGN KEY (tx) REFERENCES transactions(id_tx), + FOREIGN KEY (account) REFERENCES accounts(account), + FOREIGN KEY (spent) REFERENCES transactions(id_tx), + CONSTRAINT tx_output UNIQUE (tx, output_index) + )", + [], + )?; + wdb.conn.execute( + "CREATE TABLE sapling_witnesses ( + id_witness INTEGER PRIMARY KEY, + note INTEGER NOT NULL, + block INTEGER NOT NULL, + witness BLOB NOT NULL, + FOREIGN KEY (note) REFERENCES received_notes(id_note), + FOREIGN KEY (block) REFERENCES blocks(height), + CONSTRAINT witness_height UNIQUE (note, block) + )", + [], + )?; + wdb.conn.execute( + "CREATE TABLE sent_notes ( + id_note INTEGER PRIMARY KEY, + tx INTEGER NOT NULL, + output_index INTEGER NOT NULL, + from_account INTEGER NOT NULL, + address TEXT NOT NULL, + value INTEGER NOT NULL, + memo BLOB, + FOREIGN KEY (tx) REFERENCES transactions(id_tx), + FOREIGN KEY (from_account) REFERENCES accounts(account), + CONSTRAINT tx_output UNIQUE (tx, output_index) + )", + [], + )?; + wdb.conn.execute( + "CREATE TABLE utxos ( + id_utxo INTEGER PRIMARY KEY, + address TEXT NOT NULL, + prevout_txid BLOB NOT NULL, + prevout_idx INTEGER NOT NULL, + script BLOB NOT NULL, + value_zat INTEGER NOT NULL, + height INTEGER NOT NULL, + spent_in_tx INTEGER, + FOREIGN KEY (spent_in_tx) REFERENCES transactions(id_tx), + CONSTRAINT tx_outpoint UNIQUE (prevout_txid, prevout_idx) + )", + [], + )?; + + let address = encode_payment_address( + wdb.params.hrp_sapling_payment_address(), + &extfvk.default_address().1, + ); + let extfvk = encode_extended_full_viewing_key( + wdb.params.hrp_sapling_extended_full_viewing_key(), + extfvk, + ); + wdb.conn.execute( + "INSERT INTO accounts (account, extfvk, address, transparent_address) + VALUES (?, ?, ?, '')", + [ + u32::from(account).to_sql()?, + extfvk.to_sql()?, + address.to_sql()?, + ], + )?; + + // add a sapling sent note + wdb.conn.execute( + "INSERT INTO blocks (height, hash, time, sapling_tree) VALUES (0, 0, 0, x'000000')", + [], + )?; + + let tx = TransactionData::from_parts( + TxVersion::Sapling, + BranchId::Canopy, + 0, + BlockHeight::from(0), + None, + None, + None, + None, + ) + .freeze() + .unwrap(); + + let mut tx_bytes = vec![]; + tx.write(&mut tx_bytes).unwrap(); + wdb.conn.execute( + "INSERT INTO transactions (block, id_tx, txid, raw) VALUES (0, 0, :txid, :tx_bytes)", + named_params![ + ":txid": tx.txid().as_ref(), + ":tx_bytes": &tx_bytes[..] + ], + )?; + wdb.conn.execute( + "INSERT INTO sent_notes (tx, output_index, from_account, address, value) + VALUES (0, 0, ?, ?, 0)", + [u32::from(account).to_sql()?, address.to_sql()?], + )?; + + Ok(()) + } + let data_file = NamedTempFile::new().unwrap(); - let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&db_data).unwrap(); - - // First call with data should initialise the blocks table - init_blocks_table( - &db_data, - BlockHeight::from(1u32), - BlockHash([1; 32]), - 1, - &[], - ) - .unwrap(); + let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap(); + + let seed = [0xab; 32]; + let account = AccountId::ZERO; + let secret_key = sapling::spending_key(&seed, db_data.params.coin_type(), account); + #[allow(deprecated)] + let extfvk = secret_key.to_extended_full_viewing_key(); + + init_autoshielding(&mut db_data, &extfvk, account).unwrap(); + assert_matches!( + init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))), + Ok(_) + ); + } + + #[test] + fn init_migrate_from_main_pre_migrations() { + fn init_main( + wdb: &mut WalletDb, + ufvk: &UnifiedFullViewingKey, + account: AccountId, + ) -> Result<(), rusqlite::Error> { + wdb.conn.execute( + "CREATE TABLE accounts ( + account INTEGER PRIMARY KEY, + ufvk TEXT, + address TEXT, + transparent_address TEXT + )", + [], + )?; + wdb.conn.execute( + "CREATE TABLE blocks ( + height INTEGER PRIMARY KEY, + hash BLOB NOT NULL, + time INTEGER NOT NULL, + sapling_tree BLOB NOT NULL + )", + [], + )?; + wdb.conn.execute( + "CREATE TABLE transactions ( + id_tx INTEGER PRIMARY KEY, + txid BLOB NOT NULL UNIQUE, + created TEXT, + block INTEGER, + tx_index INTEGER, + expiry_height INTEGER, + raw BLOB, + FOREIGN KEY (block) REFERENCES blocks(height) + )", + [], + )?; + wdb.conn.execute( + "CREATE TABLE received_notes ( + id_note INTEGER PRIMARY KEY, + tx INTEGER NOT NULL, + output_index INTEGER NOT NULL, + account INTEGER NOT NULL, + diversifier BLOB NOT NULL, + value INTEGER NOT NULL, + rcm BLOB NOT NULL, + nf BLOB NOT NULL UNIQUE, + is_change INTEGER NOT NULL, + memo BLOB, + spent INTEGER, + FOREIGN KEY (tx) REFERENCES transactions(id_tx), + FOREIGN KEY (account) REFERENCES accounts(account), + FOREIGN KEY (spent) REFERENCES transactions(id_tx), + CONSTRAINT tx_output UNIQUE (tx, output_index) + )", + [], + )?; + wdb.conn.execute( + "CREATE TABLE sapling_witnesses ( + id_witness INTEGER PRIMARY KEY, + note INTEGER NOT NULL, + block INTEGER NOT NULL, + witness BLOB NOT NULL, + FOREIGN KEY (note) REFERENCES received_notes(id_note), + FOREIGN KEY (block) REFERENCES blocks(height), + CONSTRAINT witness_height UNIQUE (note, block) + )", + [], + )?; + wdb.conn.execute( + "CREATE TABLE sent_notes ( + id_note INTEGER PRIMARY KEY, + tx INTEGER NOT NULL, + output_pool INTEGER NOT NULL, + output_index INTEGER NOT NULL, + from_account INTEGER NOT NULL, + address TEXT NOT NULL, + value INTEGER NOT NULL, + memo BLOB, + FOREIGN KEY (tx) REFERENCES transactions(id_tx), + FOREIGN KEY (from_account) REFERENCES accounts(account), + CONSTRAINT tx_output UNIQUE (tx, output_pool, output_index) + )", + [], + )?; + wdb.conn.execute( + "CREATE TABLE utxos ( + id_utxo INTEGER PRIMARY KEY, + address TEXT NOT NULL, + prevout_txid BLOB NOT NULL, + prevout_idx INTEGER NOT NULL, + script BLOB NOT NULL, + value_zat INTEGER NOT NULL, + height INTEGER NOT NULL, + spent_in_tx INTEGER, + FOREIGN KEY (spent_in_tx) REFERENCES transactions(id_tx), + CONSTRAINT tx_outpoint UNIQUE (prevout_txid, prevout_idx) + )", + [], + )?; + + let ufvk_str = ufvk.encode(&wdb.params); + + // Unified addresses at the time of the addition of migrations did not contain an + // Orchard component. + let ua_request = UnifiedAddressRequest::unsafe_new(Omit, Require, UA_TRANSPARENT); + let address_str = Address::Unified( + ufvk.default_address(Some(ua_request)) + .expect("A valid default address exists for the UFVK") + .0, + ) + .encode(&wdb.params); + wdb.conn.execute( + "INSERT INTO accounts (account, ufvk, address, transparent_address) + VALUES (?, ?, ?, '')", + [ + u32::from(account).to_sql()?, + ufvk_str.to_sql()?, + address_str.to_sql()?, + ], + )?; - // Subsequent calls should return an error - init_blocks_table( - &db_data, - BlockHeight::from(2u32), - BlockHash([2; 32]), - 2, - &[], + // add a transparent "sent note" + #[cfg(feature = "transparent-inputs")] + { + let taddr = Address::Transparent( + *ufvk + .default_address(Some(ua_request)) + .expect("A valid default address exists for the UFVK") + .0 + .transparent() + .unwrap(), + ) + .encode(&wdb.params); + wdb.conn.execute( + "INSERT INTO blocks (height, hash, time, sapling_tree) VALUES (0, 0, 0, x'000000')", + [], + )?; + wdb.conn.execute( + "INSERT INTO transactions (block, id_tx, txid) VALUES (0, 0, '')", + [], + )?; + wdb.conn.execute( + "INSERT INTO sent_notes (tx, output_pool, output_index, from_account, address, value) + VALUES (0, ?, 0, ?, ?, 0)", + [pool_code(PoolType::TRANSPARENT).to_sql()?, u32::from(account).to_sql()?, taddr.to_sql()?])?; + } + + Ok(()) + } + + let data_file = NamedTempFile::new().unwrap(); + let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap(); + + let seed = [0xab; 32]; + let account = AccountId::ZERO; + let secret_key = UnifiedSpendingKey::from_seed(&db_data.params, &seed, account).unwrap(); + + init_main( + &mut db_data, + &secret_key.to_unified_full_viewing_key(), + account, ) - .unwrap_err(); + .unwrap(); + assert_matches!( + init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))), + Ok(_) + ); } #[test] - fn init_accounts_table_stores_correct_address() { + #[cfg(feature = "transparent-inputs")] + fn account_produces_expected_ua_sequence() { + use zcash_client_backend::data_api::{AccountBirthday, AccountSource, WalletRead}; + use zcash_primitives::block::BlockHash; + + let network = Network::MainNetwork; let data_file = NamedTempFile::new().unwrap(); - let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&db_data).unwrap(); + let mut db_data = WalletDb::for_path(data_file.path(), network).unwrap(); + assert_matches!(init_wallet_db(&mut db_data, None), Ok(_)); + + // Prior to adding any accounts, every seed phrase is relevant to the wallet. + let seed = test_vectors::UNIFIED[0].root_seed; + let other_seed = [7; 32]; + assert_matches!( + init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))), + Ok(()) + ); + assert_matches!( + init_wallet_db(&mut db_data, Some(Secret::new(other_seed.to_vec()))), + Ok(()) + ); + + let birthday = AccountBirthday::from_sapling_activation(&network, BlockHash([0; 32])); + let (account_id, _usk) = db_data + .create_account("", &Secret::new(seed.to_vec()), &birthday, None) + .unwrap(); + assert_matches!( + db_data.get_account(account_id), + Ok(Some(account)) if matches!( + &account.kind, + AccountSource::Derived{derivation, ..} if derivation.account_index() == zip32::AccountId::ZERO, + ) + ); + + // After adding an account, only the real seed phrase is relevant to the wallet. + assert_matches!( + init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))), + Ok(()) + ); + assert_matches!( + init_wallet_db(&mut db_data, Some(Secret::new(other_seed.to_vec()))), + Err(schemerz::MigratorError::Adapter( + WalletMigrationError::SeedNotRelevant + )) + ); - // Add an account to the wallet - let extsk = ExtendedSpendingKey::master(&[]); - let extfvks = [ExtendedFullViewingKey::from(&extsk)]; - init_accounts_table(&db_data, &extfvks).unwrap(); + for tv in &test_vectors::UNIFIED[..3] { + if let Some(Address::Unified(tvua)) = + Address::decode(&Network::MainNetwork, tv.unified_addr) + { + let (ua, di) = + wallet::get_current_address(&db_data.conn, &db_data.params, account_id) + .unwrap() + .expect("create_account generated the first address"); + assert_eq!(DiversifierIndex::from(tv.diversifier_index), di); + assert_eq!(tvua.transparent(), ua.transparent()); + assert_eq!(tvua.sapling(), ua.sapling()); + #[cfg(not(feature = "orchard"))] + assert_eq!(tv.unified_addr, ua.encode(&Network::MainNetwork)); - // The account's address should be in the data DB - let pa = get_address(&db_data, AccountId(0)).unwrap(); - assert_eq!(pa.unwrap(), extsk.default_address().unwrap().1); + // hardcoded with knowledge of what's coming next + let ua_request = UnifiedAddressRequest::unsafe_new(Omit, Require, Require); + db_data + .get_next_available_address(account_id, Some(ua_request)) + .unwrap() + .expect("get_next_available_address generated an address"); + } else { + panic!( + "{} did not decode to a valid unified address", + tv.unified_addr + ); + } + } } } diff --git a/zcash_client_sqlite/src/wallet/init/migrations.rs b/zcash_client_sqlite/src/wallet/init/migrations.rs new file mode 100644 index 0000000000..1e076736ed --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations.rs @@ -0,0 +1,349 @@ +mod add_account_birthdays; +mod add_account_uuids; +mod add_transaction_views; +mod add_utxo_account; +mod addresses_table; +mod ensure_orchard_ua_receiver; +mod ephemeral_addresses; +mod fix_bad_change_flagging; +mod fix_broken_commitment_trees; +mod full_account_ids; +mod initial_setup; +mod nullifier_map; +mod orchard_received_notes; +mod orchard_shardtree; +mod received_notes_nullable_nf; +mod receiving_key_scopes; +mod sapling_memo_consistency; +mod sent_notes_to_internal; +mod shardtree_support; +mod spend_key_available; +mod support_legacy_sqlite; +mod tx_retrieval_queue; +mod ufvk_support; +mod utxos_table; +mod utxos_to_txos; +mod v_sapling_shard_unscanned_ranges; +mod v_transactions_net; +mod v_transactions_note_uniqueness; +mod v_transactions_shielding_balance; +mod v_transactions_transparent_history; +mod v_tx_outputs_use_legacy_false; +mod wallet_summaries; + +use std::rc::Rc; + +use rusqlite::{named_params, OptionalExtension}; +use schemerz_rusqlite::RusqliteMigration; +use secrecy::SecretVec; +use uuid::Uuid; +use zcash_address::unified::{Encoding as _, Ufvk}; +use zcash_protocol::consensus; + +use super::WalletMigrationError; + +pub(super) fn all_migrations( + params: &P, + seed: Option>>, +) -> Vec>> { + // initial_setup + // / \ + // utxos_table ufvk_support + // | / \ + // | addresses_table sent_notes_to_internal + // | / / + // add_utxo_account / + // \ / + // add_transaction_views + // | + // v_transactions_net + // | + // received_notes_nullable_nf---------------------- + // / | \ + // / | \ + // --------------- shardtree_support sapling_memo_consistency nullifier_map + // / / \ \ | + // orchard_shardtree add_account_birthdays receiving_key_scopes v_transactions_transparent_history | + // | | \ | | | + // | v_sapling_shard_unscanned_ranges \ | v_tx_outputs_use_legacy_false | + // | | \ | | | + // | wallet_summaries \ | v_transactions_shielding_balance | + // | \ \ | | / + // \ \ \ | v_transactions_note_uniqueness / + // \ \ \ | / / + // \ -------------------- full_account_ids / + // \ / \ / + // \ orchard_received_notes spend_key_available / + // \ / \ / / + // \ ensure_orchard_ua_receiver utxos_to_txos / / + // \ \ | / / + // \ \ ephemeral_addresses / / + // \ \ | / / + // ------------------------------ tx_retrieval_queue ---------------------------- + // | + // support_legacy_sqlite + // / \ + // fix_broken_commitment_trees add_account_uuids + // | + // fix_bad_change_flagging + vec![ + Box::new(initial_setup::Migration {}), + Box::new(utxos_table::Migration {}), + Box::new(ufvk_support::Migration { + params: params.clone(), + seed: seed.clone(), + }), + Box::new(addresses_table::Migration { + params: params.clone(), + }), + Box::new(add_utxo_account::Migration { + _params: params.clone(), + }), + Box::new(sent_notes_to_internal::Migration {}), + Box::new(add_transaction_views::Migration), + Box::new(v_transactions_net::Migration), + Box::new(received_notes_nullable_nf::Migration), + Box::new(shardtree_support::Migration { + params: params.clone(), + }), + Box::new(nullifier_map::Migration), + Box::new(sapling_memo_consistency::Migration { + params: params.clone(), + }), + Box::new(add_account_birthdays::Migration { + params: params.clone(), + }), + Box::new(v_sapling_shard_unscanned_ranges::Migration { + params: params.clone(), + }), + Box::new(wallet_summaries::Migration), + Box::new(v_transactions_transparent_history::Migration), + Box::new(v_tx_outputs_use_legacy_false::Migration), + Box::new(v_transactions_shielding_balance::Migration), + Box::new(v_transactions_note_uniqueness::Migration), + Box::new(receiving_key_scopes::Migration { + params: params.clone(), + }), + Box::new(full_account_ids::Migration { + seed, + params: params.clone(), + }), + Box::new(orchard_shardtree::Migration { + params: params.clone(), + }), + Box::new(orchard_received_notes::Migration), + Box::new(ensure_orchard_ua_receiver::Migration { + params: params.clone(), + }), + Box::new(utxos_to_txos::Migration), + Box::new(ephemeral_addresses::Migration { + params: params.clone(), + }), + Box::new(spend_key_available::Migration), + Box::new(tx_retrieval_queue::Migration { + _params: params.clone(), + }), + Box::new(support_legacy_sqlite::Migration), + Box::new(fix_broken_commitment_trees::Migration { + params: params.clone(), + }), + Box::new(fix_bad_change_flagging::Migration), + Box::new(add_account_uuids::Migration), + ] +} + +/// All states of the migration DAG that have been exposed in a public crate release, in +/// the order that crate users would have encountered them. +/// +/// Omitted versions had the same migration state as the first prior version that is +/// included. +#[allow(dead_code)] +const PUBLIC_MIGRATION_STATES: &[&[Uuid]] = &[ + V_0_4_0, V_0_6_0, V_0_8_0, V_0_9_0, V_0_10_0, V_0_10_3, V_0_11_0, V_0_11_1, V_0_11_2, V_0_12_0, + V_0_13_0, +]; + +/// Leaf migrations in the 0.4.0 release. +const V_0_4_0: &[Uuid] = &[add_transaction_views::MIGRATION_ID]; + +/// Leaf migrations in the 0.6.0 release. +const V_0_6_0: &[Uuid] = &[v_transactions_net::MIGRATION_ID]; + +/// Leaf migrations in the 0.8.0 release. +const V_0_8_0: &[Uuid] = &[ + nullifier_map::MIGRATION_ID, + v_transactions_note_uniqueness::MIGRATION_ID, + wallet_summaries::MIGRATION_ID, +]; + +/// Leaf migrations in the 0.9.0 release. +const V_0_9_0: &[Uuid] = &[ + nullifier_map::MIGRATION_ID, + receiving_key_scopes::MIGRATION_ID, + v_transactions_note_uniqueness::MIGRATION_ID, + wallet_summaries::MIGRATION_ID, +]; + +/// Leaf migrations in the 0.10.0 release. +const V_0_10_0: &[Uuid] = &[ + nullifier_map::MIGRATION_ID, + orchard_received_notes::MIGRATION_ID, + orchard_shardtree::MIGRATION_ID, +]; + +/// Leaf migrations in the 0.10.3 release. +const V_0_10_3: &[Uuid] = &[ + ensure_orchard_ua_receiver::MIGRATION_ID, + nullifier_map::MIGRATION_ID, + orchard_shardtree::MIGRATION_ID, +]; + +/// Leaf migrations in the 0.11.0 release. +const V_0_11_0: &[Uuid] = &[ + ensure_orchard_ua_receiver::MIGRATION_ID, + ephemeral_addresses::MIGRATION_ID, + nullifier_map::MIGRATION_ID, + orchard_shardtree::MIGRATION_ID, + spend_key_available::MIGRATION_ID, + tx_retrieval_queue::MIGRATION_ID, +]; + +/// Leaf migrations in the 0.11.1 release. +const V_0_11_1: &[Uuid] = &[tx_retrieval_queue::MIGRATION_ID]; + +/// Leaf migrations in the 0.11.2 release. +const V_0_11_2: &[Uuid] = &[support_legacy_sqlite::MIGRATION_ID]; + +/// Leaf migrations in the 0.12.0 release. +const V_0_12_0: &[Uuid] = &[fix_broken_commitment_trees::MIGRATION_ID]; + +/// Leaf migrations in the 0.13.0 release. +const V_0_13_0: &[Uuid] = &[fix_bad_change_flagging::MIGRATION_ID]; + +pub(super) fn verify_network_compatibility( + conn: &rusqlite::Connection, + params: &P, +) -> Result<(), WalletMigrationError> { + // Ensure that the `ufvk_support` migration has been applied; if it hasn't, we won't be able to + // validate that the UFVKs in the wallet correspond to the network type that the wallet is + // being migrated for. + let has_ufvk = conn + .query_row( + &format!( + "SELECT 1 FROM {} WHERE id = :migration_id", + super::MIGRATIONS_TABLE + ), + named_params![":migration_id": &ufvk_support::MIGRATION_ID.as_bytes()[..]], + |row| row.get::<_, bool>(0), + ) + .optional()? + == Some(true); + + if has_ufvk { + let mut fvks_stmt = conn.prepare("SELECT ufvk FROM accounts")?; + let mut rows = fvks_stmt.query([])?; + while let Some(row) = rows.next()? { + let ufvk_str = row.get::<_, String>(0)?; + let (network, _) = Ufvk::decode(&ufvk_str).map_err(|e| { + WalletMigrationError::CorruptedData(format!("Unable to parse UFVK: {e}")) + })?; + + if network != params.network_type() { + let network_name = |n| match n { + consensus::NetworkType::Main => "mainnet", + consensus::NetworkType::Test => "testnet", + consensus::NetworkType::Regtest => "regtest", + }; + return Err(WalletMigrationError::CorruptedData(format!( + "Network type mismatch: account UFVK is for {} but attempting to initialize for {}.", + network_name(network), + network_name(params.network_type()) + ))); + } + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::collections::HashSet; + + use rusqlite::Connection; + use secrecy::Secret; + use tempfile::NamedTempFile; + use uuid::Uuid; + use zcash_protocol::consensus::Network; + + use crate::{wallet::init::init_wallet_db_internal, WalletDb}; + + /// Tests that we can migrate from a completely empty wallet database to the target + /// migrations. + pub(crate) fn test_migrate(migrations: &[Uuid]) { + let data_file = NamedTempFile::new().unwrap(); + let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap(); + + let seed = [0xab; 32]; + assert_matches!( + init_wallet_db_internal( + &mut db_data, + Some(Secret::new(seed.to_vec())), + migrations, + false + ), + Ok(_) + ); + } + + #[test] + fn migrate_between_releases_without_data() { + let data_file = NamedTempFile::new().unwrap(); + let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap(); + + let seed = [0xab; 32].to_vec(); + + let mut prev_state = HashSet::new(); + let mut ensure_migration_state_changed = |conn: &Connection| { + let new_state = conn + .prepare_cached("SELECT * FROM schemer_migrations") + .unwrap() + .query_map([], |row| row.get::<_, [u8; 16]>(0).map(Uuid::from_bytes)) + .unwrap() + .collect::, _>>() + .unwrap(); + assert!(prev_state != new_state); + prev_state = new_state; + }; + + let mut prev_leaves: &[Uuid] = &[]; + for migrations in super::PUBLIC_MIGRATION_STATES { + assert_matches!( + init_wallet_db_internal( + &mut db_data, + Some(Secret::new(seed.clone())), + migrations, + false + ), + Ok(_) + ); + + // If we have any new leaves, ensure the migration state changed. This lets us + // represent releases that changed the graph edges without introducing any new + // migrations. + if migrations.iter().any(|m| !prev_leaves.contains(m)) { + ensure_migration_state_changed(&db_data.conn); + } + + prev_leaves = *migrations; + } + + // Now check that we can migrate from the last public release to the current + // migration state in this branch. + assert_matches!( + init_wallet_db_internal(&mut db_data, Some(Secret::new(seed)), &[], false), + Ok(_) + ); + // We don't ensure that the migration state changed, because it may not have. + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/add_account_birthdays.rs b/zcash_client_sqlite/src/wallet/init/migrations/add_account_birthdays.rs new file mode 100644 index 0000000000..a094e8ff3a --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/add_account_birthdays.rs @@ -0,0 +1,130 @@ +//! This migration adds a birthday height to each account record. + +use std::collections::HashSet; + +use schemerz_rusqlite::RusqliteMigration; +use uuid::Uuid; +use zcash_protocol::consensus::{self, NetworkUpgrade}; + +use crate::wallet::init::WalletMigrationError; + +use super::shardtree_support; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0xeeec0d0d_fee0_4231_8c68_5f3a7c7c2245); + +const DEPENDENCIES: &[Uuid] = &[shardtree_support::MIGRATION_ID]; + +pub(super) struct Migration

{ + pub(super) params: P, +} + +impl

schemerz::Migration for Migration

{ + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + DEPENDENCIES.iter().copied().collect() + } + + fn description(&self) -> &'static str { + "Adds a birthday height for each account." + } +} + +impl RusqliteMigration for Migration

{ + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), Self::Error> { + transaction.execute_batch(&format!( + "ALTER TABLE accounts ADD COLUMN birthday_height INTEGER; + + -- set the birthday height to the height of the first block in the blocks table + UPDATE accounts SET birthday_height = MIN(blocks.height) FROM blocks; + -- if the blocks table is empty, set the birthday height to Sapling activation - 1 + UPDATE accounts SET birthday_height = {} WHERE birthday_height IS NULL; + + CREATE TABLE accounts_new ( + account INTEGER PRIMARY KEY, + ufvk TEXT NOT NULL, + birthday_height INTEGER NOT NULL, + recover_until_height INTEGER + ); + + INSERT INTO accounts_new (account, ufvk, birthday_height) + SELECT account, ufvk, birthday_height FROM accounts; + + PRAGMA legacy_alter_table = ON; + DROP TABLE accounts; + ALTER TABLE accounts_new RENAME TO accounts; + PRAGMA legacy_alter_table = OFF;", + u32::from( + self.params + .activation_height(NetworkUpgrade::Sapling) + .unwrap() + ) + ))?; + + Ok(()) + } + + fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), Self::Error> { + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) + } +} + +#[cfg(test)] +mod tests { + use rusqlite::named_params; + use secrecy::Secret; + use tempfile::NamedTempFile; + use zcash_keys::keys::UnifiedSpendingKey; + use zcash_protocol::consensus::Network; + use zip32::AccountId; + + use super::{DEPENDENCIES, MIGRATION_ID}; + use crate::{wallet::init::init_wallet_db_internal, WalletDb}; + + #[test] + fn migrate() { + let data_file = NamedTempFile::new().unwrap(); + let network = Network::TestNetwork; + let mut db_data = WalletDb::for_path(data_file.path(), network).unwrap(); + + let seed_bytes = vec![0xab; 32]; + init_wallet_db_internal( + &mut db_data, + Some(Secret::new(seed_bytes.clone())), + DEPENDENCIES, + false, + ) + .unwrap(); + + let usk = + UnifiedSpendingKey::from_seed(&network, &seed_bytes[..], AccountId::ZERO).unwrap(); + let ufvk_str = usk.to_unified_full_viewing_key().encode(&network); + + db_data + .conn + .execute( + "INSERT INTO accounts (account, ufvk) VALUES (0, :ufvk_str)", + named_params![":ufvk_str": ufvk_str], + ) + .unwrap(); + db_data + .conn + .execute_batch( + "INSERT INTO addresses (account, diversifier_index_be, address) + VALUES (0, X'', 'not_a_real_address');", + ) + .unwrap(); + + init_wallet_db_internal( + &mut db_data, + Some(Secret::new(seed_bytes)), + &[MIGRATION_ID], + false, + ) + .unwrap(); + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/add_account_uuids.rs b/zcash_client_sqlite/src/wallet/init/migrations/add_account_uuids.rs new file mode 100644 index 0000000000..46b1b488a7 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/add_account_uuids.rs @@ -0,0 +1,344 @@ +//! This migration adds a UUID to each account record, and adds `name` and `key_source` columns. In +//! addition, imported account records are now permitted to include key derivation metadata. + +use std::collections::HashSet; + +use rusqlite::named_params; +use schemerz_rusqlite::RusqliteMigration; +use uuid::Uuid; +use zcash_client_backend::data_api::{AccountPurpose, AccountSource, Zip32Derivation}; +use zip32::fingerprint::SeedFingerprint; + +use crate::wallet::{account_kind_code, init::WalletMigrationError}; + +use super::support_legacy_sqlite; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0xcccc623f_3243_43c7_b884_ceef25149e04); + +const DEPENDENCIES: &[Uuid] = &[support_legacy_sqlite::MIGRATION_ID]; + +pub(super) struct Migration; + +impl schemerz::Migration for Migration { + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + DEPENDENCIES.iter().copied().collect() + } + + fn description(&self) -> &'static str { + "Adds a UUID for each account." + } +} + +impl RusqliteMigration for Migration { + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), Self::Error> { + let account_kind_derived = account_kind_code(&AccountSource::Derived { + derivation: Zip32Derivation::new( + SeedFingerprint::from_bytes([0; 32]), + zip32::AccountId::ZERO, + ), + key_source: None, + }); + let account_kind_imported = account_kind_code(&AccountSource::Imported { + // the purpose here is irrelevant; we just use it to get the correct code + // for the account kind + purpose: AccountPurpose::ViewOnly, + key_source: None, + }); + transaction.execute_batch(&format!( + r#" + CREATE TABLE accounts_new ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + name TEXT, + uuid BLOB NOT NULL, + account_kind INTEGER NOT NULL DEFAULT {account_kind_derived}, + key_source TEXT, + hd_seed_fingerprint BLOB, + hd_account_index INTEGER, + ufvk TEXT, + uivk TEXT NOT NULL, + orchard_fvk_item_cache BLOB, + sapling_fvk_item_cache BLOB, + p2pkh_fvk_item_cache BLOB, + birthday_height INTEGER NOT NULL, + birthday_sapling_tree_size INTEGER, + birthday_orchard_tree_size INTEGER, + recover_until_height INTEGER, + has_spend_key INTEGER NOT NULL DEFAULT 1, + CHECK ( + ( + account_kind = {account_kind_derived} + AND hd_seed_fingerprint IS NOT NULL + AND hd_account_index IS NOT NULL + AND ufvk IS NOT NULL + ) + OR + ( + account_kind = {account_kind_imported} + AND (hd_seed_fingerprint IS NULL) = (hd_account_index IS NULL) + ) + ) + ); + "# + ))?; + + let mut q = transaction.prepare("SELECT * FROM accounts")?; + let mut rows = q.query([])?; + while let Some(row) = rows.next()? { + let preserve = |idx: &str| row.get::<_, rusqlite::types::Value>(idx); + transaction.execute( + r#" + INSERT INTO accounts_new ( + id, uuid, + account_kind, hd_seed_fingerprint, hd_account_index, + ufvk, uivk, + orchard_fvk_item_cache, sapling_fvk_item_cache, p2pkh_fvk_item_cache, + birthday_height, birthday_sapling_tree_size, birthday_orchard_tree_size, + recover_until_height, + has_spend_key + ) + VALUES ( + :account_id, :uuid, + :account_kind, :hd_seed_fingerprint, :hd_account_index, + :ufvk, :uivk, + :orchard_fvk_item_cache, :sapling_fvk_item_cache, :p2pkh_fvk_item_cache, + :birthday_height, :birthday_sapling_tree_size, :birthday_orchard_tree_size, + :recover_until_height, + :has_spend_key + ); + "#, + named_params! { + ":account_id": preserve("id")?, + ":uuid": Uuid::new_v4(), + ":account_kind": preserve("account_kind")?, + ":hd_seed_fingerprint": preserve("hd_seed_fingerprint")?, + ":hd_account_index": preserve("hd_account_index")?, + ":ufvk": preserve("ufvk")?, + ":uivk": preserve("uivk")?, + ":orchard_fvk_item_cache": preserve("orchard_fvk_item_cache")?, + ":sapling_fvk_item_cache": preserve("sapling_fvk_item_cache")?, + ":p2pkh_fvk_item_cache": preserve("p2pkh_fvk_item_cache")?, + ":birthday_height": preserve("birthday_height")?, + ":birthday_sapling_tree_size": preserve("birthday_sapling_tree_size")?, + ":birthday_orchard_tree_size": preserve("birthday_orchard_tree_size")?, + ":recover_until_height": preserve("recover_until_height")?, + ":has_spend_key": preserve("has_spend_key")?, + }, + )?; + } + + transaction.execute_batch( + "PRAGMA legacy_alter_table = ON; + DROP TABLE accounts; + ALTER TABLE accounts_new RENAME TO accounts; + PRAGMA legacy_alter_table = OFF; + + -- Add the new index. + CREATE UNIQUE INDEX accounts_uuid ON accounts (uuid); + + -- Recreate the existing indices now that the original ones have been deleted. + CREATE UNIQUE INDEX hd_account ON accounts (hd_seed_fingerprint, hd_account_index); + CREATE UNIQUE INDEX accounts_uivk ON accounts (uivk); + CREATE UNIQUE INDEX accounts_ufvk ON accounts (ufvk); + + -- Replace accounts.id with accounts.uuid in v_transactions. + DROP VIEW v_transactions; + CREATE VIEW v_transactions AS + WITH + notes AS ( + -- Outputs received in this transaction + SELECT ro.account_id AS account_id, + transactions.mined_height AS mined_height, + transactions.txid AS txid, + ro.pool AS pool, + id_within_pool_table, + ro.value AS value, + 0 AS spent_note_count, + CASE + WHEN ro.is_change THEN 1 + ELSE 0 + END AS change_note_count, + CASE + WHEN ro.is_change THEN 0 + ELSE 1 + END AS received_count, + CASE + WHEN (ro.memo IS NULL OR ro.memo = X'F6') + THEN 0 + ELSE 1 + END AS memo_present, + -- The wallet cannot receive transparent outputs in shielding transactions. + CASE + WHEN ro.pool = 0 + THEN 1 + ELSE 0 + END AS does_not_match_shielding + FROM v_received_outputs ro + JOIN transactions + ON transactions.id_tx = ro.transaction_id + UNION + -- Outputs spent in this transaction + SELECT ro.account_id AS account_id, + transactions.mined_height AS mined_height, + transactions.txid AS txid, + ro.pool AS pool, + id_within_pool_table, + -ro.value AS value, + 1 AS spent_note_count, + 0 AS change_note_count, + 0 AS received_count, + 0 AS memo_present, + -- The wallet cannot spend shielded outputs in shielding transactions. + CASE + WHEN ro.pool != 0 + THEN 1 + ELSE 0 + END AS does_not_match_shielding + FROM v_received_outputs ro + JOIN v_received_output_spends ros + ON ros.pool = ro.pool + AND ros.received_output_id = ro.id_within_pool_table + JOIN transactions + ON transactions.id_tx = ros.transaction_id + ), + -- Obtain a count of the notes that the wallet created in each transaction, + -- not counting change notes. + sent_note_counts AS ( + SELECT sent_notes.from_account_id AS account_id, + transactions.txid AS txid, + COUNT(DISTINCT sent_notes.id) AS sent_notes, + SUM( + CASE + WHEN (sent_notes.memo IS NULL OR sent_notes.memo = X'F6' OR ro.transaction_id IS NOT NULL) + THEN 0 + ELSE 1 + END + ) AS memo_count + FROM sent_notes + JOIN transactions + ON transactions.id_tx = sent_notes.tx + LEFT JOIN v_received_outputs ro + ON sent_notes.id = ro.sent_note_id + WHERE COALESCE(ro.is_change, 0) = 0 + GROUP BY account_id, txid + ), + blocks_max_height AS ( + SELECT MAX(blocks.height) AS max_height FROM blocks + ) + SELECT accounts.uuid AS account_uuid, + notes.mined_height AS mined_height, + notes.txid AS txid, + transactions.tx_index AS tx_index, + transactions.expiry_height AS expiry_height, + transactions.raw AS raw, + SUM(notes.value) AS account_balance_delta, + transactions.fee AS fee_paid, + SUM(notes.change_note_count) > 0 AS has_change, + MAX(COALESCE(sent_note_counts.sent_notes, 0)) AS sent_note_count, + SUM(notes.received_count) AS received_note_count, + SUM(notes.memo_present) + MAX(COALESCE(sent_note_counts.memo_count, 0)) AS memo_count, + blocks.time AS block_time, + ( + blocks.height IS NULL + AND transactions.expiry_height BETWEEN 1 AND blocks_max_height.max_height + ) AS expired_unmined, + SUM(notes.spent_note_count) AS spent_note_count, + ( + -- All of the wallet-spent and wallet-received notes are consistent with a + -- shielding transaction. + SUM(notes.does_not_match_shielding) = 0 + -- The transaction contains at least one wallet-spent output. + AND SUM(notes.spent_note_count) > 0 + -- The transaction contains at least one wallet-received note. + AND (SUM(notes.received_count) + SUM(notes.change_note_count)) > 0 + -- We do not know about any external outputs of the transaction. + AND MAX(COALESCE(sent_note_counts.sent_notes, 0)) = 0 + ) AS is_shielding + FROM notes + LEFT JOIN accounts ON accounts.id = notes.account_id + LEFT JOIN transactions + ON notes.txid = transactions.txid + JOIN blocks_max_height + LEFT JOIN blocks ON blocks.height = notes.mined_height + LEFT JOIN sent_note_counts + ON sent_note_counts.account_id = notes.account_id + AND sent_note_counts.txid = notes.txid + GROUP BY notes.account_id, notes.txid; + + -- Replace accounts.id with accounts.uuid in v_tx_outputs. + DROP VIEW v_tx_outputs; + CREATE VIEW v_tx_outputs AS + WITH unioned AS ( + -- select all outputs received by the wallet + SELECT transactions.txid AS txid, + ro.pool AS output_pool, + ro.output_index AS output_index, + from_account.uuid AS from_account_uuid, + to_account.uuid AS to_account_uuid, + NULL AS to_address, + ro.value AS value, + ro.is_change AS is_change, + ro.memo AS memo + FROM v_received_outputs ro + JOIN transactions + ON transactions.id_tx = ro.transaction_id + -- join to the sent_notes table to obtain `from_account_id` + LEFT JOIN sent_notes ON sent_notes.id = ro.sent_note_id + -- join on the accounts table to obtain account UUIDs + LEFT JOIN accounts from_account ON from_account.id = sent_notes.from_account_id + LEFT JOIN accounts to_account ON to_account.id = ro.account_id + UNION ALL + -- select all outputs sent from the wallet to external recipients + SELECT transactions.txid AS txid, + sent_notes.output_pool AS output_pool, + sent_notes.output_index AS output_index, + from_account.uuid AS from_account_uuid, + NULL AS to_account_uuid, + sent_notes.to_address AS to_address, + sent_notes.value AS value, + 0 AS is_change, + sent_notes.memo AS memo + FROM sent_notes + JOIN transactions + ON transactions.id_tx = sent_notes.tx + LEFT JOIN v_received_outputs ro ON ro.sent_note_id = sent_notes.id + -- join on the accounts table to obtain account UUIDs + LEFT JOIN accounts from_account ON from_account.id = sent_notes.from_account_id + ) + -- merge duplicate rows while retaining maximum information + SELECT + txid, + output_pool, + output_index, + max(from_account_uuid) AS from_account_uuid, + max(to_account_uuid) AS to_account_uuid, + max(to_address) AS to_address, + max(value) AS value, + max(is_change) AS is_change, + max(memo) AS memo + FROM unioned + GROUP BY txid, output_pool, output_index", + )?; + + Ok(()) + } + + fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), Self::Error> { + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) + } +} + +#[cfg(test)] +mod tests { + use crate::wallet::init::migrations::tests::test_migrate; + + #[test] + fn migrate() { + test_migrate(&[super::MIGRATION_ID]); + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/add_transaction_views.rs b/zcash_client_sqlite/src/wallet/init/migrations/add_transaction_views.rs new file mode 100644 index 0000000000..a7403c76d7 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/add_transaction_views.rs @@ -0,0 +1,493 @@ +//! Migration that adds transaction summary views & add fee information to transactions. +use std::collections::HashSet; + +use rusqlite::{self, types::ToSql, OptionalExtension}; +use schemerz_rusqlite::RusqliteMigration; +use uuid::Uuid; + +use zcash_primitives::transaction::Transaction; +use zcash_protocol::{ + consensus::BranchId, + value::{BalanceError, ZatBalance}, +}; + +use super::{add_utxo_account, sent_notes_to_internal}; +use crate::wallet::init::WalletMigrationError; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0x282fad2e_8372_4ca0_8bed_71821320909f); + +const DEPENDENCIES: &[Uuid] = &[ + add_utxo_account::MIGRATION_ID, + sent_notes_to_internal::MIGRATION_ID, +]; + +pub(crate) struct Migration; + +impl schemerz::Migration for Migration { + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + DEPENDENCIES.iter().copied().collect() + } + + fn description(&self) -> &'static str { + "Add transaction summary views & add fee information to transactions." + } +} + +impl RusqliteMigration for Migration { + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + enum FeeError { + Db(rusqlite::Error), + UtxoNotFound, + Balance(BalanceError), + CorruptedData(String), + } + + impl From for FeeError { + fn from(e: BalanceError) -> Self { + FeeError::Balance(e) + } + } + + impl From for FeeError { + fn from(e: rusqlite::Error) -> Self { + FeeError::Db(e) + } + } + + transaction.execute_batch("ALTER TABLE transactions ADD COLUMN fee INTEGER;")?; + + let mut stmt_list_txs = transaction.prepare("SELECT id_tx, raw FROM transactions")?; + + let mut stmt_set_fee = + transaction.prepare("UPDATE transactions SET fee = ? WHERE id_tx = ?")?; + + let mut stmt_find_utxo_value = transaction + .prepare("SELECT value_zat FROM utxos WHERE prevout_txid = ? AND prevout_idx = ?")?; + + let mut tx_rows = stmt_list_txs.query([])?; + while let Some(row) = tx_rows.next()? { + let id_tx: i64 = row.get(0)?; + let tx_bytes: Option> = row.get(1)?; + + // If only transaction metadata has been stored, and not transaction data, the fee + // information will eventually be set when the full transaction data is inserted. + if let Some(tx_bytes) = tx_bytes { + let tx = Transaction::read( + &tx_bytes[..], + // The consensus branch ID is unused in determining the fee paid, so + // just pass Nu5 as a dummy value since we know that parsing both v4 + // and v5 transactions is supported during the Nu5 epoch. + BranchId::Nu5, + ) + .map_err(|e| { + WalletMigrationError::CorruptedData(format!( + "Parsing failed for transaction {:?}: {:?}", + id_tx, e + )) + })?; + + let fee_paid = tx.fee_paid(|op| { + let op_amount = stmt_find_utxo_value + .query_row([op.hash().to_sql()?, op.n().to_sql()?], |row| { + row.get::<_, i64>(0) + }) + .optional() + .map_err(FeeError::Db)?; + + op_amount.map_or_else( + || Err(FeeError::UtxoNotFound), + |i| { + ZatBalance::from_i64(i).map_err(|_| { + FeeError::CorruptedData(format!( + "UTXO amount out of range in outpoint {:?}", + op + )) + }) + }, + ) + }); + + match fee_paid { + Ok(fee_paid) => { + stmt_set_fee.execute([i64::from(fee_paid), id_tx])?; + } + Err(FeeError::UtxoNotFound) => { + // The fee and net value will end up being null in the transactions view. + } + Err(FeeError::Db(e)) => { + return Err(WalletMigrationError::from(e)); + } + Err(FeeError::Balance(e)) => { + return Err(WalletMigrationError::from(e)); + } + Err(FeeError::CorruptedData(s)) => { + return Err(WalletMigrationError::CorruptedData(s)); + } + } + } + } + + transaction.execute_batch( + "UPDATE sent_notes SET memo = NULL + WHERE memo = X'F600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'; + UPDATE received_notes SET memo = NULL + WHERE memo = X'F600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000';")?; + + transaction.execute_batch( + "CREATE VIEW v_tx_sent AS + SELECT transactions.id_tx AS id_tx, + transactions.block AS mined_height, + transactions.tx_index AS tx_index, + transactions.txid AS txid, + transactions.expiry_height AS expiry_height, + transactions.raw AS raw, + MAX(sent_notes.from_account) AS sent_from_account, + SUM(sent_notes.value) AS sent_total, + COUNT(sent_notes.id_note) AS sent_note_count, + SUM( + CASE + WHEN sent_notes.memo IS NULL THEN 0 + ELSE 1 + END + ) AS memo_count, + blocks.time AS block_time + FROM transactions + JOIN sent_notes + ON transactions.id_tx = sent_notes.tx + LEFT JOIN blocks + ON transactions.block = blocks.height + GROUP BY sent_notes.tx, sent_notes.from_account;", + )?; + + transaction.execute_batch( + "CREATE VIEW v_tx_received AS + SELECT transactions.id_tx AS id_tx, + transactions.block AS mined_height, + transactions.tx_index AS tx_index, + transactions.txid AS txid, + transactions.expiry_height AS expiry_height, + transactions.raw AS raw, + MAX(received_notes.account) AS received_by_account, + SUM(received_notes.value) AS received_total, + COUNT(received_notes.id_note) AS received_note_count, + SUM( + CASE + WHEN received_notes.memo IS NULL THEN 0 + ELSE 1 + END + ) AS memo_count, + blocks.time AS block_time + FROM transactions + JOIN received_notes + ON transactions.id_tx = received_notes.tx + LEFT JOIN blocks + ON transactions.block = blocks.height + GROUP BY received_notes.tx, received_notes.account;", + )?; + + transaction.execute_batch( + "CREATE VIEW v_transactions AS + SELECT notes.id_tx, + notes.mined_height, + notes.tx_index, + notes.txid, + notes.expiry_height, + notes.raw, + SUM(notes.value) + MAX(notes.fee) AS net_value, + MAX(notes.fee) AS fee_paid, + SUM(notes.sent_count) == 0 AS is_wallet_internal, + SUM(notes.is_change) > 0 AS has_change, + SUM(notes.sent_count) AS sent_note_count, + SUM(notes.received_count) AS received_note_count, + SUM(notes.memo_present) AS memo_count, + blocks.time AS block_time + FROM ( + SELECT transactions.id_tx AS id_tx, + transactions.block AS mined_height, + transactions.tx_index AS tx_index, + transactions.txid AS txid, + transactions.expiry_height AS expiry_height, + transactions.raw AS raw, + 0 AS fee, + CASE + WHEN received_notes.is_change THEN 0 + ELSE value + END AS value, + 0 AS sent_count, + CASE + WHEN received_notes.is_change THEN 1 + ELSE 0 + END AS is_change, + CASE + WHEN received_notes.is_change THEN 0 + ELSE 1 + END AS received_count, + CASE + WHEN received_notes.memo IS NULL THEN 0 + ELSE 1 + END AS memo_present + FROM transactions + JOIN received_notes ON transactions.id_tx = received_notes.tx + UNION + SELECT transactions.id_tx AS id_tx, + transactions.block AS mined_height, + transactions.tx_index AS tx_index, + transactions.txid AS txid, + transactions.expiry_height AS expiry_height, + transactions.raw AS raw, + transactions.fee AS fee, + -sent_notes.value AS value, + CASE + WHEN sent_notes.from_account = sent_notes.to_account THEN 0 + ELSE 1 + END AS sent_count, + 0 AS is_change, + 0 AS received_count, + CASE + WHEN sent_notes.memo IS NULL THEN 0 + ELSE 1 + END AS memo_present + FROM transactions + JOIN sent_notes ON transactions.id_tx = sent_notes.tx + ) AS notes + LEFT JOIN blocks ON notes.mined_height = blocks.height + GROUP BY notes.id_tx;", + )?; + + Ok(()) + } + + fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) + } +} + +#[cfg(test)] +mod tests { + use rusqlite::{self, params}; + use tempfile::NamedTempFile; + + use zcash_keys::keys::UnifiedSpendingKey; + use zcash_protocol::consensus::Network; + use zip32::AccountId; + + use crate::{ + wallet::init::{init_wallet_db_internal, migrations::addresses_table}, + WalletDb, + }; + + #[cfg(feature = "transparent-inputs")] + use { + crate::wallet::init::migrations::{ufvk_support, utxos_table}, + ::transparent::{ + address::Script, + bundle::{self as transparent, Authorized, OutPoint, TxIn, TxOut}, + keys::IncomingViewingKey, + }, + zcash_keys::encoding::AddressCodec, + zcash_primitives::transaction::{TransactionData, TxVersion}, + zcash_protocol::{ + consensus::{BlockHeight, BranchId}, + value::ZatBalance, + }, + }; + + #[test] + fn transaction_views() { + let network = Network::TestNetwork; + let data_file = NamedTempFile::new().unwrap(); + let mut db_data = WalletDb::for_path(data_file.path(), network).unwrap(); + init_wallet_db_internal(&mut db_data, None, &[addresses_table::MIGRATION_ID], false) + .unwrap(); + let usk = UnifiedSpendingKey::from_seed(&network, &[0u8; 32][..], AccountId::ZERO).unwrap(); + let ufvk = usk.to_unified_full_viewing_key(); + + db_data + .conn + .execute( + "INSERT INTO accounts (account, ufvk) VALUES (0, ?)", + params![ufvk.encode(&network)], + ) + .unwrap(); + + db_data.conn.execute_batch( + "INSERT INTO blocks (height, hash, time, sapling_tree) VALUES (0, 0, 0, x'00'); + INSERT INTO transactions (block, id_tx, txid) VALUES (0, 0, ''); + + INSERT INTO sent_notes (tx, output_pool, output_index, from_account, address, value) + VALUES (0, 2, 0, 0, '', 2); + INSERT INTO sent_notes (tx, output_pool, output_index, from_account, address, value, memo) + VALUES (0, 2, 1, 0, '', 3, X'61'); + INSERT INTO sent_notes (tx, output_pool, output_index, from_account, address, value, memo) + VALUES (0, 2, 2, 0, '', 0, X'F600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'); + + INSERT INTO received_notes (tx, output_index, account, diversifier, value, rcm, nf, is_change) + VALUES (0, 0, 0, '', 2, '', 'a', false); + INSERT INTO received_notes (tx, output_index, account, diversifier, value, rcm, nf, is_change, memo) + VALUES (0, 3, 0, '', 5, '', 'b', false, X'62'); + INSERT INTO received_notes (tx, output_index, account, diversifier, value, rcm, nf, is_change, memo) + VALUES (0, 4, 0, '', 7, '', 'c', true, X'63');", + ).unwrap(); + + init_wallet_db_internal(&mut db_data, None, &[super::MIGRATION_ID], false).unwrap(); + + let mut q = db_data + .conn + .prepare("SELECT received_total, received_note_count, memo_count FROM v_tx_received") + .unwrap(); + let mut rows = q.query([]).unwrap(); + let mut row_count = 0; + while let Some(row) = rows.next().unwrap() { + row_count += 1; + let total: i64 = row.get(0).unwrap(); + let count: i64 = row.get(1).unwrap(); + let memo_count: i64 = row.get(2).unwrap(); + assert_eq!(total, 14); + assert_eq!(count, 3); + assert_eq!(memo_count, 2); + } + assert_eq!(row_count, 1); + + let mut q = db_data + .conn + .prepare("SELECT sent_total, sent_note_count, memo_count FROM v_tx_sent") + .unwrap(); + let mut rows = q.query([]).unwrap(); + let mut row_count = 0; + while let Some(row) = rows.next().unwrap() { + row_count += 1; + let total: i64 = row.get(0).unwrap(); + let count: i64 = row.get(1).unwrap(); + let memo_count: i64 = row.get(2).unwrap(); + assert_eq!(total, 5); + assert_eq!(count, 3); + assert_eq!(memo_count, 1); + } + assert_eq!(row_count, 1); + + let mut q = db_data + .conn + .prepare("SELECT net_value, has_change, memo_count FROM v_transactions") + .unwrap(); + let mut rows = q.query([]).unwrap(); + let mut row_count = 0; + while let Some(row) = rows.next().unwrap() { + row_count += 1; + let net_value: i64 = row.get(0).unwrap(); + let has_change: bool = row.get(1).unwrap(); + let memo_count: i64 = row.get(2).unwrap(); + assert_eq!(net_value, 2); + assert!(has_change); + assert_eq!(memo_count, 3); + } + assert_eq!(row_count, 1); + } + + #[test] + #[cfg(feature = "transparent-inputs")] + fn migrate_from_wm2() { + use ::transparent::keys::NonHardenedChildIndex; + use zcash_client_backend::keys::UnifiedAddressRequest; + use zcash_keys::keys::ReceiverRequirement::*; + use zcash_protocol::value::Zatoshis; + + use crate::UA_TRANSPARENT; + + let network = Network::TestNetwork; + let data_file = NamedTempFile::new().unwrap(); + let mut db_data = WalletDb::for_path(data_file.path(), network).unwrap(); + init_wallet_db_internal( + &mut db_data, + None, + &[utxos_table::MIGRATION_ID, ufvk_support::MIGRATION_ID], + false, + ) + .unwrap(); + + // create a UTXO to spend + let tx = TransactionData::from_parts( + TxVersion::Sapling, + BranchId::Canopy, + 0, + BlockHeight::from(3), + Some(transparent::Bundle { + vin: vec![TxIn { + prevout: OutPoint::fake(), + script_sig: Script(vec![]), + sequence: 0, + }], + vout: vec![TxOut { + value: Zatoshis::const_from_u64(1100000000), + script_pubkey: Script(vec![]), + }], + authorization: Authorized, + }), + None, + None, + None, + ) + .freeze() + .unwrap(); + + let mut tx_bytes = vec![]; + tx.write(&mut tx_bytes).unwrap(); + + let usk = UnifiedSpendingKey::from_seed(&network, &[0u8; 32][..], AccountId::ZERO).unwrap(); + let ufvk = usk.to_unified_full_viewing_key(); + let (ua, _) = ufvk + .default_address(Some(UnifiedAddressRequest::unsafe_new( + Omit, + Require, + UA_TRANSPARENT, + ))) + .expect("A valid default address exists for the UFVK"); + let taddr = ufvk + .transparent() + .and_then(|k| { + k.derive_external_ivk() + .ok() + .map(|k| k.derive_address(NonHardenedChildIndex::ZERO).unwrap()) + }) + .map(|a| a.encode(&network)); + + db_data.conn.execute( + "INSERT INTO accounts (account, ufvk, address, transparent_address) VALUES (0, ?, ?, ?)", + params![ufvk.encode(&network), ua.encode(&network), &taddr] + ).unwrap(); + db_data + .conn + .execute_batch( + "INSERT INTO blocks (height, hash, time, sapling_tree) VALUES (0, 0, 0, x'00');", + ) + .unwrap(); + db_data.conn.execute( + "INSERT INTO utxos (address, prevout_txid, prevout_idx, script, value_zat, height) + VALUES (?, X'0101010101010101010101010101010101010101010101010101010101010101', 1, X'', 1400000000, 1)", + [taddr] + ).unwrap(); + db_data + .conn + .execute( + "INSERT INTO transactions (block, id_tx, txid, raw) VALUES (0, 0, '', ?)", + params![tx_bytes], + ) + .unwrap(); + + init_wallet_db_internal(&mut db_data, None, &[super::MIGRATION_ID], false).unwrap(); + + let fee = db_data + .conn + .query_row("SELECT fee FROM transactions WHERE id_tx = 0", [], |row| { + Ok(ZatBalance::from_i64(row.get(0)?).unwrap()) + }) + .unwrap(); + + assert_eq!(fee, ZatBalance::from_i64(300000000).unwrap()); + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/add_utxo_account.rs b/zcash_client_sqlite/src/wallet/init/migrations/add_utxo_account.rs new file mode 100644 index 0000000000..6c737247d7 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/add_utxo_account.rs @@ -0,0 +1,239 @@ +//! A migration that adds an identifier for the account that received a UTXO to the utxos table + +use schemerz_rusqlite::RusqliteMigration; +use std::collections::HashSet; +use uuid::Uuid; +use zcash_protocol::consensus; + +use super::{addresses_table, utxos_table}; +use crate::wallet::init::WalletMigrationError; + +#[cfg(feature = "transparent-inputs")] +use { + crate::error::SqliteClientError, + ::transparent::{ + address::TransparentAddress, + keys::{IncomingViewingKey, NonHardenedChildIndex}, + }, + rusqlite::{named_params, OptionalExtension}, + std::collections::HashMap, + zcash_client_backend::wallet::TransparentAddressMetadata, + zcash_keys::{address::Address, encoding::AddressCodec, keys::UnifiedFullViewingKey}, + zip32::{AccountId, DiversifierIndex, Scope}, +}; + +/// This migration adds an account identifier column to the UTXOs table. +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0x761884d6_30d8_44ef_b204_0b82551c4ca1); + +const DEPENDENCIES: &[Uuid] = &[utxos_table::MIGRATION_ID, addresses_table::MIGRATION_ID]; + +pub(super) struct Migration

{ + pub(super) _params: P, +} + +impl

schemerz::Migration for Migration

{ + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + DEPENDENCIES.iter().copied().collect() + } + + fn description(&self) -> &'static str { + "Adds an identifier for the account that received a UTXO to the utxos table" + } +} + +impl RusqliteMigration for Migration

{ + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + transaction.execute_batch("ALTER TABLE utxos ADD COLUMN received_by_account INTEGER;")?; + + #[cfg(feature = "transparent-inputs")] + { + let mut stmt_update_utxo_account = transaction.prepare( + "UPDATE utxos SET received_by_account = :account WHERE address = :address", + )?; + + let mut stmt_fetch_accounts = transaction.prepare("SELECT account FROM accounts")?; + + let mut rows = stmt_fetch_accounts.query([])?; + while let Some(row) = rows.next()? { + let account = AccountId::try_from(row.get::<_, u32>(0)?).map_err(|_| { + WalletMigrationError::CorruptedData( + "Unexpected ZIP-32 account index.".to_string(), + ) + })?; + let taddrs = get_transparent_receivers(transaction, &self._params, account) + .map_err(|e| match e { + SqliteClientError::DbError(e) => WalletMigrationError::DbError(e), + SqliteClientError::CorruptedData(s) => { + WalletMigrationError::CorruptedData(s) + } + other => WalletMigrationError::CorruptedData(format!( + "Unexpected error in migration: {}", + other + )), + })?; + + for (taddr, _) in taddrs { + stmt_update_utxo_account.execute(named_params![ + ":account": u32::from(account), + ":address": &taddr.encode(&self._params), + ])?; + } + } + } + + transaction.execute_batch( + "CREATE TABLE utxos_new ( + id_utxo INTEGER PRIMARY KEY, + received_by_account INTEGER NOT NULL, + address TEXT NOT NULL, + prevout_txid BLOB NOT NULL, + prevout_idx INTEGER NOT NULL, + script BLOB NOT NULL, + value_zat INTEGER NOT NULL, + height INTEGER NOT NULL, + spent_in_tx INTEGER, + FOREIGN KEY (received_by_account) REFERENCES accounts(account), + FOREIGN KEY (spent_in_tx) REFERENCES transactions(id_tx), + CONSTRAINT tx_outpoint UNIQUE (prevout_txid, prevout_idx) + ); + INSERT INTO utxos_new ( + id_utxo, received_by_account, address, + prevout_txid, prevout_idx, script, value_zat, + height, spent_in_tx) + SELECT + id_utxo, received_by_account, address, + prevout_txid, prevout_idx, script, value_zat, + height, spent_in_tx + FROM utxos;", + )?; + + transaction.execute_batch( + "DROP TABLE utxos; + ALTER TABLE utxos_new RENAME TO utxos;", + )?; + + Ok(()) + } + + fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) + } +} + +#[cfg(feature = "transparent-inputs")] +fn get_transparent_receivers( + conn: &rusqlite::Connection, + params: &P, + account: AccountId, +) -> Result>, SqliteClientError> { + let mut ret: HashMap> = HashMap::new(); + + // Get all UAs derived + let mut ua_query = conn + .prepare("SELECT address, diversifier_index_be FROM addresses WHERE account = :account")?; + let mut rows = ua_query.query(named_params![":account": u32::from(account)])?; + + while let Some(row) = rows.next()? { + let ua_str: String = row.get(0)?; + let di_vec: Vec = row.get(1)?; + let mut di: [u8; 11] = di_vec.try_into().map_err(|_| { + SqliteClientError::CorruptedData("Diversifier index is not an 11-byte value".to_owned()) + })?; + di.reverse(); // BE -> LE conversion + + let ua = Address::decode(params, &ua_str) + .ok_or_else(|| { + SqliteClientError::CorruptedData("Not a valid Zcash recipient address".to_owned()) + }) + .and_then(|addr| match addr { + Address::Unified(ua) => Ok(ua), + _ => Err(SqliteClientError::CorruptedData(format!( + "Addresses table contains {} which is not a unified address", + ua_str, + ))), + })?; + + if let Some(taddr) = ua.transparent() { + let index = NonHardenedChildIndex::from_index( + DiversifierIndex::from(di).try_into().map_err(|_| { + SqliteClientError::CorruptedData( + "Unable to get diversifier for transparent address.".to_owned(), + ) + })?, + ) + .ok_or_else(|| { + SqliteClientError::CorruptedData( + "Unexpected hardened index for transparent address.".to_owned(), + ) + })?; + + ret.insert( + *taddr, + Some(TransparentAddressMetadata::new( + Scope::External.into(), + index, + )), + ); + } + } + + if let Some((taddr, address_index)) = get_legacy_transparent_address(params, conn, account)? { + ret.insert( + taddr, + Some(TransparentAddressMetadata::new( + Scope::External.into(), + address_index, + )), + ); + } + + Ok(ret) +} + +#[cfg(feature = "transparent-inputs")] +fn get_legacy_transparent_address( + params: &P, + conn: &rusqlite::Connection, + account: AccountId, +) -> Result, SqliteClientError> { + // Get the UFVK for the account. + let ufvk_str: Option = conn + .query_row( + "SELECT ufvk FROM accounts WHERE account = :account", + [u32::from(account)], + |row| row.get(0), + ) + .optional()?; + + if let Some(uvk_str) = ufvk_str { + let ufvk = UnifiedFullViewingKey::decode(params, &uvk_str) + .map_err(SqliteClientError::CorruptedData)?; + + // Derive the default transparent address (if it wasn't already part of a derived UA). + ufvk.transparent() + .map(|tfvk| { + tfvk.derive_external_ivk() + .map(|tivk| tivk.default_address()) + .map_err(SqliteClientError::TransparentDerivation) + }) + .transpose() + } else { + Ok(None) + } +} + +#[cfg(test)] +mod tests { + use crate::wallet::init::migrations::tests::test_migrate; + + #[test] + fn migrate() { + test_migrate(&[super::MIGRATION_ID]); + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/addresses_table.rs b/zcash_client_sqlite/src/wallet/init/migrations/addresses_table.rs new file mode 100644 index 0000000000..64b057a475 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/addresses_table.rs @@ -0,0 +1,228 @@ +use std::collections::HashSet; + +use rusqlite::{named_params, Transaction}; +use schemerz_rusqlite::RusqliteMigration; +use uuid::Uuid; + +use zcash_keys::{ + address::{Address, UnifiedAddress}, + encoding::AddressCodec, + keys::{ReceiverRequirement::*, UnifiedAddressRequest, UnifiedFullViewingKey}, +}; +use zcash_protocol::consensus; +use zip32::{AccountId, DiversifierIndex}; + +use crate::{wallet::init::WalletMigrationError, UA_TRANSPARENT}; + +#[cfg(feature = "transparent-inputs")] +use ::transparent::keys::IncomingViewingKey; + +use super::ufvk_support; + +/// The migration that removed the address columns from the `accounts` table, and created +/// the `accounts` table. +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0xd956978c_9c87_4d6e_815d_fb8f088d094c); + +const DEPENDENCIES: &[Uuid] = &[ufvk_support::MIGRATION_ID]; + +pub(crate) struct Migration { + pub(crate) params: P, +} + +impl schemerz::Migration for Migration

{ + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + DEPENDENCIES.iter().copied().collect() + } + + fn description(&self) -> &'static str { + "Adds the addresses table for tracking diversified UAs" + } +} + +impl RusqliteMigration for Migration

{ + type Error = WalletMigrationError; + + fn up(&self, transaction: &Transaction) -> Result<(), WalletMigrationError> { + transaction.execute_batch( + "CREATE TABLE addresses ( + account INTEGER NOT NULL, + diversifier_index_be BLOB NOT NULL, + address TEXT NOT NULL, + cached_transparent_receiver_address TEXT, + FOREIGN KEY (account) REFERENCES accounts(account), + CONSTRAINT diversification UNIQUE (account, diversifier_index_be) + ); + CREATE TABLE accounts_new ( + account INTEGER PRIMARY KEY, + ufvk TEXT NOT NULL + );", + )?; + + let mut stmt_fetch_accounts = transaction + .prepare("SELECT account, ufvk, address, transparent_address FROM accounts")?; + + let mut rows = stmt_fetch_accounts.query([])?; + while let Some(row) = rows.next()? { + let account = AccountId::try_from(row.get::<_, u32>(0)?).map_err(|_| { + WalletMigrationError::CorruptedData("Invalid ZIP-32 account index.".to_owned()) + })?; + + let ufvk_str: String = row.get(1)?; + let ufvk = UnifiedFullViewingKey::decode(&self.params, &ufvk_str) + .map_err(WalletMigrationError::CorruptedData)?; + + // Verify that the address column contains the expected value. + let address: String = row.get(2)?; + let decoded = Address::decode(&self.params, &address).ok_or_else(|| { + WalletMigrationError::CorruptedData(format!( + "Could not decode {} as a valid Zcash address.", + address + )) + })?; + let decoded_address = if let Address::Unified(ua) = decoded { + ua + } else { + return Err(WalletMigrationError::CorruptedData( + "Address in accounts table was not a Unified Address.".to_string(), + )); + }; + let (expected_address, idx) = ufvk.default_address(Some( + UnifiedAddressRequest::unsafe_new(Omit, Require, UA_TRANSPARENT), + ))?; + if decoded_address != expected_address { + return Err(WalletMigrationError::CorruptedData(format!( + "Decoded UA {} does not match the UFVK's default address {} at {:?}.", + address, + Address::Unified(expected_address).encode(&self.params), + idx, + ))); + } + + // The transparent_address column might not be filled, depending on how this + // crate was compiled. + if let Some(transparent_address) = row.get::<_, Option>(3)? { + let decoded_transparent = Address::decode(&self.params, &transparent_address) + .ok_or_else(|| { + WalletMigrationError::CorruptedData(format!( + "Could not decode {} as a valid Zcash address.", + address + )) + })?; + let decoded_transparent_address = if let Address::Transparent(addr) = + decoded_transparent + { + addr + } else { + return Err(WalletMigrationError::CorruptedData( + "Address in transparent_address column of accounts table was not a transparent address.".to_string(), + )); + }; + + // Verify that the transparent_address column contains the expected value, + // so we can confidently delete the column knowing we can regenerate the + // values from the stored UFVKs. + + // We can only check if it is the expected transparent address if the + // transparent-inputs feature flag is enabled. + #[cfg(feature = "transparent-inputs")] + { + let expected_address = ufvk + .transparent() + .and_then(|k| k.derive_external_ivk().ok().map(|k| k.default_address().0)); + if Some(decoded_transparent_address) != expected_address { + return Err(WalletMigrationError::CorruptedData(format!( + "Decoded transparent address {} is not the default transparent address.", + transparent_address, + ))); + } + } + + // If the transparent_address column is not empty, and we can't check its + // value, return an error. + #[cfg(not(feature = "transparent-inputs"))] + { + let _ = decoded_transparent_address; + return Err(WalletMigrationError::CorruptedData( + "Database needs transparent-inputs feature flag enabled to migrate" + .to_string(), + )); + } + } + + transaction.execute( + "INSERT INTO accounts_new (account, ufvk) + VALUES (:account, :ufvk)", + named_params![ + ":account": u32::from(account), + ":ufvk": ufvk.encode(&self.params), + ], + )?; + + let (address, d_idx) = ufvk.default_address(Some( + UnifiedAddressRequest::unsafe_new(Omit, Require, UA_TRANSPARENT), + ))?; + insert_address(transaction, &self.params, account, d_idx, &address)?; + } + + transaction.execute_batch( + "DROP TABLE accounts; + ALTER TABLE accounts_new RENAME TO accounts;", + )?; + + Ok(()) + } + + fn down(&self, _transaction: &Transaction) -> Result<(), WalletMigrationError> { + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) + } +} + +/// Adds the given address and diversifier index to the addresses table. +fn insert_address( + conn: &rusqlite::Connection, + params: &P, + account: AccountId, + diversifier_index: DiversifierIndex, + address: &UnifiedAddress, +) -> Result<(), rusqlite::Error> { + let mut stmt = conn.prepare_cached( + "INSERT INTO addresses ( + account, + diversifier_index_be, + address, + cached_transparent_receiver_address + ) + VALUES ( + :account, + :diversifier_index_be, + :address, + :cached_transparent_receiver_address + )", + )?; + + // the diversifier index is stored in big-endian order to allow sorting + let mut di_be = *diversifier_index.as_bytes(); + di_be.reverse(); + stmt.execute(named_params![ + ":account": u32::from(account), + ":diversifier_index_be": &di_be[..], + ":address": &address.encode(params), + ":cached_transparent_receiver_address": &address.transparent().map(|r| r.encode(params)), + ])?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::wallet::init::migrations::tests::test_migrate; + + #[test] + fn migrate() { + test_migrate(&[super::MIGRATION_ID]); + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/ensure_orchard_ua_receiver.rs b/zcash_client_sqlite/src/wallet/init/migrations/ensure_orchard_ua_receiver.rs new file mode 100644 index 0000000000..633fc37aec --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/ensure_orchard_ua_receiver.rs @@ -0,0 +1,198 @@ +//! This migration ensures that an Orchard receiver exists in the wallet's default Unified address. +use std::collections::HashSet; + +use rusqlite::named_params; +use schemerz_rusqlite::RusqliteMigration; +use uuid::Uuid; + +use zcash_keys::keys::{ + ReceiverRequirement::*, UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedIncomingViewingKey, +}; +use zcash_protocol::consensus; + +use super::orchard_received_notes; +use crate::{wallet::init::WalletMigrationError, UA_ORCHARD, UA_TRANSPARENT}; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0x604349c7_5ce5_4768_bea6_12d106ccda93); + +const DEPENDENCIES: &[Uuid] = &[orchard_received_notes::MIGRATION_ID]; + +pub(super) struct Migration

{ + pub(super) params: P, +} + +impl

schemerz::Migration for Migration

{ + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + DEPENDENCIES.iter().copied().collect() + } + + fn description(&self) -> &'static str { + "Ensures that the wallet's default address contains an Orchard receiver." + } +} + +impl RusqliteMigration for Migration

{ + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction<'_>) -> Result<(), Self::Error> { + let mut get_accounts = transaction.prepare("SELECT id, ufvk, uivk FROM accounts")?; + + let mut update_address = transaction.prepare( + r#"UPDATE "addresses" + SET address = :address + WHERE account_id = :account_id + AND diversifier_index_be = :j"#, + )?; + + let mut accounts = get_accounts.query([])?; + while let Some(row) = accounts.next()? { + let account_id = row.get::<_, u32>("id")?; + let ufvk_str: Option = row.get("ufvk")?; + let uivk = if let Some(ufvk_str) = ufvk_str { + UnifiedFullViewingKey::decode(&self.params, &ufvk_str[..]) + .map_err(|_| { + WalletMigrationError::CorruptedData("Unable to decode UFVK".to_string()) + })? + .to_unified_incoming_viewing_key() + } else { + let uivk_str: String = row.get("uivk")?; + UnifiedIncomingViewingKey::decode(&self.params, &uivk_str[..]).map_err(|_| { + WalletMigrationError::CorruptedData("Unable to decode UIVK".to_string()) + })? + }; + + let (default_addr, diversifier_index) = uivk.default_address(Some( + UnifiedAddressRequest::unsafe_new(UA_ORCHARD, Require, UA_TRANSPARENT), + ))?; + + let mut di_be = *diversifier_index.as_bytes(); + di_be.reverse(); + update_address.execute(named_params![ + ":address": default_addr.encode(&self.params), + ":account_id": account_id, + ":j": &di_be[..], + ])?; + } + + Ok(()) + } + + fn down(&self, _transaction: &rusqlite::Transaction<'_>) -> Result<(), Self::Error> { + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) + } +} + +#[cfg(test)] +mod tests { + use rusqlite::named_params; + use secrecy::SecretVec; + use tempfile::NamedTempFile; + + use zcash_keys::{ + address::Address, + keys::{ReceiverRequirement::*, UnifiedAddressRequest, UnifiedSpendingKey}, + }; + use zcash_protocol::consensus::Network; + + use crate::{ + wallet::init::{init_wallet_db, init_wallet_db_internal, migrations::addresses_table}, + WalletDb, UA_ORCHARD, UA_TRANSPARENT, + }; + + #[test] + fn init_migrate_add_orchard_receiver() { + let data_file = NamedTempFile::new().unwrap(); + let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap(); + + let seed = vec![0x10; 32]; + let account_id = 0u32; + let ufvk = UnifiedSpendingKey::from_seed( + &db_data.params, + &seed, + zip32::AccountId::try_from(account_id).unwrap(), + ) + .unwrap() + .to_unified_full_viewing_key(); + + assert_matches!( + init_wallet_db_internal( + &mut db_data, + Some(SecretVec::new(seed.clone())), + &[addresses_table::MIGRATION_ID], + false + ), + Ok(_) + ); + + // Manually create an entry in the addresses table for an address that lacks an Orchard + // receiver. + db_data + .conn + .execute( + "INSERT INTO accounts (account, ufvk) VALUES (:account_id, :ufvk)", + named_params![ + ":account_id": account_id, + ":ufvk": ufvk.encode(&db_data.params) + ], + ) + .unwrap(); + + let (addr, diversifier_index) = ufvk + .default_address(Some(UnifiedAddressRequest::unsafe_new( + Omit, + Require, + UA_TRANSPARENT, + ))) + .unwrap(); + let mut di_be = *diversifier_index.as_bytes(); + di_be.reverse(); + + db_data + .conn + .execute( + "INSERT INTO addresses (account, diversifier_index_be, address) + VALUES (:account_id, :j, :address) ", + named_params![ + ":account_id": account_id, + ":j": &di_be[..], + ":address": addr.encode(&db_data.params) + ], + ) + .unwrap(); + + match db_data + .conn + .query_row("SELECT address FROM addresses", [], |row| { + Ok(Address::decode(&db_data.params, &row.get::<_, String>(0)?).unwrap()) + }) { + Ok(Address::Unified(ua)) => { + assert!(!ua.has_orchard()); + assert!(ua.has_sapling()); + assert_eq!(ua.has_transparent(), UA_TRANSPARENT == Require); + } + other => panic!("Unexpected result from address decoding: {:?}", other), + } + + assert_matches!( + init_wallet_db(&mut db_data, Some(SecretVec::new(seed))), + Ok(_) + ); + + match db_data + .conn + .query_row("SELECT address FROM addresses", [], |row| { + Ok(Address::decode(&db_data.params, &row.get::<_, String>(0)?).unwrap()) + }) { + Ok(Address::Unified(ua)) => { + assert_eq!(ua.has_orchard(), UA_ORCHARD == Require); + assert!(ua.has_sapling()); + assert_eq!(ua.has_transparent(), UA_TRANSPARENT == Require); + } + other => panic!("Unexpected result from address decoding: {:?}", other), + } + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/ephemeral_addresses.rs b/zcash_client_sqlite/src/wallet/init/migrations/ephemeral_addresses.rs new file mode 100644 index 0000000000..109f307fff --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/ephemeral_addresses.rs @@ -0,0 +1,92 @@ +//! The migration that records ephemeral addresses for each account. +use std::collections::HashSet; + +use schemerz_rusqlite::RusqliteMigration; +use uuid::Uuid; +use zcash_protocol::consensus; + +use crate::wallet::init::WalletMigrationError; + +#[cfg(feature = "transparent-inputs")] +use crate::{wallet::transparent::ephemeral, AccountRef}; + +use super::utxos_to_txos; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0x0e1d4274_1f8e_44e2_909d_689a4bc2967b); + +const DEPENDENCIES: &[Uuid] = &[utxos_to_txos::MIGRATION_ID]; + +#[allow(dead_code)] +pub(super) struct Migration

{ + pub(super) params: P, +} + +impl

schemerz::Migration for Migration

{ + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + DEPENDENCIES.iter().copied().collect() + } + + fn description(&self) -> &'static str { + "Record ephemeral addresses for each account." + } +} + +impl RusqliteMigration for Migration

{ + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + transaction.execute_batch( + "CREATE TABLE ephemeral_addresses ( + account_id INTEGER NOT NULL, + address_index INTEGER NOT NULL, + -- nullability of this column is controlled by the index_range_and_address_nullity check + address TEXT, + used_in_tx INTEGER, + seen_in_tx INTEGER, + FOREIGN KEY (account_id) REFERENCES accounts(id), + FOREIGN KEY (used_in_tx) REFERENCES transactions(id_tx), + FOREIGN KEY (seen_in_tx) REFERENCES transactions(id_tx), + PRIMARY KEY (account_id, address_index), + CONSTRAINT ephemeral_addr_uniq UNIQUE (address), + CONSTRAINT used_implies_seen CHECK ( + used_in_tx IS NULL OR seen_in_tx IS NOT NULL + ), + CONSTRAINT index_range_and_address_nullity CHECK ( + (address_index BETWEEN 0 AND 0x7FFFFFFF AND address IS NOT NULL) OR + (address_index BETWEEN 0x80000000 AND 0x7FFFFFFF + 20 AND address IS NULL AND used_in_tx IS NULL AND seen_in_tx IS NULL) + ) + ) WITHOUT ROWID;" + )?; + + // Make sure that at least `GAP_LIMIT` ephemeral transparent addresses are + // stored in each account. + #[cfg(feature = "transparent-inputs")] + { + let mut stmt = transaction.prepare("SELECT id FROM accounts")?; + let mut rows = stmt.query([])?; + while let Some(row) = rows.next()? { + let account_id = AccountRef(row.get(0)?); + ephemeral::init_account(transaction, &self.params, account_id)?; + } + } + Ok(()) + } + + fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) + } +} + +#[cfg(test)] +mod tests { + use crate::wallet::init::migrations::tests::test_migrate; + + #[test] + fn migrate() { + test_migrate(&[super::MIGRATION_ID]); + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/fix_bad_change_flagging.rs b/zcash_client_sqlite/src/wallet/init/migrations/fix_bad_change_flagging.rs new file mode 100644 index 0000000000..38b1d49ae9 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/fix_bad_change_flagging.rs @@ -0,0 +1,79 @@ +//! Sets the `is_change` flag on output notes received by an internal key when input value was +//! provided from the account corresponding to that key. +use std::collections::HashSet; + +use rusqlite::named_params; +use schemerz_rusqlite::RusqliteMigration; +use uuid::Uuid; +use zip32::Scope; + +use crate::{ + wallet::{ + init::{migrations::fix_broken_commitment_trees, WalletMigrationError}, + scope_code, + }, + SAPLING_TABLES_PREFIX, +}; + +#[cfg(feature = "orchard")] +use crate::ORCHARD_TABLES_PREFIX; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0x6d36656d_533b_4b65_ae91_dcb95c4ad289); + +const DEPENDENCIES: &[Uuid] = &[fix_broken_commitment_trees::MIGRATION_ID]; + +pub(super) struct Migration; + +impl schemerz::Migration for Migration { + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + DEPENDENCIES.iter().copied().collect() + } + + fn description(&self) -> &'static str { + "Sets the `is_change` flag on output notes received by an internal key when input value was provided from the account corresponding to that key." + } +} + +impl RusqliteMigration for Migration { + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + let fix_change_flag = |table_prefix| { + transaction.execute( + &format!( + "UPDATE {table_prefix}_received_notes + SET is_change = 1 + FROM sent_notes sn + WHERE sn.tx = {table_prefix}_received_notes.tx + AND sn.from_account_id = {table_prefix}_received_notes.account_id + AND {table_prefix}_received_notes.recipient_key_scope = :internal_scope" + ), + named_params! {":internal_scope": scope_code(Scope::Internal)}, + ) + }; + + fix_change_flag(SAPLING_TABLES_PREFIX)?; + #[cfg(feature = "orchard")] + fix_change_flag(ORCHARD_TABLES_PREFIX)?; + + Ok(()) + } + + fn down(&self, _: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) + } +} + +#[cfg(test)] +mod tests { + use crate::wallet::init::migrations::tests::test_migrate; + + #[test] + fn migrate() { + test_migrate(&[super::MIGRATION_ID]); + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/fix_broken_commitment_trees.rs b/zcash_client_sqlite/src/wallet/init/migrations/fix_broken_commitment_trees.rs new file mode 100644 index 0000000000..39e1e0285c --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/fix_broken_commitment_trees.rs @@ -0,0 +1,81 @@ +//! Truncates away bad note commitment tree state for users whose wallets were broken by incorrect +//! reorg handling. +use std::collections::HashSet; + +use rusqlite::OptionalExtension; +use schemerz_rusqlite::RusqliteMigration; +use uuid::Uuid; +use zcash_protocol::consensus::{self, BlockHeight}; + +use crate::wallet::{ + self, + init::{migrations::support_legacy_sqlite, WalletMigrationError}, +}; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0x9fa43ce0_a387_45d1_be03_57a3edc76d01); + +const DEPENDENCIES: &[Uuid] = &[support_legacy_sqlite::MIGRATION_ID]; + +pub(super) struct Migration

{ + pub(super) params: P, +} + +impl

schemerz::Migration for Migration

{ + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + DEPENDENCIES.iter().copied().collect() + } + + fn description(&self) -> &'static str { + "Truncates away bad note commitment tree state for users whose wallets were broken by bad reorg handling." + } +} + +impl RusqliteMigration for Migration

{ + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + #[cfg(not(feature = "orchard"))] + let max_height_query = r#" + SELECT MAX(height) FROM blocks + JOIN sapling_tree_checkpoints sc ON sc.checkpoint_id = height + "#; + #[cfg(feature = "orchard")] + let max_height_query = r#" + SELECT MAX(height) FROM blocks + JOIN sapling_tree_checkpoints sc ON sc.checkpoint_id = height + JOIN orchard_tree_checkpoints oc ON oc.checkpoint_id = height + "#; + + let max_block_height = transaction + .query_row(max_height_query, [], |row| { + let cid = row.get::<_, Option>(0)?; + Ok(cid.map(BlockHeight::from)) + }) + .optional()? + .flatten(); + + if let Some(h) = max_block_height { + wallet::truncate_to_height(transaction, &self.params, h)?; + } + + Ok(()) + } + + fn down(&self, _: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) + } +} + +#[cfg(test)] +mod tests { + use crate::wallet::init::migrations::tests::test_migrate; + + #[test] + fn migrate() { + test_migrate(&[super::MIGRATION_ID]); + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/full_account_ids.rs b/zcash_client_sqlite/src/wallet/init/migrations/full_account_ids.rs new file mode 100644 index 0000000000..9d2abc5662 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/full_account_ids.rs @@ -0,0 +1,591 @@ +use std::{collections::HashSet, rc::Rc}; + +use rusqlite::{named_params, OptionalExtension, Transaction}; +use schemerz_rusqlite::RusqliteMigration; +use secrecy::{ExposeSecret, SecretVec}; +use uuid::Uuid; + +use zcash_client_backend::data_api::{AccountPurpose, AccountSource, Zip32Derivation}; +use zcash_keys::keys::{UnifiedFullViewingKey, UnifiedSpendingKey}; +use zcash_protocol::consensus; +use zip32::fingerprint::SeedFingerprint; + +use super::{ + add_account_birthdays, receiving_key_scopes, v_transactions_note_uniqueness, wallet_summaries, +}; +use crate::wallet::{account_kind_code, init::WalletMigrationError}; + +/// The migration that switched from presumed seed-derived account IDs to supporting +/// HD accounts and all sorts of imported keys. +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0x6d02ec76_8720_4cc6_b646_c4e2ce69221c); + +pub(crate) struct Migration { + pub(super) seed: Option>>, + pub(super) params: P, +} + +const DEPENDENCIES: &[Uuid] = &[ + receiving_key_scopes::MIGRATION_ID, + add_account_birthdays::MIGRATION_ID, + v_transactions_note_uniqueness::MIGRATION_ID, + wallet_summaries::MIGRATION_ID, +]; + +impl schemerz::Migration for Migration

{ + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + DEPENDENCIES.iter().copied().collect() + } + + fn description(&self) -> &'static str { + "Replaces the `account` column in the `accounts` table with columns to support all kinds of + account and key types." + } +} + +impl RusqliteMigration for Migration

{ + type Error = WalletMigrationError; + + fn up(&self, transaction: &Transaction) -> Result<(), WalletMigrationError> { + let account_kind_derived = account_kind_code(&AccountSource::Derived { + derivation: Zip32Derivation::new( + SeedFingerprint::from_bytes([0; 32]), + zip32::AccountId::ZERO, + ), + key_source: None, + }); + let account_kind_imported = account_kind_code(&AccountSource::Imported { + // the purpose here is irrelevant; we just use it to get the correct code + // for the account kind + purpose: AccountPurpose::ViewOnly, + key_source: None, + }); + transaction.execute_batch(&format!( + r#" + CREATE TABLE accounts_new ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + account_kind INTEGER NOT NULL DEFAULT {account_kind_derived}, + hd_seed_fingerprint BLOB, + hd_account_index INTEGER, + ufvk TEXT, + uivk TEXT NOT NULL, + orchard_fvk_item_cache BLOB, + sapling_fvk_item_cache BLOB, + p2pkh_fvk_item_cache BLOB, + birthday_height INTEGER NOT NULL, + birthday_sapling_tree_size INTEGER, + birthday_orchard_tree_size INTEGER, + recover_until_height INTEGER, + CHECK ( + ( + account_kind = {account_kind_derived} + AND hd_seed_fingerprint IS NOT NULL + AND hd_account_index IS NOT NULL + AND ufvk IS NOT NULL + ) + OR + ( + account_kind = {account_kind_imported} + AND hd_seed_fingerprint IS NULL + AND hd_account_index IS NULL + ) + ) + ); + CREATE UNIQUE INDEX hd_account ON accounts_new (hd_seed_fingerprint, hd_account_index); + CREATE UNIQUE INDEX accounts_uivk ON accounts_new (uivk); + CREATE UNIQUE INDEX accounts_ufvk ON accounts_new (ufvk); + "# + ))?; + + // We require the seed *if* there are existing accounts in the table. + if transaction.query_row("SELECT COUNT(*) FROM accounts", [], |row| { + Ok(row.get::<_, u32>(0)? > 0) + })? { + if let Some(seed) = &self.seed { + let seed_id = SeedFingerprint::from_seed(seed.expose_secret()) + .expect("Seed is between 32 and 252 bytes in length."); + + // We track whether we have determined seed relevance or not, in order to + // correctly report errors when checking the seed against an account: + // + // - If we encounter an error with the first account, we can assert that + // the seed is not relevant to the wallet by assuming that: + // - All accounts are from the same seed (which is historically the only + // use case that this migration supported), and + // - All accounts in the wallet must have been able to derive their USKs + // (in order to derive UIVKs). + // + // - Once the seed has been determined to be relevant (because it matched + // the first account), any subsequent account derivation failure is + // proving wrong our second assumption above, and we report this as + // corrupted data. + let mut seed_is_relevant = false; + + let mut q = transaction.prepare("SELECT * FROM accounts")?; + let mut rows = q.query([])?; + while let Some(row) = rows.next()? { + let account_index: u32 = row.get("account")?; + let birthday_height: u32 = row.get("birthday_height")?; + let recover_until_height: Option = row.get("recover_until_height")?; + + // Although 'id' is an AUTOINCREMENT column, we'll set it explicitly to match the old account value + // strictly as a matter of convenience to make this migration script easier, + // specifically around updating tables with foreign keys to this one. + let account_id = account_index; + + // Verify that the UFVK is as expected by re-deriving it. + let ufvk: String = row.get("ufvk")?; + let ufvk_parsed = UnifiedFullViewingKey::decode(&self.params, &ufvk) + .map_err(|_| WalletMigrationError::CorruptedData("Bad UFVK".to_string()))?; + let usk = UnifiedSpendingKey::from_seed( + &self.params, + seed.expose_secret(), + zip32::AccountId::try_from(account_index).map_err(|_| { + WalletMigrationError::CorruptedData("Bad account index".to_string()) + })?, + ) + .map_err(|_| { + if seed_is_relevant { + WalletMigrationError::CorruptedData( + "Unable to derive spending key from seed.".to_string(), + ) + } else { + WalletMigrationError::SeedNotRelevant + } + })?; + let expected_ufvk = usk.to_unified_full_viewing_key(); + if ufvk != expected_ufvk.encode(&self.params) { + return Err(if seed_is_relevant { + WalletMigrationError::CorruptedData( + "UFVK does not match expected value.".to_string(), + ) + } else { + WalletMigrationError::SeedNotRelevant + }); + } + + // We made it past one derived account, so the seed must be relevant. + seed_is_relevant = true; + + let uivk = ufvk_parsed + .to_unified_incoming_viewing_key() + .encode(&self.params); + + #[cfg(feature = "orchard")] + let orchard_item = ufvk_parsed.orchard().map(|k| k.to_bytes()); + #[cfg(not(feature = "orchard"))] + let orchard_item: Option> = None; + + let sapling_item = ufvk_parsed.sapling().map(|k| k.to_bytes()); + + #[cfg(feature = "transparent-inputs")] + let transparent_item = ufvk_parsed.transparent().map(|k| k.serialize()); + #[cfg(not(feature = "transparent-inputs"))] + let transparent_item: Option> = None; + + // Get the tree sizes for the birthday height from the blocks table, if + // available. + let (birthday_sapling_tree_size, birthday_orchard_tree_size) = transaction + .query_row( + "SELECT sapling_commitment_tree_size - sapling_output_count, + orchard_commitment_tree_size - orchard_action_count + FROM blocks + WHERE height = :birthday_height", + named_params![":birthday_height": birthday_height], + |row| { + Ok(row + .get::<_, Option>(0)? + .zip(row.get::<_, Option>(1)?)) + }, + ) + .optional()? + .flatten() + .map_or((None, None), |(s, o)| (Some(s), Some(o))); + + transaction.execute( + r#" + INSERT INTO accounts_new ( + id, account_kind, hd_seed_fingerprint, hd_account_index, + ufvk, uivk, + orchard_fvk_item_cache, sapling_fvk_item_cache, p2pkh_fvk_item_cache, + birthday_height, birthday_sapling_tree_size, birthday_orchard_tree_size, + recover_until_height + ) + VALUES ( + :account_id, :account_kind, :seed_id, :account_index, + :ufvk, :uivk, + :orchard_fvk_item_cache, :sapling_fvk_item_cache, :p2pkh_fvk_item_cache, + :birthday_height, :birthday_sapling_tree_size, :birthday_orchard_tree_size, + :recover_until_height + ); + "#, + named_params![ + ":account_id": account_id, + ":account_kind": account_kind_derived, + ":seed_id": seed_id.to_bytes(), + ":account_index": account_index, + ":ufvk": ufvk, + ":uivk": uivk, + ":orchard_fvk_item_cache": orchard_item, + ":sapling_fvk_item_cache": sapling_item, + ":p2pkh_fvk_item_cache": transparent_item, + ":birthday_height": birthday_height, + ":birthday_sapling_tree_size": birthday_sapling_tree_size, + ":birthday_orchard_tree_size": birthday_orchard_tree_size, + ":recover_until_height": recover_until_height, + ], + )?; + } + } else { + return Err(WalletMigrationError::SeedRequired); + } + } + + transaction.execute_batch(r#" + PRAGMA legacy_alter_table = ON; + + DROP TABLE accounts; + ALTER TABLE accounts_new RENAME TO accounts; + + -- Migrate addresses table + CREATE TABLE addresses_new ( + account_id INTEGER NOT NULL, + diversifier_index_be BLOB NOT NULL, + address TEXT NOT NULL, + cached_transparent_receiver_address TEXT, + FOREIGN KEY (account_id) REFERENCES accounts(id), + CONSTRAINT diversification UNIQUE (account_id, diversifier_index_be) + ); + CREATE INDEX "addresses_accounts" ON "addresses_new" ( + "account_id" ASC + ); + INSERT INTO addresses_new (account_id, diversifier_index_be, address, cached_transparent_receiver_address) + SELECT account, diversifier_index_be, address, cached_transparent_receiver_address + FROM addresses; + + DROP TABLE addresses; + ALTER TABLE addresses_new RENAME TO addresses; + + -- Migrate sapling_received_notes table + CREATE TABLE sapling_received_notes_new ( + id INTEGER PRIMARY KEY, + tx INTEGER NOT NULL, + output_index INTEGER NOT NULL, + account_id INTEGER NOT NULL, + diversifier BLOB NOT NULL, + value INTEGER NOT NULL, + rcm BLOB NOT NULL, + nf BLOB UNIQUE, + is_change INTEGER NOT NULL, + memo BLOB, + commitment_tree_position INTEGER, + recipient_key_scope INTEGER, + FOREIGN KEY (tx) REFERENCES transactions(id_tx), + FOREIGN KEY (account_id) REFERENCES accounts(id), + CONSTRAINT tx_output UNIQUE (tx, output_index) + ); + CREATE INDEX "sapling_received_notes_account" ON "sapling_received_notes_new" ( + "account_id" ASC + ); + CREATE INDEX "sapling_received_notes_tx" ON "sapling_received_notes_new" ( + "tx" ASC + ); + + -- Replace the `spent` column in `sapling_received_notes` with a junction table between + -- received notes and the transactions that spend them. This is necessary as otherwise + -- we cannot compute the correct value of transactions that expire unmined. + CREATE TABLE sapling_received_note_spends ( + sapling_received_note_id INTEGER NOT NULL, + transaction_id INTEGER NOT NULL, + FOREIGN KEY (sapling_received_note_id) + REFERENCES sapling_received_notes(id) + ON DELETE CASCADE, + FOREIGN KEY (transaction_id) + -- We do not delete transactions, so this does not cascade + REFERENCES transactions(id_tx), + UNIQUE (sapling_received_note_id, transaction_id) + ); + + INSERT INTO sapling_received_note_spends (sapling_received_note_id, transaction_id) + SELECT id_note, spent + FROM sapling_received_notes + WHERE spent IS NOT NULL; + + INSERT INTO sapling_received_notes_new ( + id, tx, output_index, account_id, + diversifier, value, rcm, nf, is_change, memo, commitment_tree_position, + recipient_key_scope + ) + SELECT + id_note, tx, output_index, account, + diversifier, value, rcm, nf, is_change, memo, commitment_tree_position, + recipient_key_scope + FROM sapling_received_notes; + + DROP TABLE sapling_received_notes; + ALTER TABLE sapling_received_notes_new RENAME TO sapling_received_notes; + + -- Migrate sent_notes table + CREATE TABLE sent_notes_new ( + id INTEGER PRIMARY KEY, + tx INTEGER NOT NULL, + output_pool INTEGER NOT NULL, + output_index INTEGER NOT NULL, + from_account_id INTEGER NOT NULL, + to_address TEXT, + to_account_id INTEGER, + value INTEGER NOT NULL, + memo BLOB, + FOREIGN KEY (tx) REFERENCES transactions(id_tx), + FOREIGN KEY (from_account_id) REFERENCES accounts(id), + FOREIGN KEY (to_account_id) REFERENCES accounts(id), + CONSTRAINT tx_output UNIQUE (tx, output_pool, output_index), + CONSTRAINT note_recipient CHECK ( + (to_address IS NOT NULL) OR (to_account_id IS NOT NULL) + ) + ); + CREATE INDEX sent_notes_tx ON sent_notes_new (tx); + CREATE INDEX sent_notes_from_account ON sent_notes_new (from_account_id); + CREATE INDEX sent_notes_to_account ON sent_notes_new (to_account_id); + INSERT INTO sent_notes_new (id, tx, output_pool, output_index, from_account_id, to_address, to_account_id, value, memo) + SELECT id_note, tx, output_pool, output_index, from_account, to_address, to_account, value, memo + FROM sent_notes; + + DROP TABLE sent_notes; + ALTER TABLE sent_notes_new RENAME TO sent_notes; + + -- No one uses this table any more, and it contains a reference to columns we renamed. + DROP TABLE sapling_witnesses; + + -- Migrate utxos table + CREATE TABLE utxos_new ( + id INTEGER PRIMARY KEY, + received_by_account_id INTEGER NOT NULL, + address TEXT NOT NULL, + prevout_txid BLOB NOT NULL, + prevout_idx INTEGER NOT NULL, + script BLOB NOT NULL, + value_zat INTEGER NOT NULL, + height INTEGER NOT NULL, + FOREIGN KEY (received_by_account_id) REFERENCES accounts(id), + CONSTRAINT tx_outpoint UNIQUE (prevout_txid, prevout_idx) + ); + CREATE INDEX utxos_received_by_account ON utxos_new (received_by_account_id); + + INSERT INTO utxos_new (id, received_by_account_id, address, prevout_txid, prevout_idx, script, value_zat, height) + SELECT id_utxo, received_by_account, address, prevout_txid, prevout_idx, script, value_zat, height + FROM utxos; + + -- Replace the `spent_in_tx` column in `utxos` with a junction table between received + -- outputs and the transactions that spend them. This is necessary as otherwise we + -- cannot compute the correct value of transactions that expire unmined. + CREATE TABLE transparent_received_output_spends ( + transparent_received_output_id INTEGER NOT NULL, + transaction_id INTEGER NOT NULL, + FOREIGN KEY (transparent_received_output_id) + REFERENCES utxos(id) + ON DELETE CASCADE, + FOREIGN KEY (transaction_id) + -- We do not delete transactions, so this does not cascade + REFERENCES transactions(id_tx), + UNIQUE (transparent_received_output_id, transaction_id) + ); + + INSERT INTO transparent_received_output_spends (transparent_received_output_id, transaction_id) + SELECT id_utxo, spent_in_tx + FROM utxos + WHERE spent_in_tx IS NOT NULL; + + DROP TABLE utxos; + ALTER TABLE utxos_new RENAME TO utxos; + + PRAGMA legacy_alter_table = OFF; + "#)?; + + // Rewrite v_transactions view + transaction.execute_batch(" + DROP VIEW v_transactions; + CREATE VIEW v_transactions AS + WITH + notes AS ( + SELECT sapling_received_notes.id AS id, + sapling_received_notes.account_id AS account_id, + transactions.block AS block, + transactions.txid AS txid, + 2 AS pool, + sapling_received_notes.value AS value, + CASE + WHEN sapling_received_notes.is_change THEN 1 + ELSE 0 + END AS is_change, + CASE + WHEN sapling_received_notes.is_change THEN 0 + ELSE 1 + END AS received_count, + CASE + WHEN (sapling_received_notes.memo IS NULL OR sapling_received_notes.memo = X'F6') + THEN 0 + ELSE 1 + END AS memo_present + FROM sapling_received_notes + JOIN transactions + ON transactions.id_tx = sapling_received_notes.tx + UNION + SELECT utxos.id AS id, + utxos.received_by_account_id AS account_id, + utxos.height AS block, + utxos.prevout_txid AS txid, + 0 AS pool, + utxos.value_zat AS value, + 0 AS is_change, + 1 AS received_count, + 0 AS memo_present + FROM utxos + UNION + SELECT sapling_received_notes.id AS id, + sapling_received_notes.account_id AS account_id, + transactions.block AS block, + transactions.txid AS txid, + 2 AS pool, + -sapling_received_notes.value AS value, + 0 AS is_change, + 0 AS received_count, + 0 AS memo_present + FROM sapling_received_notes + JOIN sapling_received_note_spends + ON sapling_received_note_id = sapling_received_notes.id + JOIN transactions + ON transactions.id_tx = sapling_received_note_spends.transaction_id + UNION + SELECT utxos.id AS id, + utxos.received_by_account_id AS account_id, + transactions.block AS block, + transactions.txid AS txid, + 0 AS pool, + -utxos.value_zat AS value, + 0 AS is_change, + 0 AS received_count, + 0 AS memo_present + FROM utxos + JOIN transparent_received_output_spends txo_spends + ON txo_spends.transparent_received_output_id = txos.id + JOIN transactions + ON transactions.id_tx = txo_spends.transaction_id + ), + sent_note_counts AS ( + SELECT sent_notes.from_account_id AS account_id, + transactions.txid AS txid, + COUNT(DISTINCT sent_notes.id) as sent_notes, + SUM( + CASE + WHEN (sent_notes.memo IS NULL OR sent_notes.memo = X'F6' OR sapling_received_notes.tx IS NOT NULL) + THEN 0 + ELSE 1 + END + ) AS memo_count + FROM sent_notes + JOIN transactions + ON transactions.id_tx = sent_notes.tx + LEFT JOIN sapling_received_notes + ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) = + (sapling_received_notes.tx, 2, sapling_received_notes.output_index) + WHERE COALESCE(sapling_received_notes.is_change, 0) = 0 + GROUP BY account_id, txid + ), + blocks_max_height AS ( + SELECT MAX(blocks.height) as max_height FROM blocks + ) + SELECT notes.account_id AS account_id, + notes.block AS mined_height, + notes.txid AS txid, + transactions.tx_index AS tx_index, + transactions.expiry_height AS expiry_height, + transactions.raw AS raw, + SUM(notes.value) AS account_balance_delta, + transactions.fee AS fee_paid, + SUM(notes.is_change) > 0 AS has_change, + MAX(COALESCE(sent_note_counts.sent_notes, 0)) AS sent_note_count, + SUM(notes.received_count) AS received_note_count, + SUM(notes.memo_present) + MAX(COALESCE(sent_note_counts.memo_count, 0)) AS memo_count, + blocks.time AS block_time, + ( + blocks.height IS NULL + AND transactions.expiry_height BETWEEN 1 AND blocks_max_height.max_height + ) AS expired_unmined + FROM notes + LEFT JOIN transactions + ON notes.txid = transactions.txid + JOIN blocks_max_height + LEFT JOIN blocks ON blocks.height = notes.block + LEFT JOIN sent_note_counts + ON sent_note_counts.account_id = notes.account_id + AND sent_note_counts.txid = notes.txid + GROUP BY notes.account_id, notes.txid; + + DROP VIEW v_tx_outputs; + CREATE VIEW v_tx_outputs AS + SELECT transactions.txid AS txid, + 2 AS output_pool, + sapling_received_notes.output_index AS output_index, + sent_notes.from_account_id AS from_account_id, + sapling_received_notes.account_id AS to_account_id, + NULL AS to_address, + sapling_received_notes.value AS value, + sapling_received_notes.is_change AS is_change, + sapling_received_notes.memo AS memo + FROM sapling_received_notes + JOIN transactions + ON transactions.id_tx = sapling_received_notes.tx + LEFT JOIN sent_notes + ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) = + (sapling_received_notes.tx, 2, sent_notes.output_index) + UNION + SELECT utxos.prevout_txid AS txid, + 0 AS output_pool, + utxos.prevout_idx AS output_index, + NULL AS from_account_id, + utxos.received_by_account_id AS to_account_id, + utxos.address AS to_address, + utxos.value_zat AS value, + 0 AS is_change, + NULL AS memo + FROM utxos + UNION + SELECT transactions.txid AS txid, + sent_notes.output_pool AS output_pool, + sent_notes.output_index AS output_index, + sent_notes.from_account_id AS from_account_id, + sapling_received_notes.account_id AS to_account_id, + sent_notes.to_address AS to_address, + sent_notes.value AS value, + 0 AS is_change, + sent_notes.memo AS memo + FROM sent_notes + JOIN transactions + ON transactions.id_tx = sent_notes.tx + LEFT JOIN sapling_received_notes + ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) = + (sapling_received_notes.tx, 2, sapling_received_notes.output_index) + WHERE COALESCE(sapling_received_notes.is_change, 0) = 0; + ")?; + + Ok(()) + } + + fn down(&self, _transaction: &Transaction) -> Result<(), WalletMigrationError> { + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) + } +} + +#[cfg(test)] +mod tests { + use crate::wallet::init::migrations::tests::test_migrate; + + #[test] + fn migrate() { + test_migrate(&[super::MIGRATION_ID]); + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/initial_setup.rs b/zcash_client_sqlite/src/wallet/init/migrations/initial_setup.rs new file mode 100644 index 0000000000..5433fcf676 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/initial_setup.rs @@ -0,0 +1,117 @@ +//! The migration that performs the initial setup of the wallet database. +use std::collections::HashSet; + +use schemerz_rusqlite::RusqliteMigration; +use uuid::Uuid; + +use crate::wallet::init::WalletMigrationError; + +/// Identifier for the migration that performs the initial setup of the wallet database. +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0xbc4f5e57_d600_4b6c_990f_b3538f0bfce1); + +pub(super) struct Migration; + +impl schemerz::Migration for Migration { + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + HashSet::new() + } + + fn description(&self) -> &'static str { + "Initialize the wallet database." + } +} + +impl RusqliteMigration for Migration { + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + transaction.execute_batch( + // We set the user_version field of the database to a constant value of 8 to allow + // correct integration with the Android SDK with versions of the database that were + // created prior to the introduction of migrations in this crate. This constant should + // remain fixed going forward, and should not be altered by migrations; migration + // status is maintained exclusively by the schemer_migrations table. + "PRAGMA user_version = 8; + CREATE TABLE IF NOT EXISTS accounts ( + account INTEGER PRIMARY KEY, + extfvk TEXT NOT NULL, + address TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS blocks ( + height INTEGER PRIMARY KEY, + hash BLOB NOT NULL, + time INTEGER NOT NULL, + sapling_tree BLOB NOT NULL + ); + CREATE TABLE IF NOT EXISTS transactions ( + id_tx INTEGER PRIMARY KEY, + txid BLOB NOT NULL UNIQUE, + created TEXT, + block INTEGER, + tx_index INTEGER, + expiry_height INTEGER, + raw BLOB, + FOREIGN KEY (block) REFERENCES blocks(height) + ); + CREATE TABLE IF NOT EXISTS received_notes ( + id_note INTEGER PRIMARY KEY, + tx INTEGER NOT NULL, + output_index INTEGER NOT NULL, + account INTEGER NOT NULL, + diversifier BLOB NOT NULL, + value INTEGER NOT NULL, + rcm BLOB NOT NULL, + nf BLOB NOT NULL UNIQUE, + is_change INTEGER NOT NULL, + memo BLOB, + spent INTEGER, + FOREIGN KEY (tx) REFERENCES transactions(id_tx), + FOREIGN KEY (account) REFERENCES accounts(account), + FOREIGN KEY (spent) REFERENCES transactions(id_tx), + CONSTRAINT tx_output UNIQUE (tx, output_index) + ); + CREATE TABLE IF NOT EXISTS sapling_witnesses ( + id_witness INTEGER PRIMARY KEY, + note INTEGER NOT NULL, + block INTEGER NOT NULL, + witness BLOB NOT NULL, + FOREIGN KEY (note) REFERENCES received_notes(id_note), + FOREIGN KEY (block) REFERENCES blocks(height), + CONSTRAINT witness_height UNIQUE (note, block) + ); + CREATE TABLE IF NOT EXISTS sent_notes ( + id_note INTEGER PRIMARY KEY, + tx INTEGER NOT NULL, + output_index INTEGER NOT NULL, + from_account INTEGER NOT NULL, + address TEXT NOT NULL, + value INTEGER NOT NULL, + memo BLOB, + FOREIGN KEY (tx) REFERENCES transactions(id_tx), + FOREIGN KEY (from_account) REFERENCES accounts(account), + CONSTRAINT tx_output UNIQUE (tx, output_index) + );", + )?; + Ok(()) + } + + fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + // We should never down-migrate the first migration, as that can irreversibly + // destroy data. + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) + } +} + +#[cfg(test)] +mod tests { + use crate::wallet::init::migrations::tests::test_migrate; + + #[test] + fn migrate() { + test_migrate(&[super::MIGRATION_ID]); + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/nullifier_map.rs b/zcash_client_sqlite/src/wallet/init/migrations/nullifier_map.rs new file mode 100644 index 0000000000..5891841d45 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/nullifier_map.rs @@ -0,0 +1,83 @@ +//! This migration adds a table for storing mappings from nullifiers to the transaction +//! they are revealed in. + +use std::collections::HashSet; + +use schemerz_rusqlite::RusqliteMigration; +use tracing::debug; +use uuid::Uuid; + +use crate::wallet::init::WalletMigrationError; + +use super::received_notes_nullable_nf; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0xe2d71ac5_6a44_4c6b_a9a0_6d0a79d355f1); + +const DEPENDENCIES: &[Uuid] = &[received_notes_nullable_nf::MIGRATION_ID]; + +pub(super) struct Migration; + +impl schemerz::Migration for Migration { + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + DEPENDENCIES.iter().copied().collect() + } + + fn description(&self) -> &'static str { + "Adds a lookup table for nullifiers we've observed on-chain that we haven't confirmed are not ours." + } +} + +impl RusqliteMigration for Migration { + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), Self::Error> { + // We don't enforce any foreign key constraint to the blocks table, to allow + // loading the nullifier map separately from block scanning. + debug!("Creating tables for nullifier map"); + transaction.execute_batch( + "CREATE TABLE tx_locator_map ( + block_height INTEGER NOT NULL, + tx_index INTEGER NOT NULL, + txid BLOB NOT NULL UNIQUE, + PRIMARY KEY (block_height, tx_index) + ); + CREATE TABLE nullifier_map ( + spend_pool INTEGER NOT NULL, + nf BLOB NOT NULL, + block_height INTEGER NOT NULL, + tx_index INTEGER NOT NULL, + CONSTRAINT tx_locator + FOREIGN KEY (block_height, tx_index) + REFERENCES tx_locator_map(block_height, tx_index) + ON DELETE CASCADE + ON UPDATE RESTRICT, + CONSTRAINT nf_uniq UNIQUE (spend_pool, nf) + ); + CREATE INDEX nf_map_locator_idx ON nullifier_map(block_height, tx_index);", + )?; + + Ok(()) + } + + fn down(&self, transaction: &rusqlite::Transaction) -> Result<(), Self::Error> { + transaction.execute_batch( + "DROP TABLE nullifier_map; + DROP TABLE tx_locator_map;", + )?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::wallet::init::migrations::tests::test_migrate; + + #[test] + fn migrate() { + test_migrate(&[super::MIGRATION_ID]); + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/orchard_received_notes.rs b/zcash_client_sqlite/src/wallet/init/migrations/orchard_received_notes.rs new file mode 100644 index 0000000000..2a2acd7467 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/orchard_received_notes.rs @@ -0,0 +1,327 @@ +//! This migration adds tables to the wallet database that are needed to persist Orchard received +//! notes. + +use std::collections::HashSet; + +use schemerz_rusqlite::RusqliteMigration; +use uuid::Uuid; + +use zcash_protocol::PoolType; + +use super::full_account_ids; +use crate::wallet::{init::WalletMigrationError, pool_code}; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0x51d7a273_aa19_4109_9325_80e4a5545048); + +const DEPENDENCIES: &[Uuid] = &[full_account_ids::MIGRATION_ID]; + +pub(super) struct Migration; + +impl schemerz::Migration for Migration { + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + DEPENDENCIES.iter().copied().collect() + } + + fn description(&self) -> &'static str { + "Add support for storage of Orchard received notes." + } +} + +impl RusqliteMigration for Migration { + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction<'_>) -> Result<(), Self::Error> { + transaction.execute_batch( + "CREATE TABLE orchard_received_notes ( + id INTEGER PRIMARY KEY, + tx INTEGER NOT NULL, + action_index INTEGER NOT NULL, + account_id INTEGER NOT NULL, + diversifier BLOB NOT NULL, + value INTEGER NOT NULL, + rho BLOB NOT NULL, + rseed BLOB NOT NULL, + nf BLOB UNIQUE, + is_change INTEGER NOT NULL, + memo BLOB, + commitment_tree_position INTEGER, + recipient_key_scope INTEGER, + FOREIGN KEY (tx) REFERENCES transactions(id_tx), + FOREIGN KEY (account_id) REFERENCES accounts(id), + CONSTRAINT tx_output UNIQUE (tx, action_index) + ); + CREATE INDEX orchard_received_notes_account ON orchard_received_notes ( + account_id ASC + ); + CREATE INDEX orchard_received_notes_tx ON orchard_received_notes ( + tx ASC + ); + + CREATE TABLE orchard_received_note_spends ( + orchard_received_note_id INTEGER NOT NULL, + transaction_id INTEGER NOT NULL, + FOREIGN KEY (orchard_received_note_id) + REFERENCES orchard_received_notes(id) + ON DELETE CASCADE, + FOREIGN KEY (transaction_id) + -- We do not delete transactions, so this does not cascade + REFERENCES transactions(id_tx), + UNIQUE (orchard_received_note_id, transaction_id) + );", + )?; + + transaction.execute_batch({ + let sapling_pool_code = pool_code(PoolType::SAPLING); + let orchard_pool_code = pool_code(PoolType::ORCHARD); + &format!( + "CREATE VIEW v_received_notes AS + SELECT + sapling_received_notes.id AS id_within_pool_table, + sapling_received_notes.tx, + {sapling_pool_code} AS pool, + sapling_received_notes.output_index AS output_index, + account_id, + sapling_received_notes.value, + is_change, + sapling_received_notes.memo, + sent_notes.id AS sent_note_id + FROM sapling_received_notes + LEFT JOIN sent_notes + ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) = + (sapling_received_notes.tx, {sapling_pool_code}, sapling_received_notes.output_index) + UNION + SELECT + orchard_received_notes.id AS id_within_pool_table, + orchard_received_notes.tx, + {orchard_pool_code} AS pool, + orchard_received_notes.action_index AS output_index, + account_id, + orchard_received_notes.value, + is_change, + orchard_received_notes.memo, + sent_notes.id AS sent_note_id + FROM orchard_received_notes + LEFT JOIN sent_notes + ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) = + (orchard_received_notes.tx, {orchard_pool_code}, orchard_received_notes.action_index);" + ) + })?; + + transaction.execute_batch({ + let sapling_pool_code = pool_code(PoolType::SAPLING); + let orchard_pool_code = pool_code(PoolType::ORCHARD); + &format!( + "CREATE VIEW v_received_note_spends AS + SELECT + {sapling_pool_code} AS pool, + sapling_received_note_id AS received_note_id, + transaction_id + FROM sapling_received_note_spends + UNION + SELECT + {orchard_pool_code} AS pool, + orchard_received_note_id AS received_note_id, + transaction_id + FROM orchard_received_note_spends;" + ) + })?; + + transaction.execute_batch({ + let transparent_pool_code = pool_code(PoolType::TRANSPARENT); + &format!( + "DROP VIEW v_transactions; + CREATE VIEW v_transactions AS + WITH + notes AS ( + -- Shielded notes received in this transaction + SELECT v_received_notes.account_id AS account_id, + transactions.block AS block, + transactions.txid AS txid, + v_received_notes.pool AS pool, + id_within_pool_table, + v_received_notes.value AS value, + CASE + WHEN v_received_notes.is_change THEN 1 + ELSE 0 + END AS is_change, + CASE + WHEN v_received_notes.is_change THEN 0 + ELSE 1 + END AS received_count, + CASE + WHEN (v_received_notes.memo IS NULL OR v_received_notes.memo = X'F6') + THEN 0 + ELSE 1 + END AS memo_present + FROM v_received_notes + JOIN transactions + ON transactions.id_tx = v_received_notes.tx + UNION + -- Transparent TXOs received in this transaction + SELECT utxos.received_by_account_id AS account_id, + utxos.height AS block, + utxos.prevout_txid AS txid, + {transparent_pool_code} AS pool, + utxos.id AS id_within_pool_table, + utxos.value_zat AS value, + 0 AS is_change, + 1 AS received_count, + 0 AS memo_present + FROM utxos + UNION + -- Shielded notes spent in this transaction + SELECT v_received_notes.account_id AS account_id, + transactions.block AS block, + transactions.txid AS txid, + v_received_notes.pool AS pool, + id_within_pool_table, + -v_received_notes.value AS value, + 0 AS is_change, + 0 AS received_count, + 0 AS memo_present + FROM v_received_notes + JOIN v_received_note_spends rns + ON rns.pool = v_received_notes.pool + AND rns.received_note_id = v_received_notes.id_within_pool_table + JOIN transactions + ON transactions.id_tx = rns.transaction_id + UNION + -- Transparent TXOs spent in this transaction + SELECT utxos.received_by_account_id AS account_id, + transactions.block AS block, + transactions.txid AS txid, + {transparent_pool_code} AS pool, + utxos.id AS id_within_pool_table, + -utxos.value_zat AS value, + 0 AS is_change, + 0 AS received_count, + 0 AS memo_present + FROM utxos + JOIN transparent_received_output_spends tros + ON tros.transparent_received_output_id = utxos.id + JOIN transactions + ON transactions.id_tx = tros.transaction_id + ), + -- Obtain a count of the notes that the wallet created in each transaction, + -- not counting change notes. + sent_note_counts AS ( + SELECT sent_notes.from_account_id AS account_id, + transactions.txid AS txid, + COUNT(DISTINCT sent_notes.id) as sent_notes, + SUM( + CASE + WHEN (sent_notes.memo IS NULL OR sent_notes.memo = X'F6' OR v_received_notes.tx IS NOT NULL) + THEN 0 + ELSE 1 + END + ) AS memo_count + FROM sent_notes + JOIN transactions + ON transactions.id_tx = sent_notes.tx + LEFT JOIN v_received_notes + ON sent_notes.id = v_received_notes.sent_note_id + WHERE COALESCE(v_received_notes.is_change, 0) = 0 + GROUP BY account_id, txid + ), + blocks_max_height AS ( + SELECT MAX(blocks.height) as max_height FROM blocks + ) + SELECT notes.account_id AS account_id, + notes.block AS mined_height, + notes.txid AS txid, + transactions.tx_index AS tx_index, + transactions.expiry_height AS expiry_height, + transactions.raw AS raw, + SUM(notes.value) AS account_balance_delta, + transactions.fee AS fee_paid, + SUM(notes.is_change) > 0 AS has_change, + MAX(COALESCE(sent_note_counts.sent_notes, 0)) AS sent_note_count, + SUM(notes.received_count) AS received_note_count, + SUM(notes.memo_present) + MAX(COALESCE(sent_note_counts.memo_count, 0)) AS memo_count, + blocks.time AS block_time, + ( + blocks.height IS NULL + AND transactions.expiry_height BETWEEN 1 AND blocks_max_height.max_height + ) AS expired_unmined + FROM notes + LEFT JOIN transactions + ON notes.txid = transactions.txid + JOIN blocks_max_height + LEFT JOIN blocks ON blocks.height = notes.block + LEFT JOIN sent_note_counts + ON sent_note_counts.account_id = notes.account_id + AND sent_note_counts.txid = notes.txid + GROUP BY notes.account_id, notes.txid;" + ) + })?; + + transaction.execute_batch({ + let transparent_pool_code = pool_code(PoolType::TRANSPARENT); + &format!( + "DROP VIEW v_tx_outputs; + CREATE VIEW v_tx_outputs AS + SELECT transactions.txid AS txid, + v_received_notes.pool AS output_pool, + v_received_notes.output_index AS output_index, + sent_notes.from_account_id AS from_account_id, + v_received_notes.account_id AS to_account_id, + NULL AS to_address, + v_received_notes.value AS value, + v_received_notes.is_change AS is_change, + v_received_notes.memo AS memo + FROM v_received_notes + JOIN transactions + ON transactions.id_tx = v_received_notes.tx + LEFT JOIN sent_notes + ON sent_notes.id = v_received_notes.sent_note_id + UNION + SELECT utxos.prevout_txid AS txid, + {transparent_pool_code} AS output_pool, + utxos.prevout_idx AS output_index, + NULL AS from_account_id, + utxos.received_by_account_id AS to_account_id, + utxos.address AS to_address, + utxos.value_zat AS value, + 0 AS is_change, + NULL AS memo + FROM utxos + UNION + SELECT transactions.txid AS txid, + sent_notes.output_pool AS output_pool, + sent_notes.output_index AS output_index, + sent_notes.from_account_id AS from_account_id, + v_received_notes.account_id AS to_account_id, + sent_notes.to_address AS to_address, + sent_notes.value AS value, + 0 AS is_change, + sent_notes.memo AS memo + FROM sent_notes + JOIN transactions + ON transactions.id_tx = sent_notes.tx + LEFT JOIN v_received_notes + ON sent_notes.id = v_received_notes.sent_note_id + WHERE COALESCE(v_received_notes.is_change, 0) = 0;" + ) + })?; + + Ok(()) + } + + fn down(&self, _transaction: &rusqlite::Transaction<'_>) -> Result<(), Self::Error> { + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) + } +} + +#[cfg(test)] +mod tests { + use crate::wallet::init::migrations::tests::test_migrate; + + #[test] + fn migrate() { + test_migrate(&[super::MIGRATION_ID]); + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/orchard_shardtree.rs b/zcash_client_sqlite/src/wallet/init/migrations/orchard_shardtree.rs new file mode 100644 index 0000000000..a655ffb12d --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/orchard_shardtree.rs @@ -0,0 +1,228 @@ +//! This migration adds tables to the wallet database that are needed to persist Orchard note +//! commitment tree data using the `shardtree` crate. + +use std::collections::HashSet; + +use rusqlite::{named_params, OptionalExtension}; +use schemerz_rusqlite::RusqliteMigration; +use tracing::debug; +use uuid::Uuid; +use zcash_client_backend::data_api::scanning::ScanPriority; +use zcash_protocol::consensus::{self, BlockHeight, NetworkUpgrade}; + +use super::shardtree_support; +use crate::wallet::{chain_tip_height, init::WalletMigrationError, scanning::priority_code}; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0x3a6487f7_e068_42bb_9d12_6bb8dbe6da00); + +const DEPENDENCIES: &[Uuid] = &[shardtree_support::MIGRATION_ID]; + +pub(super) struct Migration

{ + pub(super) params: P, +} + +impl

schemerz::Migration for Migration

{ + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + DEPENDENCIES.iter().copied().collect() + } + + fn description(&self) -> &'static str { + "Add support for storage of Orchard note commitment tree data using the `shardtree` crate." + } +} + +impl RusqliteMigration for Migration

{ + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + // Add shard persistence + debug!("Creating tables for Orchard shard persistence"); + transaction.execute_batch( + "CREATE TABLE orchard_tree_shards ( + shard_index INTEGER PRIMARY KEY, + subtree_end_height INTEGER, + root_hash BLOB, + shard_data BLOB, + contains_marked INTEGER, + CONSTRAINT root_unique UNIQUE (root_hash) + ); + CREATE TABLE orchard_tree_cap ( + -- cap_id exists only to be able to take advantage of `ON CONFLICT` + -- upsert functionality; the table will only ever contain one row + cap_id INTEGER PRIMARY KEY, + cap_data BLOB NOT NULL + );", + )?; + + // Add checkpoint persistence + debug!("Creating tables for checkpoint persistence"); + transaction.execute_batch( + "CREATE TABLE orchard_tree_checkpoints ( + checkpoint_id INTEGER PRIMARY KEY, + position INTEGER + ); + CREATE TABLE orchard_tree_checkpoint_marks_removed ( + checkpoint_id INTEGER NOT NULL, + mark_removed_position INTEGER NOT NULL, + FOREIGN KEY (checkpoint_id) REFERENCES orchard_tree_checkpoints(checkpoint_id) + ON DELETE CASCADE, + CONSTRAINT spend_position_unique UNIQUE (checkpoint_id, mark_removed_position) + );", + )?; + + transaction.execute_batch(&format!( + "CREATE VIEW v_orchard_shard_scan_ranges AS + SELECT + shard.shard_index, + shard.shard_index << {} AS start_position, + (shard.shard_index + 1) << {} AS end_position_exclusive, + IFNULL(prev_shard.subtree_end_height, {}) AS subtree_start_height, + shard.subtree_end_height, + shard.contains_marked, + scan_queue.block_range_start, + scan_queue.block_range_end, + scan_queue.priority + FROM orchard_tree_shards shard + LEFT OUTER JOIN orchard_tree_shards prev_shard + ON shard.shard_index = prev_shard.shard_index + 1 + -- Join with scan ranges that overlap with the subtree's involved blocks. + INNER JOIN scan_queue ON ( + subtree_start_height < scan_queue.block_range_end AND + ( + scan_queue.block_range_start <= shard.subtree_end_height OR + shard.subtree_end_height IS NULL + ) + )", + 16, // ORCHARD_SHARD_HEIGHT is only available when `feature = "orchard"` is enabled. + 16, // ORCHARD_SHARD_HEIGHT is only available when `feature = "orchard"` is enabled. + u32::from(self.params.activation_height(NetworkUpgrade::Nu5).unwrap()), + ))?; + + transaction.execute_batch(&format!( + "CREATE VIEW v_orchard_shard_unscanned_ranges AS + WITH wallet_birthday AS (SELECT MIN(birthday_height) AS height FROM accounts) + SELECT + shard_index, + start_position, + end_position_exclusive, + subtree_start_height, + subtree_end_height, + contains_marked, + block_range_start, + block_range_end, + priority + FROM v_orchard_shard_scan_ranges + INNER JOIN wallet_birthday + WHERE priority > {} + AND block_range_end > wallet_birthday.height;", + priority_code(&ScanPriority::Scanned), + ))?; + + transaction.execute_batch( + "CREATE VIEW v_orchard_shards_scan_state AS + SELECT + shard_index, + start_position, + end_position_exclusive, + subtree_start_height, + subtree_end_height, + contains_marked, + MAX(priority) AS max_priority + FROM v_orchard_shard_scan_ranges + GROUP BY + shard_index, + start_position, + end_position_exclusive, + subtree_start_height, + subtree_end_height, + contains_marked;", + )?; + + // Treat the current best-known chain tip height as the height to use for Orchard + // initialization, bounded below by NU5 activation. + if let Some(orchard_init_height) = chain_tip_height(transaction)?.and_then(|h| { + self.params + .activation_height(NetworkUpgrade::Nu5) + .map(|orchard_activation| std::cmp::max(orchard_activation, h)) + }) { + // If a scan range exists that contains the Orchard init height, split it in two at the + // init height. + if let Some((start, end, range_priority)) = transaction + .query_row_and_then( + "SELECT block_range_start, block_range_end, priority + FROM scan_queue + WHERE block_range_start <= :orchard_init_height + AND block_range_end > :orchard_init_height", + named_params![":orchard_init_height": u32::from(orchard_init_height)], + |row| { + let start = BlockHeight::from(row.get::<_, u32>(0)?); + let end = BlockHeight::from(row.get::<_, u32>(1)?); + let range_priority: i64 = row.get(2)?; + Ok((start, end, range_priority)) + }, + ) + .optional()? + { + transaction.execute( + "DELETE from scan_queue WHERE block_range_start = :start", + named_params![":start": u32::from(start)], + )?; + if start < orchard_init_height { + // Rewrite the start of the scan range to be exactly what it was prior to the + // change. + transaction.execute( + "INSERT INTO scan_queue (block_range_start, block_range_end, priority) + VALUES (:block_range_start, :block_range_end, :priority)", + named_params![ + ":block_range_start": u32::from(start), + ":block_range_end": u32::from(orchard_init_height), + ":priority": range_priority, + ], + )?; + } + // Rewrite the remainder of the range to have at least priority `Historic` + transaction.execute( + "INSERT INTO scan_queue (block_range_start, block_range_end, priority) + VALUES (:block_range_start, :block_range_end, :priority)", + named_params![ + ":block_range_start": u32::from(orchard_init_height), + ":block_range_end": u32::from(end), + ":priority": + std::cmp::max(range_priority, priority_code(&ScanPriority::Historic)), + ], + )?; + // Rewrite any scanned ranges above the end of the first Orchard + // range to have at least priority `Historic` + transaction.execute( + "UPDATE scan_queue SET priority = :historic + WHERE :block_range_start >= :orchard_initial_range_end + AND priority < :historic", + named_params![ + ":historic": priority_code(&ScanPriority::Historic), + ":orchard_initial_range_end": u32::from(end), + ], + )?; + } + } + + Ok(()) + } + + fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) + } +} + +#[cfg(test)] +mod tests { + use crate::wallet::init::migrations::tests::test_migrate; + + #[test] + fn migrate() { + test_migrate(&[super::MIGRATION_ID]); + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/received_notes_nullable_nf.rs b/zcash_client_sqlite/src/wallet/init/migrations/received_notes_nullable_nf.rs new file mode 100644 index 0000000000..f05bb57f4c --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/received_notes_nullable_nf.rs @@ -0,0 +1,374 @@ +//! A migration that renames the `received_notes` table to `sapling_received_notes` +//! and makes the `nf` column nullable. This allows change notes to be added to the +//! table prior to being mined. +use std::collections::HashSet; + +use schemerz_rusqlite::RusqliteMigration; +use uuid::Uuid; + +use super::v_transactions_net; +use crate::wallet::init::WalletMigrationError; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0xbdcdcedc_7b29_4f1c_8307_35f937f0d32a); + +const DEPENDENCIES: &[Uuid] = &[v_transactions_net::MIGRATION_ID]; + +pub(crate) struct Migration; + +impl schemerz::Migration for Migration { + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + DEPENDENCIES.iter().copied().collect() + } + + fn description(&self) -> &'static str { + "Rename `received_notes` to `sapling_received_notes` and make the `nf` column nullable." + } +} + +impl RusqliteMigration for Migration { + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + // As of this migration, the `received_notes` table only stores Sapling notes, so + // we can hard-code the `output_pool` value. + transaction.execute_batch( + "CREATE TABLE sapling_received_notes ( + id_note INTEGER PRIMARY KEY, + tx INTEGER NOT NULL, + output_index INTEGER NOT NULL, + account INTEGER NOT NULL, + diversifier BLOB NOT NULL, + value INTEGER NOT NULL, + rcm BLOB NOT NULL, + nf BLOB UNIQUE, + is_change INTEGER NOT NULL, + memo BLOB, + spent INTEGER, + FOREIGN KEY (tx) REFERENCES transactions(id_tx), + FOREIGN KEY (account) REFERENCES accounts(account), + FOREIGN KEY (spent) REFERENCES transactions(id_tx), + CONSTRAINT tx_output UNIQUE (tx, output_index) + ); + INSERT INTO sapling_received_notes SELECT * FROM received_notes;", + )?; + + transaction.execute_batch( + "ALTER TABLE sapling_witnesses RENAME TO sapling_witnesses_old; + CREATE TABLE sapling_witnesses ( + id_witness INTEGER PRIMARY KEY, + note INTEGER NOT NULL, + block INTEGER NOT NULL, + witness BLOB NOT NULL, + FOREIGN KEY (note) REFERENCES sapling_received_notes(id_note), + FOREIGN KEY (block) REFERENCES blocks(height), + CONSTRAINT witness_height UNIQUE (note, block) + ); + INSERT INTO sapling_witnesses SELECT * FROM sapling_witnesses_old; + DROP TABLE sapling_witnesses_old;", + )?; + + transaction.execute_batch( + "DROP VIEW v_tx_outputs; + CREATE VIEW v_tx_outputs AS + SELECT sapling_received_notes.tx AS id_tx, + 2 AS output_pool, + sapling_received_notes.output_index AS output_index, + sent_notes.from_account AS from_account, + sapling_received_notes.account AS to_account, + NULL AS to_address, + sapling_received_notes.value AS value, + sapling_received_notes.is_change AS is_change, + sapling_received_notes.memo AS memo + FROM sapling_received_notes + LEFT JOIN sent_notes + ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) = + (sapling_received_notes.tx, 2, sent_notes.output_index) + UNION + SELECT transactions.id_tx AS id_tx, + 0 AS output_pool, + utxos.prevout_idx AS output_index, + NULL AS from_account, + utxos.received_by_account AS to_account, + utxos.address AS to_address, + utxos.value_zat AS value, + false AS is_change, + NULL AS memo + FROM utxos + JOIN transactions + ON transactions.txid = utxos.prevout_txid + UNION + SELECT sent_notes.tx AS id_tx, + sent_notes.output_pool AS output_pool, + sent_notes.output_index AS output_index, + sent_notes.from_account AS from_account, + sapling_received_notes.account AS to_account, + sent_notes.to_address AS to_address, + sent_notes.value AS value, + false AS is_change, + sent_notes.memo AS memo + FROM sent_notes + LEFT JOIN sapling_received_notes + ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) = + (sapling_received_notes.tx, 2, sapling_received_notes.output_index) + WHERE sapling_received_notes.is_change IS NULL + OR sapling_received_notes.is_change = 0", + )?; + + transaction.execute_batch( + "DROP VIEW v_transactions; + CREATE VIEW v_transactions AS + WITH + notes AS ( + SELECT sapling_received_notes.account AS account_id, + sapling_received_notes.tx AS id_tx, + 2 AS pool, + sapling_received_notes.value AS value, + CASE + WHEN sapling_received_notes.is_change THEN 1 + ELSE 0 + END AS is_change, + CASE + WHEN sapling_received_notes.is_change THEN 0 + ELSE 1 + END AS received_count, + CASE + WHEN sapling_received_notes.memo IS NULL THEN 0 + ELSE 1 + END AS memo_present + FROM sapling_received_notes + UNION + SELECT utxos.received_by_account AS account_id, + transactions.id_tx AS id_tx, + 0 AS pool, + utxos.value_zat AS value, + 0 AS is_change, + 1 AS received_count, + 0 AS memo_present + FROM utxos + JOIN transactions + ON transactions.txid = utxos.prevout_txid + UNION + SELECT sapling_received_notes.account AS account_id, + sapling_received_notes.spent AS id_tx, + 2 AS pool, + -sapling_received_notes.value AS value, + 0 AS is_change, + 0 AS received_count, + 0 AS memo_present + FROM sapling_received_notes + WHERE sapling_received_notes.spent IS NOT NULL + ), + sent_note_counts AS ( + SELECT sent_notes.from_account AS account_id, + sent_notes.tx AS id_tx, + COUNT(DISTINCT sent_notes.id_note) as sent_notes, + SUM( + CASE + WHEN sent_notes.memo IS NULL THEN 0 + ELSE 1 + END + ) AS memo_count + FROM sent_notes + LEFT JOIN sapling_received_notes + ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) = + (sapling_received_notes.tx, 2, sapling_received_notes.output_index) + WHERE sapling_received_notes.is_change IS NULL + OR sapling_received_notes.is_change = 0 + GROUP BY account_id, id_tx + ), + blocks_max_height AS ( + SELECT MAX(blocks.height) as max_height FROM blocks + ) + SELECT notes.account_id AS account_id, + transactions.id_tx AS id_tx, + transactions.block AS mined_height, + transactions.tx_index AS tx_index, + transactions.txid AS txid, + transactions.expiry_height AS expiry_height, + transactions.raw AS raw, + SUM(notes.value) AS account_balance_delta, + transactions.fee AS fee_paid, + SUM(notes.is_change) > 0 AS has_change, + MAX(COALESCE(sent_note_counts.sent_notes, 0)) AS sent_note_count, + SUM(notes.received_count) AS received_note_count, + SUM(notes.memo_present) + MAX(COALESCE(sent_note_counts.memo_count, 0)) AS memo_count, + blocks.time AS block_time, + ( + blocks.height IS NULL + AND transactions.expiry_height <= blocks_max_height.max_height + ) AS expired_unmined + FROM transactions + JOIN notes ON notes.id_tx = transactions.id_tx + JOIN blocks_max_height + LEFT JOIN blocks ON blocks.height = transactions.block + LEFT JOIN sent_note_counts + ON sent_note_counts.account_id = notes.account_id + AND sent_note_counts.id_tx = notes.id_tx + GROUP BY notes.account_id, transactions.id_tx", + )?; + + transaction.execute_batch("DROP TABLE received_notes;")?; + + Ok(()) + } + + fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) + } +} + +#[cfg(test)] +mod tests { + use rusqlite::{self, params}; + use tempfile::NamedTempFile; + + use zcash_keys::keys::UnifiedSpendingKey; + use zcash_protocol::consensus::Network; + use zip32::AccountId; + + use crate::{ + wallet::init::{init_wallet_db_internal, migrations::v_transactions_net}, + WalletDb, + }; + + #[test] + fn received_notes_nullable_migration() { + let data_file = NamedTempFile::new().unwrap(); + let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap(); + init_wallet_db_internal( + &mut db_data, + None, + &[v_transactions_net::MIGRATION_ID], + false, + ) + .unwrap(); + + // Create an account in the wallet + let usk0 = UnifiedSpendingKey::from_seed(&db_data.params, &[0u8; 32][..], AccountId::ZERO) + .unwrap(); + let ufvk0 = usk0.to_unified_full_viewing_key(); + db_data + .conn + .execute( + "INSERT INTO accounts (account, ufvk) VALUES (0, ?)", + params![ufvk0.encode(&db_data.params)], + ) + .unwrap(); + + // Tx 0 contains two received notes of 2 and 5 zatoshis that are controlled by account 0. + db_data.conn.execute_batch( + "INSERT INTO blocks (height, hash, time, sapling_tree) VALUES (0, 0, 0, x'00'); + INSERT INTO transactions (block, id_tx, txid) VALUES (0, 0, 'tx0'); + + INSERT INTO received_notes (tx, output_index, account, diversifier, value, rcm, nf, is_change) + VALUES (0, 0, 0, '', 2, '', 'nf_a', false); + INSERT INTO received_notes (tx, output_index, account, diversifier, value, rcm, nf, is_change) + VALUES (0, 3, 0, '', 5, '', 'nf_b', false);").unwrap(); + + // Apply the current migration + init_wallet_db_internal(&mut db_data, None, &[super::MIGRATION_ID], false).unwrap(); + + { + let mut q = db_data + .conn + .prepare( + "SELECT account_id, id_tx, account_balance_delta, has_change, memo_count, sent_note_count, received_note_count + FROM v_transactions", + ) + .unwrap(); + let mut rows = q.query([]).unwrap(); + let mut row_count = 0; + while let Some(row) = rows.next().unwrap() { + row_count += 1; + let account: i64 = row.get(0).unwrap(); + let tx: i64 = row.get(1).unwrap(); + let account_balance_delta: i64 = row.get(2).unwrap(); + let has_change: bool = row.get(3).unwrap(); + let memo_count: i64 = row.get(4).unwrap(); + let sent_note_count: i64 = row.get(5).unwrap(); + let received_note_count: i64 = row.get(6).unwrap(); + match (account, tx) { + (0, 0) => { + assert_eq!(account_balance_delta, 7); + assert!(!has_change); + assert_eq!(memo_count, 0); + assert_eq!(sent_note_count, 0); + assert_eq!(received_note_count, 2); + } + other => { + panic!("(Account, Transaction) pair {:?} is not expected to exist in the wallet.", other); + } + } + } + assert_eq!(row_count, 1); + } + + // Now create an unmined transaction that spends both of our notes + db_data.conn.execute_batch( + "INSERT INTO transactions (id_tx, txid) VALUES (1, 'tx1'); + + -- Mark our existing notes as spent + UPDATE sapling_received_notes SET spent = 1 WHERE tx = 0; + + -- The note sent to the external recipient. + INSERT INTO sent_notes (tx, output_pool, output_index, from_account, to_account, to_address, value) + VALUES (1, 2, 0, 0, NULL, 'zfake', 4); + + -- The change notes. We send two notes to ensure that having multiple rows with NULL nullifiers + -- does not violate the uniqueness constraint + INSERT INTO sent_notes (tx, output_pool, output_index, from_account, to_account, to_address, value) + VALUES (1, 2, 1, 0, 0, NULL, 1); + INSERT INTO sent_notes (tx, output_pool, output_index, from_account, to_account, to_address, value) + VALUES (1, 2, 2, 0, 0, NULL, 1); + INSERT INTO sapling_received_notes (tx, output_index, account, diversifier, value, rcm, nf, is_change) + VALUES (1, 1, 0, '', 1, '', NULL, true); + INSERT INTO sapling_received_notes (tx, output_index, account, diversifier, value, rcm, nf, is_change) + VALUES (1, 2, 0, '', 1, '', NULL, true); + ").unwrap(); + { + let mut q = db_data + .conn + .prepare( + "SELECT account_id, id_tx, account_balance_delta, has_change, memo_count, sent_note_count, received_note_count + FROM v_transactions", + ) + .unwrap(); + let mut rows = q.query([]).unwrap(); + let mut row_count = 0; + while let Some(row) = rows.next().unwrap() { + row_count += 1; + let account: i64 = row.get(0).unwrap(); + let tx: i64 = row.get(1).unwrap(); + let account_balance_delta: i64 = row.get(2).unwrap(); + let has_change: bool = row.get(3).unwrap(); + let memo_count: i64 = row.get(4).unwrap(); + let sent_note_count: i64 = row.get(5).unwrap(); + let received_note_count: i64 = row.get(6).unwrap(); + match (account, tx) { + (0, 0) => { + assert_eq!(account_balance_delta, 7); + assert!(!has_change); + assert_eq!(memo_count, 0); + assert_eq!(sent_note_count, 0); + assert_eq!(received_note_count, 2); + } + (0, 1) => { + assert_eq!(account_balance_delta, -6); + assert!(has_change); + assert_eq!(memo_count, 0); + assert_eq!(sent_note_count, 1); + assert_eq!(received_note_count, 0); + } + other => { + panic!("(Account, Transaction) pair {:?} is not expected to exist in the wallet.", other); + } + } + } + assert_eq!(row_count, 2); + } + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/receiving_key_scopes.rs b/zcash_client_sqlite/src/wallet/init/migrations/receiving_key_scopes.rs new file mode 100644 index 0000000000..444e322c67 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/receiving_key_scopes.rs @@ -0,0 +1,913 @@ +//! This migration adds decryption key scope to persisted information about received notes. + +use std::collections::HashSet; + +use group::ff::PrimeField; +use incrementalmerkletree::Position; +use rusqlite::named_params; +use schemerz_rusqlite::RusqliteMigration; + +use shardtree::{store::ShardStore, ShardTree}; +use uuid::Uuid; + +use sapling::{ + note_encryption::{try_sapling_note_decryption, PreparedIncomingViewingKey, Zip212Enforcement}, + zip32::DiversifiableFullViewingKey, + Diversifier, Node, Rseed, +}; +use zcash_client_backend::data_api::SAPLING_SHARD_HEIGHT; +use zcash_keys::keys::UnifiedFullViewingKey; +use zcash_primitives::transaction::{components::sapling::zip212_enforcement, Transaction}; +use zcash_protocol::{ + consensus::{self, BlockHeight, BranchId}, + value::Zatoshis, +}; +use zip32::Scope; + +use crate::{ + wallet::{ + chain_tip_height, + commitment_tree::SqliteShardStore, + init::{migrations::shardtree_support, WalletMigrationError}, + scope_code, + }, + PRUNING_DEPTH, SAPLING_TABLES_PREFIX, +}; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0xee89ed2b_c1c2_421e_9e98_c1e3e54a7fc2); + +const DEPENDENCIES: &[Uuid] = &[shardtree_support::MIGRATION_ID]; + +pub(super) struct Migration

{ + pub(super) params: P, +} + +impl

schemerz::Migration for Migration

{ + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + DEPENDENCIES.iter().copied().collect() + } + + fn description(&self) -> &'static str { + "Add decryption key scope to persisted information about received notes." + } +} + +#[allow(clippy::type_complexity)] +fn select_note_scope>( + commitment_tree: &mut ShardTree< + S, + { sapling::NOTE_COMMITMENT_TREE_DEPTH }, + SAPLING_SHARD_HEIGHT, + >, + dfvk: &DiversifiableFullViewingKey, + diversifier: &sapling::Diversifier, + value: &sapling::value::NoteValue, + rseed: &sapling::Rseed, + note_commitment_tree_position: Position, +) -> Result, WalletMigrationError> { + // Attempt to reconstruct the note being spent using both the internal and external dfvks + // corresponding to the unified spending key, checking against the witness we are using + // to spend the note that we've used the correct key. + let external_note = dfvk + .diversified_address(*diversifier) + .map(|addr| addr.create_note(*value, *rseed)); + let internal_note = dfvk + .diversified_change_address(*diversifier) + .map(|addr| addr.create_note(*value, *rseed)); + + if let Some(recorded_node) = commitment_tree + .get_marked_leaf(note_commitment_tree_position) + .map_err(|e| { + WalletMigrationError::CorruptedData(format!( + "Error querying note commitment tree: {:?}", + e + )) + })? + { + if external_note.map(|n| Node::from_cmu(&n.cmu())) == Some(recorded_node) { + Ok(Some(Scope::External)) + } else if internal_note.map(|n| Node::from_cmu(&n.cmu())) == Some(recorded_node) { + Ok(Some(Scope::Internal)) + } else { + Err(WalletMigrationError::CorruptedData( + "Unable to reconstruct note.".to_owned(), + )) + } + } else { + Ok(None) + } +} + +impl RusqliteMigration for Migration

{ + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + transaction.execute_batch( + &format!( + "ALTER TABLE sapling_received_notes ADD COLUMN recipient_key_scope INTEGER NOT NULL DEFAULT {};", + scope_code(Scope::External) + ) + )?; + + // For all notes we have to determine whether they were actually sent to the internal key + // or the external key for the account, so we trial-decrypt the original output with the + // internal IVK and update the persisted scope value if necessary. We check all notes, + // rather than just change notes, because shielding notes may not have been considered + // change. + let mut stmt_select_notes = transaction.prepare( + "SELECT + id_note, + output_index, + transactions.raw, + transactions.block, + transactions.expiry_height, + accounts.ufvk, + diversifier, + value, + rcm, + commitment_tree_position + FROM sapling_received_notes + INNER JOIN accounts on accounts.account = sapling_received_notes.account + INNER JOIN transactions ON transactions.id_tx = sapling_received_notes.tx", + )?; + + // In the case that we don't have the raw transaction + let mut commitment_tree = ShardTree::new( + SqliteShardStore::<_, _, SAPLING_SHARD_HEIGHT>::from_connection( + transaction, + SAPLING_TABLES_PREFIX, + )?, + PRUNING_DEPTH as usize, + ); + + let mut rows = stmt_select_notes.query([])?; + while let Some(row) = rows.next()? { + let note_id: i64 = row.get(0)?; + let output_index: usize = row.get(1)?; + let tx_data_opt: Option> = row.get(2)?; + + let tx_height = row.get::<_, Option>(3)?.map(BlockHeight::from); + let tx_expiry = row.get::<_, Option>(4)?; + let zip212_height = tx_height.map_or_else( + || { + tx_expiry.filter(|h| *h != 0).map_or_else( + || chain_tip_height(transaction), + |h| Ok(Some(BlockHeight::from(h))), + ) + }, + |h| Ok(Some(h)), + )?; + + let zip212_enforcement = zip212_height.map_or_else( + || { + // If the transaction has not been mined and the expiry height is set to 0 (no + // expiry) an no chain tip information is available, then we assume it can only + // be mined under ZIP 212 enforcement rules, so we default to `On` + Zip212Enforcement::On + }, + |h| zip212_enforcement(&self.params, h), + ); + + let ufvk_str: String = row.get(5)?; + let ufvk = UnifiedFullViewingKey::decode(&self.params, &ufvk_str).map_err(|e| { + WalletMigrationError::CorruptedData(format!("Stored UFVK was invalid: {:?}", e)) + })?; + + let dfvk = ufvk.sapling().ok_or_else(|| { + WalletMigrationError::CorruptedData( + "UFVK must have a Sapling component to have received Sapling notes.".to_owned(), + ) + })?; + + // We previously set the default to external scope, so we now verify whether the output + // is decryptable using the intenally-scoped IVK and, if so, mark it as such. + if let Some(tx_data) = tx_data_opt { + let tx = Transaction::read(&tx_data[..], BranchId::Canopy).map_err(|e| { + WalletMigrationError::CorruptedData(format!( + "Unable to parse raw transaction: {:?}", + e + )) + })?; + let output = tx + .sapling_bundle() + .and_then(|b| b.shielded_outputs().get(output_index)) + .unwrap_or_else(|| { + panic!("A Sapling output must exist at index {}", output_index) + }); + + let pivk = PreparedIncomingViewingKey::new(&dfvk.to_ivk(Scope::Internal)); + if try_sapling_note_decryption(&pivk, output, zip212_enforcement).is_some() { + transaction.execute( + "UPDATE sapling_received_notes SET recipient_key_scope = :scope + WHERE id_note = :note_id", + named_params! {":scope": scope_code(Scope::Internal), ":note_id": note_id}, + )?; + } + } else { + let diversifier = { + let d: Vec<_> = row.get(6)?; + Diversifier(d[..].try_into().map_err(|_| { + WalletMigrationError::CorruptedData( + "Invalid diversifier length".to_string(), + ) + })?) + }; + + let note_value = Zatoshis::from_nonnegative_i64(row.get(7)?).map_err(|_e| { + WalletMigrationError::CorruptedData( + "Note values must be nonnegative".to_string(), + ) + })?; + + let rseed = { + let rcm_bytes: [u8; 32] = + row.get::<_, Vec>(8)?[..].try_into().map_err(|_| { + WalletMigrationError::CorruptedData(format!( + "Note {} is invalid", + note_id + )) + })?; + + let rcm = Option::from(jubjub::Fr::from_repr(rcm_bytes)).ok_or_else(|| { + WalletMigrationError::CorruptedData(format!("Note {} is invalid", note_id)) + })?; + + // The wallet database always stores the `rcm` value, and not `rseed`, + // so for note reconstruction we always use `BeforeZip212`. + Rseed::BeforeZip212(rcm) + }; + + let note_commitment_tree_position = + Position::from(u64::try_from(row.get::<_, i64>(9)?).map_err(|_| { + WalletMigrationError::CorruptedData( + "Note commitment tree position invalid.".to_string(), + ) + })?); + + let scope = select_note_scope( + &mut commitment_tree, + dfvk, + &diversifier, + &sapling::value::NoteValue::from_raw(note_value.into_u64()), + &rseed, + note_commitment_tree_position, + )?; + + if scope == Some(Scope::Internal) { + transaction.execute( + "UPDATE sapling_received_notes SET recipient_key_scope = :scope + WHERE id_note = :note_id", + named_params! {":scope": scope_code(Scope::Internal), ":note_id": note_id}, + )?; + } + } + } + + Ok(()) + } + + fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) + } +} + +#[cfg(feature = "transparent-inputs")] +#[cfg(test)] +mod tests { + use std::convert::Infallible; + + use incrementalmerkletree::Position; + use maybe_rayon::{ + iter::{IndexedParallelIterator, ParallelIterator}, + slice::ParallelSliceMut, + }; + use rand_core::OsRng; + use rusqlite::{named_params, params, Connection, OptionalExtension}; + use tempfile::NamedTempFile; + + use ::transparent::{ + builder::TransparentSigningSet, + bundle as transparent, + keys::{IncomingViewingKey, NonHardenedChildIndex}, + }; + use zcash_client_backend::{ + data_api::{BlockMetadata, WalletCommitmentTrees, SAPLING_SHARD_HEIGHT}, + decrypt_transaction, + proto::compact_formats::{CompactBlock, CompactTx}, + scanning::{scan_block, Nullifiers, ScanningKeys}, + wallet::WalletTx, + TransferType, + }; + use zcash_keys::keys::{UnifiedFullViewingKey, UnifiedSpendingKey}; + use zcash_primitives::{ + block::BlockHash, + transaction::{ + builder::{BuildConfig, BuildResult, Builder}, + fees::fixed, + Transaction, + }, + }; + use zcash_proofs::prover::LocalTxProver; + use zcash_protocol::{ + consensus::{BlockHeight, Network, NetworkUpgrade, Parameters}, + memo::MemoBytes, + value::Zatoshis, + }; + use zip32::Scope; + + use crate::{ + error::SqliteClientError, + wallet::{ + init::{ + init_wallet_db_internal, + migrations::{add_account_birthdays, shardtree_support, wallet_summaries}, + }, + memo_repr, parse_scope, + sapling::ReceivedSaplingOutput, + }, + AccountRef, TxRef, WalletDb, + }; + + // These must be different. + const EXTERNAL_VALUE: u64 = 10; + const INTERNAL_VALUE: u64 = 5; + + fn prepare_wallet_state( + db_data: &mut WalletDb, + ) -> (UnifiedFullViewingKey, BlockHeight, BuildResult) { + // Create an account in the wallet + let usk0 = + UnifiedSpendingKey::from_seed(&db_data.params, &[0u8; 32][..], zip32::AccountId::ZERO) + .unwrap(); + let ufvk0 = usk0.to_unified_full_viewing_key(); + let height = db_data + .params + .activation_height(NetworkUpgrade::Sapling) + .unwrap(); + db_data + .conn + .execute( + "INSERT INTO accounts (account, ufvk, birthday_height) VALUES (0, ?, ?)", + params![ufvk0.encode(&db_data.params), u32::from(height)], + ) + .unwrap(); + let sapling_dfvk = ufvk0.sapling().unwrap(); + let ovk = sapling_dfvk.to_ovk(Scope::External); + let (_, external_addr) = sapling_dfvk.default_address(); + let (_, internal_addr) = sapling_dfvk.change_address(); + + // Create a shielding transaction that has an external note and an internal note. + let mut builder = Builder::new( + db_data.params.clone(), + height, + BuildConfig::Standard { + sapling_anchor: Some(sapling::Anchor::empty_tree()), + orchard_anchor: None, + }, + ); + let mut transparent_signing_set = TransparentSigningSet::new(); + builder + .add_transparent_input( + transparent_signing_set.add_key( + usk0.transparent() + .derive_external_secret_key(NonHardenedChildIndex::ZERO) + .unwrap(), + ), + transparent::OutPoint::fake(), + transparent::TxOut { + value: Zatoshis::const_from_u64(EXTERNAL_VALUE + INTERNAL_VALUE), + script_pubkey: usk0 + .transparent() + .to_account_pubkey() + .derive_external_ivk() + .unwrap() + .default_address() + .0 + .script(), + }, + ) + .unwrap(); + builder + .add_sapling_output::( + Some(ovk), + external_addr, + Zatoshis::const_from_u64(EXTERNAL_VALUE), + MemoBytes::empty(), + ) + .unwrap(); + builder + .add_sapling_output::( + Some(ovk), + internal_addr, + Zatoshis::const_from_u64(INTERNAL_VALUE), + MemoBytes::empty(), + ) + .unwrap(); + let prover = LocalTxProver::bundled(); + let res = builder + .build( + &transparent_signing_set, + &[], + &[], + OsRng, + &prover, + &prover, + #[allow(deprecated)] + &fixed::FeeRule::non_standard(Zatoshis::ZERO), + ) + .unwrap(); + + (ufvk0, height, res) + } + + fn put_received_note_before_migration>( + conn: &Connection, + output: &T, + tx_ref: i64, + spent_in: Option, + ) -> Result<(), SqliteClientError> { + let mut stmt_upsert_received_note = conn.prepare_cached( + "INSERT INTO sapling_received_notes + (tx, output_index, account, diversifier, value, rcm, memo, nf, + is_change, spent, commitment_tree_position) + VALUES ( + :tx, + :output_index, + :account, + :diversifier, + :value, + :rcm, + :memo, + :nf, + :is_change, + :spent, + :commitment_tree_position + ) + ON CONFLICT (tx, output_index) DO UPDATE + SET account = :account, + diversifier = :diversifier, + value = :value, + rcm = :rcm, + nf = IFNULL(:nf, nf), + memo = IFNULL(:memo, memo), + is_change = IFNULL(:is_change, is_change), + spent = IFNULL(:spent, spent), + commitment_tree_position = IFNULL(:commitment_tree_position, commitment_tree_position)", + )?; + + let rcm = output.note().rcm().to_bytes(); + let to = output.note().recipient(); + let diversifier = to.diversifier(); + + let account = output.account_id(); + let sql_args = named_params![ + ":tx": &tx_ref, + ":output_index": i64::try_from(output.index()).expect("output indices are representable as i64"), + ":account": account.0, + ":diversifier": &diversifier.0, + ":value": output.note().value().inner(), + ":rcm": &rcm, + ":nf": output.nullifier().map(|nf| nf.0), + ":memo": memo_repr(output.memo()), + ":is_change": output.is_change(), + ":spent": spent_in, + ":commitment_tree_position": output.note_commitment_tree_position().map(u64::from), + ]; + + stmt_upsert_received_note + .execute(sql_args) + .map_err(SqliteClientError::from)?; + + Ok(()) + } + + /// This reproduces [`crate::wallet::put_tx_data`] as it was at the time + /// of the creation of this migration. + fn put_tx_data( + conn: &rusqlite::Connection, + tx: &Transaction, + fee: Option, + created_at: Option, + ) -> Result { + let mut stmt_upsert_tx_data = conn.prepare_cached( + "INSERT INTO transactions (txid, created, expiry_height, raw, fee) + VALUES (:txid, :created_at, :expiry_height, :raw, :fee) + ON CONFLICT (txid) DO UPDATE + SET expiry_height = :expiry_height, + raw = :raw, + fee = IFNULL(:fee, fee) + RETURNING id_tx", + )?; + + let txid = tx.txid(); + let mut raw_tx = vec![]; + tx.write(&mut raw_tx)?; + + let tx_params = named_params![ + ":txid": &txid.as_ref()[..], + ":created_at": created_at, + ":expiry_height": u32::from(tx.expiry_height()), + ":raw": raw_tx, + ":fee": fee.map(u64::from), + ]; + + stmt_upsert_tx_data + .query_row(tx_params, |row| row.get::<_, i64>(0).map(TxRef)) + .map_err(SqliteClientError::from) + } + + #[test] + fn receiving_key_scopes_migration_enhanced() { + let params = Network::TestNetwork; + + // Create wallet upgraded to just before the current migration. + let data_file = NamedTempFile::new().unwrap(); + let mut db_data = WalletDb::for_path(data_file.path(), params).unwrap(); + init_wallet_db_internal( + &mut db_data, + None, + &[ + add_account_birthdays::MIGRATION_ID, + shardtree_support::MIGRATION_ID, + ], + false, + ) + .unwrap(); + + let (ufvk0, height, res) = prepare_wallet_state(&mut db_data); + let tx = res.transaction(); + let account_id = AccountRef(0); + + // We can't use `decrypt_and_store_transaction` because we haven't migrated yet. + // Replicate its relevant innards here. + let d_tx = decrypt_transaction( + ¶ms, + height, + tx, + &[(account_id, ufvk0)].into_iter().collect(), + ); + + db_data + .transactionally::<_, _, rusqlite::Error>(|wdb| { + let tx_ref = put_tx_data(wdb.conn.0, d_tx.tx(), None, None).unwrap(); + + let mut spending_account_id: Option = None; + + // Orchard outputs were not supported as of the wallet states that could require this + // migration. + for output in d_tx.sapling_outputs() { + match output.transfer_type() { + TransferType::Outgoing | TransferType::WalletInternal => { + // Don't need to bother with sent outputs for this test. + if output.transfer_type() != TransferType::Outgoing { + put_received_note_before_migration( + wdb.conn.0, output, tx_ref.0, None, + ) + .unwrap(); + } + } + TransferType::Incoming => { + match spending_account_id { + Some(id) => assert_eq!(id, *output.account()), + None => { + spending_account_id = Some(*output.account()); + } + } + + put_received_note_before_migration(wdb.conn.0, output, tx_ref.0, None) + .unwrap(); + } + } + } + + Ok(()) + }) + .unwrap(); + + // Apply the current migration + init_wallet_db_internal(&mut db_data, None, &[super::MIGRATION_ID], false).unwrap(); + + // There should be two rows in the `sapling_received_notes` table with correct scopes. + let mut q = db_data + .conn + .prepare( + "SELECT value, recipient_key_scope + FROM sapling_received_notes", + ) + .unwrap(); + let mut rows = q.query([]).unwrap(); + let mut row_count = 0; + while let Some(row) = rows.next().unwrap() { + row_count += 1; + let value: u64 = row.get(0).unwrap(); + let scope = parse_scope(row.get(1).unwrap()); + match value { + EXTERNAL_VALUE => assert_eq!(scope, Some(Scope::External)), + INTERNAL_VALUE => assert_eq!(scope, Some(Scope::Internal)), + _ => { + panic!( + "(Value, Scope) pair {:?} is not expected to exist in the wallet.", + (value, scope), + ); + } + } + } + assert_eq!(row_count, 2); + } + + #[test] + fn receiving_key_scopes_migration_non_enhanced() { + let params = Network::TestNetwork; + + // Create wallet upgraded to just before the current migration. + let data_file = NamedTempFile::new().unwrap(); + let mut db_data = WalletDb::for_path(data_file.path(), params).unwrap(); + init_wallet_db_internal( + &mut db_data, + None, + &[ + wallet_summaries::MIGRATION_ID, + shardtree_support::MIGRATION_ID, + ], + false, + ) + .unwrap(); + + let (ufvk0, height, res) = prepare_wallet_state(&mut db_data); + let tx = res.transaction(); + + let mut compact_tx = CompactTx { + hash: tx.txid().as_ref()[..].into(), + ..Default::default() + }; + for output in tx.sapling_bundle().unwrap().shielded_outputs() { + compact_tx.outputs.push(output.into()); + } + let prev_hash = BlockHash([4; 32]); + let mut block = CompactBlock { + height: height.into(), + hash: vec![7; 32], + prev_hash: prev_hash.0[..].into(), + ..Default::default() + }; + block.vtx.push(compact_tx); + let scanning_keys = ScanningKeys::from_account_ufvks([(AccountRef(0), ufvk0)]); + + let scanned_block = scan_block( + ¶ms, + block, + &scanning_keys, + &Nullifiers::empty(), + Some(&BlockMetadata::from_parts( + height - 1, + prev_hash, + Some(0), + #[cfg(feature = "orchard")] + Some(0), + )), + ) + .unwrap(); + + // We can't use `put_blocks` because we haven't migrated yet. + // Replicate its relevant innards here. + let blocks = [scanned_block]; + db_data + .transactionally(|wdb| { + let start_positions = blocks.first().map(|block| { + ( + block.height(), + Position::from( + u64::from(block.sapling().final_tree_size()) + - u64::try_from(block.sapling().commitments().len()).unwrap(), + ), + ) + }); + let mut sapling_commitments = vec![]; + let mut last_scanned_height = None; + let mut note_positions = vec![]; + for block in blocks.into_iter() { + if last_scanned_height + .iter() + .any(|prev| block.height() != *prev + 1) + { + return Err(SqliteClientError::NonSequentialBlocks); + } + + // Insert the block into the database. + put_block( + wdb.conn.0, + block.height(), + block.block_hash(), + block.block_time(), + block.sapling().final_tree_size(), + block.sapling().commitments().len().try_into().unwrap(), + #[cfg(feature = "orchard")] + block.orchard().final_tree_size(), + #[cfg(feature = "orchard")] + block.orchard().commitments().len().try_into().unwrap(), + )?; + + for tx in block.transactions() { + let tx_row = put_tx_meta(wdb.conn.0, tx, block.height())?; + + for output in tx.sapling_outputs() { + put_received_note_before_migration(wdb.conn.0, output, tx_row, None)?; + } + } + + note_positions.extend(block.transactions().iter().flat_map(|wtx| { + wtx.sapling_outputs() + .iter() + .map(|out| out.note_commitment_tree_position()) + })); + + last_scanned_height = Some(block.height()); + let block_commitments = block.into_commitments(); + sapling_commitments.extend(block_commitments.sapling.into_iter().map(Some)); + } + + // We will have a start position and a last scanned height in all cases where + // `blocks` is non-empty. + if let Some(((_, start_position), _)) = start_positions.zip(last_scanned_height) { + // Create subtrees from the note commitments in parallel. + const CHUNK_SIZE: usize = 1024; + let subtrees = sapling_commitments + .par_chunks_mut(CHUNK_SIZE) + .enumerate() + .filter_map(|(i, chunk)| { + let start = start_position + (i * CHUNK_SIZE) as u64; + let end = start + chunk.len() as u64; + + shardtree::LocatedTree::from_iter( + start..end, + SAPLING_SHARD_HEIGHT.into(), + chunk.iter_mut().map(|n| n.take().expect("always Some")), + ) + }) + .map(|res| (res.subtree, res.checkpoints)) + .collect::>(); + + // Update the Sapling note commitment tree with all newly read note commitments + let mut subtrees = subtrees.into_iter(); + wdb.with_sapling_tree_mut::<_, _, SqliteClientError>(move |sapling_tree| { + for (tree, checkpoints) in &mut subtrees { + sapling_tree.insert_tree(tree, checkpoints)?; + } + + Ok(()) + })?; + } + + Ok(()) + }) + .unwrap(); + + // Apply the current migration + init_wallet_db_internal(&mut db_data, None, &[super::MIGRATION_ID], false).unwrap(); + + // There should be two rows in the `sapling_received_notes` table with correct scopes. + let mut q = db_data + .conn + .prepare( + "SELECT value, recipient_key_scope + FROM sapling_received_notes", + ) + .unwrap(); + let mut rows = q.query([]).unwrap(); + let mut row_count = 0; + while let Some(row) = rows.next().unwrap() { + row_count += 1; + let value: u64 = row.get(0).unwrap(); + let scope = parse_scope(row.get(1).unwrap()); + match value { + EXTERNAL_VALUE => assert_eq!(scope, Some(Scope::External)), + INTERNAL_VALUE => assert_eq!(scope, Some(Scope::Internal)), + _ => { + panic!( + "(Value, Scope) pair {:?} is not expected to exist in the wallet.", + (value, scope), + ); + } + } + } + assert_eq!(row_count, 2); + } + + /// This is a copy of [`crate::wallet::put_block`] as of the expected database + /// state corresponding to this migration. It is duplicated here as later + /// updates to the database schema require incompatible changes to `put_block`. + #[allow(clippy::too_many_arguments)] + fn put_block( + conn: &rusqlite::Transaction<'_>, + block_height: BlockHeight, + block_hash: BlockHash, + block_time: u32, + sapling_commitment_tree_size: u32, + sapling_output_count: u32, + #[cfg(feature = "orchard")] orchard_commitment_tree_size: u32, + #[cfg(feature = "orchard")] orchard_action_count: u32, + ) -> Result<(), SqliteClientError> { + let block_hash_data = conn + .query_row( + "SELECT hash FROM blocks WHERE height = ?", + [u32::from(block_height)], + |row| row.get::<_, Vec>(0), + ) + .optional()?; + + // Ensure that in the case of an upsert, we don't overwrite block data + // with information for a block with a different hash. + if let Some(bytes) = block_hash_data { + let expected_hash = BlockHash::try_from_slice(&bytes).ok_or_else(|| { + SqliteClientError::CorruptedData(format!( + "Invalid block hash at height {}", + u32::from(block_height) + )) + })?; + if expected_hash != block_hash { + return Err(SqliteClientError::BlockConflict(block_height)); + } + } + + let mut stmt_upsert_block = conn.prepare_cached( + "INSERT INTO blocks ( + height, + hash, + time, + sapling_commitment_tree_size, + sapling_output_count, + sapling_tree, + orchard_commitment_tree_size, + orchard_action_count + ) + VALUES ( + :height, + :hash, + :block_time, + :sapling_commitment_tree_size, + :sapling_output_count, + x'00', + :orchard_commitment_tree_size, + :orchard_action_count + ) + ON CONFLICT (height) DO UPDATE + SET hash = :hash, + time = :block_time, + sapling_commitment_tree_size = :sapling_commitment_tree_size, + sapling_output_count = :sapling_output_count, + orchard_commitment_tree_size = :orchard_commitment_tree_size, + orchard_action_count = :orchard_action_count", + )?; + + #[cfg(not(feature = "orchard"))] + let orchard_commitment_tree_size: Option = None; + #[cfg(not(feature = "orchard"))] + let orchard_action_count: Option = None; + + stmt_upsert_block.execute(named_params![ + ":height": u32::from(block_height), + ":hash": &block_hash.0[..], + ":block_time": block_time, + ":sapling_commitment_tree_size": sapling_commitment_tree_size, + ":sapling_output_count": sapling_output_count, + ":orchard_commitment_tree_size": orchard_commitment_tree_size, + ":orchard_action_count": orchard_action_count, + ])?; + + Ok(()) + } + + /// This is a copy of [`crate::wallet::put_tx_meta`] as of the expected database + /// state corresponding to this migration. It is duplicated here as later + /// updates to the database schema require incompatible changes to `put_tx_meta`. + pub(crate) fn put_tx_meta( + conn: &rusqlite::Connection, + tx: &WalletTx, + height: BlockHeight, + ) -> Result { + // It isn't there, so insert our transaction into the database. + let mut stmt_upsert_tx_meta = conn.prepare_cached( + "INSERT INTO transactions (txid, block, tx_index) + VALUES (:txid, :block, :tx_index) + ON CONFLICT (txid) DO UPDATE + SET block = :block, + tx_index = :tx_index + RETURNING id_tx", + )?; + + let txid_bytes = tx.txid(); + let tx_params = named_params![ + ":txid": &txid_bytes.as_ref()[..], + ":block": u32::from(height), + ":tx_index": i64::try_from(tx.block_index()).expect("transaction indices are representable as i64"), + ]; + + stmt_upsert_tx_meta + .query_row(tx_params, |row| row.get::<_, i64>(0)) + .map_err(SqliteClientError::from) + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/sapling_memo_consistency.rs b/zcash_client_sqlite/src/wallet/init/migrations/sapling_memo_consistency.rs new file mode 100644 index 0000000000..2413eceb17 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/sapling_memo_consistency.rs @@ -0,0 +1,236 @@ +//! This migration reads the wallet's raw transaction data and updates the `sent_notes` table to +//! ensure that memo entries are consistent with the decrypted transaction's outputs. The empty +//! memo is now consistently represented as a single `0xf6` byte. + +use std::collections::{BTreeMap, HashMap, HashSet}; + +use rusqlite::named_params; +use schemerz_rusqlite::RusqliteMigration; +use uuid::Uuid; + +use zcash_client_backend::decrypt_transaction; +use zcash_keys::keys::UnifiedFullViewingKey; +use zcash_protocol::{consensus, TxId}; +use zip32::AccountId; + +use crate::{ + error::SqliteClientError, + wallet::{get_transaction, init::WalletMigrationError, memo_repr}, +}; + +use super::received_notes_nullable_nf; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0x7029b904_6557_4aa1_9da5_6904b65d2ba5); + +const DEPENDENCIES: &[Uuid] = &[received_notes_nullable_nf::MIGRATION_ID]; + +pub(super) struct Migration

{ + pub(super) params: P, +} + +impl

schemerz::Migration for Migration

{ + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + DEPENDENCIES.iter().copied().collect() + } + + fn description(&self) -> &'static str { + "This migration reads the wallet's raw transaction data and updates the `sent_notes` table to + ensure that memo entries are consistent with the decrypted transaction's outputs. The empty + memo is now consistently represented as a single `0xf6` byte." + } +} + +impl RusqliteMigration for Migration

{ + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), Self::Error> { + let mut stmt_raw_tx = transaction.prepare( + "SELECT DISTINCT + transactions.id_tx, transactions.txid, + accounts.account, accounts.ufvk + FROM sent_notes + JOIN accounts ON sent_notes.from_account = accounts.account + JOIN transactions ON transactions.id_tx = sent_notes.tx + WHERE transactions.raw IS NOT NULL", + )?; + + let mut rows = stmt_raw_tx.query([])?; + + let mut tx_sent_notes: BTreeMap<(i64, TxId), HashMap> = + BTreeMap::new(); + while let Some(row) = rows.next()? { + let id_tx: i64 = row.get(0)?; + let txid = row.get(1).map(TxId::from_bytes)?; + let account: u32 = row.get(2)?; + let ufvk_str: String = row.get(3)?; + let ufvk = UnifiedFullViewingKey::decode(&self.params, &ufvk_str).map_err(|e| { + WalletMigrationError::CorruptedData(format!( + "Could not decode unified full viewing key for account {}: {:?}", + account, e + )) + })?; + + tx_sent_notes.entry((id_tx, txid)).or_default().insert( + AccountId::try_from(account).map_err(|_| { + WalletMigrationError::CorruptedData("Account ID is invalid".to_owned()) + })?, + ufvk, + ); + } + + let mut stmt_update_sent_memo = transaction.prepare( + "UPDATE sent_notes + SET memo = :memo + WHERE tx = :id_tx + AND output_index = :output_index", + )?; + + for ((id_tx, txid), ufvks) in tx_sent_notes { + let (block_height, tx) = get_transaction(transaction, &self.params, txid) + .map_err(|err| match err { + SqliteClientError::CorruptedData(msg) => { + WalletMigrationError::CorruptedData(msg) + } + SqliteClientError::DbError(err) => WalletMigrationError::DbError(err), + other => WalletMigrationError::CorruptedData(format!( + "An error was encountered decoding transaction data: {:?}", + other + )), + })? + .ok_or_else(|| { + WalletMigrationError::CorruptedData(format!( + "Transaction not found for id {:?}", + txid + )) + })?; + + let decrypted_outputs = decrypt_transaction(&self.params, block_height, &tx, &ufvks); + + // Orchard outputs were not supported as of the wallet states that could require this + // migration. + for d_out in decrypted_outputs.sapling_outputs() { + stmt_update_sent_memo.execute(named_params![ + ":id_tx": id_tx, + ":output_index": d_out.index(), + ":memo": memo_repr(Some(d_out.memo())) + ])?; + } + } + + // Update the `v_transactions` view to avoid counting the empty memo as a memo + transaction.execute_batch( + "DROP VIEW v_transactions; + CREATE VIEW v_transactions AS + WITH + notes AS ( + SELECT sapling_received_notes.account AS account_id, + sapling_received_notes.tx AS id_tx, + 2 AS pool, + sapling_received_notes.value AS value, + CASE + WHEN sapling_received_notes.is_change THEN 1 + ELSE 0 + END AS is_change, + CASE + WHEN sapling_received_notes.is_change THEN 0 + ELSE 1 + END AS received_count, + CASE + WHEN (sapling_received_notes.memo IS NULL OR sapling_received_notes.memo = X'F6') + THEN 0 + ELSE 1 + END AS memo_present + FROM sapling_received_notes + UNION + SELECT utxos.received_by_account AS account_id, + transactions.id_tx AS id_tx, + 0 AS pool, + utxos.value_zat AS value, + 0 AS is_change, + 1 AS received_count, + 0 AS memo_present + FROM utxos + JOIN transactions + ON transactions.txid = utxos.prevout_txid + UNION + SELECT sapling_received_notes.account AS account_id, + sapling_received_notes.spent AS id_tx, + 2 AS pool, + -sapling_received_notes.value AS value, + 0 AS is_change, + 0 AS received_count, + 0 AS memo_present + FROM sapling_received_notes + WHERE sapling_received_notes.spent IS NOT NULL + ), + sent_note_counts AS ( + SELECT sent_notes.from_account AS account_id, + sent_notes.tx AS id_tx, + COUNT(DISTINCT sent_notes.id_note) as sent_notes, + SUM( + CASE + WHEN (sent_notes.memo IS NULL OR sent_notes.memo = X'F6') + THEN 0 + ELSE 1 + END + ) AS memo_count + FROM sent_notes + LEFT JOIN sapling_received_notes + ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) = + (sapling_received_notes.tx, 2, sapling_received_notes.output_index) + WHERE sapling_received_notes.is_change IS NULL + OR sapling_received_notes.is_change = 0 + GROUP BY account_id, id_tx + ), + blocks_max_height AS ( + SELECT MAX(blocks.height) as max_height FROM blocks + ) + SELECT notes.account_id AS account_id, + transactions.id_tx AS id_tx, + transactions.block AS mined_height, + transactions.tx_index AS tx_index, + transactions.txid AS txid, + transactions.expiry_height AS expiry_height, + transactions.raw AS raw, + SUM(notes.value) AS account_balance_delta, + transactions.fee AS fee_paid, + SUM(notes.is_change) > 0 AS has_change, + MAX(COALESCE(sent_note_counts.sent_notes, 0)) AS sent_note_count, + SUM(notes.received_count) AS received_note_count, + SUM(notes.memo_present) + MAX(COALESCE(sent_note_counts.memo_count, 0)) AS memo_count, + blocks.time AS block_time, + ( + blocks.height IS NULL + AND transactions.expiry_height <= blocks_max_height.max_height + ) AS expired_unmined + FROM transactions + JOIN notes ON notes.id_tx = transactions.id_tx + JOIN blocks_max_height + LEFT JOIN blocks ON blocks.height = transactions.block + LEFT JOIN sent_note_counts + ON sent_note_counts.account_id = notes.account_id + AND sent_note_counts.id_tx = notes.id_tx + GROUP BY notes.account_id, transactions.id_tx", + )?; + + Ok(()) + } + + fn down(&self, _: &rusqlite::Transaction) -> Result<(), Self::Error> { + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) + } +} + +#[cfg(test)] +mod tests { + use crate::wallet::init::migrations::tests::test_migrate; + + #[test] + fn migrate() { + test_migrate(&[super::MIGRATION_ID]); + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/sent_notes_to_internal.rs b/zcash_client_sqlite/src/wallet/init/migrations/sent_notes_to_internal.rs new file mode 100644 index 0000000000..a549993eb7 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/sent_notes_to_internal.rs @@ -0,0 +1,89 @@ +//! A migration that adds an identifier for the account that received a sent note +//! on an internal address to the sent_notes table. +use std::collections::HashSet; + +use schemerz_rusqlite::RusqliteMigration; +use uuid::Uuid; + +use super::ufvk_support; +use crate::wallet::init::WalletMigrationError; + +/// This migration adds the `to_account` field to the `sent_notes` table. +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0x0ddbe561_8259_4212_9ab7_66fdc4a74e1d); + +const DEPENDENCIES: &[Uuid] = &[ufvk_support::MIGRATION_ID]; + +pub(super) struct Migration; + +impl schemerz::Migration for Migration { + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + DEPENDENCIES.iter().copied().collect() + } + + fn description(&self) -> &'static str { + "Adds an identifier for the account that received an internal note to the sent_notes table" + } +} + +impl RusqliteMigration for Migration { + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + // Adds the `to_account` column to the `sent_notes` table and establishes the + // foreign key relationship with the `account` table. + transaction.execute_batch( + "CREATE TABLE sent_notes_new ( + id_note INTEGER PRIMARY KEY, + tx INTEGER NOT NULL, + output_pool INTEGER NOT NULL, + output_index INTEGER NOT NULL, + from_account INTEGER NOT NULL, + to_address TEXT, + to_account INTEGER, + value INTEGER NOT NULL, + memo BLOB, + FOREIGN KEY (tx) REFERENCES transactions(id_tx), + FOREIGN KEY (from_account) REFERENCES accounts(account), + FOREIGN KEY (to_account) REFERENCES accounts(account), + CONSTRAINT tx_output UNIQUE (tx, output_pool, output_index), + CONSTRAINT note_recipient CHECK ( + (to_address IS NOT NULL) != (to_account IS NOT NULL) + ) + ); + INSERT INTO sent_notes_new ( + id_note, tx, output_pool, output_index, + from_account, to_address, + value, memo) + SELECT + id_note, tx, output_pool, output_index, + from_account, address, + value, memo + FROM sent_notes;", + )?; + + transaction.execute_batch( + "DROP TABLE sent_notes; + ALTER TABLE sent_notes_new RENAME TO sent_notes;", + )?; + + Ok(()) + } + + fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) + } +} + +#[cfg(test)] +mod tests { + use crate::wallet::init::migrations::tests::test_migrate; + + #[test] + fn migrate() { + test_migrate(&[super::MIGRATION_ID]); + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/shardtree_support.rs b/zcash_client_sqlite/src/wallet/init/migrations/shardtree_support.rs new file mode 100644 index 0000000000..88d8ba2f89 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/shardtree_support.rs @@ -0,0 +1,289 @@ +//! This migration adds tables to the wallet database that are needed to persist Sapling note +//! commitment tree data using the `shardtree` crate, and migrates existing witness data into these +//! data structures. + +use std::collections::{BTreeSet, HashSet}; + +use incrementalmerkletree::{Marking, Retention}; +use rusqlite::{named_params, params}; +use schemerz_rusqlite::RusqliteMigration; +use shardtree::{error::ShardTreeError, store::caching::CachingShardStore, ShardTree}; +use tracing::{debug, trace}; +use uuid::Uuid; + +use zcash_client_backend::data_api::{ + scanning::{ScanPriority, ScanRange}, + SAPLING_SHARD_HEIGHT, +}; +use zcash_primitives::merkle_tree::{read_commitment_tree, read_incremental_witness}; +use zcash_protocol::consensus::{self, BlockHeight, NetworkUpgrade}; + +use crate::{ + wallet::{ + block_height_extrema, + commitment_tree::SqliteShardStore, + init::{migrations::received_notes_nullable_nf, WalletMigrationError}, + scanning::insert_queue_entries, + }, + PRUNING_DEPTH, SAPLING_TABLES_PREFIX, +}; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0x7da6489d_e835_4657_8be5_f512bcce6cbf); + +const DEPENDENCIES: &[Uuid] = &[received_notes_nullable_nf::MIGRATION_ID]; + +pub(super) struct Migration

{ + pub(super) params: P, +} + +impl

schemerz::Migration for Migration

{ + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + DEPENDENCIES.iter().copied().collect() + } + + fn description(&self) -> &'static str { + "Add support for receiving storage of note commitment tree data using the `shardtree` crate." + } +} + +impl RusqliteMigration for Migration

{ + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + // Add commitment tree sizes to block metadata. + debug!("Adding new columns"); + transaction.execute_batch( + "ALTER TABLE blocks ADD COLUMN sapling_commitment_tree_size INTEGER; + ALTER TABLE blocks ADD COLUMN orchard_commitment_tree_size INTEGER; + ALTER TABLE sapling_received_notes ADD COLUMN commitment_tree_position INTEGER;", + )?; + + // Add shard persistence + debug!("Creating tables for shard persistence"); + transaction.execute_batch( + "CREATE TABLE sapling_tree_shards ( + shard_index INTEGER PRIMARY KEY, + subtree_end_height INTEGER, + root_hash BLOB, + shard_data BLOB, + contains_marked INTEGER, + CONSTRAINT root_unique UNIQUE (root_hash) + ); + CREATE TABLE sapling_tree_cap ( + -- cap_id exists only to be able to take advantage of `ON CONFLICT` + -- upsert functionality; the table will only ever contain one row + cap_id INTEGER PRIMARY KEY, + cap_data BLOB NOT NULL + );", + )?; + + // Add checkpoint persistence + debug!("Creating tables for checkpoint persistence"); + transaction.execute_batch( + "CREATE TABLE sapling_tree_checkpoints ( + checkpoint_id INTEGER PRIMARY KEY, + position INTEGER + ); + CREATE TABLE sapling_tree_checkpoint_marks_removed ( + checkpoint_id INTEGER NOT NULL, + mark_removed_position INTEGER NOT NULL, + FOREIGN KEY (checkpoint_id) REFERENCES sapling_tree_checkpoints(checkpoint_id) + ON DELETE CASCADE, + CONSTRAINT spend_position_unique UNIQUE (checkpoint_id, mark_removed_position) + );", + )?; + + let block_height_extrema = block_height_extrema(transaction)?; + + let shard_store = + SqliteShardStore::<_, sapling::Node, SAPLING_SHARD_HEIGHT>::from_connection( + transaction, + SAPLING_TABLES_PREFIX, + )?; + let shard_store = CachingShardStore::load(shard_store).map_err(ShardTreeError::Storage)?; + let mut shard_tree: ShardTree< + _, + { sapling::NOTE_COMMITMENT_TREE_DEPTH }, + SAPLING_SHARD_HEIGHT, + > = ShardTree::new(shard_store, PRUNING_DEPTH.try_into().unwrap()); + // Insert all the tree information that we can get from block-end commitment trees + { + let mut stmt_blocks = transaction.prepare("SELECT height, sapling_tree FROM blocks")?; + let mut stmt_update_block_sapling_tree_size = transaction + .prepare("UPDATE blocks SET sapling_commitment_tree_size = ? WHERE height = ?")?; + + let mut block_rows = stmt_blocks.query([])?; + while let Some(row) = block_rows.next()? { + let block_height: u32 = row.get(0)?; + let sapling_tree_data: Vec = row.get(1)?; + + let block_end_tree = read_commitment_tree::< + sapling::Node, + _, + { sapling::NOTE_COMMITMENT_TREE_DEPTH }, + >(&sapling_tree_data[..]) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + sapling_tree_data.len(), + rusqlite::types::Type::Blob, + Box::new(e), + ) + })?; + + if block_height % 1000 == 0 { + debug!(height = block_height, "Migrating tree data to shardtree"); + } + trace!( + height = block_height, + size = block_end_tree.size(), + "Storing Sapling commitment tree size" + ); + stmt_update_block_sapling_tree_size + .execute(params![block_end_tree.size(), block_height])?; + + // We only need to load frontiers into the ShardTree that are close enough + // to the wallet's known chain tip to fill `PRUNING_DEPTH` checkpoints, so + // that ShardTree's witness generation will be able to correctly handle + // anchor depths. Loading frontiers further back than this doesn't add any + // useful nodes to the ShardTree (as we don't support rollbacks beyond + // `PRUNING_DEPTH`, and we won't be finding notes in earlier blocks), and + // hurts performance (as frontier importing has a significant Merkle tree + // hashing cost). + if let Some((nonempty_frontier, scanned_range)) = block_end_tree + .to_frontier() + .value() + .zip(block_height_extrema.as_ref()) + { + let block_height = BlockHeight::from(block_height); + if block_height + PRUNING_DEPTH >= *scanned_range.end() { + trace!( + height = u32::from(block_height), + frontier = ?nonempty_frontier, + "Inserting frontier nodes", + ); + shard_tree + .insert_frontier_nodes( + nonempty_frontier.clone(), + Retention::Checkpoint { + id: block_height, + marking: Marking::Reference, + }, + ) + .map_err(|e| match e { + ShardTreeError::Query(e) => ShardTreeError::Query(e), + ShardTreeError::Insert(e) => ShardTreeError::Insert(e), + ShardTreeError::Storage(_) => unreachable!(), + })? + } + } + } + } + + // Insert all the tree information that we can get from existing incremental witnesses + debug!("Migrating witness data to shardtree"); + { + let mut stmt_blocks = + transaction.prepare("SELECT note, block, witness FROM sapling_witnesses")?; + let mut stmt_set_note_position = transaction.prepare( + "UPDATE sapling_received_notes + SET commitment_tree_position = :position + WHERE id_note = :note_id", + )?; + let mut updated_note_positions = BTreeSet::new(); + let mut rows = stmt_blocks.query([])?; + while let Some(row) = rows.next()? { + let note_id: i64 = row.get(0)?; + let block_height: u32 = row.get(1)?; + let row_data: Vec = row.get(2)?; + let witness = read_incremental_witness::< + sapling::Node, + _, + { sapling::NOTE_COMMITMENT_TREE_DEPTH }, + >(&row_data[..]) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + row_data.len(), + rusqlite::types::Type::Blob, + Box::new(e), + ) + })?; + + let witnessed_position = witness.witnessed_position(); + if !updated_note_positions.contains(&witnessed_position) { + stmt_set_note_position.execute(named_params![ + ":note_id": note_id, + ":position": u64::from(witnessed_position) + ])?; + updated_note_positions.insert(witnessed_position); + } + + shard_tree + .insert_witness_nodes(witness, BlockHeight::from(block_height)) + .map_err(|e| match e { + ShardTreeError::Query(e) => ShardTreeError::Query(e), + ShardTreeError::Insert(e) => ShardTreeError::Insert(e), + ShardTreeError::Storage(_) => unreachable!(), + })?; + } + } + + shard_tree + .into_store() + .flush() + .map_err(ShardTreeError::Storage)?; + + // Establish the scan queue & wallet history table. + // block_range_end is exclusive. + debug!("Creating table for scan queue"); + transaction.execute_batch( + "CREATE TABLE scan_queue ( + block_range_start INTEGER NOT NULL, + block_range_end INTEGER NOT NULL, + priority INTEGER NOT NULL, + CONSTRAINT range_start_uniq UNIQUE (block_range_start), + CONSTRAINT range_end_uniq UNIQUE (block_range_end), + CONSTRAINT range_bounds_order CHECK ( + block_range_start < block_range_end + ) + );", + )?; + + if let Some(scanned_range) = block_height_extrema { + // `ScanRange` uses an exclusive upper bound. + let start = *scanned_range.start(); + let chain_end = *scanned_range.end() + 1; + let ignored_range = + self.params + .activation_height(NetworkUpgrade::Sapling) + .map(|sapling_activation| { + let ignored_range_start = std::cmp::min(sapling_activation, start); + ScanRange::from_parts(ignored_range_start..start, ScanPriority::Ignored) + }); + let scanned_range = ScanRange::from_parts(start..chain_end, ScanPriority::Scanned); + insert_queue_entries( + transaction, + ignored_range.iter().chain(Some(scanned_range).iter()), + )?; + } + + Ok(()) + } + + fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) + } +} + +#[cfg(test)] +mod tests { + use crate::wallet::init::migrations::tests::test_migrate; + + #[test] + fn migrate() { + test_migrate(&[super::MIGRATION_ID]); + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/spend_key_available.rs b/zcash_client_sqlite/src/wallet/init/migrations/spend_key_available.rs new file mode 100644 index 0000000000..edb0f0b254 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/spend_key_available.rs @@ -0,0 +1,56 @@ +//! The migration that records ephemeral addresses for each account. +use std::collections::HashSet; + +use schemerz_rusqlite::RusqliteMigration; +use uuid::Uuid; + +use crate::wallet::init::WalletMigrationError; + +use super::full_account_ids; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0x07610aac_b0e3_4ba8_aaa6_cda606f0fd7b); + +const DEPENDENCIES: &[Uuid] = &[full_account_ids::MIGRATION_ID]; + +#[allow(dead_code)] +pub(super) struct Migration; + +impl schemerz::Migration for Migration { + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + DEPENDENCIES.iter().copied().collect() + } + + fn description(&self) -> &'static str { + "Track which accounts have associated spending keys." + } +} + +impl RusqliteMigration for Migration { + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + transaction.execute_batch( + "ALTER TABLE accounts ADD COLUMN has_spend_key INTEGER NOT NULL DEFAULT 1", + )?; + Ok(()) + } + + fn down(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + transaction.execute_batch("ALTER TABLE accounts DROP COLUMN has_spend_key")?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::wallet::init::migrations::tests::test_migrate; + + #[test] + fn migrate() { + test_migrate(&[super::MIGRATION_ID]); + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/support_legacy_sqlite.rs b/zcash_client_sqlite/src/wallet/init/migrations/support_legacy_sqlite.rs new file mode 100644 index 0000000000..8af005b51f --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/support_legacy_sqlite.rs @@ -0,0 +1,88 @@ +//! Modifies definitions to avoid keywords that may not be available in older SQLite versions. +use std::collections::HashSet; + +use schemerz_rusqlite::RusqliteMigration; +use uuid::Uuid; + +use crate::wallet::init::{migrations::tx_retrieval_queue, WalletMigrationError}; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0x156d8c8f_2173_4b59_89b6_75697d5a2103); + +const DEPENDENCIES: &[Uuid] = &[tx_retrieval_queue::MIGRATION_ID]; + +pub(super) struct Migration; + +impl schemerz::Migration for Migration { + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + DEPENDENCIES.iter().copied().collect() + } + + fn description(&self) -> &'static str { + "Modifies definitions to avoid keywords that may not be available in older SQLite versions." + } +} + +impl RusqliteMigration for Migration { + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + transaction.execute_batch( + r#" + DROP VIEW v_tx_outputs; + CREATE VIEW v_tx_outputs AS + -- select all outputs received by the wallet + SELECT transactions.txid AS txid, + ro.pool AS output_pool, + ro.output_index AS output_index, + sent_notes.from_account_id AS from_account_id, + ro.account_id AS to_account_id, + NULL AS to_address, + ro.value AS value, + ro.is_change AS is_change, + ro.memo AS memo + FROM v_received_outputs ro + JOIN transactions + ON transactions.id_tx = ro.transaction_id + -- join to the sent_notes table to obtain `from_account_id` + LEFT JOIN sent_notes ON sent_notes.id = ro.sent_note_id + UNION + -- select all outputs sent from the wallet to external recipients + SELECT transactions.txid AS txid, + sent_notes.output_pool AS output_pool, + sent_notes.output_index AS output_index, + sent_notes.from_account_id AS from_account_id, + NULL AS to_account_id, + sent_notes.to_address AS to_address, + sent_notes.value AS value, + 0 AS is_change, + sent_notes.memo AS memo + FROM sent_notes + JOIN transactions + ON transactions.id_tx = sent_notes.tx + LEFT JOIN v_received_outputs ro ON ro.sent_note_id = sent_notes.id + -- exclude any sent notes for which a row exists in the v_received_outputs view + WHERE ro.account_id IS NULL + "#, + )?; + + Ok(()) + } + + fn down(&self, _: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) + } +} + +#[cfg(test)] +mod tests { + use crate::wallet::init::migrations::tests::test_migrate; + + #[test] + fn migrate() { + test_migrate(&[super::MIGRATION_ID]); + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/tx_retrieval_queue.rs b/zcash_client_sqlite/src/wallet/init/migrations/tx_retrieval_queue.rs new file mode 100644 index 0000000000..1240388a58 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/tx_retrieval_queue.rs @@ -0,0 +1,309 @@ +//! Adds tables for tracking transactions to be downloaded for transparent output and/or memo retrieval. + +use rusqlite::{named_params, Transaction}; +use schemerz_rusqlite::RusqliteMigration; +use std::collections::HashSet; +use uuid::Uuid; +use zcash_primitives::transaction::builder::DEFAULT_TX_EXPIRY_DELTA; +use zcash_protocol::consensus; + +use crate::wallet::init::WalletMigrationError; + +use super::{ + ensure_orchard_ua_receiver, ephemeral_addresses, nullifier_map, orchard_shardtree, + spend_key_available, +}; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0xfec02b61_3988_4b4f_9699_98977fac9e7f); + +#[cfg(feature = "transparent-inputs")] +use { + crate::{ + error::SqliteClientError, + wallet::{ + queue_transparent_input_retrieval, queue_unmined_tx_retrieval, + transparent::{queue_transparent_spend_detection, uivk_legacy_transparent_address}, + }, + AccountRef, TxRef, + }, + rusqlite::OptionalExtension as _, + std::convert::Infallible, + zcash_client_backend::data_api::DecryptedTransaction, + zcash_keys::encoding::AddressCodec, + zcash_protocol::consensus::{BlockHeight, BranchId}, +}; + +const DEPENDENCIES: &[Uuid] = &[ + orchard_shardtree::MIGRATION_ID, + ensure_orchard_ua_receiver::MIGRATION_ID, + ephemeral_addresses::MIGRATION_ID, + spend_key_available::MIGRATION_ID, + nullifier_map::MIGRATION_ID, +]; + +pub(super) struct Migration

{ + pub(super) _params: P, +} + +impl

schemerz::Migration for Migration

{ + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + DEPENDENCIES.iter().copied().collect() + } + + fn description(&self) -> &'static str { + "Adds tables for tracking transactions to be downloaded for transparent output and/or memo retrieval." + } +} + +impl RusqliteMigration for Migration

{ + type Error = WalletMigrationError; + + fn up(&self, conn: &Transaction) -> Result<(), WalletMigrationError> { + conn.execute_batch( + "CREATE TABLE tx_retrieval_queue ( + txid BLOB NOT NULL UNIQUE, + query_type INTEGER NOT NULL, + dependent_transaction_id INTEGER, + FOREIGN KEY (dependent_transaction_id) REFERENCES transactions(id_tx) + ); + + ALTER TABLE transactions ADD COLUMN target_height INTEGER; + + CREATE TABLE transparent_spend_search_queue ( + address TEXT NOT NULL, + transaction_id INTEGER NOT NULL, + output_index INTEGER NOT NULL, + FOREIGN KEY (transaction_id) REFERENCES transactions(id_tx), + CONSTRAINT value_received_height UNIQUE (transaction_id, output_index) + ); + + CREATE TABLE transparent_spend_map ( + spending_transaction_id INTEGER NOT NULL, + prevout_txid BLOB NOT NULL, + prevout_output_index INTEGER NOT NULL, + FOREIGN KEY (spending_transaction_id) REFERENCES transactions(id_tx) + -- NOTE: We can't create a unique constraint on just (prevout_txid, prevout_output_index) + -- because the same output may be attempted to be spent in multiple transactions, even + -- though only one will ever be mined. + CONSTRAINT transparent_spend_map_unique UNIQUE ( + spending_transaction_id, prevout_txid, prevout_output_index + ) + );", + )?; + + // Add estimated target height information for each transaction we know to + // have been created by the wallet; transactions that were discovered via + // chain scanning will have their `created` field set to `NULL`. + conn.execute( + "UPDATE transactions + SET target_height = expiry_height - :default_expiry_delta + WHERE expiry_height > :default_expiry_delta + AND created IS NOT NULL", + named_params![":default_expiry_delta": DEFAULT_TX_EXPIRY_DELTA], + )?; + + // Populate the enhancement queues with any transparent history information that we don't + // already have. + #[cfg(feature = "transparent-inputs")] + { + let mut stmt_transactions = + conn.prepare("SELECT id_tx, raw, mined_height FROM transactions")?; + let mut rows = stmt_transactions.query([])?; + while let Some(row) = rows.next()? { + let tx_ref = row.get(0).map(TxRef)?; + let tx_data = row.get::<_, Option>>(1)?; + let mined_height = row.get::<_, Option>(2)?.map(BlockHeight::from); + + if let Some(tx_data) = tx_data { + let tx = zcash_primitives::transaction::Transaction::read( + &tx_data[..], + // We assume unmined transactions are created with the current consensus branch ID. + mined_height.map_or(BranchId::Sapling, |h| { + BranchId::for_height(&self._params, h) + }), + ) + .map_err(|_| { + WalletMigrationError::CorruptedData( + "Could not read serialized transaction data.".to_owned(), + ) + })?; + + for (txout, output_index) in tx + .transparent_bundle() + .iter() + .flat_map(|b| b.vout.iter()) + .zip(0u32..) + { + if let Some(address) = txout.recipient_address() { + let find_address_account = || { + conn.query_row( + "SELECT account_id FROM addresses + WHERE cached_transparent_receiver_address = :address + UNION + SELECT account_id from ephemeral_addresses + WHERE address = :address", + named_params![":address": address.encode(&self._params)], + |row| row.get(0).map(AccountRef), + ) + .optional() + }; + let find_legacy_address_account = + || -> Result, SqliteClientError> { + let mut stmt = conn.prepare("SELECT id, uivk FROM accounts")?; + let mut rows = stmt.query([])?; + while let Some(row) = rows.next()? { + let account_id = row.get(0).map(AccountRef)?; + let uivk_str = row.get::<_, String>(1)?; + + if let Some((legacy_taddr, _)) = + uivk_legacy_transparent_address( + &self._params, + &uivk_str, + )? + { + if legacy_taddr == address { + return Ok(Some(account_id)); + } + } + } + + Ok(None) + }; + + if find_address_account()?.is_some() + || find_legacy_address_account()?.is_some() + { + queue_transparent_spend_detection( + conn, + &self._params, + address, + tx_ref, + output_index, + )? + } + } + } + + let d_tx = DecryptedTransaction::<'_, Infallible>::new( + mined_height, + &tx, + vec![], + #[cfg(feature = "orchard")] + vec![], + ); + + queue_transparent_input_retrieval(conn, tx_ref, &d_tx)?; + queue_unmined_tx_retrieval(conn, &d_tx)?; + } + } + } + + Ok(()) + } + + fn down(&self, conn: &Transaction) -> Result<(), WalletMigrationError> { + conn.execute_batch( + "DROP TABLE transparent_spend_map; + DROP TABLE transparent_spend_search_queue; + ALTER TABLE transactions DROP COLUMN target_height; + DROP TABLE tx_retrieval_queue;", + )?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use rusqlite::named_params; + use secrecy::Secret; + use tempfile::NamedTempFile; + + use ::transparent::{ + address::{Script, TransparentAddress}, + bundle::{OutPoint, TxIn, TxOut}, + }; + use zcash_primitives::transaction::{Authorized, TransactionData, TxVersion}; + use zcash_protocol::{ + consensus::{BranchId, Network}, + value::Zatoshis, + }; + + use crate::{ + wallet::init::{init_wallet_db_internal, migrations::tests::test_migrate}, + WalletDb, + }; + + use super::{DEPENDENCIES, MIGRATION_ID}; + + #[test] + fn migrate() { + test_migrate(&[MIGRATION_ID]); + } + + #[test] + fn migrate_with_data() { + let data_file = NamedTempFile::new().unwrap(); + let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap(); + + let seed_bytes = vec![0xab; 32]; + + // Migrate to database state just prior to this migration. + init_wallet_db_internal( + &mut db_data, + Some(Secret::new(seed_bytes.clone())), + DEPENDENCIES, + false, + ) + .unwrap(); + + // Add transactions to the wallet that exercise the data migration. + let add_tx_to_wallet = |tx: TransactionData| { + let tx = tx.freeze().unwrap(); + let txid = tx.txid(); + let mut raw_tx = vec![]; + tx.write(&mut raw_tx).unwrap(); + db_data + .conn + .execute( + r#"INSERT INTO transactions (txid, raw) VALUES (:txid, :raw);"#, + named_params! {":txid": txid.as_ref(), ":raw": raw_tx}, + ) + .unwrap(); + }; + add_tx_to_wallet(TransactionData::from_parts( + TxVersion::Zip225, + BranchId::Nu5, + 0, + 12345678.into(), + Some(transparent::bundle::Bundle { + vin: vec![TxIn { + prevout: OutPoint::fake(), + script_sig: Script(vec![]), + sequence: 0, + }], + vout: vec![TxOut { + value: Zatoshis::const_from_u64(10_000), + script_pubkey: TransparentAddress::PublicKeyHash([7; 20]).script(), + }], + authorization: transparent::bundle::Authorized, + }), + None, + None, + None, + )); + + // Check that we can apply this migration. + init_wallet_db_internal( + &mut db_data, + Some(Secret::new(seed_bytes)), + &[MIGRATION_ID], + false, + ) + .unwrap(); + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs b/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs new file mode 100644 index 0000000000..305b0f5eb6 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs @@ -0,0 +1,314 @@ +//! Migration that adds support for unified full viewing keys. +use std::{collections::HashSet, rc::Rc}; + +use rusqlite::{named_params, params}; +use schemerz_rusqlite::RusqliteMigration; +use secrecy::{ExposeSecret, SecretVec}; +use uuid::Uuid; + +use zcash_keys::{ + address::Address, + keys::{ReceiverRequirement::*, UnifiedAddressRequest, UnifiedSpendingKey}, +}; +use zcash_protocol::{consensus, PoolType}; +use zip32::AccountId; + +#[cfg(feature = "transparent-inputs")] +use ::transparent::keys::IncomingViewingKey; + +#[cfg(feature = "transparent-inputs")] +use zcash_keys::encoding::AddressCodec; + +use crate::{ + wallet::{ + init::{migrations::initial_setup, WalletMigrationError}, + pool_code, + }, + UA_TRANSPARENT, +}; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0xbe57ef3b_388e_42ea_97e2_678dafcf9754); + +const DEPENDENCIES: &[Uuid] = &[initial_setup::MIGRATION_ID]; + +pub(super) struct Migration

{ + pub(super) params: P, + pub(super) seed: Option>>, +} + +impl

schemerz::Migration for Migration

{ + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + DEPENDENCIES.iter().copied().collect() + } + + fn description(&self) -> &'static str { + "Add support for unified full viewing keys" + } +} + +impl RusqliteMigration for Migration

{ + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + // + // Update the accounts table to store ufvks rather than extfvks + // + + transaction.execute_batch( + "CREATE TABLE accounts_new ( + account INTEGER PRIMARY KEY, + ufvk TEXT NOT NULL, + address TEXT, + transparent_address TEXT + );", + )?; + + let mut stmt_fetch_accounts = + transaction.prepare("SELECT account, address FROM accounts")?; + + // We track whether we have determined seed relevance or not, in order to + // correctly report errors when checking the seed against an account: + // + // - If we encounter an error with the first account, we can assert that the seed + // is not relevant to the wallet by assuming that: + // - All accounts are from the same seed (which is historically the only use + // case that this migration supported), and + // - All accounts in the wallet must have been able to derive their USKs (in + // order to derive UIVKs). + // + // - Once the seed has been determined to be relevant (because it matched the + // first account), any subsequent account derivation failure is proving wrong + // our second assumption above, and we report this as corrupted data. + let mut seed_is_relevant = false; + + let ua_request = UnifiedAddressRequest::unsafe_new(Omit, Require, UA_TRANSPARENT); + let mut rows = stmt_fetch_accounts.query([])?; + while let Some(row) = rows.next()? { + // We only need to check for the presence of the seed if we have keys that + // need to be migrated; otherwise, it's fine to not supply the seed if this + // migration is being used to initialize an empty database. + if let Some(seed) = &self.seed { + let account: u32 = row.get(0)?; + let account = AccountId::try_from(account).map_err(|_| { + WalletMigrationError::CorruptedData("Account ID is invalid".to_owned()) + })?; + let usk = + UnifiedSpendingKey::from_seed(&self.params, seed.expose_secret(), account) + .map_err(|_| { + if seed_is_relevant { + WalletMigrationError::CorruptedData( + "Unable to derive spending key from seed.".to_string(), + ) + } else { + WalletMigrationError::SeedNotRelevant + } + })?; + let ufvk = usk.to_unified_full_viewing_key(); + + let address: String = row.get(1)?; + let decoded = Address::decode(&self.params, &address).ok_or_else(|| { + WalletMigrationError::CorruptedData(format!( + "Could not decode {} as a valid Zcash address.", + address + )) + })?; + match decoded { + Address::Sapling(decoded_address) => { + let dfvk = ufvk.sapling().ok_or_else(|| + WalletMigrationError::CorruptedData("Derivation should have produced a UFVK containing a Sapling component.".to_owned()))?; + let (idx, expected_address) = dfvk.default_address(); + if decoded_address != expected_address { + return Err(if seed_is_relevant { + WalletMigrationError::CorruptedData( + format!("Decoded Sapling address {} does not match the ufvk's Sapling address {} at {:?}.", + address, + Address::Sapling(expected_address).encode(&self.params), + idx)) + } else { + WalletMigrationError::SeedNotRelevant + }); + } + } + Address::Transparent(_) | Address::Tex(_) => { + return Err(WalletMigrationError::CorruptedData( + "Address field value decoded to a transparent address; should have been Sapling or unified.".to_string())); + } + Address::Unified(decoded_address) => { + let (expected_address, idx) = ufvk.default_address(Some(ua_request))?; + if decoded_address != expected_address { + return Err(if seed_is_relevant { + WalletMigrationError::CorruptedData( + format!("Decoded unified address {} does not match the ufvk's default address {} at {:?}.", + address, + Address::Unified(expected_address).encode(&self.params), + idx)) + } else { + WalletMigrationError::SeedNotRelevant + }); + } + } + } + + // We made it past one derived account, so the seed must be relevant. + seed_is_relevant = true; + + let ufvk_str: String = ufvk.encode(&self.params); + let address_str: String = ufvk + .default_address(Some(ua_request))? + .0 + .encode(&self.params); + + // This migration, and the wallet behaviour before it, stored the default + // transparent address in the `accounts` table. This does not necessarily + // match the transparent receiver in the default Unified Address. Starting + // from `AddressesTableMigration` below, we no longer store transparent + // addresses directly, but instead extract them from the Unified Address + // (or from the UFVK if the UA was derived without a transparent receiver, + // which is not the case for UAs generated by this crate). + #[cfg(feature = "transparent-inputs")] + let taddress_str: Option = ufvk.transparent().and_then(|k| { + k.derive_external_ivk() + .ok() + .map(|k| k.default_address().0.encode(&self.params)) + }); + #[cfg(not(feature = "transparent-inputs"))] + let taddress_str: Option = None; + + transaction.execute( + "INSERT INTO accounts_new (account, ufvk, address, transparent_address) + VALUES (:account, :ufvk, :address, :transparent_address)", + named_params![ + ":account": &::from(account), + ":ufvk": &ufvk_str, + ":address": &address_str, + ":transparent_address": &taddress_str, + ], + )?; + } else { + return Err(WalletMigrationError::SeedRequired); + } + } + + transaction.execute_batch( + "DROP TABLE accounts; + ALTER TABLE accounts_new RENAME TO accounts;", + )?; + + // + // Update the sent_notes table to include an output_pool column that + // is respected by the uniqueness constraint + // + + transaction.execute_batch( + "CREATE TABLE sent_notes_new ( + id_note INTEGER PRIMARY KEY, + tx INTEGER NOT NULL, + output_pool INTEGER NOT NULL , + output_index INTEGER NOT NULL, + from_account INTEGER NOT NULL, + address TEXT NOT NULL, + value INTEGER NOT NULL, + memo BLOB, + FOREIGN KEY (tx) REFERENCES transactions(id_tx), + FOREIGN KEY (from_account) REFERENCES accounts(account), + CONSTRAINT tx_output UNIQUE (tx, output_pool, output_index) + );", + )?; + + // we query in a nested scope so that the col_names iterator is correctly + // dropped and doesn't maintain a lock on the table. + let has_output_pool = { + let mut stmt_fetch_columns = transaction.prepare("PRAGMA TABLE_INFO('sent_notes')")?; + let mut col_names = stmt_fetch_columns.query_map([], |row| { + let col_name: String = row.get(1)?; + Ok(col_name) + })?; + + col_names.any(|cname| cname == Ok("output_pool".to_string())) + }; + + if has_output_pool { + transaction.execute_batch( + "INSERT INTO sent_notes_new + (id_note, tx, output_pool, output_index, from_account, address, value, memo) + SELECT id_note, tx, output_pool, output_index, from_account, address, value, memo + FROM sent_notes;" + )?; + } else { + let mut stmt_fetch_sent_notes = transaction.prepare( + "SELECT id_note, tx, output_index, from_account, address, value, memo + FROM sent_notes", + )?; + + let mut stmt_insert_sent_note = transaction.prepare( + "INSERT INTO sent_notes_new + (id_note, tx, output_pool, output_index, from_account, address, value, memo) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + )?; + + let mut rows = stmt_fetch_sent_notes.query([])?; + while let Some(row) = rows.next()? { + let id_note: i64 = row.get(0)?; + let tx_ref: i64 = row.get(1)?; + let output_index: i64 = row.get(2)?; + let account_id: u32 = row.get(3)?; + let address: String = row.get(4)?; + let value: i64 = row.get(5)?; + let memo: Option> = row.get(6)?; + + let decoded_address = Address::decode(&self.params, &address).ok_or_else(|| { + WalletMigrationError::CorruptedData(format!( + "Could not decode {} as a valid Zcash address.", + address + )) + })?; + let output_pool = match decoded_address { + Address::Sapling(_) => Ok(pool_code(PoolType::SAPLING)), + Address::Transparent(_) | Address::Tex(_) => { + Ok(pool_code(PoolType::TRANSPARENT)) + } + Address::Unified(_) => Err(WalletMigrationError::CorruptedData( + "Unified addresses should not yet appear in the sent_notes table." + .to_string(), + )), + }?; + + stmt_insert_sent_note.execute(params![ + id_note, + tx_ref, + output_pool, + output_index, + account_id, + address, + value, + memo + ])?; + } + } + + transaction.execute_batch( + "DROP TABLE sent_notes; + ALTER TABLE sent_notes_new RENAME TO sent_notes;", + )?; + + Ok(()) + } + + fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) + } +} + +#[cfg(test)] +mod tests { + use crate::wallet::init::migrations::tests::test_migrate; + + #[test] + fn migrate() { + test_migrate(&[super::MIGRATION_ID]); + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/utxos_table.rs b/zcash_client_sqlite/src/wallet/init/migrations/utxos_table.rs new file mode 100644 index 0000000000..e206a26dd4 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/utxos_table.rs @@ -0,0 +1,64 @@ +//! The migration that adds initial support for transparent UTXOs to the wallet. +use std::collections::HashSet; + +use schemerz_rusqlite::RusqliteMigration; +use uuid::Uuid; + +use crate::wallet::init::{migrations::initial_setup, WalletMigrationError}; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0xa2e0ed2e_8852_475e_b0a4_f154b15b9dbe); + +const DEPENDENCIES: &[Uuid] = &[initial_setup::MIGRATION_ID]; + +pub(super) struct Migration; + +impl schemerz::Migration for Migration { + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + DEPENDENCIES.iter().copied().collect() + } + + fn description(&self) -> &'static str { + "Add support for receiving transparent UTXOs." + } +} + +impl RusqliteMigration for Migration { + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + transaction.execute_batch( + "CREATE TABLE IF NOT EXISTS utxos ( + id_utxo INTEGER PRIMARY KEY, + address TEXT NOT NULL, + prevout_txid BLOB NOT NULL, + prevout_idx INTEGER NOT NULL, + script BLOB NOT NULL, + value_zat INTEGER NOT NULL, + height INTEGER NOT NULL, + spent_in_tx INTEGER, + FOREIGN KEY (spent_in_tx) REFERENCES transactions(id_tx), + CONSTRAINT tx_outpoint UNIQUE (prevout_txid, prevout_idx) + );", + )?; + Ok(()) + } + + fn down(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + transaction.execute_batch("DROP TABLE utxos;")?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::wallet::init::migrations::tests::test_migrate; + + #[test] + fn migrate() { + test_migrate(&[super::MIGRATION_ID]); + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/utxos_to_txos.rs b/zcash_client_sqlite/src/wallet/init/migrations/utxos_to_txos.rs new file mode 100644 index 0000000000..95ed661a9f --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/utxos_to_txos.rs @@ -0,0 +1,367 @@ +//! A migration that brings transparent UTXO handling into line with that for shielded +//! outputs, and adds `spent_note_count` and `is_shielding` to `v_transactions`. +use std::collections::HashSet; + +use schemerz_rusqlite::RusqliteMigration; +use uuid::Uuid; + +use crate::wallet::init::{migrations::orchard_received_notes, WalletMigrationError}; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0x3a2562b3_f174_46a1_aa8c_1d122ca2e884); + +const DEPENDENCIES: &[Uuid] = &[orchard_received_notes::MIGRATION_ID]; + +pub(super) struct Migration; + +impl schemerz::Migration for Migration { + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + DEPENDENCIES.iter().copied().collect() + } + + fn description(&self) -> &'static str { + "Updates transparent UTXO handling to be similar to that for shielded notes, and adds spent_note_count and is_shielding to v_transactions." + } +} + +impl RusqliteMigration for Migration { + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + transaction.execute_batch(r#" + PRAGMA legacy_alter_table = ON; + + CREATE TABLE transactions_new ( + id_tx INTEGER PRIMARY KEY, + txid BLOB NOT NULL UNIQUE, + created TEXT, + block INTEGER, + mined_height INTEGER, + tx_index INTEGER, + expiry_height INTEGER, + raw BLOB, + fee INTEGER, + FOREIGN KEY (block) REFERENCES blocks(height), + CONSTRAINT height_consistency CHECK (block IS NULL OR mined_height = block) + ); + + INSERT INTO transactions_new + SELECT id_tx, txid, created, block, block, tx_index, expiry_height, raw, fee + FROM transactions; + + -- We may initially set the block height to null, which will mean that the + -- transaction may appear to be un-mined until we actually scan the block + -- containing the transaction. + INSERT INTO transactions_new (txid, block, mined_height) + SELECT + utxos.prevout_txid, + blocks.height, + blocks.height + FROM utxos + LEFT OUTER JOIN blocks ON blocks.height = utxos.height + WHERE utxos.prevout_txid NOT IN ( + SELECT txid FROM transactions + ); + + DROP TABLE transactions; + ALTER TABLE transactions_new RENAME TO transactions; + + CREATE TABLE transparent_received_outputs ( + id INTEGER PRIMARY KEY, + transaction_id INTEGER NOT NULL, + output_index INTEGER NOT NULL, + account_id INTEGER NOT NULL, + address TEXT NOT NULL, + script BLOB NOT NULL, + value_zat INTEGER NOT NULL, + max_observed_unspent_height INTEGER, + FOREIGN KEY (transaction_id) REFERENCES transactions(id_tx), + FOREIGN KEY (account_id) REFERENCES accounts(id), + CONSTRAINT transparent_output_unique UNIQUE (transaction_id, output_index) + ); + CREATE INDEX idx_transparent_received_outputs_account_id + ON "transparent_received_outputs" (account_id); + + INSERT INTO transparent_received_outputs SELECT + u.id, + t.id_tx, + prevout_idx, + received_by_account_id, + address, + script, + value_zat, + NULL + FROM utxos u + -- This being a `LEFT OUTER JOIN` provides defense in depth against dropping + -- TXOs that reference missing `transactions` entries (which should never exist + -- given the migrations above). + LEFT OUTER JOIN transactions t ON t.txid = u.prevout_txid; + + CREATE TABLE transparent_received_output_spends_new ( + transparent_received_output_id INTEGER NOT NULL, + transaction_id INTEGER NOT NULL, + FOREIGN KEY (transparent_received_output_id) + REFERENCES transparent_received_outputs(id) + ON DELETE CASCADE, + FOREIGN KEY (transaction_id) + -- We do not delete transactions, so this does not cascade + REFERENCES transactions(id_tx), + UNIQUE (transparent_received_output_id, transaction_id) + ); + + INSERT INTO transparent_received_output_spends_new + SELECT * FROM transparent_received_output_spends; + + DROP VIEW v_tx_outputs; + DROP VIEW v_transactions; + DROP VIEW v_received_notes; + DROP VIEW v_received_note_spends; + DROP TABLE transparent_received_output_spends; + ALTER TABLE transparent_received_output_spends_new + RENAME TO transparent_received_output_spends; + + CREATE VIEW v_received_outputs AS + SELECT + sapling_received_notes.id AS id_within_pool_table, + sapling_received_notes.tx AS transaction_id, + 2 AS pool, + sapling_received_notes.output_index, + account_id, + sapling_received_notes.value, + is_change, + sapling_received_notes.memo, + sent_notes.id AS sent_note_id + FROM sapling_received_notes + LEFT JOIN sent_notes + ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) = + (sapling_received_notes.tx, 2, sapling_received_notes.output_index) + UNION + SELECT + orchard_received_notes.id AS id_within_pool_table, + orchard_received_notes.tx AS transaction_id, + 3 AS pool, + orchard_received_notes.action_index AS output_index, + account_id, + orchard_received_notes.value, + is_change, + orchard_received_notes.memo, + sent_notes.id AS sent_note_id + FROM orchard_received_notes + LEFT JOIN sent_notes + ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) = + (orchard_received_notes.tx, 3, orchard_received_notes.action_index) + UNION + SELECT + u.id AS id_within_pool_table, + u.transaction_id, + 0 AS pool, + u.output_index, + u.account_id, + u.value_zat AS value, + 0 AS is_change, + NULL AS memo, + sent_notes.id AS sent_note_id + FROM transparent_received_outputs u + LEFT JOIN sent_notes + ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) = + (u.transaction_id, 0, u.output_index); + + CREATE VIEW v_received_output_spends AS + SELECT + 2 AS pool, + sapling_received_note_id AS received_output_id, + transaction_id + FROM sapling_received_note_spends + UNION + SELECT + 3 AS pool, + orchard_received_note_id AS received_output_id, + transaction_id + FROM orchard_received_note_spends + UNION + SELECT + 0 AS pool, + transparent_received_output_id AS received_output_id, + transaction_id + FROM transparent_received_output_spends; + + CREATE VIEW v_transactions AS + WITH + notes AS ( + -- Outputs received in this transaction + SELECT ro.account_id AS account_id, + transactions.mined_height AS mined_height, + transactions.txid AS txid, + ro.pool AS pool, + id_within_pool_table, + ro.value AS value, + 0 AS spent_note_count, + CASE + WHEN ro.is_change THEN 1 + ELSE 0 + END AS change_note_count, + CASE + WHEN ro.is_change THEN 0 + ELSE 1 + END AS received_count, + CASE + WHEN (ro.memo IS NULL OR ro.memo = X'F6') + THEN 0 + ELSE 1 + END AS memo_present, + -- The wallet cannot receive transparent outputs in shielding transactions. + CASE + WHEN ro.pool = 0 + THEN 1 + ELSE 0 + END AS does_not_match_shielding + FROM v_received_outputs ro + JOIN transactions + ON transactions.id_tx = ro.transaction_id + UNION + -- Outputs spent in this transaction + SELECT ro.account_id AS account_id, + transactions.mined_height AS mined_height, + transactions.txid AS txid, + ro.pool AS pool, + id_within_pool_table, + -ro.value AS value, + 1 AS spent_note_count, + 0 AS change_note_count, + 0 AS received_count, + 0 AS memo_present, + -- The wallet cannot spend shielded outputs in shielding transactions. + CASE + WHEN ro.pool != 0 + THEN 1 + ELSE 0 + END AS does_not_match_shielding + FROM v_received_outputs ro + JOIN v_received_output_spends ros + ON ros.pool = ro.pool + AND ros.received_output_id = ro.id_within_pool_table + JOIN transactions + ON transactions.id_tx = ros.transaction_id + ), + -- Obtain a count of the notes that the wallet created in each transaction, + -- not counting change notes. + sent_note_counts AS ( + SELECT sent_notes.from_account_id AS account_id, + transactions.txid AS txid, + COUNT(DISTINCT sent_notes.id) AS sent_notes, + SUM( + CASE + WHEN (sent_notes.memo IS NULL OR sent_notes.memo = X'F6' OR ro.transaction_id IS NOT NULL) + THEN 0 + ELSE 1 + END + ) AS memo_count + FROM sent_notes + JOIN transactions + ON transactions.id_tx = sent_notes.tx + LEFT JOIN v_received_outputs ro + ON sent_notes.id = ro.sent_note_id + WHERE COALESCE(ro.is_change, 0) = 0 + GROUP BY account_id, txid + ), + blocks_max_height AS ( + SELECT MAX(blocks.height) AS max_height FROM blocks + ) + SELECT notes.account_id AS account_id, + notes.mined_height AS mined_height, + notes.txid AS txid, + transactions.tx_index AS tx_index, + transactions.expiry_height AS expiry_height, + transactions.raw AS raw, + SUM(notes.value) AS account_balance_delta, + transactions.fee AS fee_paid, + SUM(notes.change_note_count) > 0 AS has_change, + MAX(COALESCE(sent_note_counts.sent_notes, 0)) AS sent_note_count, + SUM(notes.received_count) AS received_note_count, + SUM(notes.memo_present) + MAX(COALESCE(sent_note_counts.memo_count, 0)) AS memo_count, + blocks.time AS block_time, + ( + blocks.height IS NULL + AND transactions.expiry_height BETWEEN 1 AND blocks_max_height.max_height + ) AS expired_unmined, + SUM(notes.spent_note_count) AS spent_note_count, + ( + -- All of the wallet-spent and wallet-received notes are consistent with a + -- shielding transaction. + SUM(notes.does_not_match_shielding) = 0 + -- The transaction contains at least one wallet-spent output. + AND SUM(notes.spent_note_count) > 0 + -- The transaction contains at least one wallet-received note. + AND (SUM(notes.received_count) + SUM(notes.change_note_count)) > 0 + -- We do not know about any external outputs of the transaction. + AND MAX(COALESCE(sent_note_counts.sent_notes, 0)) = 0 + ) AS is_shielding + FROM notes + LEFT JOIN transactions + ON notes.txid = transactions.txid + JOIN blocks_max_height + LEFT JOIN blocks ON blocks.height = notes.mined_height + LEFT JOIN sent_note_counts + ON sent_note_counts.account_id = notes.account_id + AND sent_note_counts.txid = notes.txid + GROUP BY notes.account_id, notes.txid; + + CREATE VIEW v_tx_outputs AS + -- select all outputs received by the wallet + SELECT transactions.txid AS txid, + ro.pool AS output_pool, + ro.output_index AS output_index, + sent_notes.from_account_id AS from_account_id, + ro.account_id AS to_account_id, + NULL AS to_address, + ro.value AS value, + ro.is_change AS is_change, + ro.memo AS memo + FROM v_received_outputs ro + JOIN transactions + ON transactions.id_tx = ro.transaction_id + -- join to the sent_notes table to obtain `from_account_id` + LEFT JOIN sent_notes ON sent_notes.id = ro.sent_note_id + UNION + -- select all outputs sent from the wallet to external recipients + SELECT transactions.txid AS txid, + sent_notes.output_pool AS output_pool, + sent_notes.output_index AS output_index, + sent_notes.from_account_id AS from_account_id, + NULL AS to_account_id, + sent_notes.to_address AS to_address, + sent_notes.value AS value, + FALSE AS is_change, + sent_notes.memo AS memo + FROM sent_notes + JOIN transactions + ON transactions.id_tx = sent_notes.tx + LEFT JOIN v_received_outputs ro ON ro.sent_note_id = sent_notes.id + -- exclude any sent notes for which a row exists in the v_received_outputs view + WHERE ro.account_id IS NULL; + + DROP TABLE utxos; + + PRAGMA legacy_alter_table = OFF; + "#)?; + + Ok(()) + } + + fn down(&self, _: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) + } +} + +#[cfg(test)] +mod tests { + use crate::wallet::init::migrations::tests::test_migrate; + + #[test] + fn migrate() { + test_migrate(&[super::MIGRATION_ID]); + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/v_sapling_shard_unscanned_ranges.rs b/zcash_client_sqlite/src/wallet/init/migrations/v_sapling_shard_unscanned_ranges.rs new file mode 100644 index 0000000000..c4657aac86 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/v_sapling_shard_unscanned_ranges.rs @@ -0,0 +1,111 @@ +//! This migration adds a view that returns the un-scanned ranges associated with each sapling note +//! commitment tree shard. + +use std::collections::HashSet; + +use schemerz_rusqlite::RusqliteMigration; +use uuid::Uuid; + +use zcash_client_backend::data_api::{scanning::ScanPriority, SAPLING_SHARD_HEIGHT}; +use zcash_protocol::consensus::{self, NetworkUpgrade}; + +use crate::wallet::{init::WalletMigrationError, scanning::priority_code}; + +use super::add_account_birthdays; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0xfa934bdc_97b6_4980_8a83_b2cb1ac465fd); + +const DEPENDENCIES: &[Uuid] = &[add_account_birthdays::MIGRATION_ID]; + +pub(super) struct Migration

{ + pub(super) params: P, +} + +impl

schemerz::Migration for Migration

{ + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + DEPENDENCIES.iter().copied().collect() + } + + fn description(&self) -> &'static str { + "Adds a view that returns the un-scanned ranges associated with each sapling note commitment tree shard." + } +} + +impl RusqliteMigration for Migration

{ + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), Self::Error> { + transaction.execute_batch(&format!( + "CREATE VIEW v_sapling_shard_scan_ranges AS + SELECT + shard.shard_index, + shard.shard_index << {} AS start_position, + (shard.shard_index + 1) << {} AS end_position_exclusive, + IFNULL(prev_shard.subtree_end_height, {}) AS subtree_start_height, + shard.subtree_end_height, + shard.contains_marked, + scan_queue.block_range_start, + scan_queue.block_range_end, + scan_queue.priority + FROM sapling_tree_shards shard + LEFT OUTER JOIN sapling_tree_shards prev_shard + ON shard.shard_index = prev_shard.shard_index + 1 + -- Join with scan ranges that overlap with the subtree's involved blocks. + INNER JOIN scan_queue ON ( + subtree_start_height < scan_queue.block_range_end AND + ( + scan_queue.block_range_start <= shard.subtree_end_height OR + shard.subtree_end_height IS NULL + ) + )", + SAPLING_SHARD_HEIGHT, + SAPLING_SHARD_HEIGHT, + u32::from( + self.params + .activation_height(NetworkUpgrade::Sapling) + .unwrap() + ), + ))?; + + transaction.execute_batch(&format!( + "CREATE VIEW v_sapling_shard_unscanned_ranges AS + WITH wallet_birthday AS (SELECT MIN(birthday_height) AS height FROM accounts) + SELECT + shard_index, + start_position, + end_position_exclusive, + subtree_start_height, + subtree_end_height, + contains_marked, + block_range_start, + block_range_end, + priority + FROM v_sapling_shard_scan_ranges + INNER JOIN wallet_birthday + WHERE priority > {} + AND block_range_end > wallet_birthday.height;", + priority_code(&ScanPriority::Scanned), + ))?; + + Ok(()) + } + + fn down(&self, transaction: &rusqlite::Transaction) -> Result<(), Self::Error> { + transaction.execute_batch("DROP VIEW v_sapling_shard_unscanned_ranges;")?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::wallet::init::migrations::tests::test_migrate; + + #[test] + fn migrate() { + test_migrate(&[super::MIGRATION_ID]); + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/v_transactions_net.rs b/zcash_client_sqlite/src/wallet/init/migrations/v_transactions_net.rs new file mode 100644 index 0000000000..f0670ac22b --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/v_transactions_net.rs @@ -0,0 +1,590 @@ +//! Migration that fixes a bug in v_transactions that caused the change to be incorrectly ignored +//! as received value. +use std::collections::HashSet; + +use rusqlite::named_params; +use schemerz_rusqlite::RusqliteMigration; +use uuid::Uuid; + +use zcash_protocol::PoolType; + +use super::add_transaction_views; +use crate::wallet::{init::WalletMigrationError, pool_code}; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0x2aa4d24f_51aa_4a4c_8d9b_e5b8a762865f); + +const DEPENDENCIES: &[Uuid] = &[add_transaction_views::MIGRATION_ID]; + +pub(crate) struct Migration; + +impl schemerz::Migration for Migration { + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + DEPENDENCIES.iter().copied().collect() + } + + fn description(&self) -> &'static str { + "Fix transaction views to correctly handle double-entry accounting for change." + } +} + +impl RusqliteMigration for Migration { + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + // As of this migration, the `received_notes` table only stores Sapling notes, so + // we can hard-code the `output_pool` value. + transaction.execute( + "INSERT INTO sent_notes (tx, output_pool, output_index, from_account, to_account, value) + SELECT tx, :output_pool, output_index, account, account, value + FROM received_notes + WHERE received_notes.is_change + EXCEPT + SELECT tx, :output_pool, output_index, from_account, from_account, value + FROM sent_notes", + named_params![ + ":output_pool": &pool_code(PoolType::SAPLING) + ] + )?; + + transaction.execute_batch( + "DROP VIEW v_tx_received; + DROP VIEW v_tx_sent;", + )?; + + transaction.execute_batch( + "CREATE VIEW v_tx_outputs AS + SELECT received_notes.tx AS id_tx, + 2 AS output_pool, + received_notes.output_index AS output_index, + sent_notes.from_account AS from_account, + received_notes.account AS to_account, + NULL AS to_address, + received_notes.value AS value, + received_notes.is_change AS is_change, + received_notes.memo AS memo + FROM received_notes + LEFT JOIN sent_notes + ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) = + (received_notes.tx, 2, sent_notes.output_index) + UNION + SELECT transactions.id_tx AS id_tx, + 0 AS output_pool, + utxos.prevout_idx AS output_index, + NULL AS from_account, + utxos.received_by_account AS to_account, + utxos.address AS to_address, + utxos.value_zat AS value, + false AS is_change, + NULL AS memo + FROM utxos + JOIN transactions + ON transactions.txid = utxos.prevout_txid + UNION + SELECT sent_notes.tx AS id_tx, + sent_notes.output_pool AS output_pool, + sent_notes.output_index AS output_index, + sent_notes.from_account AS from_account, + received_notes.account AS to_account, + sent_notes.to_address AS to_address, + sent_notes.value AS value, + false AS is_change, + sent_notes.memo AS memo + FROM sent_notes + LEFT JOIN received_notes + ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) = + (received_notes.tx, 2, received_notes.output_index) + WHERE received_notes.is_change IS NULL + OR received_notes.is_change = 0", + )?; + + transaction.execute_batch( + "DROP VIEW v_transactions; + CREATE VIEW v_transactions AS + WITH + notes AS ( + SELECT received_notes.account AS account_id, + received_notes.tx AS id_tx, + 2 AS pool, + received_notes.value AS value, + CASE + WHEN received_notes.is_change THEN 1 + ELSE 0 + END AS is_change, + CASE + WHEN received_notes.is_change THEN 0 + ELSE 1 + END AS received_count, + CASE + WHEN received_notes.memo IS NULL THEN 0 + ELSE 1 + END AS memo_present + FROM received_notes + UNION + SELECT utxos.received_by_account AS account_id, + transactions.id_tx AS id_tx, + 0 AS pool, + utxos.value_zat AS value, + 0 AS is_change, + 1 AS received_count, + 0 AS memo_present + FROM utxos + JOIN transactions + ON transactions.txid = utxos.prevout_txid + UNION + SELECT received_notes.account AS account_id, + received_notes.spent AS id_tx, + 2 AS pool, + -received_notes.value AS value, + 0 AS is_change, + 0 AS received_count, + 0 AS memo_present + FROM received_notes + WHERE received_notes.spent IS NOT NULL + ), + sent_note_counts AS ( + SELECT sent_notes.from_account AS account_id, + sent_notes.tx AS id_tx, + COUNT(DISTINCT sent_notes.id_note) as sent_notes, + SUM( + CASE + WHEN sent_notes.memo IS NULL THEN 0 + ELSE 1 + END + ) AS memo_count + FROM sent_notes + LEFT JOIN received_notes + ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) = + (received_notes.tx, 2, received_notes.output_index) + WHERE received_notes.is_change IS NULL + OR received_notes.is_change = 0 + GROUP BY account_id, id_tx + ), + blocks_max_height AS ( + SELECT MAX(blocks.height) as max_height FROM blocks + ) + SELECT notes.account_id AS account_id, + transactions.id_tx AS id_tx, + transactions.block AS mined_height, + transactions.tx_index AS tx_index, + transactions.txid AS txid, + transactions.expiry_height AS expiry_height, + transactions.raw AS raw, + SUM(notes.value) AS account_balance_delta, + transactions.fee AS fee_paid, + SUM(notes.is_change) > 0 AS has_change, + MAX(COALESCE(sent_note_counts.sent_notes, 0)) AS sent_note_count, + SUM(notes.received_count) AS received_note_count, + SUM(notes.memo_present) + MAX(COALESCE(sent_note_counts.memo_count, 0)) AS memo_count, + blocks.time AS block_time, + ( + blocks.height IS NULL + AND transactions.expiry_height <= blocks_max_height.max_height + ) AS expired_unmined + FROM transactions + JOIN notes ON notes.id_tx = transactions.id_tx + JOIN blocks_max_height + LEFT JOIN blocks ON blocks.height = transactions.block + LEFT JOIN sent_note_counts + ON sent_note_counts.account_id = notes.account_id + AND sent_note_counts.id_tx = notes.id_tx + GROUP BY notes.account_id, transactions.id_tx", + )?; + + Ok(()) + } + + fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) + } +} + +#[cfg(test)] +mod tests { + use rusqlite::{self, params}; + use tempfile::NamedTempFile; + + use zcash_keys::keys::UnifiedSpendingKey; + use zcash_protocol::consensus::Network; + use zip32::AccountId; + + use crate::{ + wallet::init::{init_wallet_db_internal, migrations::add_transaction_views}, + WalletDb, + }; + + #[test] + fn v_transactions_net() { + let data_file = NamedTempFile::new().unwrap(); + let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap(); + init_wallet_db_internal( + &mut db_data, + None, + &[add_transaction_views::MIGRATION_ID], + false, + ) + .unwrap(); + + // Create two accounts in the wallet. + let usk0 = UnifiedSpendingKey::from_seed(&db_data.params, &[0u8; 32][..], AccountId::ZERO) + .unwrap(); + let ufvk0 = usk0.to_unified_full_viewing_key(); + db_data + .conn + .execute( + "INSERT INTO accounts (account, ufvk) VALUES (0, ?)", + params![ufvk0.encode(&db_data.params)], + ) + .unwrap(); + + let usk1 = UnifiedSpendingKey::from_seed( + &db_data.params, + &[1u8; 32][..], + AccountId::try_from(1).unwrap(), + ) + .unwrap(); + let ufvk1 = usk1.to_unified_full_viewing_key(); + db_data + .conn + .execute( + "INSERT INTO accounts (account, ufvk) VALUES (1, ?)", + params![ufvk1.encode(&db_data.params)], + ) + .unwrap(); + + // - Tx 0 contains two received notes of 2 and 5 zatoshis that are controlled by account 0. + db_data.conn.execute_batch( + "INSERT INTO blocks (height, hash, time, sapling_tree) VALUES (0, 0, 0, x'00'); + INSERT INTO transactions (block, id_tx, txid) VALUES (0, 0, 'tx0'); + + INSERT INTO received_notes (tx, output_index, account, diversifier, value, rcm, nf, is_change) + VALUES (0, 0, 0, '', 2, '', 'nf_a', false); + INSERT INTO received_notes (tx, output_index, account, diversifier, value, rcm, nf, is_change) + VALUES (0, 3, 0, '', 5, '', 'nf_b', false);").unwrap(); + + // - Tx 1 creates two notes of 2 and 3 zatoshis for an external address, and a change note + // of 2 zatoshis. This is representative of a historic transaction where no `sent_notes` + // entry was created for the change value. + db_data.conn.execute_batch( + "INSERT INTO blocks (height, hash, time, sapling_tree) VALUES (1, 1, 1, x'00'); + INSERT INTO transactions (block, id_tx, txid) VALUES (1, 1, 'tx1'); + UPDATE received_notes SET spent = 1 WHERE tx = 0; + INSERT INTO sent_notes (tx, output_pool, output_index, from_account, to_account, to_address, value) + VALUES (1, 2, 0, 0, NULL, 'addra', 2); + INSERT INTO sent_notes (tx, output_pool, output_index, from_account, to_account, to_address, value, memo) + VALUES (1, 2, 1, 0, NULL, 'addrb', 3, X'61'); + INSERT INTO received_notes (tx, output_index, account, diversifier, value, rcm, nf, is_change) + VALUES (1, 2, 0, '', 2, '', 'nf_c', true);").unwrap(); + + // - Tx 2 sends the half of the wallet value from account 0 to account 1 and returns the + // other half to the sending account as change. Also there's a random transparent utxo, + // received, who knows where it came from but it's for account 0. + db_data.conn.execute_batch( + "INSERT INTO blocks (height, hash, time, sapling_tree) VALUES (2, 2, 2, x'00'); + INSERT INTO transactions (block, id_tx, txid) VALUES (2, 2, 'tx2'); + UPDATE received_notes SET spent = 2 WHERE tx = 1; + INSERT INTO utxos (received_by_account, address, prevout_txid, prevout_idx, script, value_zat, height) + VALUES (0, 'taddr_tx2', 'tx2', 0, '', 1, 2); + INSERT INTO sent_notes (tx, output_pool, output_index, from_account, to_account, to_address, value) + VALUES (2, 2, 0, 0, 0, NULL, 1); + INSERT INTO sent_notes (tx, output_pool, output_index, from_account, to_account, to_address, value) + VALUES (2, 2, 1, 0, 1, NULL, 1); + INSERT INTO received_notes (tx, output_index, account, diversifier, value, rcm, nf, is_change) + VALUES (2, 0, 0, '', 1, '', 'nf_d', true); + INSERT INTO received_notes (tx, output_index, account, diversifier, value, rcm, nf, is_change) + VALUES (2, 1, 1, '', 1, '', 'nf_e', false);", + ).unwrap(); + + // - Tx 3 just receives transparent funds and does nothing else. For this to work, the + // transaction must be retrieved by the wallet. + db_data.conn.execute_batch( + "INSERT INTO blocks (height, hash, time, sapling_tree) VALUES (3, 3, 3, x'00'); + INSERT INTO transactions (block, id_tx, txid) VALUES (3, 3, 'tx3'); + + INSERT INTO utxos (received_by_account, address, prevout_txid, prevout_idx, script, value_zat, height) + VALUES (0, 'taddr_tx3', 'tx3', 0, '', 1, 3);").unwrap(); + + // Behavior prior to change: + { + let mut q = db_data + .conn + .prepare( + "SELECT id_tx, received_by_account, received_total, received_note_count, memo_count + FROM v_tx_received", + ) + .unwrap(); + let mut rows = q.query([]).unwrap(); + let mut row_count = 0; + while let Some(row) = rows.next().unwrap() { + row_count += 1; + let tx: i64 = row.get(0).unwrap(); + let account: i64 = row.get(1).unwrap(); + let total: i64 = row.get(2).unwrap(); + let count: i64 = row.get(3).unwrap(); + let memo_count: i64 = row.get(4).unwrap(); + match (account, tx) { + (0, 0) => { + assert_eq!(total, 7); + assert_eq!(count, 2); + assert_eq!(memo_count, 0); + } + (0, 1) => { + // ERROR: transaction 1 only has change, should not be counted as received + assert_eq!(total, 2); + assert_eq!(count, 1); + assert_eq!(memo_count, 0); + } + (0, 2) => { + // ERROR: transaction 2 was counted twice: as a received transfer, and as change. + // Also, received transparent funds didn't appear in `v_transactions`. + assert_eq!(total, 1); + assert_eq!(count, 1); + assert_eq!(memo_count, 0); + } + (1, 2) => { + // Transaction 2 should be counted as received, as this is a cross-account + // transfer, not change. + assert_eq!(total, 1); + assert_eq!(count, 1); + assert_eq!(memo_count, 0); + } + _ => { + panic!("No such transaction."); + } + } + } + assert_eq!(row_count, 4); + + let mut q = db_data + .conn + .prepare("SELECT id_tx, sent_total, sent_note_count, memo_count FROM v_tx_sent") + .unwrap(); + let mut rows = q.query([]).unwrap(); + let mut row_count = 0; + while let Some(row) = rows.next().unwrap() { + row_count += 1; + let tx: i64 = row.get(0).unwrap(); + let total: i64 = row.get(1).unwrap(); + let count: i64 = row.get(2).unwrap(); + let memo_count: i64 = row.get(3).unwrap(); + match tx { + 1 => { + assert_eq!(total, 5); + assert_eq!(count, 2); + assert_eq!(memo_count, 1); + } + 2 => { + // ERROR: the total "sent" includes the change + assert_eq!(total, 2); + assert_eq!(count, 2); + assert_eq!(memo_count, 0); + } + other => { + panic!("Transaction {} is not a sent tx.", other); + } + } + } + assert_eq!(row_count, 2); + } + + // Run this migration + init_wallet_db_internal(&mut db_data, None, &[super::MIGRATION_ID], false).unwrap(); + + // Corrected behavior after v_transactions has been updated + { + let mut q = db_data + .conn + .prepare( + "SELECT account_id, id_tx, account_balance_delta, has_change, memo_count, sent_note_count, received_note_count + FROM v_transactions", + ) + .unwrap(); + let mut rows = q.query([]).unwrap(); + let mut row_count = 0; + while let Some(row) = rows.next().unwrap() { + row_count += 1; + let account: i64 = row.get(0).unwrap(); + let tx: i64 = row.get(1).unwrap(); + let account_balance_delta: i64 = row.get(2).unwrap(); + let has_change: bool = row.get(3).unwrap(); + let memo_count: i64 = row.get(4).unwrap(); + let sent_note_count: i64 = row.get(5).unwrap(); + let received_note_count: i64 = row.get(6).unwrap(); + match (account, tx) { + (0, 0) => { + assert_eq!(account_balance_delta, 7); + assert!(!has_change); + assert_eq!(memo_count, 0); + } + (0, 1) => { + assert_eq!(account_balance_delta, -5); + assert!(has_change); + assert_eq!(memo_count, 1); + } + (0, 2) => { + assert_eq!(account_balance_delta, 0); + assert!(has_change); + assert_eq!(memo_count, 0); + assert_eq!(sent_note_count, 1); + assert_eq!(received_note_count, 1); + } + (1, 2) => { + assert_eq!(account_balance_delta, 1); + assert!(!has_change); + assert_eq!(memo_count, 0); + assert_eq!(sent_note_count, 0); + assert_eq!(received_note_count, 1); + } + (0, 3) => { + assert_eq!(account_balance_delta, 1); + assert!(!has_change); + assert_eq!(memo_count, 0); + assert_eq!(sent_note_count, 0); + assert_eq!(received_note_count, 1); + } + other => { + panic!("(Account, Transaction) pair {:?} is not expected to exist in the wallet.", other); + } + } + } + assert_eq!(row_count, 5); + } + + // tests for v_tx_outputs + { + let mut q = db_data + .conn + .prepare("SELECT * FROM v_tx_outputs WHERE id_tx = 1") + .unwrap(); + let mut rows = q.query([]).unwrap(); + let mut row_count = 0; + while let Some(row) = rows.next().unwrap() { + row_count += 1; + let tx: i64 = row.get(0).unwrap(); + let output_pool: i64 = row.get(1).unwrap(); + let output_index: i64 = row.get(2).unwrap(); + let from_account: Option = row.get(3).unwrap(); + let to_account: Option = row.get(4).unwrap(); + let to_address: Option = row.get(5).unwrap(); + let value: i64 = row.get(6).unwrap(); + let is_change: bool = row.get(7).unwrap(); + //let memo: Option = row.get(7).unwrap(); + match output_index { + 0 => { + assert_eq!(output_pool, 2); + assert_eq!(from_account, Some(0)); + assert_eq!(to_account, None); + assert_eq!(to_address, Some("addra".to_string())); + assert_eq!(value, 2); + assert!(!is_change); + } + 1 => { + assert_eq!(output_pool, 2); + assert_eq!(from_account, Some(0)); + assert_eq!(to_account, None); + assert_eq!(to_address, Some("addrb".to_string())); + assert_eq!(value, 3); + assert!(!is_change); + } + 2 => { + assert_eq!(output_pool, 2); + assert_eq!(from_account, Some(0)); + assert_eq!(to_account, Some(0)); + assert_eq!(to_address, None); + assert_eq!(value, 2); + assert!(is_change); + } + other => { + panic!("Unexpected output index for tx {}: {}.", tx, other); + } + } + } + assert_eq!(row_count, 3); + + let mut q = db_data + .conn + .prepare("SELECT * FROM v_tx_outputs WHERE id_tx = 2") + .unwrap(); + let mut rows = q.query([]).unwrap(); + let mut row_count = 0; + while let Some(row) = rows.next().unwrap() { + row_count += 1; + let tx: i64 = row.get(0).unwrap(); + let output_pool: i64 = row.get(1).unwrap(); + let output_index: i64 = row.get(2).unwrap(); + let from_account: Option = row.get(3).unwrap(); + let to_account: Option = row.get(4).unwrap(); + let to_address: Option = row.get(5).unwrap(); + let value: i64 = row.get(6).unwrap(); + let is_change: bool = row.get(7).unwrap(); + match (output_pool, output_index) { + (0, 0) => { + assert_eq!(from_account, None); + assert_eq!(to_account, Some(0)); + assert_eq!(to_address, Some("taddr_tx2".to_string())); + assert_eq!(value, 1); + assert!(!is_change); + } + (2, 0) => { + assert_eq!(from_account, Some(0)); + assert_eq!(to_account, Some(0)); + assert_eq!(to_address, None); + assert_eq!(value, 1); + assert!(is_change); + } + (2, 1) => { + assert_eq!(from_account, Some(0)); + assert_eq!(to_account, Some(1)); + assert_eq!(to_address, None); + assert_eq!(value, 1); + assert!(!is_change); + } + other => { + panic!( + "Unexpected output pool and index for tx {}: {:?}.", + tx, other + ); + } + } + } + assert_eq!(row_count, 3); + + let mut q = db_data + .conn + .prepare("SELECT * FROM v_tx_outputs WHERE id_tx = 3") + .unwrap(); + let mut rows = q.query([]).unwrap(); + let mut row_count = 0; + while let Some(row) = rows.next().unwrap() { + row_count += 1; + let tx: i64 = row.get(0).unwrap(); + let output_pool: i64 = row.get(1).unwrap(); + let output_index: i64 = row.get(2).unwrap(); + let from_account: Option = row.get(3).unwrap(); + let to_account: Option = row.get(4).unwrap(); + let to_address: Option = row.get(5).unwrap(); + let value: i64 = row.get(6).unwrap(); + let is_change: bool = row.get(7).unwrap(); + match (output_pool, output_index) { + (0, 0) => { + assert_eq!(from_account, None); + assert_eq!(to_account, Some(0)); + assert_eq!(to_address, Some("taddr_tx3".to_string())); + assert_eq!(value, 1); + assert!(!is_change); + } + other => { + panic!( + "Unexpected output pool and index for tx {}: {:?}.", + tx, other + ); + } + } + } + assert_eq!(row_count, 1); + } + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/v_transactions_note_uniqueness.rs b/zcash_client_sqlite/src/wallet/init/migrations/v_transactions_note_uniqueness.rs new file mode 100644 index 0000000000..d5770e5124 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/v_transactions_note_uniqueness.rs @@ -0,0 +1,256 @@ +//! This migration fixes a bug in `v_transactions` where distinct but otherwise identical notes +//! were being incorrectly deduplicated. + +use std::collections::HashSet; + +use schemerz_rusqlite::RusqliteMigration; +use uuid::Uuid; + +use crate::wallet::init::WalletMigrationError; + +use super::v_transactions_shielding_balance; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0xdba47c86_13b5_4601_94b2_0cde0abe1e45); + +const DEPENDENCIES: &[Uuid] = &[v_transactions_shielding_balance::MIGRATION_ID]; + +pub(super) struct Migration; + +impl schemerz::Migration for Migration { + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + DEPENDENCIES.iter().copied().collect() + } + + fn description(&self) -> &'static str { + "Fixes a bug in v_transactions that was omitting value from identically-valued notes." + } +} + +impl RusqliteMigration for Migration { + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), Self::Error> { + transaction.execute_batch( + "DROP VIEW v_transactions; + CREATE VIEW v_transactions AS + WITH + notes AS ( + SELECT sapling_received_notes.id_note AS id, + sapling_received_notes.account AS account_id, + transactions.block AS block, + transactions.txid AS txid, + 2 AS pool, + sapling_received_notes.value AS value, + CASE + WHEN sapling_received_notes.is_change THEN 1 + ELSE 0 + END AS is_change, + CASE + WHEN sapling_received_notes.is_change THEN 0 + ELSE 1 + END AS received_count, + CASE + WHEN (sapling_received_notes.memo IS NULL OR sapling_received_notes.memo = X'F6') + THEN 0 + ELSE 1 + END AS memo_present + FROM sapling_received_notes + JOIN transactions + ON transactions.id_tx = sapling_received_notes.tx + UNION + SELECT utxos.id_utxo AS id, + utxos.received_by_account AS account_id, + utxos.height AS block, + utxos.prevout_txid AS txid, + 0 AS pool, + utxos.value_zat AS value, + 0 AS is_change, + 1 AS received_count, + 0 AS memo_present + FROM utxos + UNION + SELECT sapling_received_notes.id_note AS id, + sapling_received_notes.account AS account_id, + transactions.block AS block, + transactions.txid AS txid, + 2 AS pool, + -sapling_received_notes.value AS value, + 0 AS is_change, + 0 AS received_count, + 0 AS memo_present + FROM sapling_received_notes + JOIN transactions + ON transactions.id_tx = sapling_received_notes.spent + UNION + SELECT utxos.id_utxo AS id, + utxos.received_by_account AS account_id, + transactions.block AS block, + transactions.txid AS txid, + 0 AS pool, + -utxos.value_zat AS value, + 0 AS is_change, + 0 AS received_count, + 0 AS memo_present + FROM utxos + JOIN transactions + ON transactions.id_tx = utxos.spent_in_tx + ), + sent_note_counts AS ( + SELECT sent_notes.from_account AS account_id, + transactions.txid AS txid, + COUNT(DISTINCT sent_notes.id_note) as sent_notes, + SUM( + CASE + WHEN (sent_notes.memo IS NULL OR sent_notes.memo = X'F6' OR sapling_received_notes.tx IS NOT NULL) + THEN 0 + ELSE 1 + END + ) AS memo_count + FROM sent_notes + JOIN transactions + ON transactions.id_tx = sent_notes.tx + LEFT JOIN sapling_received_notes + ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) = + (sapling_received_notes.tx, 2, sapling_received_notes.output_index) + WHERE COALESCE(sapling_received_notes.is_change, 0) = 0 + GROUP BY account_id, txid + ), + blocks_max_height AS ( + SELECT MAX(blocks.height) as max_height FROM blocks + ) + SELECT notes.account_id AS account_id, + notes.block AS mined_height, + notes.txid AS txid, + transactions.tx_index AS tx_index, + transactions.expiry_height AS expiry_height, + transactions.raw AS raw, + SUM(notes.value) AS account_balance_delta, + transactions.fee AS fee_paid, + SUM(notes.is_change) > 0 AS has_change, + MAX(COALESCE(sent_note_counts.sent_notes, 0)) AS sent_note_count, + SUM(notes.received_count) AS received_note_count, + SUM(notes.memo_present) + MAX(COALESCE(sent_note_counts.memo_count, 0)) AS memo_count, + blocks.time AS block_time, + ( + blocks.height IS NULL + AND transactions.expiry_height BETWEEN 1 AND blocks_max_height.max_height + ) AS expired_unmined + FROM notes + LEFT JOIN transactions + ON notes.txid = transactions.txid + JOIN blocks_max_height + LEFT JOIN blocks ON blocks.height = notes.block + LEFT JOIN sent_note_counts + ON sent_note_counts.account_id = notes.account_id + AND sent_note_counts.txid = notes.txid + GROUP BY notes.account_id, notes.txid;" + )?; + + Ok(()) + } + + fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), Self::Error> { + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) + } +} + +#[cfg(test)] +mod tests { + use rusqlite::{self, params}; + use tempfile::NamedTempFile; + + use zcash_keys::keys::UnifiedSpendingKey; + use zcash_protocol::consensus::Network; + use zip32::AccountId; + + use crate::{ + wallet::init::{init_wallet_db_internal, migrations::v_transactions_net}, + WalletDb, + }; + + #[test] + fn v_transactions_note_uniqueness_migration() { + let data_file = NamedTempFile::new().unwrap(); + let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap(); + init_wallet_db_internal( + &mut db_data, + None, + &[v_transactions_net::MIGRATION_ID], + false, + ) + .unwrap(); + + // Create an account in the wallet + let usk0 = UnifiedSpendingKey::from_seed(&db_data.params, &[0u8; 32][..], AccountId::ZERO) + .unwrap(); + let ufvk0 = usk0.to_unified_full_viewing_key(); + db_data + .conn + .execute( + "INSERT INTO accounts (account, ufvk) VALUES (0, ?)", + params![ufvk0.encode(&db_data.params)], + ) + .unwrap(); + + // Tx 0 contains two received notes, both of 2 zatoshis, that are controlled by account 0. + db_data.conn.execute_batch( + "INSERT INTO blocks (height, hash, time, sapling_tree) VALUES (0, 0, 0, x'00'); + INSERT INTO transactions (block, id_tx, txid) VALUES (0, 0, 'tx0'); + + INSERT INTO received_notes (tx, output_index, account, diversifier, value, rcm, nf, is_change) + VALUES (0, 0, 0, '', 2, '', 'nf_a', false); + INSERT INTO received_notes (tx, output_index, account, diversifier, value, rcm, nf, is_change) + VALUES (0, 3, 0, '', 2, '', 'nf_b', false);").unwrap(); + + let check_balance_delta = |db_data: &mut WalletDb, + expected_notes: i64| { + let mut q = db_data + .conn + .prepare( + "SELECT account_id, account_balance_delta, has_change, memo_count, sent_note_count, received_note_count + FROM v_transactions", + ) + .unwrap(); + let mut rows = q.query([]).unwrap(); + let mut row_count = 0; + while let Some(row) = rows.next().unwrap() { + row_count += 1; + let account: i64 = row.get(0).unwrap(); + let account_balance_delta: i64 = row.get(1).unwrap(); + let has_change: bool = row.get(2).unwrap(); + let memo_count: i64 = row.get(3).unwrap(); + let sent_note_count: i64 = row.get(4).unwrap(); + let received_note_count: i64 = row.get(5).unwrap(); + match account { + 0 => { + assert_eq!(account_balance_delta, 2 * expected_notes); + assert!(!has_change); + assert_eq!(memo_count, 0); + assert_eq!(sent_note_count, 0); + assert_eq!(received_note_count, expected_notes); + } + other => { + panic!( + "Account {:?} is not expected to exist in the wallet.", + other + ); + } + } + } + assert_eq!(row_count, 1); + }; + + // Check for the bug (#1020). + check_balance_delta(&mut db_data, 1); + + // Apply the current migration. + init_wallet_db_internal(&mut db_data, None, &[super::MIGRATION_ID], false).unwrap(); + + // Now it should be correct. + check_balance_delta(&mut db_data, 2); + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/v_transactions_shielding_balance.rs b/zcash_client_sqlite/src/wallet/init/migrations/v_transactions_shielding_balance.rs new file mode 100644 index 0000000000..94e8d54218 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/v_transactions_shielding_balance.rs @@ -0,0 +1,165 @@ +//! This migration reworks transaction history views to correctly include spent transparent utxo +//! value. + +use std::collections::HashSet; + +use schemerz_rusqlite::RusqliteMigration; +use uuid::Uuid; + +use crate::wallet::init::WalletMigrationError; + +use super::v_tx_outputs_use_legacy_false; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0xb8fe5112_4365_473c_8b42_2b07c0f0adaf); + +const DEPENDENCIES: &[Uuid] = &[v_tx_outputs_use_legacy_false::MIGRATION_ID]; + +pub(super) struct Migration; + +impl schemerz::Migration for Migration { + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + DEPENDENCIES.iter().copied().collect() + } + + fn description(&self) -> &'static str { + "Updates v_transactions to include spent UTXOs." + } +} + +impl RusqliteMigration for Migration { + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), Self::Error> { + transaction.execute_batch( + "DROP VIEW v_transactions; + CREATE VIEW v_transactions AS + WITH + notes AS ( + SELECT sapling_received_notes.account AS account_id, + transactions.block AS block, + transactions.txid AS txid, + 2 AS pool, + sapling_received_notes.value AS value, + CASE + WHEN sapling_received_notes.is_change THEN 1 + ELSE 0 + END AS is_change, + CASE + WHEN sapling_received_notes.is_change THEN 0 + ELSE 1 + END AS received_count, + CASE + WHEN (sapling_received_notes.memo IS NULL OR sapling_received_notes.memo = X'F6') + THEN 0 + ELSE 1 + END AS memo_present + FROM sapling_received_notes + JOIN transactions + ON transactions.id_tx = sapling_received_notes.tx + UNION + SELECT utxos.received_by_account AS account_id, + utxos.height AS block, + utxos.prevout_txid AS txid, + 0 AS pool, + utxos.value_zat AS value, + 0 AS is_change, + 1 AS received_count, + 0 AS memo_present + FROM utxos + UNION + SELECT sapling_received_notes.account AS account_id, + transactions.block AS block, + transactions.txid AS txid, + 2 AS pool, + -sapling_received_notes.value AS value, + 0 AS is_change, + 0 AS received_count, + 0 AS memo_present + FROM sapling_received_notes + JOIN transactions + ON transactions.id_tx = sapling_received_notes.spent + UNION + SELECT utxos.received_by_account AS account_id, + transactions.block AS block, + transactions.txid AS txid, + 0 AS pool, + -utxos.value_zat AS value, + 0 AS is_change, + 0 AS received_count, + 0 AS memo_present + FROM utxos + JOIN transactions + ON transactions.id_tx = utxos.spent_in_tx + ), + sent_note_counts AS ( + SELECT sent_notes.from_account AS account_id, + transactions.txid AS txid, + COUNT(DISTINCT sent_notes.id_note) as sent_notes, + SUM( + CASE + WHEN (sent_notes.memo IS NULL OR sent_notes.memo = X'F6' OR sapling_received_notes.tx IS NOT NULL) + THEN 0 + ELSE 1 + END + ) AS memo_count + FROM sent_notes + JOIN transactions + ON transactions.id_tx = sent_notes.tx + LEFT JOIN sapling_received_notes + ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) = + (sapling_received_notes.tx, 2, sapling_received_notes.output_index) + WHERE COALESCE(sapling_received_notes.is_change, 0) = 0 + GROUP BY account_id, txid + ), + blocks_max_height AS ( + SELECT MAX(blocks.height) as max_height FROM blocks + ) + SELECT notes.account_id AS account_id, + notes.block AS mined_height, + notes.txid AS txid, + transactions.tx_index AS tx_index, + transactions.expiry_height AS expiry_height, + transactions.raw AS raw, + SUM(notes.value) AS account_balance_delta, + transactions.fee AS fee_paid, + SUM(notes.is_change) > 0 AS has_change, + MAX(COALESCE(sent_note_counts.sent_notes, 0)) AS sent_note_count, + SUM(notes.received_count) AS received_note_count, + SUM(notes.memo_present) + MAX(COALESCE(sent_note_counts.memo_count, 0)) AS memo_count, + blocks.time AS block_time, + ( + blocks.height IS NULL + AND transactions.expiry_height BETWEEN 1 AND blocks_max_height.max_height + ) AS expired_unmined + FROM notes + LEFT JOIN transactions + ON notes.txid = transactions.txid + JOIN blocks_max_height + LEFT JOIN blocks ON blocks.height = notes.block + LEFT JOIN sent_note_counts + ON sent_note_counts.account_id = notes.account_id + AND sent_note_counts.txid = notes.txid + GROUP BY notes.account_id, notes.txid;" + )?; + + Ok(()) + } + + fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), Self::Error> { + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) + } +} + +#[cfg(test)] +mod tests { + use crate::wallet::init::migrations::tests::test_migrate; + + #[test] + fn migrate() { + test_migrate(&[super::MIGRATION_ID]); + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/v_transactions_transparent_history.rs b/zcash_client_sqlite/src/wallet/init/migrations/v_transactions_transparent_history.rs new file mode 100644 index 0000000000..196c84cd82 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/v_transactions_transparent_history.rs @@ -0,0 +1,201 @@ +//! This migration reworks transaction history views to correctly include history +//! of transparent utxos for which we lack complete transaction information. + +use std::collections::HashSet; + +use schemerz_rusqlite::RusqliteMigration; +use uuid::Uuid; + +use crate::wallet::init::WalletMigrationError; + +use super::sapling_memo_consistency; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0xaa0a4168_b41b_44c5_a47d_c4c66603cfab); + +const DEPENDENCIES: &[Uuid] = &[sapling_memo_consistency::MIGRATION_ID]; + +pub(super) struct Migration; + +impl schemerz::Migration for Migration { + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + DEPENDENCIES.iter().copied().collect() + } + + fn description(&self) -> &'static str { + "Updates transaction history views to fix potential errors in transparent history." + } +} + +impl RusqliteMigration for Migration { + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), Self::Error> { + transaction.execute_batch( + "DROP VIEW v_transactions; + CREATE VIEW v_transactions AS + WITH + notes AS ( + SELECT sapling_received_notes.account AS account_id, + transactions.block AS block, + transactions.txid AS txid, + 2 AS pool, + sapling_received_notes.value AS value, + CASE + WHEN sapling_received_notes.is_change THEN 1 + ELSE 0 + END AS is_change, + CASE + WHEN sapling_received_notes.is_change THEN 0 + ELSE 1 + END AS received_count, + CASE + WHEN (sapling_received_notes.memo IS NULL OR sapling_received_notes.memo = X'F6') + THEN 0 + ELSE 1 + END AS memo_present + FROM sapling_received_notes + JOIN transactions + ON transactions.id_tx = sapling_received_notes.tx + UNION + SELECT utxos.received_by_account AS account_id, + utxos.height AS block, + utxos.prevout_txid AS txid, + 0 AS pool, + utxos.value_zat AS value, + 0 AS is_change, + 1 AS received_count, + 0 AS memo_present + FROM utxos + UNION + SELECT sapling_received_notes.account AS account_id, + transactions.block AS block, + transactions.txid AS txid, + 2 AS pool, + -sapling_received_notes.value AS value, + 0 AS is_change, + 0 AS received_count, + 0 AS memo_present + FROM sapling_received_notes + JOIN transactions + ON transactions.id_tx = sapling_received_notes.spent + ), + sent_note_counts AS ( + SELECT sent_notes.from_account AS account_id, + transactions.txid AS txid, + COUNT(DISTINCT sent_notes.id_note) as sent_notes, + SUM( + CASE + WHEN (sent_notes.memo IS NULL OR sent_notes.memo = X'F6' OR sapling_received_notes.tx IS NOT NULL) + THEN 0 + ELSE 1 + END + ) AS memo_count + FROM sent_notes + JOIN transactions + ON transactions.id_tx = sent_notes.tx + LEFT JOIN sapling_received_notes + ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) = + (sapling_received_notes.tx, 2, sapling_received_notes.output_index) + WHERE COALESCE(sapling_received_notes.is_change, 0) = 0 + GROUP BY account_id, txid + ), + blocks_max_height AS ( + SELECT MAX(blocks.height) as max_height FROM blocks + ) + SELECT notes.account_id AS account_id, + notes.block AS mined_height, + notes.txid AS txid, + transactions.tx_index AS tx_index, + transactions.expiry_height AS expiry_height, + transactions.raw AS raw, + SUM(notes.value) AS account_balance_delta, + transactions.fee AS fee_paid, + SUM(notes.is_change) > 0 AS has_change, + MAX(COALESCE(sent_note_counts.sent_notes, 0)) AS sent_note_count, + SUM(notes.received_count) AS received_note_count, + SUM(notes.memo_present) + MAX(COALESCE(sent_note_counts.memo_count, 0)) AS memo_count, + blocks.time AS block_time, + ( + blocks.height IS NULL + AND transactions.expiry_height BETWEEN 1 AND blocks_max_height.max_height + ) AS expired_unmined + FROM notes + LEFT JOIN transactions + ON notes.txid = transactions.txid + JOIN blocks_max_height + LEFT JOIN blocks ON blocks.height = notes.block + LEFT JOIN sent_note_counts + ON sent_note_counts.account_id = notes.account_id + AND sent_note_counts.txid = notes.txid + GROUP BY notes.account_id, notes.txid;" + )?; + + transaction.execute_batch( + "DROP VIEW v_tx_outputs; + CREATE VIEW v_tx_outputs AS + SELECT transactions.txid AS txid, + 2 AS output_pool, + sapling_received_notes.output_index AS output_index, + sent_notes.from_account AS from_account, + sapling_received_notes.account AS to_account, + NULL AS to_address, + sapling_received_notes.value AS value, + sapling_received_notes.is_change AS is_change, + sapling_received_notes.memo AS memo + FROM sapling_received_notes + JOIN transactions + ON transactions.id_tx = sapling_received_notes.tx + LEFT JOIN sent_notes + ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) = + (sapling_received_notes.tx, 2, sent_notes.output_index) + UNION + SELECT utxos.prevout_txid AS txid, + 0 AS output_pool, + utxos.prevout_idx AS output_index, + NULL AS from_account, + utxos.received_by_account AS to_account, + utxos.address AS to_address, + utxos.value_zat AS value, + false AS is_change, + NULL AS memo + FROM utxos + UNION + SELECT transactions.txid AS txid, + sent_notes.output_pool AS output_pool, + sent_notes.output_index AS output_index, + sent_notes.from_account AS from_account, + sapling_received_notes.account AS to_account, + sent_notes.to_address AS to_address, + sent_notes.value AS value, + false AS is_change, + sent_notes.memo AS memo + FROM sent_notes + JOIN transactions + ON transactions.id_tx = sent_notes.tx + LEFT JOIN sapling_received_notes + ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) = + (sapling_received_notes.tx, 2, sapling_received_notes.output_index) + WHERE COALESCE(sapling_received_notes.is_change, 0) = 0;", + )?; + + Ok(()) + } + + fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), Self::Error> { + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) + } +} + +#[cfg(test)] +mod tests { + use crate::wallet::init::migrations::tests::test_migrate; + + #[test] + fn migrate() { + test_migrate(&[super::MIGRATION_ID]); + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/v_tx_outputs_use_legacy_false.rs b/zcash_client_sqlite/src/wallet/init/migrations/v_tx_outputs_use_legacy_false.rs new file mode 100644 index 0000000000..be25f13617 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/v_tx_outputs_use_legacy_false.rs @@ -0,0 +1,102 @@ +//! This migration revises the `v_tx_outputs` view to support SQLite 3.19.x +//! which did not define `TRUE` and `FALSE` constants. This is required in +//! order to support Android API 27 + +use std::collections::HashSet; + +use schemerz_rusqlite::RusqliteMigration; +use uuid::Uuid; + +use crate::wallet::init::WalletMigrationError; + +use super::v_transactions_transparent_history; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0xb3e21434_286f_41f3_8d71_44cce968ab2b); + +const DEPENDENCIES: &[Uuid] = &[v_transactions_transparent_history::MIGRATION_ID]; + +pub(super) struct Migration; + +impl schemerz::Migration for Migration { + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + DEPENDENCIES.iter().copied().collect() + } + + fn description(&self) -> &'static str { + "Updates v_tx_outputs to remove use of `true` and `false` constants for legacy SQLite version support." + } +} + +impl RusqliteMigration for Migration { + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), Self::Error> { + transaction.execute_batch( + "DROP VIEW v_tx_outputs; + CREATE VIEW v_tx_outputs AS + SELECT transactions.txid AS txid, + 2 AS output_pool, + sapling_received_notes.output_index AS output_index, + sent_notes.from_account AS from_account, + sapling_received_notes.account AS to_account, + NULL AS to_address, + sapling_received_notes.value AS value, + sapling_received_notes.is_change AS is_change, + sapling_received_notes.memo AS memo + FROM sapling_received_notes + JOIN transactions + ON transactions.id_tx = sapling_received_notes.tx + LEFT JOIN sent_notes + ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) = + (sapling_received_notes.tx, 2, sent_notes.output_index) + UNION + SELECT utxos.prevout_txid AS txid, + 0 AS output_pool, + utxos.prevout_idx AS output_index, + NULL AS from_account, + utxos.received_by_account AS to_account, + utxos.address AS to_address, + utxos.value_zat AS value, + 0 AS is_change, + NULL AS memo + FROM utxos + UNION + SELECT transactions.txid AS txid, + sent_notes.output_pool AS output_pool, + sent_notes.output_index AS output_index, + sent_notes.from_account AS from_account, + sapling_received_notes.account AS to_account, + sent_notes.to_address AS to_address, + sent_notes.value AS value, + 0 AS is_change, + sent_notes.memo AS memo + FROM sent_notes + JOIN transactions + ON transactions.id_tx = sent_notes.tx + LEFT JOIN sapling_received_notes + ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) = + (sapling_received_notes.tx, 2, sapling_received_notes.output_index) + WHERE COALESCE(sapling_received_notes.is_change, 0) = 0;", + )?; + + Ok(()) + } + + fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), Self::Error> { + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) + } +} + +#[cfg(test)] +mod tests { + use crate::wallet::init::migrations::tests::test_migrate; + + #[test] + fn migrate() { + test_migrate(&[super::MIGRATION_ID]); + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/wallet_summaries.rs b/zcash_client_sqlite/src/wallet/init/migrations/wallet_summaries.rs new file mode 100644 index 0000000000..e6b73a74ed --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/wallet_summaries.rs @@ -0,0 +1,99 @@ +//! This migration adds views and database changes required to provide accurate wallet summaries. + +use std::collections::HashSet; + +use schemerz_rusqlite::RusqliteMigration; +use uuid::Uuid; + +use crate::wallet::init::WalletMigrationError; + +use super::v_sapling_shard_unscanned_ranges; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0xc5bf7f71_2297_41ff_89e1_75e07c4e8838); + +const DEPENDENCIES: &[Uuid] = &[v_sapling_shard_unscanned_ranges::MIGRATION_ID]; + +pub(super) struct Migration; + +impl schemerz::Migration for Migration { + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + DEPENDENCIES.iter().copied().collect() + } + + fn description(&self) -> &'static str { + "Adds views and data required to produce accurate wallet summaries." + } +} + +impl RusqliteMigration for Migration { + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), Self::Error> { + // Add columns to the `blocks` table to track the number of scanned outputs in each block. + // We use the note commitment tree size information that we have in contiguous regions to + // populate this data, but we don't make any attempt to handle the boundary cases because + // we're just using this information for the progress metric, which can be a bit sloppy. + transaction.execute_batch( + "ALTER TABLE blocks ADD COLUMN sapling_output_count INTEGER; + ALTER TABLE blocks ADD COLUMN orchard_action_count INTEGER;", + )?; + + transaction.execute_batch( + // set the number of outputs everywhere that we have sequential blocks + "CREATE TEMPORARY TABLE block_deltas AS + SELECT + cur.height AS height, + (cur.sapling_commitment_tree_size - prev.sapling_commitment_tree_size) AS sapling_delta, + (cur.orchard_commitment_tree_size - prev.orchard_commitment_tree_size) AS orchard_delta + FROM blocks cur + INNER JOIN blocks prev + ON cur.height = prev.height + 1; + + UPDATE blocks + SET sapling_output_count = block_deltas.sapling_delta, + orchard_action_count = block_deltas.orchard_delta + FROM block_deltas + WHERE block_deltas.height = blocks.height;" + )?; + + transaction.execute_batch( + "CREATE VIEW v_sapling_shards_scan_state AS + SELECT + shard_index, + start_position, + end_position_exclusive, + subtree_start_height, + subtree_end_height, + contains_marked, + MAX(priority) AS max_priority + FROM v_sapling_shard_scan_ranges + GROUP BY + shard_index, + start_position, + end_position_exclusive, + subtree_start_height, + subtree_end_height, + contains_marked;", + )?; + + Ok(()) + } + + fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), Self::Error> { + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) + } +} + +#[cfg(test)] +mod tests { + use crate::wallet::init::migrations::tests::test_migrate; + + #[test] + fn migrate() { + test_migrate(&[super::MIGRATION_ID]); + } +} diff --git a/zcash_client_sqlite/src/wallet/orchard.rs b/zcash_client_sqlite/src/wallet/orchard.rs new file mode 100644 index 0000000000..41f9572020 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/orchard.rs @@ -0,0 +1,542 @@ +use std::{collections::HashSet, rc::Rc}; + +use incrementalmerkletree::Position; +use orchard::{ + keys::Diversifier, + note::{Note, Nullifier, RandomSeed, Rho}, +}; +use rusqlite::{named_params, types::Value, Connection, Row, Transaction}; + +use zcash_client_backend::{ + data_api::NullifierQuery, + wallet::{ReceivedNote, WalletOrchardOutput}, + DecryptedOutput, TransferType, +}; +use zcash_keys::keys::UnifiedFullViewingKey; +use zcash_primitives::transaction::TxId; +use zcash_protocol::{ + consensus::{self, BlockHeight}, + memo::MemoBytes, + value::Zatoshis, + ShieldedProtocol, +}; +use zip32::Scope; + +use crate::{error::SqliteClientError, AccountUuid, ReceivedNoteId, TxRef}; + +use super::{get_account_ref, memo_repr, parse_scope, scope_code}; + +/// This trait provides a generalization over shielded output representations. +pub(crate) trait ReceivedOrchardOutput { + type AccountId; + + fn index(&self) -> usize; + fn account_id(&self) -> Self::AccountId; + fn note(&self) -> &Note; + fn memo(&self) -> Option<&MemoBytes>; + fn is_change(&self) -> bool; + fn nullifier(&self) -> Option<&Nullifier>; + fn note_commitment_tree_position(&self) -> Option; + fn recipient_key_scope(&self) -> Option; +} + +impl ReceivedOrchardOutput for WalletOrchardOutput { + type AccountId = AccountId; + + fn index(&self) -> usize { + self.index() + } + fn account_id(&self) -> Self::AccountId { + *WalletOrchardOutput::account_id(self) + } + fn note(&self) -> &Note { + WalletOrchardOutput::note(self) + } + fn memo(&self) -> Option<&MemoBytes> { + None + } + fn is_change(&self) -> bool { + WalletOrchardOutput::is_change(self) + } + fn nullifier(&self) -> Option<&Nullifier> { + self.nf() + } + fn note_commitment_tree_position(&self) -> Option { + Some(WalletOrchardOutput::note_commitment_tree_position(self)) + } + fn recipient_key_scope(&self) -> Option { + self.recipient_key_scope() + } +} + +impl ReceivedOrchardOutput for DecryptedOutput { + type AccountId = AccountId; + + fn index(&self) -> usize { + self.index() + } + fn account_id(&self) -> Self::AccountId { + *self.account() + } + fn note(&self) -> &orchard::note::Note { + self.note() + } + fn memo(&self) -> Option<&MemoBytes> { + Some(self.memo()) + } + fn is_change(&self) -> bool { + self.transfer_type() == TransferType::WalletInternal + } + fn nullifier(&self) -> Option<&Nullifier> { + None + } + fn note_commitment_tree_position(&self) -> Option { + None + } + fn recipient_key_scope(&self) -> Option { + if self.transfer_type() == TransferType::WalletInternal { + Some(Scope::Internal) + } else { + Some(Scope::External) + } + } +} + +fn to_spendable_note( + params: &P, + row: &Row, +) -> Result>, SqliteClientError> { + let note_id = ReceivedNoteId(ShieldedProtocol::Orchard, row.get("id")?); + let txid = row.get::<_, [u8; 32]>("txid").map(TxId::from_bytes)?; + let action_index = row.get("action_index")?; + let diversifier = { + let d: Vec<_> = row.get("diversifier")?; + if d.len() != 11 { + return Err(SqliteClientError::CorruptedData( + "Invalid diversifier length".to_string(), + )); + } + let mut tmp = [0; 11]; + tmp.copy_from_slice(&d); + Diversifier::from_bytes(tmp) + }; + + let note_value: u64 = row.get::<_, i64>("value")?.try_into().map_err(|_e| { + SqliteClientError::CorruptedData("Note values must be nonnegative".to_string()) + })?; + + let rho = { + let rho_bytes: [u8; 32] = row.get("rho")?; + Option::from(Rho::from_bytes(&rho_bytes)) + .ok_or_else(|| SqliteClientError::CorruptedData("Invalid rho.".to_string())) + }?; + + let rseed = { + let rseed_bytes: [u8; 32] = row.get("rseed")?; + Option::from(RandomSeed::from_bytes(rseed_bytes, &rho)).ok_or_else(|| { + SqliteClientError::CorruptedData("Invalid Orchard random seed.".to_string()) + }) + }?; + + let note_commitment_tree_position = Position::from( + u64::try_from(row.get::<_, i64>("commitment_tree_position")?).map_err(|_| { + SqliteClientError::CorruptedData("Note commitment tree position invalid.".to_string()) + })?, + ); + + let ufvk_str: Option = row.get("ufvk")?; + let scope_code: Option = row.get("recipient_key_scope")?; + + // If we don't have information about the recipient key scope or the ufvk we can't determine + // which spending key to use. This may be because the received note was associated with an + // imported viewing key, so we treat such notes as not spendable. Although this method is + // presently only called using the results of queries where both the ufvk and + // recipient_key_scope columns are checked to be non-null, this is method is written + // defensively to account for the fact that both of these are nullable columns in case it + // is used elsewhere in the future. + ufvk_str + .zip(scope_code) + .map(|(ufvk_str, scope_code)| { + let ufvk = UnifiedFullViewingKey::decode(params, &ufvk_str) + .map_err(SqliteClientError::CorruptedData)?; + + let spending_key_scope = parse_scope(scope_code).ok_or_else(|| { + SqliteClientError::CorruptedData(format!("Invalid key scope code {}", scope_code)) + })?; + let recipient = ufvk + .orchard() + .map(|fvk| fvk.to_ivk(spending_key_scope).address(diversifier)) + .ok_or_else(|| { + SqliteClientError::CorruptedData("Diversifier invalid.".to_owned()) + })?; + + let note = Option::from(Note::from_parts( + recipient, + orchard::value::NoteValue::from_raw(note_value), + rho, + rseed, + )) + .ok_or_else(|| SqliteClientError::CorruptedData("Invalid Orchard note.".to_string()))?; + + Ok(ReceivedNote::from_parts( + note_id, + txid, + action_index, + note, + spending_key_scope, + note_commitment_tree_position, + )) + }) + .transpose() +} + +pub(crate) fn get_spendable_orchard_note( + conn: &Connection, + params: &P, + txid: &TxId, + index: u32, +) -> Result>, SqliteClientError> { + super::common::get_spendable_note( + conn, + params, + txid, + index, + ShieldedProtocol::Orchard, + to_spendable_note, + ) +} + +pub(crate) fn select_spendable_orchard_notes( + conn: &Connection, + params: &P, + account: AccountUuid, + target_value: Zatoshis, + anchor_height: BlockHeight, + exclude: &[ReceivedNoteId], +) -> Result>, SqliteClientError> { + super::common::select_spendable_notes( + conn, + params, + account, + target_value, + anchor_height, + exclude, + ShieldedProtocol::Orchard, + to_spendable_note, + ) +} + +/// Records the specified shielded output as having been received. +/// +/// This implementation relies on the facts that: +/// - A transaction will not contain more than 2^63 shielded outputs. +/// - A note value will never exceed 2^63 zatoshis. +pub(crate) fn put_received_note>( + conn: &Transaction, + output: &T, + tx_ref: TxRef, + spent_in: Option, +) -> Result<(), SqliteClientError> { + let account_id = get_account_ref(conn, output.account_id())?; + let mut stmt_upsert_received_note = conn.prepare_cached( + "INSERT INTO orchard_received_notes + ( + tx, action_index, account_id, + diversifier, value, rho, rseed, memo, nf, + is_change, commitment_tree_position, + recipient_key_scope + ) + VALUES ( + :tx, :action_index, :account_id, + :diversifier, :value, :rho, :rseed, :memo, :nf, + :is_change, :commitment_tree_position, + :recipient_key_scope + ) + ON CONFLICT (tx, action_index) DO UPDATE + SET account_id = :account_id, + diversifier = :diversifier, + value = :value, + rho = :rho, + rseed = :rseed, + nf = IFNULL(:nf, nf), + memo = IFNULL(:memo, memo), + is_change = MAX(:is_change, is_change), + commitment_tree_position = IFNULL(:commitment_tree_position, commitment_tree_position), + recipient_key_scope = :recipient_key_scope + RETURNING orchard_received_notes.id", + )?; + + let rseed = output.note().rseed(); + let to = output.note().recipient(); + let diversifier = to.diversifier(); + + let sql_args = named_params![ + ":tx": tx_ref.0, + ":action_index": i64::try_from(output.index()).expect("output indices are representable as i64"), + ":account_id": account_id.0, + ":diversifier": diversifier.as_array(), + ":value": output.note().value().inner(), + ":rho": output.note().rho().to_bytes(), + ":rseed": &rseed.as_bytes(), + ":nf": output.nullifier().map(|nf| nf.to_bytes()), + ":memo": memo_repr(output.memo()), + ":is_change": output.is_change(), + ":commitment_tree_position": output.note_commitment_tree_position().map(u64::from), + ":recipient_key_scope": output.recipient_key_scope().map(scope_code), + ]; + + let received_note_id = stmt_upsert_received_note + .query_row(sql_args, |row| row.get::<_, i64>(0)) + .map_err(SqliteClientError::from)?; + + if let Some(spent_in) = spent_in { + conn.execute( + "INSERT INTO orchard_received_note_spends (orchard_received_note_id, transaction_id) + VALUES (:orchard_received_note_id, :transaction_id) + ON CONFLICT (orchard_received_note_id, transaction_id) DO NOTHING", + named_params![ + ":orchard_received_note_id": received_note_id, + ":transaction_id": spent_in.0 + ], + )?; + } + Ok(()) +} + +/// Retrieves the set of nullifiers for "potentially spendable" Orchard notes that the +/// wallet is tracking. +/// +/// "Potentially spendable" means: +/// - The transaction in which the note was created has been observed as mined. +/// - No transaction in which the note's nullifier appears has been observed as mined. +pub(crate) fn get_orchard_nullifiers( + conn: &Connection, + query: NullifierQuery, +) -> Result, SqliteClientError> { + // Get the nullifiers for the notes we are tracking + let mut stmt_fetch_nullifiers = match query { + NullifierQuery::Unspent => conn.prepare( + "SELECT a.uuid, rn.nf + FROM orchard_received_notes rn + JOIN accounts a ON a.id = rn.account_id + JOIN transactions tx ON tx.id_tx = rn.tx + WHERE rn.nf IS NOT NULL + AND tx.block IS NOT NULL + AND rn.id NOT IN ( + SELECT spends.orchard_received_note_id + FROM orchard_received_note_spends spends + JOIN transactions stx ON stx.id_tx = spends.transaction_id + WHERE stx.block IS NOT NULL -- the spending tx is mined + OR stx.expiry_height IS NULL -- the spending tx will not expire + )", + )?, + NullifierQuery::All => conn.prepare( + "SELECT a.uuid, rn.nf + FROM orchard_received_notes rn + JOIN accounts a ON a.id = rn.account_id + WHERE nf IS NOT NULL", + )?, + }; + + let nullifiers = stmt_fetch_nullifiers.query_and_then([], |row| { + let account = AccountUuid(row.get(0)?); + let nf_bytes: [u8; 32] = row.get(1)?; + Ok::<_, rusqlite::Error>((account, Nullifier::from_bytes(&nf_bytes).unwrap())) + })?; + + let res: Vec<_> = nullifiers.collect::>()?; + Ok(res) +} + +pub(crate) fn detect_spending_accounts<'a>( + conn: &Connection, + nfs: impl Iterator, +) -> Result, rusqlite::Error> { + let mut account_q = conn.prepare_cached( + "SELECT a.uuid + FROM orchard_received_notes rn + JOIN accounts a ON a.id = rn.account_id + WHERE rn.nf IN rarray(:nf_ptr)", + )?; + + let nf_values: Vec = nfs.map(|nf| Value::Blob(nf.to_bytes().to_vec())).collect(); + let nf_ptr = Rc::new(nf_values); + let res = account_q + .query_and_then(named_params![":nf_ptr": &nf_ptr], |row| { + row.get(0).map(AccountUuid) + })? + .collect::, _>>()?; + + Ok(res) +} + +/// Marks a given nullifier as having been revealed in the construction +/// of the specified transaction. +/// +/// Marking a note spent in this fashion does NOT imply that the +/// spending transaction has been mined. +pub(crate) fn mark_orchard_note_spent( + conn: &Connection, + tx_ref: TxRef, + nf: &Nullifier, +) -> Result { + let mut stmt_mark_orchard_note_spent = conn.prepare_cached( + "INSERT INTO orchard_received_note_spends (orchard_received_note_id, transaction_id) + SELECT id, :transaction_id FROM orchard_received_notes WHERE nf = :nf + ON CONFLICT (orchard_received_note_id, transaction_id) DO NOTHING", + )?; + + match stmt_mark_orchard_note_spent.execute(named_params![ + ":nf": nf.to_bytes(), + ":transaction_id": tx_ref.0 + ])? { + 0 => Ok(false), + 1 => Ok(true), + _ => unreachable!("nf column is marked as UNIQUE"), + } +} + +#[cfg(test)] +pub(crate) mod tests { + + use zcash_client_backend::data_api::testing::{ + orchard::OrchardPoolTester, sapling::SaplingPoolTester, + }; + + use crate::testing::{self}; + + #[test] + fn send_single_step_proposed_transfer() { + testing::pool::send_single_step_proposed_transfer::() + } + + #[test] + fn send_with_multiple_change_outputs() { + testing::pool::send_with_multiple_change_outputs::() + } + + #[test] + #[cfg(feature = "transparent-inputs")] + fn send_multi_step_proposed_transfer() { + testing::pool::send_multi_step_proposed_transfer::() + } + + #[test] + #[cfg(feature = "transparent-inputs")] + fn proposal_fails_if_not_all_ephemeral_outputs_consumed() { + testing::pool::proposal_fails_if_not_all_ephemeral_outputs_consumed::() + } + + #[test] + fn create_to_address_fails_on_incorrect_usk() { + testing::pool::create_to_address_fails_on_incorrect_usk::() + } + + #[test] + fn proposal_fails_with_no_blocks() { + testing::pool::proposal_fails_with_no_blocks::() + } + + #[test] + fn spend_fails_on_unverified_notes() { + testing::pool::spend_fails_on_unverified_notes::() + } + + #[test] + fn spend_fails_on_locked_notes() { + testing::pool::spend_fails_on_locked_notes::() + } + + #[test] + fn ovk_policy_prevents_recovery_from_chain() { + testing::pool::ovk_policy_prevents_recovery_from_chain::() + } + + #[test] + fn spend_succeeds_to_t_addr_zero_change() { + testing::pool::spend_succeeds_to_t_addr_zero_change::() + } + + #[test] + fn change_note_spends_succeed() { + testing::pool::change_note_spends_succeed::() + } + + #[test] + fn external_address_change_spends_detected_in_restore_from_seed() { + testing::pool::external_address_change_spends_detected_in_restore_from_seed::< + OrchardPoolTester, + >() + } + + #[test] + #[ignore] // FIXME: #1316 This requires support for dust outputs. + #[cfg(not(feature = "expensive-tests"))] + fn zip317_spend() { + testing::pool::zip317_spend::() + } + + #[test] + #[cfg(feature = "transparent-inputs")] + fn shield_transparent() { + testing::pool::shield_transparent::() + } + + #[test] + fn birthday_in_anchor_shard() { + testing::pool::birthday_in_anchor_shard::() + } + + #[test] + fn checkpoint_gaps() { + testing::pool::checkpoint_gaps::() + } + + #[test] + fn scan_cached_blocks_detects_spends_out_of_order() { + testing::pool::scan_cached_blocks_detects_spends_out_of_order::() + } + + #[test] + fn metadata_queries_exclude_unwanted_notes() { + testing::pool::metadata_queries_exclude_unwanted_notes::() + } + + #[test] + fn pool_crossing_required() { + testing::pool::pool_crossing_required::() + } + + #[test] + fn fully_funded_fully_private() { + testing::pool::fully_funded_fully_private::() + } + + #[test] + #[cfg(feature = "transparent-inputs")] + fn fully_funded_send_to_t() { + testing::pool::fully_funded_send_to_t::() + } + + #[test] + fn multi_pool_checkpoint() { + testing::pool::multi_pool_checkpoint::() + } + + #[test] + fn multi_pool_checkpoints_with_pruning() { + testing::pool::multi_pool_checkpoints_with_pruning::() + } + + #[cfg(feature = "pczt-tests")] + #[test] + fn pczt_single_step_orchard_only() { + testing::pool::pczt_single_step::() + } + + #[cfg(feature = "pczt-tests")] + #[test] + fn pczt_single_step_orchard_to_sapling() { + testing::pool::pczt_single_step::() + } +} diff --git a/zcash_client_sqlite/src/wallet/sapling.rs b/zcash_client_sqlite/src/wallet/sapling.rs new file mode 100644 index 0000000000..e676b6ada1 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/sapling.rs @@ -0,0 +1,560 @@ +//! Functions for Sapling support in the wallet. + +use std::{collections::HashSet, rc::Rc}; + +use group::ff::PrimeField; +use incrementalmerkletree::Position; +use rusqlite::{named_params, types::Value, Connection, Row, Transaction}; + +use sapling::{self, Diversifier, Nullifier, Rseed}; +use zcash_client_backend::{ + data_api::NullifierQuery, + wallet::{ReceivedNote, WalletSaplingOutput}, + DecryptedOutput, TransferType, +}; +use zcash_keys::keys::UnifiedFullViewingKey; +use zcash_primitives::transaction::TxId; +use zcash_protocol::{ + consensus::{self, BlockHeight}, + memo::MemoBytes, + value::Zatoshis, + ShieldedProtocol, +}; +use zip32::Scope; + +use crate::{error::SqliteClientError, AccountUuid, ReceivedNoteId, TxRef}; + +use super::{get_account_ref, memo_repr, parse_scope, scope_code}; + +/// This trait provides a generalization over shielded output representations. +pub(crate) trait ReceivedSaplingOutput { + type AccountId; + + fn index(&self) -> usize; + fn account_id(&self) -> Self::AccountId; + fn note(&self) -> &sapling::Note; + fn memo(&self) -> Option<&MemoBytes>; + fn is_change(&self) -> bool; + fn nullifier(&self) -> Option<&sapling::Nullifier>; + fn note_commitment_tree_position(&self) -> Option; + fn recipient_key_scope(&self) -> Option; +} + +impl ReceivedSaplingOutput for WalletSaplingOutput { + type AccountId = AccountId; + + fn index(&self) -> usize { + self.index() + } + fn account_id(&self) -> Self::AccountId { + *WalletSaplingOutput::account_id(self) + } + fn note(&self) -> &sapling::Note { + WalletSaplingOutput::note(self) + } + fn memo(&self) -> Option<&MemoBytes> { + None + } + fn is_change(&self) -> bool { + WalletSaplingOutput::is_change(self) + } + fn nullifier(&self) -> Option<&sapling::Nullifier> { + self.nf() + } + fn note_commitment_tree_position(&self) -> Option { + Some(WalletSaplingOutput::note_commitment_tree_position(self)) + } + fn recipient_key_scope(&self) -> Option { + self.recipient_key_scope() + } +} + +impl ReceivedSaplingOutput for DecryptedOutput { + type AccountId = AccountId; + + fn index(&self) -> usize { + self.index() + } + fn account_id(&self) -> Self::AccountId { + *self.account() + } + fn note(&self) -> &sapling::Note { + self.note() + } + fn memo(&self) -> Option<&MemoBytes> { + Some(self.memo()) + } + fn is_change(&self) -> bool { + self.transfer_type() == TransferType::WalletInternal + } + fn nullifier(&self) -> Option<&sapling::Nullifier> { + None + } + fn note_commitment_tree_position(&self) -> Option { + None + } + fn recipient_key_scope(&self) -> Option { + if self.transfer_type() == TransferType::WalletInternal { + Some(Scope::Internal) + } else { + Some(Scope::External) + } + } +} + +fn to_spendable_note( + params: &P, + row: &Row, +) -> Result>, SqliteClientError> { + let note_id = ReceivedNoteId(ShieldedProtocol::Sapling, row.get("id")?); + let txid = row.get::<_, [u8; 32]>("txid").map(TxId::from_bytes)?; + let output_index = row.get("output_index")?; + let diversifier = { + let d: Vec<_> = row.get("diversifier")?; + if d.len() != 11 { + return Err(SqliteClientError::CorruptedData( + "Invalid diversifier length".to_string(), + )); + } + let mut tmp = [0; 11]; + tmp.copy_from_slice(&d); + Diversifier(tmp) + }; + + let note_value: u64 = row.get::<_, i64>("value")?.try_into().map_err(|_e| { + SqliteClientError::CorruptedData("Note values must be nonnegative".to_string()) + })?; + + let rseed = { + let rcm_bytes: Vec<_> = row.get("rcm")?; + + // We store rcm directly in the data DB, regardless of whether the note + // used a v1 or v2 note plaintext, so for the purposes of spending let's + // pretend this is a pre-ZIP 212 note. + let rcm = Option::from(jubjub::Fr::from_repr( + rcm_bytes[..] + .try_into() + .map_err(|_| SqliteClientError::InvalidNote)?, + )) + .ok_or(SqliteClientError::InvalidNote)?; + Rseed::BeforeZip212(rcm) + }; + + let note_commitment_tree_position = Position::from( + u64::try_from(row.get::<_, i64>("commitment_tree_position")?).map_err(|_| { + SqliteClientError::CorruptedData("Note commitment tree position invalid.".to_string()) + })?, + ); + + let ufvk_str: Option = row.get("ufvk")?; + let scope_code: Option = row.get("recipient_key_scope")?; + + // If we don't have information about the recipient key scope or the ufvk we can't determine + // which spending key to use. This may be because the received note was associated with an + // imported viewing key, so we treat such notes as not spendable. Although this method is + // presently only called using the results of queries where both the ufvk and + // recipient_key_scope columns are checked to be non-null, this is method is written + // defensively to account for the fact that both of these are nullable columns in case it + // is used elsewhere in the future. + ufvk_str + .zip(scope_code) + .map(|(ufvk_str, scope_code)| { + let ufvk = UnifiedFullViewingKey::decode(params, &ufvk_str) + .map_err(SqliteClientError::CorruptedData)?; + + let spending_key_scope = parse_scope(scope_code).ok_or_else(|| { + SqliteClientError::CorruptedData(format!("Invalid key scope code {}", scope_code)) + })?; + + let recipient = match spending_key_scope { + Scope::Internal => ufvk + .sapling() + .and_then(|dfvk| dfvk.diversified_change_address(diversifier)), + Scope::External => ufvk + .sapling() + .and_then(|dfvk| dfvk.diversified_address(diversifier)), + } + .ok_or_else(|| SqliteClientError::CorruptedData("Diversifier invalid.".to_owned()))?; + + Ok(ReceivedNote::from_parts( + note_id, + txid, + output_index, + sapling::Note::from_parts( + recipient, + sapling::value::NoteValue::from_raw(note_value), + rseed, + ), + spending_key_scope, + note_commitment_tree_position, + )) + }) + .transpose() +} + +// The `clippy::let_and_return` lint is explicitly allowed here because a bug in Clippy +// (https://github.com/rust-lang/rust-clippy/issues/11308) means it fails to identify that the `result` temporary +// is required in order to resolve the borrows involved in the `query_and_then` call. +#[allow(clippy::let_and_return)] +pub(crate) fn get_spendable_sapling_note( + conn: &Connection, + params: &P, + txid: &TxId, + index: u32, +) -> Result>, SqliteClientError> { + super::common::get_spendable_note( + conn, + params, + txid, + index, + ShieldedProtocol::Sapling, + to_spendable_note, + ) +} + +/// Utility method for determining whether we have any spendable notes +/// +/// If the tip shard has unscanned ranges below the anchor height and greater than or equal to +/// the wallet birthday, none of our notes can be spent because we cannot construct witnesses at +/// the provided anchor height. +pub(crate) fn select_spendable_sapling_notes( + conn: &Connection, + params: &P, + account: AccountUuid, + target_value: Zatoshis, + anchor_height: BlockHeight, + exclude: &[ReceivedNoteId], +) -> Result>, SqliteClientError> { + super::common::select_spendable_notes( + conn, + params, + account, + target_value, + anchor_height, + exclude, + ShieldedProtocol::Sapling, + to_spendable_note, + ) +} + +/// Retrieves the set of nullifiers for "potentially spendable" Sapling notes that the +/// wallet is tracking. +/// +/// "Potentially spendable" means: +/// - The transaction in which the note was created has been observed as mined. +/// - No transaction in which the note's nullifier appears has been observed as mined. +pub(crate) fn get_sapling_nullifiers( + conn: &Connection, + query: NullifierQuery, +) -> Result, SqliteClientError> { + // Get the nullifiers for the notes we are tracking + let mut stmt_fetch_nullifiers = match query { + NullifierQuery::Unspent => conn.prepare( + "SELECT a.uuid, rn.nf + FROM sapling_received_notes rn + JOIN accounts a ON a.id = rn.account_id + JOIN transactions tx ON tx.id_tx = rn.tx + WHERE rn.nf IS NOT NULL + AND tx.block IS NOT NULL + AND rn.id NOT IN ( + SELECT spends.sapling_received_note_id + FROM sapling_received_note_spends spends + JOIN transactions stx ON stx.id_tx = spends.transaction_id + WHERE stx.block IS NOT NULL -- the spending tx is mined + OR stx.expiry_height IS NULL -- the spending tx will not expire + )", + ), + NullifierQuery::All => conn.prepare( + "SELECT a.uuid, rn.nf + FROM sapling_received_notes rn + JOIN accounts a ON a.id = rn.account_id + WHERE nf IS NOT NULL", + ), + }?; + + let nullifiers = stmt_fetch_nullifiers.query_and_then([], |row| { + let account = AccountUuid(row.get(0)?); + let nf_bytes: Vec = row.get(1)?; + Ok::<_, rusqlite::Error>((account, sapling::Nullifier::from_slice(&nf_bytes).unwrap())) + })?; + + let res: Vec<_> = nullifiers.collect::>()?; + Ok(res) +} + +pub(crate) fn detect_spending_accounts<'a>( + conn: &Connection, + nfs: impl Iterator, +) -> Result, rusqlite::Error> { + let mut account_q = conn.prepare_cached( + "SELECT accounts.uuid + FROM sapling_received_notes rn + JOIN accounts ON accounts.id = rn.account_id + WHERE rn.nf IN rarray(:nf_ptr)", + )?; + + let nf_values: Vec = nfs.map(|nf| Value::Blob(nf.to_vec())).collect(); + let nf_ptr = Rc::new(nf_values); + let res = account_q + .query_and_then(named_params![":nf_ptr": &nf_ptr], |row| { + row.get(0).map(AccountUuid) + })? + .collect::, _>>()?; + + Ok(res) +} + +/// Marks a given nullifier as having been revealed in the construction +/// of the specified transaction. +/// +/// Marking a note spent in this fashion does NOT imply that the +/// spending transaction has been mined. +pub(crate) fn mark_sapling_note_spent( + conn: &Connection, + tx_ref: TxRef, + nf: &sapling::Nullifier, +) -> Result { + let mut stmt_mark_sapling_note_spent = conn.prepare_cached( + "INSERT INTO sapling_received_note_spends (sapling_received_note_id, transaction_id) + SELECT id, :transaction_id FROM sapling_received_notes WHERE nf = :nf + ON CONFLICT (sapling_received_note_id, transaction_id) DO NOTHING", + )?; + + match stmt_mark_sapling_note_spent.execute(named_params![ + ":nf": &nf.0[..], + ":transaction_id": tx_ref.0 + ])? { + 0 => Ok(false), + 1 => Ok(true), + _ => unreachable!("nf column is marked as UNIQUE"), + } +} + +/// Records the specified shielded output as having been received. +/// +/// This implementation relies on the facts that: +/// - A transaction will not contain more than 2^63 shielded outputs. +/// - A note value will never exceed 2^63 zatoshis. +pub(crate) fn put_received_note>( + conn: &Transaction, + output: &T, + tx_ref: TxRef, + spent_in: Option, +) -> Result<(), SqliteClientError> { + let account_id = get_account_ref(conn, output.account_id())?; + let mut stmt_upsert_received_note = conn.prepare_cached( + "INSERT INTO sapling_received_notes + (tx, output_index, account_id, diversifier, value, rcm, memo, nf, + is_change, commitment_tree_position, + recipient_key_scope) + VALUES ( + :tx, + :output_index, + :account_id, + :diversifier, + :value, + :rcm, + :memo, + :nf, + :is_change, + :commitment_tree_position, + :recipient_key_scope + ) + ON CONFLICT (tx, output_index) DO UPDATE + SET account_id = :account_id, + diversifier = :diversifier, + value = :value, + rcm = :rcm, + nf = IFNULL(:nf, nf), + memo = IFNULL(:memo, memo), + is_change = MAX(:is_change, is_change), + commitment_tree_position = IFNULL(:commitment_tree_position, commitment_tree_position), + recipient_key_scope = :recipient_key_scope + RETURNING sapling_received_notes.id", + )?; + + let rcm = output.note().rcm().to_repr(); + let to = output.note().recipient(); + let diversifier = to.diversifier(); + + let sql_args = named_params![ + ":tx": tx_ref.0, + ":output_index": i64::try_from(output.index()).expect("output indices are representable as i64"), + ":account_id": account_id.0, + ":diversifier": &diversifier.0, + ":value": output.note().value().inner(), + ":rcm": &rcm, + ":nf": output.nullifier().map(|nf| nf.0), + ":memo": memo_repr(output.memo()), + ":is_change": output.is_change(), + ":commitment_tree_position": output.note_commitment_tree_position().map(u64::from), + ":recipient_key_scope": output.recipient_key_scope().map(scope_code) + ]; + + let received_note_id = stmt_upsert_received_note + .query_row(sql_args, |row| row.get::<_, i64>(0)) + .map_err(SqliteClientError::from)?; + + if let Some(spent_in) = spent_in { + conn.execute( + "INSERT INTO sapling_received_note_spends (sapling_received_note_id, transaction_id) + VALUES (:sapling_received_note_id, :transaction_id) + ON CONFLICT (sapling_received_note_id, transaction_id) DO NOTHING", + named_params![ + ":sapling_received_note_id": received_note_id, + ":transaction_id": spent_in.0 + ], + )?; + } + + Ok(()) +} + +#[cfg(test)] +pub(crate) mod tests { + use zcash_client_backend::data_api::testing::sapling::SaplingPoolTester; + + use crate::testing; + + #[cfg(feature = "orchard")] + use zcash_client_backend::data_api::testing::orchard::OrchardPoolTester; + + #[test] + fn send_single_step_proposed_transfer() { + testing::pool::send_single_step_proposed_transfer::() + } + + #[test] + fn send_with_multiple_change_outputs() { + testing::pool::send_with_multiple_change_outputs::() + } + + #[test] + #[cfg(feature = "transparent-inputs")] + fn send_multi_step_proposed_transfer() { + testing::pool::send_multi_step_proposed_transfer::() + } + + #[test] + #[cfg(feature = "transparent-inputs")] + fn proposal_fails_if_not_all_ephemeral_outputs_consumed() { + testing::pool::proposal_fails_if_not_all_ephemeral_outputs_consumed::() + } + + #[test] + fn create_to_address_fails_on_incorrect_usk() { + testing::pool::create_to_address_fails_on_incorrect_usk::() + } + + #[test] + fn proposal_fails_with_no_blocks() { + testing::pool::proposal_fails_with_no_blocks::() + } + + #[test] + fn spend_fails_on_unverified_notes() { + testing::pool::spend_fails_on_unverified_notes::() + } + + #[test] + fn spend_fails_on_locked_notes() { + testing::pool::spend_fails_on_locked_notes::() + } + + #[test] + fn ovk_policy_prevents_recovery_from_chain() { + testing::pool::ovk_policy_prevents_recovery_from_chain::() + } + + #[test] + fn spend_succeeds_to_t_addr_zero_change() { + testing::pool::spend_succeeds_to_t_addr_zero_change::() + } + + #[test] + fn change_note_spends_succeed() { + testing::pool::change_note_spends_succeed::() + } + + #[test] + fn external_address_change_spends_detected_in_restore_from_seed() { + testing::pool::external_address_change_spends_detected_in_restore_from_seed::< + SaplingPoolTester, + >() + } + + #[test] + #[ignore] // FIXME: #1316 This requires support for dust outputs. + #[cfg(not(feature = "expensive-tests"))] + fn zip317_spend() { + testing::pool::zip317_spend::() + } + + #[test] + #[cfg(feature = "transparent-inputs")] + fn shield_transparent() { + testing::pool::shield_transparent::() + } + + #[test] + fn birthday_in_anchor_shard() { + testing::pool::birthday_in_anchor_shard::() + } + + #[test] + fn checkpoint_gaps() { + testing::pool::checkpoint_gaps::() + } + + #[test] + fn scan_cached_blocks_detects_spends_out_of_order() { + testing::pool::scan_cached_blocks_detects_spends_out_of_order::() + } + + #[test] + fn metadata_queries_exclude_unwanted_notes() { + testing::pool::metadata_queries_exclude_unwanted_notes::() + } + + #[test] + #[cfg(feature = "orchard")] + fn pool_crossing_required() { + testing::pool::pool_crossing_required::() + } + + #[test] + #[cfg(feature = "orchard")] + fn fully_funded_fully_private() { + testing::pool::fully_funded_fully_private::() + } + + #[test] + #[cfg(all(feature = "orchard", feature = "transparent-inputs"))] + fn fully_funded_send_to_t() { + testing::pool::fully_funded_send_to_t::() + } + + #[test] + #[cfg(feature = "orchard")] + fn multi_pool_checkpoint() { + testing::pool::multi_pool_checkpoint::() + } + + #[test] + #[cfg(feature = "orchard")] + fn multi_pool_checkpoints_with_pruning() { + testing::pool::multi_pool_checkpoints_with_pruning::() + } + + #[cfg(feature = "pczt-tests")] + #[test] + fn pczt_single_step_sapling_only() { + testing::pool::pczt_single_step::() + } + + #[cfg(feature = "pczt-tests")] + #[test] + fn pczt_single_step_sapling_to_orchard() { + testing::pool::pczt_single_step::() + } +} diff --git a/zcash_client_sqlite/src/wallet/scanning.rs b/zcash_client_sqlite/src/wallet/scanning.rs new file mode 100644 index 0000000000..9c69751211 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/scanning.rs @@ -0,0 +1,1898 @@ +use incrementalmerkletree::{Address, Position}; +use rusqlite::{self, named_params, types::Value, OptionalExtension}; +use std::cmp::{max, min}; +use std::collections::BTreeSet; +use std::ops::Range; +use std::rc::Rc; +use tracing::{debug, trace}; + +use zcash_client_backend::data_api::{ + scanning::{spanning_tree::SpanningTree, ScanPriority, ScanRange}, + SAPLING_SHARD_HEIGHT, +}; +use zcash_protocol::{ + consensus::{self, BlockHeight, NetworkUpgrade}, + ShieldedProtocol, +}; + +use crate::{ + error::SqliteClientError, + wallet::{block_height_extrema, init::WalletMigrationError}, + PRUNING_DEPTH, SAPLING_TABLES_PREFIX, VERIFY_LOOKAHEAD, +}; + +use super::wallet_birthday; + +#[cfg(feature = "orchard")] +use {crate::ORCHARD_TABLES_PREFIX, zcash_client_backend::data_api::ORCHARD_SHARD_HEIGHT}; + +#[cfg(not(feature = "orchard"))] +use zcash_protocol::PoolType; + +pub(crate) fn priority_code(priority: &ScanPriority) -> i64 { + use ScanPriority::*; + match priority { + Ignored => 0, + Scanned => 10, + Historic => 20, + OpenAdjacent => 30, + FoundNote => 40, + ChainTip => 50, + Verify => 60, + } +} + +pub(crate) fn parse_priority_code(code: i64) -> Option { + use ScanPriority::*; + match code { + 0 => Some(Ignored), + 10 => Some(Scanned), + 20 => Some(Historic), + 30 => Some(OpenAdjacent), + 40 => Some(FoundNote), + 50 => Some(ChainTip), + 60 => Some(Verify), + _ => None, + } +} + +pub(crate) fn suggest_scan_ranges( + conn: &rusqlite::Connection, + min_priority: ScanPriority, +) -> Result, SqliteClientError> { + let mut stmt_scan_ranges = conn.prepare_cached( + "SELECT block_range_start, block_range_end, priority + FROM scan_queue + WHERE priority >= :min_priority + ORDER BY priority DESC, block_range_end DESC", + )?; + + let mut rows = + stmt_scan_ranges.query(named_params![":min_priority": priority_code(&min_priority)])?; + + let mut result = vec![]; + while let Some(row) = rows.next()? { + let range = Range { + start: row.get::<_, u32>(0).map(BlockHeight::from)?, + end: row.get::<_, u32>(1).map(BlockHeight::from)?, + }; + let code = row.get::<_, i64>(2)?; + let priority = parse_priority_code(code).ok_or_else(|| { + SqliteClientError::CorruptedData(format!("scan priority not recognized: {}", code)) + })?; + + result.push(ScanRange::from_parts(range, priority)); + } + + Ok(result) +} + +pub(crate) fn insert_queue_entries<'a>( + conn: &rusqlite::Connection, + entries: impl Iterator, +) -> Result<(), rusqlite::Error> { + let mut stmt = conn.prepare_cached( + "INSERT INTO scan_queue (block_range_start, block_range_end, priority) + VALUES (:block_range_start, :block_range_end, :priority)", + )?; + + for entry in entries { + trace!("Inserting queue entry {}", entry); + if !entry.is_empty() { + stmt.execute(named_params![ + ":block_range_start": u32::from(entry.block_range().start), + ":block_range_end": u32::from(entry.block_range().end), + ":priority": priority_code(&entry.priority()) + ])?; + } + } + + Ok(()) +} + +/// A trait that abstracts over the construction of wallet errors. +/// +/// In order to make it possible to use [`replace_queue_entries`] in database migrations as well as +/// in code that returns `SqliteClientError`, it is necessary for that method to be polymorphic in +/// the error type. +pub(crate) trait WalletError { + fn db_error(err: rusqlite::Error) -> Self; + fn corrupt(message: String) -> Self; +} + +impl WalletError for SqliteClientError { + fn db_error(err: rusqlite::Error) -> Self { + SqliteClientError::DbError(err) + } + + fn corrupt(message: String) -> Self { + SqliteClientError::CorruptedData(message) + } +} + +impl WalletError for WalletMigrationError { + fn db_error(err: rusqlite::Error) -> Self { + WalletMigrationError::DbError(err) + } + + fn corrupt(message: String) -> Self { + WalletMigrationError::CorruptedData(message) + } +} + +pub(crate) fn replace_queue_entries( + conn: &rusqlite::Transaction<'_>, + query_range: &Range, + entries: impl Iterator, + force_rescans: bool, +) -> Result<(), E> { + let (to_create, to_delete_ends) = { + let mut suggested_stmt = conn + .prepare_cached( + "SELECT block_range_start, block_range_end, priority + FROM scan_queue + -- Ignore ranges that do not overlap and are not adjacent to the query range. + WHERE NOT (block_range_start > :end OR :start > block_range_end) + ORDER BY block_range_end", + ) + .map_err(E::db_error)?; + + let mut rows = suggested_stmt + .query(named_params![ + ":start": u32::from(query_range.start), + ":end": u32::from(query_range.end), + ]) + .map_err(E::db_error)?; + + // Iterate over the ranges in the scan queue that overlap the range that we have + // identified as needing to be fully scanned. For each such range add it to the + // spanning tree (these should all be nonoverlapping ranges, but we might coalesce + // some in the process). + let mut to_create: Option = None; + let mut to_delete_ends: Vec = vec![]; + while let Some(row) = rows.next().map_err(E::db_error)? { + let entry = ScanRange::from_parts( + Range { + start: BlockHeight::from(row.get::<_, u32>(0).map_err(E::db_error)?), + end: BlockHeight::from(row.get::<_, u32>(1).map_err(E::db_error)?), + }, + { + let code = row.get::<_, i64>(2).map_err(E::db_error)?; + parse_priority_code(code).ok_or_else(|| { + E::corrupt(format!("scan priority not recognized: {}", code)) + })? + }, + ); + to_delete_ends.push(Value::from(u32::from(entry.block_range().end))); + to_create = if let Some(cur) = to_create { + Some(cur.insert(entry, force_rescans)) + } else { + Some(SpanningTree::Leaf(entry)) + }; + } + + // Update the tree that we read from the database, or if we didn't find any ranges + // start with the scanned range. + for entry in entries { + to_create = if let Some(cur) = to_create { + Some(cur.insert(entry, force_rescans)) + } else { + Some(SpanningTree::Leaf(entry)) + }; + } + + (to_create, to_delete_ends) + }; + + if let Some(tree) = to_create { + let ends_ptr = Rc::new(to_delete_ends); + conn.execute( + "DELETE FROM scan_queue WHERE block_range_end IN rarray(:ends)", + named_params![":ends": ends_ptr], + ) + .map_err(E::db_error)?; + + let scan_ranges = tree.into_vec(); + insert_queue_entries(conn, scan_ranges.iter()).map_err(E::db_error)?; + } + + Ok(()) +} + +fn extend_range( + conn: &rusqlite::Transaction<'_>, + range: &Range, + required_subtree_indices: BTreeSet, + table_prefix: &'static str, + fallback_start_height: Option, + birthday_height: Option, +) -> Result>, SqliteClientError> { + // we'll either have both min and max bounds, or we'll have neither + let subtree_index_bounds = required_subtree_indices + .iter() + .min() + .zip(required_subtree_indices.iter().max()); + + let mut shard_end_stmt = conn.prepare_cached(&format!( + "SELECT subtree_end_height + FROM {}_tree_shards + WHERE shard_index = :shard_index", + table_prefix + ))?; + + let mut shard_end = |index: u64| -> Result, rusqlite::Error> { + Ok(shard_end_stmt + .query_row(named_params![":shard_index": index], |row| { + row.get::<_, Option>(0) + .map(|opt| opt.map(BlockHeight::from)) + }) + .optional()? + .flatten()) + }; + + // If no notes belonging to the wallet were found, we don't need to extend the scanning + // range suggestions to include the associated subtrees, and our bounds are just the + // scanned range. Otherwise, ensure that all shard ranges starting from the wallet + // birthday are included. + subtree_index_bounds + .map(|(min_idx, max_idx)| { + let range_min = if *min_idx > 0 { + // get the block height of the end of the previous shard + shard_end(*min_idx - 1)? + } else { + // our lower bound is going to be the fallback height + fallback_start_height + }; + + // bound the minimum to the wallet birthday + let range_min = range_min.map(|h| birthday_height.map_or(h, |b| std::cmp::max(b, h))); + + // Get the block height for the end of the current shard, and make it an + // exclusive end bound. + let range_max = shard_end(*max_idx)?.map(|end| end + 1); + + Ok::, rusqlite::Error>(Range { + start: range.start.min(range_min.unwrap_or(range.start)), + end: range.end.max(range_max.unwrap_or(range.end)), + }) + }) + .transpose() + .map_err(SqliteClientError::from) +} + +pub(crate) fn scan_complete( + conn: &rusqlite::Transaction<'_>, + params: &P, + range: Range, + wallet_note_positions: &[(ShieldedProtocol, Position)], +) -> Result<(), SqliteClientError> { + // Read the wallet birthday (if known). + // TODO: use per-pool birthdays? + let wallet_birthday = wallet_birthday(conn)?; + + // Determine the range of block heights for which we will be updating the scan queue. + let extended_range = { + // If notes have been detected in the scan, we need to extend any adjacent un-scanned + // ranges starting from the wallet birthday to include the blocks needed to complete + // the note commitment tree subtrees containing the positions of the discovered notes. + // We will query by subtree index to find these bounds. + let mut required_sapling_subtrees = BTreeSet::new(); + #[cfg(feature = "orchard")] + let mut required_orchard_subtrees = BTreeSet::new(); + for (protocol, position) in wallet_note_positions { + match protocol { + ShieldedProtocol::Sapling => { + required_sapling_subtrees.insert( + Address::above_position(SAPLING_SHARD_HEIGHT.into(), *position).index(), + ); + } + ShieldedProtocol::Orchard => { + #[cfg(feature = "orchard")] + required_orchard_subtrees.insert( + Address::above_position(ORCHARD_SHARD_HEIGHT.into(), *position).index(), + ); + + #[cfg(not(feature = "orchard"))] + return Err(SqliteClientError::UnsupportedPoolType(PoolType::Shielded( + *protocol, + ))); + } + } + } + + let extended_range = extend_range( + conn, + &range, + required_sapling_subtrees, + SAPLING_TABLES_PREFIX, + params.activation_height(NetworkUpgrade::Sapling), + wallet_birthday, + )?; + + #[cfg(feature = "orchard")] + let extended_range = extend_range( + conn, + extended_range.as_ref().unwrap_or(&range), + required_orchard_subtrees, + ORCHARD_TABLES_PREFIX, + params.activation_height(NetworkUpgrade::Nu5), + wallet_birthday, + )? + .or(extended_range); + + #[allow(clippy::let_and_return)] + extended_range + }; + + let query_range = extended_range.clone().unwrap_or_else(|| range.clone()); + + let scanned = ScanRange::from_parts(range.clone(), ScanPriority::Scanned); + + // If any of the extended range actually extends beyond the scanned range, we need to + // scan that extension in order to make the found note(s) spendable. We need to avoid + // creating empty ranges here, as that acts as an optimization barrier preventing + // `SpanningTree` from merging non-empty scanned ranges on either side. + let extended_before = extended_range + .as_ref() + .map(|extended| ScanRange::from_parts(extended.start..range.start, ScanPriority::FoundNote)) + .filter(|range| !range.is_empty()); + let extended_after = extended_range + .map(|extended| ScanRange::from_parts(range.end..extended.end, ScanPriority::FoundNote)) + .filter(|range| !range.is_empty()); + + let replacement = Some(scanned) + .into_iter() + .chain(extended_before) + .chain(extended_after); + + replace_queue_entries::(conn, &query_range, replacement, false)?; + + Ok(()) +} + +fn tip_shard_end_height( + conn: &rusqlite::Transaction<'_>, + table_prefix: &'static str, +) -> Result, rusqlite::Error> { + conn.query_row( + &format!( + "SELECT MAX(subtree_end_height) FROM {}_tree_shards", + table_prefix + ), + [], + |row| Ok(row.get::<_, Option>(0)?.map(BlockHeight::from)), + ) +} + +pub(crate) fn update_chain_tip( + conn: &rusqlite::Transaction<'_>, + params: &P, + new_tip: BlockHeight, +) -> Result<(), SqliteClientError> { + // If the caller provided a chain tip that is before Sapling activation, do nothing. + let sapling_activation = match params.activation_height(NetworkUpgrade::Sapling) { + Some(h) if h <= new_tip => h, + _ => return Ok(()), + }; + + // Read the previous max scanned height from the blocks table + let max_scanned = block_height_extrema(conn)?.map(|range| *range.end()); + + // Read the wallet birthday (if known). + let wallet_birthday = wallet_birthday(conn)?; + + // If the chain tip is below the prior max scanned height, then the caller has caught + // the chain in the middle of a reorg. Do nothing; the caller will continue using the + // old scan ranges and either: + // - encounter an error trying to fetch the blocks (and thus trigger the same handling + // logic as if this happened with the old linear scanning code); or + // - encounter a discontinuity error in `scan_cached_blocks`, at which point they will + // call `WalletDb::truncate_to_height` as part of their reorg handling which will + // resolve the problem. + // + // We don't check the shard height, as normal usage would have the caller update the + // shard state prior to this call, so it is possible and expected to be in a situation + // where we should update the tip-related scan ranges but not the shard-related ones. + match max_scanned { + Some(h) if new_tip < h => return Ok(()), + _ => (), + }; + + // `ScanRange` uses an exclusive upper bound. + let chain_end = new_tip + 1; + + // Read the maximum height from each of the shards tables. The minimum of the two + // gives the start of a height range that covers the last incomplete shard of both the + // Sapling and Orchard pools. + let sapling_shard_tip = tip_shard_end_height(conn, SAPLING_TABLES_PREFIX)?; + #[cfg(feature = "orchard")] + let orchard_shard_tip = tip_shard_end_height(conn, ORCHARD_TABLES_PREFIX)?; + + #[cfg(feature = "orchard")] + let min_shard_tip = match (sapling_shard_tip, orchard_shard_tip) { + (None, None) => None, + (None, Some(o)) => Some(o), + (Some(s), None) => Some(s), + (Some(s), Some(o)) => Some(std::cmp::min(s, o)), + }; + #[cfg(not(feature = "orchard"))] + let min_shard_tip = sapling_shard_tip; + + // Create a scanning range for the fragment of the last shard leading up to new tip. + // We set a lower bound at the wallet birthday (if known), because account creation + // requires specifying a tree frontier that ensures we don't need tree information + // prior to the birthday. + let tip_shard_entry = min_shard_tip.filter(|h| h < &chain_end).map(|h| { + let min_to_scan = wallet_birthday.filter(|b| b > &h).unwrap_or(h); + ScanRange::from_parts(min_to_scan..chain_end, ScanPriority::ChainTip) + }); + + // Create scan ranges to either validate potentially invalid blocks at the wallet's + // view of the chain tip, or connect the prior tip to the new tip. + let tip_entry = max_scanned.map_or_else( + || { + // No blocks have been scanned, so we need to anchor the start of the new scan + // range to something else. + wallet_birthday.map_or_else( + // We don't have a wallet birthday, which means we have no accounts yet. + // We can therefore ignore all blocks up to the chain tip. + || ScanRange::from_parts(sapling_activation..chain_end, ScanPriority::Ignored), + // We have a wallet birthday, so mark all blocks between that and the + // chain tip as `Historic` (performing wallet recovery). + |wallet_birthday| { + ScanRange::from_parts(wallet_birthday..chain_end, ScanPriority::Historic) + }, + ) + }, + |max_scanned| { + // The scan range starts at the block after the max scanned height. Since + // `scan_cached_blocks` retrieves the metadata for the block being connected to + // (if it exists), the connectivity of the scan range to the max scanned block + // will always be checked if relevant. + let min_unscanned = max_scanned + 1; + + // If we don't have shard metadata, this means we're doing linear scanning, so + // create a scan range from the prior tip to the current tip with `Historic` + // priority. + if tip_shard_entry.is_none() { + ScanRange::from_parts(min_unscanned..chain_end, ScanPriority::Historic) + } else { + // Determine the height to which we expect new blocks retrieved from the + // block source to be stable and not subject to being reorg'ed. + let stable_height = new_tip.saturating_sub(PRUNING_DEPTH); + + // If the wallet's max scanned height is above the stable height, + // prioritize the range between it and the new tip as `ChainTip`. + if max_scanned > stable_height { + // We are in the steady-state case, where a wallet is close to the + // chain tip and just needs to catch up. + // + // This overlaps the `tip_shard_entry` range and so will be coalesced + // with it. + ScanRange::from_parts(min_unscanned..chain_end, ScanPriority::ChainTip) + } else { + // In this case, the max scanned height is considered stable relative + // to the chain tip. However, it may be stable or unstable relative to + // the prior chain tip, which we could determine by looking up the + // prior chain tip height from the scan queue. For simplicity we merge + // these two cases together, and proceed as though the max scanned + // block is unstable relative to the prior chain tip. + // + // To confirm its stability, prioritize the `VERIFY_LOOKAHEAD` blocks + // above the max scanned height as `Verify`: + // + // - We use `Verify` to ensure that a connectivity check is performed, + // along with any required rewinds, before any `ChainTip` ranges + // (from this or any prior `update_chain_tip` call) are scanned. + // + // - We prioritize `VERIFY_LOOKAHEAD` blocks because this is expected + // to be 12.5 minutes, within which it is reasonable for a user to + // have potentially received a transaction (if they opened their + // wallet to provide an address to someone else, or spent their own + // funds creating a change output), without necessarily having left + // their wallet open long enough for the transaction to be mined and + // the corresponding block to be scanned. + // + // - We limit the range to at most the stable region, to prevent any + // `Verify` ranges from being susceptible to reorgs, and potentially + // interfering with subsequent `Verify` ranges defined by future + // calls to `update_chain_tip`. Any gap between `stable_height` and + // `shard_start_height` will be filled by the scan range merging + // logic with a `Historic` range. + // + // If `max_scanned == stable_height` then this is a zero-length range. + // In this case, any non-empty `(stable_height+1)..shard_start_height` + // will be marked `Historic`, minimising the prioritised blocks at the + // chain tip and allowing for other ranges (for example, `FoundNote`) + // to take priority. + ScanRange::from_parts( + min_unscanned..min(stable_height + 1, min_unscanned + VERIFY_LOOKAHEAD), + ScanPriority::Verify, + ) + } + } + }, + ); + if let Some(entry) = &tip_shard_entry { + debug!("{} will update latest shard", entry); + } + debug!("{} will connect prior scanned state to new tip", tip_entry); + + let query_range = match tip_shard_entry.as_ref() { + Some(se) => Range { + start: min(se.block_range().start, tip_entry.block_range().start), + end: max(se.block_range().end, tip_entry.block_range().end), + }, + None => tip_entry.block_range().clone(), + }; + + replace_queue_entries::( + conn, + &query_range, + tip_shard_entry.into_iter().chain(Some(tip_entry)), + false, + )?; + + Ok(()) +} + +#[cfg(test)] +pub(crate) mod tests { + use std::num::NonZeroU8; + + use incrementalmerkletree::{frontier::Frontier, Hashable, Position}; + + use secrecy::SecretVec; + use zcash_client_backend::data_api::{ + chain::{ChainState, CommitmentTreeRoot}, + scanning::{spanning_tree::testing::scan_range, ScanPriority}, + testing::{ + pool::ShieldedPoolTester, sapling::SaplingPoolTester, AddressType, FakeCompactOutput, + InitialChainState, TestBuilder, TestState, + }, + AccountBirthday, Ratio, WalletRead, WalletWrite, + }; + use zcash_primitives::block::BlockHash; + use zcash_protocol::{ + consensus::{BlockHeight, NetworkUpgrade, Parameters}, + local_consensus::LocalNetwork, + value::Zatoshis, + }; + + use crate::{ + error::SqliteClientError, + testing::{ + db::{TestDb, TestDbFactory}, + BlockCache, + }, + wallet::scanning::{insert_queue_entries, replace_queue_entries, suggest_scan_ranges}, + VERIFY_LOOKAHEAD, + }; + + #[cfg(feature = "orchard")] + use { + incrementalmerkletree::Level, + orchard::tree::MerkleHashOrchard, + std::{convert::Infallible, num::NonZeroU32}, + zcash_client_backend::{ + data_api::{ + testing::orchard::OrchardPoolTester, wallet::input_selection::GreedyInputSelector, + WalletCommitmentTrees, + }, + fees::{standard, DustOutputPolicy, StandardFeeRule}, + wallet::OvkPolicy, + }, + zcash_protocol::memo::Memo, + }; + + #[test] + fn sapling_scan_complete() { + scan_complete::(); + } + + #[test] + #[cfg(feature = "orchard")] + fn orchard_scan_complete() { + scan_complete::(); + } + + fn scan_complete() { + use ScanPriority::*; + + // We'll start inserting leaf notes 5 notes after the end of the third subtree, with a gap + // of 10 blocks. After `scan_cached_blocks`, the scan queue should have a requested scan + // range of 300..310 with `FoundNote` priority, 310..320 with `Scanned` priority. + // We set both Sapling and Orchard to the same initial tree size for simplicity. + let prior_block_hash = BlockHash([0; 32]); + let initial_sapling_tree_size: u32 = (0x1 << 16) * 3 + 5; + let initial_orchard_tree_size: u32 = (0x1 << 16) * 3 + 5; + let initial_height_offset = 310; + + let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory::default()) + .with_block_cache(BlockCache::new()) + .with_initial_chain_state(|rng, network| { + let sapling_activation_height = + network.activation_height(NetworkUpgrade::Sapling).unwrap(); + // Construct a fake chain state for the end of block 300 + let (prior_sapling_roots, sapling_initial_tree) = + Frontier::random_with_prior_subtree_roots( + rng, + initial_sapling_tree_size.into(), + NonZeroU8::new(16).unwrap(), + ); + let prior_sapling_roots = prior_sapling_roots + .into_iter() + .zip(1u32..) + .map(|(root, i)| { + CommitmentTreeRoot::from_parts(sapling_activation_height + (100 * i), root) + }) + .collect::>(); + + #[cfg(feature = "orchard")] + let (prior_orchard_roots, orchard_initial_tree) = + Frontier::random_with_prior_subtree_roots( + rng, + initial_orchard_tree_size.into(), + NonZeroU8::new(16).unwrap(), + ); + #[cfg(feature = "orchard")] + let prior_orchard_roots = prior_orchard_roots + .into_iter() + .zip(1u32..) + .map(|(root, i)| { + CommitmentTreeRoot::from_parts(sapling_activation_height + (100 * i), root) + }) + .collect::>(); + + InitialChainState { + chain_state: ChainState::new( + sapling_activation_height + initial_height_offset - 1, + prior_block_hash, + sapling_initial_tree, + #[cfg(feature = "orchard")] + orchard_initial_tree, + ), + prior_sapling_roots, + #[cfg(feature = "orchard")] + prior_orchard_roots, + } + }) + .with_account_from_sapling_activation(BlockHash([3; 32])) + .build(); + + let sapling_activation_height = st.sapling_activation_height(); + + let dfvk = T::test_account_fvk(&st); + let value = Zatoshis::const_from_u64(50000); + let initial_height = sapling_activation_height + initial_height_offset; + st.generate_block_at( + initial_height, + prior_block_hash, + &[FakeCompactOutput::new( + &dfvk, + AddressType::DefaultExternal, + value, + )], + initial_sapling_tree_size, + initial_orchard_tree_size, + false, + ); + + for _ in 1..=10 { + st.generate_next_block( + &dfvk, + AddressType::DefaultExternal, + Zatoshis::const_from_u64(10000), + ); + } + + st.scan_cached_blocks(initial_height, 10); + + // Verify the that adjacent range needed to make the note spendable has been prioritized. + let sap_active = u32::from(sapling_activation_height); + assert_matches!( + suggest_scan_ranges(st.wallet().conn(), Historic), + Ok(scan_ranges) if scan_ranges == vec![ + scan_range((sap_active + 300)..(sap_active + 310), FoundNote) + ] + ); + + // Check that the scanned range has been properly persisted. + assert_matches!( + suggest_scan_ranges(st.wallet().conn(), Scanned), + Ok(scan_ranges) if scan_ranges == vec![ + scan_range((sap_active + 300)..(sap_active + 310), FoundNote), + scan_range((sap_active + 310)..(sap_active + 320), Scanned) + ] + ); + + // Simulate the wallet going offline for a bit, update the chain tip to 20 blocks in the + // future. + assert_matches!( + st.wallet_mut() + .update_chain_tip(sapling_activation_height + 340), + Ok(()) + ); + + // Check the scan range again, we should see a `ChainTip` range for the period we've been + // offline. + assert_matches!( + suggest_scan_ranges(st.wallet().conn(), Historic), + Ok(scan_ranges) if scan_ranges == vec![ + scan_range((sap_active + 320)..(sap_active + 341), ChainTip), + scan_range((sap_active + 300)..(sap_active + 310), ChainTip) + ] + ); + + // Now simulate a jump ahead more than 100 blocks. + assert_matches!( + st.wallet_mut() + .update_chain_tip(sapling_activation_height + 450), + Ok(()) + ); + + // Check the scan range again, we should see a `Validate` range for the previous wallet + // tip, and then a `ChainTip` for the remaining range. + assert_matches!( + suggest_scan_ranges(st.wallet().conn(), Historic), + Ok(scan_ranges) if scan_ranges == vec![ + scan_range((sap_active + 320)..(sap_active + 330), Verify), + scan_range((sap_active + 330)..(sap_active + 451), ChainTip), + scan_range((sap_active + 300)..(sap_active + 310), ChainTip) + ] + ); + + // The wallet summary should be requesting the second-to-last root, as the last + // shard is incomplete. + assert_eq!( + st.wallet() + .get_wallet_summary(0) + .unwrap() + .map(|s| T::next_subtree_index(&s)), + Some(2), + ); + } + + /// Creates wallet and chain state such that: + /// * Shielded chain history begins at NU5 activation + /// * Both the Sapling and the Orchard note commitment trees have the following structure: + /// * The initial 2^16 shard of the note commitment tree covers `initial_shard_blocks` blocks. + /// If `insert_prior_roots` is set, the root of the initial shard is inserted into each note + /// commitment tree. This can be used to simulate the circumstance where note commitment tree + /// roots have been inserted prior to scanning. + /// * The wallet birthday is located `birthday_offset` blocks into the second shard. + /// * The note commitment tree contains 2^16+1235 notes at the end of the block prior to the + /// wallet birthday. + pub(crate) fn test_with_nu5_birthday_offset( + initial_shard_blocks: u32, + birthday_offset: u32, + prior_block_hash: BlockHash, + insert_prior_roots: bool, + ) -> ( + TestState, + T::Fvk, + AccountBirthday, + u32, + ) { + let st = TestBuilder::new() + .with_data_store_factory(TestDbFactory::default()) + .with_block_cache(BlockCache::new()) + .with_initial_chain_state(|rng, network| { + // We set the Sapling and Orchard frontiers at the birthday height to be + // 1234 notes into the second shard. + let frontier_position = Position::from((0x1 << 16) + 1234); + let initial_shard_end = + network.activation_height(NetworkUpgrade::Nu5).unwrap() + initial_shard_blocks; + let birthday_height = initial_shard_end + birthday_offset; + + // Construct a fake chain state for the end of the block with the given + // birthday_offset from the end of the last shard. + let (prior_sapling_roots, sapling_initial_tree) = + Frontier::random_with_prior_subtree_roots( + rng, + (frontier_position + 1).into(), + NonZeroU8::new(16).unwrap(), + ); + #[cfg(feature = "orchard")] + let (prior_orchard_roots, orchard_initial_tree) = + Frontier::random_with_prior_subtree_roots( + rng, + (frontier_position + 1).into(), + NonZeroU8::new(16).unwrap(), + ); + + InitialChainState { + chain_state: ChainState::new( + birthday_height, + prior_block_hash, + sapling_initial_tree, + #[cfg(feature = "orchard")] + orchard_initial_tree, + ), + prior_sapling_roots: if insert_prior_roots { + prior_sapling_roots + .into_iter() + .map(|root| CommitmentTreeRoot::from_parts(initial_shard_end, root)) + .collect() + } else { + vec![] + }, + #[cfg(feature = "orchard")] + prior_orchard_roots: if insert_prior_roots { + prior_orchard_roots + .into_iter() + .map(|root| CommitmentTreeRoot::from_parts(initial_shard_end, root)) + .collect() + } else { + vec![] + }, + } + }) + .with_account_having_current_birthday() + .build(); + + let birthday = st.test_account().unwrap().birthday().clone(); + let dfvk = T::test_account_fvk(&st); + let sap_active = st.sapling_activation_height(); + + (st, dfvk, birthday, sap_active.into()) + } + + #[test] + fn sapling_create_account_creates_ignored_range() { + create_account_creates_ignored_range::(); + } + + #[test] + #[cfg(feature = "orchard")] + fn orchard_create_account_creates_ignored_range() { + create_account_creates_ignored_range::(); + } + + fn create_account_creates_ignored_range() { + use ScanPriority::*; + + // Use a non-zero birthday offset because Sapling and NU5 are activated at the same height. + let (st, _, birthday, sap_active) = + test_with_nu5_birthday_offset::(50, 26, BlockHash([0; 32]), true); + let birthday_height = birthday.height().into(); + + let expected = vec![ + // The range up to the wallet's birthday height is ignored. + scan_range(sap_active..birthday_height, Ignored), + ]; + let actual = suggest_scan_ranges(st.wallet().conn(), Ignored).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn update_chain_tip_before_create_account() { + use ScanPriority::*; + + let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory::default()) + .with_block_cache(BlockCache::new()) + .build(); + let sap_active = st.sapling_activation_height(); + + // Update the chain tip. + let new_tip = sap_active + 1000; + st.wallet_mut().update_chain_tip(new_tip).unwrap(); + let chain_end = u32::from(new_tip + 1); + + let expected = vec![ + // The range up to the chain end is ignored. + scan_range(sap_active.into()..chain_end, Ignored), + ]; + let actual = suggest_scan_ranges(st.wallet().conn(), Ignored).unwrap(); + assert_eq!(actual, expected); + + // Now add an account. + let wallet_birthday = sap_active + 500; + st.wallet_mut() + .create_account( + "", + &SecretVec::new(vec![0; 32]), + &AccountBirthday::from_parts( + ChainState::empty(wallet_birthday - 1, BlockHash([0; 32])), + None, + ), + None, + ) + .unwrap(); + + let expected = vec![ + // The account's birthday onward is marked for recovery. + scan_range(wallet_birthday.into()..chain_end, Historic), + // The range up to the wallet's birthday height is ignored. + scan_range(sap_active.into()..wallet_birthday.into(), Ignored), + ]; + let actual = suggest_scan_ranges(st.wallet().conn(), Ignored).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn sapling_update_chain_tip_with_no_subtree_roots() { + update_chain_tip_with_no_subtree_roots::(); + } + + #[cfg(feature = "orchard")] + #[test] + fn orchard_update_chain_tip_with_no_subtree_roots() { + update_chain_tip_with_no_subtree_roots::(); + } + + fn update_chain_tip_with_no_subtree_roots() { + use ScanPriority::*; + + // Use a non-zero birthday offset because Sapling and NU5 are activated at the same height. + let (mut st, _, birthday, sap_active) = + test_with_nu5_birthday_offset::(50, 26, BlockHash([0; 32]), false); + + // Set up the following situation: + // + // prior_tip new_tip + // |<--- 500 --->| + // wallet_birthday + let prior_tip = birthday.height(); + let wallet_birthday = birthday.height().into(); + + // Update the chain tip. + let new_tip = prior_tip + 500; + st.wallet_mut().update_chain_tip(new_tip).unwrap(); + let chain_end = u32::from(new_tip + 1); + + // Verify that the suggested scan ranges match what is expected. + let expected = vec![ + // The wallet's birthday onward is marked for recovery. Because we don't + // yet have any chain state, it is marked with `Historic` priority rather + // than `ChainTip`. + scan_range(wallet_birthday..chain_end, Historic), + // The range below the wallet's birthday height is ignored. + scan_range(sap_active..wallet_birthday, Ignored), + ]; + + let actual = suggest_scan_ranges(st.wallet().conn(), Ignored).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn sapling_update_chain_tip_when_never_scanned() { + update_chain_tip_when_never_scanned::(); + } + + #[cfg(feature = "orchard")] + #[test] + fn orchard_update_chain_tip_when_never_scanned() { + update_chain_tip_when_never_scanned::(); + } + + fn update_chain_tip_when_never_scanned() { + use ScanPriority::*; + + // Use a non-zero birthday offset because Sapling and NU5 are activated at the same height. + let (mut st, _, birthday, sap_active) = + test_with_nu5_birthday_offset::(76, 1000, BlockHash([0; 32]), true); + + // Set up the following situation: + // + // last_shard_start prior_tip new_tip + // |<----- 1000 ----->|<--- 500 --->| + // wallet_birthday + let prior_tip_height = birthday.height(); + + // Update the chain tip. + let tip_height = prior_tip_height + 500; + st.wallet_mut().update_chain_tip(tip_height).unwrap(); + let chain_end = u32::from(tip_height + 1); + + // Verify that the suggested scan ranges match what is expected. + let expected = vec![ + // The last (incomplete) shard's range starting from the wallet birthday is + // marked for catching up to the chain tip, to ensure that if any notes are + // discovered after the wallet's birthday, they will be spendable. + scan_range(birthday.height().into()..chain_end, ChainTip), + // The range below the birthday height is ignored. + scan_range(sap_active..birthday.height().into(), Ignored), + ]; + + let actual = suggest_scan_ranges(st.wallet().conn(), Ignored).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn sapling_update_chain_tip_unstable_max_scanned() { + update_chain_tip_unstable_max_scanned::(); + } + + #[test] + #[cfg(feature = "orchard")] + fn orchard_update_chain_tip_unstable_max_scanned() { + update_chain_tip_unstable_max_scanned::(); + } + + fn update_chain_tip_unstable_max_scanned() { + use ScanPriority::*; + // Set up the following situation: + // + // prior_tip new_tip + // |<------- 10 ------->|<--- 500 --->|<- 40 ->|<-- 70 -->|<- 20 ->| + // initial_shard_end wallet_birthday max_scanned last_shard_start + // + let birthday_offset = 76; + let birthday_prior_block_hash = BlockHash([0; 32]); + // We set the Sapling and Orchard frontiers at the birthday block initial state to 1234 + // notes beyond the end of the first shard. + let frontier_tree_size: u32 = (0x1 << 16) + 1234; + let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory::default()) + .with_block_cache(BlockCache::new()) + .with_initial_chain_state(|rng, network| { + let birthday_height = + network.activation_height(NetworkUpgrade::Nu5).unwrap() + birthday_offset; + + // Construct a fake chain state for the end of the block with the given + // birthday_offset from the Nu5 birthday. + let (prior_sapling_roots, sapling_initial_tree) = + Frontier::random_with_prior_subtree_roots( + rng, + frontier_tree_size.into(), + NonZeroU8::new(16).unwrap(), + ); + // There will only be one prior root + let prior_sapling_roots = prior_sapling_roots + .into_iter() + .map(|root| CommitmentTreeRoot::from_parts(birthday_height - 10, root)) + .collect::>(); + + #[cfg(feature = "orchard")] + let (prior_orchard_roots, orchard_initial_tree) = + Frontier::random_with_prior_subtree_roots( + rng, + frontier_tree_size.into(), + NonZeroU8::new(16).unwrap(), + ); + // There will only be one prior root + #[cfg(feature = "orchard")] + let prior_orchard_roots = prior_orchard_roots + .into_iter() + .map(|root| CommitmentTreeRoot::from_parts(birthday_height - 10, root)) + .collect::>(); + + InitialChainState { + chain_state: ChainState::new( + birthday_height - 1, + birthday_prior_block_hash, + sapling_initial_tree, + #[cfg(feature = "orchard")] + orchard_initial_tree, + ), + prior_sapling_roots, + #[cfg(feature = "orchard")] + prior_orchard_roots, + } + }) + .with_account_having_current_birthday() + .build(); + + let account = st.test_account().cloned().unwrap(); + let dfvk = T::test_account_fvk(&st); + let sap_active = st.sapling_activation_height(); + let max_scanned = account.birthday().height() + 500; + + // Set up prior chain state. This simulates us having imported a wallet + // with a birthday 520 blocks below the chain tip. + let prior_tip = max_scanned + 40; + st.wallet_mut().update_chain_tip(prior_tip).unwrap(); + + let pre_birthday_range = scan_range( + sap_active.into()..account.birthday().height().into(), + Ignored, + ); + + // Verify that the suggested scan ranges match what is expected. + let expected = vec![ + scan_range( + account.birthday().height().into()..(prior_tip + 1).into(), + ChainTip, + ), + pre_birthday_range.clone(), + ]; + let actual = suggest_scan_ranges(st.wallet().conn(), Ignored).unwrap(); + assert_eq!(actual, expected); + + // Simulate that in the blocks between the wallet birthday and the max_scanned height, + // there are 10 Sapling notes and 10 Orchard notes created on the chain. + st.generate_block_at( + max_scanned, + BlockHash([1u8; 32]), + &[FakeCompactOutput::new( + &dfvk, + AddressType::DefaultExternal, + // 1235 notes into the second shard + Zatoshis::const_from_u64(10000), + )], + frontier_tree_size + 10, + frontier_tree_size + 10, + false, + ); + st.scan_cached_blocks(max_scanned, 1); + + // Verify that the suggested scan ranges match what is expected. + let expected = vec![ + scan_range((max_scanned + 1).into()..(prior_tip + 1).into(), ChainTip), + scan_range( + account.birthday().height().into()..max_scanned.into(), + ChainTip, + ), + scan_range(max_scanned.into()..(max_scanned + 1).into(), Scanned), + pre_birthday_range.clone(), + ]; + + let actual = suggest_scan_ranges(st.wallet().conn(), Ignored).unwrap(); + assert_eq!(actual, expected); + + // Now simulate shutting down, and then restarting 90 blocks later, after a shard + // has been completed. We have to update both trees, because otherwise we will pick the + // lesser of the tip shard start heights as where we must scan from. + let last_shard_start = prior_tip + 70; + st.put_subtree_roots( + 1, + &[CommitmentTreeRoot::from_parts( + last_shard_start, + // fake a hash, the value doesn't matter + sapling::Node::empty_leaf(), + )], + #[cfg(feature = "orchard")] + 1, + #[cfg(feature = "orchard")] + &[CommitmentTreeRoot::from_parts( + last_shard_start, + // fake a hash, the value doesn't matter + MerkleHashOrchard::empty_leaf(), + )], + ) + .unwrap(); + + // Just inserting the subtree roots doesn't affect the scan ranges. + let actual = suggest_scan_ranges(st.wallet().conn(), Ignored).unwrap(); + assert_eq!(actual, expected); + + let new_tip = last_shard_start + 20; + st.wallet_mut().update_chain_tip(new_tip).unwrap(); + + // Verify that the suggested scan ranges match what is expected + let expected = vec![ + // The max scanned block's connectivity is verified by scanning the next 10 blocks. + scan_range( + (max_scanned + 1).into()..(max_scanned + 1 + VERIFY_LOOKAHEAD).into(), + Verify, + ), + // The last shard needs to catch up to the chain tip in order to make notes spendable. + scan_range(last_shard_start.into()..u32::from(new_tip + 1), ChainTip), + // The range between the verification blocks and the prior tip is still in the queue. + scan_range( + (max_scanned + 1 + VERIFY_LOOKAHEAD).into()..(prior_tip + 1).into(), + ChainTip, + ), + // The remainder of the second-to-last shard's range is still in the queue. + scan_range( + account.birthday().height().into()..max_scanned.into(), + ChainTip, + ), + // The gap between the prior tip and the last shard is deferred as low priority. + scan_range((prior_tip + 1).into()..last_shard_start.into(), Historic), + // The max scanned block itself is left as-is. + scan_range(max_scanned.into()..(max_scanned + 1).into(), Scanned), + // The range below the second-to-last shard is ignored. + pre_birthday_range, + ]; + + let actual = suggest_scan_ranges(st.wallet().conn(), Ignored).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn sapling_update_chain_tip_stable_max_scanned() { + update_chain_tip_stable_max_scanned::(); + } + + #[test] + #[cfg(feature = "orchard")] + fn orchard_update_chain_tip_stable_max_scanned() { + update_chain_tip_stable_max_scanned::(); + } + + fn update_chain_tip_stable_max_scanned() { + use ScanPriority::*; + + // Set up the following situation: + // + // prior_tip new_tip + // |<--- 500 --->|<- 20 ->|<-- 50 -->|<- 20 ->| + // wallet_birthday max_scanned last_shard_start + // + let birthday_offset = 76; + let birthday_prior_block_hash = BlockHash([0; 32]); + // We set the Sapling and Orchard frontiers at the birthday block initial state to 1234 + // notes beyond the end of the first shard. + let frontier_tree_size: u32 = (0x1 << 16) + 1234; + let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory::default()) + .with_block_cache(BlockCache::new()) + .with_initial_chain_state(|rng, network| { + let birthday_height = + network.activation_height(NetworkUpgrade::Nu5).unwrap() + birthday_offset; + + // Construct a fake chain state for the end of the block with the given + // birthday_offset from the Nu5 birthday. + let (prior_sapling_roots, sapling_initial_tree) = + Frontier::random_with_prior_subtree_roots( + rng, + frontier_tree_size.into(), + NonZeroU8::new(16).unwrap(), + ); + // There will only be one prior root + let prior_sapling_roots = prior_sapling_roots + .into_iter() + .map(|root| CommitmentTreeRoot::from_parts(birthday_height - 10, root)) + .collect::>(); + + #[cfg(feature = "orchard")] + let (prior_orchard_roots, orchard_initial_tree) = + Frontier::random_with_prior_subtree_roots( + rng, + frontier_tree_size.into(), + NonZeroU8::new(16).unwrap(), + ); + // There will only be one prior root + #[cfg(feature = "orchard")] + let prior_orchard_roots = prior_orchard_roots + .into_iter() + .map(|root| CommitmentTreeRoot::from_parts(birthday_height - 10, root)) + .collect::>(); + + InitialChainState { + chain_state: ChainState::new( + birthday_height - 1, + birthday_prior_block_hash, + sapling_initial_tree, + #[cfg(feature = "orchard")] + orchard_initial_tree, + ), + prior_sapling_roots, + #[cfg(feature = "orchard")] + prior_orchard_roots, + } + }) + .with_account_having_current_birthday() + .build(); + + let account = st.test_account().cloned().unwrap(); + let dfvk = T::test_account_fvk(&st); + let birthday = account.birthday(); + let sap_active = st.sapling_activation_height(); + + // If none of the wallet's accounts have a recover-until height, then there + // is no recovery phase for the wallet, and therefore the denominator in the + // resulting ratio (the number of notes in the recovery range) is zero. + let no_recovery = Some(Ratio::new(0, 0)); + + // We have scan ranges and a subtree, but have scanned no blocks. Given the number of + // blocks scanned in the previous subtree, we estimate the number of notes in the current + // subtree + let summary = st.get_wallet_summary(1); + assert_eq!( + summary.as_ref().and_then(|s| s.progress().recovery()), + no_recovery, + ); + assert_matches!( + summary.map(|s| s.progress().scan()), + Some(ratio) if *ratio.numerator() == 0 + ); + + // Set up prior chain state. This simulates us having imported a wallet + // with a birthday 520 blocks below the chain tip. + let max_scanned = birthday.height() + 500; + let prior_tip = max_scanned + 20; + st.wallet_mut().update_chain_tip(prior_tip).unwrap(); + + // Verify that the suggested scan ranges match what is expected. + let expected = vec![ + scan_range(birthday.height().into()..(prior_tip + 1).into(), ChainTip), + scan_range(sap_active.into()..birthday.height().into(), Ignored), + ]; + + let actual = suggest_scan_ranges(st.wallet().conn(), Ignored).unwrap(); + assert_eq!(actual, expected); + + // Simulate that in the blocks between the wallet birthday and the max_scanned height, + // there are 10 Sapling notes and 10 Orchard notes created on the chain. + st.generate_block_at( + max_scanned, + BlockHash([1; 32]), + &[FakeCompactOutput::new( + &dfvk, + AddressType::DefaultExternal, + Zatoshis::const_from_u64(10000), + )], + frontier_tree_size + 10, + frontier_tree_size + 10, + false, + ); + st.scan_cached_blocks(max_scanned, 1); + + // We have scanned a block, so we now have a starting tree position, 500 blocks above the + // wallet birthday but before the end of the shard. + let summary = st.get_wallet_summary(1); + assert_eq!(summary.as_ref().map(|s| T::next_subtree_index(s)), Some(0)); + + assert_eq!( + summary.as_ref().and_then(|s| s.progress().recovery()), + no_recovery + ); + + // Progress denominator depends on which pools are enabled (which changes the + // initial tree states), and is extrapolated from the scanned range. + let expected_denom = 10 + + ((1234 + 10) * (prior_tip - max_scanned)) / (max_scanned - (birthday.height() - 10)); + #[cfg(feature = "orchard")] + let expected_denom = expected_denom * 2; + let expected_denom = expected_denom + 1; + assert_eq!( + summary.map(|s| s.progress().scan()), + Some(Ratio::new(1, u64::from(expected_denom))) + ); + + // Now simulate shutting down, and then restarting 70 blocks later, after the + // shard containing our birthday has been completed in one pool. + let last_shard_start = prior_tip + 50; + T::put_subtree_roots( + &mut st, + 1, + &[CommitmentTreeRoot::from_parts( + last_shard_start, + // fake a hash, the value doesn't matter + T::empty_tree_leaf(), + )], + ) + .unwrap(); + + { + let mut shard_stmt = st + .wallet_mut() + .db_mut() + .conn + .prepare("SELECT shard_index, subtree_end_height FROM sapling_tree_shards") + .unwrap(); + assert_eq!( + (shard_stmt + .query_and_then::<_, rusqlite::Error, _, _>([], |row| { + Ok((row.get::<_, u32>(0)?, row.get::<_, Option>(1)?)) + }) + .unwrap() + .collect::, _>>()) + .unwrap() + .len(), + 2, + ); + } + + { + let mut shard_stmt = st + .wallet_mut() + .db_mut() + .conn + .prepare("SELECT shard_index, subtree_end_height FROM orchard_tree_shards") + .unwrap(); + #[cfg(not(feature = "orchard"))] + let expected_shards = 0; + #[cfg(feature = "orchard")] + let expected_shards = 2; + assert_eq!( + (shard_stmt + .query_and_then::<_, rusqlite::Error, _, _>([], |row| { + Ok((row.get::<_, u32>(0)?, row.get::<_, Option>(1)?)) + }) + .unwrap() + .collect::, _>>()) + .unwrap() + .len(), + expected_shards, + ); + } + + let new_tip = last_shard_start + 20; + st.wallet_mut().update_chain_tip(new_tip).unwrap(); + let chain_end = u32::from(new_tip + 1); + + // Verify that the suggested scan ranges match what is expected. + let expected = vec![ + // The blocks after the max scanned block up to the chain tip are prioritised. + scan_range((max_scanned + 1).into()..chain_end, ChainTip), + // The remainder of the second-to-last shard's range is still in the queue. + scan_range(birthday.height().into()..max_scanned.into(), ChainTip), + // The max scanned block itself is left as-is. + scan_range(max_scanned.into()..(max_scanned + 1).into(), Scanned), + // The range below the second-to-last shard is ignored. + scan_range(sap_active.into()..birthday.height().into(), Ignored), + ]; + + let actual = suggest_scan_ranges(st.wallet().conn(), Ignored).unwrap(); + assert_eq!(actual, expected); + + // We've crossed a subtree boundary, but only in one pool. + let expected_denom = (1 << 16) * 2 + + ((1 << 16) * (new_tip - last_shard_start)) + / (last_shard_start - (birthday.height() - 10)) + - frontier_tree_size; + #[cfg(feature = "orchard")] + let expected_denom = expected_denom + + (10 + + ((1234 + 10) * (new_tip - max_scanned)) + / (max_scanned - (birthday.height() - 10))); + let summary = st.get_wallet_summary(1); + assert_eq!( + summary.map(|s| s.progress().scan()), + Some(Ratio::new(1, u64::from(expected_denom))) + ); + } + + #[test] + fn replace_queue_entries_merges_previous_range() { + use ScanPriority::*; + + let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory::default()) + .build(); + + let ranges = vec![ + scan_range(150..200, ChainTip), + scan_range(100..150, Scanned), + scan_range(0..100, Ignored), + ]; + + { + let tx = st.wallet_mut().conn_mut().transaction().unwrap(); + insert_queue_entries(&tx, ranges.iter()).unwrap(); + tx.commit().unwrap(); + } + + let actual = suggest_scan_ranges(st.wallet().conn(), Ignored).unwrap(); + assert_eq!(actual, ranges); + + { + let tx = st.wallet_mut().conn_mut().transaction().unwrap(); + replace_queue_entries::( + &tx, + &(BlockHeight::from(150)..BlockHeight::from(160)), + vec![scan_range(150..160, Scanned)].into_iter(), + false, + ) + .unwrap(); + tx.commit().unwrap(); + } + + let expected = vec![ + scan_range(160..200, ChainTip), + scan_range(100..160, Scanned), + scan_range(0..100, Ignored), + ]; + + let actual = suggest_scan_ranges(st.wallet().conn(), Ignored).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn replace_queue_entries_merges_subsequent_range() { + use ScanPriority::*; + + let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory::default()) + .build(); + + let ranges = vec![ + scan_range(150..200, ChainTip), + scan_range(100..150, Scanned), + scan_range(0..100, Ignored), + ]; + + { + let tx = st.wallet_mut().conn_mut().transaction().unwrap(); + insert_queue_entries(&tx, ranges.iter()).unwrap(); + tx.commit().unwrap(); + } + + let actual = suggest_scan_ranges(st.wallet().conn(), Ignored).unwrap(); + assert_eq!(actual, ranges); + + { + let tx = st.wallet_mut().conn_mut().transaction().unwrap(); + replace_queue_entries::( + &tx, + &(BlockHeight::from(90)..BlockHeight::from(100)), + vec![scan_range(90..100, Scanned)].into_iter(), + false, + ) + .unwrap(); + tx.commit().unwrap(); + } + + let expected = vec![ + scan_range(150..200, ChainTip), + scan_range(90..150, Scanned), + scan_range(0..90, Ignored), + ]; + + let actual = suggest_scan_ranges(st.wallet().conn(), Ignored).unwrap(); + assert_eq!(actual, expected); + } + + /// This sets up the case wherein: + /// * The wallet birthday is in the shard prior to the chain tip + /// * The user receives funds in the last complete block in the birthday shard, + /// in the last note in that block. + /// * The next block crosses the shard boundary, with two notes in the prior + /// shard and two notes in the subsequent shard. + /// * An additional 110 blocks are scanned, to ensure that the checkpoint + /// is pruned. + /// + /// The diagram below shows the arrangement. the position of the X indicates the + /// note commitment for the note belonging to our wallet. + /// ``` + /// blocks: |<---- 5000 ---->|<----- 10 ---->|<--- 11 --->|<- 1 ->|<- 1 ->|<----- 110 ----->| + /// nu5_activation birthday chain_tip + /// commitments: |<---- 2^16 ---->|<--(2^16-50)-->|<--- 44 --->|<-___X->|<- 4 ->|<----- 110 ------| + /// shards: |<--- shard0 --->|<---------------- shard1 --------------->|<-------- shard2 -------->... + /// ``` + /// + /// # Parameters: + /// - `with_birthday_subtree_root`: When this is set to `true`, the wallet state will be + /// initialized such that the subtree root containing the wallet birthday has been inserted + /// into the note commitment tree. + #[cfg(feature = "orchard")] + fn prepare_orchard_block_spanning_test( + with_birthday_subtree_root: bool, + ) -> TestState { + let birthday_nu5_offset = 5000; + let birthday_prior_block_hash = BlockHash([0; 32]); + // We set the Sapling and Orchard frontiers at the birthday block initial state to 50 + // notes back from the end of the second shard. + let birthday_tree_size: u32 = (0x1 << 17) - 50; + let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory::default()) + .with_block_cache(BlockCache::new()) + .with_initial_chain_state(|rng, network| { + let birthday_height = + network.activation_height(NetworkUpgrade::Nu5).unwrap() + birthday_nu5_offset; + + let (prior_orchard_roots, orchard_initial_tree) = + Frontier::random_with_prior_subtree_roots( + rng, + birthday_tree_size.into(), + NonZeroU8::new(16).unwrap(), + ); + + // There will only be one prior root. The completion height of the first shard will + // be 10 blocks prior to the wallet birthday height. This isn't actually enough + // block space to fit in 2^16-50 note commitments, but that's irrelevant here since + // we never need to look at those blocks or those notes. + let prior_orchard_roots = prior_orchard_roots + .into_iter() + .map(|root| CommitmentTreeRoot::from_parts(birthday_height - 10, root)) + .collect::>(); + + InitialChainState { + chain_state: ChainState::new( + birthday_height - 1, + birthday_prior_block_hash, + Frontier::empty(), // the Sapling tree is unused in this test + orchard_initial_tree, + ), + prior_sapling_roots: vec![], + prior_orchard_roots, + } + }) + .with_account_having_current_birthday() + .build(); + + let account = st.test_account().cloned().unwrap(); + let birthday = account.birthday(); + + let ofvk = OrchardPoolTester::random_fvk(st.rng_mut()); + let dfvk = OrchardPoolTester::test_account_fvk(&st); + + // Create the cache by adding: + // * 11 blocks each containing 4 Orchard notes that are not for this wallet + // * 1 block containing 4 Orchard notes, the last of which belongs to this wallet + // * 1 block containing 4 Orchard notes not for this wallet, this will cross the shard + // boundary + // * another 110 blocks each containing a single note not for this wallet + { + let fake_output = |for_this_wallet| { + FakeCompactOutput::new( + if for_this_wallet { + dfvk.clone() + } else { + ofvk.clone() + }, + AddressType::DefaultExternal, + Zatoshis::const_from_u64(100000), + ) + }; + + let mut final_orchard_tree = birthday.orchard_frontier().clone(); + // Generate the birthday block plus 10 more + for _ in 0..11 { + let (_, res, _) = st.generate_next_block_multi(&vec![fake_output(false); 4]); + for c in res.orchard() { + final_orchard_tree.append(*c); + } + } + + // Generate a block with the last note in the block belonging to the wallet + let (_, res, _) = st.generate_next_block_multi(&vec![ + // 3 Orchard notes not for this wallet + fake_output(false), + fake_output(false), + fake_output(false), + // One Orchard note for this wallet + fake_output(true), + ]); + for c in res.orchard() { + final_orchard_tree.append(*c); + } + + // Generate one block spanning the shard boundary + let (spanning_block_height, res, _) = + st.generate_next_block_multi(&vec![fake_output(false); 4]); + + // Add two note commitments to the Orchard frontier to complete the 2^16 subtree. We + // can then add that subtree root to the Orchard frontier, so that we can compute the + // root of the completed subtree. + for c in res.orchard().iter().take(2) { + final_orchard_tree.append(*c); + } + + assert_eq!(final_orchard_tree.tree_size(), 0x1 << 17); + assert_eq!(spanning_block_height, birthday.height() + 12); + + // Insert the root of the completed subtree if `with_birthday_subtree_root` is set. + // This simulates the situation where the subtree roots have all been inserted prior + // to scanning. + if with_birthday_subtree_root { + st.wallet_mut() + .put_orchard_subtree_roots( + 1, + &[CommitmentTreeRoot::from_parts( + spanning_block_height, + final_orchard_tree + .value() + .unwrap() + .root(Some(Level::from(16))), + )], + ) + .unwrap(); + } + + // Add blocks up to the chain tip. + let mut chain_tip_height = spanning_block_height; + for _ in 0..110 { + let (h, res, _) = st.generate_next_block_multi(&vec![fake_output(false)]); + for c in res.orchard() { + final_orchard_tree.append(*c); + } + chain_tip_height = h; + } + + assert_eq!(chain_tip_height, birthday.height() + 122); + } + + st + } + + #[test] + #[cfg(feature = "orchard")] + fn orchard_block_spanning_tip_boundary_complete() { + use zcash_client_backend::data_api::Account as _; + + let mut st = prepare_orchard_block_spanning_test(true); + let account = st.test_account().cloned().unwrap(); + let birthday = account.birthday(); + + // set the chain tip to the final block height we expect + let new_tip = birthday.height() + 122; + st.wallet_mut().update_chain_tip(new_tip).unwrap(); + + // Verify that the suggested scan ranges includes only the chain-tip range with ChainTip + // priority, and that the range from the wallet birthday to the end of the birthday shard + // has Historic priority. + let birthday_height = birthday.height().into(); + let expected = vec![ + scan_range( + (birthday_height + 12)..(new_tip + 1).into(), + ScanPriority::ChainTip, + ), + scan_range( + birthday_height..(birthday_height + 12), + ScanPriority::Historic, + ), + scan_range( + st.sapling_activation_height().into()..birthday.height().into(), + ScanPriority::Ignored, + ), + ]; + + let actual = suggest_scan_ranges(st.wallet().conn(), ScanPriority::Ignored).unwrap(); + assert_eq!(actual, expected); + + // Scan the chain-tip range. + st.scan_cached_blocks(birthday.height() + 12, 112); + + // We haven't yet discovered our note, so balances should still be zero + assert_eq!(st.get_total_balance(account.id()), Zatoshis::ZERO); + + // Now scan the historic range; this should discover our note, which should now be + // spendable. + st.scan_cached_blocks(birthday.height(), 12); + assert_eq!( + st.get_total_balance(account.id()), + Zatoshis::const_from_u64(100000) + ); + assert_eq!( + st.get_spendable_balance(account.id(), 10), + Zatoshis::const_from_u64(100000) + ); + + // Spend the note. + let to_extsk = OrchardPoolTester::sk(&[0xf5; 32]); + let to = OrchardPoolTester::sk_default_address(&to_extsk); + let request = zip321::TransactionRequest::new(vec![zip321::Payment::without_memo( + to.to_zcash_address(st.network()), + Zatoshis::const_from_u64(10000), + )]) + .unwrap(); + + let fee_rule = StandardFeeRule::Zip317; + + let change_memo = "Test change memo".parse::().unwrap(); + let change_strategy = standard::SingleOutputChangeStrategy::new( + fee_rule, + Some(change_memo.into()), + OrchardPoolTester::SHIELDED_PROTOCOL, + DustOutputPolicy::default(), + ); + let input_selector = GreedyInputSelector::new(); + + let proposal = st + .propose_transfer( + account.id(), + &input_selector, + &change_strategy, + request, + NonZeroU32::new(10).unwrap(), + ) + .unwrap(); + + let create_proposed_result = st.create_proposed_transactions::( + account.usk(), + OvkPolicy::Sender, + &proposal, + ); + assert_matches!(&create_proposed_result, Ok(txids) if txids.len() == 1); + } + + /// This test verifies that missing a single block that is required for computing a witness is + /// sufficient to prevent witness construction. + #[test] + #[cfg(feature = "orchard")] + fn orchard_block_spanning_tip_boundary_incomplete() { + use zcash_client_backend::data_api::Account as _; + + let mut st = prepare_orchard_block_spanning_test(false); + let account = st.test_account().cloned().unwrap(); + let birthday = account.birthday(); + + // set the chain tip to the final position we expect + let new_tip = birthday.height() + 122; + st.wallet_mut().update_chain_tip(new_tip).unwrap(); + + // Verify that the suggested scan ranges includes only the chain-tip range with ChainTip + // priority, and that the range from the wallet birthday to the end of the birthday shard + // has Historic priority. + let birthday_height = birthday.height().into(); + let expected = vec![ + scan_range( + birthday_height..(new_tip + 1).into(), + ScanPriority::ChainTip, + ), + scan_range( + st.sapling_activation_height().into()..birthday_height, + ScanPriority::Ignored, + ), + ]; + + let actual = suggest_scan_ranges(st.wallet().conn(), ScanPriority::Ignored).unwrap(); + assert_eq!(actual, expected); + + // Scan the chain-tip range, but omitting the spanning block. + st.scan_cached_blocks(birthday.height() + 13, 112); + + // We haven't yet discovered our note, so balances should still be zero + assert_eq!(st.get_total_balance(account.id()), Zatoshis::ZERO); + + // Now scan the historic range; this should discover our note but not + // complete the tree. The note should not be considered spendable. + st.scan_cached_blocks(birthday.height(), 12); + assert_eq!( + st.get_total_balance(account.id()), + Zatoshis::const_from_u64(100000) + ); + assert_eq!(st.get_spendable_balance(account.id(), 10), Zatoshis::ZERO); + + // Attempting to spend the note should fail to generate a proposal + let to_extsk = OrchardPoolTester::sk(&[0xf5; 32]); + let to = OrchardPoolTester::sk_default_address(&to_extsk); + let request = zip321::TransactionRequest::new(vec![zip321::Payment::without_memo( + to.to_zcash_address(st.network()), + Zatoshis::const_from_u64(10000), + )]) + .unwrap(); + + let fee_rule = StandardFeeRule::Zip317; + + let change_memo = "Test change memo".parse::().unwrap(); + let change_strategy = standard::SingleOutputChangeStrategy::new( + fee_rule, + Some(change_memo.into()), + OrchardPoolTester::SHIELDED_PROTOCOL, + DustOutputPolicy::default(), + ); + let input_selector = GreedyInputSelector::new(); + + let proposal = st.propose_transfer( + account.id(), + &input_selector, + &change_strategy, + request.clone(), + NonZeroU32::new(10).unwrap(), + ); + + assert_matches!(proposal, Err(_)); + + // Scan the missing block + st.scan_cached_blocks(birthday.height() + 12, 1); + + // Verify that it's now possible to create the proposal + let proposal = st.propose_transfer( + account.id(), + &input_selector, + &change_strategy, + request, + NonZeroU32::new(10).unwrap(), + ); + + assert_matches!(proposal, Ok(_)); + } +} diff --git a/zcash_client_sqlite/src/wallet/transact.rs b/zcash_client_sqlite/src/wallet/transact.rs deleted file mode 100644 index 5fccb430f3..0000000000 --- a/zcash_client_sqlite/src/wallet/transact.rs +++ /dev/null @@ -1,714 +0,0 @@ -//! Functions for creating transactions. -//! -use rusqlite::{named_params, Row}; -use std::convert::TryInto; - -use ff::PrimeField; - -use zcash_primitives::{ - consensus::BlockHeight, - merkle_tree::IncrementalWitness, - sapling::{Diversifier, Rseed}, - transaction::components::Amount, -}; - -use zcash_client_backend::wallet::{AccountId, SpendableNote}; - -use crate::{error::SqliteClientError, WalletDb}; - -fn to_spendable_note(row: &Row) -> Result { - let diversifier = { - let d: Vec<_> = row.get(0)?; - if d.len() != 11 { - return Err(SqliteClientError::CorruptedData( - "Invalid diversifier length".to_string(), - )); - } - let mut tmp = [0; 11]; - tmp.copy_from_slice(&d); - Diversifier(tmp) - }; - - let note_value = Amount::from_i64(row.get(1)?).unwrap(); - - let rseed = { - let rcm_bytes: Vec<_> = row.get(2)?; - - // We store rcm directly in the data DB, regardless of whether the note - // used a v1 or v2 note plaintext, so for the purposes of spending let's - // pretend this is a pre-ZIP 212 note. - let rcm = jubjub::Fr::from_repr( - rcm_bytes[..] - .try_into() - .map_err(|_| SqliteClientError::InvalidNote)?, - ) - .ok_or(SqliteClientError::InvalidNote)?; - Rseed::BeforeZip212(rcm) - }; - - let witness = { - let d: Vec<_> = row.get(3)?; - IncrementalWitness::read(&d[..])? - }; - - Ok(SpendableNote { - diversifier, - note_value, - rseed, - witness, - }) -} - -pub fn get_spendable_notes

( - wdb: &WalletDb

, - account: AccountId, - anchor_height: BlockHeight, -) -> Result, SqliteClientError> { - let mut stmt_select_notes = wdb.conn.prepare( - "SELECT diversifier, value, rcm, witness - FROM received_notes - INNER JOIN transactions ON transactions.id_tx = received_notes.tx - INNER JOIN sapling_witnesses ON sapling_witnesses.note = received_notes.id_note - WHERE account = :account - AND spent IS NULL - AND transactions.block <= :anchor_height - AND sapling_witnesses.block = :anchor_height", - )?; - - // Select notes - let notes = stmt_select_notes.query_and_then_named::<_, SqliteClientError, _>( - named_params![ - ":account": &i64::from(account.0), - ":anchor_height": &u32::from(anchor_height), - ], - to_spendable_note, - )?; - - notes.collect::>() -} - -pub fn select_spendable_notes

( - wdb: &WalletDb

, - account: AccountId, - target_value: Amount, - anchor_height: BlockHeight, -) -> Result, SqliteClientError> { - // The goal of this SQL statement is to select the oldest notes until the required - // value has been reached, and then fetch the witnesses at the desired height for the - // selected notes. This is achieved in several steps: - // - // 1) Use a window function to create a view of all notes, ordered from oldest to - // newest, with an additional column containing a running sum: - // - Unspent notes accumulate the values of all unspent notes in that note's - // account, up to itself. - // - Spent notes accumulate the values of all notes in the transaction they were - // spent in, up to itself. - // - // 2) Select all unspent notes in the desired account, along with their running sum. - // - // 3) Select all notes for which the running sum was less than the required value, as - // well as a single note for which the sum was greater than or equal to the - // required value, bringing the sum of all selected notes across the threshold. - // - // 4) Match the selected notes against the witnesses at the desired height. - let mut stmt_select_notes = wdb.conn.prepare( - "WITH selected AS ( - WITH eligible AS ( - SELECT id_note, diversifier, value, rcm, - SUM(value) OVER - (PARTITION BY account, spent ORDER BY id_note) AS so_far - FROM received_notes - INNER JOIN transactions ON transactions.id_tx = received_notes.tx - WHERE account = :account AND spent IS NULL AND transactions.block <= :anchor_height - ) - SELECT * FROM eligible WHERE so_far < :target_value - UNION - SELECT * FROM (SELECT * FROM eligible WHERE so_far >= :target_value LIMIT 1) - ), witnesses AS ( - SELECT note, witness FROM sapling_witnesses - WHERE block = :anchor_height - ) - SELECT selected.diversifier, selected.value, selected.rcm, witnesses.witness - FROM selected - INNER JOIN witnesses ON selected.id_note = witnesses.note", - )?; - - // Select notes - let notes = stmt_select_notes.query_and_then_named::<_, SqliteClientError, _>( - named_params![ - ":account": &i64::from(account.0), - ":anchor_height": &u32::from(anchor_height), - ":target_value": &i64::from(target_value), - ], - to_spendable_note, - )?; - - notes.collect::>() -} - -#[cfg(test)] -mod tests { - use rusqlite::Connection; - use tempfile::NamedTempFile; - - use zcash_primitives::{ - block::BlockHash, - consensus::BlockHeight, - legacy::TransparentAddress, - sapling::{note_encryption::try_sapling_output_recovery, prover::TxProver}, - transaction::{components::Amount, Transaction}, - zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, - }; - - use zcash_proofs::prover::LocalTxProver; - - use zcash_client_backend::{ - data_api::{chain::scan_cached_blocks, wallet::create_spend_to_address, WalletRead}, - wallet::OvkPolicy, - }; - - use crate::{ - chain::init::init_cache_database, - tests::{self, fake_compact_block, insert_into_cache, sapling_activation_height}, - wallet::{ - get_balance, get_balance_at, - init::{init_accounts_table, init_blocks_table, init_wallet_db}, - }, - AccountId, BlockDb, DataConnStmtCache, WalletDb, - }; - - fn test_prover() -> impl TxProver { - match LocalTxProver::with_default_location() { - Some(tx_prover) => tx_prover, - None => { - panic!("Cannot locate the Zcash parameters. Please run zcash-fetch-params or fetch-params.sh to download the parameters, and then re-run the tests."); - } - } - } - - #[test] - fn create_to_address_fails_on_incorrect_extsk() { - let data_file = NamedTempFile::new().unwrap(); - let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&db_data).unwrap(); - - // Add two accounts to the wallet - let extsk0 = ExtendedSpendingKey::master(&[]); - let extsk1 = ExtendedSpendingKey::master(&[0]); - let extfvks = [ - ExtendedFullViewingKey::from(&extsk0), - ExtendedFullViewingKey::from(&extsk1), - ]; - init_accounts_table(&db_data, &extfvks).unwrap(); - let to = extsk0.default_address().unwrap().1.into(); - - // Invalid extsk for the given account should cause an error - let mut db_write = db_data.get_update_ops().unwrap(); - match create_spend_to_address( - &mut db_write, - &tests::network(), - test_prover(), - AccountId(0), - &extsk1, - &to, - Amount::from_u64(1).unwrap(), - None, - OvkPolicy::Sender, - ) { - Ok(_) => panic!("Should have failed"), - Err(e) => assert_eq!(e.to_string(), "Incorrect ExtendedSpendingKey for account 0"), - } - - match create_spend_to_address( - &mut db_write, - &tests::network(), - test_prover(), - AccountId(1), - &extsk0, - &to, - Amount::from_u64(1).unwrap(), - None, - OvkPolicy::Sender, - ) { - Ok(_) => panic!("Should have failed"), - Err(e) => assert_eq!(e.to_string(), "Incorrect ExtendedSpendingKey for account 1"), - } - } - - #[test] - fn create_to_address_fails_with_no_blocks() { - let data_file = NamedTempFile::new().unwrap(); - let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&db_data).unwrap(); - - // Add an account to the wallet - let extsk = ExtendedSpendingKey::master(&[]); - let extfvks = [ExtendedFullViewingKey::from(&extsk)]; - init_accounts_table(&db_data, &extfvks).unwrap(); - let to = extsk.default_address().unwrap().1.into(); - - // We cannot do anything if we aren't synchronised - let mut db_write = db_data.get_update_ops().unwrap(); - match create_spend_to_address( - &mut db_write, - &tests::network(), - test_prover(), - AccountId(0), - &extsk, - &to, - Amount::from_u64(1).unwrap(), - None, - OvkPolicy::Sender, - ) { - Ok(_) => panic!("Should have failed"), - Err(e) => assert_eq!(e.to_string(), "Must scan blocks first"), - } - } - - #[test] - fn create_to_address_fails_on_insufficient_balance() { - let data_file = NamedTempFile::new().unwrap(); - let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&db_data).unwrap(); - init_blocks_table( - &db_data, - BlockHeight::from(1u32), - BlockHash([1; 32]), - 1, - &[], - ) - .unwrap(); - - // Add an account to the wallet - let extsk = ExtendedSpendingKey::master(&[]); - let extfvks = [ExtendedFullViewingKey::from(&extsk)]; - init_accounts_table(&db_data, &extfvks).unwrap(); - let to = extsk.default_address().unwrap().1.into(); - - // Account balance should be zero - assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), Amount::zero()); - - // We cannot spend anything - let mut db_write = db_data.get_update_ops().unwrap(); - match create_spend_to_address( - &mut db_write, - &tests::network(), - test_prover(), - AccountId(0), - &extsk, - &to, - Amount::from_u64(1).unwrap(), - None, - OvkPolicy::Sender, - ) { - Ok(_) => panic!("Should have failed"), - Err(e) => assert_eq!( - e.to_string(), - "Insufficient balance (have 0, need 1001 including fee)" - ), - } - } - - #[test] - fn create_to_address_fails_on_unverified_notes() { - let cache_file = NamedTempFile::new().unwrap(); - let db_cache = BlockDb(Connection::open(cache_file.path()).unwrap()); - init_cache_database(&db_cache).unwrap(); - - let data_file = NamedTempFile::new().unwrap(); - let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&db_data).unwrap(); - - // Add an account to the wallet - let extsk = ExtendedSpendingKey::master(&[]); - let extfvk = ExtendedFullViewingKey::from(&extsk); - init_accounts_table(&db_data, &[extfvk.clone()]).unwrap(); - - // Add funds to the wallet in a single note - let value = Amount::from_u64(50000).unwrap(); - let (cb, _) = fake_compact_block( - sapling_activation_height(), - BlockHash([0; 32]), - extfvk.clone(), - value, - ); - insert_into_cache(&db_cache, &cb); - let mut db_write = db_data.get_update_ops().unwrap(); - scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); - - // Verified balance matches total balance - let (_, anchor_height) = (&db_data).get_target_and_anchor_heights().unwrap().unwrap(); - assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value); - assert_eq!( - get_balance_at(&db_data, AccountId(0), anchor_height).unwrap(), - value - ); - - // Add more funds to the wallet in a second note - let (cb, _) = fake_compact_block( - sapling_activation_height() + 1, - cb.hash(), - extfvk.clone(), - value, - ); - insert_into_cache(&db_cache, &cb); - scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); - - // Verified balance does not include the second note - let (_, anchor_height2) = (&db_data).get_target_and_anchor_heights().unwrap().unwrap(); - assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value + value); - assert_eq!( - get_balance_at(&db_data, AccountId(0), anchor_height2).unwrap(), - value - ); - - // Spend fails because there are insufficient verified notes - let extsk2 = ExtendedSpendingKey::master(&[]); - let to = extsk2.default_address().unwrap().1.into(); - match create_spend_to_address( - &mut db_write, - &tests::network(), - test_prover(), - AccountId(0), - &extsk, - &to, - Amount::from_u64(70000).unwrap(), - None, - OvkPolicy::Sender, - ) { - Ok(_) => panic!("Should have failed"), - Err(e) => assert_eq!( - e.to_string(), - "Insufficient balance (have 50000, need 71000 including fee)" - ), - } - - // Mine blocks SAPLING_ACTIVATION_HEIGHT + 2 to 9 until just before the second - // note is verified - for i in 2..10 { - let (cb, _) = fake_compact_block( - sapling_activation_height() + i, - cb.hash(), - extfvk.clone(), - value, - ); - insert_into_cache(&db_cache, &cb); - } - scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); - - // Second spend still fails - match create_spend_to_address( - &mut db_write, - &tests::network(), - test_prover(), - AccountId(0), - &extsk, - &to, - Amount::from_u64(70000).unwrap(), - None, - OvkPolicy::Sender, - ) { - Ok(_) => panic!("Should have failed"), - Err(e) => assert_eq!( - e.to_string(), - "Insufficient balance (have 50000, need 71000 including fee)" - ), - } - - // Mine block 11 so that the second note becomes verified - let (cb, _) = - fake_compact_block(sapling_activation_height() + 10, cb.hash(), extfvk, value); - insert_into_cache(&db_cache, &cb); - scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); - - // Second spend should now succeed - create_spend_to_address( - &mut db_write, - &tests::network(), - test_prover(), - AccountId(0), - &extsk, - &to, - Amount::from_u64(70000).unwrap(), - None, - OvkPolicy::Sender, - ) - .unwrap(); - } - - #[test] - fn create_to_address_fails_on_locked_notes() { - let cache_file = NamedTempFile::new().unwrap(); - let db_cache = BlockDb(Connection::open(cache_file.path()).unwrap()); - init_cache_database(&db_cache).unwrap(); - - let data_file = NamedTempFile::new().unwrap(); - let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&db_data).unwrap(); - - // Add an account to the wallet - let extsk = ExtendedSpendingKey::master(&[]); - let extfvk = ExtendedFullViewingKey::from(&extsk); - init_accounts_table(&db_data, &[extfvk.clone()]).unwrap(); - - // Add funds to the wallet in a single note - let value = Amount::from_u64(50000).unwrap(); - let (cb, _) = fake_compact_block( - sapling_activation_height(), - BlockHash([0; 32]), - extfvk, - value, - ); - insert_into_cache(&db_cache, &cb); - let mut db_write = db_data.get_update_ops().unwrap(); - scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); - assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value); - - // Send some of the funds to another address - let extsk2 = ExtendedSpendingKey::master(&[]); - let to = extsk2.default_address().unwrap().1.into(); - create_spend_to_address( - &mut db_write, - &tests::network(), - test_prover(), - AccountId(0), - &extsk, - &to, - Amount::from_u64(15000).unwrap(), - None, - OvkPolicy::Sender, - ) - .unwrap(); - - // A second spend fails because there are no usable notes - match create_spend_to_address( - &mut db_write, - &tests::network(), - test_prover(), - AccountId(0), - &extsk, - &to, - Amount::from_u64(2000).unwrap(), - None, - OvkPolicy::Sender, - ) { - Ok(_) => panic!("Should have failed"), - Err(e) => assert_eq!( - e.to_string(), - "Insufficient balance (have 0, need 3000 including fee)" - ), - } - - // Mine blocks SAPLING_ACTIVATION_HEIGHT + 1 to 21 (that don't send us funds) - // until just before the first transaction expires - for i in 1..22 { - let (cb, _) = fake_compact_block( - sapling_activation_height() + i, - cb.hash(), - ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[i as u8])), - value, - ); - insert_into_cache(&db_cache, &cb); - } - scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); - - // Second spend still fails - match create_spend_to_address( - &mut db_write, - &tests::network(), - test_prover(), - AccountId(0), - &extsk, - &to, - Amount::from_u64(2000).unwrap(), - None, - OvkPolicy::Sender, - ) { - Ok(_) => panic!("Should have failed"), - Err(e) => assert_eq!( - e.to_string(), - "Insufficient balance (have 0, need 3000 including fee)" - ), - } - - // Mine block SAPLING_ACTIVATION_HEIGHT + 22 so that the first transaction expires - let (cb, _) = fake_compact_block( - sapling_activation_height() + 22, - cb.hash(), - ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[22])), - value, - ); - insert_into_cache(&db_cache, &cb); - scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); - - // Second spend should now succeed - create_spend_to_address( - &mut db_write, - &tests::network(), - test_prover(), - AccountId(0), - &extsk, - &to, - Amount::from_u64(2000).unwrap(), - None, - OvkPolicy::Sender, - ) - .unwrap(); - } - - #[test] - fn ovk_policy_prevents_recovery_from_chain() { - let network = tests::network(); - let cache_file = NamedTempFile::new().unwrap(); - let db_cache = BlockDb(Connection::open(cache_file.path()).unwrap()); - init_cache_database(&db_cache).unwrap(); - - let data_file = NamedTempFile::new().unwrap(); - let db_data = WalletDb::for_path(data_file.path(), network).unwrap(); - init_wallet_db(&db_data).unwrap(); - - // Add an account to the wallet - let extsk = ExtendedSpendingKey::master(&[]); - let extfvk = ExtendedFullViewingKey::from(&extsk); - init_accounts_table(&db_data, &[extfvk.clone()]).unwrap(); - - // Add funds to the wallet in a single note - let value = Amount::from_u64(50000).unwrap(); - let (cb, _) = fake_compact_block( - sapling_activation_height(), - BlockHash([0; 32]), - extfvk.clone(), - value, - ); - insert_into_cache(&db_cache, &cb); - let mut db_write = db_data.get_update_ops().unwrap(); - scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); - assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value); - - let extsk2 = ExtendedSpendingKey::master(&[]); - let addr2 = extsk2.default_address().unwrap().1; - let to = addr2.clone().into(); - - let send_and_recover_with_policy = |db_write: &mut DataConnStmtCache<'_, _>, ovk_policy| { - let tx_row = create_spend_to_address( - db_write, - &tests::network(), - test_prover(), - AccountId(0), - &extsk, - &to, - Amount::from_u64(15000).unwrap(), - None, - ovk_policy, - ) - .unwrap(); - - // Fetch the transaction from the database - let raw_tx: Vec<_> = db_write - .wallet_db - .conn - .query_row( - "SELECT raw FROM transactions - WHERE id_tx = ?", - &[tx_row], - |row| row.get(0), - ) - .unwrap(); - let tx = Transaction::read(&raw_tx[..]).unwrap(); - - // Fetch the output index from the database - let output_index: i64 = db_write - .wallet_db - .conn - .query_row( - "SELECT output_index FROM sent_notes - WHERE tx = ?", - &[tx_row], - |row| row.get(0), - ) - .unwrap(); - - let output = &tx.shielded_outputs[output_index as usize]; - - try_sapling_output_recovery( - &network, - sapling_activation_height(), - &extfvk.fvk.ovk, - output, - ) - }; - - // Send some of the funds to another address, keeping history. - // The recipient output is decryptable by the sender. - let (_, recovered_to, _) = - send_and_recover_with_policy(&mut db_write, OvkPolicy::Sender).unwrap(); - assert_eq!(&recovered_to, &addr2); - - // Mine blocks SAPLING_ACTIVATION_HEIGHT + 1 to 22 (that don't send us funds) - // so that the first transaction expires - for i in 1..=22 { - let (cb, _) = fake_compact_block( - sapling_activation_height() + i, - cb.hash(), - ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[i as u8])), - value, - ); - insert_into_cache(&db_cache, &cb); - } - scan_cached_blocks(&network, &db_cache, &mut db_write, None).unwrap(); - - // Send the funds again, discarding history. - // Neither transaction output is decryptable by the sender. - assert!(send_and_recover_with_policy(&mut db_write, OvkPolicy::Discard).is_none()); - } - - #[test] - fn create_to_address_succeeds_to_t_addr_zero_change() { - let cache_file = NamedTempFile::new().unwrap(); - let db_cache = BlockDb(Connection::open(cache_file.path()).unwrap()); - init_cache_database(&db_cache).unwrap(); - - let data_file = NamedTempFile::new().unwrap(); - let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&db_data).unwrap(); - - // Add an account to the wallet - let extsk = ExtendedSpendingKey::master(&[]); - let extfvk = ExtendedFullViewingKey::from(&extsk); - init_accounts_table(&db_data, &[extfvk.clone()]).unwrap(); - - // Add funds to the wallet in a single note - let value = Amount::from_u64(51000).unwrap(); - let (cb, _) = fake_compact_block( - sapling_activation_height(), - BlockHash([0; 32]), - extfvk, - value, - ); - insert_into_cache(&db_cache, &cb); - let mut db_write = db_data.get_update_ops().unwrap(); - scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); - - // Verified balance matches total balance - let (_, anchor_height) = (&db_data).get_target_and_anchor_heights().unwrap().unwrap(); - assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value); - assert_eq!( - get_balance_at(&db_data, AccountId(0), anchor_height).unwrap(), - value - ); - - let to = TransparentAddress::PublicKey([7; 20]).into(); - create_spend_to_address( - &mut db_write, - &tests::network(), - test_prover(), - AccountId(0), - &extsk, - &to, - Amount::from_u64(50000).unwrap(), - None, - OvkPolicy::Sender, - ) - .unwrap(); - } -} diff --git a/zcash_client_sqlite/src/wallet/transparent.rs b/zcash_client_sqlite/src/wallet/transparent.rs new file mode 100644 index 0000000000..47ee17d967 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/transparent.rs @@ -0,0 +1,982 @@ +//! Functions for transparent input support in the wallet. +use std::collections::{HashMap, HashSet}; + +use rusqlite::OptionalExtension; +use rusqlite::{named_params, Connection, Row}; + +use ::transparent::{ + address::{Script, TransparentAddress}, + bundle::{OutPoint, TxOut}, + keys::{IncomingViewingKey, NonHardenedChildIndex}, +}; +use zcash_address::unified::{Ivk, Uivk}; +use zcash_client_backend::{ + data_api::{AccountBalance, TransactionDataRequest}, + wallet::{TransparentAddressMetadata, WalletTransparentOutput}, +}; +use zcash_keys::{address::Address, encoding::AddressCodec}; +use zcash_primitives::transaction::builder::DEFAULT_TX_EXPIRY_DELTA; +use zcash_protocol::{ + consensus::{self, BlockHeight}, + value::{ZatBalance, Zatoshis}, +}; +use zip32::{DiversifierIndex, Scope}; + +use super::{chain_tip_height, get_account_ids}; +use crate::AccountUuid; +use crate::{error::SqliteClientError, TxRef, UtxoId}; + +pub(crate) mod ephemeral; + +pub(crate) fn detect_spending_accounts<'a>( + conn: &Connection, + spent: impl Iterator, +) -> Result, rusqlite::Error> { + let mut account_q = conn.prepare_cached( + "SELECT accounts.uuid + FROM transparent_received_outputs o + JOIN accounts ON accounts.id = o.account_id + JOIN transactions t ON t.id_tx = o.transaction_id + WHERE t.txid = :prevout_txid + AND o.output_index = :prevout_idx", + )?; + + let mut acc = HashSet::new(); + for prevout in spent { + for account in account_q.query_and_then( + named_params![ + ":prevout_txid": prevout.hash(), + ":prevout_idx": prevout.n() + ], + |row| row.get(0).map(AccountUuid), + )? { + acc.insert(account?); + } + } + + Ok(acc) +} + +/// Returns the `NonHardenedChildIndex` corresponding to a diversifier index +/// given as bytes in big-endian order (the reverse of the usual order). +fn address_index_from_diversifier_index_be( + diversifier_index_be: &[u8], +) -> Result { + let mut di: [u8; 11] = diversifier_index_be.try_into().map_err(|_| { + SqliteClientError::CorruptedData("Diversifier index is not an 11-byte value".to_owned()) + })?; + di.reverse(); // BE -> LE conversion + + NonHardenedChildIndex::from_index(DiversifierIndex::from(di).try_into().map_err(|_| { + SqliteClientError::CorruptedData( + "Unable to get diversifier for transparent address.".to_string(), + ) + })?) + .ok_or_else(|| { + SqliteClientError::CorruptedData( + "Unexpected hardened index for transparent address.".to_string(), + ) + }) +} + +pub(crate) fn get_transparent_receivers( + conn: &rusqlite::Connection, + params: &P, + account_uuid: AccountUuid, +) -> Result>, SqliteClientError> { + let mut ret: HashMap> = HashMap::new(); + + // Get all UAs derived + let mut ua_query = conn.prepare( + "SELECT address, diversifier_index_be + FROM addresses + JOIN accounts ON accounts.id = addresses.account_id + WHERE accounts.uuid = :account_uuid", + )?; + let mut rows = ua_query.query(named_params![":account_uuid": account_uuid.0])?; + + while let Some(row) = rows.next()? { + let ua_str: String = row.get(0)?; + let di_vec: Vec = row.get(1)?; + + let ua = Address::decode(params, &ua_str) + .ok_or_else(|| { + SqliteClientError::CorruptedData("Not a valid Zcash recipient address".to_owned()) + }) + .and_then(|addr| match addr { + Address::Unified(ua) => Ok(ua), + _ => Err(SqliteClientError::CorruptedData(format!( + "Addresses table contains {} which is not a unified address", + ua_str, + ))), + })?; + + if let Some(taddr) = ua.transparent() { + let address_index = address_index_from_diversifier_index_be(&di_vec)?; + let metadata = TransparentAddressMetadata::new(Scope::External.into(), address_index); + ret.insert(*taddr, Some(metadata)); + } + } + + if let Some((taddr, address_index)) = + get_legacy_transparent_address(params, conn, account_uuid)? + { + let metadata = TransparentAddressMetadata::new(Scope::External.into(), address_index); + ret.insert(taddr, Some(metadata)); + } + + Ok(ret) +} + +pub(crate) fn uivk_legacy_transparent_address( + params: &P, + uivk_str: &str, +) -> Result, SqliteClientError> { + use ::transparent::keys::ExternalIvk; + use zcash_address::unified::{Container as _, Encoding as _}; + + let (network, uivk) = Uivk::decode(uivk_str) + .map_err(|e| SqliteClientError::CorruptedData(format!("Unable to parse UIVK: {e}")))?; + + if params.network_type() != network { + let network_name = |n| match n { + consensus::NetworkType::Main => "mainnet", + consensus::NetworkType::Test => "testnet", + consensus::NetworkType::Regtest => "regtest", + }; + return Err(SqliteClientError::CorruptedData(format!( + "Network type mismatch: account UIVK is for {} but a {} address was requested.", + network_name(network), + network_name(params.network_type()) + ))); + } + + // Derive the default transparent address (if it wasn't already part of a derived UA). + for item in uivk.items() { + if let Ivk::P2pkh(tivk_bytes) = item { + let tivk = ExternalIvk::deserialize(&tivk_bytes)?; + return Ok(Some(tivk.default_address())); + } + } + + Ok(None) +} + +pub(crate) fn get_legacy_transparent_address( + params: &P, + conn: &rusqlite::Connection, + account_uuid: AccountUuid, +) -> Result, SqliteClientError> { + // Get the UIVK for the account. + let uivk_str: Option = conn + .query_row( + "SELECT uivk FROM accounts WHERE uuid = :account_uuid", + named_params![":account_uuid": account_uuid.0], + |row| row.get(0), + ) + .optional()?; + + if let Some(uivk_str) = uivk_str { + return uivk_legacy_transparent_address(params, &uivk_str); + } + + Ok(None) +} + +fn to_unspent_transparent_output(row: &Row) -> Result { + let txid: Vec = row.get("txid")?; + let mut txid_bytes = [0u8; 32]; + txid_bytes.copy_from_slice(&txid); + + let index: u32 = row.get("output_index")?; + let script_pubkey = Script(row.get("script")?); + let raw_value: i64 = row.get("value_zat")?; + let value = Zatoshis::from_nonnegative_i64(raw_value).map_err(|_| { + SqliteClientError::CorruptedData(format!("Invalid UTXO value: {}", raw_value)) + })?; + let height: Option = row.get("received_height")?; + + let outpoint = OutPoint::new(txid_bytes, index); + WalletTransparentOutput::from_parts( + outpoint, + TxOut { + value, + script_pubkey, + }, + height.map(BlockHeight::from), + ) + .ok_or_else(|| { + SqliteClientError::CorruptedData( + "Txout script_pubkey value did not correspond to a P2PKH or P2SH address".to_string(), + ) + }) +} + +/// Select an output to fund a new transaction that is targeting at least `chain_tip_height + 1`. +pub(crate) fn get_wallet_transparent_output( + conn: &rusqlite::Connection, + outpoint: &OutPoint, + allow_unspendable: bool, +) -> Result, SqliteClientError> { + let chain_tip_height = chain_tip_height(conn)?; + + // This could return as unspent outputs that are actually not spendable, if they are the + // outputs of deshielding transactions where the spend anchors have been invalidated by a + // rewind or spent in a transaction that has not been observed by this wallet. There isn't a + // way to detect the circumstance related to anchor invalidation at present, but it should be + // vanishingly rare as the vast majority of rewinds are of a single block. + let mut stmt_select_utxo = conn.prepare_cached( + "SELECT t.txid, u.output_index, u.script, + u.value_zat, t.mined_height AS received_height + FROM transparent_received_outputs u + JOIN transactions t ON t.id_tx = u.transaction_id + WHERE t.txid = :txid + AND u.output_index = :output_index + -- the transaction that created the output is mined or is definitely unexpired + AND ( + :allow_unspendable + OR ( + ( + t.mined_height IS NOT NULL -- tx is mined + -- TODO: uncomment the following two lines in order to enable zero-conf spends + -- OR t.expiry_height = 0 -- tx will not expire + -- OR t.expiry_height >= :mempool_height -- tx has not yet expired + ) + -- and the output is unspent + AND u.id NOT IN ( + SELECT txo_spends.transparent_received_output_id + FROM transparent_received_output_spends txo_spends + JOIN transactions tx ON tx.id_tx = txo_spends.transaction_id + WHERE tx.mined_height IS NOT NULL -- the spending tx is mined + OR tx.expiry_height = 0 -- the spending tx will not expire + OR tx.expiry_height >= :mempool_height -- the spending tx has not yet expired + ) + ) + )", + )?; + + let result: Result, SqliteClientError> = stmt_select_utxo + .query_and_then( + named_params![ + ":txid": outpoint.hash(), + ":output_index": outpoint.n(), + ":mempool_height": chain_tip_height.map(|h| u32::from(h) + 1), + ":allow_unspendable": allow_unspendable + ], + to_unspent_transparent_output, + )? + .next() + .transpose(); + + result +} + +/// Returns the list of spendable transparent outputs received by this wallet at `address` +/// such that, at height `target_height`: +/// * the transaction that produced the output had or will have at least `min_confirmations` +/// confirmations; and +/// * the output is unspent as of the current chain tip. +/// +/// An output that is potentially spent by an unmined transaction in the mempool is excluded +/// iff the spending transaction will not be expired at `target_height`. +/// +/// This could, in very rare circumstances, return as unspent outputs that are actually not +/// spendable, if they are the outputs of deshielding transactions where the spend anchors have +/// been invalidated by a rewind. There isn't a way to detect this circumstance at present, but +/// it should be vanishingly rare as the vast majority of rewinds are of a single block. +pub(crate) fn get_spendable_transparent_outputs( + conn: &rusqlite::Connection, + params: &P, + address: &TransparentAddress, + target_height: BlockHeight, + min_confirmations: u32, +) -> Result, SqliteClientError> { + let confirmed_height = target_height - min_confirmations; + + let mut stmt_utxos = conn.prepare( + "SELECT t.txid, u.output_index, u.script, + u.value_zat, t.mined_height AS received_height + FROM transparent_received_outputs u + JOIN transactions t ON t.id_tx = u.transaction_id + WHERE u.address = :address + -- the transaction that created the output is mined or unexpired as of `confirmed_height` + AND ( + t.mined_height <= :confirmed_height -- tx is mined + -- TODO: uncomment the following lines in order to enable zero-conf spends + -- OR ( + -- :min_confirmations = 0 + -- AND ( + -- t.expiry_height = 0 -- tx will not expire + -- OR t.expiry_height >= :target_height + -- ) + -- ) + ) + -- and the output is unspent + AND u.id NOT IN ( + SELECT txo_spends.transparent_received_output_id + FROM transparent_received_output_spends txo_spends + JOIN transactions tx ON tx.id_tx = txo_spends.transaction_id + WHERE tx.mined_height IS NOT NULL -- the spending transaction is mined + OR tx.expiry_height = 0 -- the spending tx will not expire + OR tx.expiry_height >= :target_height -- the spending tx has not yet expired + -- we are intentionally conservative and exclude outputs that are potentially spent + -- as of the target height, even if they might actually be spendable due to expiry + -- of the spending transaction as of the chain tip + )", + )?; + + let addr_str = address.encode(params); + let mut rows = stmt_utxos.query(named_params![ + ":address": addr_str, + ":confirmed_height": u32::from(confirmed_height), + ":target_height": u32::from(target_height), + //":min_confirmations": min_confirmations + ])?; + + let mut utxos = Vec::::new(); + while let Some(row) = rows.next()? { + let output = to_unspent_transparent_output(row)?; + utxos.push(output); + } + + Ok(utxos) +} + +/// Returns a mapping from each transparent receiver associated with the specified account +/// to its not-yet-shielded UTXO balance, including only the effects of transactions mined +/// at a block height less than or equal to `summary_height`. +/// +/// Only non-ephemeral transparent receivers with a non-zero balance at the summary height +/// will be included. +pub(crate) fn get_transparent_balances( + conn: &rusqlite::Connection, + params: &P, + account_uuid: AccountUuid, + summary_height: BlockHeight, +) -> Result, SqliteClientError> { + let chain_tip_height = chain_tip_height(conn)?.ok_or(SqliteClientError::ChainHeightUnknown)?; + + let mut stmt_address_balances = conn.prepare( + "SELECT u.address, SUM(u.value_zat) + FROM transparent_received_outputs u + JOIN accounts ON accounts.id = u.account_id + JOIN transactions t ON t.id_tx = u.transaction_id + WHERE accounts.uuid = :account_uuid + -- the transaction that created the output is mined or is definitely unexpired + AND ( + t.mined_height <= :summary_height -- tx is mined + OR ( -- or the caller has requested to include zero-conf funds that are not expired + :summary_height > :chain_tip_height + AND ( + t.expiry_height = 0 -- tx will not expire + OR t.expiry_height >= :summary_height + ) + ) + ) + -- and the output is unspent + AND u.id NOT IN ( + SELECT txo_spends.transparent_received_output_id + FROM transparent_received_output_spends txo_spends + JOIN transactions tx ON tx.id_tx = txo_spends.transaction_id + WHERE tx.mined_height IS NOT NULL -- the spending tx is mined + OR tx.expiry_height = 0 -- the spending tx will not expire + OR tx.expiry_height >= :spend_expiry_height -- the spending tx is unexpired + ) + GROUP BY u.address", + )?; + + let mut res = HashMap::new(); + let mut rows = stmt_address_balances.query(named_params![ + ":account_uuid": account_uuid.0, + ":summary_height": u32::from(summary_height), + ":chain_tip_height": u32::from(chain_tip_height), + ":spend_expiry_height": u32::from(std::cmp::min(summary_height, chain_tip_height + 1)), + ])?; + while let Some(row) = rows.next()? { + let taddr_str: String = row.get(0)?; + let taddr = TransparentAddress::decode(params, &taddr_str)?; + let value = Zatoshis::from_nonnegative_i64(row.get(1)?)?; + + res.insert(taddr, value); + } + + Ok(res) +} + +#[tracing::instrument(skip(conn, account_balances))] +pub(crate) fn add_transparent_account_balances( + conn: &rusqlite::Connection, + mempool_height: BlockHeight, + min_confirmations: u32, + account_balances: &mut HashMap, +) -> Result<(), SqliteClientError> { + // TODO (#1592): Ability to distinguish between Transparent pending change and pending non-change + let mut stmt_account_spendable_balances = conn.prepare( + "SELECT a.uuid, SUM(u.value_zat) + FROM transparent_received_outputs u + JOIN accounts a ON a.id = u.account_id + JOIN transactions t ON t.id_tx = u.transaction_id + -- the transaction that created the output is mined and with enough confirmations + WHERE ( + t.mined_height < :mempool_height -- tx is mined + AND :mempool_height - t.mined_height >= :min_confirmations -- has at least min_confirmations + ) + -- and the received txo is unspent + AND u.id NOT IN ( + SELECT transparent_received_output_id + FROM transparent_received_output_spends txo_spends + JOIN transactions tx + ON tx.id_tx = txo_spends.transaction_id + WHERE tx.mined_height IS NOT NULL -- the spending tx is mined + OR tx.expiry_height = 0 -- the spending tx will not expire + OR tx.expiry_height >= :mempool_height -- the spending tx is unexpired + ) + GROUP BY a.uuid", + )?; + let mut rows = stmt_account_spendable_balances.query(named_params![ + ":mempool_height": u32::from(mempool_height), + ":min_confirmations": min_confirmations, + ])?; + + while let Some(row) = rows.next()? { + let account = AccountUuid(row.get(0)?); + let raw_value = row.get(1)?; + let value = Zatoshis::from_nonnegative_i64(raw_value).map_err(|_| { + SqliteClientError::CorruptedData(format!("Negative UTXO value {:?}", raw_value)) + })?; + + account_balances + .entry(account) + .or_insert(AccountBalance::ZERO) + .with_unshielded_balance_mut(|bal| bal.add_spendable_value(value))?; + } + + let mut stmt_account_unconfirmed_balances = conn.prepare( + "SELECT a.uuid, SUM(u.value_zat) + FROM transparent_received_outputs u + JOIN accounts a ON a.id = u.account_id + JOIN transactions t ON t.id_tx = u.transaction_id + -- the transaction that created the output is mined with not enough confirmations or is definitely unexpired + WHERE ( + t.mined_height < :mempool_height + AND :mempool_height - t.mined_height < :min_confirmations -- tx is mined but not confirmed + OR t.expiry_height = 0 -- tx will not expire + OR t.expiry_height >= :mempool_height + ) + -- and the received txo is unspent + AND u.id NOT IN ( + SELECT transparent_received_output_id + FROM transparent_received_output_spends txo_spends + JOIN transactions tx + ON tx.id_tx = txo_spends.transaction_id + WHERE tx.mined_height IS NOT NULL -- the spending tx is mined + OR tx.expiry_height = 0 -- the spending tx will not expire + OR tx.expiry_height >= :mempool_height -- the spending tx is unexpired + ) + GROUP BY a.uuid", + )?; + + let mut rows = stmt_account_unconfirmed_balances.query(named_params![ + ":mempool_height": u32::from(mempool_height), + ":min_confirmations": min_confirmations, + ])?; + + while let Some(row) = rows.next()? { + let account = AccountUuid(row.get(0)?); + let raw_value = row.get(1)?; + let value = Zatoshis::from_nonnegative_i64(raw_value).map_err(|_| { + SqliteClientError::CorruptedData(format!("Negative UTXO value {:?}", raw_value)) + })?; + + account_balances + .entry(account) + .or_insert(AccountBalance::ZERO) + .with_unshielded_balance_mut(|bal| bal.add_pending_spendable_value(value))?; + } + Ok(()) +} + +/// Marks the given UTXO as having been spent. +/// +/// Returns `true` if the UTXO was known to the wallet. +pub(crate) fn mark_transparent_utxo_spent( + conn: &rusqlite::Connection, + spent_in_tx: TxRef, + outpoint: &OutPoint, +) -> Result { + let spend_params = named_params![ + ":spent_in_tx": spent_in_tx.0, + ":prevout_txid": outpoint.hash(), + ":prevout_idx": outpoint.n(), + ]; + let mut stmt_mark_transparent_utxo_spent = conn.prepare_cached( + "INSERT INTO transparent_received_output_spends (transparent_received_output_id, transaction_id) + SELECT txo.id, :spent_in_tx + FROM transparent_received_outputs txo + JOIN transactions t ON t.id_tx = txo.transaction_id + WHERE t.txid = :prevout_txid + AND txo.output_index = :prevout_idx + ON CONFLICT (transparent_received_output_id, transaction_id) + -- The following UPDATE is effectively a no-op, but we perform it anyway so that the + -- number of affected rows can be used to determine whether a record existed. + DO UPDATE SET transaction_id = :spent_in_tx", + )?; + let affected_rows = stmt_mark_transparent_utxo_spent.execute(spend_params)?; + + // Since we know that the output is spent, we no longer need to search for + // it to find out if it has been spent. + let mut stmt_remove_spend_detection = conn.prepare_cached( + "DELETE FROM transparent_spend_search_queue + WHERE output_index = :prevout_idx + AND transaction_id IN ( + SELECT id_tx FROM transactions WHERE txid = :prevout_txid + )", + )?; + stmt_remove_spend_detection.execute(named_params![ + ":prevout_txid": outpoint.hash(), + ":prevout_idx": outpoint.n(), + ])?; + + // If no rows were affected, we know that we don't actually have the output in + // `transparent_received_outputs` yet, so we have to record the output as spent + // so that when we eventually detect the output, we can create the spend record. + if affected_rows == 0 { + conn.execute( + "INSERT INTO transparent_spend_map ( + spending_transaction_id, + prevout_txid, + prevout_output_index + ) + VALUES (:spent_in_tx, :prevout_txid, :prevout_idx) + ON CONFLICT (spending_transaction_id, prevout_txid, prevout_output_index) + DO NOTHING", + spend_params, + )?; + } + + Ok(affected_rows > 0) +} + +/// Adds the given received UTXO to the datastore. +pub(crate) fn put_received_transparent_utxo( + conn: &rusqlite::Connection, + params: &P, + output: &WalletTransparentOutput, +) -> Result { + let address = output.recipient_address(); + if let Some(receiving_account) = + find_account_uuid_for_transparent_address(conn, params, address)? + { + put_transparent_output( + conn, + params, + output.outpoint(), + output.txout(), + output.mined_height(), + address, + receiving_account, + true, + ) + } else { + // The UTXO was not for any of our transparent addresses. + Err(SqliteClientError::AddressNotRecognized(*address)) + } +} + +/// Returns the vector of [`TransactionDataRequest`]s that represents the information needed by the +/// wallet backend in order to be able to present a complete view of wallet history and memo data. +pub(crate) fn transaction_data_requests( + conn: &rusqlite::Connection, + params: &P, +) -> Result, SqliteClientError> { + // `lightwalletd` will return an error for `GetTaddressTxids` requests having an end height + // greater than the current chain tip height, so we take the chain tip height into account + // here in order to make this pothole easier for clients of the API to avoid. + let chain_tip_height = super::chain_tip_height(conn)?; + + // We cannot construct address-based transaction data requests for the case where we cannot + // determine the height at which to begin, so we require that either the target height or mined + // height be set. + let mut address_request_stmt = conn.prepare_cached( + "SELECT ssq.address, IFNULL(t.target_height, t.mined_height) + FROM transparent_spend_search_queue ssq + JOIN transactions t ON t.id_tx = ssq.transaction_id + WHERE t.target_height IS NOT NULL + OR t.mined_height IS NOT NULL", + )?; + + let result = address_request_stmt + .query_and_then([], |row| { + let address = TransparentAddress::decode(params, &row.get::<_, String>(0)?)?; + let block_range_start = BlockHeight::from(row.get::<_, u32>(1)?); + let max_end_height = block_range_start + DEFAULT_TX_EXPIRY_DELTA + 1; + + Ok::( + TransactionDataRequest::SpendsFromAddress { + address, + block_range_start, + block_range_end: Some( + chain_tip_height + .map_or(max_end_height, |h| std::cmp::min(h + 1, max_end_height)), + ), + }, + ) + })? + .collect::, _>>()?; + + Ok(result) +} + +pub(crate) fn get_transparent_address_metadata( + conn: &rusqlite::Connection, + params: &P, + account_uuid: AccountUuid, + address: &TransparentAddress, +) -> Result, SqliteClientError> { + let address_str = address.encode(params); + + if let Some(di_vec) = conn + .query_row( + "SELECT diversifier_index_be FROM addresses + JOIN accounts ON addresses.account_id = accounts.id + WHERE accounts.uuid = :account_uuid + AND cached_transparent_receiver_address = :address", + named_params![":account_uuid": account_uuid.0, ":address": &address_str], + |row| row.get::<_, Vec>(0), + ) + .optional()? + { + let address_index = address_index_from_diversifier_index_be(&di_vec)?; + let metadata = TransparentAddressMetadata::new(Scope::External.into(), address_index); + return Ok(Some(metadata)); + } + + if let Some((legacy_taddr, address_index)) = + get_legacy_transparent_address(params, conn, account_uuid)? + { + if &legacy_taddr == address { + let metadata = TransparentAddressMetadata::new(Scope::External.into(), address_index); + return Ok(Some(metadata)); + } + } + + // Search known ephemeral addresses. + if let Some(address_index) = + ephemeral::find_index_for_ephemeral_address_str(conn, account_uuid, &address_str)? + { + return Ok(Some(ephemeral::metadata(address_index))); + } + + Ok(None) +} + +/// Attempts to determine the account that received the given transparent output. +/// +/// The following three locations in the wallet's key tree are searched: +/// - Transparent receivers that have been generated as part of a Unified Address. +/// - Transparent ephemeral addresses that have been reserved or are within +/// the gap limit from the last reserved address. +/// - "Legacy transparent addresses" (at BIP 44 address index 0 within an account). +/// +/// Returns `Ok(None)` if the transparent output's recipient address is not in any of the +/// above locations. This means the wallet considers the output "not interesting". +pub(crate) fn find_account_uuid_for_transparent_address( + conn: &rusqlite::Connection, + params: &P, + address: &TransparentAddress, +) -> Result, SqliteClientError> { + let address_str = address.encode(params); + + if let Some(account_id) = conn + .query_row( + "SELECT accounts.uuid + FROM addresses + JOIN accounts ON accounts.id = addresses.account_id + WHERE cached_transparent_receiver_address = :address", + named_params![":address": &address_str], + |row| Ok(AccountUuid(row.get(0)?)), + ) + .optional()? + { + return Ok(Some(account_id)); + } + + // Search known ephemeral addresses. + if let Some(account_id) = ephemeral::find_account_for_ephemeral_address_str(conn, &address_str)? + { + return Ok(Some(account_id)); + } + + let account_ids = get_account_ids(conn)?; + + // If the UTXO is received at the legacy transparent address (at BIP 44 address + // index 0 within its particular account, which we specifically ensure is returned + // from `get_transparent_receivers`), there may be no entry in the addresses table + // that can be used to tie the address to a particular account. In this case, we + // look up the legacy address for each account in the wallet, and check whether it + // matches the address for the received UTXO. + for &account_id in account_ids.iter() { + if let Some((legacy_taddr, _)) = get_legacy_transparent_address(params, conn, account_id)? { + if &legacy_taddr == address { + return Ok(Some(account_id)); + } + } + } + + Ok(None) +} + +/// Add a transparent output relevant to this wallet to the database. +/// +/// `output_height` may be None if this is an ephemeral output from a +/// transaction we created, that we do not yet know to have been mined. +#[allow(clippy::too_many_arguments)] +pub(crate) fn put_transparent_output( + conn: &rusqlite::Connection, + params: &P, + outpoint: &OutPoint, + txout: &TxOut, + output_height: Option, + address: &TransparentAddress, + receiving_account_uuid: AccountUuid, + known_unspent: bool, +) -> Result { + let output_height = output_height.map(u32::from); + let receiving_account_id = super::get_account_ref(conn, receiving_account_uuid)?; + + // Check whether we have an entry in the blocks table for the output height; + // if not, the transaction will be updated with its mined height when the + // associated block is scanned. + let block = match output_height { + Some(height) => conn + .query_row( + "SELECT height FROM blocks WHERE height = :height", + named_params![":height": height], + |row| row.get::<_, u32>(0), + ) + .optional()?, + None => None, + }; + + let id_tx = conn.query_row( + "INSERT INTO transactions (txid, block, mined_height) + VALUES (:txid, :block, :mined_height) + ON CONFLICT (txid) DO UPDATE + SET block = IFNULL(block, :block), + mined_height = :mined_height + RETURNING id_tx", + named_params![ + ":txid": &outpoint.hash().to_vec(), + ":block": block, + ":mined_height": output_height + ], + |row| row.get::<_, i64>(0), + )?; + + let spent_height = conn + .query_row( + "SELECT t.mined_height + FROM transactions t + JOIN transparent_received_output_spends ts ON ts.transaction_id = t.id_tx + JOIN transparent_received_outputs tro ON tro.id = ts.transparent_received_output_id + WHERE tro.transaction_id = :transaction_id + AND tro.output_index = :output_index", + named_params![ + ":transaction_id": id_tx, + ":output_index": &outpoint.n(), + ], + |row| { + row.get::<_, Option>(0) + .map(|o| o.map(BlockHeight::from)) + }, + ) + .optional()? + .flatten(); + + // The max observed unspent height is either the spending transaction's mined height - 1, or + // the current chain tip height if the UTXO was received via a path that confirmed that it was + // unspent, such as by querying the UTXO set of the network. + let max_observed_unspent = match spent_height { + Some(h) => Some(h - 1), + None => { + if known_unspent { + chain_tip_height(conn)? + } else { + None + } + } + }; + + let mut stmt_upsert_transparent_output = conn.prepare_cached( + "INSERT INTO transparent_received_outputs ( + transaction_id, output_index, + account_id, address, script, + value_zat, max_observed_unspent_height + ) + VALUES ( + :transaction_id, :output_index, + :account_id, :address, :script, + :value_zat, :max_observed_unspent_height + ) + ON CONFLICT (transaction_id, output_index) DO UPDATE + SET account_id = :account_id, + address = :address, + script = :script, + value_zat = :value_zat, + max_observed_unspent_height = IFNULL(:max_observed_unspent_height, max_observed_unspent_height) + RETURNING id", + )?; + + let sql_args = named_params![ + ":transaction_id": id_tx, + ":output_index": &outpoint.n(), + ":account_id": receiving_account_id.0, + ":address": &address.encode(params), + ":script": &txout.script_pubkey.0, + ":value_zat": &i64::from(ZatBalance::from(txout.value)), + ":max_observed_unspent_height": max_observed_unspent.map(u32::from), + ]; + + let utxo_id = stmt_upsert_transparent_output + .query_row(sql_args, |row| row.get::<_, i64>(0).map(UtxoId))?; + + // If we have a record of the output already having been spent, then mark it as spent using the + // stored reference to the spending transaction. + let spending_tx_ref = conn + .query_row( + "SELECT ts.spending_transaction_id + FROM transparent_spend_map ts + JOIN transactions t ON t.id_tx = ts.spending_transaction_id + WHERE ts.prevout_txid = :prevout_txid + AND ts.prevout_output_index = :prevout_idx + ORDER BY t.block NULLS LAST LIMIT 1", + named_params![ + ":prevout_txid": outpoint.txid().as_ref(), + ":prevout_idx": outpoint.n() + ], + |row| row.get::<_, i64>(0).map(TxRef), + ) + .optional()?; + + if let Some(spending_transaction_id) = spending_tx_ref { + mark_transparent_utxo_spent(conn, spending_transaction_id, outpoint)?; + } + + Ok(utxo_id) +} + +/// Adds a request to retrieve transactions involving the specified address to the transparent +/// spend search queue. Note that such requests are _not_ for data related to `tx_ref`, but instead +/// a request to find where the UTXO with the outpoint `(tx_ref, output_index)` is spent. +/// +/// ### Parameters +/// - `receiving_address`: The address that received the UTXO. +/// - `tx_ref`: The transaction in which the UTXO was received. +/// - `output_index`: The index of the output within `vout` of the specified transaction. +pub(crate) fn queue_transparent_spend_detection( + conn: &rusqlite::Transaction<'_>, + params: &P, + receiving_address: TransparentAddress, + tx_ref: TxRef, + output_index: u32, +) -> Result<(), SqliteClientError> { + let mut stmt = conn.prepare_cached( + "INSERT INTO transparent_spend_search_queue + (address, transaction_id, output_index) + VALUES + (:address, :transaction_id, :output_index) + ON CONFLICT (transaction_id, output_index) DO NOTHING", + )?; + + let addr_str = receiving_address.encode(params); + stmt.execute(named_params! { + ":address": addr_str, + ":transaction_id": tx_ref.0, + ":output_index": output_index + })?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use secrecy::Secret; + use transparent::keys::NonHardenedChildIndex; + use zcash_client_backend::{ + data_api::{testing::TestBuilder, Account as _, WalletWrite, GAP_LIMIT}, + wallet::TransparentAddressMetadata, + }; + use zcash_primitives::block::BlockHash; + + use crate::{ + testing::{db::TestDbFactory, BlockCache}, + wallet::{get_account_ref, transparent::ephemeral}, + WalletDb, + }; + + #[test] + fn put_received_transparent_utxo() { + zcash_client_backend::data_api::testing::transparent::put_received_transparent_utxo( + TestDbFactory::default(), + ); + } + + #[test] + fn transparent_balance_across_shielding() { + zcash_client_backend::data_api::testing::transparent::transparent_balance_across_shielding( + TestDbFactory::default(), + BlockCache::new(), + ); + } + + #[test] + fn transparent_balance_spendability() { + zcash_client_backend::data_api::testing::transparent::transparent_balance_spendability( + TestDbFactory::default(), + BlockCache::new(), + ); + } + + #[test] + fn ephemeral_address_management() { + let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory::default()) + .with_block_cache(BlockCache::new()) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let birthday = st.test_account().unwrap().birthday().clone(); + let account0_uuid = st.test_account().unwrap().account().id(); + let account0_id = get_account_ref(&st.wallet().db().conn, account0_uuid).unwrap(); + + let check = |db: &WalletDb<_, _>, account_id| { + eprintln!("checking {account_id:?}"); + assert_matches!(ephemeral::first_unstored_index(&db.conn, account_id), Ok(addr_index) if addr_index == GAP_LIMIT); + assert_matches!(ephemeral::first_unreserved_index(&db.conn, account_id), Ok(addr_index) if addr_index == 0); + + let known_addrs = + ephemeral::get_known_ephemeral_addresses(&db.conn, &db.params, account_id, None) + .unwrap(); + + let expected_metadata: Vec = (0..GAP_LIMIT) + .map(|i| ephemeral::metadata(NonHardenedChildIndex::from_index(i).unwrap())) + .collect(); + let actual_metadata: Vec = + known_addrs.into_iter().map(|(_, meta)| meta).collect(); + assert_eq!(actual_metadata, expected_metadata); + }; + + check(st.wallet().db(), account0_id); + + // Creating a new account should initialize `ephemeral_addresses` for that account. + let seed1 = vec![0x01; 32]; + let (account1_uuid, _usk) = st + .wallet_mut() + .db_mut() + .create_account("test1", &Secret::new(seed1), &birthday, None) + .unwrap(); + let account1_id = get_account_ref(&st.wallet().db().conn, account1_uuid).unwrap(); + assert_ne!(account0_id, account1_id); + check(st.wallet().db(), account1_id); + } +} diff --git a/zcash_client_sqlite/src/wallet/transparent/ephemeral.rs b/zcash_client_sqlite/src/wallet/transparent/ephemeral.rs new file mode 100644 index 0000000000..293eaa9550 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/transparent/ephemeral.rs @@ -0,0 +1,465 @@ +//! Functions for wallet support of ephemeral transparent addresses. +use std::cmp::{max, min}; +use std::ops::Range; + +use rusqlite::{named_params, OptionalExtension}; + +use ::transparent::{ + address::TransparentAddress, + keys::{EphemeralIvk, NonHardenedChildIndex, TransparentKeyScope}, +}; +use zcash_client_backend::{data_api::GAP_LIMIT, wallet::TransparentAddressMetadata}; +use zcash_keys::keys::UnifiedFullViewingKey; +use zcash_keys::{encoding::AddressCodec, keys::AddressGenerationError}; +use zcash_primitives::transaction::TxId; +use zcash_protocol::consensus; + +use crate::wallet::{self, get_account_ref}; +use crate::AccountUuid; +use crate::{error::SqliteClientError, AccountRef, TxRef}; + +// Returns `TransparentAddressMetadata` in the ephemeral scope for the +// given address index. +pub(crate) fn metadata(address_index: NonHardenedChildIndex) -> TransparentAddressMetadata { + TransparentAddressMetadata::new(TransparentKeyScope::EPHEMERAL, address_index) +} + +/// Returns the first unstored ephemeral address index in the given account. +pub(crate) fn first_unstored_index( + conn: &rusqlite::Connection, + account_id: AccountRef, +) -> Result { + match conn + .query_row( + "SELECT address_index FROM ephemeral_addresses + WHERE account_id = :account_id + ORDER BY address_index DESC + LIMIT 1", + named_params![":account_id": account_id.0], + |row| row.get::<_, u32>(0), + ) + .optional()? + { + Some(i) if i >= (1 << 31) + GAP_LIMIT => { + unreachable!("violates constraint index_range_and_address_nullity") + } + Some(i) => Ok(i.checked_add(1).unwrap()), + None => Ok(0), + } +} + +/// Returns the first unreserved ephemeral address index in the given account. +pub(crate) fn first_unreserved_index( + conn: &rusqlite::Connection, + account_id: AccountRef, +) -> Result { + first_unstored_index(conn, account_id)? + .checked_sub(GAP_LIMIT) + .ok_or(SqliteClientError::CorruptedData( + "ephemeral_addresses table has not been initialized".to_owned(), + )) +} + +/// Returns the first ephemeral address index in the given account that +/// would violate the gap invariant if used. +pub(crate) fn first_unsafe_index( + conn: &rusqlite::Connection, + account_id: AccountRef, +) -> Result { + // The inner join with `transactions` excludes addresses for which + // `seen_in_tx` is NULL. The query also excludes addresses observed + // to have been mined in a transaction that we currently see as unmined. + // This is conservative in terms of avoiding violation of the gap + // invariant: it can only cause us to get to the end of the gap sooner. + // + // TODO: do we want to only consider transactions with a minimum number + // of confirmations here? + let first_unmined_index: u32 = match conn + .query_row( + "SELECT address_index FROM ephemeral_addresses + JOIN transactions t ON t.id_tx = seen_in_tx + WHERE account_id = :account_id AND t.mined_height IS NOT NULL + ORDER BY address_index DESC + LIMIT 1", + named_params![":account_id": account_id.0], + |row| row.get::<_, u32>(0), + ) + .optional()? + { + Some(i) if i >= 1 << 31 => { + unreachable!("violates constraint index_range_and_address_nullity") + } + Some(i) => i.checked_add(1).unwrap(), + None => 0, + }; + Ok(min( + 1 << 31, + first_unmined_index.checked_add(GAP_LIMIT).unwrap(), + )) +} + +/// Utility function to return an `Range` that starts at `i` +/// and is of length up to `n`. The range is truncated if necessary +/// so that it contains no elements beyond the maximum valid address +/// index, `(1 << 31) - 1`. +pub(crate) fn range_from(i: u32, n: u32) -> Range { + let first = min(1 << 31, i); + let last = min(1 << 31, i.saturating_add(n)); + first..last +} + +/// Returns the ephemeral transparent IVK for a given account ID. +pub(crate) fn get_ephemeral_ivk( + conn: &rusqlite::Connection, + params: &P, + account_id: AccountRef, +) -> Result, SqliteClientError> { + let ufvk = conn + .query_row( + "SELECT ufvk FROM accounts WHERE id = :account_id", + named_params![":account_id": account_id.0], + |row| { + let ufvk_str: Option = row.get("ufvk")?; + Ok(ufvk_str.map(|s| { + UnifiedFullViewingKey::decode(params, &s[..]) + .map_err(SqliteClientError::BadAccountData) + })) + }, + ) + .optional()? + .ok_or(SqliteClientError::AccountUnknown)? + .transpose()?; + + let eivk = ufvk + .as_ref() + .and_then(|ufvk| ufvk.transparent()) + .map(|t| t.derive_ephemeral_ivk()) + .transpose()?; + + Ok(eivk) +} + +/// Returns a vector of ephemeral transparent addresses associated with the given +/// account controlled by this wallet, along with their metadata. The result includes +/// reserved addresses, and addresses for `GAP_LIMIT` additional indices (capped to +/// the maximum index). +/// +/// If `index_range` is some `Range`, it limits the result to addresses with indices +/// in that range. +pub(crate) fn get_known_ephemeral_addresses( + conn: &rusqlite::Connection, + params: &P, + account_id: AccountRef, + index_range: Option>, +) -> Result, SqliteClientError> { + let index_range = index_range.unwrap_or(0..(1 << 31)); + + let mut stmt = conn.prepare( + "SELECT address, address_index + FROM ephemeral_addresses ea + WHERE ea.account_id = :account_id + AND address_index >= :start + AND address_index < :end + ORDER BY address_index", + )?; + let mut rows = stmt.query(named_params![ + ":account_id": account_id.0, + ":start": index_range.start, + ":end": min(1 << 31, index_range.end), + ])?; + + let mut result = vec![]; + + while let Some(row) = rows.next()? { + let addr_str: String = row.get(0)?; + let raw_index: u32 = row.get(1)?; + let address_index = NonHardenedChildIndex::from_index(raw_index) + .expect("where clause ensures this is in range"); + let address = TransparentAddress::decode(params, &addr_str)?; + result.push((address, metadata(address_index))); + } + Ok(result) +} + +/// If this is a known ephemeral address in any account, return its account id. +pub(crate) fn find_account_for_ephemeral_address_str( + conn: &rusqlite::Connection, + address_str: &str, +) -> Result, SqliteClientError> { + Ok(conn + .query_row( + "SELECT accounts.uuid + FROM ephemeral_addresses ea + JOIN accounts ON accounts.id = ea.account_id + WHERE address = :address", + named_params![":address": &address_str], + |row| Ok(AccountUuid(row.get(0)?)), + ) + .optional()?) +} + +/// If this is a known ephemeral address in the given account, return its index. +pub(crate) fn find_index_for_ephemeral_address_str( + conn: &rusqlite::Connection, + account_uuid: AccountUuid, + address_str: &str, +) -> Result, SqliteClientError> { + let account_id = get_account_ref(conn, account_uuid)?; + Ok(conn + .query_row( + "SELECT address_index FROM ephemeral_addresses + WHERE account_id = :account_id AND address = :address", + named_params![":account_id": account_id.0, ":address": &address_str], + |row| row.get::<_, u32>(0), + ) + .optional()? + .map(|index| { + NonHardenedChildIndex::from_index(index) + .expect("valid by constraint index_range_and_address_nullity") + })) +} + +/// Returns a vector with the next `n` previously unreserved ephemeral addresses for +/// the given account. +/// +/// # Errors +/// +/// * `SqliteClientError::AccountUnknown`, if there is no account with the given id. +/// * `SqliteClientError::UnknownZip32Derivation`, if the account is imported and +/// it is not possible to derive new addresses for it. +/// * `SqliteClientError::ReachedGapLimit`, if it is not possible to reserve `n` addresses +/// within the gap limit after the last address in this account that is known to have an +/// output in a mined transaction. +/// * `SqliteClientError::AddressGeneration(AddressGenerationError::DiversifierSpaceExhausted)`, +/// if the limit on transparent address indices has been reached. +pub(crate) fn reserve_next_n_ephemeral_addresses( + conn: &rusqlite::Transaction, + params: &P, + account_id: AccountRef, + n: usize, +) -> Result, SqliteClientError> { + if n == 0 { + return Ok(vec![]); + } + + let first_unreserved = first_unreserved_index(conn, account_id)?; + let first_unsafe = first_unsafe_index(conn, account_id)?; + let allocation = range_from( + first_unreserved, + u32::try_from(n).map_err(|_| AddressGenerationError::DiversifierSpaceExhausted)?, + ); + + if allocation.len() < n { + return Err(AddressGenerationError::DiversifierSpaceExhausted.into()); + } + if allocation.end > first_unsafe { + let account_uuid = wallet::get_account_uuid(conn, account_id)?; + return Err(SqliteClientError::ReachedGapLimit( + account_uuid, + max(first_unreserved, first_unsafe), + )); + } + reserve_until(conn, params, account_id, allocation.end)?; + get_known_ephemeral_addresses(conn, params, account_id, Some(allocation)) +} + +/// Initialize the `ephemeral_addresses` table. This must be called when +/// creating or migrating an account. +pub(crate) fn init_account( + conn: &rusqlite::Transaction, + params: &P, + account_id: AccountRef, +) -> Result<(), SqliteClientError> { + reserve_until(conn, params, account_id, 0) +} + +/// Extend the range of stored addresses in an account if necessary so that the index of the next +/// address to reserve will be *at least* `next_to_reserve`. If no transparent key exists for the +/// given account or it would already have been at least `next_to_reserve`, then do nothing. +/// +/// Note that this is called from database migration code. +/// +/// # Panics +/// +/// Panics if the precondition `next_to_reserve <= (1 << 31)` does not hold. +fn reserve_until( + conn: &rusqlite::Transaction, + params: &P, + account_id: AccountRef, + next_to_reserve: u32, +) -> Result<(), SqliteClientError> { + assert!(next_to_reserve <= 1 << 31); + + if let Some(ephemeral_ivk) = get_ephemeral_ivk(conn, params, account_id)? { + let first_unstored = first_unstored_index(conn, account_id)?; + let range_to_store = first_unstored..(next_to_reserve.checked_add(GAP_LIMIT).unwrap()); + if range_to_store.is_empty() { + return Ok(()); + } + + // used_in_tx and seen_in_tx are initially NULL + let mut stmt_insert_ephemeral_address = conn.prepare_cached( + "INSERT INTO ephemeral_addresses (account_id, address_index, address) + VALUES (:account_id, :address_index, :address)", + )?; + + for raw_index in range_to_store { + // The range to store may contain indicies that are out of the valid range of non hardened + // child indices; we still store explicit rows in the ephemeral_addresses table for these + // so that it's possible to find the first unused address using dead reckoning with the gap + // limit. + let address_str_opt = NonHardenedChildIndex::from_index(raw_index) + .map(|address_index| { + ephemeral_ivk + .derive_ephemeral_address(address_index) + .map(|addr| addr.encode(params)) + }) + .transpose()?; + + stmt_insert_ephemeral_address.execute(named_params![ + ":account_id": account_id.0, + ":address_index": raw_index, + ":address": address_str_opt, + ])?; + } + } + + Ok(()) +} + +/// Returns a `SqliteClientError::EphemeralAddressReuse` error if the address was +/// already used. +fn ephemeral_address_reuse_check( + conn: &rusqlite::Transaction, + address_str: &str, +) -> Result<(), SqliteClientError> { + // It is intentional that we don't require `t.mined_height` to be non-null. + // That is, we conservatively treat an ephemeral address as potentially + // reused even if we think that the transaction where we had evidence of + // its use is at present unmined. This should never occur in supported + // situations where only a single correctly operating wallet instance is + // using a given seed, because such a wallet will not reuse an address that + // it ever reserved. + // + // `COALESCE(used_in_tx, seen_in_tx)` can only differ from `used_in_tx` + // if the address was reserved, an error occurred in transaction creation + // before calling `mark_ephemeral_address_as_used`, and then we saw the + // address in another transaction (presumably created by another wallet + // instance, or as a result of a bug) anyway. + let res = conn + .query_row( + "SELECT t.txid FROM ephemeral_addresses + LEFT OUTER JOIN transactions t + ON t.id_tx = COALESCE(used_in_tx, seen_in_tx) + WHERE address = :address", + named_params![":address": address_str], + |row| row.get::<_, Option>>(0), + ) + .optional()? + .flatten(); + + if let Some(txid_bytes) = res { + let txid = TxId::from_bytes( + txid_bytes + .try_into() + .map_err(|_| SqliteClientError::CorruptedData("invalid txid".to_owned()))?, + ); + Err(SqliteClientError::EphemeralAddressReuse( + address_str.to_owned(), + txid, + )) + } else { + Ok(()) + } +} + +/// If `address` is one of our ephemeral addresses, mark it as having an output +/// in a transaction that we have just created. This has no effect if `address` is +/// not one of our ephemeral addresses. +/// +/// Returns a `SqliteClientError::EphemeralAddressReuse` error if the address was +/// already used. +pub(crate) fn mark_ephemeral_address_as_used( + conn: &rusqlite::Transaction, + params: &P, + ephemeral_address: &TransparentAddress, + tx_ref: TxRef, +) -> Result<(), SqliteClientError> { + let address_str = ephemeral_address.encode(params); + ephemeral_address_reuse_check(conn, &address_str)?; + + // We update both `used_in_tx` and `seen_in_tx` here, because a used address has + // necessarily been seen in a transaction. We will not treat this as extending the + // range of addresses that are safe to reserve unless and until the transaction is + // observed as mined. + let update_result = conn + .query_row( + "UPDATE ephemeral_addresses + SET used_in_tx = :tx_ref, seen_in_tx = :tx_ref + WHERE address = :address + RETURNING account_id, address_index", + named_params![":tx_ref": tx_ref.0, ":address": address_str], + |row| Ok((AccountRef(row.get::<_, u32>(0)?), row.get::<_, u32>(1)?)), + ) + .optional()?; + + // Maintain the invariant that the last `GAP_LIMIT` addresses are unused and unseen. + if let Some((account_id, address_index)) = update_result { + let next_to_reserve = address_index.checked_add(1).expect("ensured by constraint"); + reserve_until(conn, params, account_id, next_to_reserve)?; + } + Ok(()) +} + +/// If `address` is one of our ephemeral addresses, mark it as having an output +/// in the given mined transaction (which may or may not be a transaction we sent). +/// +/// `tx_ref` must be a valid transaction reference. This call has no effect if +/// `address` is not one of our ephemeral addresses. +pub(crate) fn mark_ephemeral_address_as_seen( + conn: &rusqlite::Transaction, + params: &P, + address: &TransparentAddress, + tx_ref: TxRef, +) -> Result<(), SqliteClientError> { + let address_str = address.encode(params); + + // Figure out which transaction was mined earlier: `tx_ref`, or any existing + // tx referenced by `seen_in_tx` for the given address. Prefer the existing + // reference in case of a tie or if both transactions are unmined. + // This slightly reduces the chance of unnecessarily reaching the gap limit + // too early in some corner cases (because the earlier transaction is less + // likely to be unmined). + // + // The query should always return a value if `tx_ref` is valid. + let earlier_ref = conn.query_row( + "SELECT id_tx FROM transactions + LEFT OUTER JOIN ephemeral_addresses e + ON id_tx = e.seen_in_tx + WHERE id_tx = :tx_ref OR e.address = :address + ORDER BY mined_height ASC NULLS LAST, + tx_index ASC NULLS LAST, + e.seen_in_tx ASC NULLS LAST + LIMIT 1", + named_params![":tx_ref": tx_ref.0, ":address": address_str], + |row| row.get::<_, i64>(0), + )?; + + let update_result = conn + .query_row( + "UPDATE ephemeral_addresses + SET seen_in_tx = :seen_in_tx + WHERE address = :address + RETURNING account_id, address_index", + named_params![":seen_in_tx": &earlier_ref, ":address": address_str], + |row| Ok((AccountRef(row.get::<_, u32>(0)?), row.get::<_, u32>(1)?)), + ) + .optional()?; + + // Maintain the invariant that the last `GAP_LIMIT` addresses are unused and unseen. + if let Some((account_id, address_index)) = update_result { + let next_to_reserve = address_index.checked_add(1).expect("ensured by constraint"); + reserve_until(conn, params, account_id, next_to_reserve)?; + } + Ok(()) +} diff --git a/zcash_extensions/CHANGELOG.md b/zcash_extensions/CHANGELOG.md index 744a7b1f71..1fa144636d 100644 --- a/zcash_extensions/CHANGELOG.md +++ b/zcash_extensions/CHANGELOG.md @@ -6,4 +6,10 @@ and this library adheres to Rust's notion of [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +### Changed +- MSRV is now 1.81.0. + +## [0.1.0] - 2024-07-15 Initial release. +MSRV is 1.70.0. diff --git a/zcash_extensions/Cargo.toml b/zcash_extensions/Cargo.toml index c15fb51d66..b61e136497 100644 --- a/zcash_extensions/Cargo.toml +++ b/zcash_extensions/Cargo.toml @@ -1,19 +1,39 @@ [package] name = "zcash_extensions" description = "Zcash Extension implementations & consensus node integration layer." -version = "0.0.0" +version = "0.1.0" authors = ["Jack Grigg ", "Kris Nuttycombe "] homepage = "https://github.com/zcash/librustzcash" -repository = "https://github.com/zcash/librustzcash" -license = "MIT OR Apache-2.0" -edition = "2018" +repository.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true +categories.workspace = true + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] [dependencies] -blake2b_simd = "0.5" -zcash_primitives = { version = "0.5", path = "../zcash_primitives", features = ["zfuture"] } +blake2b_simd.workspace = true +zcash_primitives = { workspace = true, features = ["non-standard-fees"] } +zcash_protocol.workspace = true [dev-dependencies] -ff = "0.8" -jubjub = "0.5.1" -rand_core = "0.5.1" -zcash_proofs = { version = "0.5", path = "../zcash_proofs" } +ff.workspace = true +jubjub.workspace = true +rand_core.workspace = true +sapling.workspace = true +orchard.workspace = true +transparent.workspace = true +zcash_address.workspace = true +zcash_proofs = { workspace = true, features = ["local-prover", "bundled-prover"] } + +[features] +transparent-inputs = [] + +[lib] +bench = false + +[lints] +workspace = true diff --git a/zcash_extensions/README.md b/zcash_extensions/README.md new file mode 100644 index 0000000000..b0af0d6cec --- /dev/null +++ b/zcash_extensions/README.md @@ -0,0 +1,20 @@ +# zcash_extensions + +This library contains Rust code to support [Transparent Zcash Extensions](https://zips.z.cash/zip-0222). + +## License + +Licensed under either of + + * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or + http://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you, as defined in the Apache-2.0 +license, shall be dual licensed as above, without any additional terms or +conditions. diff --git a/zcash_extensions/src/consensus/transparent.rs b/zcash_extensions/src/consensus/transparent.rs index 9568bb2a93..6c5a4b4554 100644 --- a/zcash_extensions/src/consensus/transparent.rs +++ b/zcash_extensions/src/consensus/transparent.rs @@ -2,8 +2,10 @@ use std::convert::TryFrom; use zcash_primitives::consensus::{BlockHeight, BranchId}; -use zcash_primitives::extensions::transparent::{Error, Extension, Precondition, Witness}; -use zcash_primitives::transaction::{components::TzeOut, Transaction}; +use zcash_primitives::extensions::transparent::{ + AuthData, Error, Extension, Precondition, Witness, +}; +use zcash_primitives::transaction::{components::tze::TzeOut, Transaction}; use crate::transparent::demo; @@ -67,7 +69,7 @@ pub trait Epoch { fn verify<'a>( &self, precondition: &Precondition, - witness: &Witness, + witness: &Witness, ctx: &Context<'a>, ) -> Result<(), Error>; } @@ -76,15 +78,18 @@ pub trait Epoch { /// by the context. impl<'a> demo::Context for Context<'a> { fn is_tze_only(&self) -> bool { - self.tx.vin.is_empty() - && self.tx.vout.is_empty() - && self.tx.shielded_spends.is_empty() - && self.tx.shielded_outputs.is_empty() - && self.tx.joinsplits.is_empty() + self.tx.transparent_bundle().is_none() + && self.tx.sapling_bundle().is_none() + && self.tx.sprout_bundle().is_none() + && self.tx.orchard_bundle().is_none() } fn tx_tze_outputs(&self) -> &[TzeOut] { - &self.tx.tze_outputs + if let Some(bundle) = self.tx.tze_bundle() { + &bundle.vout + } else { + &[] + } } } @@ -98,7 +103,7 @@ impl Epoch for EpochVTest { fn verify<'a>( &self, precondition: &Precondition, - witness: &Witness, + witness: &Witness, ctx: &Context<'a>, ) -> Result<(), Error> { let ext_id = ExtensionId::try_from(precondition.extension_id) diff --git a/zcash_extensions/src/lib.rs b/zcash_extensions/src/lib.rs index 70788b6e9d..5dc0c812cc 100644 --- a/zcash_extensions/src/lib.rs +++ b/zcash_extensions/src/lib.rs @@ -1,5 +1,12 @@ +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] // Catch documentation errors caused by code changes. -#![deny(broken_intra_doc_links)] +#![deny(rustdoc::broken_intra_doc_links)] +// For workspace compilation reasons, we have this crate in the workspace and just leave +// it empty if `zfuture` is not enabled. + +#[cfg(zcash_unstable = "zfuture")] pub mod consensus; +#[cfg(zcash_unstable = "zfuture")] pub mod transparent; diff --git a/zcash_extensions/src/transparent/demo.rs b/zcash_extensions/src/transparent/demo.rs index 6109ec8585..de0355b2f9 100644 --- a/zcash_extensions/src/transparent/demo.rs +++ b/zcash_extensions/src/transparent/demo.rs @@ -18,25 +18,25 @@ //! - `tx_b`: `[ TzeIn(tx_a, preimage_1) -> TzeOut(value, hash_2) ]` //! - `tx_c`: `[ TzeIn(tx_b, preimage_2) -> [any output types...] ]` -use std::convert::TryFrom; -use std::convert::TryInto; use std::fmt; +use std::ops::{Deref, DerefMut}; use blake2b_simd::Params; use zcash_primitives::{ extensions::transparent::{Extension, ExtensionTxBuilder, FromPayload, ToPayload}, - transaction::components::{amount::Amount, TzeOut, TzeOutPoint}, + transaction::components::tze::{OutPoint, TzeOut}, }; +use zcash_protocol::value::Zatoshis; /// Types and constants used for Mode 0 (open a channel) mod open { pub const MODE: u32 = 0; - #[derive(Debug, PartialEq)] + #[derive(Debug, PartialEq, Eq)] pub struct Precondition(pub [u8; 32]); - #[derive(Debug, PartialEq)] + #[derive(Debug, PartialEq, Eq)] pub struct Witness(pub [u8; 32]); } @@ -44,15 +44,15 @@ mod open { mod close { pub const MODE: u32 = 1; - #[derive(Debug, PartialEq)] + #[derive(Debug, PartialEq, Eq)] pub struct Precondition(pub [u8; 32]); - #[derive(Debug, PartialEq)] + #[derive(Debug, PartialEq, Eq)] pub struct Witness(pub [u8; 32]); } /// The precondition type for the demo extension. -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub enum Precondition { Open(open::Precondition), Close(close::Precondition), @@ -72,7 +72,7 @@ impl Precondition { /// Errors that may be produced during parsing and verification of demo preconditions and /// witnesses. -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub enum Error { /// Parse error indicating that the payload of the condition or the witness was /// not 32 bytes. @@ -154,7 +154,7 @@ impl ToPayload for Precondition { } /// The witness type for the demo extension. -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub enum Witness { Open(open::Witness), Close(close::Witness), @@ -336,6 +336,20 @@ pub struct DemoBuilder { pub extension_id: u32, } +impl Deref for DemoBuilder { + type Target = B; + + fn deref(&self) -> &Self::Target { + &self.txn_builder + } +} + +impl DerefMut for DemoBuilder { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.txn_builder + } +} + /// Errors that can occur in construction of transactions using `DemoBuilder`. #[derive(Debug)] pub enum DemoBuildError { @@ -356,12 +370,12 @@ pub enum DemoBuildError { /// Convenience methods for use with [`zcash_primitives::transaction::builder::Builder`] /// for constructing transactions that utilize the features of the demo extension. -impl<'a, B: ExtensionTxBuilder<'a>> DemoBuilder<&mut B> { +impl<'a, B: ExtensionTxBuilder<'a>> DemoBuilder { /// Add a channel-opening precondition to the outputs of the transaction under /// construction. pub fn demo_open( &mut self, - value: Amount, + value: Zatoshis, hash_1: [u8; 32], ) -> Result<(), DemoBuildError> { // Call through to the generic builder. @@ -374,8 +388,8 @@ impl<'a, B: ExtensionTxBuilder<'a>> DemoBuilder<&mut B> { /// precondition to the transaction under construction. pub fn demo_transfer_to_close( &mut self, - prevout: (TzeOutPoint, TzeOut), - transfer_amount: Amount, + prevout: (OutPoint, TzeOut), + transfer_amount: Zatoshis, preimage_1: [u8; 32], hash_2: [u8; 32], ) -> Result<(), DemoBuildError> { @@ -416,7 +430,7 @@ impl<'a, B: ExtensionTxBuilder<'a>> DemoBuilder<&mut B> { /// Add a channel-closing witness to the transaction under construction. pub fn demo_close( &mut self, - prevout: (TzeOutPoint, TzeOut), + prevout: (OutPoint, TzeOut), preimage_2: [u8; 32], ) -> Result<(), DemoBuildError> { let hash_2 = { @@ -460,32 +474,54 @@ impl<'a, B: ExtensionTxBuilder<'a>> DemoBuilder<&mut B> { #[cfg(test)] mod tests { + use std::convert::Infallible; + use blake2b_simd::Params; - use ff::{Field, PrimeField}; + use ff::Field; use rand_core::OsRng; - use zcash_proofs::prover::LocalTxProver; - + use sapling::{zip32::ExtendedSpendingKey, Node, Rseed}; + use transparent::{address::TransparentAddress, builder::TransparentSigningSet}; use zcash_primitives::{ - consensus::{BranchId, H0, TEST_NETWORK}, extensions::transparent::{self as tze, Extension, FromPayload, ToPayload}, - legacy::TransparentAddress, - merkle_tree::{CommitmentTree, IncrementalWitness}, - sapling::Node, - sapling::Rseed, transaction::{ - builder::Builder, - components::{ - amount::{Amount, DEFAULT_FEE}, - TzeIn, TzeOut, TzeOutPoint, - }, - Transaction, TransactionData, + builder::{BuildConfig, Builder}, + components::tze::{Authorized, Bundle, OutPoint, TzeIn, TzeOut}, + fees::{fixed, zip317::MINIMUM_FEE}, + Transaction, TransactionData, TxVersion, }, - zip32::ExtendedSpendingKey, }; + use zcash_protocol::{ + consensus::{BlockHeight, BranchId, NetworkType, NetworkUpgrade, Parameters}, + value::Zatoshis, + }; + + use zcash_proofs::prover::LocalTxProver; use super::{close, hash_1, open, Context, DemoBuilder, Precondition, Program, Witness}; + #[derive(PartialEq, Copy, Clone, Debug)] + struct FutureNetwork; + + impl Parameters for FutureNetwork { + fn activation_height(&self, nu: NetworkUpgrade) -> Option { + match nu { + NetworkUpgrade::Overwinter => Some(BlockHeight::from_u32(207_500)), + NetworkUpgrade::Sapling => Some(BlockHeight::from_u32(280_000)), + NetworkUpgrade::Blossom => Some(BlockHeight::from_u32(584_000)), + NetworkUpgrade::Heartwood => Some(BlockHeight::from_u32(903_800)), + NetworkUpgrade::Canopy => Some(BlockHeight::from_u32(1_028_500)), + NetworkUpgrade::Nu5 => Some(BlockHeight::from_u32(1_200_000)), + NetworkUpgrade::Nu6 => Some(BlockHeight::from_u32(1_300_000)), + NetworkUpgrade::ZFuture => Some(BlockHeight::from_u32(1_400_000)), + } + } + + fn network_type(&self) -> NetworkType { + NetworkType::Test + } + } + fn demo_hashes(preimage_1: &[u8; 32], preimage_2: &[u8; 32]) -> ([u8; 32], [u8; 32]) { let hash_2 = { let mut hash = [0; 32]; @@ -563,15 +599,34 @@ mod tests { /// by the context. impl<'a> Context for Ctx<'a> { fn is_tze_only(&self) -> bool { - self.tx.vin.is_empty() - && self.tx.vout.is_empty() - && self.tx.shielded_spends.is_empty() - && self.tx.shielded_outputs.is_empty() - && self.tx.joinsplits.is_empty() + self.tx.transparent_bundle().is_none() + && self.tx.sapling_bundle().is_none() + && self.tx.sprout_bundle().is_none() + && self.tx.orchard_bundle().is_none() } fn tx_tze_outputs(&self) -> &[TzeOut] { - &self.tx.tze_outputs + match self.tx.tze_bundle() { + Some(b) => &b.vout, + None => &[], + } + } + } + + fn demo_builder<'a>( + height: BlockHeight, + sapling_anchor: sapling::Anchor, + ) -> DemoBuilder> { + DemoBuilder { + txn_builder: Builder::new( + FutureNetwork, + height, + BuildConfig::Standard { + sapling_anchor: Some(sapling_anchor), + orchard_anchor: Some(orchard::Anchor::empty_tree()), + }, + ), + extension_id: 0, } } @@ -612,51 +667,93 @@ mod tests { // let out_a = TzeOut { - value: Amount::from_u64(1).unwrap(), + value: Zatoshis::from_u64(1).unwrap(), precondition: tze::Precondition::from(0, &Precondition::open(hash_1)), }; - let mut mtx_a = TransactionData::zfuture(); - mtx_a.tze_outputs.push(out_a); - let tx_a = mtx_a.freeze().unwrap(); + let tx_a = TransactionData::from_parts_zfuture( + TxVersion::ZFuture, + BranchId::ZFuture, + 0, + 0u32.into(), + None, + None, + None, + None, + Some(Bundle { + vin: vec![], + vout: vec![out_a], + authorization: Authorized, + }), + ) + .freeze() + .unwrap(); // // Transfer // let in_b = TzeIn { - prevout: TzeOutPoint::new(tx_a.txid().0, 0), + prevout: OutPoint::new(tx_a.txid(), 0), witness: tze::Witness::from(0, &Witness::open(preimage_1)), }; let out_b = TzeOut { - value: Amount::from_u64(1).unwrap(), + value: Zatoshis::const_from_u64(1), precondition: tze::Precondition::from(0, &Precondition::close(hash_2)), }; - let mut mtx_b = TransactionData::zfuture(); - mtx_b.tze_inputs.push(in_b); - mtx_b.tze_outputs.push(out_b); - let tx_b = mtx_b.freeze().unwrap(); + + let tx_b = TransactionData::from_parts_zfuture( + TxVersion::ZFuture, + BranchId::ZFuture, + 0, + 0u32.into(), + None, + None, + None, + None, + Some(Bundle { + vin: vec![in_b], + vout: vec![out_b], + authorization: Authorized, + }), + ) + .freeze() + .unwrap(); // // Closing transaction // let in_c = TzeIn { - prevout: TzeOutPoint::new(tx_b.txid().0, 0), + prevout: OutPoint::new(tx_b.txid(), 0), witness: tze::Witness::from(0, &Witness::close(preimage_2)), }; - let mut mtx_c = TransactionData::zfuture(); - mtx_c.tze_inputs.push(in_c); - let tx_c = mtx_c.freeze().unwrap(); + let tx_c = TransactionData::from_parts_zfuture( + TxVersion::ZFuture, + BranchId::ZFuture, + 0, + 0u32.into(), + None, + None, + None, + None, + Some(Bundle { + vin: vec![in_c], + vout: vec![], + authorization: Authorized, + }), + ) + .freeze() + .unwrap(); // Verify tx_b { let ctx = Ctx { tx: &tx_b }; assert_eq!( Program.verify( - &tx_a.tze_outputs[0].precondition, - &tx_b.tze_inputs[0].witness, + &tx_a.tze_bundle().unwrap().vout[0].precondition, + &tx_b.tze_bundle().unwrap().vin[0].witness, &ctx ), Ok(()) @@ -668,8 +765,8 @@ mod tests { let ctx = Ctx { tx: &tx_c }; assert_eq!( Program.verify( - &tx_b.tze_outputs[0].precondition, - &tx_c.tze_inputs[0].witness, + &tx_b.tze_bundle().unwrap().vout[0].precondition, + &tx_c.tze_bundle().unwrap().vin[0].witness, &ctx ), Ok(()) @@ -682,121 +779,144 @@ mod tests { let preimage_1 = [1; 32]; let preimage_2 = [2; 32]; - // Only run the test if we have the prover parameters. - let prover = match LocalTxProver::with_default_location() { - Some(prover) => prover, - None => return, - }; + let tx_height = FutureNetwork + .activation_height(NetworkUpgrade::ZFuture) + .unwrap(); + + let prover = LocalTxProver::bundled(); // // Opening transaction // let mut rng = OsRng; - let mut builder_a = Builder::new_with_rng_zfuture(TEST_NETWORK, H0, rng); + + // FIXME: implement zcash_primitives::transaction::fees::FutureFeeRule for zip317::FeeRule. + #[allow(deprecated)] + let fee_rule = fixed::FeeRule::non_standard(MINIMUM_FEE); // create some inputs to spend let extsk = ExtendedSpendingKey::master(&[]); - let to = extsk.default_address().unwrap().1; - let note1 = to - .create_note(110000, Rseed::BeforeZip212(jubjub::Fr::random(&mut rng))) - .unwrap(); - let cm1 = Node::new(note1.cmu().to_repr()); - let mut tree = CommitmentTree::empty(); + let dfvk = extsk.to_diversifiable_full_viewing_key(); + let to = extsk.default_address().1; + let sapling_extsks = &[extsk]; + let note1 = to.create_note( + sapling::value::NoteValue::from_raw(110000), + Rseed::BeforeZip212(jubjub::Fr::random(&mut rng)), + ); + let cm1 = Node::from_cmu(¬e1.cmu()); + let mut tree = sapling::CommitmentTree::empty(); // fake that the note appears in some previous // shielded output tree.append(cm1).unwrap(); - let witness1 = IncrementalWitness::from_tree(&tree); + let witness1 = sapling::IncrementalWitness::from_tree(tree).unwrap(); + let mut builder_a = demo_builder(tx_height, witness1.root().into()); builder_a - .add_sapling_spend(extsk, *to.diversifier(), note1, witness1.path().unwrap()) + .add_sapling_spend::(dfvk.fvk().clone(), note1, witness1.path().unwrap()) .unwrap(); - let mut db_a = DemoBuilder { - txn_builder: &mut builder_a, - extension_id: 0, - }; - - let value = Amount::from_u64(100000).unwrap(); + let value = Zatoshis::const_from_u64(100000); let (h1, h2) = demo_hashes(&preimage_1, &preimage_2); - db_a.demo_open(value, h1) + builder_a + .demo_open(value.into(), h1) .map_err(|e| format!("open failure: {:?}", e)) .unwrap(); - let (tx_a, _) = builder_a - .build(BranchId::Canopy, &prover) + let res_a = builder_a + .txn_builder + .build_zfuture( + &TransparentSigningSet::new(), + sapling_extsks, + &[], + OsRng, + &prover, + &prover, + &fee_rule, + ) .map_err(|e| format!("build failure: {:?}", e)) .unwrap(); + let tze_a = res_a.transaction().tze_bundle().unwrap(); // // Transfer // - let mut builder_b = Builder::new_with_rng_zfuture(TEST_NETWORK, H0, rng); - let mut db_b = DemoBuilder { - txn_builder: &mut builder_b, - extension_id: 0, - }; + let mut builder_b = demo_builder(tx_height + 1, sapling::Anchor::empty_tree()); let prevout_a = ( - TzeOutPoint::new(tx_a.txid().0, 0), - tx_a.tze_outputs[0].clone(), + OutPoint::new(res_a.transaction().txid(), 0), + tze_a.vout[0].clone(), ); - let value_xfr = value - DEFAULT_FEE; - db_b.demo_transfer_to_close(prevout_a, value_xfr, preimage_1, h2) + let value_xfr = (value - fee_rule.fixed_fee()).unwrap(); + builder_b + .demo_transfer_to_close(prevout_a, value_xfr.into(), preimage_1, h2) .map_err(|e| format!("transfer failure: {:?}", e)) .unwrap(); - let (tx_b, _) = builder_b - .build(BranchId::Canopy, &prover) + let res_b = builder_b + .txn_builder + .build_zfuture( + &TransparentSigningSet::new(), + sapling_extsks, + &[], + OsRng, + &prover, + &prover, + &fee_rule, + ) .map_err(|e| format!("build failure: {:?}", e)) .unwrap(); + let tze_b = res_b.transaction().tze_bundle().unwrap(); // // Closing transaction // - let mut builder_c = Builder::new_with_rng_zfuture(TEST_NETWORK, H0, rng); - let mut db_c = DemoBuilder { - txn_builder: &mut builder_c, - extension_id: 0, - }; + let mut builder_c = demo_builder(tx_height + 2, sapling::Anchor::empty_tree()); let prevout_b = ( - TzeOutPoint::new(tx_a.txid().0, 0), - tx_b.tze_outputs[0].clone(), + OutPoint::new(res_a.transaction().txid(), 0), + tze_b.vout[0].clone(), ); - db_c.demo_close(prevout_b, preimage_2) + builder_c + .demo_close(prevout_b, preimage_2) .map_err(|e| format!("close failure: {:?}", e)) .unwrap(); builder_c .add_transparent_output( - &TransparentAddress::PublicKey([0; 20]), - value_xfr - DEFAULT_FEE, + &TransparentAddress::PublicKeyHash([0; 20]), + (value_xfr - fee_rule.fixed_fee()).unwrap(), ) .unwrap(); - let (tx_c, _) = builder_c - .build(BranchId::Canopy, &prover) + let res_c = builder_c + .txn_builder + .build_zfuture( + &TransparentSigningSet::new(), + sapling_extsks, + &[], + OsRng, + &prover, + &prover, + &fee_rule, + ) .map_err(|e| format!("build failure: {:?}", e)) .unwrap(); + let tze_c = res_c.transaction().tze_bundle().unwrap(); // Verify tx_b - let ctx0 = Ctx { tx: &tx_b }; + let ctx0 = Ctx { + tx: res_b.transaction(), + }; assert_eq!( - Program.verify( - &tx_a.tze_outputs[0].precondition, - &tx_b.tze_inputs[0].witness, - &ctx0 - ), + Program.verify(&tze_a.vout[0].precondition, &tze_b.vin[0].witness, &ctx0), Ok(()) ); // Verify tx_c - let ctx1 = Ctx { tx: &tx_c }; + let ctx1 = Ctx { + tx: res_c.transaction(), + }; assert_eq!( - Program.verify( - &tx_b.tze_outputs[0].precondition, - &tx_c.tze_inputs[0].witness, - &ctx1 - ), + Program.verify(&tze_b.vout[0].precondition, &tze_c.vin[0].witness, &ctx1), Ok(()) ); } diff --git a/zcash_history/CHANGELOG.md b/zcash_history/CHANGELOG.md index f4477718af..3e010d8ad0 100644 --- a/zcash_history/CHANGELOG.md +++ b/zcash_history/CHANGELOG.md @@ -6,6 +6,28 @@ and this library adheres to Rust's notion of [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Changed +- MSRV is now 1.81.0. + +## [0.4.0] - 2023-03-01 +### Changed +- MSRV is now 1.65.0. +- Bumped dependencies to `primitive-types 0.12`. + +## [0.3.0] - 2022-05-11 +### Added +- Support for multiple history tree versions: + - `zcash_history::Version` trait. + - `zcash_history::V1`, marking the original history tree version. + - `zcash_history::V2`, marking the history tree version from NU5. +- `zcash_history::Entry::new_leaf` + +### Changed +- MSRV is now 1.56.1. +- `zcash_history::{Entry, IndexedNode, Tree}` now have a `Version` parameter. + +### Removed +- `impl From for Entry` (replaced by `Entry::new_leaf`). ## [0.2.0] - 2020-03-13 No changes, just a version bump. diff --git a/zcash_history/Cargo.toml b/zcash_history/Cargo.toml index d25a1347c3..5f9fc93225 100644 --- a/zcash_history/Cargo.toml +++ b/zcash_history/Cargo.toml @@ -1,17 +1,29 @@ [package] name = "zcash_history" -version = "0.2.0" +version = "0.4.0" authors = ["NikVolf "] -edition = "2018" -license = "MIT/Apache-2.0" -documentation = "https://docs.rs/zcash_history/" +edition.workspace = true +rust-version.workspace = true +repository.workspace = true +license.workspace = true description = "Library for Zcash blockchain history tools" +categories.workspace = true [dev-dependencies] -assert_matches = "1.3.0" -quickcheck = "0.9" +assert_matches.workspace = true +proptest.workspace = true [dependencies] -bigint = "4" -byteorder = "1" -blake2 = { package = "blake2b_simd", version = "0.5" } +primitive-types = { version = "0.12", default-features = false } +byteorder.workspace = true +blake2b_simd.workspace = true +proptest = { workspace = true, optional = true } + +[features] +test-dependencies = ["dep:proptest"] + +[lib] +bench = false + +[lints] +workspace = true diff --git a/zcash_history/examples/lib/shared.rs b/zcash_history/examples/lib/shared.rs index 61f84c8423..c4f8eecaf4 100644 --- a/zcash_history/examples/lib/shared.rs +++ b/zcash_history/examples/lib/shared.rs @@ -1,8 +1,8 @@ -use zcash_history::{Entry, EntryLink, NodeData, Tree}; +use zcash_history::{Entry, EntryLink, NodeData, Tree, V1}; pub struct NodeDataIterator { return_stack: Vec, - tree: Tree, + tree: Tree, cursor: usize, leaf_cursor: usize, } @@ -56,7 +56,7 @@ impl NodeDataIterator { let tree = Tree::new( 3, vec![(2, root)], - vec![(0, leaf(1).into()), (1, leaf(2).into())], + vec![(0, Entry::new_leaf(leaf(1))), (1, Entry::new_leaf(leaf(2)))], ); NodeDataIterator { diff --git a/zcash_history/examples/long.rs b/zcash_history/examples/long.rs index e074ff38ac..0a5599fccd 100644 --- a/zcash_history/examples/long.rs +++ b/zcash_history/examples/long.rs @@ -1,12 +1,12 @@ -use zcash_history::{Entry, EntryLink, NodeData, Tree}; +use zcash_history::{Entry, EntryLink, NodeData, Tree, V1}; #[path = "lib/shared.rs"] mod share; -fn draft(into: &mut Vec<(u32, Entry)>, vec: &[NodeData], peak_pos: usize, h: u32) { +fn draft(into: &mut Vec<(u32, Entry)>, vec: &[NodeData], peak_pos: usize, h: u32) { let node_data = vec[peak_pos - 1].clone(); - let peak: Entry = match h { - 0 => node_data.into(), + let peak = match h { + 0 => Entry::new_leaf(node_data), _ => Entry::new( node_data, EntryLink::Stored((peak_pos - (1 << h) - 1) as u32), @@ -19,7 +19,7 @@ fn draft(into: &mut Vec<(u32, Entry)>, vec: &[NodeData], peak_pos: usize, h: u32 into.push(((peak_pos - 1) as u32, peak)); } -fn prepare_tree(vec: &[NodeData]) -> Tree { +fn prepare_tree(vec: &[NodeData]) -> Tree { assert!(!vec.is_empty()); // integer log2 of (vec.len()+1), -1 diff --git a/zcash_history/examples/write.rs b/zcash_history/examples/write.rs index cd9d6511cd..0d894eb74c 100644 --- a/zcash_history/examples/write.rs +++ b/zcash_history/examples/write.rs @@ -32,7 +32,7 @@ fn main() { node.write(&mut buf).expect("Failed to write data"); } - let mut file = std::fs::File::create(&out_file_path).expect("Failed to create output file"); + let mut file = std::fs::File::create(out_file_path).expect("Failed to create output file"); file.write_all(&buf[..]) .expect("Failed to write data to file"); diff --git a/zcash_history/src/entry.rs b/zcash_history/src/entry.rs index 310382b04a..81c6b46ff7 100644 --- a/zcash_history/src/entry.rs +++ b/zcash_history/src/entry.rs @@ -1,26 +1,34 @@ use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; -use crate::{EntryKind, EntryLink, Error, NodeData, MAX_NODE_DATA_SIZE}; +use crate::{EntryKind, EntryLink, Error, Version, MAX_NODE_DATA_SIZE}; /// Max serialized length of entry data. pub const MAX_ENTRY_SIZE: usize = MAX_NODE_DATA_SIZE + 9; /// MMR Entry. #[derive(Debug)] -pub struct Entry { +pub struct Entry { pub(crate) kind: EntryKind, - pub(crate) data: NodeData, + pub(crate) data: V::NodeData, } -impl Entry { +impl Entry { /// New entry of type node. - pub fn new(data: NodeData, left: EntryLink, right: EntryLink) -> Self { + pub fn new(data: V::NodeData, left: EntryLink, right: EntryLink) -> Self { Entry { kind: EntryKind::Node(left, right), data, } } + /// Creates a new leaf. + pub fn new_leaf(data: V::NodeData) -> Self { + Entry { + kind: EntryKind::Leaf, + data, + } + } + /// Returns if is this node complete (has total of 2^N leaves) pub fn complete(&self) -> bool { let leaves = self.leaf_count(); @@ -29,7 +37,7 @@ impl Entry { /// Number of leaves under this node. pub fn leaf_count(&self) -> u64 { - self.data.end_height - (self.data.start_height - 1) + V::end_height(&self.data) - (V::start_height(&self.data) - 1) } /// Is this node a leaf. @@ -67,7 +75,7 @@ impl Entry { } }; - let data = NodeData::read(consensus_branch_id, r)?; + let data = V::read(consensus_branch_id, r)?; Ok(Entry { kind, data }) } @@ -88,7 +96,7 @@ impl Entry { } } - self.data.write(w)?; + V::write(&self.data, w)?; Ok(()) } @@ -100,16 +108,7 @@ impl Entry { } } -impl From for Entry { - fn from(s: NodeData) -> Self { - Entry { - kind: EntryKind::Leaf, - data: s, - } - } -} - -impl std::fmt::Display for Entry { +impl std::fmt::Display for Entry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self.kind { EntryKind::Node(l, r) => write!(f, "node({}, {}, ..)", l, r), diff --git a/zcash_history/src/lib.rs b/zcash_history/src/lib.rs index 5732ff640e..deb4867fc0 100644 --- a/zcash_history/src/lib.rs +++ b/zcash_history/src/lib.rs @@ -3,16 +3,18 @@ //! To be used in zebra and via FFI bindings in zcashd // Catch documentation errors caused by code changes. -#![deny(broken_intra_doc_links)] +#![deny(rustdoc::broken_intra_doc_links)] #![warn(missing_docs)] mod entry; mod node_data; mod tree; +mod version; pub use entry::{Entry, MAX_ENTRY_SIZE}; pub use node_data::{NodeData, MAX_NODE_DATA_SIZE}; pub use tree::Tree; +pub use version::{Version, V1, V2}; /// Crate-level error type #[derive(Debug)] @@ -33,7 +35,7 @@ impl std::fmt::Display for Error { } } -/// Reference to to the tree node. +/// Reference to the tree node. #[repr(C)] #[derive(Clone, Copy, Debug)] pub enum EntryLink { diff --git a/zcash_history/src/node_data.rs b/zcash_history/src/node_data.rs index 0c93c9f781..def91e326d 100644 --- a/zcash_history/src/node_data.rs +++ b/zcash_history/src/node_data.rs @@ -1,6 +1,7 @@ -use bigint::U256; -use blake2::Params as Blake2Params; -use byteorder::{ByteOrder, LittleEndian, ReadBytesExt, WriteBytesExt}; +use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; +use primitive_types::U256; + +use crate::Version; /// Maximum serialized size of the node metadata. pub const MAX_NODE_DATA_SIZE: usize = 32 + // subtree commitment @@ -13,13 +14,16 @@ pub const MAX_NODE_DATA_SIZE: usize = 32 + // subtree commitment 32 + // subtree total work 9 + // start height (compact uint) 9 + // end height (compact uint) - 9; // Sapling tx count (compact uint) - // = total of 171 + 9 + // Sapling tx count (compact uint) + 32 + // start Orchard tree root + 32 + // end Orchard tree root + 9; // Orchard tx count (compact uint) + // = total of 244 -/// Node metadata. +/// V1 node metadata. #[repr(C)] #[derive(Debug, Clone, Default)] -#[cfg_attr(test, derive(PartialEq))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct NodeData { /// Consensus branch id, should be provided by deserializing node. pub consensus_branch_id: u32, @@ -47,49 +51,20 @@ pub struct NodeData { pub sapling_tx: u64, } -fn blake2b_personal(personalization: &[u8], input: &[u8]) -> [u8; 32] { - let hash_result = Blake2Params::new() - .hash_length(32) - .personal(personalization) - .to_state() - .update(input) - .finalize(); - let mut result = [0u8; 32]; - result.copy_from_slice(hash_result.as_bytes()); - result -} - -fn personalization(branch_id: u32) -> [u8; 16] { - let mut result = [0u8; 16]; - result[..12].copy_from_slice(b"ZcashHistory"); - LittleEndian::write_u32(&mut result[12..], branch_id); - result -} - impl NodeData { /// Combine two nodes metadata. pub fn combine(left: &NodeData, right: &NodeData) -> NodeData { - assert_eq!(left.consensus_branch_id, right.consensus_branch_id); - - let mut hash_buf = [0u8; MAX_NODE_DATA_SIZE * 2]; - let size = { - let mut cursor = ::std::io::Cursor::new(&mut hash_buf[..]); - left.write(&mut cursor) - .expect("Writing to memory buf with enough length cannot fail; qed"); - right - .write(&mut cursor) - .expect("Writing to memory buf with enough length cannot fail; qed"); - cursor.position() as usize - }; - - let hash = blake2b_personal( - &personalization(left.consensus_branch_id), - &hash_buf[..size], - ); + crate::V1::combine(left, right) + } + pub(crate) fn combine_inner( + subtree_commitment: [u8; 32], + left: &NodeData, + right: &NodeData, + ) -> NodeData { NodeData { consensus_branch_id: left.consensus_branch_id, - subtree_commitment: hash, + subtree_commitment, start_time: left.start_time, end_time: right.end_time, start_target: left.start_target, @@ -180,63 +155,118 @@ impl NodeData { /// Convert to byte representation. pub fn to_bytes(&self) -> Vec { - let mut buf = [0u8; MAX_NODE_DATA_SIZE]; - let pos = { - let mut cursor = std::io::Cursor::new(&mut buf[..]); - self.write(&mut cursor).expect("Cursor cannot fail"); - cursor.position() as usize - }; - - buf[0..pos].to_vec() + crate::V1::to_bytes(self) } /// Convert from byte representation. pub fn from_bytes>(consensus_branch_id: u32, buf: T) -> std::io::Result { - let mut cursor = std::io::Cursor::new(buf); - Self::read(consensus_branch_id, &mut cursor) + crate::V1::from_bytes(consensus_branch_id, buf) } /// Hash node metadata pub fn hash(&self) -> [u8; 32] { - let bytes = self.to_bytes(); - - blake2b_personal(&personalization(self.consensus_branch_id), &bytes) + crate::V1::hash(self) } } -#[cfg(test)] -impl quickcheck::Arbitrary for NodeData { - fn arbitrary(gen: &mut G) -> Self { - let mut node_data = NodeData { - consensus_branch_id: 0, +/// V2 node metadata. +#[derive(Debug, Clone, Default)] +#[cfg_attr(test, derive(PartialEq, Eq))] +pub struct V2 { + /// The V1 node data retained in V2. + pub v1: NodeData, + /// Start Orchard tree root. + pub start_orchard_root: [u8; 32], + /// End Orchard tree root. + pub end_orchard_root: [u8; 32], + /// Number of Orchard transactions. + pub orchard_tx: u64, +} + +impl V2 { + pub(crate) fn combine_inner(subtree_commitment: [u8; 32], left: &V2, right: &V2) -> V2 { + V2 { + v1: NodeData::combine_inner(subtree_commitment, &left.v1, &right.v1), + start_orchard_root: left.start_orchard_root, + end_orchard_root: right.end_orchard_root, + orchard_tx: left.orchard_tx + right.orchard_tx, + } + } + + /// Write to the byte representation. + pub fn write(&self, w: &mut W) -> std::io::Result<()> { + self.v1.write(w)?; + w.write_all(&self.start_orchard_root)?; + w.write_all(&self.end_orchard_root)?; + NodeData::write_compact(w, self.orchard_tx)?; + Ok(()) + } + + /// Read from the byte representation. + pub fn read(consensus_branch_id: u32, r: &mut R) -> std::io::Result { + let mut data = V2 { + v1: NodeData::read(consensus_branch_id, r)?, ..Default::default() }; - gen.fill_bytes(&mut node_data.subtree_commitment[..]); - node_data.start_time = gen.next_u32(); - node_data.end_time = gen.next_u32(); - node_data.start_target = gen.next_u32(); - node_data.end_target = gen.next_u32(); - gen.fill_bytes(&mut node_data.start_sapling_root[..]); - gen.fill_bytes(&mut node_data.end_sapling_root[..]); - let mut number = [0u8; 32]; - gen.fill_bytes(&mut number[..]); - node_data.subtree_total_work = U256::from_little_endian(&number[..]); - node_data.start_height = gen.next_u64(); - node_data.end_height = gen.next_u64(); - node_data.sapling_tx = gen.next_u64(); - - node_data + r.read_exact(&mut data.start_orchard_root)?; + r.read_exact(&mut data.end_orchard_root)?; + data.orchard_tx = NodeData::read_compact(r)?; + + Ok(data) + } +} + +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing { + use primitive_types::U256; + use proptest::array::uniform32; + use proptest::prelude::{any, prop_compose}; + + use super::NodeData; + + prop_compose! { + pub fn arb_node_data()( + subtree_commitment in uniform32(any::()), + start_time in any::(), + end_time in any::(), + start_target in any::(), + end_target in any::(), + start_sapling_root in uniform32(any::()), + end_sapling_root in uniform32(any::()), + subtree_total_work in uniform32(any::()), + start_height in any::(), + end_height in any::(), + sapling_tx in any::(), + ) -> NodeData { + NodeData { + consensus_branch_id: 0, + subtree_commitment, + start_time, + end_time, + start_target, + end_target, + start_sapling_root, + end_sapling_root, + subtree_total_work: U256::from_little_endian(&subtree_total_work[..]), + start_height, + end_height, + sapling_tx + } + } } } #[cfg(test)] mod tests { + use super::testing::arb_node_data; + use proptest::prelude::*; + use super::NodeData; - use quickcheck::{quickcheck, TestResult}; - quickcheck! { - fn serialization_round_trip(node_data: NodeData) -> TestResult { - TestResult::from_bool(NodeData::from_bytes(0, &node_data.to_bytes()).unwrap() == node_data) + proptest! { + #[test] + fn serialization_round_trip(node_data in arb_node_data()) { + assert_eq!(NodeData::from_bytes(0, node_data.to_bytes()).unwrap(), node_data); } } } diff --git a/zcash_history/src/tree.rs b/zcash_history/src/tree.rs index 666623741f..e5a207ec58 100644 --- a/zcash_history/src/tree.rs +++ b/zcash_history/src/tree.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use crate::{Entry, EntryKind, EntryLink, Error, NodeData}; +use crate::{Entry, EntryKind, EntryLink, Error, Version}; /// Represents partially loaded tree. /// @@ -13,11 +13,11 @@ use crate::{Entry, EntryKind, EntryLink, Error, NodeData}; /// Intended use of this `Tree` is to instantiate it based on partially loaded data (see example /// how to pick right nodes from the array representation of MMR Tree), perform several operations /// (append-s/delete-s) and then drop it. -pub struct Tree { - stored: HashMap, +pub struct Tree { + stored: HashMap>, // This can grow indefinitely if `Tree` is misused as a self-contained data structure - generated: Vec, + generated: Vec>, // number of persistent(!) tree entries stored_count: u32, @@ -25,9 +25,9 @@ pub struct Tree { root: EntryLink, } -impl Tree { +impl Tree { /// Resolve link originated from this tree - pub fn resolve_link(&self, link: EntryLink) -> Result { + pub fn resolve_link(&self, link: EntryLink) -> Result, Error> { match link { EntryLink::Generated(index) => self.generated.get(index as usize), EntryLink::Stored(index) => self.stored.get(&index), @@ -36,14 +36,14 @@ impl Tree { .ok_or(Error::ExpectedInMemory(link)) } - fn push(&mut self, data: Entry) -> EntryLink { + fn push(&mut self, data: Entry) -> EntryLink { let idx = self.stored_count; self.stored_count += 1; self.stored.insert(idx, data); EntryLink::Stored(idx) } - fn push_generated(&mut self, data: Entry) -> EntryLink { + fn push_generated(&mut self, data: Entry) -> EntryLink { self.generated.push(data); EntryLink::Generated(self.generated.len() as u32 - 1) } @@ -51,7 +51,7 @@ impl Tree { /// Populate tree with plain list of the leaves/nodes. For now, only for tests, /// since this `Tree` structure is for partially loaded tree (but it might change) #[cfg(test)] - pub fn populate(loaded: Vec, root: EntryLink) -> Self { + pub fn populate(loaded: Vec>, root: EntryLink) -> Self { let mut result = Tree::invalid(); result.stored_count = loaded.len() as u32; for (idx, item) in loaded.into_iter().enumerate() { @@ -72,7 +72,7 @@ impl Tree { } } - /// New view into the the tree array representation + /// New view into the tree array representation /// /// `length` is total length of the array representation (is generally not a sum of /// peaks.len + extra.len) @@ -83,7 +83,7 @@ impl Tree { /// # Panics /// /// Will panic if `peaks` is empty. - pub fn new(length: u32, peaks: Vec<(u32, Entry)>, extra: Vec<(u32, Entry)>) -> Self { + pub fn new(length: u32, peaks: Vec<(u32, Entry)>, extra: Vec<(u32, Entry)>) -> Self { assert!(!peaks.is_empty()); let mut result = Tree::invalid(); @@ -135,9 +135,9 @@ impl Tree { /// Returns links to actual nodes that has to be persisted as the result of the append. /// If completed without error, at least one link to the appended /// node (with metadata provided in `new_leaf`) will be returned. - pub fn append_leaf(&mut self, new_leaf: NodeData) -> Result, Error> { + pub fn append_leaf(&mut self, new_leaf: V::NodeData) -> Result, Error> { let root = self.root; - let new_leaf_link = self.push(new_leaf.into()); + let new_leaf_link = self.push(Entry::new_leaf(new_leaf)); let mut appended = vec![new_leaf_link]; let mut peaks = Vec::new(); @@ -245,7 +245,7 @@ impl Tree { } } - let mut new_root = *peaks.get(0).expect("At lest 1 elements in peaks"); + let mut new_root = *peaks.first().expect("At lest 1 elements in peaks"); for next_peak in peaks.into_iter().skip(1) { new_root = self.push_generated(combine_nodes( @@ -274,7 +274,7 @@ impl Tree { } /// Reference to the root node. - pub fn root_node(&self) -> Result { + pub fn root_node(&self) -> Result, Error> { self.resolve_link(self.root) } @@ -286,12 +286,12 @@ impl Tree { /// Reference to the node with link attached. #[derive(Debug)] -pub struct IndexedNode<'a> { - node: &'a Entry, +pub struct IndexedNode<'a, V: Version> { + node: &'a Entry, link: EntryLink, } -impl<'a> IndexedNode<'a> { +impl IndexedNode<'_, V> { fn left(&self) -> Result { self.node.left().map_err(|e| e.augment(self.link)) } @@ -301,12 +301,12 @@ impl<'a> IndexedNode<'a> { } /// Reference to the entry struct. - pub fn node(&self) -> &Entry { + pub fn node(&self) -> &Entry { self.node } /// Reference to the entry metadata. - pub fn data(&self) -> &NodeData { + pub fn data(&self) -> &V::NodeData { &self.node.data } @@ -316,43 +316,49 @@ impl<'a> IndexedNode<'a> { } } -fn combine_nodes<'a>(left: IndexedNode<'a>, right: IndexedNode<'a>) -> Entry { +fn combine_nodes<'a, V: Version>(left: IndexedNode<'a, V>, right: IndexedNode<'a, V>) -> Entry { Entry { kind: EntryKind::Node(left.link, right.link), - data: NodeData::combine(&left.node.data, &right.node.data), + data: V::combine(&left.node.data, &right.node.data), } } #[cfg(test)] mod tests { + use super::{Entry, EntryKind, EntryLink, Tree}; + use crate::{node_data, NodeData, Version, V2}; - use super::{Entry, EntryKind, EntryLink, NodeData, Tree}; use assert_matches::assert_matches; - use quickcheck::{quickcheck, TestResult}; - - fn leaf(height: u32) -> NodeData { - NodeData { - consensus_branch_id: 1, - subtree_commitment: [0u8; 32], - start_time: 0, - end_time: 0, - start_target: 0, - end_target: 0, - start_sapling_root: [0u8; 32], - end_sapling_root: [0u8; 32], - subtree_total_work: 0.into(), - start_height: height as u64, - end_height: height as u64, - sapling_tx: 7, + use proptest::prelude::*; + + fn leaf(height: u32) -> node_data::V2 { + node_data::V2 { + v1: NodeData { + consensus_branch_id: 1, + subtree_commitment: [0u8; 32], + start_time: 0, + end_time: 0, + start_target: 0, + end_target: 0, + start_sapling_root: [0u8; 32], + end_sapling_root: [0u8; 32], + subtree_total_work: 0.into(), + start_height: height as u64, + end_height: height as u64, + sapling_tx: 7, + }, + start_orchard_root: [0u8; 32], + end_orchard_root: [0u8; 32], + orchard_tx: 42, } } - fn initial() -> Tree { - let node1: Entry = leaf(1).into(); - let node2: Entry = leaf(2).into(); + fn initial() -> Tree { + let node1 = Entry::new_leaf(leaf(1)); + let node2 = Entry::new_leaf(leaf(2)); let node3 = Entry { - data: NodeData::combine(&node1.data, &node2.data), + data: V2::combine(&node1.data, &node2.data), kind: EntryKind::Leaf, }; @@ -360,7 +366,7 @@ mod tests { } // returns tree with specified number of leafs and it's root - fn generated(length: u32) -> Tree { + fn generated(length: u32) -> Tree { assert!(length >= 3); let mut tree = initial(); for i in 2..length { @@ -391,7 +397,7 @@ mod tests { // // so only (3) is added as real leaf // while new root, (4g) is generated one - assert_eq!(new_root.data.end_height, 3); + assert_eq!(new_root.data.v1.end_height, 3); assert_eq!(appended.len(), 1); // ** APPEND 4 ** @@ -415,7 +421,7 @@ mod tests { // // so (4), (5), (6) are added as real leaves // and new root, (6) is stored one - assert_eq!(new_root.data.end_height, 4); + assert_eq!(new_root.data.v1.end_height, 4); assert_eq!(appended.len(), 3); assert_matches!(tree.root(), EntryLink::Stored(6)); @@ -442,7 +448,7 @@ mod tests { // // so (7) is added as real leaf // and new root, (8g) is generated one - assert_eq!(new_root.data.end_height, 5); + assert_eq!(new_root.data.v1.end_height, 5); assert_eq!(appended.len(), 1); assert_matches!(tree.root(), EntryLink::Generated(_)); tree.for_children(tree.root(), |l, r| { @@ -474,7 +480,7 @@ mod tests { // // so (7) is added as real leaf // and new root, (10g) is generated one - assert_eq!(new_root.data.end_height, 6); + assert_eq!(new_root.data.v1.end_height, 6); assert_eq!(appended.len(), 2); assert_matches!(tree.root(), EntryLink::Generated(_)); tree.for_children(tree.root(), |l, r| { @@ -509,7 +515,7 @@ mod tests { // // so (10) is added as real leaf // and new root, (12g) is generated one - assert_eq!(new_root.data.end_height, 7); + assert_eq!(new_root.data.v1.end_height, 7); assert_eq!(appended.len(), 1); assert_matches!(tree.root(), EntryLink::Generated(_)); tree.for_children(tree.root(), |l, r| { @@ -633,100 +639,88 @@ mod tests { assert_eq!(tree.len(), 4083); // 4095 - log2(4096) } - quickcheck! { - fn there_and_back(number: u32) -> TestResult { - if number > 1024*1024 { - TestResult::discard() - } else { - let mut tree = initial(); - for i in 0..number { - tree.append_leaf(leaf(i+3)).expect("Failed to append"); - } - for _ in 0..number { - tree.truncate_leaf().expect("Failed to truncate"); - } - - TestResult::from_bool(matches!(tree.root(), EntryLink::Stored(2))) + proptest! { + #[test] + fn prop_there_and_back(number in 0u32..=1024) { + let mut tree = initial(); + for i in 0..number { + tree.append_leaf(leaf(i+3)).expect("Failed to append"); + } + for _ in 0..number { + tree.truncate_leaf().expect("Failed to truncate"); } - } - fn leaf_count(number: u32) -> TestResult { - if !(3..=1024 * 1024).contains(&number) { - TestResult::discard() - } else { - let mut tree = initial(); - for i in 1..(number-1) { - tree.append_leaf(leaf(i+2)).expect("Failed to append"); - } + assert_matches!(tree.root(), EntryLink::Stored(2)); + } - TestResult::from_bool( - tree.root_node().expect("no root").node.leaf_count() == number as u64 - ) + #[test] + fn prop_leaf_count(number in 3u32..=1024) { + let mut tree = initial(); + for i in 1..(number-1) { + tree.append_leaf(leaf(i+2)).expect("Failed to append"); } + + assert_eq!(tree.root_node().expect("no root").node.leaf_count(), number as u64); } - fn parity(number: u32) -> TestResult { - if !(3..=2048 * 2048).contains(&number) { - TestResult::discard() - } else { - let mut tree = initial(); - for i in 1..(number-1) { - tree.append_leaf(leaf(i+2)).expect("Failed to append"); - } + #[test] + fn prop_parity(number in 3u32..=2048) { + let mut tree = initial(); + for i in 1..(number-1) { + tree.append_leaf(leaf(i+2)).expect("Failed to append"); + } - TestResult::from_bool( - if number & (number - 1) == 0 { - matches!(tree.root(), EntryLink::Stored(_)) - } else { - matches!(tree.root(), EntryLink::Generated(_)) - } - ) + if number & (number - 1) == 0 { + assert_matches!(tree.root(), EntryLink::Stored(_)); + } else { + assert_matches!(tree.root(), EntryLink::Generated(_)); } } - fn parity_with_truncate(add: u32, delete: u32) -> TestResult { + #[test] + fn prop_parity_with_truncate( + add_and_delete in (0u32..=2048).prop_flat_map( + |add| (Just(add), 0..=add) + ) + ) { + let (add, delete) = add_and_delete; // First we add `add` number of leaves, then delete `delete` number of leaves // What is left should be consistent with generated-stored structure - if add > 2048 * 2048 || add < delete { - TestResult::discard() - } else { - let mut tree = initial(); - for i in 0..add { - tree.append_leaf(leaf(i+3)).expect("Failed to append"); - } - for _ in 0..delete { - tree.truncate_leaf().expect("Failed to truncate"); - } + let mut tree = initial(); + for i in 0..add { + tree.append_leaf(leaf(i+3)).expect("Failed to append"); + } + for _ in 0..delete { + tree.truncate_leaf().expect("Failed to truncate"); + } - let total = add - delete + 2; + let total = add - delete + 2; - TestResult::from_bool( - if total & (total - 1) == 0 { - matches!(tree.root(), EntryLink::Stored(_)) - } else { - matches!(tree.root(), EntryLink::Generated(_)) - } - ) + if total & (total - 1) == 0 { + assert_matches!(tree.root(), EntryLink::Stored(_)); + } else { + assert_matches!(tree.root(), EntryLink::Generated(_)); } } - // Length of tree is always less than number of leaves squared - fn stored_length(add: u32, delete: u32) -> TestResult { - if add > 2048 * 2048 || add < delete { - TestResult::discard() - } else { - let mut tree = initial(); - for i in 0..add { - tree.append_leaf(leaf(i+3)).expect("Failed to append"); - } - for _ in 0..delete { - tree.truncate_leaf().expect("Failed to truncate"); - } + #[test] + fn prop_stored_length( + add_and_delete in (0u32..=2048).prop_flat_map( + |add| (Just(add), 0..=add) + ) + ) { + let (add, delete) = add_and_delete; + let mut tree = initial(); + for i in 0..add { + tree.append_leaf(leaf(i+3)).expect("Failed to append"); + } + for _ in 0..delete { + tree.truncate_leaf().expect("Failed to truncate"); + } - let total = add - delete + 2; + let total = add - delete + 2; - TestResult::from_bool(total * total > tree.len()) - } + assert!(total * total > tree.len()) } } } diff --git a/zcash_history/src/version.rs b/zcash_history/src/version.rs new file mode 100644 index 0000000000..bfc18fa6f0 --- /dev/null +++ b/zcash_history/src/version.rs @@ -0,0 +1,181 @@ +use std::fmt; +use std::io; + +use blake2b_simd::Params as Blake2Params; +use byteorder::{ByteOrder, LittleEndian}; + +use crate::{node_data, NodeData, MAX_NODE_DATA_SIZE}; + +fn blake2b_personal(personalization: &[u8], input: &[u8]) -> [u8; 32] { + let hash_result = Blake2Params::new() + .hash_length(32) + .personal(personalization) + .to_state() + .update(input) + .finalize(); + let mut result = [0u8; 32]; + result.copy_from_slice(hash_result.as_bytes()); + result +} + +fn personalization(branch_id: u32) -> [u8; 16] { + let mut result = [0u8; 16]; + result[..12].copy_from_slice(b"ZcashHistory"); + LittleEndian::write_u32(&mut result[12..], branch_id); + result +} + +/// A version of the chain history tree. +pub trait Version { + /// The node data for this tree version. + type NodeData: fmt::Debug; + + /// Returns the consensus branch ID for the given node data. + fn consensus_branch_id(data: &Self::NodeData) -> u32; + + /// Returns the start height for the given node data. + fn start_height(data: &Self::NodeData) -> u64; + + /// Returns the end height for the given node data. + fn end_height(data: &Self::NodeData) -> u64; + + /// Combines two nodes' metadata. + fn combine(left: &Self::NodeData, right: &Self::NodeData) -> Self::NodeData { + assert_eq!( + Self::consensus_branch_id(left), + Self::consensus_branch_id(right) + ); + + let mut hash_buf = [0u8; MAX_NODE_DATA_SIZE * 2]; + let size = { + let mut cursor = ::std::io::Cursor::new(&mut hash_buf[..]); + Self::write(left, &mut cursor) + .expect("Writing to memory buf with enough length cannot fail; qed"); + Self::write(right, &mut cursor) + .expect("Writing to memory buf with enough length cannot fail; qed"); + cursor.position() as usize + }; + + let hash = blake2b_personal( + &personalization(Self::consensus_branch_id(left)), + &hash_buf[..size], + ); + + Self::combine_inner(hash, left, right) + } + + /// Combines two nodes metadata. + /// + /// For internal use. + fn combine_inner( + subtree_commitment: [u8; 32], + left: &Self::NodeData, + right: &Self::NodeData, + ) -> Self::NodeData; + + /// Parses node data from the given reader. + fn read(consensus_branch_id: u32, r: &mut R) -> io::Result; + + /// Writes the byte representation of the given node data to the given writer. + fn write(data: &Self::NodeData, w: &mut W) -> io::Result<()>; + + /// Converts to byte representation. + #[allow(clippy::wrong_self_convention)] + fn to_bytes(data: &Self::NodeData) -> Vec { + let mut buf = [0u8; MAX_NODE_DATA_SIZE]; + let pos = { + let mut cursor = std::io::Cursor::new(&mut buf[..]); + Self::write(data, &mut cursor).expect("Cursor cannot fail"); + cursor.position() as usize + }; + + buf[0..pos].to_vec() + } + + /// Convert from byte representation. + fn from_bytes>(consensus_branch_id: u32, buf: T) -> io::Result { + let mut cursor = std::io::Cursor::new(buf); + Self::read(consensus_branch_id, &mut cursor) + } + + /// Hash node metadata + fn hash(data: &Self::NodeData) -> [u8; 32] { + let bytes = Self::to_bytes(data); + + blake2b_personal(&personalization(Self::consensus_branch_id(data)), &bytes) + } +} + +/// Version 1 of the Zcash chain history tree. +/// +/// This version was used for the Heartwood and Canopy epochs. +pub enum V1 {} + +impl Version for V1 { + type NodeData = NodeData; + + fn consensus_branch_id(data: &Self::NodeData) -> u32 { + data.consensus_branch_id + } + + fn start_height(data: &Self::NodeData) -> u64 { + data.start_height + } + + fn end_height(data: &Self::NodeData) -> u64 { + data.end_height + } + + fn combine_inner( + subtree_commitment: [u8; 32], + left: &Self::NodeData, + right: &Self::NodeData, + ) -> Self::NodeData { + NodeData::combine_inner(subtree_commitment, left, right) + } + + fn read(consensus_branch_id: u32, r: &mut R) -> io::Result { + NodeData::read(consensus_branch_id, r) + } + + fn write(data: &Self::NodeData, w: &mut W) -> io::Result<()> { + data.write(w) + } +} + +/// Version 2 of the Zcash chain history tree. +/// +/// This version is used from the NU5 epoch. +pub enum V2 {} + +impl Version for V2 { + type NodeData = node_data::V2; + + fn consensus_branch_id(data: &Self::NodeData) -> u32 { + data.v1.consensus_branch_id + } + + fn start_height(data: &Self::NodeData) -> u64 { + data.v1.start_height + } + + fn end_height(data: &Self::NodeData) -> u64 { + data.v1.end_height + } + + fn combine_inner( + subtree_commitment: [u8; 32], + left: &Self::NodeData, + right: &Self::NodeData, + ) -> Self::NodeData { + node_data::V2::combine_inner(subtree_commitment, left, right) + } + + fn read(consensus_branch_id: u32, r: &mut R) -> io::Result { + node_data::V2::read(consensus_branch_id, r) + } + + fn write(data: &Self::NodeData, w: &mut W) -> io::Result<()> { + data.write(w) + } +} diff --git a/zcash_keys/CHANGELOG.md b/zcash_keys/CHANGELOG.md new file mode 100644 index 0000000000..0dd4c8c27a --- /dev/null +++ b/zcash_keys/CHANGELOG.md @@ -0,0 +1,181 @@ +All notable changes to this library will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this library adheres to Rust's notion of +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- `no-std` compatibility (`alloc` is required). A default-enabled `std` feature + flag has been added gating the `std::error::Error` usage. +- `zcash_keys::keys::ReceiverRequirement` +- `zcash_keys::Address::to_transparent_address` + +### Changed +- MSRV is now 1.81.0. +- Migrated to `bip32 =0.6.0-pre.1`, `nonempty 0.11`. +- `zcash_keys::keys::UnifiedAddressRequest` has been substantially modified; + instead of a collection of boolean flags, it is now a collection of + `ReceiverRequirement` values that describe how addresses may be constructed + in the case that keys for a particular protocol are absent or it is not + possible to generate a specific receiver at a given diversifier index. + Behavior of methods that accept a `UnifiedAddressRequest` have been modified + accordingly. In addition, request construction methods that previously + returned `None` to indicate an attempt to generate an invalid request now + return `Err(())` + +### Removed +- `zcash_keys::keys::UnifiedAddressRequest::all` (use + `UnifiedAddressRequest::ALLOW_ALL` or + `UnifiedFullViewingKey::to_address_request` instead) + +## [0.6.0] - 2024-12-16 + +### Changed +- Migrated to `bech32 0.11`, `sapling-crypto 0.4`. +- Added dependency on `zcash_transparent 0.1` to replace dependency + on `zcash_primitives`. +- The `UnifiedAddressRequest` argument to the following methods is now optional: + - `zcash_keys::keys::UnifiedSpendingKey::address` + - `zcash_keys::keys::UnifiedSpendingKey::default_address` + - `zcash_keys::keys::UnifiedFullViewingKey::find_address` + - `zcash_keys::keys::UnifiedFullViewingKey::default_address` + - `zcash_keys::keys::UnifiedIncomingViewingKey::address` + - `zcash_keys::keys::UnifiedIncomingViewingKey::find_address` + - `zcash_keys::keys::UnifiedIncomingViewingKey::default_address` + +## [0.5.0] - 2024-11-14 + +### Changed +- Migrated to `zcash_primitives 0.20.0` +- MSRV is now 1.77.0. + +## [0.4.0] - 2024-10-04 + +### Added +- `zcash_keys::encoding::decode_extfvk_with_network` +- `impl std::error::Error for Bech32DecodeError` +- `impl std::error::Error for DecodingError` +- `impl std::error::Error for DerivationError` + +### Changed +- Migrated to `orchard 0.10`, `sapling-crypto 0.3`, `zcash_address 0.6`, + `zcash_primitives 0.19`, `zcash_protocol 0.4`. + +## [0.3.0] - 2024-08-19 +### Notable changes +- `zcash_keys`: + - Now supports TEX (transparent-source-only) addresses as specified + in [ZIP 320](https://zips.z.cash/zip-0320). + - An `unstable-frost` feature has been added in order to be able to + temporarily expose API features that are needed specifically when creating + FROST threshold signatures. The features under this flag will be removed + once key derivation for FROST has been fully specified and implemented. + +### Added +- `zcash_keys::address::Address::try_from_zcash_address` +- `zcash_keys::address::Receiver` +- `zcash_keys::keys::UnifiedAddressRequest` + - `intersect` + - `to_address_request` + +### Changed +- MSRV is now 1.70.0. +- Updated dependencies: + - `zcash_address-0.4` + - `zcash_encoding-0.2.1` + - `zcash_primitives-0.16` + - `zcash_protocol-0.2` +- `zcash_keys::Address` has a new variant `Tex`. +- `zcash_keys::address::Address::has_receiver` has been renamed to `can_receive_as`. +- `zcash_keys::keys`: + - The (unstable) encoding of `UnifiedSpendingKey` has changed. + - `DerivationError::Transparent` now contains `bip32::Error`. + +## [0.2.0] - 2024-03-25 + +### Added +- `zcash_keys::address::Address::has_receiver` +- `impl Display for zcash_keys::keys::AddressGenerationError` +- `impl std::error::Error for zcash_keys::keys::AddressGenerationError` +- `impl From for zcash_keys::keys::DerivationError` + when the `transparent-inputs` feature is enabled. +- `zcash_keys::keys::DecodingError` +- `zcash_keys::keys::UnifiedFullViewingKey::{parse, to_unified_incoming_viewing_key}` +- `zcash_keys::keys::UnifiedIncomingViewingKey` + +### Changed +- `zcash_keys::keys::UnifiedFullViewingKey::{find_address, default_address}` + now return `Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError>` + (instead of `Option<(UnifiedAddress, DiversifierIndex)>` for `find_address`). +- `zcash_keys::keys::AddressGenerationError` + - Added `DiversifierSpaceExhausted` variant. +- At least one of the `orchard`, `sapling`, or `transparent-inputs` features + must be enabled for the `keys` module to be accessible. +- Updated to `zcash_primitives-0.15.0` + +### Removed +- `UnifiedFullViewingKey::new` has been placed behind the `test-dependencies` + feature flag. UFVKs should only be produced by derivation from the USK, or + parsed from their string representation. + +### Fixed +- `UnifiedFullViewingKey::find_address` can now find an address for a diversifier + index outside the valid transparent range if you aren't requesting a + transparent receiver. + +## [0.1.1] - 2024-03-04 + +### Added +- `zcash_keys::keys::UnifiedAddressRequest::all` + +### Fixed +- A missing application of the `sapling` feature flag was remedied; + prior to this fix it was not possible to use this crate without the + `sapling` feature enabled. + +## [0.1.0] - 2024-03-01 +The entries below are relative to the `zcash_client_backend` crate as of +`zcash_client_backend 0.10.0`. + +### Added +- `zcash_keys::address` (moved from `zcash_client_backend::address`). Further + additions to this module: + - `UnifiedAddress::{has_orchard, has_sapling, has_transparent}` + - `UnifiedAddress::receiver_types` + - `UnifiedAddress::unknown` +- `zcash_keys::encoding` (moved from `zcash_client_backend::encoding`). +- `zcash_keys::keys` (moved from `zcash_client_backend::keys`). Further + additions to this module: + - `AddressGenerationError` + - `UnifiedAddressRequest` +- A new `orchard` feature flag has been added to make it possible to + build client code without `orchard` dependendencies. +- `zcash_keys::address::Address::to_zcash_address` + +### Changed +- The following methods and enum variants have been placed behind an `orchard` + feature flag: + - `zcash_keys::address::UnifiedAddress::orchard` + - `zcash_keys::keys::DerivationError::Orchard` + - `zcash_keys::keys::UnifiedSpendingKey::orchard` +- `zcash_keys::address`: + - `RecipientAddress` has been renamed to `Address`. + - `Address::Shielded` has been renamed to `Address::Sapling`. + - `UnifiedAddress::from_receivers` no longer takes an Orchard receiver + argument unless the `orchard` feature is enabled. +- `zcash_keys::keys`: + - `UnifiedSpendingKey::address` now takes an argument that specifies the + receivers to be generated in the resulting address. Also, it now returns + `Result` instead of + `Option` so that we may better report to the user how + address generation has failed. + - `UnifiedSpendingKey::transparent` is now only available when the + `transparent-inputs` feature is enabled. + - `UnifiedFullViewingKey::new` no longer takes an Orchard full viewing key + argument unless the `orchard` feature is enabled. + +### Removed +- `zcash_keys::address::AddressMetadata` + (use `zcash_client_backend::data_api::TransparentAddressMetadata` instead). diff --git a/zcash_keys/Cargo.toml b/zcash_keys/Cargo.toml new file mode 100644 index 0000000000..b0621f03ce --- /dev/null +++ b/zcash_keys/Cargo.toml @@ -0,0 +1,114 @@ +[package] +name = "zcash_keys" +description = "Zcash key and address management" +version = "0.6.0" +authors = [ + "Jack Grigg ", + "Kris Nuttycombe " +] +homepage = "https://github.com/zcash/librustzcash" +repository.workspace = true +readme = "README.md" +license.workspace = true +edition.workspace = true +rust-version.workspace = true +categories.workspace = true + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +zcash_address.workspace = true +zcash_encoding.workspace = true +zcash_protocol.workspace = true +zip32.workspace = true + +# Dependencies exposed in a public API: +nonempty.workspace = true + +# - CSPRNG +rand_core.workspace = true + +# - Encodings +bech32.workspace = true +bs58.workspace = true +core2.workspace = true + +# - Transparent protocols +bip32 = { workspace = true, optional = true } +transparent.workspace = true + +# - Logging and metrics +memuse.workspace = true +tracing.workspace = true + +# - Secret management +secrecy.workspace = true +subtle.workspace = true + +# - Shielded protocols +bls12_381.workspace = true +group.workspace = true +orchard = { workspace = true, optional = true } +sapling = { workspace = true, optional = true } + +# - Test dependencies +proptest = { workspace = true, optional = true } + +# Dependencies used internally: +# (Breaking upgrades to these are usually backwards-compatible, but check MSRVs.) +# - Documentation +document-features = { workspace = true, optional = true } + +# - Encodings +byteorder = { workspace = true, optional = true } + +# - Digests +blake2b_simd = { workspace = true } + +[dev-dependencies] +hex.workspace = true +jubjub.workspace = true +proptest.workspace = true +rand_core.workspace = true +orchard = { workspace = true, features = ["circuit"] } +zcash_address = { workspace = true, features = ["test-dependencies"] } + +[features] +default = ["std"] +std = ["dep:document-features"] + +## Enables use of transparent key parts and addresses +transparent-inputs = [ + "dep:bip32", + "transparent/transparent-inputs", +] + +## Enables use of Orchard key parts and addresses +orchard = ["dep:orchard"] + +## Enables use of Sapling key parts and addresses +sapling = ["dep:sapling"] + +## Exposes APIs that are useful for testing, such as `proptest` strategies. +test-dependencies = [ + "dep:proptest", + "orchard?/test-dependencies", + "sapling?/test-dependencies", + "transparent/test-dependencies", +] + +#! ### Experimental features + +## Exposes unstable APIs that are compatible with FROST key management +unstable-frost = ["orchard"] + +## Exposes unstable APIs. Their behaviour may change at any time. +unstable = ["dep:byteorder"] + +[badges] +maintenance = { status = "actively-developed" } + +[lints] +workspace = true diff --git a/zcash_keys/LICENSE-APACHE b/zcash_keys/LICENSE-APACHE new file mode 100644 index 0000000000..1e5006dc14 --- /dev/null +++ b/zcash_keys/LICENSE-APACHE @@ -0,0 +1,202 @@ + 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 [yyyy] [name of copyright owner] + +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/zcash_keys/LICENSE-MIT b/zcash_keys/LICENSE-MIT new file mode 100644 index 0000000000..1581c90d16 --- /dev/null +++ b/zcash_keys/LICENSE-MIT @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017-2019 Electric Coin Company + +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/zcash_keys/README.md b/zcash_keys/README.md new file mode 100644 index 0000000000..a8852a3ba3 --- /dev/null +++ b/zcash_keys/README.md @@ -0,0 +1,22 @@ +# zcash_keys + +This library contains Rust structs and traits for Zcash key and address parsing +and encoding. + +## License + +Licensed under either of + + * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or + http://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you, as defined in the Apache-2.0 +license, shall be dual licensed as above, without any additional terms or +conditions. + diff --git a/zcash_keys/src/address.rs b/zcash_keys/src/address.rs new file mode 100644 index 0000000000..3a196ecf99 --- /dev/null +++ b/zcash_keys/src/address.rs @@ -0,0 +1,571 @@ +//! Structs for handling supported address types. + +use alloc::string::{String, ToString}; +use alloc::vec::Vec; + +use transparent::address::TransparentAddress; +use zcash_address::{ + unified::{self, Container, Encoding, Typecode}, + ConversionError, ToAddress, TryFromRawAddress, ZcashAddress, +}; +use zcash_protocol::consensus::{self, NetworkType}; + +#[cfg(feature = "sapling")] +use sapling::PaymentAddress; +use zcash_protocol::{PoolType, ShieldedProtocol}; + +/// A Unified Address. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct UnifiedAddress { + #[cfg(feature = "orchard")] + orchard: Option, + #[cfg(feature = "sapling")] + sapling: Option, + transparent: Option, + unknown: Vec<(u32, Vec)>, +} + +impl TryFrom for UnifiedAddress { + type Error = &'static str; + + fn try_from(ua: unified::Address) -> Result { + #[cfg(feature = "orchard")] + let mut orchard = None; + #[cfg(feature = "sapling")] + let mut sapling = None; + let mut transparent = None; + + let mut unknown: Vec<(u32, Vec)> = vec![]; + + // We can use as-parsed order here for efficiency, because we're breaking out the + // receivers we support from the unknown receivers. + for item in ua.items_as_parsed() { + match item { + unified::Receiver::Orchard(data) => { + #[cfg(feature = "orchard")] + { + orchard = Some( + Option::from(orchard::Address::from_raw_address_bytes(data)) + .ok_or("Invalid Orchard receiver in Unified Address")?, + ); + } + #[cfg(not(feature = "orchard"))] + { + unknown.push((unified::Typecode::Orchard.into(), data.to_vec())); + } + } + + unified::Receiver::Sapling(data) => { + #[cfg(feature = "sapling")] + { + sapling = Some( + PaymentAddress::from_bytes(data) + .ok_or("Invalid Sapling receiver in Unified Address")?, + ); + } + #[cfg(not(feature = "sapling"))] + { + unknown.push((unified::Typecode::Sapling.into(), data.to_vec())); + } + } + + unified::Receiver::P2pkh(data) => { + transparent = Some(TransparentAddress::PublicKeyHash(*data)); + } + + unified::Receiver::P2sh(data) => { + transparent = Some(TransparentAddress::ScriptHash(*data)); + } + + unified::Receiver::Unknown { typecode, data } => { + unknown.push((*typecode, data.clone())); + } + } + } + + Ok(Self { + #[cfg(feature = "orchard")] + orchard, + #[cfg(feature = "sapling")] + sapling, + transparent, + unknown, + }) + } +} + +impl UnifiedAddress { + /// Constructs a Unified Address from a given set of receivers. + /// + /// Returns `None` if the receivers would produce an invalid Unified Address (namely, + /// if no shielded receiver is provided). + pub fn from_receivers( + #[cfg(feature = "orchard")] orchard: Option, + #[cfg(feature = "sapling")] sapling: Option, + transparent: Option, + // TODO: Add handling for address metadata items. + ) -> Option { + #[cfg(feature = "orchard")] + let has_orchard = orchard.is_some(); + #[cfg(not(feature = "orchard"))] + let has_orchard = false; + + #[cfg(feature = "sapling")] + let has_sapling = sapling.is_some(); + #[cfg(not(feature = "sapling"))] + let has_sapling = false; + + if has_orchard || has_sapling { + Some(Self { + #[cfg(feature = "orchard")] + orchard, + #[cfg(feature = "sapling")] + sapling, + transparent, + unknown: vec![], + }) + } else { + // UAs require at least one shielded receiver. + None + } + } + + /// Returns whether this address has an Orchard receiver. + /// + /// This method is available irrespective of whether the `orchard` feature flag is enabled. + pub fn has_orchard(&self) -> bool { + #[cfg(not(feature = "orchard"))] + return false; + #[cfg(feature = "orchard")] + return self.orchard.is_some(); + } + + /// Returns the Orchard receiver within this Unified Address, if any. + #[cfg(feature = "orchard")] + pub fn orchard(&self) -> Option<&orchard::Address> { + self.orchard.as_ref() + } + + /// Returns whether this address has a Sapling receiver. + pub fn has_sapling(&self) -> bool { + #[cfg(not(feature = "sapling"))] + return false; + + #[cfg(feature = "sapling")] + return self.sapling.is_some(); + } + + /// Returns the Sapling receiver within this Unified Address, if any. + #[cfg(feature = "sapling")] + pub fn sapling(&self) -> Option<&PaymentAddress> { + self.sapling.as_ref() + } + + /// Returns whether this address has a Transparent receiver. + pub fn has_transparent(&self) -> bool { + self.transparent.is_some() + } + + /// Returns the transparent receiver within this Unified Address, if any. + pub fn transparent(&self) -> Option<&TransparentAddress> { + self.transparent.as_ref() + } + + /// Returns the set of unknown receivers of the unified address. + pub fn unknown(&self) -> &[(u32, Vec)] { + &self.unknown + } + + fn to_address(&self, net: NetworkType) -> ZcashAddress { + let items = self + .unknown + .iter() + .map(|(typecode, data)| unified::Receiver::Unknown { + typecode: *typecode, + data: data.clone(), + }); + + #[cfg(feature = "orchard")] + let items = items.chain( + self.orchard + .as_ref() + .map(|addr| addr.to_raw_address_bytes()) + .map(unified::Receiver::Orchard), + ); + + #[cfg(feature = "sapling")] + let items = items.chain( + self.sapling + .as_ref() + .map(|pa| pa.to_bytes()) + .map(unified::Receiver::Sapling), + ); + + let items = items.chain(self.transparent.as_ref().map(|taddr| match taddr { + TransparentAddress::PublicKeyHash(data) => unified::Receiver::P2pkh(*data), + TransparentAddress::ScriptHash(data) => unified::Receiver::P2sh(*data), + })); + + let ua = unified::Address::try_from_items(items.collect()) + .expect("UnifiedAddress should only be constructed safely"); + ZcashAddress::from_unified(net, ua) + } + + /// Returns the string encoding of this `UnifiedAddress` for the given network. + pub fn encode(&self, params: &P) -> String { + self.to_address(params.network_type()).to_string() + } + + /// Returns the set of receiver typecodes. + pub fn receiver_types(&self) -> Vec { + let result = core::iter::empty(); + #[cfg(feature = "orchard")] + let result = result.chain(self.orchard.map(|_| Typecode::Orchard)); + #[cfg(feature = "sapling")] + let result = result.chain(self.sapling.map(|_| Typecode::Sapling)); + let result = result.chain(self.transparent.map(|taddr| match taddr { + TransparentAddress::PublicKeyHash(_) => Typecode::P2pkh, + TransparentAddress::ScriptHash(_) => Typecode::P2sh, + })); + let result = result.chain( + self.unknown() + .iter() + .map(|(typecode, _)| Typecode::Unknown(*typecode)), + ); + result.collect() + } +} + +/// An enumeration of protocol-level receiver types. +/// +/// While these correspond to unified address receiver types, this is a distinct type because it is +/// used to represent the protocol-level recipient of a transfer, instead of a part of an encoded +/// address. +pub enum Receiver { + #[cfg(feature = "orchard")] + Orchard(orchard::Address), + #[cfg(feature = "sapling")] + Sapling(PaymentAddress), + Transparent(TransparentAddress), +} + +impl Receiver { + /// Converts this receiver to a [`ZcashAddress`] for the given network. + /// + /// This conversion function selects the least-capable address format possible; this means that + /// Orchard receivers will be rendered as Unified addresses, Sapling receivers will be rendered + /// as bare Sapling addresses, and Transparent receivers will be rendered as taddrs. + pub fn to_zcash_address(&self, net: NetworkType) -> ZcashAddress { + match self { + #[cfg(feature = "orchard")] + Receiver::Orchard(addr) => { + let receiver = unified::Receiver::Orchard(addr.to_raw_address_bytes()); + let ua = unified::Address::try_from_items(vec![receiver]) + .expect("A unified address may contain a single Orchard receiver."); + ZcashAddress::from_unified(net, ua) + } + #[cfg(feature = "sapling")] + Receiver::Sapling(addr) => ZcashAddress::from_sapling(net, addr.to_bytes()), + Receiver::Transparent(TransparentAddress::PublicKeyHash(data)) => { + ZcashAddress::from_transparent_p2pkh(net, *data) + } + Receiver::Transparent(TransparentAddress::ScriptHash(data)) => { + ZcashAddress::from_transparent_p2sh(net, *data) + } + } + } + + /// Returns whether or not this receiver corresponds to `addr`, or is contained + /// in `addr` when the latter is a Unified Address. + pub fn corresponds(&self, addr: &ZcashAddress) -> bool { + addr.matches_receiver(&match self { + #[cfg(feature = "orchard")] + Receiver::Orchard(addr) => unified::Receiver::Orchard(addr.to_raw_address_bytes()), + #[cfg(feature = "sapling")] + Receiver::Sapling(addr) => unified::Receiver::Sapling(addr.to_bytes()), + Receiver::Transparent(TransparentAddress::PublicKeyHash(data)) => { + unified::Receiver::P2pkh(*data) + } + Receiver::Transparent(TransparentAddress::ScriptHash(data)) => { + unified::Receiver::P2sh(*data) + } + }) + } +} + +/// An address that funds can be sent to. +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum Address { + /// A Sapling payment address. + #[cfg(feature = "sapling")] + Sapling(PaymentAddress), + + /// A transparent address corresponding to either a public key hash or a script hash. + Transparent(TransparentAddress), + + /// A [ZIP 316] Unified Address. + /// + /// [ZIP 316]: https://zips.z.cash/zip-0316 + Unified(UnifiedAddress), + + /// A [ZIP 320] transparent-source-only P2PKH address, or "TEX address". + /// + /// [ZIP 320]: https://zips.z.cash/zip-0320 + Tex([u8; 20]), +} + +#[cfg(feature = "sapling")] +impl From for Address { + fn from(addr: PaymentAddress) -> Self { + Address::Sapling(addr) + } +} + +impl From for Address { + fn from(addr: TransparentAddress) -> Self { + Address::Transparent(addr) + } +} + +impl From for Address { + fn from(addr: UnifiedAddress) -> Self { + Address::Unified(addr) + } +} + +impl TryFromRawAddress for Address { + type Error = &'static str; + + #[cfg(feature = "sapling")] + fn try_from_raw_sapling(data: [u8; 43]) -> Result> { + let pa = PaymentAddress::from_bytes(&data).ok_or("Invalid Sapling payment address")?; + Ok(pa.into()) + } + + fn try_from_raw_unified( + ua: zcash_address::unified::Address, + ) -> Result> { + UnifiedAddress::try_from(ua) + .map_err(ConversionError::User) + .map(Address::from) + } + + fn try_from_raw_transparent_p2pkh( + data: [u8; 20], + ) -> Result> { + Ok(TransparentAddress::PublicKeyHash(data).into()) + } + + fn try_from_raw_transparent_p2sh(data: [u8; 20]) -> Result> { + Ok(TransparentAddress::ScriptHash(data).into()) + } + + fn try_from_raw_tex(data: [u8; 20]) -> Result> { + Ok(Address::Tex(data)) + } +} + +impl Address { + /// Attempts to decode an [`Address`] value from its [`ZcashAddress`] encoded representation. + /// + /// Returns `None` if any error is encountered in decoding. Use + /// [`Self::try_from_zcash_address(s.parse()?)?`] if you need detailed error information. + pub fn decode(params: &P, s: &str) -> Option { + Self::try_from_zcash_address(params, s.parse::().ok()?).ok() + } + + /// Attempts to decode an [`Address`] value from its [`ZcashAddress`] encoded representation. + pub fn try_from_zcash_address( + params: &P, + zaddr: ZcashAddress, + ) -> Result> { + zaddr.convert_if_network(params.network_type()) + } + + /// Converts this [`Address`] to its encoded [`ZcashAddress`] representation. + pub fn to_zcash_address(&self, params: &P) -> ZcashAddress { + let net = params.network_type(); + + match self { + #[cfg(feature = "sapling")] + Address::Sapling(pa) => ZcashAddress::from_sapling(net, pa.to_bytes()), + Address::Transparent(addr) => match addr { + TransparentAddress::PublicKeyHash(data) => { + ZcashAddress::from_transparent_p2pkh(net, *data) + } + TransparentAddress::ScriptHash(data) => { + ZcashAddress::from_transparent_p2sh(net, *data) + } + }, + Address::Unified(ua) => ua.to_address(net), + Address::Tex(data) => ZcashAddress::from_tex(net, *data), + } + } + + /// Converts this [`Address`] to its encoded string representation. + pub fn encode(&self, params: &P) -> String { + self.to_zcash_address(params).to_string() + } + + /// Returns whether or not this [`Address`] can receive funds in the specified pool. + pub fn can_receive_as(&self, pool_type: PoolType) -> bool { + match self { + #[cfg(feature = "sapling")] + Address::Sapling(_) => { + matches!(pool_type, PoolType::Shielded(ShieldedProtocol::Sapling)) + } + Address::Transparent(_) | Address::Tex(_) => { + matches!(pool_type, PoolType::Transparent) + } + Address::Unified(ua) => match pool_type { + PoolType::Transparent => ua.has_transparent(), + PoolType::Shielded(ShieldedProtocol::Sapling) => ua.has_sapling(), + PoolType::Shielded(ShieldedProtocol::Orchard) => ua.has_orchard(), + }, + } + } + + /// Returns the transparent address corresponding to this address, if it is a transparent + /// address, a Unified address with a transparent receiver, or ZIP 320 (TEX) address. + pub fn to_transparent_address(&self) -> Option { + match self { + #[cfg(feature = "sapling")] + Address::Sapling(_) => None, + Address::Transparent(addr) => Some(*addr), + Address::Unified(ua) => ua.transparent().copied(), + Address::Tex(addr_bytes) => Some(TransparentAddress::PublicKeyHash(*addr_bytes)), + } + } +} + +#[cfg(all( + any( + feature = "orchard", + feature = "sapling", + feature = "transparent-inputs" + ), + any(test, feature = "test-dependencies") +))] +pub mod testing { + use proptest::prelude::*; + use zcash_protocol::consensus::Network; + + use crate::keys::{testing::arb_unified_spending_key, UnifiedAddressRequest}; + + use super::{Address, UnifiedAddress}; + + #[cfg(feature = "sapling")] + use sapling::testing::arb_payment_address; + use transparent::address::testing::arb_transparent_addr; + + pub fn arb_unified_addr( + params: Network, + request: UnifiedAddressRequest, + ) -> impl Strategy { + arb_unified_spending_key(params).prop_map(move |k| k.default_address(Some(request)).0) + } + + #[cfg(feature = "sapling")] + pub fn arb_addr(request: UnifiedAddressRequest) -> impl Strategy { + prop_oneof![ + arb_payment_address().prop_map(Address::Sapling), + arb_transparent_addr().prop_map(Address::Transparent), + arb_unified_addr(Network::TestNetwork, request).prop_map(Address::Unified), + proptest::array::uniform20(any::()).prop_map(Address::Tex), + ] + } + + #[cfg(not(feature = "sapling"))] + pub fn arb_addr(request: UnifiedAddressRequest) -> impl Strategy { + return prop_oneof![ + arb_transparent_addr().prop_map(Address::Transparent), + arb_unified_addr(Network::TestNetwork, request).prop_map(Address::Unified), + proptest::array::uniform20(any::()).prop_map(Address::Tex), + ]; + } +} + +#[cfg(test)] +mod tests { + use zcash_address::test_vectors; + use zcash_protocol::consensus::MAIN_NETWORK; + + use super::{Address, UnifiedAddress}; + + #[cfg(feature = "sapling")] + use crate::keys::sapling; + + #[cfg(any(feature = "orchard", feature = "sapling"))] + use zip32::AccountId; + + #[test] + #[cfg(any(feature = "orchard", feature = "sapling"))] + fn ua_round_trip() { + #[cfg(feature = "orchard")] + let orchard = { + let sk = + orchard::keys::SpendingKey::from_zip32_seed(&[0; 32], 0, AccountId::ZERO).unwrap(); + let fvk = orchard::keys::FullViewingKey::from(&sk); + Some(fvk.address_at(0u32, orchard::keys::Scope::External)) + }; + + #[cfg(feature = "sapling")] + let sapling = { + let extsk = sapling::spending_key(&[0; 32], 0, AccountId::ZERO); + let dfvk = extsk.to_diversifiable_full_viewing_key(); + Some(dfvk.default_address().1) + }; + + let transparent = None; + + #[cfg(all(feature = "orchard", feature = "sapling"))] + let ua = UnifiedAddress::from_receivers(orchard, sapling, transparent).unwrap(); + + #[cfg(all(not(feature = "orchard"), feature = "sapling"))] + let ua = UnifiedAddress::from_receivers(sapling, transparent).unwrap(); + + #[cfg(all(feature = "orchard", not(feature = "sapling")))] + let ua = UnifiedAddress::from_receivers(orchard, transparent).unwrap(); + + let addr = Address::Unified(ua); + let addr_str = addr.encode(&MAIN_NETWORK); + assert_eq!(Address::decode(&MAIN_NETWORK, &addr_str), Some(addr)); + } + + #[test] + #[cfg(not(any(feature = "orchard", feature = "sapling")))] + fn ua_round_trip() { + let transparent = None; + assert_eq!(UnifiedAddress::from_receivers(transparent), None) + } + + #[test] + fn ua_parsing() { + for tv in test_vectors::UNIFIED { + match Address::decode(&MAIN_NETWORK, tv.unified_addr) { + Some(Address::Unified(ua)) => { + assert_eq!( + ua.has_transparent(), + tv.p2pkh_bytes.is_some() || tv.p2sh_bytes.is_some() + ); + #[cfg(feature = "sapling")] + assert_eq!(ua.has_sapling(), tv.sapling_raw_addr.is_some()); + #[cfg(feature = "orchard")] + assert_eq!(ua.has_orchard(), tv.orchard_raw_addr.is_some()); + } + Some(_) => { + panic!( + "{} did not decode to a unified address value.", + tv.unified_addr + ); + } + None => { + panic!( + "Failed to decode unified address from test vector: {}", + tv.unified_addr + ); + } + } + } + } +} diff --git a/zcash_keys/src/encoding.rs b/zcash_keys/src/encoding.rs new file mode 100644 index 0000000000..7be07a3d4b --- /dev/null +++ b/zcash_keys/src/encoding.rs @@ -0,0 +1,672 @@ +//! Encoding and decoding functions for Zcash key and address structs. +//! +//! Human-Readable Prefixes (HRPs) for Bech32 encodings are located in the +//! [zcash_protocol::constants] module. + +use crate::address::UnifiedAddress; +use alloc::borrow::ToOwned; +use alloc::string::{String, ToString}; +use bs58::{self, decode::Error as Bs58Error}; +use core::fmt; + +use transparent::address::TransparentAddress; +use zcash_address::unified::{self, Encoding}; +use zcash_protocol::consensus::{self, NetworkConstants}; + +#[cfg(feature = "sapling")] +use { + alloc::vec::Vec, + bech32::{ + primitives::decode::{CheckedHrpstring, CheckedHrpstringError}, + Bech32, Hrp, + }, + core2::io::{self, Write}, + sapling::zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, + zcash_protocol::consensus::NetworkType, +}; + +#[cfg(feature = "sapling")] +fn bech32_encode(hrp: &str, write: F) -> String +where + F: Fn(&mut dyn Write) -> io::Result<()>, +{ + let mut data: Vec = vec![]; + write(&mut data).expect("Should be able to write to a Vec"); + bech32::encode::(Hrp::parse_unchecked(hrp), &data).expect("encoding is short enough") +} + +#[derive(Clone, Debug, PartialEq, Eq)] +#[cfg(feature = "sapling")] +pub enum Bech32DecodeError { + Bech32Error(bech32::DecodeError), + Hrp(CheckedHrpstringError), + ReadError, + HrpMismatch { expected: String, actual: String }, +} + +#[cfg(feature = "sapling")] +impl From for Bech32DecodeError { + fn from(err: bech32::DecodeError) -> Self { + Bech32DecodeError::Bech32Error(err) + } +} + +#[cfg(feature = "sapling")] +impl From for Bech32DecodeError { + fn from(err: CheckedHrpstringError) -> Self { + Bech32DecodeError::Hrp(err) + } +} + +#[cfg(feature = "sapling")] +impl fmt::Display for Bech32DecodeError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self { + Bech32DecodeError::Bech32Error(e) => write!(f, "{}", e), + Bech32DecodeError::Hrp(e) => write!(f, "Incorrect HRP encoding: {e}"), + Bech32DecodeError::ReadError => { + write!(f, "Failed to decode key from its binary representation.") + } + Bech32DecodeError::HrpMismatch { expected, actual } => write!( + f, + "Key was encoded for a different network: expected {}, got {}.", + expected, actual + ), + } + } +} + +#[cfg(all(feature = "sapling", feature = "std"))] +impl std::error::Error for Bech32DecodeError {} + +#[cfg(feature = "sapling")] +fn bech32_decode(hrp: &str, s: &str, read: F) -> Result +where + F: Fn(Vec) -> Option, +{ + let parsed = CheckedHrpstring::new::(s)?; + if parsed.hrp().as_str() != hrp { + Err(Bech32DecodeError::HrpMismatch { + expected: hrp.to_string(), + actual: parsed.hrp().as_str().to_owned(), + }) + } else { + read(parsed.byte_iter().collect::>()).ok_or(Bech32DecodeError::ReadError) + } +} + +/// A trait for encoding and decoding Zcash addresses. +pub trait AddressCodec

+where + Self: core::marker::Sized, +{ + type Error; + + /// Encode a Zcash address. + /// + /// # Arguments + /// * `params` - The network the address is to be used on. + fn encode(&self, params: &P) -> String; + + /// Decodes a Zcash address from its string representation. + /// + /// # Arguments + /// * `params` - The network the address is to be used on. + /// * `address` - The string representation of the address. + fn decode(params: &P, address: &str) -> Result; +} + +#[derive(Debug)] +pub enum TransparentCodecError { + UnsupportedAddressType(String), + Base58(Bs58Error), +} + +impl fmt::Display for TransparentCodecError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self { + TransparentCodecError::UnsupportedAddressType(s) => write!( + f, + "Could not recognize {} as a supported p2sh or p2pkh address.", + s + ), + TransparentCodecError::Base58(e) => write!(f, "{}", e), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for TransparentCodecError {} + +impl AddressCodec

for TransparentAddress { + type Error = TransparentCodecError; + + fn encode(&self, params: &P) -> String { + encode_transparent_address( + ¶ms.b58_pubkey_address_prefix(), + ¶ms.b58_script_address_prefix(), + self, + ) + } + + fn decode(params: &P, address: &str) -> Result { + decode_transparent_address( + ¶ms.b58_pubkey_address_prefix(), + ¶ms.b58_script_address_prefix(), + address, + ) + .map_err(TransparentCodecError::Base58) + .and_then(|opt| { + opt.ok_or_else(|| TransparentCodecError::UnsupportedAddressType(address.to_string())) + }) + } +} + +#[cfg(feature = "sapling")] +impl AddressCodec

for sapling::PaymentAddress { + type Error = Bech32DecodeError; + + fn encode(&self, params: &P) -> String { + encode_payment_address(params.hrp_sapling_payment_address(), self) + } + + fn decode(params: &P, address: &str) -> Result { + decode_payment_address(params.hrp_sapling_payment_address(), address) + } +} + +impl AddressCodec

for UnifiedAddress { + type Error = String; + + fn encode(&self, params: &P) -> String { + self.encode(params) + } + + fn decode(params: &P, address: &str) -> Result { + unified::Address::decode(address) + .map_err(|e| format!("{}", e)) + .and_then(|(network, addr)| { + if params.network_type() == network { + UnifiedAddress::try_from(addr).map_err(|e| e.to_owned()) + } else { + Err(format!( + "Address {} is for a different network: {:?}", + address, network + )) + } + }) + } +} + +/// Writes an [`ExtendedSpendingKey`] as a Bech32-encoded string. +/// +/// # Examples +/// +/// ``` +/// use zcash_protocol::constants::testnet::{COIN_TYPE, HRP_SAPLING_EXTENDED_SPENDING_KEY}; +/// use zip32::AccountId; +/// +/// use zcash_keys::{ +/// encoding::encode_extended_spending_key, +/// keys::sapling, +/// }; +/// +/// let extsk = sapling::spending_key(&[0; 32][..], COIN_TYPE, AccountId::ZERO); +/// let encoded = encode_extended_spending_key(HRP_SAPLING_EXTENDED_SPENDING_KEY, &extsk); +/// ``` +/// [`ExtendedSpendingKey`]: sapling::zip32::ExtendedSpendingKey +#[cfg(feature = "sapling")] +pub fn encode_extended_spending_key(hrp: &str, extsk: &ExtendedSpendingKey) -> String { + bech32_encode(hrp, |w| extsk.write(w)) +} + +/// Decodes an [`ExtendedSpendingKey`] from a Bech32-encoded string. +/// +/// [`ExtendedSpendingKey`]: sapling::zip32::ExtendedSpendingKey +#[cfg(feature = "sapling")] +pub fn decode_extended_spending_key( + hrp: &str, + s: &str, +) -> Result { + bech32_decode(hrp, s, |data| ExtendedSpendingKey::read(&data[..]).ok()) +} + +/// Writes an [`ExtendedFullViewingKey`] as a Bech32-encoded string. +/// +/// # Examples +/// +/// ``` +/// use ::sapling::zip32::ExtendedFullViewingKey; +/// use zcash_protocol::constants::testnet::{COIN_TYPE, HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY}; +/// use zip32::AccountId; +/// use zcash_keys::{ +/// encoding::encode_extended_full_viewing_key, +/// keys::sapling, +/// }; +/// +/// let extsk = sapling::spending_key(&[0; 32][..], COIN_TYPE, AccountId::ZERO); +/// let extfvk = extsk.to_extended_full_viewing_key(); +/// let encoded = encode_extended_full_viewing_key(HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, &extfvk); +/// ``` +/// [`ExtendedFullViewingKey`]: sapling::zip32::ExtendedFullViewingKey +#[cfg(feature = "sapling")] +pub fn encode_extended_full_viewing_key(hrp: &str, extfvk: &ExtendedFullViewingKey) -> String { + bech32_encode(hrp, |w| extfvk.write(w)) +} + +/// Decodes an [`ExtendedFullViewingKey`] from a Bech32-encoded string, verifying that it matches +/// the provided human-readable prefix. +#[cfg(feature = "sapling")] +pub fn decode_extended_full_viewing_key( + hrp: &str, + s: &str, +) -> Result { + bech32_decode(hrp, s, |data| ExtendedFullViewingKey::read(&data[..]).ok()) +} + +/// Decodes an [`ExtendedFullViewingKey`] and the [`NetworkType`] that it is intended for use with +/// from a Bech32-encoded string. +#[cfg(feature = "sapling")] +pub fn decode_extfvk_with_network( + s: &str, +) -> Result<(NetworkType, ExtendedFullViewingKey), Bech32DecodeError> { + use zcash_protocol::constants::{mainnet, regtest, testnet}; + + let parsed = CheckedHrpstring::new::(s)?; + let network = match parsed.hrp().as_str() { + mainnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY => Ok(NetworkType::Main), + testnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY => Ok(NetworkType::Test), + regtest::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY => Ok(NetworkType::Regtest), + other => Err(Bech32DecodeError::HrpMismatch { + expected: format!( + "One of {}, {}, or {}", + mainnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, + testnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, + regtest::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, + ), + actual: other.to_string(), + }), + }?; + let fvk = ExtendedFullViewingKey::read(&parsed.byte_iter().collect::>()[..]) + .map_err(|_| Bech32DecodeError::ReadError)?; + + Ok((network, fvk)) +} + +/// Writes a [`PaymentAddress`] as a Bech32-encoded string. +/// +/// # Examples +/// +/// ``` +/// use group::Group; +/// use sapling::{Diversifier, PaymentAddress}; +/// use zcash_keys::{ +/// encoding::encode_payment_address, +/// }; +/// use zcash_protocol::constants::testnet::HRP_SAPLING_PAYMENT_ADDRESS; +/// +/// let pa = PaymentAddress::from_bytes(&[ +/// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x8e, 0x11, +/// 0x9d, 0x72, 0x99, 0x2b, 0x56, 0x0d, 0x26, 0x50, 0xff, 0xe0, 0xbe, 0x7f, 0x35, 0x42, +/// 0xfd, 0x97, 0x00, 0x3c, 0xb7, 0xcc, 0x3a, 0xbf, 0xf8, 0x1a, 0x7f, 0x90, 0x37, 0xf3, +/// 0xea, +/// ]) +/// .unwrap(); +/// +/// assert_eq!( +/// encode_payment_address(HRP_SAPLING_PAYMENT_ADDRESS, &pa), +/// "ztestsapling1qqqqqqqqqqqqqqqqqqcguyvaw2vjk4sdyeg0lc970u659lvhqq7t0np6hlup5lusxle75ss7jnk", +/// ); +/// ``` +/// [`PaymentAddress`]: sapling::PaymentAddress +#[cfg(feature = "sapling")] +pub fn encode_payment_address(hrp: &str, addr: &sapling::PaymentAddress) -> String { + bech32_encode(hrp, |w| w.write_all(&addr.to_bytes())) +} + +/// Writes a [`PaymentAddress`] as a Bech32-encoded string +/// using the human-readable prefix values defined in the specified +/// network parameters. +/// +/// [`PaymentAddress`]: sapling::PaymentAddress +#[cfg(feature = "sapling")] +pub fn encode_payment_address_p( + params: &P, + addr: &sapling::PaymentAddress, +) -> String { + encode_payment_address(params.hrp_sapling_payment_address(), addr) +} + +/// Decodes a [`PaymentAddress`] from a Bech32-encoded string. +/// +/// # Examples +/// +/// ``` +/// use group::Group; +/// use sapling::{Diversifier, PaymentAddress}; +/// use zcash_keys::{ +/// encoding::decode_payment_address, +/// }; +/// use zcash_protocol::consensus::{TEST_NETWORK, NetworkConstants, Parameters}; +/// +/// let pa = PaymentAddress::from_bytes(&[ +/// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x8e, 0x11, +/// 0x9d, 0x72, 0x99, 0x2b, 0x56, 0x0d, 0x26, 0x50, 0xff, 0xe0, 0xbe, 0x7f, 0x35, 0x42, +/// 0xfd, 0x97, 0x00, 0x3c, 0xb7, 0xcc, 0x3a, 0xbf, 0xf8, 0x1a, 0x7f, 0x90, 0x37, 0xf3, +/// 0xea, +/// ]) +/// .unwrap(); +/// +/// assert_eq!( +/// decode_payment_address( +/// TEST_NETWORK.hrp_sapling_payment_address(), +/// "ztestsapling1qqqqqqqqqqqqqqqqqqcguyvaw2vjk4sdyeg0lc970u659lvhqq7t0np6hlup5lusxle75ss7jnk", +/// ), +/// Ok(pa), +/// ); +/// ``` +/// [`PaymentAddress`]: sapling::PaymentAddress +#[cfg(feature = "sapling")] +pub fn decode_payment_address( + hrp: &str, + s: &str, +) -> Result { + bech32_decode(hrp, s, |data| { + if data.len() != 43 { + return None; + } + + let mut bytes = [0; 43]; + bytes.copy_from_slice(&data); + sapling::PaymentAddress::from_bytes(&bytes) + }) +} + +/// Writes a [`TransparentAddress`] as a Base58Check-encoded string. +/// +/// # Examples +/// +/// ``` +/// use zcash_keys::encoding::encode_transparent_address; +/// use zcash_protocol::consensus::{TEST_NETWORK, NetworkConstants, Parameters}; +/// use transparent::address::TransparentAddress; +/// +/// assert_eq!( +/// encode_transparent_address( +/// &TEST_NETWORK.b58_pubkey_address_prefix(), +/// &TEST_NETWORK.b58_script_address_prefix(), +/// &TransparentAddress::PublicKeyHash([0; 20]), +/// ), +/// "tm9iMLAuYMzJ6jtFLcA7rzUmfreGuKvr7Ma", +/// ); +/// +/// assert_eq!( +/// encode_transparent_address( +/// &TEST_NETWORK.b58_pubkey_address_prefix(), +/// &TEST_NETWORK.b58_script_address_prefix(), +/// &TransparentAddress::ScriptHash([0; 20]), +/// ), +/// "t26YoyZ1iPgiMEWL4zGUm74eVWfhyDMXzY2", +/// ); +pub fn encode_transparent_address( + pubkey_version: &[u8], + script_version: &[u8], + addr: &TransparentAddress, +) -> String { + let decoded = match addr { + TransparentAddress::PublicKeyHash(key_id) => { + let mut decoded = vec![0; pubkey_version.len() + 20]; + decoded[..pubkey_version.len()].copy_from_slice(pubkey_version); + decoded[pubkey_version.len()..].copy_from_slice(key_id); + decoded + } + TransparentAddress::ScriptHash(script_id) => { + let mut decoded = vec![0; script_version.len() + 20]; + decoded[..script_version.len()].copy_from_slice(script_version); + decoded[script_version.len()..].copy_from_slice(script_id); + decoded + } + }; + bs58::encode(decoded).with_check().into_string() +} + +/// Writes a [`TransparentAddress`] as a Base58Check-encoded string. +/// using the human-readable prefix values defined in the specified +/// network parameters. +pub fn encode_transparent_address_p( + params: &P, + addr: &TransparentAddress, +) -> String { + encode_transparent_address( + ¶ms.b58_pubkey_address_prefix(), + ¶ms.b58_script_address_prefix(), + addr, + ) +} + +/// Decodes a [`TransparentAddress`] from a Base58Check-encoded string. +/// +/// # Examples +/// +/// ``` +/// use zcash_protocol::consensus::{TEST_NETWORK, NetworkConstants, Parameters}; +/// use transparent::address::TransparentAddress; +/// use zcash_keys::{ +/// encoding::decode_transparent_address, +/// }; +/// +/// assert_eq!( +/// decode_transparent_address( +/// &TEST_NETWORK.b58_pubkey_address_prefix(), +/// &TEST_NETWORK.b58_script_address_prefix(), +/// "tm9iMLAuYMzJ6jtFLcA7rzUmfreGuKvr7Ma", +/// ), +/// Ok(Some(TransparentAddress::PublicKeyHash([0; 20]))), +/// ); +/// +/// assert_eq!( +/// decode_transparent_address( +/// &TEST_NETWORK.b58_pubkey_address_prefix(), +/// &TEST_NETWORK.b58_script_address_prefix(), +/// "t26YoyZ1iPgiMEWL4zGUm74eVWfhyDMXzY2", +/// ), +/// Ok(Some(TransparentAddress::ScriptHash([0; 20]))), +/// ); +pub fn decode_transparent_address( + pubkey_version: &[u8], + script_version: &[u8], + s: &str, +) -> Result, Bs58Error> { + bs58::decode(s).with_check(None).into_vec().map(|decoded| { + if decoded.starts_with(pubkey_version) { + decoded[pubkey_version.len()..] + .try_into() + .ok() + .map(TransparentAddress::PublicKeyHash) + } else if decoded.starts_with(script_version) { + decoded[script_version.len()..] + .try_into() + .ok() + .map(TransparentAddress::ScriptHash) + } else { + None + } + }) +} + +#[cfg(test)] +#[cfg(feature = "sapling")] +mod tests_sapling { + use super::{ + decode_extended_full_viewing_key, decode_extended_spending_key, decode_payment_address, + encode_extended_full_viewing_key, encode_extended_spending_key, encode_payment_address, + Bech32DecodeError, + }; + use sapling::{zip32::ExtendedSpendingKey, PaymentAddress}; + use zcash_protocol::constants; + + #[test] + fn extended_spending_key() { + let extsk = ExtendedSpendingKey::master(&[0; 32][..]); + + let encoded_main = "secret-extended-key-main1qqqqqqqqqqqqqq8n3zjjmvhhr854uy3qhpda3ml34haf0x388z5r7h4st4kpsf6qysqws3xh6qmha7gna72fs2n4clnc9zgyd22s658f65pex4exe56qjk5pqj9vfdq7dfdhjc2rs9jdwq0zl99uwycyrxzp86705rk687spn44e2uhm7h0hsagfvkk4n7n6nfer6u57v9cac84t7nl2zth0xpyfeg0w2p2wv2yn6jn923aaz0vdaml07l60ahapk6efchyxwysrvjs87qvlj"; + let encoded_test = "secret-extended-key-test1qqqqqqqqqqqqqq8n3zjjmvhhr854uy3qhpda3ml34haf0x388z5r7h4st4kpsf6qysqws3xh6qmha7gna72fs2n4clnc9zgyd22s658f65pex4exe56qjk5pqj9vfdq7dfdhjc2rs9jdwq0zl99uwycyrxzp86705rk687spn44e2uhm7h0hsagfvkk4n7n6nfer6u57v9cac84t7nl2zth0xpyfeg0w2p2wv2yn6jn923aaz0vdaml07l60ahapk6efchyxwysrvjsvzyw8j"; + + assert_eq!( + encode_extended_spending_key( + constants::mainnet::HRP_SAPLING_EXTENDED_SPENDING_KEY, + &extsk + ), + encoded_main + ); + assert_eq!( + decode_extended_spending_key( + constants::mainnet::HRP_SAPLING_EXTENDED_SPENDING_KEY, + encoded_main + ) + .unwrap(), + extsk + ); + + assert_eq!( + encode_extended_spending_key( + constants::testnet::HRP_SAPLING_EXTENDED_SPENDING_KEY, + &extsk + ), + encoded_test + ); + assert_eq!( + decode_extended_spending_key( + constants::testnet::HRP_SAPLING_EXTENDED_SPENDING_KEY, + encoded_test + ) + .unwrap(), + extsk + ); + } + + #[test] + #[allow(deprecated)] + fn extended_full_viewing_key() { + let extfvk = ExtendedSpendingKey::master(&[0; 32][..]).to_extended_full_viewing_key(); + + let encoded_main = "zxviews1qqqqqqqqqqqqqq8n3zjjmvhhr854uy3qhpda3ml34haf0x388z5r7h4st4kpsf6qy3zw4wc246aw9rlfyg5ndlwvne7mwdq0qe6vxl42pqmcf8pvmmd5slmjxduqa9evgej6wa3th2505xq4nggrxdm93rxk4rpdjt5nmq2vn44e2uhm7h0hsagfvkk4n7n6nfer6u57v9cac84t7nl2zth0xpyfeg0w2p2wv2yn6jn923aaz0vdaml07l60ahapk6efchyxwysrvjsxmansf"; + let encoded_test = "zxviewtestsapling1qqqqqqqqqqqqqq8n3zjjmvhhr854uy3qhpda3ml34haf0x388z5r7h4st4kpsf6qy3zw4wc246aw9rlfyg5ndlwvne7mwdq0qe6vxl42pqmcf8pvmmd5slmjxduqa9evgej6wa3th2505xq4nggrxdm93rxk4rpdjt5nmq2vn44e2uhm7h0hsagfvkk4n7n6nfer6u57v9cac84t7nl2zth0xpyfeg0w2p2wv2yn6jn923aaz0vdaml07l60ahapk6efchyxwysrvjs8evfkz"; + let encoded_regtest = "zxviewregtestsapling1qqqqqqqqqqqqqq8n3zjjmvhhr854uy3qhpda3ml34haf0x388z5r7h4st4kpsf6qy3zw4wc246aw9rlfyg5ndlwvne7mwdq0qe6vxl42pqmcf8pvmmd5slmjxduqa9evgej6wa3th2505xq4nggrxdm93rxk4rpdjt5nmq2vn44e2uhm7h0hsagfvkk4n7n6nfer6u57v9cac84t7nl2zth0xpyfeg0w2p2wv2yn6jn923aaz0vdaml07l60ahapk6efchyxwysrvjskjkzax"; + assert_eq!( + encode_extended_full_viewing_key( + constants::mainnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, + &extfvk + ), + encoded_main + ); + assert_eq!( + decode_extended_full_viewing_key( + constants::mainnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, + encoded_main + ) + .unwrap(), + extfvk + ); + + assert_eq!( + encode_extended_full_viewing_key( + constants::testnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, + &extfvk + ), + encoded_test + ); + + assert_eq!( + encode_extended_full_viewing_key( + constants::regtest::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, + &extfvk + ), + encoded_regtest + ); + + assert_eq!( + decode_extended_full_viewing_key( + constants::regtest::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, + encoded_regtest + ) + .unwrap(), + extfvk + ); + } + + #[test] + fn payment_address() { + let addr = PaymentAddress::from_bytes(&[ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x8e, 0x11, + 0x9d, 0x72, 0x99, 0x2b, 0x56, 0x0d, 0x26, 0x50, 0xff, 0xe0, 0xbe, 0x7f, 0x35, 0x42, + 0xfd, 0x97, 0x00, 0x3c, 0xb7, 0xcc, 0x3a, 0xbf, 0xf8, 0x1a, 0x7f, 0x90, 0x37, 0xf3, + 0xea, + ]) + .unwrap(); + + let encoded_main = + "zs1qqqqqqqqqqqqqqqqqqcguyvaw2vjk4sdyeg0lc970u659lvhqq7t0np6hlup5lusxle75c8v35z"; + let encoded_test = + "ztestsapling1qqqqqqqqqqqqqqqqqqcguyvaw2vjk4sdyeg0lc970u659lvhqq7t0np6hlup5lusxle75ss7jnk"; + let encoded_regtest = + "zregtestsapling1qqqqqqqqqqqqqqqqqqcguyvaw2vjk4sdyeg0lc970u659lvhqq7t0np6hlup5lusxle7505hlz3"; + + assert_eq!( + encode_payment_address(constants::mainnet::HRP_SAPLING_PAYMENT_ADDRESS, &addr), + encoded_main + ); + + assert_eq!( + decode_payment_address( + constants::mainnet::HRP_SAPLING_PAYMENT_ADDRESS, + encoded_main + ) + .unwrap(), + addr + ); + + assert_eq!( + encode_payment_address(constants::testnet::HRP_SAPLING_PAYMENT_ADDRESS, &addr), + encoded_test + ); + + assert_eq!( + encode_payment_address(constants::regtest::HRP_SAPLING_PAYMENT_ADDRESS, &addr), + encoded_regtest + ); + + assert_eq!( + decode_payment_address( + constants::testnet::HRP_SAPLING_PAYMENT_ADDRESS, + encoded_test + ) + .unwrap(), + addr + ); + + assert_eq!( + decode_payment_address( + constants::regtest::HRP_SAPLING_PAYMENT_ADDRESS, + encoded_regtest + ) + .unwrap(), + addr + ); + } + + #[test] + fn invalid_diversifier() { + // Has a diversifier of `[1u8; 11]`. + let encoded_main = + "zs1qyqszqgpqyqszqgpqycguyvaw2vjk4sdyeg0lc970u659lvhqq7t0np6hlup5lusxle75ugum9p"; + + assert_eq!( + decode_payment_address( + constants::mainnet::HRP_SAPLING_PAYMENT_ADDRESS, + encoded_main, + ), + Err(Bech32DecodeError::ReadError) + ); + } +} diff --git a/zcash_keys/src/keys.rs b/zcash_keys/src/keys.rs new file mode 100644 index 0000000000..dc6a04daa0 --- /dev/null +++ b/zcash_keys/src/keys.rs @@ -0,0 +1,1845 @@ +//! Helper functions for managing light client key material. +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +use core::fmt::{self, Display}; + +use zcash_address::unified::{self, Container, Encoding, Typecode, Ufvk, Uivk}; +use zcash_protocol::consensus; +use zip32::{AccountId, DiversifierIndex}; + +use crate::address::UnifiedAddress; + +#[cfg(any(feature = "sapling", feature = "orchard"))] +use zcash_protocol::consensus::NetworkConstants; + +#[cfg(feature = "transparent-inputs")] +use { + core::convert::TryInto, + transparent::keys::{IncomingViewingKey, NonHardenedChildIndex}, +}; + +#[cfg(all( + feature = "transparent-inputs", + any(test, feature = "test-dependencies") +))] +use transparent::address::TransparentAddress; + +#[cfg(feature = "unstable")] +use { + byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}, + core::convert::TryFrom, + core2::io::{Read, Write}, + zcash_encoding::CompactSize, + zcash_protocol::consensus::BranchId, +}; + +#[cfg(feature = "orchard")] +use orchard::{self, keys::Scope}; + +#[cfg(all(feature = "sapling", feature = "unstable"))] +use ::sapling::zip32::ExtendedFullViewingKey; + +#[cfg(feature = "sapling")] +pub mod sapling { + pub use sapling::zip32::{ + DiversifiableFullViewingKey, ExtendedFullViewingKey, ExtendedSpendingKey, + }; + use zip32::{AccountId, ChildIndex}; + + /// Derives the ZIP 32 [`ExtendedSpendingKey`] for a given coin type and account from the + /// given seed. + /// + /// # Panics + /// + /// Panics if `seed` is shorter than 32 bytes. + /// + /// # Examples + /// + /// ``` + /// use zcash_protocol::constants::testnet::COIN_TYPE; + /// use zcash_keys::keys::sapling; + /// use zip32::AccountId; + /// + /// let extsk = sapling::spending_key(&[0; 32][..], COIN_TYPE, AccountId::ZERO); + /// ``` + /// [`ExtendedSpendingKey`]: sapling::zip32::ExtendedSpendingKey + pub fn spending_key(seed: &[u8], coin_type: u32, account: AccountId) -> ExtendedSpendingKey { + if seed.len() < 32 { + panic!("ZIP 32 seeds MUST be at least 32 bytes"); + } + + ExtendedSpendingKey::from_path( + &ExtendedSpendingKey::master(seed), + &[ + ChildIndex::hardened(32), + ChildIndex::hardened(coin_type), + account.into(), + ], + ) + } +} + +#[cfg(feature = "transparent-inputs")] +fn to_transparent_child_index(j: DiversifierIndex) -> Option { + let (low_4_bytes, rest) = j.as_bytes().split_at(4); + let transparent_j = u32::from_le_bytes(low_4_bytes.try_into().unwrap()); + if rest.iter().any(|b| b != &0) { + None + } else { + NonHardenedChildIndex::from_index(transparent_j) + } +} + +#[derive(Debug)] +pub enum DerivationError { + #[cfg(feature = "orchard")] + Orchard(orchard::zip32::Error), + #[cfg(feature = "transparent-inputs")] + Transparent(bip32::Error), +} + +impl Display for DerivationError { + fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + #[cfg(feature = "orchard")] + DerivationError::Orchard(e) => write!(_f, "Orchard error: {}", e), + #[cfg(feature = "transparent-inputs")] + DerivationError::Transparent(e) => write!(_f, "Transparent error: {}", e), + #[cfg(not(any(feature = "orchard", feature = "transparent-inputs")))] + other => { + unreachable!("Unhandled DerivationError variant {:?}", other) + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for DerivationError {} + +/// A version identifier for the encoding of unified spending keys. +/// +/// Each era corresponds to a range of block heights. During an era, the unified spending key +/// parsed from an encoded form tagged with that era's identifier is expected to provide +/// sufficient spending authority to spend any non-Sprout shielded note created in a transaction +/// within the era's block range. +#[cfg(feature = "unstable")] +#[derive(Debug, PartialEq, Eq)] +pub enum Era { + /// The Orchard era begins at Orchard activation, and will end if a new pool that requires a + /// change to unified spending keys is introduced. + Orchard, +} + +/// A type for errors that can occur when decoding keys from their serialized representations. +#[derive(Debug, PartialEq, Eq)] +pub enum DecodingError { + #[cfg(feature = "unstable")] + ReadError(&'static str), + #[cfg(feature = "unstable")] + EraInvalid, + #[cfg(feature = "unstable")] + EraMismatch(Era), + #[cfg(feature = "unstable")] + TypecodeInvalid, + #[cfg(feature = "unstable")] + LengthInvalid, + #[cfg(feature = "unstable")] + LengthMismatch(Typecode, u32), + #[cfg(feature = "unstable")] + InsufficientData(Typecode), + /// The key data could not be decoded from its string representation to a valid key. + KeyDataInvalid(Typecode), +} + +impl core::fmt::Display for DecodingError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + #[cfg(feature = "unstable")] + DecodingError::ReadError(s) => write!(f, "Read error: {}", s), + #[cfg(feature = "unstable")] + DecodingError::EraInvalid => write!(f, "Invalid era"), + #[cfg(feature = "unstable")] + DecodingError::EraMismatch(e) => write!(f, "Era mismatch: actual {:?}", e), + #[cfg(feature = "unstable")] + DecodingError::TypecodeInvalid => write!(f, "Invalid typecode"), + #[cfg(feature = "unstable")] + DecodingError::LengthInvalid => write!(f, "Invalid length"), + #[cfg(feature = "unstable")] + DecodingError::LengthMismatch(t, l) => { + write!( + f, + "Length mismatch: received {} bytes for typecode {:?}", + l, t + ) + } + #[cfg(feature = "unstable")] + DecodingError::InsufficientData(t) => { + write!(f, "Insufficient data for typecode {:?}", t) + } + DecodingError::KeyDataInvalid(t) => write!(f, "Invalid key data for key type {:?}", t), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for DecodingError {} + +#[cfg(feature = "unstable")] +impl Era { + /// Returns the unique identifier for the era. + fn id(&self) -> u32 { + // We use the consensus branch id of the network upgrade that introduced a + // new USK format as the identifier for the era. + match self { + Era::Orchard => u32::from(BranchId::Nu5), + } + } + + fn try_from_id(id: u32) -> Option { + BranchId::try_from(id).ok().and_then(|b| match b { + BranchId::Nu5 => Some(Era::Orchard), + _ => None, + }) + } +} + +/// A set of spending keys that are all associated with a single ZIP-0032 account identifier. +#[derive(Clone, Debug)] +pub struct UnifiedSpendingKey { + #[cfg(feature = "transparent-inputs")] + transparent: transparent::keys::AccountPrivKey, + #[cfg(feature = "sapling")] + sapling: sapling::ExtendedSpendingKey, + #[cfg(feature = "orchard")] + orchard: orchard::keys::SpendingKey, +} + +impl UnifiedSpendingKey { + pub fn from_seed( + _params: &P, + seed: &[u8], + _account: AccountId, + ) -> Result { + if seed.len() < 32 { + panic!("ZIP 32 seeds MUST be at least 32 bytes"); + } + + UnifiedSpendingKey::from_checked_parts( + #[cfg(feature = "transparent-inputs")] + transparent::keys::AccountPrivKey::from_seed(_params, seed, _account) + .map_err(DerivationError::Transparent)?, + #[cfg(feature = "sapling")] + sapling::spending_key(seed, _params.coin_type(), _account), + #[cfg(feature = "orchard")] + orchard::keys::SpendingKey::from_zip32_seed(seed, _params.coin_type(), _account) + .map_err(DerivationError::Orchard)?, + ) + } + + /// Construct a USK from its constituent parts, after verifying that UIVK derivation can + /// succeed. + fn from_checked_parts( + #[cfg(feature = "transparent-inputs")] transparent: transparent::keys::AccountPrivKey, + #[cfg(feature = "sapling")] sapling: sapling::ExtendedSpendingKey, + #[cfg(feature = "orchard")] orchard: orchard::keys::SpendingKey, + ) -> Result { + // Verify that FVK and IVK derivation succeed; we don't want to construct a USK + // that can't derive transparent addresses. + #[cfg(feature = "transparent-inputs")] + let _ = transparent.to_account_pubkey().derive_external_ivk()?; + + Ok(UnifiedSpendingKey { + #[cfg(feature = "transparent-inputs")] + transparent, + #[cfg(feature = "sapling")] + sapling, + #[cfg(feature = "orchard")] + orchard, + }) + } + + pub fn to_unified_full_viewing_key(&self) -> UnifiedFullViewingKey { + UnifiedFullViewingKey { + #[cfg(feature = "transparent-inputs")] + transparent: Some(self.transparent.to_account_pubkey()), + #[cfg(feature = "sapling")] + sapling: Some(self.sapling.to_diversifiable_full_viewing_key()), + #[cfg(feature = "orchard")] + orchard: Some((&self.orchard).into()), + unknown: vec![], + } + } + + /// Returns the transparent component of the unified key at the + /// BIP44 path `m/44'/'/'`. + #[cfg(feature = "transparent-inputs")] + pub fn transparent(&self) -> &transparent::keys::AccountPrivKey { + &self.transparent + } + + /// Returns the Sapling extended spending key component of this unified spending key. + #[cfg(feature = "sapling")] + pub fn sapling(&self) -> &sapling::ExtendedSpendingKey { + &self.sapling + } + + /// Returns the Orchard spending key component of this unified spending key. + #[cfg(feature = "orchard")] + pub fn orchard(&self) -> &orchard::keys::SpendingKey { + &self.orchard + } + + /// Returns a binary encoding of this key suitable for decoding with [`Self::from_bytes`]. + /// + /// The encoded form of a unified spending key is only intended for use + /// within wallets when required for storage and/or crossing FFI boundaries; + /// unified spending keys should not be exposed to users, and consequently + /// no string-based encoding is defined. This encoding does not include any + /// internal validation metadata (such as checksums) as keys decoded from + /// this form will necessarily be validated when the attempt is made to + /// spend a note that they have authority for. + #[cfg(feature = "unstable")] + pub fn to_bytes(&self, era: Era) -> Vec { + let mut result = vec![]; + result.write_u32::(era.id()).unwrap(); + + #[cfg(feature = "orchard")] + { + let orchard_key = self.orchard(); + CompactSize::write(&mut result, usize::try_from(Typecode::Orchard).unwrap()).unwrap(); + + let orchard_key_bytes = orchard_key.to_bytes(); + CompactSize::write(&mut result, orchard_key_bytes.len()).unwrap(); + result.write_all(orchard_key_bytes).unwrap(); + } + + #[cfg(feature = "sapling")] + { + let sapling_key = self.sapling(); + CompactSize::write(&mut result, usize::try_from(Typecode::Sapling).unwrap()).unwrap(); + + let sapling_key_bytes = sapling_key.to_bytes(); + CompactSize::write(&mut result, sapling_key_bytes.len()).unwrap(); + result.write_all(&sapling_key_bytes).unwrap(); + } + + #[cfg(feature = "transparent-inputs")] + { + let account_tkey = self.transparent(); + CompactSize::write(&mut result, usize::try_from(Typecode::P2pkh).unwrap()).unwrap(); + + let account_tkey_bytes = account_tkey.to_bytes(); + CompactSize::write(&mut result, account_tkey_bytes.len()).unwrap(); + result.write_all(&account_tkey_bytes).unwrap(); + } + + result + } + + /// Decodes a [`UnifiedSpendingKey`] value from its serialized representation. + /// + /// See [`Self::to_bytes`] for additional detail about the encoded form. + #[allow(clippy::unnecessary_unwrap)] + #[cfg(feature = "unstable")] + pub fn from_bytes(era: Era, encoded: &[u8]) -> Result { + let mut source = core2::io::Cursor::new(encoded); + let decoded_era = source + .read_u32::() + .map_err(|_| DecodingError::ReadError("era")) + .and_then(|id| Era::try_from_id(id).ok_or(DecodingError::EraInvalid))?; + + if decoded_era != era { + return Err(DecodingError::EraMismatch(decoded_era)); + } + + #[cfg(feature = "orchard")] + let mut orchard = None; + #[cfg(feature = "sapling")] + let mut sapling = None; + #[cfg(feature = "transparent-inputs")] + let mut transparent = None; + loop { + let tc = CompactSize::read_t::<_, u32>(&mut source) + .map_err(|_| DecodingError::ReadError("typecode")) + .and_then(|v| Typecode::try_from(v).map_err(|_| DecodingError::TypecodeInvalid))?; + + let len = CompactSize::read_t::<_, u32>(&mut source) + .map_err(|_| DecodingError::ReadError("key length"))?; + + match tc { + Typecode::Orchard => { + if len != 32 { + return Err(DecodingError::LengthMismatch(Typecode::Orchard, len)); + } + + let mut key = [0u8; 32]; + source + .read_exact(&mut key) + .map_err(|_| DecodingError::InsufficientData(Typecode::Orchard))?; + + #[cfg(feature = "orchard")] + { + orchard = Some( + Option::::from( + orchard::keys::SpendingKey::from_bytes(key), + ) + .ok_or(DecodingError::KeyDataInvalid(Typecode::Orchard))?, + ); + } + } + Typecode::Sapling => { + if len != 169 { + return Err(DecodingError::LengthMismatch(Typecode::Sapling, len)); + } + + let mut key = [0u8; 169]; + source + .read_exact(&mut key) + .map_err(|_| DecodingError::InsufficientData(Typecode::Sapling))?; + + #[cfg(feature = "sapling")] + { + sapling = Some( + sapling::ExtendedSpendingKey::from_bytes(&key) + .map_err(|_| DecodingError::KeyDataInvalid(Typecode::Sapling))?, + ); + } + } + Typecode::P2pkh => { + if len != 74 { + return Err(DecodingError::LengthMismatch(Typecode::P2pkh, len)); + } + + let mut key = [0u8; 74]; + source + .read_exact(&mut key) + .map_err(|_| DecodingError::InsufficientData(Typecode::P2pkh))?; + + #[cfg(feature = "transparent-inputs")] + { + transparent = Some( + transparent::keys::AccountPrivKey::from_bytes(&key) + .ok_or(DecodingError::KeyDataInvalid(Typecode::P2pkh))?, + ); + } + } + _ => { + return Err(DecodingError::TypecodeInvalid); + } + } + + #[cfg(feature = "orchard")] + let has_orchard = orchard.is_some(); + #[cfg(not(feature = "orchard"))] + let has_orchard = true; + + #[cfg(feature = "sapling")] + let has_sapling = sapling.is_some(); + #[cfg(not(feature = "sapling"))] + let has_sapling = true; + + #[cfg(feature = "transparent-inputs")] + let has_transparent = transparent.is_some(); + #[cfg(not(feature = "transparent-inputs"))] + let has_transparent = true; + + if has_orchard && has_sapling && has_transparent { + return UnifiedSpendingKey::from_checked_parts( + #[cfg(feature = "transparent-inputs")] + transparent.unwrap(), + #[cfg(feature = "sapling")] + sapling.unwrap(), + #[cfg(feature = "orchard")] + orchard.unwrap(), + ) + .map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2pkh)); + } + } + } + + #[cfg(any(test, feature = "test-dependencies"))] + pub fn default_address( + &self, + request: Option, + ) -> (UnifiedAddress, DiversifierIndex) { + self.to_unified_full_viewing_key() + .default_address(request) + .unwrap() + } + + #[cfg(all( + feature = "transparent-inputs", + any(test, feature = "test-dependencies") + ))] + pub fn default_transparent_address(&self) -> (TransparentAddress, NonHardenedChildIndex) { + self.transparent() + .to_account_pubkey() + .derive_external_ivk() + .unwrap() + .default_address() + } +} + +/// Errors that can occur in the generation of unified addresses. +#[derive(Clone, Debug)] +pub enum AddressGenerationError { + /// The requested diversifier index was outside the range of valid transparent + /// child address indices. + #[cfg(feature = "transparent-inputs")] + InvalidTransparentChildIndex(DiversifierIndex), + /// The diversifier index could not be mapped to a valid Sapling diversifier. + #[cfg(feature = "sapling")] + InvalidSaplingDiversifierIndex(DiversifierIndex), + /// The space of available diversifier indices has been exhausted. + DiversifierSpaceExhausted, + /// A requested address typecode was not recognized, so we are unable to generate the address + /// as requested. + ReceiverTypeNotSupported(Typecode), + /// A requested address typecode was recognized, but the unified key being used to generate the + /// address lacks an item of the requested type. + KeyNotAvailable(Typecode), + /// A Unified address cannot be generated without at least one shielded receiver being + /// included. + ShieldedReceiverRequired, +} + +impl fmt::Display for AddressGenerationError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self { + #[cfg(feature = "transparent-inputs")] + AddressGenerationError::InvalidTransparentChildIndex(i) => { + write!( + f, + "Child index {:?} does not generate a valid transparent receiver", + i + ) + } + #[cfg(feature = "sapling")] + AddressGenerationError::InvalidSaplingDiversifierIndex(i) => { + write!( + f, + "Child index {:?} does not generate a valid Sapling receiver", + i + ) + } + AddressGenerationError::DiversifierSpaceExhausted => { + write!( + f, + "Exhausted the space of diversifier indices without finding an address." + ) + } + AddressGenerationError::ReceiverTypeNotSupported(t) => { + write!( + f, + "Unified Address generation does not yet support receivers of type {:?}.", + t + ) + } + AddressGenerationError::KeyNotAvailable(t) => { + write!( + f, + "The Unified Viewing Key does not contain a key for typecode {:?}.", + t + ) + } + AddressGenerationError::ShieldedReceiverRequired => { + write!(f, "A Unified Address requires at least one shielded (Sapling or Orchard) receiver.") + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for AddressGenerationError {} + +/// An enumeration of the ways in which a receiver may be requested to be present in a generated +/// [`UnifiedAddress`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ReceiverRequirement { + /// A receiver of the associated type is required to be present in the generated + /// `[UnifiedAddress`], and if it is not possible to generate a receiver of this type, the + /// address generation method should return an error. When calling [`Self::intersect`], this + /// variant will be preferred over [`ReceiverRequirement::Allow`]. + Require, + /// The associated receiver should be included, if a corresponding item exists in the IVK from + /// which the address is being derived and derivation of the receiver succeeds at the given + /// diversifier index. + Allow, + /// No receiver of the associated type may be included in the generated [`UnifiedAddress`] + /// under any circumstances. When calling [`Self::intersect`], this variant will be preferred + /// over [`ReceiverRequirement::Allow`]. + Omit, +} + +impl ReceiverRequirement { + /// Return the intersection of two requirements that chooses the stronger requirement, if one + /// exists. [`ReceiverRequirement::Require`] and [`ReceiverRequirement::Omit`] are + /// incompatible; attempting an intersection between these will return an error. + pub fn intersect(self, other: Self) -> Result { + use ReceiverRequirement::*; + match (self, other) { + (Require, Omit) => Err(()), + (Require, Require) => Ok(Require), + (Require, Allow) => Ok(Require), + (Allow, Require) => Ok(Require), + (Allow, Allow) => Ok(Allow), + (Allow, Omit) => Ok(Omit), + (Omit, Require) => Err(()), + (Omit, Allow) => Ok(Omit), + (Omit, Omit) => Ok(Omit), + } + } +} + +/// Specification for how a unified address should be generated from a unified viewing key. +#[derive(Clone, Copy, Debug)] +pub struct UnifiedAddressRequest { + orchard: ReceiverRequirement, + sapling: ReceiverRequirement, + p2pkh: ReceiverRequirement, +} + +impl UnifiedAddressRequest { + /// Construct a new unified address request from its constituent parts. + /// + /// Returns `Err(())` if the resulting unified address would not include at least one shielded receiver. + pub fn new( + orchard: ReceiverRequirement, + sapling: ReceiverRequirement, + p2pkh: ReceiverRequirement, + ) -> Result { + use ReceiverRequirement::*; + if orchard == Omit && sapling == Omit { + Err(()) + } else { + Ok(Self { + orchard, + sapling, + p2pkh, + }) + } + } + + /// Constructs a new unified address request that allows a receiver of each type. + pub const ALLOW_ALL: UnifiedAddressRequest = { + use ReceiverRequirement::*; + Self::unsafe_new(Allow, Allow, Allow) + }; + + /// Constructs a new unified address request that includes only the receivers that are allowed + /// both in itself and a given other request. Returns [`None`] if requirements are incompatible + /// or if no shielded receiver type is allowed. + pub fn intersect(&self, other: &UnifiedAddressRequest) -> Result { + let orchard = self.orchard.intersect(other.orchard)?; + let sapling = self.sapling.intersect(other.sapling)?; + let p2pkh = self.p2pkh.intersect(other.p2pkh)?; + Self::new(orchard, sapling, p2pkh) + } + + /// Construct a new unified address request from its constituent parts. + /// + /// Panics: at least one of `orchard` or `sapling` must be allowed. + pub const fn unsafe_new( + orchard: ReceiverRequirement, + sapling: ReceiverRequirement, + p2pkh: ReceiverRequirement, + ) -> Self { + use ReceiverRequirement::*; + if matches!(orchard, Omit) && matches!(sapling, Omit) { + panic!("At least one shielded receiver must be allowed.") + } + + Self { + orchard, + sapling, + p2pkh, + } + } +} + +#[cfg(feature = "transparent-inputs")] +impl From for DerivationError { + fn from(e: bip32::Error) -> Self { + DerivationError::Transparent(e) + } +} + +/// A [ZIP 316](https://zips.z.cash/zip-0316) unified full viewing key. +#[derive(Clone, Debug)] +pub struct UnifiedFullViewingKey { + #[cfg(feature = "transparent-inputs")] + transparent: Option, + #[cfg(feature = "sapling")] + sapling: Option, + #[cfg(feature = "orchard")] + orchard: Option, + unknown: Vec<(u32, Vec)>, +} + +impl UnifiedFullViewingKey { + /// Construct a new unified full viewing key. + /// + /// This method is only available when the `test-dependencies` feature is enabled, + /// as derivation from the USK or deserialization from the serialized form should + /// be used instead. + #[cfg(any(test, feature = "test-dependencies"))] + pub fn new( + #[cfg(feature = "transparent-inputs")] transparent: Option< + transparent::keys::AccountPubKey, + >, + #[cfg(feature = "sapling")] sapling: Option, + #[cfg(feature = "orchard")] orchard: Option, + // TODO: Implement construction of UFVKs with metadata items. + ) -> Result { + Self::from_checked_parts( + #[cfg(feature = "transparent-inputs")] + transparent, + #[cfg(feature = "sapling")] + sapling, + #[cfg(feature = "orchard")] + orchard, + // We don't currently allow constructing new UFVKs with unknown items, but we store + // this to allow parsing such UFVKs. + vec![], + ) + } + + #[cfg(feature = "unstable-frost")] + pub fn from_orchard_fvk( + orchard: orchard::keys::FullViewingKey, + ) -> Result { + Self::from_checked_parts( + #[cfg(feature = "transparent-inputs")] + None, + #[cfg(feature = "sapling")] + None, + #[cfg(feature = "orchard")] + Some(orchard), + // We don't currently allow constructing new UFVKs with unknown items, but we store + // this to allow parsing such UFVKs. + vec![], + ) + } + + #[cfg(all(feature = "sapling", feature = "unstable"))] + pub fn from_sapling_extended_full_viewing_key( + sapling: ExtendedFullViewingKey, + ) -> Result { + Self::from_checked_parts( + #[cfg(feature = "transparent-inputs")] + None, + #[cfg(feature = "sapling")] + Some(sapling.to_diversifiable_full_viewing_key()), + #[cfg(feature = "orchard")] + None, + // We don't currently allow constructing new UFVKs with unknown items, but we store + // this to allow parsing such UFVKs. + vec![], + ) + } + + /// Construct a UFVK from its constituent parts, after verifying that UIVK derivation can + /// succeed. + fn from_checked_parts( + #[cfg(feature = "transparent-inputs")] transparent: Option< + transparent::keys::AccountPubKey, + >, + #[cfg(feature = "sapling")] sapling: Option, + #[cfg(feature = "orchard")] orchard: Option, + unknown: Vec<(u32, Vec)>, + ) -> Result { + // Verify that IVK derivation succeeds; we don't want to construct a UFVK + // that can't derive transparent addresses. + #[cfg(feature = "transparent-inputs")] + let _ = transparent + .as_ref() + .map(|t| t.derive_external_ivk()) + .transpose()?; + + Ok(UnifiedFullViewingKey { + #[cfg(feature = "transparent-inputs")] + transparent, + #[cfg(feature = "sapling")] + sapling, + #[cfg(feature = "orchard")] + orchard, + unknown, + }) + } + + /// Parses a `UnifiedFullViewingKey` from its [ZIP 316] string encoding. + /// + /// [ZIP 316]: https://zips.z.cash/zip-0316 + pub fn decode(params: &P, encoding: &str) -> Result { + let (net, ufvk) = unified::Ufvk::decode(encoding).map_err(|e| e.to_string())?; + let expected_net = params.network_type(); + if net != expected_net { + return Err(format!( + "UFVK is for network {:?} but we expected {:?}", + net, expected_net, + )); + } + + Self::parse(&ufvk).map_err(|e| e.to_string()) + } + + /// Parses a `UnifiedFullViewingKey` from its [ZIP 316] string encoding. + /// + /// [ZIP 316]: https://zips.z.cash/zip-0316 + pub fn parse(ufvk: &Ufvk) -> Result { + #[cfg(feature = "orchard")] + let mut orchard = None; + #[cfg(feature = "sapling")] + let mut sapling = None; + #[cfg(feature = "transparent-inputs")] + let mut transparent = None; + + // We can use as-parsed order here for efficiency, because we're breaking out the + // receivers we support from the unknown receivers. + let unknown = ufvk + .items_as_parsed() + .iter() + .filter_map(|receiver| match receiver { + #[cfg(feature = "orchard")] + unified::Fvk::Orchard(data) => orchard::keys::FullViewingKey::from_bytes(data) + .ok_or(DecodingError::KeyDataInvalid(Typecode::Orchard)) + .map(|addr| { + orchard = Some(addr); + None + }) + .transpose(), + #[cfg(not(feature = "orchard"))] + unified::Fvk::Orchard(data) => Some(Ok::<_, DecodingError>(( + u32::from(unified::Typecode::Orchard), + data.to_vec(), + ))), + #[cfg(feature = "sapling")] + unified::Fvk::Sapling(data) => { + sapling::DiversifiableFullViewingKey::from_bytes(data) + .ok_or(DecodingError::KeyDataInvalid(Typecode::Sapling)) + .map(|pa| { + sapling = Some(pa); + None + }) + .transpose() + } + #[cfg(not(feature = "sapling"))] + unified::Fvk::Sapling(data) => Some(Ok::<_, DecodingError>(( + u32::from(unified::Typecode::Sapling), + data.to_vec(), + ))), + #[cfg(feature = "transparent-inputs")] + unified::Fvk::P2pkh(data) => transparent::keys::AccountPubKey::deserialize(data) + .map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2pkh)) + .map(|tfvk| { + transparent = Some(tfvk); + None + }) + .transpose(), + #[cfg(not(feature = "transparent-inputs"))] + unified::Fvk::P2pkh(data) => Some(Ok::<_, DecodingError>(( + u32::from(unified::Typecode::P2pkh), + data.to_vec(), + ))), + unified::Fvk::Unknown { typecode, data } => Some(Ok((*typecode, data.clone()))), + }) + .collect::>()?; + + Self::from_checked_parts( + #[cfg(feature = "transparent-inputs")] + transparent, + #[cfg(feature = "sapling")] + sapling, + #[cfg(feature = "orchard")] + orchard, + unknown, + ) + .map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2pkh)) + } + + /// Returns the string encoding of this `UnifiedFullViewingKey` for the given network. + pub fn encode(&self, params: &P) -> String { + self.to_ufvk().encode(¶ms.network_type()) + } + + /// Returns the string encoding of this `UnifiedFullViewingKey` for the given network. + fn to_ufvk(&self) -> Ufvk { + let items = core::iter::empty().chain(self.unknown.iter().map(|(typecode, data)| { + unified::Fvk::Unknown { + typecode: *typecode, + data: data.clone(), + } + })); + #[cfg(feature = "orchard")] + let items = items.chain( + self.orchard + .as_ref() + .map(|fvk| fvk.to_bytes()) + .map(unified::Fvk::Orchard), + ); + #[cfg(feature = "sapling")] + let items = items.chain( + self.sapling + .as_ref() + .map(|dfvk| dfvk.to_bytes()) + .map(unified::Fvk::Sapling), + ); + #[cfg(feature = "transparent-inputs")] + let items = items.chain( + self.transparent + .as_ref() + .map(|tfvk| tfvk.serialize().try_into().unwrap()) + .map(unified::Fvk::P2pkh), + ); + + unified::Ufvk::try_from_items(items.collect()) + .expect("UnifiedFullViewingKey should only be constructed safely") + } + + /// Derives a Unified Incoming Viewing Key from this Unified Full Viewing Key. + pub fn to_unified_incoming_viewing_key(&self) -> UnifiedIncomingViewingKey { + UnifiedIncomingViewingKey { + #[cfg(feature = "transparent-inputs")] + transparent: self.transparent.as_ref().map(|t| { + t.derive_external_ivk() + .expect("Transparent IVK derivation was checked at construction.") + }), + #[cfg(feature = "sapling")] + sapling: self.sapling.as_ref().map(|s| s.to_external_ivk()), + #[cfg(feature = "orchard")] + orchard: self.orchard.as_ref().map(|o| o.to_ivk(Scope::External)), + unknown: Vec::new(), + } + } + + /// Returns the transparent component of the unified key at the + /// BIP44 path `m/44'/'/'`. + #[cfg(feature = "transparent-inputs")] + pub fn transparent(&self) -> Option<&transparent::keys::AccountPubKey> { + self.transparent.as_ref() + } + + /// Returns the Sapling diversifiable full viewing key component of this unified key. + #[cfg(feature = "sapling")] + pub fn sapling(&self) -> Option<&sapling::DiversifiableFullViewingKey> { + self.sapling.as_ref() + } + + /// Returns the Orchard full viewing key component of this unified key. + #[cfg(feature = "orchard")] + pub fn orchard(&self) -> Option<&orchard::keys::FullViewingKey> { + self.orchard.as_ref() + } + + /// Attempts to derive the Unified Address for the given diversifier index and receiver types. + /// If `request` is None, the address should be derived to contain a receiver for each item in + /// this UFVK. + /// + /// Returns `None` if the specified index does not produce a valid diversifier. + pub fn address( + &self, + j: DiversifierIndex, + request: Option, + ) -> Result { + self.to_unified_incoming_viewing_key().address(j, request) + } + + /// Searches the diversifier space starting at diversifier index `j` for one which will produce + /// a valid diversifier, and return the Unified Address constructed using that diversifier + /// along with the index at which the valid diversifier was found. If `request` is None, the + /// address should be derived to contain a receiver for each item in this UFVK. + /// + /// Returns an `Err(AddressGenerationError)` if no valid diversifier exists or if the features + /// required to satisfy the unified address request are not properly enabled. + pub fn find_address( + &self, + j: DiversifierIndex, + request: Option, + ) -> Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError> { + self.to_unified_incoming_viewing_key() + .find_address(j, request) + } + + /// Find the Unified Address corresponding to the smallest valid diversifier index, along with + /// that index. If `request` is None, the address should be derived to contain a receiver for + /// each item in this UFVK. + /// + /// Returns an `Err(AddressGenerationError)` if no valid diversifier exists or if the features + /// required to satisfy the unified address request are not properly enabled. + pub fn default_address( + &self, + request: Option, + ) -> Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError> { + self.find_address(DiversifierIndex::new(), request) + } +} + +/// A [ZIP 316](https://zips.z.cash/zip-0316) unified incoming viewing key. +#[derive(Clone, Debug)] +pub struct UnifiedIncomingViewingKey { + #[cfg(feature = "transparent-inputs")] + transparent: Option, + #[cfg(feature = "sapling")] + sapling: Option<::sapling::zip32::IncomingViewingKey>, + #[cfg(feature = "orchard")] + orchard: Option, + /// Stores the unrecognized elements of the unified encoding. + unknown: Vec<(u32, Vec)>, +} + +impl UnifiedIncomingViewingKey { + /// Construct a new unified incoming viewing key. + /// + /// This method is only available when the `test-dependencies` feature is enabled, + /// as derivation from the UFVK or deserialization from the serialized form should + /// be used instead. + #[cfg(any(test, feature = "test-dependencies"))] + pub fn new( + #[cfg(feature = "transparent-inputs")] transparent: Option, + #[cfg(feature = "sapling")] sapling: Option<::sapling::zip32::IncomingViewingKey>, + #[cfg(feature = "orchard")] orchard: Option, + // TODO: Implement construction of UIVKs with metadata items. + ) -> UnifiedIncomingViewingKey { + UnifiedIncomingViewingKey { + #[cfg(feature = "transparent-inputs")] + transparent, + #[cfg(feature = "sapling")] + sapling, + #[cfg(feature = "orchard")] + orchard, + // We don't allow constructing new UFVKs with unknown items, but we store + // this to allow parsing such UFVKs. + unknown: vec![], + } + } + + /// Parses a `UnifiedFullViewingKey` from its [ZIP 316] string encoding. + /// + /// [ZIP 316]: https://zips.z.cash/zip-0316 + pub fn decode(params: &P, encoding: &str) -> Result { + let (net, ufvk) = unified::Uivk::decode(encoding).map_err(|e| e.to_string())?; + let expected_net = params.network_type(); + if net != expected_net { + return Err(format!( + "UIVK is for network {:?} but we expected {:?}", + net, expected_net, + )); + } + + Self::parse(&ufvk).map_err(|e| e.to_string()) + } + + /// Constructs a unified incoming viewing key from a parsed unified encoding. + fn parse(uivk: &Uivk) -> Result { + #[cfg(feature = "orchard")] + let mut orchard = None; + #[cfg(feature = "sapling")] + let mut sapling = None; + #[cfg(feature = "transparent-inputs")] + let mut transparent = None; + + let mut unknown = vec![]; + + // We can use as-parsed order here for efficiency, because we're breaking out the + // receivers we support from the unknown receivers. + for receiver in uivk.items_as_parsed() { + match receiver { + unified::Ivk::Orchard(data) => { + #[cfg(feature = "orchard")] + { + orchard = Some( + Option::from(orchard::keys::IncomingViewingKey::from_bytes(data)) + .ok_or(DecodingError::KeyDataInvalid(Typecode::Orchard))?, + ); + } + + #[cfg(not(feature = "orchard"))] + unknown.push((u32::from(unified::Typecode::Orchard), data.to_vec())); + } + unified::Ivk::Sapling(data) => { + #[cfg(feature = "sapling")] + { + sapling = Some( + Option::from(::sapling::zip32::IncomingViewingKey::from_bytes(data)) + .ok_or(DecodingError::KeyDataInvalid(Typecode::Sapling))?, + ); + } + + #[cfg(not(feature = "sapling"))] + unknown.push((u32::from(unified::Typecode::Sapling), data.to_vec())); + } + unified::Ivk::P2pkh(data) => { + #[cfg(feature = "transparent-inputs")] + { + transparent = Some( + transparent::keys::ExternalIvk::deserialize(data) + .map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2pkh))?, + ); + } + + #[cfg(not(feature = "transparent-inputs"))] + unknown.push((u32::from(unified::Typecode::P2pkh), data.to_vec())); + } + unified::Ivk::Unknown { typecode, data } => { + unknown.push((*typecode, data.clone())); + } + } + } + + Ok(Self { + #[cfg(feature = "transparent-inputs")] + transparent, + #[cfg(feature = "sapling")] + sapling, + #[cfg(feature = "orchard")] + orchard, + unknown, + }) + } + + /// Returns the string encoding of this `UnifiedFullViewingKey` for the given network. + pub fn encode(&self, params: &P) -> String { + self.render().encode(¶ms.network_type()) + } + + /// Converts this unified incoming viewing key to a unified encoding. + fn render(&self) -> Uivk { + let items = core::iter::empty().chain(self.unknown.iter().map(|(typecode, data)| { + unified::Ivk::Unknown { + typecode: *typecode, + data: data.clone(), + } + })); + #[cfg(feature = "orchard")] + let items = items.chain( + self.orchard + .as_ref() + .map(|ivk| ivk.to_bytes()) + .map(unified::Ivk::Orchard), + ); + #[cfg(feature = "sapling")] + let items = items.chain( + self.sapling + .as_ref() + .map(|divk| divk.to_bytes()) + .map(unified::Ivk::Sapling), + ); + #[cfg(feature = "transparent-inputs")] + let items = items.chain( + self.transparent + .as_ref() + .map(|tivk| tivk.serialize().try_into().unwrap()) + .map(unified::Ivk::P2pkh), + ); + + unified::Uivk::try_from_items(items.collect()) + .expect("UnifiedIncomingViewingKey should only be constructed safely.") + } + + /// Returns the Transparent external IVK, if present. + #[cfg(feature = "transparent-inputs")] + pub fn transparent(&self) -> &Option { + &self.transparent + } + + /// Returns the Sapling IVK, if present. + #[cfg(feature = "sapling")] + pub fn sapling(&self) -> &Option<::sapling::zip32::IncomingViewingKey> { + &self.sapling + } + + /// Returns the Orchard IVK, if present. + #[cfg(feature = "orchard")] + pub fn orchard(&self) -> &Option { + &self.orchard + } + + /// Attempts to derive the Unified Address for the given diversifier index and receiver types. + /// If `request` is None, the address will be derived to contain a receiver for each item in + /// this UFVK. + /// + /// Returns an error if the this key does not produce a valid receiver for a required receiver + /// type at the given diversifier index. + pub fn address( + &self, + _j: DiversifierIndex, + request: Option, + ) -> Result { + use ReceiverRequirement::*; + + let request = request + .or(self.to_address_request().ok()) + .ok_or(AddressGenerationError::ShieldedReceiverRequired)?; + + #[cfg(feature = "orchard")] + let mut orchard = None; + if request.orchard != Omit { + #[cfg(not(feature = "orchard"))] + if request.orchard == Require { + return Err(AddressGenerationError::ReceiverTypeNotSupported( + Typecode::Orchard, + )); + } + + #[cfg(feature = "orchard")] + if let Some(oivk) = &self.orchard { + let orchard_j = orchard::keys::DiversifierIndex::from(*_j.as_bytes()); + orchard = Some(oivk.address_at(orchard_j)) + } else if request.orchard == Require { + return Err(AddressGenerationError::KeyNotAvailable(Typecode::Orchard)); + } + } + + #[cfg(feature = "sapling")] + let mut sapling = None; + if request.sapling != Omit { + #[cfg(not(feature = "sapling"))] + if request.sapling == Require { + return Err(AddressGenerationError::ReceiverTypeNotSupported( + Typecode::Sapling, + )); + } + + #[cfg(feature = "sapling")] + if let Some(divk) = &self.sapling { + // If a Sapling receiver type is requested, we must be able to construct an + // address; if we're unable to do so, then no Unified Address exists at this + // diversifier and we use `?` to early-return from this method. + sapling = match (request.sapling, divk.address_at(_j)) { + (Require | Allow, Some(addr)) => Ok(Some(addr)), + (Require, None) => { + Err(AddressGenerationError::InvalidSaplingDiversifierIndex(_j)) + } + _ => Ok(None), + }?; + } else if request.sapling == Require { + return Err(AddressGenerationError::KeyNotAvailable(Typecode::Sapling)); + } + } + + #[cfg(feature = "transparent-inputs")] + let mut transparent = None; + if request.p2pkh != Omit { + #[cfg(not(feature = "transparent-inputs"))] + if request.p2pkh == Require { + return Err(AddressGenerationError::ReceiverTypeNotSupported( + Typecode::P2pkh, + )); + } + + #[cfg(feature = "transparent-inputs")] + if let Some(tivk) = self.transparent.as_ref() { + // If a transparent receiver type is requested, we must be able to construct an + // address; if we're unable to do so, then no Unified Address exists at this + // diversifier. + let j = to_transparent_child_index(_j); + + transparent = match (request.p2pkh, j.and_then(|j| tivk.derive_address(j).ok())) { + (Require | Allow, Some(addr)) => Ok(Some(addr)), + (Require, None) => { + Err(AddressGenerationError::InvalidTransparentChildIndex(_j)) + } + _ => Ok(None), + }?; + } else if request.p2pkh == Require { + return Err(AddressGenerationError::KeyNotAvailable(Typecode::P2pkh)); + } + } + #[cfg(not(feature = "transparent-inputs"))] + let transparent = None; + + UnifiedAddress::from_receivers( + #[cfg(feature = "orchard")] + orchard, + #[cfg(feature = "sapling")] + sapling, + transparent, + ) + .ok_or(AddressGenerationError::ShieldedReceiverRequired) + } + + /// Searches the diversifier space starting at diversifier index `j` for one which will produce + /// a valid address that conforms to the provided request, and returns that Unified Address + /// along with the index at which the valid diversifier was found. + /// + /// If [`None`] is specified for the `request` parameter, a default request that [`Require`]s a + /// receiver be present for each key item enabled by the feature flags in use will be used to + /// search the diversifier space. + /// + /// Returns an `Err(AddressGenerationError)` if no valid diversifier exists or if the features + /// required to satisfy the unified address request are not enabled. + /// + /// [`Require`]: ReceiverRequirement::Require + #[allow(unused_mut)] + pub fn find_address( + &self, + mut j: DiversifierIndex, + request: Option, + ) -> Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError> { + let request = request + .or_else(|| self.to_address_request().ok()) + .ok_or(AddressGenerationError::ShieldedReceiverRequired)?; + + // If we need to generate a transparent receiver, check that the user has not + // specified an invalid transparent child index, from which we can never search to + // find a valid index. + #[cfg(feature = "transparent-inputs")] + if request.p2pkh == ReceiverRequirement::Require + && self.transparent.is_some() + && to_transparent_child_index(j).is_none() + { + return Err(AddressGenerationError::InvalidTransparentChildIndex(j)); + } + + // Find a working diversifier and construct the associated address. + loop { + let res = self.address(j, Some(request)); + match res { + Ok(ua) => { + return Ok((ua, j)); + } + #[cfg(feature = "sapling")] + Err(AddressGenerationError::InvalidSaplingDiversifierIndex(_)) => { + if j.increment().is_err() { + return Err(AddressGenerationError::DiversifierSpaceExhausted); + } + } + Err(other) => { + return Err(other); + } + } + } + } + + /// Find the Unified Address corresponding to the smallest valid diversifier index, along with + /// that index. If `request` is None, the address will be derived to contain a receiver for + /// each data item in this UFVK. + /// + /// Returns an error if the this key does not produce a valid receiver for a required receiver + /// type at any diversifier index. + pub fn default_address( + &self, + request: Option, + ) -> Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError> { + self.find_address(DiversifierIndex::new(), request) + } + + /// Constructs a [`UnifiedAddressRequest`] that requires a receiver for each data item of this UIVK. + /// + /// Returns [`Err`] if the resulting request would not include a shielded receiver. + #[allow(unused_mut)] + pub fn to_address_request(&self) -> Result { + use ReceiverRequirement::*; + + let mut orchard = Omit; + #[cfg(feature = "orchard")] + if self.orchard.is_some() { + orchard = Require; + } + + let mut sapling = Omit; + #[cfg(feature = "sapling")] + if self.sapling.is_some() { + sapling = Require; + } + + let mut p2pkh = Omit; + #[cfg(feature = "transparent-inputs")] + if self.transparent.is_some() { + p2pkh = Require; + } + + UnifiedAddressRequest::new(orchard, sapling, p2pkh) + } +} + +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing { + use proptest::prelude::*; + + use super::UnifiedSpendingKey; + use zcash_protocol::consensus::Network; + use zip32::AccountId; + + pub fn arb_unified_spending_key(params: Network) -> impl Strategy { + prop::array::uniform32(prop::num::u8::ANY).prop_flat_map(move |seed| { + prop::num::u32::ANY + .prop_map(move |account| { + UnifiedSpendingKey::from_seed( + ¶ms, + &seed, + AccountId::try_from(account & ((1 << 31) - 1)).unwrap(), + ) + }) + .prop_filter("seeds must generate valid USKs", |v| v.is_ok()) + .prop_map(|v| v.unwrap()) + }) + } +} + +#[cfg(test)] +mod tests { + use proptest::prelude::proptest; + + use zcash_protocol::consensus::MAIN_NETWORK; + use zip32::AccountId; + + #[cfg(any(feature = "sapling", feature = "orchard"))] + use { + super::{UnifiedFullViewingKey, UnifiedIncomingViewingKey}, + zcash_address::unified::{Encoding, Uivk}, + }; + + #[cfg(feature = "orchard")] + use zip32::Scope; + + #[cfg(feature = "sapling")] + use super::sapling; + + #[cfg(feature = "transparent-inputs")] + use { + crate::{address::Address, encoding::AddressCodec}, + alloc::string::ToString, + alloc::vec::Vec, + transparent::keys::{AccountPrivKey, IncomingViewingKey}, + zcash_address::test_vectors, + zip32::DiversifierIndex, + }; + + #[cfg(feature = "unstable")] + use super::{testing::arb_unified_spending_key, Era, UnifiedSpendingKey}; + + #[cfg(all(feature = "orchard", feature = "unstable"))] + use subtle::ConstantTimeEq; + + #[cfg(feature = "transparent-inputs")] + fn seed() -> Vec { + let seed_hex = "6ef5f84def6f4b9d38f466586a8380a38593bd47c8cda77f091856176da47f26b5bd1c8d097486e5635df5a66e820d28e1d73346f499801c86228d43f390304f"; + hex::decode(seed_hex).unwrap() + } + + #[test] + #[should_panic] + #[cfg(feature = "sapling")] + fn spending_key_panics_on_short_seed() { + let _ = sapling::spending_key(&[0; 31][..], 0, AccountId::ZERO); + } + + #[cfg(feature = "transparent-inputs")] + #[test] + fn pk_to_taddr() { + use transparent::keys::NonHardenedChildIndex; + + let taddr = AccountPrivKey::from_seed(&MAIN_NETWORK, &seed(), AccountId::ZERO) + .unwrap() + .to_account_pubkey() + .derive_external_ivk() + .unwrap() + .derive_address(NonHardenedChildIndex::ZERO) + .unwrap() + .encode(&MAIN_NETWORK); + assert_eq!(taddr, "t1PKtYdJJHhc3Pxowmznkg7vdTwnhEsCvR4".to_string()); + } + + #[test] + #[cfg(any(feature = "orchard", feature = "sapling"))] + fn ufvk_round_trip() { + #[cfg(feature = "orchard")] + let orchard = { + let sk = + orchard::keys::SpendingKey::from_zip32_seed(&[0; 32], 0, AccountId::ZERO).unwrap(); + Some(orchard::keys::FullViewingKey::from(&sk)) + }; + + #[cfg(feature = "sapling")] + let sapling = { + let extsk = sapling::spending_key(&[0; 32], 0, AccountId::ZERO); + Some(extsk.to_diversifiable_full_viewing_key()) + }; + + #[cfg(feature = "transparent-inputs")] + let transparent = { + let privkey = + AccountPrivKey::from_seed(&MAIN_NETWORK, &[0; 32], AccountId::ZERO).unwrap(); + Some(privkey.to_account_pubkey()) + }; + + let ufvk = UnifiedFullViewingKey::new( + #[cfg(feature = "transparent-inputs")] + transparent, + #[cfg(feature = "sapling")] + sapling, + #[cfg(feature = "orchard")] + orchard, + ); + + let ufvk = ufvk.expect("Orchard or Sapling fvk is present."); + let encoded = ufvk.encode(&MAIN_NETWORK); + + // Test encoded form against known values; these test vectors contain Orchard receivers + // that will be treated as unknown if the `orchard` feature is not enabled. + let encoded_with_t = "uview1tg6rpjgju2s2j37gkgjq79qrh5lvzr6e0ed3n4sf4hu5qd35vmsh7avl80xa6mx7ryqce9hztwaqwrdthetpy4pc0kce25x453hwcmax02p80pg5savlg865sft9reat07c5vlactr6l2pxtlqtqunt2j9gmvr8spcuzf07af80h5qmut38h0gvcfa9k4rwujacwwca9vu8jev7wq6c725huv8qjmhss3hdj2vh8cfxhpqcm2qzc34msyrfxk5u6dqttt4vv2mr0aajreww5yufpk0gn4xkfm888467k7v6fmw7syqq6cceu078yw8xja502jxr0jgum43lhvpzmf7eu5dmnn6cr6f7p43yw8znzgxg598mllewnx076hljlvynhzwn5es94yrv65tdg3utuz2u3sras0wfcq4adxwdvlk387d22g3q98t5z74quw2fa4wed32escx8dwh4mw35t4jwf35xyfxnu83mk5s4kw2glkgsshmxk"; + let _encoded_no_t = "uview12z384wdq76ceewlsu0esk7d97qnd23v2qnvhujxtcf2lsq8g4hwzpx44fwxssnm5tg8skyh4tnc8gydwxefnnm0hd0a6c6etmj0pp9jqkdsllkr70u8gpf7ndsfqcjlqn6dec3faumzqlqcmtjf8vp92h7kj38ph2786zx30hq2wru8ae3excdwc8w0z3t9fuw7mt7xy5sn6s4e45kwm0cjp70wytnensgdnev286t3vew3yuwt2hcz865y037k30e428dvgne37xvyeal2vu8yjnznphf9t2rw3gdp0hk5zwq00ws8f3l3j5n3qkqgsyzrwx4qzmgq0xwwk4vz2r6vtsykgz089jncvycmem3535zjwvvtvjw8v98y0d5ydwte575gjm7a7k"; + + // We test the full roundtrip only with the `sapling` and `orchard` features enabled, + // because we will not generate these parts of the encoding if the UFVK does not have an + // these parts. + #[cfg(all(feature = "sapling", feature = "orchard"))] + { + #[cfg(feature = "transparent-inputs")] + assert_eq!(encoded, encoded_with_t); + #[cfg(not(feature = "transparent-inputs"))] + assert_eq!(encoded, _encoded_no_t); + } + + let decoded = UnifiedFullViewingKey::decode(&MAIN_NETWORK, &encoded).unwrap(); + let reencoded = decoded.encode(&MAIN_NETWORK); + assert_eq!(encoded, reencoded); + + #[cfg(feature = "transparent-inputs")] + assert_eq!( + decoded.transparent.map(|t| t.serialize()), + ufvk.transparent.as_ref().map(|t| t.serialize()), + ); + #[cfg(feature = "sapling")] + assert_eq!( + decoded.sapling.map(|s| s.to_bytes()), + ufvk.sapling.map(|s| s.to_bytes()), + ); + #[cfg(feature = "orchard")] + assert_eq!( + decoded.orchard.map(|o| o.to_bytes()), + ufvk.orchard.map(|o| o.to_bytes()), + ); + + let decoded_with_t = UnifiedFullViewingKey::decode(&MAIN_NETWORK, encoded_with_t).unwrap(); + #[cfg(feature = "transparent-inputs")] + assert_eq!( + decoded_with_t.transparent.map(|t| t.serialize()), + ufvk.transparent.as_ref().map(|t| t.serialize()), + ); + + // Both Orchard and Sapling enabled + #[cfg(all( + feature = "orchard", + feature = "sapling", + feature = "transparent-inputs" + ))] + assert_eq!(decoded_with_t.unknown.len(), 0); + #[cfg(all( + feature = "orchard", + feature = "sapling", + not(feature = "transparent-inputs") + ))] + assert_eq!(decoded_with_t.unknown.len(), 1); + + // Orchard enabled + #[cfg(all( + feature = "orchard", + not(feature = "sapling"), + feature = "transparent-inputs" + ))] + assert_eq!(decoded_with_t.unknown.len(), 1); + #[cfg(all( + feature = "orchard", + not(feature = "sapling"), + not(feature = "transparent-inputs") + ))] + assert_eq!(decoded_with_t.unknown.len(), 2); + + // Sapling enabled + #[cfg(all( + not(feature = "orchard"), + feature = "sapling", + feature = "transparent-inputs" + ))] + assert_eq!(decoded_with_t.unknown.len(), 1); + #[cfg(all( + not(feature = "orchard"), + feature = "sapling", + not(feature = "transparent-inputs") + ))] + assert_eq!(decoded_with_t.unknown.len(), 2); + } + + #[test] + #[cfg(feature = "transparent-inputs")] + fn ufvk_derivation() { + use crate::keys::UnifiedAddressRequest; + + use super::{ReceiverRequirement::*, UnifiedSpendingKey}; + + for tv in test_vectors::UNIFIED { + let usk = UnifiedSpendingKey::from_seed( + &MAIN_NETWORK, + &tv.root_seed, + AccountId::try_from(tv.account).unwrap(), + ) + .expect("seed produced a valid unified spending key"); + + let d_idx = DiversifierIndex::from(tv.diversifier_index); + let ufvk = usk.to_unified_full_viewing_key(); + + // The test vectors contain some diversifier indices that do not generate + // valid Sapling addresses, so skip those. + #[cfg(feature = "sapling")] + if ufvk.sapling().unwrap().address(d_idx).is_none() { + continue; + } + + let ua = ufvk + .address( + d_idx, + Some(UnifiedAddressRequest::unsafe_new(Omit, Require, Require)), + ) + .unwrap_or_else(|err| { + panic!( + "unified address generation failed for account {}: {:?}", + tv.account, err + ) + }); + + match Address::decode(&MAIN_NETWORK, tv.unified_addr) { + Some(Address::Unified(tvua)) => { + // We always derive transparent and Sapling receivers, but not + // every value in the test vectors has these present. + if tvua.has_transparent() { + assert_eq!(tvua.transparent(), ua.transparent()); + } + #[cfg(feature = "sapling")] + if tvua.has_sapling() { + assert_eq!(tvua.sapling(), ua.sapling()); + } + } + _other => { + panic!( + "{} did not decode to a valid unified address", + tv.unified_addr + ); + } + } + } + } + + #[test] + #[cfg(any(feature = "orchard", feature = "sapling"))] + fn uivk_round_trip() { + use zcash_protocol::consensus::NetworkType; + + #[cfg(feature = "orchard")] + let orchard = { + let sk = + orchard::keys::SpendingKey::from_zip32_seed(&[0; 32], 0, AccountId::ZERO).unwrap(); + Some(orchard::keys::FullViewingKey::from(&sk).to_ivk(Scope::External)) + }; + + #[cfg(feature = "sapling")] + let sapling = { + let extsk = sapling::spending_key(&[0; 32], 0, AccountId::ZERO); + Some(extsk.to_diversifiable_full_viewing_key().to_external_ivk()) + }; + + #[cfg(feature = "transparent-inputs")] + let transparent = { + let privkey = + AccountPrivKey::from_seed(&MAIN_NETWORK, &[0; 32], AccountId::ZERO).unwrap(); + Some(privkey.to_account_pubkey().derive_external_ivk().unwrap()) + }; + + let uivk = UnifiedIncomingViewingKey::new( + #[cfg(feature = "transparent-inputs")] + transparent, + #[cfg(feature = "sapling")] + sapling, + #[cfg(feature = "orchard")] + orchard, + ); + + let encoded = uivk.render().encode(&NetworkType::Main); + + // Test encoded form against known values; these test vectors contain Orchard receivers + // that will be treated as unknown if the `orchard` feature is not enabled. + let encoded_with_t = "uivk1z28yg638vjwusmf0zc9ad2j0mpv6s42wc5kqt004aaqfu5xxxgu7mdcydn9qf723fnryt34s6jyxyw0jt7spq04c3v9ze6qe9gjjc5aglz8zv5pqtw58czd0actynww5n85z3052kzgy6cu0fyjafyp4sr4kppyrrwhwev2rr0awq6m8d66esvk6fgacggqnswg5g9gkv6t6fj9ajhyd0gmel4yzscprpzduncc0e2lywufup6fvzf6y8cefez2r99pgge5yyfuus0r60khgu895pln5e7nn77q6s9kh2uwf6lrfu06ma2kd7r05jjvl4hn6nupge8fajh0cazd7mkmz23t79w"; + let _encoded_no_t = "uivk1020vq9j5zeqxh303sxa0zv2hn9wm9fev8x0p8yqxdwyzde9r4c90fcglc63usj0ycl2scy8zxuhtser0qrq356xfy8x3vyuxu7f6gas75svl9v9m3ctuazsu0ar8e8crtx7x6zgh4kw8xm3q4rlkpm9er2wefxhhf9pn547gpuz9vw27gsdp6c03nwlrxgzhr2g6xek0x8l5avrx9ue9lf032tr7kmhqf3nfdxg7ldfgx6yf09g"; + + // We test the full roundtrip only with the `sapling` and `orchard` features enabled, + // because we will not generate these parts of the encoding if the UIVK does not have an + // these parts. + #[cfg(all(feature = "sapling", feature = "orchard"))] + { + #[cfg(feature = "transparent-inputs")] + assert_eq!(encoded, encoded_with_t); + #[cfg(not(feature = "transparent-inputs"))] + assert_eq!(encoded, _encoded_no_t); + } + + let decoded = UnifiedIncomingViewingKey::parse(&Uivk::decode(&encoded).unwrap().1).unwrap(); + let reencoded = decoded.render().encode(&NetworkType::Main); + assert_eq!(encoded, reencoded); + + #[cfg(feature = "transparent-inputs")] + assert_eq!( + decoded.transparent.map(|t| t.serialize()), + uivk.transparent.as_ref().map(|t| t.serialize()), + ); + #[cfg(feature = "sapling")] + assert_eq!( + decoded.sapling.map(|s| s.to_bytes()), + uivk.sapling.map(|s| s.to_bytes()), + ); + #[cfg(feature = "orchard")] + assert_eq!( + decoded.orchard.map(|o| o.to_bytes()), + uivk.orchard.map(|o| o.to_bytes()), + ); + + let decoded_with_t = + UnifiedIncomingViewingKey::parse(&Uivk::decode(encoded_with_t).unwrap().1).unwrap(); + #[cfg(feature = "transparent-inputs")] + assert_eq!( + decoded_with_t.transparent.map(|t| t.serialize()), + uivk.transparent.as_ref().map(|t| t.serialize()), + ); + + // Both Orchard and Sapling enabled + #[cfg(all( + feature = "orchard", + feature = "sapling", + feature = "transparent-inputs" + ))] + assert_eq!(decoded_with_t.unknown.len(), 0); + #[cfg(all( + feature = "orchard", + feature = "sapling", + not(feature = "transparent-inputs") + ))] + assert_eq!(decoded_with_t.unknown.len(), 1); + + // Orchard enabled + #[cfg(all( + feature = "orchard", + not(feature = "sapling"), + feature = "transparent-inputs" + ))] + assert_eq!(decoded_with_t.unknown.len(), 1); + #[cfg(all( + feature = "orchard", + not(feature = "sapling"), + not(feature = "transparent-inputs") + ))] + assert_eq!(decoded_with_t.unknown.len(), 2); + + // Sapling enabled + #[cfg(all( + not(feature = "orchard"), + feature = "sapling", + feature = "transparent-inputs" + ))] + assert_eq!(decoded_with_t.unknown.len(), 1); + #[cfg(all( + not(feature = "orchard"), + feature = "sapling", + not(feature = "transparent-inputs") + ))] + assert_eq!(decoded_with_t.unknown.len(), 2); + } + + #[test] + #[cfg(feature = "transparent-inputs")] + fn uivk_derivation() { + use crate::keys::UnifiedAddressRequest; + + use super::{ReceiverRequirement::*, UnifiedSpendingKey}; + + for tv in test_vectors::UNIFIED { + let usk = UnifiedSpendingKey::from_seed( + &MAIN_NETWORK, + &tv.root_seed, + AccountId::try_from(tv.account).unwrap(), + ) + .expect("seed produced a valid unified spending key"); + + let d_idx = DiversifierIndex::from(tv.diversifier_index); + let uivk = usk + .to_unified_full_viewing_key() + .to_unified_incoming_viewing_key(); + + // The test vectors contain some diversifier indices that do not generate + // valid Sapling addresses, so skip those. + #[cfg(feature = "sapling")] + if uivk.sapling().as_ref().unwrap().address_at(d_idx).is_none() { + continue; + } + + let ua = uivk + .address( + d_idx, + Some(UnifiedAddressRequest::unsafe_new(Omit, Require, Require)), + ) + .unwrap_or_else(|err| { + panic!( + "unified address generation failed for account {}: {:?}", + tv.account, err + ) + }); + + match Address::decode(&MAIN_NETWORK, tv.unified_addr) { + Some(Address::Unified(tvua)) => { + // We always derive transparent and Sapling receivers, but not + // every value in the test vectors has these present. + if tvua.has_transparent() { + assert_eq!(tvua.transparent(), ua.transparent()); + } + #[cfg(feature = "sapling")] + if tvua.has_sapling() { + assert_eq!(tvua.sapling(), ua.sapling()); + } + } + _other => { + panic!( + "{} did not decode to a valid unified address", + tv.unified_addr + ); + } + } + } + } + + proptest! { + #[test] + #[cfg(feature = "unstable")] + fn prop_usk_roundtrip(usk in arb_unified_spending_key(zcash_protocol::consensus::Network::MainNetwork)) { + let encoded = usk.to_bytes(Era::Orchard); + + #[allow(clippy::let_and_return)] + let encoded_len = { + let len = 4; + + #[cfg(feature = "orchard")] + let len = len + 2 + 32; + + let len = len + 2 + 169; + + // Transparent part is an `xprv` transparent extended key deserialized + // into bytes from Base58, minus the 4 prefix bytes. + #[cfg(feature = "transparent-inputs")] + let len = len + 2 + 74; + + #[allow(clippy::let_and_return)] + len + }; + assert_eq!(encoded.len(), encoded_len); + + let decoded = UnifiedSpendingKey::from_bytes(Era::Orchard, &encoded); + let decoded = decoded.unwrap_or_else(|e| panic!("Error decoding USK: {:?}", e)); + + #[cfg(feature = "orchard")] + assert!(bool::from(decoded.orchard().ct_eq(usk.orchard()))); + + assert_eq!(decoded.sapling(), usk.sapling()); + + #[cfg(feature = "transparent-inputs")] + assert_eq!(decoded.transparent().to_bytes(), usk.transparent().to_bytes()); + } + } +} diff --git a/zcash_keys/src/lib.rs b/zcash_keys/src/lib.rs new file mode 100644 index 0000000000..48830ebd02 --- /dev/null +++ b/zcash_keys/src/lib.rs @@ -0,0 +1,33 @@ +//! *A crate for Zcash key and address management.* +//! +//! `zcash_keys` contains Rust structs, traits and functions for creating Zcash spending +//! and viewing keys and addresses. +//! +//! ## Feature flags +#![cfg_attr(feature = "std", doc = "## Feature flags")] +#![cfg_attr(feature = "std", doc = document_features::document_features!())] +//! + +#![no_std] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +// Catch documentation errors caused by code changes. +#![deny(rustdoc::broken_intra_doc_links)] +// Temporary until we have addressed all Result cases. +#![allow(clippy::result_unit_err)] + +#[macro_use] +extern crate alloc; + +#[cfg(feature = "std")] +extern crate std; + +pub mod address; +pub mod encoding; + +#[cfg(any( + feature = "orchard", + feature = "sapling", + feature = "transparent-inputs" +))] +pub mod keys; diff --git a/zcash_primitives/CHANGELOG.md b/zcash_primitives/CHANGELOG.md index f87f53d1fd..6fccebc2d2 100644 --- a/zcash_primitives/CHANGELOG.md +++ b/zcash_primitives/CHANGELOG.md @@ -6,12 +6,933 @@ and this library adheres to Rust's notion of [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +### Changed +- MSRV is now 1.81.0. +- Migrated to `bip32 =0.6.0-pre.1`, `nonempty 0.11`, `secp256k1 0.29`, + `incrementalmerkletree 0.8`. + +### Deprecated +- `zcash_primitives::consensus` (use `zcash_protocol::consensus` instead) +- `zcash_primitives::constants` (use `zcash_protocol::constants` instead) +- `zcash_primitives::memo` (use `zcash_protocol::memo` instead) +- `zcash_primitives::zip32` (use the `zip32` crate instead) +- `zcash_primitives::legacy` (use the `zcash_transparent` crate instead) +- `zcash_primitives::transaction::components::Amount` (use `zcash_protocol::value::ZatBalance` instead) +- `zcash_primitives::transaction::components::amount`: + - `BalanceError` (use `zcash_protocol::value::BalanceError` instead) + - `Amount` (use `zcash_protocol::value::ZatBalance` instead) + - `NonNegativeAmount` (use `zcash_protocol::value::Zatoshis` instead) + - `COIN` (use `zcash_protocol::value::COIN` instead) + - module `testing` (use `zcash_protocol::value::testing` instead) + - `arb_positive_amount` (use `zcash_protocol::value::testing::arb_positive_zat_balance` instead.) + - `arb_amount` (use `zcash_protocol::value::testing::arb_zat_balance` instead.) + - `arb_nonnegative_amount` (use `::zcash_protocol::value::testing::arb_zatoshis` instead.) + +### Removed +- `zcash_primitives::transaction::sighash::TransparentAuthorizingContext` was + removed as there is no way to deprecate a previously-reexported trait name. + Use `zcash_transparent::sighash::TransparentAuthorizingContext` instead. + +## [0.21.0] - 2024-12-16 + +### Added +- `zcash_primitives::legacy::Script::address` +- `zcash_primitives::transaction` + - `TransactionData::try_map_bundles` + - `builder::{PcztResult, PcztParts}` + - `builder::Builder::build_for_pczt` + - `components::transparent`: + - `pczt` module. + - `EffectsOnly` + - `impl MapAuth for ()` + - `builder::TransparentSigningSet` + - `sighash::SighashType` + +### Changed +- Migrated to `sapling-crypto` version `0.4`. +- `zcash_primitives::transaction::components::transparent`: + - `builder::TransparentBuilder::add_input` now takes `secp256k1::PublicKey` + instead of `secp256k1::SecretKey`. + - `Bundle::apply_signatures` has had its arguments replaced with + a function providing the sighash calculation, and `&TransparentSigningSet`. + - `builder::Error` has a new variant `MissingSigningKey`. +- `zcash_primitives::transaction::builder`: + - `Builder::add_orchard_spend` now takes `orchard::keys::FullViewingKey` + instead of `&orchard::keys::SpendingKey`. + - `Builder::add_sapling_spend` now takes `sapling::keys::FullViewingKey` + instead of `&sapling::zip32::ExtendedSpendingKey`. + - `Builder::add_transparent_input` now takes `secp256k1::PublicKey` instead of + `secp256k1::SecretKey`. + - `Builder::build` now takes several additional arguments: + - `&TransparentSigningSet` + - `&[sapling::zip32::ExtendedSpendingKey]` + - `&[orchard::keys::SpendAuthorizingKey]` +- `zcash_primitives::transaction::sighash`: + - `SignableInput::Transparent` is now a wrapper around + `zcash_transparent::sighash::SignableInput`. + +## [0.20.0] - 2024-11-14 + +### Added +- A new feature flag, `non-standard-fees`, has been added. This flag is now + required in order to make use of any types or methods that enable non-standard + fee calculation. + +### Changed +- MSRV is now 1.77.0. +- `zcash_primitives::transaction::fees`: + - The `fixed` module has been moved behind the `non-standard-fees` feature + flag. Using a fixed fee may result in a transaction that cannot be mined on + the current Zcash network. To calculate the ZIP 317 fee, use + `zip317::FeeRule::standard()`. + - `zip317::FeeRule::non_standard` has been moved behind the `non-standard-fees` + feature flag. Using a non-standard fee may result in a transaction that cannot + be mined on the current `Zcash` network. + +### Deprecated +- `zcash_primitives::transaction::fees`: + - `StandardFeeRule` has been deprecated. It was never used within `zcash_primitives` + and should have been a member of `zcash_client_backend::fees` instead. + +### Removed +- `zcash_primitives::transaction::fees`: + - `StandardFeeRule` itself has been removed; it was not used in this crate. + Its use in `zcash_client_backend` has been replaced with + `zcash_client_backend::fees::StandardFeeRule`. + - `fixed::FeeRule::standard`. This constructor was misleadingly named: using a + fixed fee does not conform to any current Zcash standard. To calculate the + ZIP 317 fee, use `zip317::FeeRule::standard()`. To preserve the current + behaviour, use `fixed::FeeRule::non_standard(zip317::MINIMUM_FEE)`, + but note that this is likely to result in transactions that cannot be mined. + +## [0.19.0] - 2024-10-02 + +### Changed +- Migrated to `zcash_address 0.6`. + +### Fixed +- The previous release did not bump `zcash_address` and ended up depending on + multiple versions of `zcash_protocol`, which didn't cause a code conflict but + results in two different consensus protocol states being present in the + dependency tree. + +## [0.18.0] - 2024-10-02 + +### Changed +- Update dependencies to `incrementalmerkletree 0.7`, `orchard 0.10`, + `sapling-crypto 0.3`, `zcash_protocol 0.4`. + +## [0.17.0] - 2024-08-26 + +### Changed +- Update dependencies to `zcash_protocol 0.3`, `zcash_address 0.5`. + +## [0.16.0] - 2024-08-19 + +### Added +- `zcash_primitives::legacy::keys`: + - `impl From for bip32::ChildNumber` + - `impl From for bip32::ChildNumber` + - `impl TryFrom for NonHardenedChildIndex` + - `EphemeralIvk` + - `AccountPubKey::derive_ephemeral_ivk` + - `TransparentKeyScope::custom` is now `const`. + - `TransparentKeyScope::{EXTERNAL, INTERNAL, EPHEMERAL}` +- `zcash_primitives::legacy::Script::serialized_size` +- `zcash_primitives::transaction::fees::transparent`: + - `InputSize` + - `InputView::serialized_size` + - `OutputView::serialized_size` +- `zcash_primitives::transaction::component::transparent::OutPoint::txid` +- `zcash_primitives::transaction::builder::DEFAULT_TX_EXPIRY_DELTA` + +### Changed +- MSRV is now 1.70.0. +- Bumped dependencies to `secp256k1 0.27`, `incrementalmerkletree 0.6`, + `orchard 0.9`, `sapling-crypto 0.2`. +- `zcash_primitives::legacy::keys`: + - `AccountPrivKey::{from_bytes, to_bytes}` now use the byte encoding from the + inside of a `xprv` Base58 string encoding from BIP 32, excluding the prefix + bytes (i.e. starting with `depth`). + - `AccountPrivKey::from_extended_privkey` now takes + `bip32::ExtendedPrivateKey`. + - The following methods now return `Result<_, bip32::Error>`: + - `AccountPrivKey::from_seed` + - `AccountPrivKey::derive_secret_key` + - `AccountPrivKey::derive_external_secret_key` + - `AccountPrivKey::derive_internal_secret_key` + - `AccountPubKey::derive_external_ivk` + - `AccountPubKey::derive_internal_ivk` + - `AccountPubKey::deserialize` + - `IncomingViewingKey::derive_address` +- `zcash_primitives::transaction::fees::FeeRule::fee_required`: the types + of parameters relating to transparent inputs and outputs have changed. + This method now requires their `tx_in` and `tx_out` serialized sizes + (expressed as iterators of `InputSize` for inputs and `usize` for outputs) + rather than a slice of `InputView` or `OutputView`. + +### Deprecated +- `zcash_primitives::transaction::fees::zip317::FeeRule::non_standard` has been + deprecated, because in general it can calculate fees that violate ZIP 317, which + might cause transactions built with it to fail. Maintaining the generality of the + current implementation imposes ongoing maintenance costs, and so it is likely to + be removed in the near future. Use `transaction::fees::zip317::FeeRule::standard()` + instead to comply with ZIP 317. + +### Removed +- The `zcash_primitives::zip339` module, which reexported parts of the API of + the `bip0039` crate, has been removed. Use the `bip0039` crate directly + instead. +- The `hdwallet` dependency and its effect on `zcash_primitives::legacy::keys`: + - `impl From for hdwallet::KeyIndex` + - `impl From for hdwallet::KeyIndex` + - `impl TryFrom for NonHardenedChildIndex` + +## [0.15.1] - 2024-05-23 + +- Fixed `sapling-crypto` dependency to not enable its `multicore` feature flag + when the default features of `zcash_primitives` are disabled. + +## [0.15.0] - 2024-03-25 + +### Added +- `zcash_primitives::transaction::components::sapling::zip212_enforcement` + +### Changed +- The following modules are now re-exported from the `zcash_protocol` crate. + Additional changes have also been made therein; refer to the `zcash_protocol` + changelog for details. + - `zcash_primitives::consensus` re-exports `zcash_protocol::consensus`. + - `zcash_primitives::constants` re-exports `zcash_protocol::constants`. + - `zcash_primitives::transaction::components::amount` re-exports + `zcash_protocol::value`. Many of the conversions to and from the + `Amount` and `NonNegativeAmount` value types now return + `Result<_, BalanceError>` instead of `Result<_, ()>`. + - `zcash_primitives::memo` re-exports `zcash_protocol::memo`. + - Update to `orchard` version `0.8.0` + +### Removed +- `zcash_primitives::consensus::sapling_zip212_enforcement` instead use + `zcash_primitives::transaction::components::sapling::zip212_enforcement`. +- From `zcash_primitive::components::transaction`: + - `impl From for u64` + - `impl TryFrom for NonNegativeAmount` + - `impl From for sapling::value::NoteValue` + - `impl TryFrom for Amount` + - `impl From for orchard::NoteValue` +- The `local_consensus` module and feature flag have been removed; use the module + from the `zcash_protocol` crate instead. +- `unstable-nu6` and `zfuture` feature flags (use `--cfg zcash_unstable=\"nu6\"` + or `--cfg zcash_unstable=\"zfuture\"` in `RUSTFLAGS` and `RUSTDOCFLAGS` + instead). + +## [0.14.0] - 2024-03-01 +### Added +- Dependency on `bellman 0.14`, `sapling-crypto 0.1`. +- `zcash_primitives::consensus::sapling_zip212_enforcement` +- `zcash_primitives::legacy::keys`: + - `AccountPrivKey::derive_secret_key` + - `NonHardenedChildIndex` + - `TransparentKeyScope` +- `zcash_primitives::local_consensus` module, behind the `local-consensus` + feature flag. + - The `LocalNetwork` struct provides a type for specifying network upgrade + activation heights for a local or specific configuration of a full node. + Developers can make use of this type when connecting to a Regtest node by + replicating the activation heights used on their node configuration. + - `impl zcash_primitives::consensus::Parameters for LocalNetwork` uses the + provided activation heights, and `zcash_primitives::constants::regtest::` + for everything else. +- `zcash_primitives::transaction`: + - `builder::{BuildConfig, FeeError, get_fee, BuildResult}` + - `builder::Error::SaplingBuilderNotAvailable` + - `components::sapling`: + - Sapling bundle component parsers, behind the `temporary-zcashd` feature + flag: + - `temporary_zcashd_read_spend_v4` + - `temporary_zcashd_read_output_v4` + - `temporary_zcashd_write_output_v4` + - `temporary_zcashd_read_v4_components` + - `temporary_zcashd_write_v4_components` + - `components::transparent`: + - `builder::TransparentInputInfo` + - `fees::StandardFeeRule` + - Constants in `fees::zip317`: + - `MARGINAL_FEE` + - `GRACE_ACTIONS` + - `P2PKH_STANDARD_INPUT_SIZE` + - `P2PKH_STANDARD_OUTPUT_SIZE` + - `impl From for [u8; 32]` +- `zcash_primitives::zip32`: + - `ChildIndex::hardened` + - `ChildIndex::index` + - `ChainCode::new` + - `ChainCode::as_bytes` + - `impl From for ChildIndex` +- Additions related to `zcash_primitive::components::amount::Amount` + and `zcash_primitive::components::amount::NonNegativeAmount`: + - `impl TryFrom for u64` + - `Amount::const_from_u64` + - `NonNegativeAmount::const_from_u64` + - `NonNegativeAmount::from_nonnegative_i64_le_bytes` + - `NonNegativeAmount::to_i64_le_bytes` + - `NonNegativeAmount::is_zero` + - `NonNegativeAmount::is_positive` + - `impl From<&NonNegativeAmount> for Amount` + - `impl From for u64` + - `impl From for zcash_primitives::sapling::value::NoteValue` + - `impl From for orchard::::NoteValue` + - `impl Sum for Option` + - `impl<'a> Sum<&'a NonNegativeAmount> for Option` + - `impl TryFrom for NonNegativeAmount` + - `impl TryFrom for NonNegativeAmount` +- `impl {Clone, PartialEq, Eq} for zcash_primitives::memo::Error` + +### Changed +- Migrated to `orchard 0.7`. +- `zcash_primitives::legacy`: + - `TransparentAddress` variants have changed: + - `TransparentAddress::PublicKey` has been renamed to `PublicKeyHash` + - `TransparentAddress::Script` has been renamed to `ScriptHash` + - `keys::{derive_external_secret_key, derive_internal_secret_key}` arguments + changed from `u32` to `NonHardenedChildIndex`. +- `zcash_primitives::transaction`: + - `builder`: + - `Builder` now has a generic parameter for the type of progress notifier, + which needs to implement `sapling::builder::ProverProgress` in order to + build transactions. + - `Builder::new` now takes a `BuildConfig` argument instead of an optional + Orchard anchor. Anchors for both Sapling and Orchard are now required at + the time of builder construction. + - `Builder::{build, build_zfuture}` now take + `&impl SpendProver, &impl OutputProver` instead of `&impl TxProver`. + - `Builder::add_sapling_spend` no longer takes a `diversifier` argument as + the diversifier may be obtained from the note. + - `Builder::add_sapling_spend` now takes its `ExtendedSpendingKey` argument + by reference. + - `Builder::{add_sapling_spend, add_sapling_output}` now return `Error`s + instead of the underlying `sapling_crypto::builder::Error`s when returning + `Err`. + - `Builder::add_orchard_spend` now takes its `SpendingKey` argument by + reference. + - `Builder::with_progress_notifier` now consumes `self` and returns a + `Builder` typed on the provided channel. + - `Builder::get_fee` now returns a `builder::FeeError` instead of the bare + `FeeRule::Error` when returning `Err`. + - `Builder::build` now returns a `Result` instead of + using a tuple to return the constructed transaction and build metadata. + - `Error::OrchardAnchorNotAvailable` has been renamed to + `OrchardBuilderNotAvailable`. + - `build` and `build_zfuture` each now take an additional `rng` argument. + - `components`: + - `transparent::TxOut.value` now has type `NonNegativeAmount` instead of + `Amount`. + - `sapling::MapAuth` trait methods now take `&mut self` instead of `&self`. + - `transparent::fees` has been moved to + `zcash_primitives::transaction::fees::transparent` + - `transparent::builder::TransparentBuilder::{inputs, outputs}` have changed + to return `&[TransparentInputInfo]` and `&[TxOut]` respectively, in order + to avoid coupling to the fee traits. + - `Unauthorized::SaplingAuth` now has type `InProgress`. + - `fees::FeeRule::fee_required` now takes an additional `orchard_action_count` + argument. + - The following methods now take `NonNegativeAmount` instead of `Amount`: + - `builder::Builder::{add_sapling_output, add_transparent_output}` + - `components::transparent::builder::TransparentBuilder::add_output` + - `fees::fixed::FeeRule::non_standard` + - `fees::zip317::FeeRule::non_standard` + - The following methods now return `NonNegativeAmount` instead of `Amount`: + - `components::amount::testing::arb_nonnegative_amount` + - `fees::transparent`: + - `InputView::value` + - `OutputView::value` + - `fees::FeeRule::{fee_required, fee_required_zfuture}` + - `fees::fixed::FeeRule::fixed_fee` + - `fees::zip317::FeeRule::marginal_fee` + - `sighash::TransparentAuthorizingContext::input_amounts` +- `zcash_primitives::zip32`: + - `ChildIndex` has been changed from an enum to an opaque struct, and no + longer supports non-hardened indices. + +### Removed +- `zcash_primitives::constants`: + - All `const` values (moved to `sapling_crypto::constants`). +- `zcash_primitives::keys` module, as it was empty after the removal of: + - `PRF_EXPAND_PERSONALIZATION` + - `OutgoingViewingKey` (use `sapling_crypto::keys::OutgoingViewingKey` + instead). + - `prf_expand, prf_expand_vec` (use `zcash_spec::PrfExpand` instead). +- `zcash_primitives::sapling` module (use the `sapling-crypto` crate instead). +- `zcash_primitives::transaction::components::sapling`: + - The following types were removed from this module (moved into + `sapling_crypto::bundle`): + - `Bundle` + - `SpendDescription, SpendDescriptionV5` + - `OutputDescription, OutputDescriptionV5` + - `Authorization, Authorized` + - `GrothProofBytes` + - `CompactOutputDescription` (moved to `sapling_crypto::note_encryption`). + - `Unproven` + - `builder` (moved to `sapling_crypto::builder`). + - `builder::Unauthorized` (use `builder::InProgress` instead). + - `testing::{arb_bundle, arb_output_description}` (moved into + `sapling_crypto::bundle::testing`). + - `SpendDescription::::apply_signature` + - `Bundle::::apply_signatures` (use + `Bundle::>::apply_signatures` instead). + - The `fees` module was removed. Its contents were unused in this crate, + are now instead made available by `zcash_client_backend::fees::sapling`. +- `impl From for u64` +- `zcash_primitives::zip32`: + - `sapling` module (moved to `sapling_crypto::zip32`). + - `ChildIndex::Hardened` (use `ChildIndex::hardened` instead). + - `ChildIndex::NonHardened` + - `sapling::ExtendedFullViewingKey::derive_child` + +### Fixed +- `zcash_primitives::keys::ExpandedSpendingKey::from_spending_key` now panics if the + spending key expands to `ask = 0`. This has a negligible probability of occurring. +- `zcash_primitives::zip32::ExtendedSpendingKey::derive_child` now panics if the + child key has `ask = 0`. This has a negligible probability of occurring. + +## [0.13.0] - 2023-09-25 +### Added +- `zcash_primitives::consensus::BlockHeight::saturating_sub` +- `zcash_primitives::transaction::builder`: + - `Builder::add_orchard_spend` + - `Builder::add_orchard_output` +- `zcash_primitives::transaction::components::orchard::builder` module +- `impl HashSer for String` is provided under the `test-dependencies` feature + flag. This is a test-only impl; the identity leaf value is `_` and the combining + operation is concatenation. +- `zcash_primitives::transaction::components::amount::NonNegativeAmount::ZERO` +- Additional trait implementations for `NonNegativeAmount`: + - `TryFrom for NonNegativeAmount` + - `Add for NonNegativeAmount` + - `Add for Option` + - `Sub for NonNegativeAmount` + - `Sub for Option` + - `Mul for NonNegativeAmount` +- `zcash_primitives::block::BlockHash::try_from_slice` + +### Changed +- Migrated to `incrementalmerkletree 0.5`, `orchard 0.6`. +- `zcash_primitives::transaction`: + - `builder::Builder::{new, new_with_rng}` now take an optional `orchard_anchor` + argument which must be provided in order to enable Orchard spends and recipients. + - All `builder::Builder` methods now require the bound `R: CryptoRng` on + `Builder<'a, P, R>`. A non-`CryptoRng` randomness source is still accepted + by `builder::Builder::test_only_new_with_rng`, which **MUST NOT** be used in + production. + - `builder::Error` has several additional variants for Orchard-related errors. + - `fees::FeeRule::fee_required` now takes an additional argument, + `orchard_action_count` + - `Unauthorized`'s associated type `OrchardAuth` is now + `orchard::builder::InProgress` + instead of `zcash_primitives::transaction::components::orchard::Unauthorized` +- `zcash_primitives::consensus::NetworkUpgrade` now implements `PartialEq`, `Eq` +- `zcash_primitives::legacy::Script` now has a custom `Debug` implementation that + renders script details in a much more legible fashion. +- `zcash_primitives::sapling::redjubjub::Signature` now has a custom `Debug` + implementation that renders details in a much more legible fashion. +- `zcash_primitives::sapling::tree::Node` now has a custom `Debug` + implementation that renders details in a much more legible fashion. + +### Removed +- `impl {PartialEq, Eq} for transaction::builder::Error` + (use `assert_matches!` where error comparisons are required) +- `zcash_primitives::transaction::components::orchard::Unauthorized` +- `zcash_primitives::transaction::components::amount::DEFAULT_FEE` was + deprecated in 0.12.0 and has now been removed. + +## [0.12.0] - 2023-06-06 +### Added +- `zcash_primitives::transaction`: + - `Transaction::temporary_zcashd_read_v5_sapling` + - `Transaction::temporary_zcashd_write_v5_sapling` +- Implementations of `memuse::DynamicUsage` for the following types: + - `zcash_primitives::transaction::components::sapling`: + - `Bundle` + - `SpendDescription` + +### Changed +- MSRV is now 1.65.0. +- Bumped dependencies to `secp256k1 0.26`, `hdwallet 0.4`, `incrementalmerkletree 0.4` + `zcash_note_encryption 0.4`, `orchard 0.5` + +### Removed +- `merkle_tree::Hashable` has been removed and its uses have been replaced by + `incrementalmerkletree::Hashable` and `merkle_tree::HashSer`. +- The `Hashable` bound on the `Node` parameter to the `IncrementalWitness` + type has been removed. +- `sapling::SAPLING_COMMITMENT_TREE_DEPTH_U8` and `sapling::SAPLING_COMMITMENT_TREE_DEPTH` + have been removed; use `sapling::NOTE_COMMITMENT_TREE_DEPTH` instead. +- `merkle_tree::{CommitmentTree, IncrementalWitness, MerklePath}` have been removed in + favor of versions of these types that are now provided by the + `incrementalmerkletree` crate. The replacement types now use const generic + parameters for enforcing the note commitment tree depth. Serialization + methods for these types that do not exist for the `incrementalmerkletree` + replacement types have been replaced by new methods in the `merkle_tree` module. +- `merkle_tree::incremental::write_auth_fragment_v1` has been removed without replacement. +- The `merkle_tree::incremental` module has been removed; its former contents + were either moved to the `merkle_tree` module or were `zcashd`-specific + serialization methods which have been removed entirely and moved into the + [zcashd](https://github.com/zcash/zcash) repository. +- The dependency on the `bridgetree` crate has been removed from + `zcash_primitives` and the following zcashd-specific serialization methods + have been moved to the [zcashd](https://github.com/zcash/zcash) repository: + - `read_auth_fragment_v1` + - `read_bridge_v1` + - `read_bridge_v2` + - `write_bridge_v2` + - `write_bridge` + - `read_checkpoint_v1` + - `read_checkpoint_v2` + - `write_checkpoint_v2` + - `read_tree` + - `write_tree` +- `merkle_tree::{SER_V1, SER_V2}` have been removed as they are now unused. + +### Moved +- The following constants and methods have been moved from the + `merkle_tree::incremental` module into the `merkle_tree` module to + consolidate the serialization code for commitment tree frontiers: + - `write_usize_leu64` + - `read_leu64_usize` + - `write_position` + - `read_position` + - `write_address` + - `read_address` + - `read_frontier_v0` + - `write_nonempty_frontier` + - `read_nonempty_frontier_v1` + - `write_frontier_v1` + - `read_frontier_v1` + +### Added +- `merkle_tree::incremental::{read_address, write_address}` +- `merkle_tree::incremental::read_bridge_v2` +- `merkle_tree::write_commitment_tree` replaces `merkle_tree::CommitmentTree::write` +- `merkle_tree::read_commitment_tree` replaces `merkle_tree::CommitmentTree::read` +- `merkle_tree::write_incremental_witness` replaces `merkle_tree::IncrementalWitness::write` +- `merkle_tree::read_incremental_witness` replaces `merkle_tree::IncrementalWitness::read` +- `merkle_tree::merkle_path_from_slice` replaces `merkle_tree::MerklePath::from_slice` +- `sapling::{CommitmentTree, IncrementalWitness, MerklePath, NOTE_COMMITMENT_TREE_DEPTH}` +- `transaction::fees::zip317::MINIMUM_FEE`, reflecting the minimum possible + [ZIP 317](https://zips.z.cash/zip-0317) conventional fee. +- `transaction::components::amount::Amount::const_from_i64`, intended for constructing + a constant `Amount`. + +### Changed +- The bounds on the `H` parameter to the following methods have changed: + - `merkle_tree::incremental::read_frontier_v0` + - `merkle_tree::incremental::read_auth_fragment_v1` +- The depth of the `merkle_tree::{CommitmentTree, IncrementalWitness, and MerklePath}` + data types are now statically constrained using const generic type parameters. +- `transaction::fees::fixed::FeeRule::standard()` now uses the ZIP 317 minimum fee + (10000 zatoshis rather than 1000 zatoshis) as the fixed fee. To be compliant with + ZIP 317, use `transaction::fees::zip317::FeeRule::standard()` instead. + +### Deprecated +- `transaction::components::amount::DEFAULT_FEE` has been deprecated. Depending on + context, you may want to use `transaction::fees::zip317::MINIMUM_FEE`, or calculate + the ZIP 317 conventional fee using `transaction::fees::zip317::FeeRule` instead. +- `transaction::fees::fixed::FeeRule::standard()` has been deprecated. + Use either `transaction::fees::zip317::FeeRule::standard()` or + `transaction::fees::fixed::FeeRule::non_standard`. + +## [0.11.0] - 2023-04-15 +### Added +- `zcash_primitives::zip32::fingerprint` module, containing types for deriving + ZIP 32 Seed Fingerprints. + +### Changed +- Bumped dependencies to `bls12_381 0.8`, `ff 0.13`, `group 0.13`, + `jubjub 0.10`, `orchard 0.4`, `sha2 0.10`, `bip0039 0.10`, + `zcash_note_encryption 0.3`. + +## [0.10.2] - 2023-03-16 +### Added +- `zcash_primitives::sapling::note`: + - `NoteCommitment::temporary_zcashd_derive` +- A new feature flag, `multicore`, has been added and is enabled by default. + This allows users to selectively disable multicore support for Orchard proof + creation by setting `default_features = false` on their `zcash_primitives` + dependency, such as is needed to enable `wasm32-wasi` compilation. + +## [0.10.1] - 2023-03-08 +### Added +- Sapling bundle component constructors, behind the `temporary-zcashd` feature + flag. These temporarily re-expose the ability to construct invalid Sapling + bundles (that was removed in 0.10.0), and will be removed in a future release: + - `zcash_primitives::transaction::components::sapling`: + - `Bundle::temporary_zcashd_from_parts` + - `SpendDescription::temporary_zcashd_from_parts` + - `OutputDescription::temporary_zcashd_from_parts` + +## [0.10.0] - 2023-02-01 +### Added +- `zcash_primitives::sapling`: + - `keys::DiversifiedTransmissionKey` + - `keys::{EphemeralSecretKey, EphemeralPublicKey, SharedSecret}` + - `keys::{PreparedIncomingViewingKey, PreparedEphemeralPublicKey}` + (re-exported from `note_encryption`). + - `note`, a module containing types related to Sapling notes. The existing + `Note` and `Rseed` types are re-exported here, and new types are added. + - `Node::from_cmu` + - `value`, containing types for handling Sapling note values and value + commitments. + - `Note::from_parts` + - `Note::{recipient, value, rseed}` getter methods. + - `impl Eq for Note` + - `impl Copy for PaymentAddress` + ### Changed -- MSRV is now 1.51.0. +- MSRV is now 1.60.0. +- `zcash_primitives::transaction::components::sapling::builder`: + - `SaplingBuilder::add_output` now takes a + `zcash_primitives::sapling::value::NoteValue`. +- `zcash_primitives::sapling`: + - `PaymentAddress::from_parts` now rejects invalid diversifiers. + - `PaymentAddress::create_note` is now infallible. + - `DiversifiedTransmissionKey` is now used instead of `jubjub::SubgroupPoint` + in the following places: + - `PaymentAddress::from_parts` + - `PaymentAddress::pk_d` + - `note_encryption::SaplingDomain::DiversifiedTransmissionKey` + - `EphemeralSecretKey` is now used instead of `jubjub::Scalar` in the + following places: + - `Note::generate_or_derive_esk` + - `note_encryption::SaplingDomain::EphemeralSecretKey` + - `note_encryption::SaplingDomain::EphemeralPublicKey` is now + `EphemeralPublicKey` instead of `jubjub::ExtendedPoint`. + - `note_encryption::SaplingDomain::SharedSecret` is now `SharedSecret` instead + of `jubjub::SubgroupPoint`. +- Note commitments now use + `zcash_primitives::sapling::note::ExtractedNoteCommitment` instead of + `bls12_381::Scalar` in the following places: + - `zcash_primitives::sapling`: + - `Note::cmu` + - `zcash_primitives::sapling::note_encryption`: + - `SaplingDomain::ExtractedCommitment` + - `zcash_primitives::transaction::components::sapling`: + - `OutputDescription::cmu` + - The `cmu` field of `CompactOutputDescription`. +- Value commitments now use `zcash_primitives::sapling::value::ValueCommitment` + instead of `jubjub::ExtendedPoint` in the following places: + - `zcash_primitives::sapling::note_encryption`: + - `prf_ock` + - `SaplingDomain::ValueCommitment` + - `zcash_primitives::sapling::prover`: + - `TxProver::{spend_proof, output_proof}` return type. + - `zcash_primitives::transaction::components`: + - `SpendDescription::cv` + - `OutputDescription::cv` +- `zcash_primitives::transaction::components`: + - `sapling::{Bundle, SpendDescription, OutputDescription}` have had their + fields replaced by getter methods. + - The associated type `sapling::Authorization::Proof` has been replaced by + `Authorization::{SpendProof, OutputProof}`. + - `sapling::MapAuth::map_proof` has been replaced by + `MapAuth::{map_spend_proof, map_output_proof}`. + +### Removed +- `zcash_primitives::sapling`: + - The fields of `Note` are now private (use the new getter methods instead). + - `Note::uncommitted` (use `Node::empty_leaf` instead). + - `Note::derive_esk` (use `SaplingDomain::derive_esk` instead). + - `Note::commitment` (use `Node::from_cmu` instead). + - `PaymentAddress::g_d` + - `NoteValue` (use `zcash_primitives::sapling::value::NoteValue` instead). + - `ValueCommitment` (use `zcash_primitives::sapling::value::ValueCommitment` + or `zcash_proofs::circuit::sapling::ValueCommitmentPreimage` instead). + - `note_encryption::sapling_ka_agree` + - `testing::{arb_note_value, arb_positive_note_value}` (use the methods in + `zcash_primitives::sapling::value::testing` instead). +- `zcash_primitives::transaction::components`: + - The fields of `sapling::{SpendDescriptionV5, OutputDescriptionV5}` (they are + now opaque types; use `sapling::{SpendDescription, OutputDescription}` + instead). + - `sapling::read_point` + +## [0.9.1] - 2022-12-06 +### Fixed +- `zcash_primitives::transaction::builder`: + - `Builder::build` was calling `FeeRule::fee_required` with the number of + Sapling outputs that have been added to the builder. It now instead provides + the number of outputs that will be in the final Sapling bundle, including + any padding. + +## [0.9.0] - 2022-11-12 +### Added +- Added to `zcash_primitives::transaction::builder`: + - `Error::{InsufficientFunds, ChangeRequired, Balance, Fee}` + - `Builder` state accessor methods: + - `Builder::{params, target_height}` + - `Builder::{transparent_inputs, transparent_outputs}` + - `Builder::{sapling_inputs, sapling_outputs}` +- `zcash_primitives::transaction::fees`, a new module containing abstractions + and types related to fee calculations. + - `FeeRule`, a trait that describes how to compute the fee required for a + transaction given inputs and outputs to the transaction. + - `fixed`, a module containing an implementation of the old fixed fee rule. + - `zip317`, a module containing an implementation of the ZIP 317 fee rules. +- Added to `zcash_primitives::transaction::components`: + - `amount::NonNegativeAmount` + - Added to the `orchard` module: + - `impl MapAuth for ()` + (the identity map). + - Added to the `sapling` module: + - `impl MapAuth for ()` (the identity map). + - `builder::SaplingBuilder::{inputs, outputs}`: accessors for Sapling + builder state. + - `fees`, a module with Sapling-specific fee traits. + - Added to the `transparent` module: + - `impl {PartialOrd, Ord} for OutPoint` + - `builder::TransparentBuilder::{inputs, outputs}`: accessors for + transparent builder state. + - `fees`, a module with transparent-specific fee traits. +- Added to `zcash_primitives::sapling`: + - `Note::commitment` + - `impl Eq for PaymentAddress` +- Added to `zcash_primitives::zip32`: + - `impl TryFrom for u32` + - `sapling::DiversifiableFullViewingKey::{diversified_address, diversified_change_address}` + +### Changed +- `zcash_primitives::transaction::builder`: + - `Builder::build` now takes a `FeeRule` argument which is used to compute the + fee for the transaction as part of the build process. + - `Builder::value_balance` now returns `Result` instead + of `Option`. + - `Builder::{new, new_with_rng}` no longer fixes the fee for transactions to + 0.00001 ZEC; the builder instead computes the fee using a `FeeRule` + implementation at build time. + - `Error` now is parameterized by the types that can now be produced by fee + calculation. +- `zcash_primitives::transaction::components::tze::builder::Builder::value_balance` now + returns `Result` instead of `Option`. + +### Deprecated +- `zcash_primitives::zip32::sapling::to_extended_full_viewing_key` (use + `to_diversifiable_full_viewing_key` instead). + +### Removed +- Removed from `zcash_primitives::transaction::builder`: + - `Builder::{new_with_fee, new_with_rng_and_fee`} (use `Builder::{new, new_with_rng}` + instead along with a `FeeRule` implementation passed to `Builder::build`.) + - `Builder::send_change_to` (change outputs must be added to the builder by + the caller, just like any other output). + - `Error::ChangeIsNegative` + - `Error::NoChangeAddress` + - `Error::InvalidAmount` (replaced by `Error::BalanceError`). +- Removed from `zcash_primitives::transaction::components::sapling::builder`: + - `SaplingBuilder::get_candidate_change_address` (change outputs must now be + added by the caller). +- Removed from `zcash_primitives::zip32::sapling`: + - `impl From<&ExtendedSpendingKey> for ExtendedFullViewingKey` (use + `ExtendedSpendingKey::to_diversifiable_full_viewing_key` instead). +- `zcash_primitives::sapling::Node::new` (use `Node::from_scalar` or preferably + `Note::commitment` instead). + +## [0.8.1] - 2022-10-19 +### Added +- `zcash_primitives::legacy`: + - `impl {Copy, Eq, Ord} for TransparentAddress` + - `keys::AccountPrivKey::{to_bytes, from_bytes}` +- `zcash_primitives::sapling::NullifierDerivingKey` +- Added in `zcash_primitives::sapling::keys` + - `DecodingError` + - `Scope` + - `ExpandedSpendingKey::from_bytes` + - `ExtendedSpendingKey::{from_bytes, to_bytes}` +- `zcash_primitives::sapling::note_encryption`: + - `PreparedIncomingViewingKey` + - `PreparedEphemeralPublicKey` +- Added in `zcash_primitives::zip32` + - `ChainCode::as_bytes` + - `DiversifierIndex::{as_bytes}` + - Implementations of `From` and `From` for `DiversifierIndex` +- `zcash_primitives::zip32::sapling` has been added and now contains + all of the Sapling zip32 key types that were previously located in + `zcash_primitives::zip32` directly. The base `zip32` module reexports + the moved types for backwards compatibility. + - `DiversifierKey::{from_bytes, as_bytes}` + - `ExtendedSpendingKey::{from_bytes, to_bytes}` +- `zcash_primitives::transaction::Builder` constructors: + - `Builder::new_with_fee` + - `Builder::new_with_rng_and_fee` +- `zcash_primitives::transaction::TransactionData::fee_paid` +- `zcash_primitives::transaction::components::amount::BalanceError` +- Added in `zcash_primitives::transaction::components::sprout` + - `Bundle::value_balance` + - `JSDescription::net_value` +- Added in `zcash_primitives::transaction::components::transparent` + - `Bundle::value_balance` + - `TxOut::recipient_address` +- Implementations of `memuse::DynamicUsage` for the following types: + - `zcash_primitives::block::BlockHash` + - `zcash_primitives::consensus`: + - `BlockHeight` + - `MainNetwork`, `TestNetwork`, `Network` + - `NetworkUpgrade`, `BranchId` + - `zcash_primitives::sapling`: + - `keys::Scope` + - `note_encryption::SaplingDomain` + - `zcash_primitives::transaction`: + - `TxId` + - `components::sapling::CompactOutputDescription` + - `components::sapling::{OutputDescription, OutputDescriptionV5}` + - `zcash_primitives::zip32::AccountId` + +### Changed +- Migrated to `group 0.13`, `orchard 0.3`, `zcash_address 0.2`, `zcash_encoding 0.2`. +- `zcash_primitives::sapling::ViewingKey` now stores `nk` as a + `NullifierDerivingKey` instead of as a bare `jubjub::SubgroupPoint`. +- The signature of `zcash_primitives::sapling::Note::nf` has changed to + take just a `NullifierDerivingKey` (the only capability it actually required) + rather than the full `ViewingKey` as its first argument. +- Made the internals of `zip32::DiversifierKey` private; use `from_bytes` and + `as_bytes` on this type instead. +- `zcash_primitives::sapling::note_encryption` APIs now expose precomputations + explicitly (where previously they were performed internally), to enable users + to avoid recomputing incoming viewing key precomputations. Users now need to + call `PreparedIncomingViewingKey::new` to convert their `SaplingIvk`s into + their precomputed forms, and can do so wherever it makes sense in their stack. + - `SaplingDomain::IncomingViewingKey` is now `PreparedIncomingViewingKey` + instead of `SaplingIvk`. + - `try_sapling_note_decryption` and `try_sapling_compact_note_decryption` now + take `&PreparedIncomingViewingKey` instead of `&SaplingIvk`. + +### Removed +- `zcash_primitives::legacy::Script::address` This method was not generally + safe to use on arbitrary scripts, only on script_pubkey values. Its + functionality is now available via + `zcash_primitives::transaction::components::transparent::TxOut::recipient_address` + +## [0.8.0] - 2022-10-19 +This release was yanked because it depended on the wrong versions of `zcash_address` +and `zcash_encoding`. + +## [0.7.0] - 2022-06-24 +### Changed +- Bumped dependencies to `equihash 0.2`, `orchard 0.2`. +- `zcash_primitives::consensus`: + - `MainNetwork::activation_height` now returns the activation height for + `NetworkUpgrade::Nu5`. + +## [0.6.0] - 2022-05-11 +### Added +- `zcash_primitives::sapling::redjubjub::PublicKey::verify_with_zip216`, for + controlling how RedJubjub signatures are validated. `PublicKey::verify` has + been altered to always use post-ZIP 216 validation rules. +- `zcash_primitives::transaction::Builder::with_progress_notifier`, for setting + a notification channel on which transaction build progress updates will be + sent. +- `zcash_primitives::transaction::Txid::{read, write, from_bytes}` +- `zcash_primitives::sapling::NoteValue` a typesafe wrapper for Sapling note values. +- `zcash_primitives::consensus::BranchId::{height_range, height_bounds}` functions + to provide range values for branch active heights. +- `zcash_primitives::consensus::NetworkUpgrade::Nu5` value representing the Nu5 upgrade. +- `zcash_primitives::consensus::BranchId::Nu5` value representing the Nu5 consensus branch. +- New modules under `zcash_primitives::transaction::components` for building parts of + transactions: + - `sapling::builder` for Sapling transaction components. + - `transparent::builder` for transparent transaction components. + - `tze::builder` for TZE transaction components. + - `orchard` parsing and serialization for Orchard transaction components. +- `zcash_primitives::transaction::Authorization` a trait representing a type-level + record of authorization types that correspond to signatures, witnesses, and + proofs for each Zcash sub-protocol (transparent, Sprout, Sapling, TZE, and + Orchard). This type makes it possible to encode a type-safe state machine + for the application of authorizing data to a transaction; implementations of + this trait represent different states of the authorization process. +- New bundle types under the `zcash_primitives::transaction` submodules, one for + each Zcash sub-protocol. These are now used instead of bare fields + within the `TransactionData` type. + - `components::sapling::Bundle` bundle of + Sapling transaction elements. This new struct is parameterized by a + type bounded on a newly added `sapling::Authorization` trait which + is used to enable static reasoning about the state of Sapling proofs and + authorizing data, as described above. + - `components::transparent::Bundle` bundle of + transparent transaction elements. This new struct is parameterized by a + type bounded on a newly added `transparent::Authorization` trait which + is used to enable static reasoning about the state of transparent witness + data, as described above. + - `components::tze::Bundle` bundle of TZE + transaction elements. This new struct is parameterized by a + type bounded on a newly added `tze::Authorization` trait which + is used to enable static reasoning about the state of TZE witness + data, as described above. +- `zcash_primitives::serialize` has been factored out as a new `zcash_encoding` + crate, which can be found in the `components` directory. +- `zcash_primitives::transaction::components::Amount` now implements + `memuse::DynamicUsage`, to enable `orchard::Bundle<_, Amount>::dynamic_usage`. +- `zcash_primitives::zip32::diversifier` has been renamed to `find_sapling_diversifier` + and `sapling_diversifier` has been added. `find_sapling_diversifier` searches the + diversifier index space, whereas `sapling_diversifier` just attempts to use the + provided diversifier index and returns `None` if it does not produce a valid + diversifier. +- `zcash_primitives::zip32::DiversifierKey::diversifier` has been renamed to + `find_diversifier` and the `diversifier` method has new semantics. + `find_diversifier` searches the diversifier index space to find a diversifier + index which produces a valid diversifier, whereas `diversifier` just attempts + to use the provided diversifier index and returns `None` if it does not + produce a valid diversifier. +- `zcash_primitives::zip32::ExtendedFullViewingKey::address` has been renamed + to `find_address` and the `address` method has new semantics. `find_address` + searches the diversifier index space until it obtains a valid diversifier, + and returns the address corresponding to that diversifier, whereas `address` + just attempts to create an address corresponding to the diversifier derived + from the provided diversifier index and returns `None` if the provided index + does not produce a valid diversifier. +- `zcash_primitives::zip32::ExtendedSpendingKey.derive_internal` has been + added to facilitate the derivation of an internal (change) spending key. + This spending key can be used to spend change sent to an internal address + corresponding to the associated full viewing key as specified in + [ZIP 316](https://zips.z.cash/zip-0316#encoding-of-unified-full-incoming-viewing-keys).. +- `zcash_primitives::zip32::ExtendedFullViewingKey.derive_internal` has been + added to facilitate the derivation of an internal (change) spending key. + This spending key can be used to spend change sent to an internal address + corresponding to the associated full viewing key as specified in + [ZIP 32](https://zips.z.cash/zip-0032#deriving-a-sapling-internal-spending-key). +- `zcash_primitives::zip32::sapling_derive_internal_fvk` provides the + internal implementation of `ExtendedFullViewingKey.derive_internal` but does + not require a complete extended full viewing key, just the full viewing key + and the diversifier key. In the future, this function will likely be + refactored to become a member function of a new `DiversifiableFullViewingKey` + type, which represents the ability to derive IVKs, OVKs, and addresses, but + not child viewing keys. +- `zcash_primitives::sapling::keys::DiversifiableFullViewingKey::change_address` + has been added as a convenience method for obtaining the change address + at the default diversifier. This address **MUST NOT** be encoded and exposed + to users. User interfaces should instead mark these notes as "change notes" or + "internal wallet operations". +- A new module `zcash_primitives::legacy::keys` has been added under the + `transparent-inputs` feature flag to support types related to supporting + transparent components of unified addresses and derivation of OVKs for + shielding funds from the transparent pool. +- A `zcash_primitives::transaction::components::amount::Amount::sum` + convenience method has been added to facilitate bounds-checked summation of + account values. +- The `zcash_primitives::zip32::AccountId`, a type-safe wrapper for ZIP 32 + account indices. +- In `zcash_primitives::transaction::components::amount`: + - `impl Sum<&Amount> for Option` + +### Changed +- MSRV is now 1.56.1. +- Bumped dependencies to `ff 0.12`, `group 0.12`, `bls12_381 0.7`, `jubjub 0.9`, + `bitvec 1`. - The following modules and helpers have been moved into `zcash_primitives::sapling`: - `zcash_primitives::group_hash` - `zcash_primitives::keys` + - `zcash_primitives::sapling::keys::{prf_expand, prf_expand_vec, OutgoingViewingKey}` + have all been moved into to the this module to reflect the fact that they + are used outside of the Sapling protocol. - `zcash_primitives::pedersen_hash` - `zcash_primitives::primitives::*` (moved into `zcash_primitives::sapling`) - `zcash_primitives::prover` @@ -19,6 +940,47 @@ and this library adheres to Rust's notion of - `zcash_primitives::util::{hash_to_scalar, generate_random_rseed}` - Renamed `zcash_primitives::transaction::components::JSDescription` to `JsDescription` (matching Rust naming conventions). +- `zcash_primitives::transaction::TxId` contents is now private. +- Renamed `zcash_primitives::transaction::components::tze::hash` to + `zcash_primitives::transaction::components::tze::txid` +- `zcash_primitives::transaction::components::tze::TzeOutPoint` constructor + now taxes a TxId rather than a raw byte array. +- `zcash_primitives::transaction::components::Amount` addition, subtraction, + and summation now return `Option` rather than panicing on overflow. +- `zcash_primitives::transaction::builder`: + - `Error` has been modified to wrap the error types produced by its child + builders. + - `Builder::build` no longer takes a consensus branch ID parameter. The + builder now selects the correct consensus branch ID for the given target + height. +- The `zcash_primitives::transaction::TransactionData` struct has been modified + such that it now contains common header information, and then contains + a separate `Bundle` value for each sub-protocol (transparent, Sprout, Sapling, + and TZE) and an Orchard bundle value has been added. `TransactionData` is now + parameterized by a type bounded on the newly added + `zcash_primitives::transaction::Authorization` trait. This bound has been + propagated to the individual transaction builders, such that the authorization + state of a transaction is clearly represented in the type and the presence + or absence of witness and/or proof data is statically known, instead of being only + determined at runtime via the presence or absence of `Option`al values. +- `zcash_primitives::transaction::components::sapling` parsing and serialization + have been adapted for use with the new `sapling::Bundle` type. +- `zcash_primitives::transaction::Transaction` parsing and serialization + have been adapted for use with the new `TransactionData` organization. +- Generators for property testing have been moved out of the main transaction + module such that they are now colocated in the modules with the types + that they generate. +- The `ephemeral_key` field of `OutputDescription` has had its type changed from + `jubjub::ExtendedPoint` to `zcash_note_encryption::EphemeralKeyBytes`. +- The `epk: jubjub::ExtendedPoint` field of `CompactOutputDescription ` has been + replaced by `ephemeral_key: zcash_note_encryption::EphemeralKeyBytes`. +- The `zcash_primitives::transaction::Builder::add_sapling_output` method + now takes its `MemoBytes` argument as a required field rather than an + optional one. If the empty memo is desired, use + `MemoBytes::from(Memo::Empty)` explicitly. +- `zcash_primitives::zip32`: + - `ExtendedSpendingKey::default_address` no longer returns `Option<_>`. + - `ExtendedFullViewingKey::default_address` no longer returns `Option<_>`. ## [0.5.0] - 2021-03-26 ### Added @@ -121,7 +1083,7 @@ and this library adheres to Rust's notion of - `try_sapling_output_recovery` - `try_sapling_output_recovery_with_ock` - `zcash_primitives::primitives::SaplingIvk` is now used where functions - previously used undistinguished `jubjub::Fr` values; this affects Sapling + previously used undistinguished `jubjub::Fr` values; this affects Sapling note decryption and handling of IVKs by the wallet backend code. - `zcash_primitives::primitives::ViewingKey::ivk` now returns `SaplingIvk` - `zcash_primitives::primitives::Note::nf` now returns `Nullifier`. diff --git a/zcash_primitives/Cargo.toml b/zcash_primitives/Cargo.toml index a950331cff..229360eea6 100644 --- a/zcash_primitives/Cargo.toml +++ b/zcash_primitives/Cargo.toml @@ -1,66 +1,161 @@ [package] name = "zcash_primitives" description = "Rust implementations of the Zcash primitives" -version = "0.5.0" +version = "0.21.0" authors = [ "Jack Grigg ", "Kris Nuttycombe " ] homepage = "https://github.com/zcash/librustzcash" -repository = "https://github.com/zcash/librustzcash" +repository.workspace = true readme = "README.md" -license = "MIT OR Apache-2.0" -edition = "2018" +license.workspace = true +edition.workspace = true +rust-version.workspace = true +categories.workspace = true [package.metadata.docs.rs] all-features = true +rustdoc-args = ["--cfg", "docsrs"] [dependencies] -aes = "0.6" -bitvec = "0.18" -blake2b_simd = "0.5" -blake2s_simd = "0.5" -bls12_381 = "0.3.1" -byteorder = "1" -crypto_api_chachapoly = "0.4" -equihash = { version = "0.1", path = "../components/equihash" } -ff = "0.8" -fpe = "0.4" -group = "0.8" -hex = "0.4" -jubjub = "0.5.1" -lazy_static = "1" -log = "0.4" -proptest = { version = "0.10.1", optional = true } -rand = "0.7" -rand_core = "0.5.1" -ripemd160 = { version = "0.9", optional = true } -secp256k1 = { version = "0.20", optional = true } -sha2 = "0.9" -subtle = "2.2.3" -zcash_note_encryption = { version = "0.0", path = "../components/zcash_note_encryption" } - -# Temporary workaround for https://github.com/myrrlyn/funty/issues/3 -funty = "=1.1.0" +equihash.workspace = true +zcash_address.workspace = true +zcash_encoding.workspace = true +zcash_protocol.workspace = true +zip32.workspace = true + +# Dependencies exposed in a public API: +# (Breaking upgrades to these require a breaking upgrade to this crate.) +# - CSPRNG +rand.workspace = true +rand_core.workspace = true + +# - Digests (output types exposed) +blake2b_simd.workspace = true +sha2.workspace = true + +# - Logging and metrics +memuse.workspace = true +tracing = { workspace = true, default-features = false } + +# - Secret management +subtle.workspace = true + +# - Shielded protocols +ff.workspace = true +group.workspace = true +jubjub.workspace = true +nonempty.workspace = true +orchard.workspace = true +sapling.workspace = true +zcash_spec.workspace = true + +# - Note Commitment Trees +incrementalmerkletree = { workspace = true, features = ["legacy-api"] } + +# - Test dependencies +proptest = { workspace = true, optional = true } + +# - Transparent protocol +transparent.workspace = true + +# - Transparent inputs +# - `Error` type exposed +bip32.workspace = true +# - `SecretKey` and `PublicKey` types exposed +secp256k1 = { workspace = true, optional = true } + +# Dependencies used internally: +# (Breaking upgrades to these are usually backwards-compatible, but check MSRVs.) +# - Boilerplate +getset.workspace = true + +# - Documentation +document-features = { workspace = true, optional = true } + +# - Encodings +bs58.workspace = true +hex.workspace = true + +# - Shielded protocols +redjubjub.workspace = true + +# - Transparent protocol +ripemd.workspace = true + +# - ZIP 32 +fpe.workspace = true + +# No-std support +core2.workspace = true + +[dependencies.zcash_note_encryption] +workspace = true +features = ["pre-zip-212"] [dev-dependencies] -criterion = "0.3" -hex-literal = "0.3" -proptest = "0.10.1" -rand_xorshift = "0.2" +chacha20poly1305 = "0.10" +criterion.workspace = true +incrementalmerkletree = { workspace = true, features = ["legacy-api", "test-dependencies"] } +proptest.workspace = true +assert_matches.workspace = true +rand_xorshift.workspace = true +transparent = { workspace = true, features = ["test-dependencies"] } +sapling = { workspace = true, features = ["test-dependencies"] } +orchard = { workspace = true, features = ["test-dependencies"] } +zcash_protocol = { workspace = true, features = ["test-dependencies"] } + +[target.'cfg(unix)'.dev-dependencies] +pprof = { version = "0.13", features = ["criterion", "flamegraph"] } [features] -transparent-inputs = ["ripemd160", "secp256k1"] -test-dependencies = ["proptest"] -zfuture = [] +default = ["multicore", "std", "circuits"] +std = ["document-features", "redjubjub/std"] -[[bench]] -name = "note_decryption" -harness = false +## Enables creating proofs +circuits = ["orchard/circuit", "sapling/circuit"] + +## Enables multithreading support for creating proofs. +multicore = ["orchard/multicore", "sapling/multicore"] + +## Enables spending transparent notes with the transaction builder. +transparent-inputs = [ + "transparent/transparent-inputs", + "bip32/secp256k1-ffi", + "dep:secp256k1", +] + +### A temporary feature flag that exposes granular APIs needed by `zcashd`. These APIs +### should not be relied upon and will be removed in a future release. +temporary-zcashd = [] + +## Exposes APIs that are useful for testing, such as `proptest` strategies. +test-dependencies = [ + "dep:proptest", + "incrementalmerkletree/test-dependencies", + "orchard/test-dependencies", + "sapling/test-dependencies", + "transparent/test-dependencies", + "zcash_protocol/test-dependencies", +] + +## A feature used to isolate tests that are expensive to run. Test-only. +expensive-tests = [] + +## A feature that provides `FeeRule` implementations and constructors for +## non-standard fees. +non-standard-fees = [] + +[lib] +bench = false [[bench]] -name = "pedersen_hash" +name = "note_decryption" harness = false [badges] maintenance = { status = "actively-developed" } + +[lints] +workspace = true diff --git a/zcash_primitives/benches/note_decryption.rs b/zcash_primitives/benches/note_decryption.rs index 6200d52028..9ea3504a2e 100644 --- a/zcash_primitives/benches/note_decryption.rs +++ b/zcash_primitives/benches/note_decryption.rs @@ -1,69 +1,135 @@ -use criterion::{criterion_group, criterion_main, Criterion}; +use core::iter; + +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; use ff::Field; use rand_core::OsRng; -use zcash_primitives::{ - consensus::{NetworkUpgrade::Canopy, Parameters, TestNetwork, TEST_NETWORK}, - memo::MemoBytes, - sapling::{ - note_encryption::{sapling_note_encryption, try_sapling_note_decryption}, - util::generate_random_rseed, - Diversifier, PaymentAddress, SaplingIvk, ValueCommitment, +use sapling::{ + self, + note_encryption::{ + try_sapling_compact_note_decryption, try_sapling_note_decryption, CompactOutputDescription, + PreparedIncomingViewingKey, SaplingDomain, }, - transaction::components::{OutputDescription, GROTH_PROOF_SIZE}, + prover::mock::{MockOutputProver, MockSpendProver}, + value::NoteValue, + Diversifier, SaplingIvk, +}; +use zcash_note_encryption::batch; +use zcash_primitives::transaction::components::sapling::zip212_enforcement; +use zcash_protocol::{ + consensus::{NetworkUpgrade::Canopy, Parameters, TEST_NETWORK}, + value::ZatBalance, }; +#[cfg(unix)] +use pprof::criterion::{Output, PProfProfiler}; + fn bench_note_decryption(c: &mut Criterion) { let mut rng = OsRng; let height = TEST_NETWORK.activation_height(Canopy).unwrap(); + let zip212_enforcement = zip212_enforcement(&TEST_NETWORK, height); let valid_ivk = SaplingIvk(jubjub::Fr::random(&mut rng)); let invalid_ivk = SaplingIvk(jubjub::Fr::random(&mut rng)); - // Construct a fake Sapling output as if we had just deserialized a transaction. + // Construct a Sapling output. let output = { let diversifier = Diversifier([0; 11]); - let pk_d = diversifier.g_d().unwrap() * valid_ivk.0; - let pa = PaymentAddress::from_parts(diversifier, pk_d).unwrap(); - - let rseed = generate_random_rseed(&TEST_NETWORK, height, &mut rng); - - // Construct the value commitment for the proof instance - let value = 100; - let value_commitment = ValueCommitment { - value, - randomness: jubjub::Fr::random(&mut rng), - }; - let cv = value_commitment.commitment().into(); - - let note = pa.create_note(value, rseed).unwrap(); - let cmu = note.cmu(); - - let ne = - sapling_note_encryption::<_, TestNetwork>(None, note, pa, MemoBytes::empty(), &mut rng); - let ephemeral_key = *ne.epk(); - let enc_ciphertext = ne.encrypt_note_plaintext(); - let out_ciphertext = ne.encrypt_outgoing_plaintext(&cv, &cmu, &mut rng); - - OutputDescription { - cv, - cmu, - ephemeral_key, - enc_ciphertext, - out_ciphertext, - zkproof: [0; GROTH_PROOF_SIZE], - } + let pa = valid_ivk.to_payment_address(diversifier).unwrap(); + + let mut builder = sapling::builder::Builder::new( + zip212_enforcement, + // We use the Coinbase bundle type because we don't need to use + // any inputs for this benchmark. + sapling::builder::BundleType::Coinbase, + sapling::Anchor::empty_tree(), + ); + builder + .add_output(None, pa, NoteValue::from_raw(100), None) + .unwrap(); + let (bundle, _) = builder + .build::(&[], &mut rng) + .unwrap() + .unwrap(); + bundle.shielded_outputs()[0].clone() }; - let mut group = c.benchmark_group("Sapling note decryption"); + let valid_ivk = PreparedIncomingViewingKey::new(&valid_ivk); + let invalid_ivk = PreparedIncomingViewingKey::new(&invalid_ivk); + + { + let mut group = c.benchmark_group("sapling-note-decryption"); + group.throughput(Throughput::Elements(1)); + + group.bench_function("valid", |b| { + b.iter(|| try_sapling_note_decryption(&valid_ivk, &output, zip212_enforcement).unwrap()) + }); + + group.bench_function("invalid", |b| { + b.iter(|| try_sapling_note_decryption(&invalid_ivk, &output, zip212_enforcement)) + }); + + let compact = CompactOutputDescription::from(output.clone()); + + group.bench_function("compact-valid", |b| { + b.iter(|| { + try_sapling_compact_note_decryption(&valid_ivk, &compact, zip212_enforcement) + .unwrap() + }) + }); - group.bench_function("valid", |b| { - b.iter(|| try_sapling_note_decryption(&TEST_NETWORK, height, &valid_ivk, &output).unwrap()) - }); + group.bench_function("compact-invalid", |b| { + b.iter(|| { + try_sapling_compact_note_decryption(&invalid_ivk, &compact, zip212_enforcement) + }) + }); + } - group.bench_function("invalid", |b| { - b.iter(|| try_sapling_note_decryption(&TEST_NETWORK, height, &invalid_ivk, &output)) - }); + { + let mut group = c.benchmark_group("sapling-batch-note-decryption"); + + for (nivks, noutputs) in [(1, 10), (10, 1), (10, 10), (50, 50)] { + let invalid_ivks: Vec<_> = iter::repeat(invalid_ivk.clone()).take(nivks).collect(); + let valid_ivks: Vec<_> = iter::repeat(valid_ivk.clone()).take(nivks).collect(); + + let outputs: Vec<_> = iter::repeat(output.clone()) + .take(noutputs) + .map(|output| (SaplingDomain::new(zip212_enforcement), output)) + .collect(); + + group.bench_function( + BenchmarkId::new(format!("valid-{}", nivks), noutputs), + |b| b.iter(|| batch::try_note_decryption(&valid_ivks, &outputs)), + ); + + group.bench_function( + BenchmarkId::new(format!("invalid-{}", nivks), noutputs), + |b| b.iter(|| batch::try_note_decryption(&invalid_ivks, &outputs)), + ); + + let compact: Vec<_> = outputs + .into_iter() + .map(|(domain, output)| (domain, CompactOutputDescription::from(output))) + .collect(); + + group.bench_function( + BenchmarkId::new(format!("compact-valid-{}", nivks), noutputs), + |b| b.iter(|| batch::try_compact_note_decryption(&valid_ivks, &compact)), + ); + + group.bench_function( + BenchmarkId::new(format!("compact-invalid-{}", nivks), noutputs), + |b| b.iter(|| batch::try_compact_note_decryption(&invalid_ivks, &compact)), + ); + } + } } +#[cfg(unix)] +criterion_group! { + name = benches; + config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flamegraph(None))); + targets = bench_note_decryption +} +#[cfg(not(unix))] criterion_group!(benches, bench_note_decryption); criterion_main!(benches); diff --git a/zcash_primitives/benches/pedersen_hash.rs b/zcash_primitives/benches/pedersen_hash.rs deleted file mode 100644 index 38e4bafed1..0000000000 --- a/zcash_primitives/benches/pedersen_hash.rs +++ /dev/null @@ -1,18 +0,0 @@ -use criterion::{criterion_group, criterion_main, Criterion}; -use rand_core::{OsRng, RngCore}; -use zcash_primitives::sapling::pedersen_hash::{pedersen_hash, Personalization}; - -fn bench_pedersen_hash(c: &mut Criterion) { - let rng = &mut OsRng; - let bits = (0..510) - .map(|_| (rng.next_u32() % 2) != 0) - .collect::>(); - let personalization = Personalization::MerkleTree(31); - - c.bench_function("Pedersen hash", |b| { - b.iter(|| pedersen_hash(personalization, bits.clone())) - }); -} - -criterion_group!(benches, bench_pedersen_hash); -criterion_main!(benches); diff --git a/zcash_primitives/proptest-regressions/merkle_tree.txt b/zcash_primitives/proptest-regressions/merkle_tree.txt new file mode 100644 index 0000000000..d7e77e33c4 --- /dev/null +++ b/zcash_primitives/proptest-regressions/merkle_tree.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc ad8649940f45fef5aad3355d7ca193e7d018285b04f9d976197ff8e1fec8687a # shrinks to ct = CommitmentTree { left: Some(Node { repr: [31, 136, 59, 216, 239, 236, 65, 132, 70, 226, 212, 35, 165, 194, 10, 57, 188, 94, 141, 22, 236, 68, 38, 57, 219, 28, 160, 173, 156, 62, 220, 34] }), right: Some(Node { repr: [117, 7, 10, 72, 207, 40, 193, 79, 126, 156, 158, 193, 63, 190, 49, 179, 227, 188, 144, 85, 30, 143, 186, 203, 154, 76, 5, 113, 42, 16, 60, 41] }), parents: [Some(Node { repr: [92, 253, 34, 29, 13, 202, 250, 201, 54, 210, 88, 207, 183, 117, 166, 126, 63, 101, 34, 44, 119, 88, 122, 240, 51, 77, 53, 148, 190, 141, 226, 103] }), Some(Node { repr: [91, 140, 162, 31, 9, 135, 96, 209, 170, 45, 143, 251, 108, 37, 94, 84, 95, 22, 28, 109, 140, 61, 249, 41, 220, 207, 149, 136, 93, 220, 161, 87] }), Some(Node { repr: [21, 173, 87, 125, 78, 242, 185, 197, 174, 47, 114, 229, 189, 35, 154, 221, 164, 232, 181, 26, 232, 216, 228, 244, 81, 127, 222, 10, 22, 241, 134, 106] }), None, Some(Node { repr: [88, 145, 84, 46, 146, 135, 252, 5, 21, 50, 137, 85, 36, 136, 101, 55, 153, 134, 71, 41, 95, 73, 17, 151, 170, 141, 195, 95, 11, 204, 181, 85] }), None, None] } diff --git a/zcash_primitives/proptest-regressions/merkle_tree/incremental.txt b/zcash_primitives/proptest-regressions/merkle_tree/incremental.txt new file mode 100644 index 0000000000..e1ff91c834 --- /dev/null +++ b/zcash_primitives/proptest-regressions/merkle_tree/incremental.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc f6df6e3a7a1641029b9f39a671046ba39745ded73de8d7444e7c27a8f73e1365 # shrinks to t = CommitmentTree { left: Some(Node { repr: [36, 96, 18, 1, 228, 118, 68, 158, 142, 67, 253, 219, 85, 192, 179, 142, 230, 218, 145, 73, 159, 211, 208, 58, 182, 136, 108, 95, 137, 166, 232, 10] }), right: Some(Node { repr: [10, 211, 222, 223, 94, 55, 180, 62, 79, 50, 38, 55, 73, 152, 245, 181, 157, 40, 89, 177, 51, 96, 154, 78, 185, 74, 118, 11, 54, 188, 151, 181] }), parents: [None, None, Some(Node { repr: [99, 240, 35, 62, 160, 23, 150, 46, 3, 226, 153, 214, 59, 25, 19, 85, 247, 234, 174, 75, 93, 165, 99, 116, 194, 243, 103, 155, 166, 131, 10, 68] }), Some(Node { repr: [106, 249, 220, 118, 49, 239, 102, 59, 121, 101, 110, 82, 194, 242, 72, 24, 209, 160, 24, 225, 124, 138, 138, 52, 157, 6, 43, 180, 212, 8, 117, 3] })] } diff --git a/zcash_primitives/proptest-regressions/transaction/tests.txt b/zcash_primitives/proptest-regressions/transaction/tests.txt index 9f76d44639..341ba3ff3d 100644 --- a/zcash_primitives/proptest-regressions/transaction/tests.txt +++ b/zcash_primitives/proptest-regressions/transaction/tests.txt @@ -4,4 +4,3 @@ # # It is recommended to check this file in to source control so that # everyone who runs the test benefits from these saved cases. -cc 23a823e9da7e6ae62a79153ab97362dd9d81b8d9eafc396c87870dfa8aa7354c # shrinks to tx = Transaction { txid: TxId([67, 236, 122, 87, 159, 85, 97, 164, 42, 126, 150, 55, 173, 65, 86, 103, 39, 53, 166, 88, 190, 39, 82, 24, 24, 1, 247, 35, 186, 51, 22, 210]), data: TransactionData( version = Sprout(1), vin = [], vout = [], lock_time = 0, expiry_height = BlockHeight(0), value_balance = Amount(1), shielded_spends = [], shielded_outputs = [], joinsplits = [], joinsplit_pubkey = None, binding_sig = None) } diff --git a/zcash_primitives/src/block.rs b/zcash_primitives/src/block.rs index 5af523e022..9748941f00 100644 --- a/zcash_primitives/src/block.rs +++ b/zcash_primitives/src/block.rs @@ -1,18 +1,36 @@ //! Structs and methods for handling Zcash block headers. -use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; +use alloc::string::ToString; +use alloc::vec::Vec; +use core::fmt; +use core::ops::Deref; +use core2::io::{self, Read, Write}; + +use crate::encoding::{ReadBytesExt, WriteBytesExt}; +use memuse::DynamicUsage; use sha2::{Digest, Sha256}; -use std::fmt; -use std::io::{self, Read, Write}; -use std::ops::Deref; -use crate::serialize::Vector; +use zcash_encoding::Vector; pub use equihash; -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +/// The identifier for a Zcash block. +/// +/// This is the SHA-256d hash of the encoded [`BlockHeader`]. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] pub struct BlockHash(pub [u8; 32]); +memuse::impl_no_dynamic_usage!(BlockHash); + +impl fmt::Debug for BlockHash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // The (byte-flipped) hex string is more useful than the raw bytes, because we can + // look that up in RPC methods and block explorers. + let block_hash_str = self.to_string(); + f.debug_tuple("BlockHash").field(&block_hash_str).finish() + } +} + impl fmt::Display for BlockHash { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { let mut data = self.0; @@ -28,10 +46,20 @@ impl BlockHash { /// /// This function will panic if the slice is not exactly 32 bytes. pub fn from_slice(bytes: &[u8]) -> Self { - assert_eq!(bytes.len(), 32); - let mut hash = [0; 32]; - hash.copy_from_slice(&bytes); - BlockHash(hash) + Self::try_from_slice(bytes).unwrap() + } + + /// Constructs a [`BlockHash`] from the given slice. + /// + /// Returns `None` if `bytes` has any length other than 32 + pub fn try_from_slice(bytes: &[u8]) -> Option { + if bytes.len() == 32 { + let mut hash = [0; 32]; + hash.copy_from_slice(bytes); + Some(BlockHash(hash)) + } else { + None + } } } @@ -49,6 +77,7 @@ impl Deref for BlockHeader { } } +/// The information contained in a Zcash block header. pub struct BlockHeaderData { pub version: i32, pub prev_block: BlockHash, @@ -77,7 +106,7 @@ impl BlockHeader { header .hash .0 - .copy_from_slice(&Sha256::digest(&Sha256::digest(&raw))); + .copy_from_slice(&Sha256::digest(Sha256::digest(&raw))); Ok(header) } @@ -87,7 +116,7 @@ impl BlockHeader { } pub fn read(mut reader: R) -> io::Result { - let version = reader.read_i32::()?; + let version = reader.read_i32_le()?; let mut prev_block = BlockHash([0; 32]); reader.read_exact(&mut prev_block.0)?; @@ -98,8 +127,8 @@ impl BlockHeader { let mut final_sapling_root = [0; 32]; reader.read_exact(&mut final_sapling_root)?; - let time = reader.read_u32::()?; - let bits = reader.read_u32::()?; + let time = reader.read_u32_le()?; + let bits = reader.read_u32_le()?; let mut nonce = [0; 32]; reader.read_exact(&mut nonce)?; @@ -119,12 +148,12 @@ impl BlockHeader { } pub fn write(&self, mut writer: W) -> io::Result<()> { - writer.write_i32::(self.version)?; + writer.write_i32_le(self.version)?; writer.write_all(&self.prev_block.0)?; writer.write_all(&self.merkle_root)?; writer.write_all(&self.final_sapling_root)?; - writer.write_u32::(self.time)?; - writer.write_u32::(self.bits)?; + writer.write_u32_le(self.time)?; + writer.write_u32_le(self.bits)?; writer.write_all(&self.nonce)?; Vector::write(&mut writer, &self.solution, |w, b| w.write_u8(*b))?; @@ -135,6 +164,7 @@ impl BlockHeader { #[cfg(test)] mod tests { use super::BlockHeader; + use alloc::vec::Vec; const HEADER_MAINNET_415000: [u8; 1487] = [ 0x04, 0x00, 0x00, 0x00, 0x52, 0x74, 0xb4, 0x3b, 0x9e, 0x4a, 0xd8, 0xf4, 0x3e, 0x93, 0xf7, diff --git a/zcash_primitives/src/consensus.rs b/zcash_primitives/src/consensus.rs deleted file mode 100644 index 4e5c49dd4b..0000000000 --- a/zcash_primitives/src/consensus.rs +++ /dev/null @@ -1,555 +0,0 @@ -//! Consensus logic and parameters. - -use std::cmp::{Ord, Ordering}; -use std::convert::TryFrom; -use std::fmt; -use std::ops::{Add, Sub}; - -use crate::constants; - -/// A wrapper type representing blockchain heights. Safe conversion from -/// various integer types, as well as addition and subtraction, are provided. -#[repr(transparent)] -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct BlockHeight(u32); - -pub const H0: BlockHeight = BlockHeight(0); - -impl BlockHeight { - pub const fn from_u32(v: u32) -> BlockHeight { - BlockHeight(v) - } -} - -impl fmt::Display for BlockHeight { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(formatter) - } -} - -impl Ord for BlockHeight { - fn cmp(&self, other: &Self) -> Ordering { - self.0.cmp(&other.0) - } -} - -impl PartialOrd for BlockHeight { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl From for BlockHeight { - fn from(value: u32) -> Self { - BlockHeight(value) - } -} - -impl From for u32 { - fn from(value: BlockHeight) -> u32 { - value.0 - } -} - -impl TryFrom for BlockHeight { - type Error = std::num::TryFromIntError; - - fn try_from(value: u64) -> Result { - u32::try_from(value).map(BlockHeight) - } -} - -impl From for u64 { - fn from(value: BlockHeight) -> u64 { - value.0 as u64 - } -} - -impl TryFrom for BlockHeight { - type Error = std::num::TryFromIntError; - - fn try_from(value: i32) -> Result { - u32::try_from(value).map(BlockHeight) - } -} - -impl TryFrom for BlockHeight { - type Error = std::num::TryFromIntError; - - fn try_from(value: i64) -> Result { - u32::try_from(value).map(BlockHeight) - } -} - -impl From for i64 { - fn from(value: BlockHeight) -> i64 { - value.0 as i64 - } -} - -impl Add for BlockHeight { - type Output = Self; - - fn add(self, other: u32) -> Self { - BlockHeight(self.0 + other) - } -} - -impl Add for BlockHeight { - type Output = Self; - - fn add(self, other: Self) -> Self { - self + other.0 - } -} - -impl Sub for BlockHeight { - type Output = Self; - - fn sub(self, other: u32) -> Self { - if other > self.0 { - panic!("Subtraction resulted in negative block height."); - } - - BlockHeight(self.0 - other) - } -} - -impl Sub for BlockHeight { - type Output = Self; - - fn sub(self, other: Self) -> Self { - self - other.0 - } -} - -/// Zcash consensus parameters. -pub trait Parameters: Clone { - /// Returns the activation height for a particular network upgrade, - /// if an activation height has been set. - fn activation_height(&self, nu: NetworkUpgrade) -> Option; - - /// Determines whether the specified network upgrade is active as of the - /// provided block height on the network to which this Parameters value applies. - fn is_nu_active(&self, nu: NetworkUpgrade, height: BlockHeight) -> bool { - self.activation_height(nu).map_or(false, |h| h <= height) - } - - /// The coin type for ZEC, as defined by [SLIP 44]. - /// - /// [SLIP 44]: https://github.com/satoshilabs/slips/blob/master/slip-0044.md - fn coin_type(&self) -> u32; - - /// Returns the human-readable prefix for Bech32-encoded Sapling extended spending keys - /// the network to which this Parameters value applies. - /// - /// Defined in [ZIP 32]. - /// - /// [`ExtendedSpendingKey`]: zcash_primitives::zip32::ExtendedSpendingKey - /// [ZIP 32]: https://github.com/zcash/zips/blob/master/zip-0032.rst - fn hrp_sapling_extended_spending_key(&self) -> &str; - - /// Returns the human-readable prefix for Bech32-encoded Sapling extended full - /// viewing keys for the network to which this Parameters value applies. - /// - /// Defined in [ZIP 32]. - /// - /// [`ExtendedFullViewingKey`]: zcash_primitives::zip32::ExtendedFullViewingKey - /// [ZIP 32]: https://github.com/zcash/zips/blob/master/zip-0032.rst - fn hrp_sapling_extended_full_viewing_key(&self) -> &str; - - /// Returns the Bech32-encoded human-readable prefix for Sapling payment addresses - /// viewing keys for the network to which this Parameters value applies. - /// - /// Defined in section 5.6.4 of the [Zcash Protocol Specification]. - /// - /// [`PaymentAddress`]: zcash_primitives::primitives::PaymentAddress - /// [Zcash Protocol Specification]: https://github.com/zcash/zips/blob/master/protocol/protocol.pdf - fn hrp_sapling_payment_address(&self) -> &str; - - /// Returns the human-readable prefix for Base58Check-encoded transparent - /// pay-to-public-key-hash payment addresses for the network to which this Parameters value - /// applies. - /// - /// [`TransparentAddress::PublicKey`]: zcash_primitives::legacy::TransparentAddress::PublicKey - fn b58_pubkey_address_prefix(&self) -> [u8; 2]; - - /// Returns the human-readable prefix for Base58Check-encoded transparent pay-to-script-hash - /// payment addresses for the network to which this Parameters value applies. - /// - /// [`TransparentAddress::Script`]: zcash_primitives::legacy::TransparentAddress::Script - fn b58_script_address_prefix(&self) -> [u8; 2]; -} - -/// Marker struct for the production network. -#[derive(PartialEq, Copy, Clone, Debug)] -pub struct MainNetwork; - -pub const MAIN_NETWORK: MainNetwork = MainNetwork; - -impl Parameters for MainNetwork { - fn activation_height(&self, nu: NetworkUpgrade) -> Option { - match nu { - NetworkUpgrade::Overwinter => Some(BlockHeight(347_500)), - NetworkUpgrade::Sapling => Some(BlockHeight(419_200)), - NetworkUpgrade::Blossom => Some(BlockHeight(653_600)), - NetworkUpgrade::Heartwood => Some(BlockHeight(903_000)), - NetworkUpgrade::Canopy => Some(BlockHeight(1_046_400)), - #[cfg(feature = "zfuture")] - NetworkUpgrade::ZFuture => None, - } - } - - fn coin_type(&self) -> u32 { - constants::mainnet::COIN_TYPE - } - - fn hrp_sapling_extended_spending_key(&self) -> &str { - constants::mainnet::HRP_SAPLING_EXTENDED_SPENDING_KEY - } - - fn hrp_sapling_extended_full_viewing_key(&self) -> &str { - constants::mainnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY - } - - fn hrp_sapling_payment_address(&self) -> &str { - constants::mainnet::HRP_SAPLING_PAYMENT_ADDRESS - } - - fn b58_pubkey_address_prefix(&self) -> [u8; 2] { - constants::mainnet::B58_PUBKEY_ADDRESS_PREFIX - } - - fn b58_script_address_prefix(&self) -> [u8; 2] { - constants::mainnet::B58_SCRIPT_ADDRESS_PREFIX - } -} - -/// Marker struct for the test network. -#[derive(PartialEq, Copy, Clone, Debug)] -pub struct TestNetwork; - -pub const TEST_NETWORK: TestNetwork = TestNetwork; - -impl Parameters for TestNetwork { - fn activation_height(&self, nu: NetworkUpgrade) -> Option { - match nu { - NetworkUpgrade::Overwinter => Some(BlockHeight(207_500)), - NetworkUpgrade::Sapling => Some(BlockHeight(280_000)), - NetworkUpgrade::Blossom => Some(BlockHeight(584_000)), - NetworkUpgrade::Heartwood => Some(BlockHeight(903_800)), - NetworkUpgrade::Canopy => Some(BlockHeight(1_028_500)), - #[cfg(feature = "zfuture")] - NetworkUpgrade::ZFuture => None, - } - } - - fn coin_type(&self) -> u32 { - constants::testnet::COIN_TYPE - } - - fn hrp_sapling_extended_spending_key(&self) -> &str { - constants::testnet::HRP_SAPLING_EXTENDED_SPENDING_KEY - } - - fn hrp_sapling_extended_full_viewing_key(&self) -> &str { - constants::testnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY - } - - fn hrp_sapling_payment_address(&self) -> &str { - constants::testnet::HRP_SAPLING_PAYMENT_ADDRESS - } - - fn b58_pubkey_address_prefix(&self) -> [u8; 2] { - constants::testnet::B58_PUBKEY_ADDRESS_PREFIX - } - - fn b58_script_address_prefix(&self) -> [u8; 2] { - constants::testnet::B58_SCRIPT_ADDRESS_PREFIX - } -} - -#[derive(PartialEq, Copy, Clone, Debug)] -pub enum Network { - MainNetwork, - TestNetwork, -} - -impl Parameters for Network { - fn activation_height(&self, nu: NetworkUpgrade) -> Option { - match self { - Network::MainNetwork => MAIN_NETWORK.activation_height(nu), - Network::TestNetwork => TEST_NETWORK.activation_height(nu), - } - } - - fn coin_type(&self) -> u32 { - match self { - Network::MainNetwork => MAIN_NETWORK.coin_type(), - Network::TestNetwork => TEST_NETWORK.coin_type(), - } - } - - fn hrp_sapling_extended_spending_key(&self) -> &str { - match self { - Network::MainNetwork => MAIN_NETWORK.hrp_sapling_extended_spending_key(), - Network::TestNetwork => TEST_NETWORK.hrp_sapling_extended_spending_key(), - } - } - - fn hrp_sapling_extended_full_viewing_key(&self) -> &str { - match self { - Network::MainNetwork => MAIN_NETWORK.hrp_sapling_extended_full_viewing_key(), - Network::TestNetwork => TEST_NETWORK.hrp_sapling_extended_full_viewing_key(), - } - } - - fn hrp_sapling_payment_address(&self) -> &str { - match self { - Network::MainNetwork => MAIN_NETWORK.hrp_sapling_payment_address(), - Network::TestNetwork => TEST_NETWORK.hrp_sapling_payment_address(), - } - } - - fn b58_pubkey_address_prefix(&self) -> [u8; 2] { - match self { - Network::MainNetwork => MAIN_NETWORK.b58_pubkey_address_prefix(), - Network::TestNetwork => TEST_NETWORK.b58_pubkey_address_prefix(), - } - } - - fn b58_script_address_prefix(&self) -> [u8; 2] { - match self { - Network::MainNetwork => MAIN_NETWORK.b58_script_address_prefix(), - Network::TestNetwork => TEST_NETWORK.b58_script_address_prefix(), - } - } -} - -/// An event that occurs at a specified height on the Zcash chain, at which point the -/// consensus rules enforced by the network are altered. -/// -/// See [ZIP 200](https://zips.z.cash/zip-0200) for more details. -#[derive(Clone, Copy, Debug)] -pub enum NetworkUpgrade { - /// The [Overwinter] network upgrade. - /// - /// [Overwinter]: https://z.cash/upgrade/overwinter/ - Overwinter, - /// The [Sapling] network upgrade. - /// - /// [Sapling]: https://z.cash/upgrade/sapling/ - Sapling, - /// The [Blossom] network upgrade. - /// - /// [Blossom]: https://z.cash/upgrade/blossom/ - Blossom, - /// The [Heartwood] network upgrade. - /// - /// [Heartwood]: https://z.cash/upgrade/heartwood/ - Heartwood, - /// The [Canopy] network upgrade. - /// - /// [Canopy]: https://z.cash/upgrade/canopy/ - Canopy, - /// The ZFUTURE network upgrade. - /// - /// This upgrade is expected never to activate on mainnet; - /// it is intended for use in integration testing of functionality - /// that is a candidate for integration in a future network upgrade. - #[cfg(feature = "zfuture")] - ZFuture, -} - -impl fmt::Display for NetworkUpgrade { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - NetworkUpgrade::Overwinter => write!(f, "Overwinter"), - NetworkUpgrade::Sapling => write!(f, "Sapling"), - NetworkUpgrade::Blossom => write!(f, "Blossom"), - NetworkUpgrade::Heartwood => write!(f, "Heartwood"), - NetworkUpgrade::Canopy => write!(f, "Canopy"), - #[cfg(feature = "zfuture")] - NetworkUpgrade::ZFuture => write!(f, "ZFUTURE"), - } - } -} - -impl NetworkUpgrade { - fn branch_id(self) -> BranchId { - match self { - NetworkUpgrade::Overwinter => BranchId::Overwinter, - NetworkUpgrade::Sapling => BranchId::Sapling, - NetworkUpgrade::Blossom => BranchId::Blossom, - NetworkUpgrade::Heartwood => BranchId::Heartwood, - NetworkUpgrade::Canopy => BranchId::Canopy, - #[cfg(feature = "zfuture")] - NetworkUpgrade::ZFuture => BranchId::ZFuture, - } - } -} - -/// The network upgrades on the Zcash chain in order of activation. -/// -/// This order corresponds to the activation heights, but because Rust enums are -/// full-fledged algebraic data types, we need to define it manually. -const UPGRADES_IN_ORDER: &[NetworkUpgrade] = &[ - NetworkUpgrade::Overwinter, - NetworkUpgrade::Sapling, - NetworkUpgrade::Blossom, - NetworkUpgrade::Heartwood, - NetworkUpgrade::Canopy, -]; - -pub const ZIP212_GRACE_PERIOD: u32 = 32256; - -/// A globally-unique identifier for a set of consensus rules within the Zcash chain. -/// -/// Each branch ID in this enum corresponds to one of the epochs between a pair of Zcash -/// network upgrades. For example, `BranchId::Overwinter` corresponds to the blocks -/// starting at Overwinter activation, and ending the block before Sapling activation. -/// -/// The main use of the branch ID is in signature generation: transactions commit to a -/// specific branch ID by including it as part of [`signature_hash`]. This ensures -/// two-way replay protection for transactions across network upgrades. -/// -/// See [ZIP 200](https://zips.z.cash/zip-0200) for more details. -/// -/// [`signature_hash`]: crate::transaction::signature_hash -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum BranchId { - /// The consensus rules at the launch of Zcash. - Sprout, - /// The consensus rules deployed by [`NetworkUpgrade::Overwinter`]. - Overwinter, - /// The consensus rules deployed by [`NetworkUpgrade::Sapling`]. - Sapling, - /// The consensus rules deployed by [`NetworkUpgrade::Blossom`]. - Blossom, - /// The consensus rules deployed by [`NetworkUpgrade::Heartwood`]. - Heartwood, - /// The consensus rules deployed by [`NetworkUpgrade::Canopy`]. - Canopy, - /// Candidates for future consensus rules; this branch will never - /// activate on mainnet. - #[cfg(feature = "zfuture")] - ZFuture, -} - -impl TryFrom for BranchId { - type Error = &'static str; - - fn try_from(value: u32) -> Result { - match value { - 0 => Ok(BranchId::Sprout), - 0x5ba8_1b19 => Ok(BranchId::Overwinter), - 0x76b8_09bb => Ok(BranchId::Sapling), - 0x2bb4_0e60 => Ok(BranchId::Blossom), - 0xf5b9_230b => Ok(BranchId::Heartwood), - 0xe9ff_75a6 => Ok(BranchId::Canopy), - #[cfg(feature = "zfuture")] - 0xffff_ffff => Ok(BranchId::ZFuture), - _ => Err("Unknown consensus branch ID"), - } - } -} - -impl From for u32 { - fn from(consensus_branch_id: BranchId) -> u32 { - match consensus_branch_id { - BranchId::Sprout => 0, - BranchId::Overwinter => 0x5ba8_1b19, - BranchId::Sapling => 0x76b8_09bb, - BranchId::Blossom => 0x2bb4_0e60, - BranchId::Heartwood => 0xf5b9_230b, - BranchId::Canopy => 0xe9ff_75a6, - #[cfg(feature = "zfuture")] - BranchId::ZFuture => 0xffff_ffff, - } - } -} - -impl BranchId { - /// Returns the branch ID corresponding to the consensus rule set that is active at - /// the given height. - /// - /// This is the branch ID that should be used when creating transactions. - pub fn for_height(parameters: &P, height: BlockHeight) -> Self { - for nu in UPGRADES_IN_ORDER.iter().rev() { - if parameters.is_nu_active(*nu, height) { - return nu.branch_id(); - } - } - - // Sprout rules apply before any network upgrade - BranchId::Sprout - } -} - -#[cfg(test)] -mod tests { - use std::convert::TryFrom; - - use super::{ - BlockHeight, BranchId, NetworkUpgrade, Parameters, MAIN_NETWORK, UPGRADES_IN_ORDER, - }; - - #[test] - fn nu_ordering() { - for i in 1..UPGRADES_IN_ORDER.len() { - let nu_a = UPGRADES_IN_ORDER[i - 1]; - let nu_b = UPGRADES_IN_ORDER[i]; - match ( - MAIN_NETWORK.activation_height(nu_a), - MAIN_NETWORK.activation_height(nu_b), - ) { - (a, b) if a < b => (), - _ => panic!( - "{} should not be before {} in UPGRADES_IN_ORDER", - nu_a, nu_b - ), - } - } - } - - #[test] - fn nu_is_active() { - assert!(!MAIN_NETWORK.is_nu_active(NetworkUpgrade::Overwinter, BlockHeight(0))); - assert!(!MAIN_NETWORK.is_nu_active(NetworkUpgrade::Overwinter, BlockHeight(347_499))); - assert!(MAIN_NETWORK.is_nu_active(NetworkUpgrade::Overwinter, BlockHeight(347_500))); - } - - #[test] - fn branch_id_from_u32() { - assert_eq!(BranchId::try_from(0), Ok(BranchId::Sprout)); - assert!(BranchId::try_from(1).is_err()); - } - - #[test] - fn branch_id_for_height() { - assert_eq!( - BranchId::for_height(&MAIN_NETWORK, BlockHeight(0)), - BranchId::Sprout, - ); - assert_eq!( - BranchId::for_height(&MAIN_NETWORK, BlockHeight(419_199)), - BranchId::Overwinter, - ); - assert_eq!( - BranchId::for_height(&MAIN_NETWORK, BlockHeight(419_200)), - BranchId::Sapling, - ); - assert_eq!( - BranchId::for_height(&MAIN_NETWORK, BlockHeight(903_000)), - BranchId::Heartwood, - ); - assert_eq!( - BranchId::for_height(&MAIN_NETWORK, BlockHeight(1_046_400)), - BranchId::Canopy, - ); - assert_eq!( - BranchId::for_height(&MAIN_NETWORK, BlockHeight(5_000_000)), - BranchId::Canopy, - ); - } -} diff --git a/zcash_primitives/src/constants.rs b/zcash_primitives/src/constants.rs deleted file mode 100644 index 95bcd53262..0000000000 --- a/zcash_primitives/src/constants.rs +++ /dev/null @@ -1,440 +0,0 @@ -//! Various constants used by the Zcash primitives. - -use ff::PrimeField; -use group::Group; -use jubjub::SubgroupPoint; -use lazy_static::lazy_static; - -pub mod mainnet; -pub mod regtest; -pub mod testnet; - -/// First 64 bytes of the BLAKE2s input during group hash. -/// This is chosen to be some random string that we couldn't have anticipated when we designed -/// the algorithm, for rigidity purposes. -/// We deliberately use an ASCII hex string of 32 bytes here. -pub const GH_FIRST_BLOCK: &[u8; 64] = - b"096b36a5804bfacef1691e173c366a47ff5ba84a44f26ddd7e8d9f79d5b42df0"; - -// BLAKE2s invocation personalizations -/// BLAKE2s Personalization for CRH^ivk = BLAKE2s(ak | nk) -pub const CRH_IVK_PERSONALIZATION: &[u8; 8] = b"Zcashivk"; - -/// BLAKE2s Personalization for PRF^nf = BLAKE2s(nk | rho) -pub const PRF_NF_PERSONALIZATION: &[u8; 8] = b"Zcash_nf"; - -// Group hash personalizations -/// BLAKE2s Personalization for Pedersen hash generators. -pub const PEDERSEN_HASH_GENERATORS_PERSONALIZATION: &[u8; 8] = b"Zcash_PH"; - -/// BLAKE2s Personalization for the group hash for key diversification -pub const KEY_DIVERSIFICATION_PERSONALIZATION: &[u8; 8] = b"Zcash_gd"; - -/// BLAKE2s Personalization for the spending key base point -pub const SPENDING_KEY_GENERATOR_PERSONALIZATION: &[u8; 8] = b"Zcash_G_"; - -/// BLAKE2s Personalization for the proof generation key base point -pub const PROOF_GENERATION_KEY_BASE_GENERATOR_PERSONALIZATION: &[u8; 8] = b"Zcash_H_"; - -/// BLAKE2s Personalization for the value commitment generator for the value -pub const VALUE_COMMITMENT_GENERATOR_PERSONALIZATION: &[u8; 8] = b"Zcash_cv"; - -/// BLAKE2s Personalization for the nullifier position generator (for computing rho) -pub const NULLIFIER_POSITION_IN_TREE_GENERATOR_PERSONALIZATION: &[u8; 8] = b"Zcash_J_"; - -/// The prover will demonstrate knowledge of discrete log with respect to this base when -/// they are constructing a proof, in order to authorize proof construction. -pub const PROOF_GENERATION_KEY_GENERATOR: SubgroupPoint = SubgroupPoint::from_raw_unchecked( - bls12_381::Scalar::from_raw([ - 0x3af2_dbef_b96e_2571, - 0xadf2_d038_f2fb_b820, - 0x7043_03f1_e890_6081, - 0x1457_a502_31cd_e2df, - ]), - bls12_381::Scalar::from_raw([ - 0x467a_f9f7_e05d_e8e7, - 0x50df_51ea_f5a1_49d2, - 0xdec9_0184_0f49_48cc, - 0x54b6_d107_18df_2a7a, - ]), -); - -/// The note commitment is randomized over this generator. -pub const NOTE_COMMITMENT_RANDOMNESS_GENERATOR: SubgroupPoint = SubgroupPoint::from_raw_unchecked( - bls12_381::Scalar::from_raw([ - 0xa514_3b34_a8e3_6462, - 0xf091_9d06_ffb1_ecda, - 0xa140_9aa1_f33b_ec2c, - 0x26eb_9f8a_9ec7_2a8c, - ]), - bls12_381::Scalar::from_raw([ - 0xd4fc_6365_796c_77ac, - 0x96b7_8bea_fa9c_c44c, - 0x949d_7747_6e26_2c95, - 0x114b_7501_ad10_4c57, - ]), -); - -/// The node commitment is randomized again by the position in order to supply the -/// nullifier computation with a unique input w.r.t. the note being spent, to prevent -/// Faerie gold attacks. -pub const NULLIFIER_POSITION_GENERATOR: SubgroupPoint = SubgroupPoint::from_raw_unchecked( - bls12_381::Scalar::from_raw([ - 0x2ce3_3921_888d_30db, - 0xe81c_ee09_a561_229e, - 0xdb56_b6db_8d80_75ed, - 0x2400_c2e2_e336_2644, - ]), - bls12_381::Scalar::from_raw([ - 0xa3f7_fa36_c72b_0065, - 0xe155_b8e8_ffff_2e42, - 0xfc9e_8a15_a096_ba8f, - 0x6136_9d54_40bf_84a5, - ]), -); - -/// The value commitment is used to check balance between inputs and outputs. The value is -/// placed over this generator. -pub const VALUE_COMMITMENT_VALUE_GENERATOR: SubgroupPoint = SubgroupPoint::from_raw_unchecked( - bls12_381::Scalar::from_raw([ - 0x3618_3b2c_b4d7_ef51, - 0x9472_c89a_c043_042d, - 0xd861_8ed1_d15f_ef4e, - 0x273f_910d_9ecc_1615, - ]), - bls12_381::Scalar::from_raw([ - 0xa77a_81f5_0667_c8d7, - 0xbc33_32d0_fa1c_cd18, - 0xd322_94fd_8977_4ad6, - 0x466a_7e3a_82f6_7ab1, - ]), -); - -/// The value commitment is randomized over this generator, for privacy. -pub const VALUE_COMMITMENT_RANDOMNESS_GENERATOR: SubgroupPoint = SubgroupPoint::from_raw_unchecked( - bls12_381::Scalar::from_raw([ - 0x3bce_3b77_9366_4337, - 0xd1d8_da41_af03_744e, - 0x7ff6_826a_d580_04b4, - 0x6800_f4fa_0f00_1cfc, - ]), - bls12_381::Scalar::from_raw([ - 0x3cae_fab9_380b_6a8b, - 0xad46_f1b0_473b_803b, - 0xe6fb_2a6e_1e22_ab50, - 0x6d81_d3a9_cb45_dedb, - ]), -); - -/// The spender proves discrete log with respect to this base at spend time. -pub const SPENDING_KEY_GENERATOR: SubgroupPoint = SubgroupPoint::from_raw_unchecked( - bls12_381::Scalar::from_raw([ - 0x47bf_4692_0a95_a753, - 0xd5b9_a7d3_ef8e_2827, - 0xd418_a7ff_2675_3b6a, - 0x0926_d4f3_2059_c712, - ]), - bls12_381::Scalar::from_raw([ - 0x3056_32ad_aaf2_b530, - 0x6d65_674d_cedb_ddbc, - 0x53bb_37d0_c21c_fd05, - 0x57a1_019e_6de9_b675, - ]), -); - -/// The generators (for each segment) used in all Pedersen commitments. -pub const PEDERSEN_HASH_GENERATORS: &[SubgroupPoint] = &[ - SubgroupPoint::from_raw_unchecked( - bls12_381::Scalar::from_raw([ - 0x194e_4292_6f66_1b51, - 0x2f0c_718f_6f0f_badd, - 0xb5ea_25de_7ec0_e378, - 0x73c0_16a4_2ded_9578, - ]), - bls12_381::Scalar::from_raw([ - 0x77bf_abd4_3224_3cca, - 0xf947_2e8b_c04e_4632, - 0x79c9_166b_837e_dc5e, - 0x289e_87a2_d352_1b57, - ]), - ), - SubgroupPoint::from_raw_unchecked( - bls12_381::Scalar::from_raw([ - 0xb981_9dc8_2d90_607e, - 0xa361_ee3f_d48f_df77, - 0x52a3_5a8c_1908_dd87, - 0x15a3_6d1f_0f39_0d88, - ]), - bls12_381::Scalar::from_raw([ - 0x7b0d_c53c_4ebf_1891, - 0x1f3a_beeb_98fa_d3e8, - 0xf789_1142_c001_d925, - 0x015d_8c7f_5b43_fe33, - ]), - ), - SubgroupPoint::from_raw_unchecked( - bls12_381::Scalar::from_raw([ - 0x76d6_f7c2_b67f_c475, - 0xbae8_e5c4_6641_ae5c, - 0xeb69_ae39_f5c8_4210, - 0x6643_21a5_8246_e2f6, - ]), - bls12_381::Scalar::from_raw([ - 0x80ed_502c_9793_d457, - 0x8bb2_2a7f_1784_b498, - 0xe000_a46c_8e8c_e853, - 0x362e_1500_d24e_ee9e, - ]), - ), - SubgroupPoint::from_raw_unchecked( - bls12_381::Scalar::from_raw([ - 0x4c76_7804_c1c4_a2cc, - 0x7d02_d50e_654b_87f2, - 0xedc5_f4a9_cff2_9fd5, - 0x323a_6548_ce9d_9876, - ]), - bls12_381::Scalar::from_raw([ - 0x8471_4bec_a335_70e9, - 0x5103_afa1_a11f_6a85, - 0x9107_0acb_d8d9_47b7, - 0x2f7e_e40c_4b56_cad8, - ]), - ), - SubgroupPoint::from_raw_unchecked( - bls12_381::Scalar::from_raw([ - 0x4680_9430_657f_82d1, - 0xefd5_9313_05f2_f0bf, - 0x89b6_4b4e_0336_2796, - 0x3bd2_6660_00b5_4796, - ]), - bls12_381::Scalar::from_raw([ - 0x9996_8299_c365_8aef, - 0xb3b9_d809_5859_d14c, - 0x3978_3238_1406_c9e5, - 0x494b_c521_03ab_9d0a, - ]), - ), - SubgroupPoint::from_raw_unchecked( - bls12_381::Scalar::from_raw([ - 0xcb3c_0232_58d3_2079, - 0x1d9e_5ca2_1135_ff6f, - 0xda04_9746_d76d_3ee5, - 0x6344_7b2b_a31b_b28a, - ]), - bls12_381::Scalar::from_raw([ - 0x4360_8211_9f8d_629a, - 0xa802_00d2_c66b_13a7, - 0x64cd_b107_0a13_6a28, - 0x64ec_4689_e8bf_b6e5, - ]), - ), -]; - -/// The maximum number of chunks per segment of the Pedersen hash. -pub const PEDERSEN_HASH_CHUNKS_PER_GENERATOR: usize = 63; - -/// The window size for exponentiation of Pedersen hash generators outside the circuit. -pub const PEDERSEN_HASH_EXP_WINDOW_SIZE: u32 = 8; - -lazy_static! { - /// The exp table for [`PEDERSEN_HASH_GENERATORS`]. - pub static ref PEDERSEN_HASH_EXP_TABLE: Vec>> = - generate_pedersen_hash_exp_table(); -} - -/// Creates the exp table for the Pedersen hash generators. -fn generate_pedersen_hash_exp_table() -> Vec>> { - let window = PEDERSEN_HASH_EXP_WINDOW_SIZE; - - PEDERSEN_HASH_GENERATORS - .iter() - .cloned() - .map(|mut g| { - let mut tables = vec![]; - - let mut num_bits = 0; - while num_bits <= jubjub::Fr::NUM_BITS { - let mut table = Vec::with_capacity(1 << window); - let mut base = SubgroupPoint::identity(); - - for _ in 0..(1 << window) { - table.push(base); - base += g; - } - - tables.push(table); - num_bits += window; - - for _ in 0..window { - g = g.double(); - } - } - - tables - }) - .collect() -} - -#[cfg(test)] -mod tests { - use jubjub::SubgroupPoint; - - use super::*; - use crate::sapling::group_hash::group_hash; - - fn find_group_hash(m: &[u8], personalization: &[u8; 8]) -> SubgroupPoint { - let mut tag = m.to_vec(); - let i = tag.len(); - tag.push(0u8); - - loop { - let gh = group_hash(&tag, personalization); - - // We don't want to overflow and start reusing generators - assert!(tag[i] != u8::max_value()); - tag[i] += 1; - - if let Some(gh) = gh { - break gh; - } - } - } - - #[test] - fn proof_generation_key_base_generator() { - assert_eq!( - find_group_hash(&[], PROOF_GENERATION_KEY_BASE_GENERATOR_PERSONALIZATION), - PROOF_GENERATION_KEY_GENERATOR, - ); - } - - #[test] - fn note_commitment_randomness_generator() { - assert_eq!( - find_group_hash(b"r", PEDERSEN_HASH_GENERATORS_PERSONALIZATION), - NOTE_COMMITMENT_RANDOMNESS_GENERATOR, - ); - } - - #[test] - fn nullifier_position_generator() { - assert_eq!( - find_group_hash(&[], NULLIFIER_POSITION_IN_TREE_GENERATOR_PERSONALIZATION), - NULLIFIER_POSITION_GENERATOR, - ); - } - - #[test] - fn value_commitment_value_generator() { - assert_eq!( - find_group_hash(b"v", VALUE_COMMITMENT_GENERATOR_PERSONALIZATION), - VALUE_COMMITMENT_VALUE_GENERATOR, - ); - } - - #[test] - fn value_commitment_randomness_generator() { - assert_eq!( - find_group_hash(b"r", VALUE_COMMITMENT_GENERATOR_PERSONALIZATION), - VALUE_COMMITMENT_RANDOMNESS_GENERATOR, - ); - } - - #[test] - fn spending_key_generator() { - assert_eq!( - find_group_hash(&[], SPENDING_KEY_GENERATOR_PERSONALIZATION), - SPENDING_KEY_GENERATOR, - ); - } - - #[test] - fn pedersen_hash_generators() { - for (m, actual) in PEDERSEN_HASH_GENERATORS.iter().enumerate() { - assert_eq!( - &find_group_hash( - &(m as u32).to_le_bytes(), - PEDERSEN_HASH_GENERATORS_PERSONALIZATION - ), - actual - ); - } - } - - #[test] - fn no_duplicate_fixed_base_generators() { - let fixed_base_generators = [ - PROOF_GENERATION_KEY_GENERATOR, - NOTE_COMMITMENT_RANDOMNESS_GENERATOR, - NULLIFIER_POSITION_GENERATOR, - VALUE_COMMITMENT_VALUE_GENERATOR, - VALUE_COMMITMENT_RANDOMNESS_GENERATOR, - SPENDING_KEY_GENERATOR, - ]; - - // Check for duplicates, far worse than spec inconsistencies! - for (i, p1) in fixed_base_generators.iter().enumerate() { - if p1.is_identity().into() { - panic!("Neutral element!"); - } - - for p2 in fixed_base_generators.iter().skip(i + 1) { - if p1 == p2 { - panic!("Duplicate generator!"); - } - } - } - } - - /// Check for simple relations between the generators, that make finding collisions easy; - /// far worse than spec inconsistencies! - fn check_consistency_of_pedersen_hash_generators( - pedersen_hash_generators: &[jubjub::SubgroupPoint], - ) { - for (i, p1) in pedersen_hash_generators.iter().enumerate() { - if p1.is_identity().into() { - panic!("Neutral element!"); - } - for p2 in pedersen_hash_generators.iter().skip(i + 1) { - if p1 == p2 { - panic!("Duplicate generator!"); - } - if *p1 == -p2 { - panic!("Inverse generator!"); - } - } - - // check for a generator being the sum of any other two - for (j, p2) in pedersen_hash_generators.iter().enumerate() { - if j == i { - continue; - } - for (k, p3) in pedersen_hash_generators.iter().enumerate() { - if k == j || k == i { - continue; - } - let sum = p2 + p3; - if sum == *p1 { - panic!("Linear relation between generators!"); - } - } - } - } - } - - #[test] - fn pedersen_hash_generators_consistency() { - check_consistency_of_pedersen_hash_generators(PEDERSEN_HASH_GENERATORS); - } - - #[test] - #[should_panic(expected = "Linear relation between generators!")] - fn test_jubjub_bls12_pedersen_hash_generators_consistency_check_linear_relation() { - let mut pedersen_hash_generators = PEDERSEN_HASH_GENERATORS.to_vec(); - - // Test for linear relation - pedersen_hash_generators.push(PEDERSEN_HASH_GENERATORS[0] + PEDERSEN_HASH_GENERATORS[1]); - - check_consistency_of_pedersen_hash_generators(&pedersen_hash_generators); - } -} diff --git a/zcash_primitives/src/constants/mainnet.rs b/zcash_primitives/src/constants/mainnet.rs deleted file mode 100644 index bd0e473f43..0000000000 --- a/zcash_primitives/src/constants/mainnet.rs +++ /dev/null @@ -1,40 +0,0 @@ -//! Constants for the Zcash main network. - -/// The mainnet coin type for ZEC, as defined by [SLIP 44]. -/// -/// [SLIP 44]: https://github.com/satoshilabs/slips/blob/master/slip-0044.md -pub const COIN_TYPE: u32 = 133; - -/// The HRP for a Bech32-encoded mainnet [`ExtendedSpendingKey`]. -/// -/// Defined in [ZIP 32]. -/// -/// [`ExtendedSpendingKey`]: crate::zip32::ExtendedSpendingKey -/// [ZIP 32]: https://github.com/zcash/zips/blob/master/zip-0032.rst -pub const HRP_SAPLING_EXTENDED_SPENDING_KEY: &str = "secret-extended-key-main"; - -/// The HRP for a Bech32-encoded mainnet [`ExtendedFullViewingKey`]. -/// -/// Defined in [ZIP 32]. -/// -/// [`ExtendedFullViewingKey`]: crate::zip32::ExtendedFullViewingKey -/// [ZIP 32]: https://github.com/zcash/zips/blob/master/zip-0032.rst -pub const HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY: &str = "zxviews"; - -/// The HRP for a Bech32-encoded mainnet [`PaymentAddress`]. -/// -/// Defined in section 5.6.4 of the [Zcash Protocol Specification]. -/// -/// [`PaymentAddress`]: crate::sapling::PaymentAddress -/// [Zcash Protocol Specification]: https://github.com/zcash/zips/blob/master/protocol/protocol.pdf -pub const HRP_SAPLING_PAYMENT_ADDRESS: &str = "zs"; - -/// The prefix for a Base58Check-encoded mainnet [`TransparentAddress::PublicKey`]. -/// -/// [`TransparentAddress::PublicKey`]: crate::legacy::TransparentAddress::PublicKey -pub const B58_PUBKEY_ADDRESS_PREFIX: [u8; 2] = [0x1c, 0xb8]; - -/// The prefix for a Base58Check-encoded mainnet [`TransparentAddress::Script`]. -/// -/// [`TransparentAddress::Script`]: crate::legacy::TransparentAddress::Script -pub const B58_SCRIPT_ADDRESS_PREFIX: [u8; 2] = [0x1c, 0xbd]; diff --git a/zcash_primitives/src/constants/regtest.rs b/zcash_primitives/src/constants/regtest.rs deleted file mode 100644 index 86fb95eb91..0000000000 --- a/zcash_primitives/src/constants/regtest.rs +++ /dev/null @@ -1,46 +0,0 @@ -//! # Regtest constants -//! -//! `regtest` is a `zcashd`-specific environment used for local testing. They mostly reuse -//! the testnet constants. -//! These constants are defined in [the `zcashd` codebase]. -//! -//! [the `zcashd` codebase]: - -/// The regtest cointype reuses the testnet cointype -pub const COIN_TYPE: u32 = 1; - -/// The HRP for a Bech32-encoded regtest [`ExtendedSpendingKey`]. -/// -/// It is defined in [the `zcashd` codebase]. -/// -/// [`ExtendedSpendingKey`]: crate::zip32::ExtendedSpendingKey -/// [the `zcashd` codebase]: -pub const HRP_SAPLING_EXTENDED_SPENDING_KEY: &str = "secret-extended-key-regtest"; - -/// The HRP for a Bech32-encoded regtest [`ExtendedFullViewingKey`]. -/// -/// It is defined in [the `zcashd` codebase]. -/// -/// [`ExtendedFullViewingKey`]: crate::zip32::ExtendedFullViewingKey -/// [the `zcashd` codebase]: -pub const HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY: &str = "zxviewregtestsapling"; - -/// The HRP for a Bech32-encoded regtest [`PaymentAddress`]. -/// -/// It is defined in [the `zcashd` codebase]. -/// -/// [`PaymentAddress`]: crate::sapling::PaymentAddress -/// [the `zcashd` codebase]: -pub const HRP_SAPLING_PAYMENT_ADDRESS: &str = "zregtestsapling"; - -/// The prefix for a Base58Check-encoded regtest [`TransparentAddress::PublicKey`]. -/// Same as the testnet prefix. -/// -/// [`TransparentAddress::PublicKey`]: crate::legacy::TransparentAddress::PublicKey -pub const B58_PUBKEY_ADDRESS_PREFIX: [u8; 2] = [0x1d, 0x25]; - -/// The prefix for a Base58Check-encoded regtest [`TransparentAddress::Script`]. -/// Same as the testnet prefix. -/// -/// [`TransparentAddress::Script`]: crate::legacy::TransparentAddress::Script -pub const B58_SCRIPT_ADDRESS_PREFIX: [u8; 2] = [0x1c, 0xba]; diff --git a/zcash_primitives/src/constants/testnet.rs b/zcash_primitives/src/constants/testnet.rs deleted file mode 100644 index d11c0e9830..0000000000 --- a/zcash_primitives/src/constants/testnet.rs +++ /dev/null @@ -1,40 +0,0 @@ -//! Constants for the Zcash test network. - -/// The testnet coin type for ZEC, as defined by [SLIP 44]. -/// -/// [SLIP 44]: https://github.com/satoshilabs/slips/blob/master/slip-0044.md -pub const COIN_TYPE: u32 = 1; - -/// The HRP for a Bech32-encoded testnet [`ExtendedSpendingKey`]. -/// -/// Defined in [ZIP 32]. -/// -/// [`ExtendedSpendingKey`]: crate::zip32::ExtendedSpendingKey -/// [ZIP 32]: https://github.com/zcash/zips/blob/master/zip-0032.rst -pub const HRP_SAPLING_EXTENDED_SPENDING_KEY: &str = "secret-extended-key-test"; - -/// The HRP for a Bech32-encoded testnet [`ExtendedFullViewingKey`]. -/// -/// Defined in [ZIP 32]. -/// -/// [`ExtendedFullViewingKey`]: crate::zip32::ExtendedFullViewingKey -/// [ZIP 32]: https://github.com/zcash/zips/blob/master/zip-0032.rst -pub const HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY: &str = "zxviewtestsapling"; - -/// The HRP for a Bech32-encoded testnet [`PaymentAddress`]. -/// -/// Defined in section 5.6.4 of the [Zcash Protocol Specification]. -/// -/// [`PaymentAddress`]: crate::sapling::PaymentAddress -/// [Zcash Protocol Specification]: https://github.com/zcash/zips/blob/master/protocol/protocol.pdf -pub const HRP_SAPLING_PAYMENT_ADDRESS: &str = "ztestsapling"; - -/// The prefix for a Base58Check-encoded testnet [`TransparentAddress::PublicKey`]. -/// -/// [`TransparentAddress::PublicKey`]: crate::legacy::TransparentAddress::PublicKey -pub const B58_PUBKEY_ADDRESS_PREFIX: [u8; 2] = [0x1d, 0x25]; - -/// The prefix for a Base58Check-encoded testnet [`TransparentAddress::Script`]. -/// -/// [`TransparentAddress::Script`]: crate::legacy::TransparentAddress::Script -pub const B58_SCRIPT_ADDRESS_PREFIX: [u8; 2] = [0x1c, 0xba]; diff --git a/zcash_primitives/src/encoding.rs b/zcash_primitives/src/encoding.rs new file mode 100644 index 0000000000..d9221880cb --- /dev/null +++ b/zcash_primitives/src/encoding.rs @@ -0,0 +1,83 @@ +//! Utility traits for encoding and decoding using core2.io primitives. +//! +//! This module is used in lieu of the `byteorder` crate, which uses `std::io::{Read, Write}` +//! and therefore does not support `no_std` usage. +use blake2b_simd::{Hash, State}; +use core2::io::{self, Read, Write}; + +pub(crate) trait ReadBytesExt { + fn read_u8(self) -> io::Result; + fn read_u32_le(self) -> io::Result; + fn read_i32_le(self) -> io::Result; + fn read_u64_le(self) -> io::Result; +} + +impl ReadBytesExt for &mut R { + fn read_u8(self) -> io::Result { + let mut repr = [0u8; 1]; + self.read_exact(&mut repr)?; + Ok(repr[0]) + } + + fn read_u32_le(self) -> io::Result { + let mut repr = [0u8; 4]; + self.read_exact(&mut repr)?; + Ok(u32::from_le_bytes(repr)) + } + + fn read_i32_le(self) -> io::Result { + let mut repr = [0u8; 4]; + self.read_exact(&mut repr)?; + Ok(i32::from_le_bytes(repr)) + } + + fn read_u64_le(self) -> io::Result { + let mut repr = [0u8; 8]; + self.read_exact(&mut repr)?; + Ok(u64::from_le_bytes(repr)) + } +} + +pub(crate) trait WriteBytesExt { + fn write_u8(self, value: u8) -> io::Result<()>; + fn write_u32_le(self, value: u32) -> io::Result<()>; + fn write_i32_le(self, value: i32) -> io::Result<()>; + fn write_u64_le(self, value: u64) -> io::Result<()>; +} + +impl WriteBytesExt for &mut W { + fn write_u8(self, value: u8) -> io::Result<()> { + self.write_all(&[value]) + } + + fn write_i32_le(self, value: i32) -> io::Result<()> { + self.write_all(&value.to_le_bytes()) + } + + fn write_u32_le(self, value: u32) -> io::Result<()> { + self.write_all(&value.to_le_bytes()) + } + + fn write_u64_le(self, value: u64) -> io::Result<()> { + self.write_all(&value.to_le_bytes()) + } +} + +pub(crate) struct StateWrite(pub(crate) State); + +impl StateWrite { + pub(crate) fn finalize(&self) -> Hash { + self.0.finalize() + } +} + +impl Write for StateWrite { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.0.update(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} diff --git a/zcash_primitives/src/extensions/transparent.rs b/zcash_primitives/src/extensions/transparent.rs index 4b8b31d5f8..513eadaf9f 100644 --- a/zcash_primitives/src/extensions/transparent.rs +++ b/zcash_primitives/src/extensions/transparent.rs @@ -1,12 +1,19 @@ //! Core traits and structs for Transparent Zcash Extensions. -use crate::transaction::components::{Amount, TzeOut, TzeOutPoint}; -use std::fmt; +use alloc::vec::Vec; +use core::fmt; + +use crate::transaction::components::tze::{self, TzeOut}; +use zcash_protocol::value::Zatoshis; + +/// A typesafe wrapper for witness payloads +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AuthData(pub Vec); /// Binary parsing capability for TZE preconditions & witnesses. /// /// Serialization formats interpreted by implementations of this trait become consensus-critical -/// upon activation of of the extension that uses them. +/// upon activation of the extension that uses them. pub trait FromPayload: Sized { type Error; @@ -17,7 +24,7 @@ pub trait FromPayload: Sized { /// Binary serialization capability for TZE preconditions & witnesses. /// /// Serialization formats used by implementations of this trait become consensus-critical upon -/// activation of of the extension that uses them. +/// activation of the extension that uses them. pub trait ToPayload { /// Returns a serialized payload and its corresponding mode. fn to_payload(&self) -> (u32, Vec); @@ -29,7 +36,7 @@ pub trait ToPayload { /// used inside of a transaction, and extension-specific types. The payload field of this struct is /// treated as opaque to all but the extension corresponding to the encapsulated `extension_id` /// value. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Precondition { pub extension_id: u32, pub mode: u32, @@ -61,33 +68,43 @@ impl Precondition { /// used inside of a transaction, and extension-specific types. The payload field of this struct is /// treated as opaque to all but the extension corresponding to the encapsulated `extension_id` /// value. -#[derive(Clone, Debug, PartialEq)] -pub struct Witness { +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Witness { pub extension_id: u32, pub mode: u32, - pub payload: Vec, + pub payload: T, +} + +impl Witness { + pub fn map_payload U>(self, f: F) -> Witness { + Witness { + extension_id: self.extension_id, + mode: self.mode, + payload: f(self.payload), + } + } } -impl Witness { +impl Witness { /// Produce the intermediate format for an extension-specific witness /// type. - pub fn from(extension_id: u32, value: &P) -> Witness { + pub fn from(extension_id: u32, value: &P) -> Witness { let (mode, payload) = value.to_payload(); Witness { extension_id, mode, - payload, + payload: AuthData(payload), } } /// Attempt to parse an extension-specific witness value from the /// intermediate representation. pub fn try_to(&self) -> Result { - P::from_payload(self.mode, &self.payload) + P::from_payload(self.mode, &self.payload.0) } } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub enum Error { InvalidExtensionId(u32), ProgramError(E), @@ -137,7 +154,7 @@ pub trait Extension { fn verify( &self, precondition: &Precondition, - witness: &Witness, + witness: &Witness, context: &C, ) -> Result<(), Self::Error> where @@ -146,8 +163,8 @@ pub trait Extension { { self.verify_inner( &Self::Precondition::from_payload(precondition.mode, &precondition.payload)?, - &Self::Witness::from_payload(witness.mode, &witness.payload)?, - &context, + &Self::Witness::from_payload(witness.mode, &witness.payload.0)?, + context, ) } } @@ -178,18 +195,18 @@ pub trait ExtensionTxBuilder<'a> { &mut self, extension_id: u32, mode: u32, - prevout: (TzeOutPoint, TzeOut), + prevout: (tze::OutPoint, TzeOut), witness_builder: WBuilder, ) -> Result<(), Self::BuildError> where WBuilder: 'a + (FnOnce(&Self::BuildCtx) -> Result); /// Adds a TZE precondition to the transaction which must be satisfied by a future transaction's - /// witness in order to spend the specified `amount`. + /// witness in order to spend the specified value. fn add_tze_output( &mut self, extension_id: u32, - value: Amount, + value: Zatoshis, guarded_by: &Precondition, ) -> Result<(), Self::BuildError>; } diff --git a/zcash_primitives/src/legacy.rs b/zcash_primitives/src/legacy.rs deleted file mode 100644 index 5e8b38a755..0000000000 --- a/zcash_primitives/src/legacy.rs +++ /dev/null @@ -1,204 +0,0 @@ -//! Support for legacy transparent addresses and scripts. - -use byteorder::{ReadBytesExt, WriteBytesExt}; -use std::io::{self, Read, Write}; -use std::ops::Shl; - -use crate::serialize::Vector; - -/// Minimal subset of script opcodes. -enum OpCode { - // push value - PushData1 = 0x4c, - PushData2 = 0x4d, - PushData4 = 0x4e, - - // stack ops - Dup = 0x76, - - // bit logic - Equal = 0x87, - EqualVerify = 0x88, - - // crypto - Hash160 = 0xa9, - CheckSig = 0xac, -} - -/// A serialized script, used inside transparent inputs and outputs of a transaction. -#[derive(Clone, Debug, Default, PartialEq)] -pub struct Script(pub Vec); - -impl Script { - pub fn read(mut reader: R) -> io::Result { - let script = Vector::read(&mut reader, |r| r.read_u8())?; - Ok(Script(script)) - } - - pub fn write(&self, mut writer: W) -> io::Result<()> { - Vector::write(&mut writer, &self.0, |w, e| w.write_u8(*e)) - } - - /// Returns the address that this Script contains, if any. - pub fn address(&self) -> Option { - if self.0.len() == 25 - && self.0[0..3] == [OpCode::Dup as u8, OpCode::Hash160 as u8, 0x14] - && self.0[23..25] == [OpCode::EqualVerify as u8, OpCode::CheckSig as u8] - { - let mut hash = [0; 20]; - hash.copy_from_slice(&self.0[3..23]); - Some(TransparentAddress::PublicKey(hash)) - } else if self.0.len() == 23 - && self.0[0..2] == [OpCode::Hash160 as u8, 0x14] - && self.0[22] == OpCode::Equal as u8 - { - let mut hash = [0; 20]; - hash.copy_from_slice(&self.0[2..22]); - Some(TransparentAddress::Script(hash)) - } else { - None - } - } -} - -impl Shl for Script { - type Output = Self; - - fn shl(mut self, rhs: OpCode) -> Self { - self.0.push(rhs as u8); - self - } -} - -impl Shl<&[u8]> for Script { - type Output = Self; - - fn shl(mut self, data: &[u8]) -> Self { - if data.len() < OpCode::PushData1 as usize { - self.0.push(data.len() as u8); - } else if data.len() <= 0xff { - self.0.push(OpCode::PushData1 as u8); - self.0.push(data.len() as u8); - } else if data.len() <= 0xffff { - self.0.push(OpCode::PushData2 as u8); - self.0.extend(&(data.len() as u16).to_le_bytes()); - } else { - self.0.push(OpCode::PushData4 as u8); - self.0.extend(&(data.len() as u32).to_le_bytes()); - } - self.0.extend(data); - self - } -} - -/// A transparent address corresponding to either a public key or a `Script`. -#[derive(Debug, PartialEq, PartialOrd, Hash, Clone)] -pub enum TransparentAddress { - PublicKey([u8; 20]), - Script([u8; 20]), -} - -impl TransparentAddress { - /// Generate the `scriptPubKey` corresponding to this address. - pub fn script(&self) -> Script { - match self { - TransparentAddress::PublicKey(key_id) => { - // P2PKH script - Script::default() - << OpCode::Dup - << OpCode::Hash160 - << &key_id[..] - << OpCode::EqualVerify - << OpCode::CheckSig - } - TransparentAddress::Script(script_id) => { - // P2SH script - Script::default() << OpCode::Hash160 << &script_id[..] << OpCode::Equal - } - } - } -} - -#[cfg(any(test, feature = "test-dependencies"))] -pub mod testing { - use proptest::prelude::{any, prop_compose}; - - use super::TransparentAddress; - - prop_compose! { - pub fn arb_transparent_addr()(v in proptest::array::uniform20(any::())) -> TransparentAddress { - TransparentAddress::PublicKey(v) - } - } -} - -#[cfg(test)] -mod tests { - use super::{OpCode, Script, TransparentAddress}; - - #[test] - fn script_opcode() { - { - let script = Script::default() << OpCode::PushData1; - assert_eq!(&script.0, &[OpCode::PushData1 as u8]); - } - } - - #[test] - fn script_pushdata() { - { - let script = Script::default() << &[1, 2, 3, 4][..]; - assert_eq!(&script.0, &[4, 1, 2, 3, 4]); - } - - { - let short_data = vec![2; 100]; - let script = Script::default() << &short_data[..]; - assert_eq!(script.0[0], OpCode::PushData1 as u8); - assert_eq!(script.0[1] as usize, 100); - assert_eq!(&script.0[2..], &short_data[..]); - } - - { - let medium_data = vec![7; 1024]; - let script = Script::default() << &medium_data[..]; - assert_eq!(script.0[0], OpCode::PushData2 as u8); - assert_eq!(&script.0[1..3], &[0x00, 0x04][..]); - assert_eq!(&script.0[3..], &medium_data[..]); - } - - { - let long_data = vec![42; 1_000_000]; - let script = Script::default() << &long_data[..]; - assert_eq!(script.0[0], OpCode::PushData4 as u8); - assert_eq!(&script.0[1..5], &[0x40, 0x42, 0x0f, 0x00][..]); - assert_eq!(&script.0[5..], &long_data[..]); - } - } - - #[test] - fn p2pkh() { - let addr = TransparentAddress::PublicKey([4; 20]); - assert_eq!( - &addr.script().0, - &[ - 0x76, 0xa9, 0x14, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, - 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x88, 0xac, - ] - ); - assert_eq!(addr.script().address(), Some(addr)); - } - - #[test] - fn p2sh() { - let addr = TransparentAddress::Script([7; 20]); - assert_eq!( - &addr.script().0, - &[ - 0xa9, 0x14, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, - 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x87, - ] - ); - assert_eq!(addr.script().address(), Some(addr)); - } -} diff --git a/zcash_primitives/src/lib.rs b/zcash_primitives/src/lib.rs index a2a113d404..7aac5940ae 100644 --- a/zcash_primitives/src/lib.rs +++ b/zcash_primitives/src/lib.rs @@ -2,26 +2,57 @@ //! //! `zcash_primitives` is a library that provides the core structs and functions necessary //! for working with Zcash. +//! +//! ## Feature flags +#![cfg_attr(feature = "std", doc = document_features::document_features!())] +//! #![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] // Catch documentation errors caused by code changes. -#![deny(broken_intra_doc_links)] +#![deny(rustdoc::broken_intra_doc_links)] // Temporary until we have addressed all Result cases. #![allow(clippy::result_unit_err)] +// Present to reduce refactoring noise from changing all the imports inside this crate for +// the `sapling` crate extraction. +#![allow(clippy::single_component_path_imports)] +#![no_std] + +#[cfg(feature = "std")] +extern crate std; + +#[macro_use] +extern crate alloc; pub mod block; -pub mod consensus; -pub mod constants; -pub mod legacy; -pub mod memo; +pub(crate) mod encoding; +#[cfg(zcash_unstable = "zfuture")] +pub mod extensions; pub mod merkle_tree; -pub mod sapling; -pub mod serialize; pub mod transaction; -pub mod zip32; - -#[cfg(feature = "zfuture")] -pub mod extensions; -#[cfg(test)] -mod test_vectors; +#[deprecated(note = "This module is deprecated; use `::zcash_protocol::consensus` instead.")] +pub mod consensus { + pub use zcash_protocol::consensus::*; +} +#[deprecated(note = "This module is deprecated; use `::zcash_protocol::constants` instead.")] +pub mod constants { + pub use zcash_protocol::constants::*; +} +#[deprecated(note = "This module is deprecated; use `::zcash_protocol::memo` instead.")] +pub mod memo { + pub use zcash_protocol::memo::*; +} +#[deprecated(note = "This module is deprecated; use the `zip32` crate instead.")] +pub mod zip32 { + pub use zip32::*; +} +#[deprecated(note = "This module is deprecated; use the `zcash_transparent` crate instead.")] +pub mod legacy { + pub use transparent::address::*; + #[cfg(feature = "transparent-inputs")] + #[deprecated(note = "This module is deprecated; use `::zcash_transparent::keys` instead.")] + pub mod keys { + pub use transparent::keys::*; + } +} diff --git a/zcash_primitives/src/merkle_tree.rs b/zcash_primitives/src/merkle_tree.rs index f01a218d8f..f712634fb1 100644 --- a/zcash_primitives/src/merkle_tree.rs +++ b/zcash_primitives/src/merkle_tree.rs @@ -1,506 +1,384 @@ -//! Implementation of a Merkle tree of commitments used to prove the existence of notes. +//! Parsers and serializers for Zcash Merkle trees. -use byteorder::{LittleEndian, ReadBytesExt}; -use std::collections::VecDeque; -use std::io::{self, Read, Write}; +use alloc::vec::Vec; +use core2::io::{self, Read, Write}; -use crate::sapling::SAPLING_COMMITMENT_TREE_DEPTH; -use crate::serialize::{Optional, Vector}; +use crate::encoding::{ReadBytesExt, WriteBytesExt}; +use incrementalmerkletree::{ + frontier::{CommitmentTree, Frontier, NonEmptyFrontier}, + witness::IncrementalWitness, + Address, Hashable, Level, MerklePath, Position, +}; +use orchard::tree::MerkleHashOrchard; +use zcash_encoding::{Optional, Vector}; /// A hashable node within a Merkle tree. -pub trait Hashable: Clone + Copy { +pub trait HashSer { /// Parses a node from the given byte source. - fn read(reader: R) -> io::Result; + fn read(reader: R) -> io::Result + where + Self: Sized; /// Serializes this node. fn write(&self, writer: W) -> io::Result<()>; - - /// Returns the parent node within the tree of the two given nodes. - fn combine(_: usize, _: &Self, _: &Self) -> Self; - - /// Returns a blank leaf node. - fn blank() -> Self; - - /// Returns the empty root for the given depth. - fn empty_root(_: usize) -> Self; -} - -struct PathFiller { - queue: VecDeque, } -impl PathFiller { - fn empty() -> Self { - PathFiller { - queue: VecDeque::new(), - } +impl HashSer for sapling::Node { + fn read(mut reader: R) -> io::Result { + let mut repr = [0u8; 32]; + reader.read_exact(&mut repr)?; + Option::from(Self::from_bytes(repr)).ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidData, + "Non-canonical encoding of Jubjub base field value.", + ) + }) } - fn next(&mut self, depth: usize) -> Node { - self.queue - .pop_front() - .unwrap_or_else(|| Node::empty_root(depth)) + fn write(&self, mut writer: W) -> io::Result<()> { + writer.write_all(&self.to_bytes()) } } -/// A Merkle tree of note commitments. -/// -/// The depth of the Merkle tree is fixed at 32, equal to the depth of the Sapling -/// commitment tree. -#[derive(Clone)] -pub struct CommitmentTree { - left: Option, - right: Option, - parents: Vec>, -} - -impl CommitmentTree { - /// Creates an empty tree. - pub fn empty() -> Self { - CommitmentTree { - left: None, - right: None, - parents: vec![], - } - } - - /// Reads a `CommitmentTree` from its serialized form. - #[allow(clippy::redundant_closure)] - pub fn read(mut reader: R) -> io::Result { - let left = Optional::read(&mut reader, |r| Node::read(r))?; - let right = Optional::read(&mut reader, |r| Node::read(r))?; - let parents = Vector::read(&mut reader, |r| Optional::read(r, |r| Node::read(r)))?; - - Ok(CommitmentTree { - left, - right, - parents, +impl HashSer for MerkleHashOrchard { + fn read(mut reader: R) -> io::Result + where + Self: Sized, + { + let mut repr = [0u8; 32]; + reader.read_exact(&mut repr)?; + >::from(Self::from_bytes(&repr)).ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidData, + "Non-canonical encoding of Pallas base field value.", + ) }) } - /// Serializes this tree as an array of bytes. - pub fn write(&self, mut writer: W) -> io::Result<()> { - Optional::write(&mut writer, &self.left, |w, n| n.write(w))?; - Optional::write(&mut writer, &self.right, |w, n| n.write(w))?; - Vector::write(&mut writer, &self.parents, |w, e| { - Optional::write(w, e, |w, n| n.write(w)) - }) + fn write(&self, mut writer: W) -> io::Result<()> { + writer.write_all(&self.to_bytes()) } +} - /// Returns the number of leaf nodes in the tree. - pub fn size(&self) -> usize { - self.parents.iter().enumerate().fold( - match (self.left, self.right) { - (None, None) => 0, - (Some(_), None) => 1, - (Some(_), Some(_)) => 2, - (None, Some(_)) => unreachable!(), - }, - |acc, (i, p)| { - // Treat occupation of parents array as a binary number - // (right-shifted by 1) - acc + if p.is_some() { 1 << (i + 1) } else { 0 } - }, +/// Writes a usize value encoded as a u64 in little-endian order. Since usize +/// is platform-dependent, we consistently represent it as u64 in serialized +/// formats. +pub fn write_usize_leu64(mut writer: W, value: usize) -> io::Result<()> { + writer.write_u64_le(u64::try_from(value).map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidData, + "usize value was outside the representable range of a u64", ) - } - - fn is_complete(&self, depth: usize) -> bool { - self.left.is_some() - && self.right.is_some() - && self.parents.len() == depth - 1 - && self.parents.iter().all(|p| p.is_some()) - } - - /// Adds a leaf node to the tree. - /// - /// Returns an error if the tree is full. - pub fn append(&mut self, node: Node) -> Result<(), ()> { - self.append_inner(node, SAPLING_COMMITMENT_TREE_DEPTH) - } - - fn append_inner(&mut self, node: Node, depth: usize) -> Result<(), ()> { - if self.is_complete(depth) { - // Tree is full - return Err(()); - } - - match (self.left, self.right) { - (None, _) => self.left = Some(node), - (_, None) => self.right = Some(node), - (Some(l), Some(r)) => { - let mut combined = Node::combine(0, &l, &r); - self.left = Some(node); - self.right = None; - - for i in 0..depth { - if i < self.parents.len() { - if let Some(p) = self.parents[i] { - combined = Node::combine(i + 1, &p, &combined); - self.parents[i] = None; - } else { - self.parents[i] = Some(combined); - break; - } - } else { - self.parents.push(Some(combined)); - break; - } - } - } - } - - Ok(()) - } - - /// Returns the current root of the tree. - pub fn root(&self) -> Node { - self.root_inner(SAPLING_COMMITMENT_TREE_DEPTH, PathFiller::empty()) - } + })?) +} - fn root_inner(&self, depth: usize, mut filler: PathFiller) -> Node { - assert!(depth > 0); +/// Reads a usize value encoded as a u64 in little-endian order. Since usize +/// is platform-dependent, we consistently represent it as u64 in serialized +/// formats. +pub fn read_leu64_usize(mut reader: R) -> io::Result { + let mut repr = [0u8; 8]; + reader.read_exact(&mut repr)?; + usize::try_from(u64::from_le_bytes(repr)).map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidData, + "usize could not be decoded from a 64-bit value", + ) + }) +} - // 1) Hash left and right leaves together. - // - Empty leaves are used as needed. - let leaf_root = Node::combine( - 0, - &self.left.unwrap_or_else(|| filler.next(0)), - &self.right.unwrap_or_else(|| filler.next(0)), - ); +pub fn write_position(mut writer: W, position: Position) -> io::Result<()> { + writer.write_u64_le(u64::from(position)) +} - // 2) Hash in parents up to the currently-filled depth. - // - Roots of the empty subtrees are used as needed. - let mid_root = self - .parents - .iter() - .enumerate() - .fold(leaf_root, |root, (i, p)| match p { - Some(node) => Node::combine(i + 1, node, &root), - None => Node::combine(i + 1, &root, &filler.next(i + 1)), - }); - - // 3) Hash in roots of the empty subtrees up to the final depth. - ((self.parents.len() + 1)..depth) - .fold(mid_root, |root, d| Node::combine(d, &root, &filler.next(d))) - } +pub fn read_position(mut reader: R) -> io::Result { + reader.read_u64_le().map(Position::from) } -/// An updatable witness to a path from a position in a particular [`CommitmentTree`]. -/// -/// Appending the same commitments in the same order to both the original -/// [`CommitmentTree`] and this `IncrementalWitness` will result in a witness to the path -/// from the target position to the root of the updated tree. -/// -/// # Examples -/// -/// ``` -/// use ff::{Field, PrimeField}; -/// use rand_core::OsRng; -/// use zcash_primitives::{ -/// merkle_tree::{CommitmentTree, IncrementalWitness}, -/// sapling::Node, -/// }; -/// -/// let mut rng = OsRng; -/// -/// let mut tree = CommitmentTree::::empty(); -/// -/// tree.append(Node::new(bls12_381::Scalar::random(&mut rng).to_repr())); -/// tree.append(Node::new(bls12_381::Scalar::random(&mut rng).to_repr())); -/// let mut witness = IncrementalWitness::from_tree(&tree); -/// assert_eq!(witness.position(), 1); -/// assert_eq!(tree.root(), witness.root()); -/// -/// let cmu = Node::new(bls12_381::Scalar::random(&mut rng).to_repr()); -/// tree.append(cmu); -/// witness.append(cmu); -/// assert_eq!(tree.root(), witness.root()); -/// ``` -#[derive(Clone)] -pub struct IncrementalWitness { - tree: CommitmentTree, - filled: Vec, - cursor_depth: usize, - cursor: Option>, +pub fn write_address(mut writer: W, addr: Address) -> io::Result<()> { + writer.write_u8(addr.level().into())?; + writer.write_u64_le(addr.index()) } -impl IncrementalWitness { - /// Creates an `IncrementalWitness` for the most recent commitment added to the given - /// [`CommitmentTree`]. - pub fn from_tree(tree: &CommitmentTree) -> IncrementalWitness { - IncrementalWitness { - tree: tree.clone(), - filled: vec![], - cursor_depth: 0, - cursor: None, - } - } +pub fn read_address(mut reader: R) -> io::Result

{ + let level = reader.read_u8().map(Level::from)?; + let index = reader.read_u64_le()?; + Ok(Address::from_parts(level, index)) +} - /// Reads an `IncrementalWitness` from its serialized form. - #[allow(clippy::redundant_closure)] - pub fn read(mut reader: R) -> io::Result { - let tree = CommitmentTree::read(&mut reader)?; - let filled = Vector::read(&mut reader, |r| Node::read(r))?; - let cursor = Optional::read(&mut reader, |r| CommitmentTree::read(r))?; +pub fn read_frontier_v0( + mut reader: R, +) -> io::Result> { + let tree = read_commitment_tree(&mut reader)?; - let mut witness = IncrementalWitness { - tree, - filled, - cursor_depth: 0, - cursor, - }; + Ok(tree.to_frontier()) +} - witness.cursor_depth = witness.next_depth(); +pub fn write_nonempty_frontier_v1( + mut writer: W, + frontier: &NonEmptyFrontier, +) -> io::Result<()> { + write_position(&mut writer, frontier.position())?; + if frontier.position().is_right_child() { + // The v1 serialization wrote the sibling of a right-hand leaf as an optional value, rather + // than as part of the ommers vector. + frontier + .ommers() + .first() + .expect("ommers vector cannot be empty for right-hand nodes") + .write(&mut writer)?; + Optional::write(&mut writer, Some(frontier.leaf()), |w, n: &H| n.write(w))?; + Vector::write(&mut writer, &frontier.ommers()[1..], |w, e| e.write(w))?; + } else { + frontier.leaf().write(&mut writer)?; + Optional::write(&mut writer, None, |w, n: &H| n.write(w))?; + Vector::write(&mut writer, frontier.ommers(), |w, e| e.write(w))?; + } + + Ok(()) +} - Ok(witness) - } +#[allow(clippy::redundant_closure)] +pub fn read_nonempty_frontier_v1( + mut reader: R, +) -> io::Result> { + let position = read_position(&mut reader)?; + let left = H::read(&mut reader)?; + let right = Optional::read(&mut reader, H::read)?; + let mut ommers = Vector::read(&mut reader, |r| H::read(r))?; + + let leaf = if let Some(right) = right { + // if the frontier has a right leaf, then the left leaf is the first ommer + ommers.insert(0, left); + right + } else { + left + }; + + NonEmptyFrontier::from_parts(position, leaf, ommers).map_err(|_err| { + io::Error::new( + io::ErrorKind::InvalidData, + "Parsing resulted in an invalid Merkle frontier", + ) + }) +} - /// Serializes this `IncrementalWitness` as an array of bytes. - pub fn write(&self, mut writer: W) -> io::Result<()> { - self.tree.write(&mut writer)?; - Vector::write(&mut writer, &self.filled, |w, n| n.write(w))?; - Optional::write(&mut writer, &self.cursor, |w, t| t.write(w)) - } +pub fn write_frontier_v1( + writer: W, + frontier: &Frontier, +) -> io::Result<()> { + Optional::write(writer, frontier.value(), write_nonempty_frontier_v1) +} - /// Returns the position of the witnessed leaf node in the commitment tree. - pub fn position(&self) -> usize { - self.tree.size() - 1 +#[allow(clippy::redundant_closure)] +pub fn read_frontier_v1(reader: R) -> io::Result> { + match Optional::read(reader, read_nonempty_frontier_v1)? { + None => Ok(Frontier::empty()), + Some(f) => Frontier::try_from(f).map_err(|_err| { + io::Error::new( + io::ErrorKind::InvalidData, + "Parsing resulted in an invalid Merkle frontier", + ) + }), } +} - fn filler(&self) -> PathFiller { - let cursor_root = self - .cursor - .as_ref() - .map(|c| c.root_inner(self.cursor_depth, PathFiller::empty())); +/// Reads a legacy `CommitmentTree` from its serialized form. +pub fn read_commitment_tree( + mut reader: R, +) -> io::Result> { + let left = Optional::read(&mut reader, Node::read)?; + let right = Optional::read(&mut reader, Node::read)?; + let parents = Vector::read(&mut reader, |r| Optional::read(r, Node::read))?; + + CommitmentTree::from_parts(left, right, parents).map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidData, + "parents vector exceeded tree depth", + ) + }) +} - PathFiller { - queue: self.filled.iter().cloned().chain(cursor_root).collect(), - } - } +/// Serializes a legacy `CommitmentTree` as an array of bytes. +pub fn write_commitment_tree( + tree: &CommitmentTree, + mut writer: W, +) -> io::Result<()> { + Optional::write(&mut writer, tree.left().as_ref(), |w, n| n.write(w))?; + Optional::write(&mut writer, tree.right().as_ref(), |w, n| n.write(w))?; + Vector::write(&mut writer, tree.parents(), |w, e| { + Optional::write(w, e.as_ref(), |w, n| n.write(w)) + }) +} - /// Finds the next "depth" of an unfilled subtree. - fn next_depth(&self) -> usize { - let mut skip = self.filled.len(); +/// Reads an `IncrementalWitness` from its serialized form. +#[allow(clippy::redundant_closure)] +pub fn read_incremental_witness( + mut reader: R, +) -> io::Result> { + let tree = read_commitment_tree(&mut reader)?; + let filled = Vector::read(&mut reader, |r| Node::read(r))?; + let cursor = Optional::read(&mut reader, read_commitment_tree)?; + + IncrementalWitness::from_parts(tree, filled, cursor).ok_or(io::Error::new( + io::ErrorKind::InvalidData, + "invalid witness: inconsistency detected between witness parts", + )) +} - if self.tree.left.is_none() { - if skip > 0 { - skip -= 1; - } else { - return 0; - } - } +/// Serializes an `IncrementalWitness` as an array of bytes. +pub fn write_incremental_witness( + witness: &IncrementalWitness, + mut writer: W, +) -> io::Result<()> { + write_commitment_tree(witness.tree(), &mut writer)?; + Vector::write(&mut writer, witness.filled(), |w, n| n.write(w))?; + Optional::write(&mut writer, witness.cursor().as_ref(), |w, t| { + write_commitment_tree(t, w) + }) +} - if self.tree.right.is_none() { - if skip > 0 { - skip -= 1; +/// Reads a Merkle path from its serialized form. +pub fn merkle_path_from_slice( + mut witness: &[u8], +) -> io::Result> { + // Skip the first byte, which should be DEPTH to signify the length of + // the following vector of Pedersen hashes. + if witness[0] != DEPTH { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "depth is not as expected", + )); + } + witness = &witness[1..]; + + // Begin to construct the authentication path + let iter = witness.chunks_exact(33); + witness = iter.remainder(); + + // The vector works in reverse + let auth_path = iter + .rev() + .map(|bytes| { + // Length of inner vector should be the length of a Pedersen hash + if bytes[0] == 32 { + // Sibling node should be an element of Fr + Node::read(&bytes[1..]) } else { - return 0; - } - } - - let mut d = 1; - for p in &self.tree.parents { - if p.is_none() { - if skip > 0 { - skip -= 1; - } else { - return d; - } + Err(io::Error::new( + io::ErrorKind::InvalidData, + "length of auth path element is not the expected 32 bytes", + )) } - d += 1; - } - - d + skip + }) + .collect::>>()?; + if auth_path.len() != usize::from(DEPTH) { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "auth path has unexpected length", + )); + } + + // Read the position from the witness + let position = witness.read_u64_le().map(Position::from)?; + + // The witness should be empty now; if it wasn't, the caller would + // have provided more information than they should have, indicating + // a bug downstream + if witness.is_empty() { + MerklePath::from_parts(auth_path, position).map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidData, + "auth path contained incorrect number of elements", + ) + }) + } else { + Err(io::Error::new( + io::ErrorKind::InvalidData, + "trailing data found in auth path decoding", + )) } +} - /// Tracks a leaf node that has been added to the underlying tree. - /// - /// Returns an error if the tree is full. - pub fn append(&mut self, node: Node) -> Result<(), ()> { - self.append_inner(node, SAPLING_COMMITMENT_TREE_DEPTH) - } +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing { + use crate::encoding::{ReadBytesExt, WriteBytesExt}; + use alloc::string::String; + use core2::io::{self, Read, Write}; + use incrementalmerkletree::frontier::testing::TestNode; + use zcash_encoding::Vector; - fn append_inner(&mut self, node: Node, depth: usize) -> Result<(), ()> { - if let Some(mut cursor) = self.cursor.take() { - cursor - .append_inner(node, depth) - .expect("cursor should not be full"); - if cursor.is_complete(self.cursor_depth) { - self.filled - .push(cursor.root_inner(self.cursor_depth, PathFiller::empty())); - } else { - self.cursor = Some(cursor); - } - } else { - self.cursor_depth = self.next_depth(); - if self.cursor_depth >= depth { - // Tree is full - return Err(()); - } + use super::HashSer; - if self.cursor_depth == 0 { - self.filled.push(node); - } else { - let mut cursor = CommitmentTree::empty(); - cursor - .append_inner(node, depth) - .expect("cursor should not be full"); - self.cursor = Some(cursor); - } + impl HashSer for TestNode { + fn read(mut reader: R) -> io::Result { + reader.read_u64_le().map(TestNode) } - Ok(()) - } - - /// Returns the current root of the tree corresponding to the witness. - pub fn root(&self) -> Node { - self.root_inner(SAPLING_COMMITMENT_TREE_DEPTH) - } - - fn root_inner(&self, depth: usize) -> Node { - self.tree.root_inner(depth, self.filler()) - } - - /// Returns the current witness, or None if the tree is empty. - pub fn path(&self) -> Option> { - self.path_inner(SAPLING_COMMITMENT_TREE_DEPTH) - } - - fn path_inner(&self, depth: usize) -> Option> { - let mut filler = self.filler(); - let mut auth_path = Vec::new(); - - if let Some(node) = self.tree.left { - if self.tree.right.is_some() { - auth_path.push((node, true)); - } else { - auth_path.push((filler.next(0), false)); - } - } else { - // Can't create an authentication path for the beginning of the tree - return None; + fn write(&self, mut writer: W) -> io::Result<()> { + writer.write_u64_le(self.0) } + } - for (i, p) in self.tree.parents.iter().enumerate() { - auth_path.push(match p { - Some(node) => (*node, true), - None => (filler.next(i + 1), false), - }); + impl HashSer for String { + fn read(reader: R) -> io::Result { + Vector::read(reader, |r| r.read_u8()).and_then(|xs| { + String::from_utf8(xs).map_err(|_e| { + io::Error::new(io::ErrorKind::InvalidData, "not a valid utf8 string") + }) + }) } - for i in self.tree.parents.len()..(depth - 1) { - auth_path.push((filler.next(i + 1), false)); + fn write(&self, writer: W) -> io::Result<()> { + Vector::write(writer, self.as_bytes(), |w, b| w.write_all(&[*b])) } - assert_eq!(auth_path.len(), depth); - - Some(MerklePath::from_path(auth_path, self.position() as u64)) } } -/// A path from a position in a particular commitment tree to the root of that tree. -#[derive(Clone, Debug, PartialEq)] -pub struct MerklePath { - pub auth_path: Vec<(Node, bool)>, - pub position: u64, -} +#[cfg(test)] +mod tests { + use alloc::string::{String, ToString}; + use alloc::vec::Vec; + use assert_matches::assert_matches; + use incrementalmerkletree::{ + frontier::{testing::arb_commitment_tree, Frontier, PathFiller}, + witness::IncrementalWitness, + Hashable, + }; + use proptest::prelude::*; + use proptest::strategy::Strategy; -impl MerklePath { - /// Constructs a Merkle path directly from a path and position. - pub fn from_path(auth_path: Vec<(Node, bool)>, position: u64) -> Self { - MerklePath { - auth_path, - position, - } - } + use super::{ + merkle_path_from_slice, read_commitment_tree, read_frontier_v0, read_frontier_v1, + read_incremental_witness, write_commitment_tree, write_frontier_v1, + write_incremental_witness, CommitmentTree, HashSer, + }; + use ::sapling::{self, Node}; - /// Reads a Merkle path from its serialized form. - pub fn from_slice(witness: &[u8]) -> Result { - Self::from_slice_with_depth(witness, SAPLING_COMMITMENT_TREE_DEPTH) - } + proptest! { + #[test] + fn frontier_serialization_v0(t in arb_commitment_tree::<_, _, 32>(0, sapling::testing::arb_node())) + { + let mut buffer = vec![]; + write_commitment_tree(&t, &mut buffer).unwrap(); + let frontier: Frontier = read_frontier_v0(&buffer[..]).unwrap(); - fn from_slice_with_depth(mut witness: &[u8], depth: usize) -> Result { - // Skip the first byte, which should be "depth" to signify the length of - // the following vector of Pedersen hashes. - if witness[0] != depth as u8 { - return Err(()); - } - witness = &witness[1..]; - - // Begin to construct the authentication path - let iter = witness.chunks_exact(33); - witness = iter.remainder(); - - // The vector works in reverse - let mut auth_path = iter - .rev() - .map(|bytes| { - // Length of inner vector should be the length of a Pedersen hash - if bytes[0] == 32 { - // Sibling node should be an element of Fr - Node::read(&bytes[1..]) - .map(|sibling| { - // Set the value in the auth path; we put false here - // for now (signifying the position bit) which we'll - // fill in later. - (sibling, false) - }) - .map_err(|_| ()) - } else { - Err(()) - } - }) - .collect::, _>>()?; - if auth_path.len() != depth { - return Err(()); + let expected: Frontier = t.to_frontier(); + assert_eq!(frontier, expected); } - // Read the position from the witness - let position = witness.read_u64::().map_err(|_| ())?; + #[test] + fn frontier_serialization_v1(t in arb_commitment_tree::<_, _, 32>(1, sapling::testing::arb_node())) + { + let original: Frontier = t.to_frontier(); - // Given the position, let's finish constructing the authentication - // path - let mut tmp = position; - for entry in auth_path.iter_mut() { - entry.1 = (tmp & 1) == 1; - tmp >>= 1; - } + let mut buffer = vec![]; + write_frontier_v1(&mut buffer, &original).unwrap(); + let read: Frontier = read_frontier_v1(&buffer[..]).unwrap(); - // The witness should be empty now; if it wasn't, the caller would - // have provided more information than they should have, indicating - // a bug downstream - if witness.is_empty() { - Ok(MerklePath { - auth_path, - position, - }) - } else { - Err(()) + assert_eq!(read, original); } } - /// Returns the root of the tree corresponding to this path applied to `leaf`. - pub fn root(&self, leaf: Node) -> Node { - self.auth_path - .iter() - .enumerate() - .fold( - leaf, - |root, (i, (p, leaf_is_on_right))| match leaf_is_on_right { - false => Node::combine(i, &root, p), - true => Node::combine(i, p, &root), - }, - ) - } -} - -#[cfg(test)] -mod tests { - use super::{CommitmentTree, Hashable, IncrementalWitness, MerklePath, PathFiller}; - use crate::sapling::Node; - - use std::convert::TryInto; - use std::io::{self, Read, Write}; - const HEX_EMPTY_ROOTS: [&str; 33] = [ "0100000000000000000000000000000000000000000000000000000000000000", "817de36ab2d57feb077634bca77819c8e0bd298c04f6fed0e6a83cc1356ca155", @@ -537,71 +415,11 @@ mod tests { "fbc2f4300c01f0b7820d00e3347c8da4ee614674376cbc45359daa54f9b5493e", ]; - const TESTING_DEPTH: usize = 4; - - struct TestCommitmentTree(CommitmentTree); - - impl TestCommitmentTree { - fn new() -> Self { - TestCommitmentTree(CommitmentTree::empty()) - } - - pub fn read(reader: R) -> io::Result { - let tree = CommitmentTree::read(reader)?; - Ok(TestCommitmentTree(tree)) - } - - pub fn write(&self, writer: W) -> io::Result<()> { - self.0.write(writer) - } - - fn size(&self) -> usize { - self.0.size() - } - - fn append(&mut self, node: Node) -> Result<(), ()> { - self.0.append_inner(node, TESTING_DEPTH) - } - - fn root(&self) -> Node { - self.0.root_inner(TESTING_DEPTH, PathFiller::empty()) - } - } - - struct TestIncrementalWitness(IncrementalWitness); - - impl TestIncrementalWitness { - fn from_tree(tree: &TestCommitmentTree) -> Self { - TestIncrementalWitness(IncrementalWitness::from_tree(&tree.0)) - } - - pub fn read(reader: R) -> io::Result { - let witness = IncrementalWitness::read(reader)?; - Ok(TestIncrementalWitness(witness)) - } - - pub fn write(&self, writer: W) -> io::Result<()> { - self.0.write(writer) - } - - fn append(&mut self, node: Node) -> Result<(), ()> { - self.0.append_inner(node, TESTING_DEPTH) - } - - fn root(&self) -> Node { - self.0.root_inner(TESTING_DEPTH) - } - - fn path(&self) -> Option> { - self.0.path_inner(TESTING_DEPTH) - } - } - #[test] fn empty_root_test_vectors() { let mut tmp = [0u8; 32]; - for (i, &expected) in HEX_EMPTY_ROOTS.iter().enumerate() { - Node::empty_root(i) + for (&expected, i) in HEX_EMPTY_ROOTS.iter().zip(0u8..) { + Node::empty_root(i.into()) .write(&mut tmp[..]) .expect("length is 32 bytes"); assert_eq!(hex::encode(tmp), expected); @@ -611,7 +429,7 @@ mod tests { #[test] fn sapling_empty_root() { let mut tmp = [0u8; 32]; - CommitmentTree::::empty() + sapling::CommitmentTree::empty() .root() .write(&mut tmp[..]) .expect("length is 32 bytes"); @@ -623,10 +441,10 @@ mod tests { #[test] fn empty_commitment_tree_roots() { - let tree = CommitmentTree::::empty(); + let tree = sapling::CommitmentTree::empty(); let mut tmp = [0u8; 32]; - for (i, &expected) in HEX_EMPTY_ROOTS.iter().enumerate().skip(1) { - tree.root_inner(i, PathFiller::empty()) + for (&expected, i) in HEX_EMPTY_ROOTS.iter().zip(0u8..).skip(1) { + tree.root_at_depth(i, PathFiller::empty()) .write(&mut tmp[..]) .expect("length is 32 bytes"); assert_eq!(hex::encode(tmp), expected); @@ -960,40 +778,46 @@ mod tests { "01f43e3aac61e5a753062d4d0508c26ceaf5e4c0c58ba3c956e104b5d2cf67c41c0003015991131c5c25911b35fcea2a8343e2dfd7a4d5b45493390e0cb184394d91c34901002df68503da9247dfde6585cb8c9fa94897cf21735f8fc1b32116ef474de05c010d6b42350c11df4fcc17987c13d8492ba4e8b3f31eb9baff9be5d8890cfa512d013a3661bc12b72646c94bc6c92796e81953985ee62d80a9ec3645a9a95740ac1500", ]; + const TESTING_DEPTH: u8 = 4; + fn assert_root_eq(root: Node, expected: &str) { let mut tmp = [0u8; 32]; root.write(&mut tmp[..]).expect("length is 32 bytes"); assert_eq!(hex::encode(tmp), expected); } - fn assert_tree_ser_eq(tree: &TestCommitmentTree, expected: &str) { + fn assert_tree_ser_eq(tree: &CommitmentTree, expected: &str) { // Check that the tree matches its encoding let mut tmp = Vec::new(); - tree.write(&mut tmp).unwrap(); + write_commitment_tree(tree, &mut tmp).unwrap(); assert_eq!(hex::encode(&tmp[..]), expected); // Check round-trip encoding - let decoded = TestCommitmentTree::read(&hex::decode(expected).unwrap()[..]).unwrap(); + let decoded: CommitmentTree = + read_commitment_tree(&hex::decode(expected).unwrap()[..]).unwrap(); tmp.clear(); - decoded.write(&mut tmp).unwrap(); + write_commitment_tree(&decoded, &mut tmp).unwrap(); assert_eq!(hex::encode(tmp), expected); } - fn assert_witness_ser_eq(witness: &TestIncrementalWitness, expected: &str) { + fn assert_witness_ser_eq( + witness: &IncrementalWitness, + expected: &str, + ) { // Check that the witness matches its encoding let mut tmp = Vec::new(); - witness.write(&mut tmp).unwrap(); + write_incremental_witness(witness, &mut tmp).unwrap(); assert_eq!(hex::encode(&tmp[..]), expected); // Check round-trip encoding - let decoded = - TestIncrementalWitness::read(&hex::decode(expected).unwrap()[..]).unwrap(); + let decoded: IncrementalWitness = + read_incremental_witness(&hex::decode(expected).unwrap()[..]).unwrap(); tmp.clear(); - decoded.write(&mut tmp).unwrap(); + write_incremental_witness(&decoded, &mut tmp).unwrap(); assert_eq!(hex::encode(tmp), expected); } - let mut tree = TestCommitmentTree::new(); + let mut tree = CommitmentTree::::empty(); assert_eq!(tree.size(), 0); let mut witnesses = vec![]; @@ -1003,10 +827,17 @@ mod tests { for i in 0..16 { let cmu = hex::decode(commitments[i]).unwrap(); - let cmu = Node::new(cmu[..].try_into().unwrap()); + let cmu = Node::from_bytes(cmu[..].try_into().unwrap()).unwrap(); // Witness here - witnesses.push((TestIncrementalWitness::from_tree(&tree), last_cmu)); + witnesses.push(( + if tree.is_empty() { + IncrementalWitness::invalid_empty_witness() + } else { + IncrementalWitness::from_tree(tree.clone()).unwrap() + }, + last_cmu, + )); // Now append a commitment to the tree assert!(tree.append(cmu).is_ok()); @@ -1026,12 +857,10 @@ mod tests { if let Some(leaf) = leaf { let path = witness.path().expect("should be able to create a path"); - let expected = MerklePath::from_slice_with_depth( - &hex::decode(paths[paths_i]).unwrap(), - TESTING_DEPTH, - ) - .unwrap(); + let expected = + merkle_path_from_slice(&hex::decode(paths[paths_i]).unwrap()).unwrap(); assert_eq!(path, expected); + assert_eq!(path.root(*leaf), witness.root()); paths_i += 1; } else { @@ -1050,10 +879,37 @@ mod tests { } // Tree should be full now - let node = Node::blank(); + let node = Node::empty_leaf(); assert!(tree.append(node).is_err()); for (witness, _) in witnesses.as_mut_slice() { assert!(witness.append(node).is_err()); } } + + proptest! { + #[test] + fn prop_commitment_tree_roundtrip_str(ct in arb_commitment_tree::<_, _, 8>(32, any::().prop_map(|c| c.to_string()))) { + let frontier: Frontier = ct.to_frontier(); + let ct0 = CommitmentTree::from_frontier(&frontier); + assert_eq!(ct, ct0); + let frontier0: Frontier = ct0.to_frontier(); + assert_eq!(frontier, frontier0); + } + + #[test] + fn prop_commitment_tree_roundtrip_node(ct in arb_commitment_tree::<_, _, 8>(32, sapling::testing::arb_node())) { + let frontier: Frontier = ct.to_frontier(); + let ct0 = CommitmentTree::from_frontier(&frontier); + assert_eq!(ct, ct0); + let frontier0: Frontier = ct0.to_frontier(); + assert_eq!(frontier, frontier0); + } + + #[test] + fn prop_commitment_tree_roundtrip_ser(ct in arb_commitment_tree::<_, _, 8>(32, sapling::testing::arb_node())) { + let mut serialized = vec![]; + assert_matches!(write_commitment_tree(&ct, &mut serialized), Ok(())); + assert_matches!(read_commitment_tree::<_, _, 8>(&serialized[..]), Ok(ct_out) if ct == ct_out); + } + } } diff --git a/zcash_primitives/src/sapling.rs b/zcash_primitives/src/sapling.rs deleted file mode 100644 index f99f8efe14..0000000000 --- a/zcash_primitives/src/sapling.rs +++ /dev/null @@ -1,487 +0,0 @@ -//! Structs and constants specific to the Sapling shielded pool. - -pub mod group_hash; -pub mod keys; -pub mod note_encryption; -pub mod pedersen_hash; -pub mod prover; -pub mod redjubjub; -pub mod util; - -use bitvec::{order::Lsb0, view::AsBits}; -use blake2s_simd::Params as Blake2sParams; -use byteorder::{LittleEndian, WriteBytesExt}; -use ff::{Field, PrimeField}; -use group::{Curve, Group, GroupEncoding}; -use lazy_static::lazy_static; -use rand_core::{CryptoRng, RngCore}; -use std::array::TryFromSliceError; -use std::convert::TryInto; -use std::io::{self, Read, Write}; -use subtle::{Choice, ConstantTimeEq}; - -use crate::{ - constants::{self, SPENDING_KEY_GENERATOR}, - merkle_tree::Hashable, -}; - -use self::{ - group_hash::group_hash, - keys::prf_expand, - pedersen_hash::{pedersen_hash, Personalization}, - redjubjub::{PrivateKey, PublicKey, Signature}, -}; - -pub const SAPLING_COMMITMENT_TREE_DEPTH: usize = 32; - -/// Compute a parent node in the Sapling commitment tree given its two children. -pub fn merkle_hash(depth: usize, lhs: &[u8; 32], rhs: &[u8; 32]) -> [u8; 32] { - let lhs = { - let mut tmp = [false; 256]; - for (a, b) in tmp.iter_mut().zip(lhs.as_bits::()) { - *a = *b; - } - tmp - }; - - let rhs = { - let mut tmp = [false; 256]; - for (a, b) in tmp.iter_mut().zip(rhs.as_bits::()) { - *a = *b; - } - tmp - }; - - jubjub::ExtendedPoint::from(pedersen_hash( - Personalization::MerkleTree(depth), - lhs.iter() - .copied() - .take(bls12_381::Scalar::NUM_BITS as usize) - .chain( - rhs.iter() - .copied() - .take(bls12_381::Scalar::NUM_BITS as usize), - ), - )) - .to_affine() - .get_u() - .to_repr() -} - -/// A node within the Sapling commitment tree. -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct Node { - repr: [u8; 32], -} - -impl Node { - pub fn new(repr: [u8; 32]) -> Self { - Node { repr } - } -} - -impl Hashable for Node { - fn read(mut reader: R) -> io::Result { - let mut repr = [0u8; 32]; - reader.read_exact(&mut repr)?; - Ok(Node::new(repr)) - } - - fn write(&self, mut writer: W) -> io::Result<()> { - writer.write_all(self.repr.as_ref()) - } - - fn combine(depth: usize, lhs: &Self, rhs: &Self) -> Self { - Node { - repr: merkle_hash(depth, &lhs.repr, &rhs.repr), - } - } - - fn blank() -> Self { - Node { - repr: Note::uncommitted().to_repr(), - } - } - - fn empty_root(depth: usize) -> Self { - EMPTY_ROOTS[depth] - } -} - -impl From for bls12_381::Scalar { - fn from(node: Node) -> Self { - bls12_381::Scalar::from_repr(node.repr).expect("Tree nodes should be in the prime field") - } -} - -lazy_static! { - static ref EMPTY_ROOTS: Vec = { - let mut v = vec![Node::blank()]; - for d in 0..SAPLING_COMMITMENT_TREE_DEPTH { - let next = Node::combine(d, &v[d], &v[d]); - v.push(next); - } - v - }; -} - -/// Create the spendAuthSig for a Sapling SpendDescription. -pub fn spend_sig( - ask: PrivateKey, - ar: jubjub::Fr, - sighash: &[u8; 32], - rng: &mut R, -) -> Signature { - spend_sig_internal(ask, ar, sighash, rng) -} - -pub(crate) fn spend_sig_internal( - ask: PrivateKey, - ar: jubjub::Fr, - sighash: &[u8; 32], - rng: &mut R, -) -> Signature { - // We compute `rsk`... - let rsk = ask.randomize(ar); - - // We compute `rk` from there (needed for key prefixing) - let rk = PublicKey::from_private(&rsk, SPENDING_KEY_GENERATOR); - - // Compute the signature's message for rk/spend_auth_sig - let mut data_to_be_signed = [0u8; 64]; - data_to_be_signed[0..32].copy_from_slice(&rk.0.to_bytes()); - (&mut data_to_be_signed[32..64]).copy_from_slice(&sighash[..]); - - // Do the signing - rsk.sign(&data_to_be_signed, rng, SPENDING_KEY_GENERATOR) -} - -#[derive(Clone)] -pub struct ValueCommitment { - pub value: u64, - pub randomness: jubjub::Fr, -} - -impl ValueCommitment { - pub fn commitment(&self) -> jubjub::SubgroupPoint { - (constants::VALUE_COMMITMENT_VALUE_GENERATOR * jubjub::Fr::from(self.value)) - + (constants::VALUE_COMMITMENT_RANDOMNESS_GENERATOR * self.randomness) - } -} - -#[derive(Clone)] -pub struct ProofGenerationKey { - pub ak: jubjub::SubgroupPoint, - pub nsk: jubjub::Fr, -} - -impl ProofGenerationKey { - pub fn to_viewing_key(&self) -> ViewingKey { - ViewingKey { - ak: self.ak, - nk: constants::PROOF_GENERATION_KEY_GENERATOR * self.nsk, - } - } -} - -#[derive(Debug, Clone)] -pub struct ViewingKey { - pub ak: jubjub::SubgroupPoint, - pub nk: jubjub::SubgroupPoint, -} - -impl ViewingKey { - pub fn rk(&self, ar: jubjub::Fr) -> jubjub::SubgroupPoint { - self.ak + constants::SPENDING_KEY_GENERATOR * ar - } - - pub fn ivk(&self) -> SaplingIvk { - let mut h = [0; 32]; - h.copy_from_slice( - Blake2sParams::new() - .hash_length(32) - .personal(constants::CRH_IVK_PERSONALIZATION) - .to_state() - .update(&self.ak.to_bytes()) - .update(&self.nk.to_bytes()) - .finalize() - .as_bytes(), - ); - - // Drop the most significant five bits, so it can be interpreted as a scalar. - h[31] &= 0b0000_0111; - - SaplingIvk(jubjub::Fr::from_repr(h).expect("should be a valid scalar")) - } - - pub fn to_payment_address(&self, diversifier: Diversifier) -> Option { - self.ivk().to_payment_address(diversifier) - } -} - -#[derive(Debug, Clone)] -pub struct SaplingIvk(pub jubjub::Fr); - -impl SaplingIvk { - pub fn to_payment_address(&self, diversifier: Diversifier) -> Option { - diversifier.g_d().and_then(|g_d| { - let pk_d = g_d * self.0; - - PaymentAddress::from_parts(diversifier, pk_d) - }) - } - - pub fn to_repr(&self) -> [u8; 32] { - self.0.to_repr() - } -} - -#[derive(Copy, Clone, Debug, PartialEq)] -pub struct Diversifier(pub [u8; 11]); - -impl Diversifier { - pub fn g_d(&self) -> Option { - group_hash(&self.0, constants::KEY_DIVERSIFICATION_PERSONALIZATION) - } -} - -/// A Sapling payment address. -/// -/// # Invariants -/// -/// `pk_d` is guaranteed to be prime-order (i.e. in the prime-order subgroup of Jubjub, -/// and not the identity). -#[derive(Clone, Debug)] -pub struct PaymentAddress { - pk_d: jubjub::SubgroupPoint, - diversifier: Diversifier, -} - -impl PartialEq for PaymentAddress { - fn eq(&self, other: &Self) -> bool { - self.pk_d == other.pk_d && self.diversifier == other.diversifier - } -} - -impl PaymentAddress { - /// Constructs a PaymentAddress from a diversifier and a Jubjub point. - /// - /// Returns None if `pk_d` is the identity. - pub fn from_parts(diversifier: Diversifier, pk_d: jubjub::SubgroupPoint) -> Option { - if pk_d.is_identity().into() { - None - } else { - Some(PaymentAddress { pk_d, diversifier }) - } - } - - /// Constructs a PaymentAddress from a diversifier and a Jubjub point. - /// - /// Only for test code, as this explicitly bypasses the invariant. - #[cfg(test)] - pub(crate) fn from_parts_unchecked( - diversifier: Diversifier, - pk_d: jubjub::SubgroupPoint, - ) -> Self { - PaymentAddress { pk_d, diversifier } - } - - /// Parses a PaymentAddress from bytes. - pub fn from_bytes(bytes: &[u8; 43]) -> Option { - let diversifier = { - let mut tmp = [0; 11]; - tmp.copy_from_slice(&bytes[0..11]); - Diversifier(tmp) - }; - // Check that the diversifier is valid - diversifier.g_d()?; - - let pk_d = jubjub::SubgroupPoint::from_bytes(bytes[11..43].try_into().unwrap()); - if pk_d.is_some().into() { - PaymentAddress::from_parts(diversifier, pk_d.unwrap()) - } else { - None - } - } - - /// Returns the byte encoding of this `PaymentAddress`. - pub fn to_bytes(&self) -> [u8; 43] { - let mut bytes = [0; 43]; - bytes[0..11].copy_from_slice(&self.diversifier.0); - bytes[11..].copy_from_slice(&self.pk_d.to_bytes()); - bytes - } - - /// Returns the [`Diversifier`] for this `PaymentAddress`. - pub fn diversifier(&self) -> &Diversifier { - &self.diversifier - } - - /// Returns `pk_d` for this `PaymentAddress`. - pub fn pk_d(&self) -> &jubjub::SubgroupPoint { - &self.pk_d - } - - pub fn g_d(&self) -> Option { - self.diversifier.g_d() - } - - pub fn create_note(&self, value: u64, rseed: Rseed) -> Option { - self.g_d().map(|g_d| Note { - value, - rseed, - g_d, - pk_d: self.pk_d, - }) - } -} - -/// Enum for note randomness before and after [ZIP 212](https://zips.z.cash/zip-0212). -/// -/// Before ZIP 212, the note commitment trapdoor `rcm` must be a scalar value. -/// After ZIP 212, the note randomness `rseed` is a 32-byte sequence, used to derive -/// both the note commitment trapdoor `rcm` and the ephemeral private key `esk`. -#[derive(Copy, Clone, Debug)] -pub enum Rseed { - BeforeZip212(jubjub::Fr), - AfterZip212([u8; 32]), -} - -/// Typesafe wrapper for nullifier values. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct Nullifier(pub [u8; 32]); - -impl Nullifier { - pub fn from_slice(bytes: &[u8]) -> Result { - bytes.try_into().map(Nullifier) - } - - pub fn to_vec(&self) -> Vec { - self.0.to_vec() - } -} - -impl ConstantTimeEq for Nullifier { - fn ct_eq(&self, other: &Self) -> Choice { - self.0.ct_eq(&other.0) - } -} - -#[derive(Clone, Debug)] -pub struct Note { - /// The value of the note - pub value: u64, - /// The diversified base of the address, GH(d) - pub g_d: jubjub::SubgroupPoint, - /// The public key of the address, g_d^ivk - pub pk_d: jubjub::SubgroupPoint, - /// rseed - pub rseed: Rseed, -} - -impl PartialEq for Note { - fn eq(&self, other: &Self) -> bool { - self.value == other.value - && self.g_d == other.g_d - && self.pk_d == other.pk_d - && self.rcm() == other.rcm() - } -} - -impl Note { - pub fn uncommitted() -> bls12_381::Scalar { - // The smallest u-coordinate that is not on the curve - // is one. - bls12_381::Scalar::one() - } - - /// Computes the note commitment, returning the full point. - fn cm_full_point(&self) -> jubjub::SubgroupPoint { - // Calculate the note contents, as bytes - let mut note_contents = vec![]; - - // Writing the value in little endian - (&mut note_contents) - .write_u64::(self.value) - .unwrap(); - - // Write g_d - note_contents.extend_from_slice(&self.g_d.to_bytes()); - - // Write pk_d - note_contents.extend_from_slice(&self.pk_d.to_bytes()); - - assert_eq!(note_contents.len(), 32 + 32 + 8); - - // Compute the Pedersen hash of the note contents - let hash_of_contents = pedersen_hash( - Personalization::NoteCommitment, - note_contents - .into_iter() - .flat_map(|byte| (0..8).map(move |i| ((byte >> i) & 1) == 1)), - ); - - // Compute final commitment - (constants::NOTE_COMMITMENT_RANDOMNESS_GENERATOR * self.rcm()) + hash_of_contents - } - - /// Computes the nullifier given the viewing key and - /// note position - pub fn nf(&self, viewing_key: &ViewingKey, position: u64) -> Nullifier { - // Compute rho = cm + position.G - let rho = self.cm_full_point() - + (constants::NULLIFIER_POSITION_GENERATOR * jubjub::Fr::from(position)); - - // Compute nf = BLAKE2s(nk | rho) - Nullifier::from_slice( - Blake2sParams::new() - .hash_length(32) - .personal(constants::PRF_NF_PERSONALIZATION) - .to_state() - .update(&viewing_key.nk.to_bytes()) - .update(&rho.to_bytes()) - .finalize() - .as_bytes(), - ) - .unwrap() - } - - /// Computes the note commitment - pub fn cmu(&self) -> bls12_381::Scalar { - // The commitment is in the prime order subgroup, so mapping the - // commitment to the u-coordinate is an injective encoding. - jubjub::ExtendedPoint::from(self.cm_full_point()) - .to_affine() - .get_u() - } - - pub fn rcm(&self) -> jubjub::Fr { - match self.rseed { - Rseed::BeforeZip212(rcm) => rcm, - Rseed::AfterZip212(rseed) => { - jubjub::Fr::from_bytes_wide(prf_expand(&rseed, &[0x04]).as_array()) - } - } - } - - pub fn generate_or_derive_esk(&self, rng: &mut R) -> jubjub::Fr { - self.generate_or_derive_esk_internal(rng) - } - - pub(crate) fn generate_or_derive_esk_internal(&self, rng: &mut R) -> jubjub::Fr { - match self.derive_esk() { - None => jubjub::Fr::random(rng), - Some(esk) => esk, - } - } - - /// Returns the derived `esk` if this note was created after ZIP 212 activated. - pub fn derive_esk(&self) -> Option { - match self.rseed { - Rseed::BeforeZip212(_) => None, - Rseed::AfterZip212(rseed) => Some(jubjub::Fr::from_bytes_wide( - prf_expand(&rseed, &[0x05]).as_array(), - )), - } - } -} diff --git a/zcash_primitives/src/sapling/group_hash.rs b/zcash_primitives/src/sapling/group_hash.rs deleted file mode 100644 index 5a9f06a096..0000000000 --- a/zcash_primitives/src/sapling/group_hash.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! Implementation of [group hashing into Jubjub][grouphash]. -//! -//! [grouphash]: https://zips.z.cash/protocol/protocol.pdf#concretegrouphashjubjub - -use ff::PrimeField; -use group::{cofactor::CofactorGroup, Group, GroupEncoding}; - -use crate::constants; -use blake2s_simd::Params; - -/// Produces a random point in the Jubjub curve. -/// The point is guaranteed to be prime order -/// and not the identity. -#[allow(clippy::assertions_on_constants)] -pub fn group_hash(tag: &[u8], personalization: &[u8]) -> Option { - assert_eq!(personalization.len(), 8); - - // Check to see that scalar field is 255 bits - assert!(bls12_381::Scalar::NUM_BITS == 255); - - let h = Params::new() - .hash_length(32) - .personal(personalization) - .to_state() - .update(constants::GH_FIRST_BLOCK) - .update(tag) - .finalize(); - - let p = jubjub::ExtendedPoint::from_bytes(h.as_array()); - if p.is_some().into() { - // ::clear_cofactor is implemented using - // ExtendedPoint::mul_by_cofactor in the jubjub crate. - let p = CofactorGroup::clear_cofactor(&p.unwrap()); - - if p.is_identity().into() { - None - } else { - Some(p) - } - } else { - None - } -} diff --git a/zcash_primitives/src/sapling/keys.rs b/zcash_primitives/src/sapling/keys.rs deleted file mode 100644 index 27b302ae8c..0000000000 --- a/zcash_primitives/src/sapling/keys.rs +++ /dev/null @@ -1,236 +0,0 @@ -//! Sapling key components. -//! -//! Implements [section 4.2.2] of the Zcash Protocol Specification. -//! -//! [section 4.2.2]: https://zips.z.cash/protocol/protocol.pdf#saplingkeycomponents - -use crate::{ - constants::{PROOF_GENERATION_KEY_GENERATOR, SPENDING_KEY_GENERATOR}, - sapling::{ProofGenerationKey, ViewingKey}, -}; -use blake2b_simd::{Hash as Blake2bHash, Params as Blake2bParams}; -use ff::PrimeField; -use group::{Group, GroupEncoding}; -use std::io::{self, Read, Write}; -use subtle::CtOption; - -pub const PRF_EXPAND_PERSONALIZATION: &[u8; 16] = b"Zcash_ExpandSeed"; - -/// PRF^expand(sk, t) := BLAKE2b-512("Zcash_ExpandSeed", sk || t) -pub fn prf_expand(sk: &[u8], t: &[u8]) -> Blake2bHash { - prf_expand_vec(sk, &[t]) -} - -pub fn prf_expand_vec(sk: &[u8], ts: &[&[u8]]) -> Blake2bHash { - let mut h = Blake2bParams::new() - .hash_length(64) - .personal(PRF_EXPAND_PERSONALIZATION) - .to_state(); - h.update(sk); - for t in ts { - h.update(t); - } - h.finalize() -} - -/// An outgoing viewing key -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct OutgoingViewingKey(pub [u8; 32]); - -/// A Sapling expanded spending key -#[derive(Clone)] -pub struct ExpandedSpendingKey { - pub ask: jubjub::Fr, - pub nsk: jubjub::Fr, - pub ovk: OutgoingViewingKey, -} - -/// A Sapling full viewing key -#[derive(Debug)] -pub struct FullViewingKey { - pub vk: ViewingKey, - pub ovk: OutgoingViewingKey, -} - -impl ExpandedSpendingKey { - pub fn from_spending_key(sk: &[u8]) -> Self { - let ask = jubjub::Fr::from_bytes_wide(prf_expand(sk, &[0x00]).as_array()); - let nsk = jubjub::Fr::from_bytes_wide(prf_expand(sk, &[0x01]).as_array()); - let mut ovk = OutgoingViewingKey([0u8; 32]); - ovk.0 - .copy_from_slice(&prf_expand(sk, &[0x02]).as_bytes()[..32]); - ExpandedSpendingKey { ask, nsk, ovk } - } - - pub fn proof_generation_key(&self) -> ProofGenerationKey { - ProofGenerationKey { - ak: SPENDING_KEY_GENERATOR * self.ask, - nsk: self.nsk, - } - } - - pub fn read(mut reader: R) -> io::Result { - let mut ask_repr = [0u8; 32]; - reader.read_exact(ask_repr.as_mut())?; - let ask = jubjub::Fr::from_repr(ask_repr) - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "ask not in field"))?; - - let mut nsk_repr = [0u8; 32]; - reader.read_exact(nsk_repr.as_mut())?; - let nsk = jubjub::Fr::from_repr(nsk_repr) - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "nsk not in field"))?; - - let mut ovk = [0u8; 32]; - reader.read_exact(&mut ovk)?; - - Ok(ExpandedSpendingKey { - ask, - nsk, - ovk: OutgoingViewingKey(ovk), - }) - } - - pub fn write(&self, mut writer: W) -> io::Result<()> { - writer.write_all(self.ask.to_repr().as_ref())?; - writer.write_all(self.nsk.to_repr().as_ref())?; - writer.write_all(&self.ovk.0)?; - - Ok(()) - } - - pub fn to_bytes(&self) -> [u8; 96] { - let mut result = [0u8; 96]; - self.write(&mut result[..]) - .expect("should be able to serialize an ExpandedSpendingKey"); - result - } -} - -impl Clone for FullViewingKey { - fn clone(&self) -> Self { - FullViewingKey { - vk: ViewingKey { - ak: self.vk.ak, - nk: self.vk.nk, - }, - ovk: self.ovk, - } - } -} - -impl FullViewingKey { - pub fn from_expanded_spending_key(expsk: &ExpandedSpendingKey) -> Self { - FullViewingKey { - vk: ViewingKey { - ak: SPENDING_KEY_GENERATOR * expsk.ask, - nk: PROOF_GENERATION_KEY_GENERATOR * expsk.nsk, - }, - ovk: expsk.ovk, - } - } - - pub fn read(mut reader: R) -> io::Result { - let ak = { - let mut buf = [0u8; 32]; - reader.read_exact(&mut buf)?; - jubjub::SubgroupPoint::from_bytes(&buf).and_then(|p| CtOption::new(p, !p.is_identity())) - }; - let nk = { - let mut buf = [0u8; 32]; - reader.read_exact(&mut buf)?; - jubjub::SubgroupPoint::from_bytes(&buf) - }; - if ak.is_none().into() { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "ak not of prime order", - )); - } - if nk.is_none().into() { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "nk not in prime-order subgroup", - )); - } - let ak = ak.unwrap(); - let nk = nk.unwrap(); - - let mut ovk = [0u8; 32]; - reader.read_exact(&mut ovk)?; - - Ok(FullViewingKey { - vk: ViewingKey { ak, nk }, - ovk: OutgoingViewingKey(ovk), - }) - } - - pub fn write(&self, mut writer: W) -> io::Result<()> { - writer.write_all(&self.vk.ak.to_bytes())?; - writer.write_all(&self.vk.nk.to_bytes())?; - writer.write_all(&self.ovk.0)?; - - Ok(()) - } - - pub fn to_bytes(&self) -> [u8; 96] { - let mut result = [0u8; 96]; - self.write(&mut result[..]) - .expect("should be able to serialize a FullViewingKey"); - result - } -} - -#[cfg(any(test, feature = "test-dependencies"))] -pub mod testing { - use proptest::collection::vec; - use proptest::prelude::{any, prop_compose}; - - use crate::{ - sapling::PaymentAddress, - zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, - }; - - prop_compose! { - pub fn arb_extended_spending_key()(v in vec(any::(), 32..252)) -> ExtendedSpendingKey { - ExtendedSpendingKey::master(&v) - } - } - - prop_compose! { - pub fn arb_shielded_addr()(extsk in arb_extended_spending_key()) -> PaymentAddress { - let extfvk = ExtendedFullViewingKey::from(&extsk); - extfvk.default_address().unwrap().1 - } - } -} - -#[cfg(test)] -mod tests { - use group::{Group, GroupEncoding}; - - use super::FullViewingKey; - use crate::constants::SPENDING_KEY_GENERATOR; - - #[test] - fn ak_must_be_prime_order() { - let mut buf = [0; 96]; - let identity = jubjub::SubgroupPoint::identity(); - - // Set both ak and nk to the identity. - buf[0..32].copy_from_slice(&identity.to_bytes()); - buf[32..64].copy_from_slice(&identity.to_bytes()); - - // ak is not allowed to be the identity. - assert_eq!( - FullViewingKey::read(&buf[..]).unwrap_err().to_string(), - "ak not of prime order" - ); - - // Set ak to a basepoint. - let basepoint = SPENDING_KEY_GENERATOR; - buf[0..32].copy_from_slice(&basepoint.to_bytes()); - - // nk is allowed to be the identity. - assert!(FullViewingKey::read(&buf[..]).is_ok()); - } -} diff --git a/zcash_primitives/src/sapling/note_encryption.rs b/zcash_primitives/src/sapling/note_encryption.rs deleted file mode 100644 index f50e2416d8..0000000000 --- a/zcash_primitives/src/sapling/note_encryption.rs +++ /dev/null @@ -1,1353 +0,0 @@ -//! Implementation of in-band secret distribution for Zcash transactions. -use blake2b_simd::{Hash as Blake2bHash, Params as Blake2bParams}; -use byteorder::{LittleEndian, WriteBytesExt}; -use ff::PrimeField; -use group::{cofactor::CofactorGroup, GroupEncoding}; -use rand_core::RngCore; -use std::convert::TryInto; - -use zcash_note_encryption::{ - try_compact_note_decryption, try_note_decryption, try_output_recovery_with_ock, Domain, - EphemeralKeyBytes, NoteEncryption, NotePlaintextBytes, NoteValidity, OutPlaintextBytes, - OutgoingCipherKey, ShieldedOutput, COMPACT_NOTE_SIZE, NOTE_PLAINTEXT_SIZE, OUT_CIPHERTEXT_SIZE, - OUT_PLAINTEXT_SIZE, -}; - -use crate::{ - consensus::{self, BlockHeight, NetworkUpgrade::Canopy, ZIP212_GRACE_PERIOD}, - memo::MemoBytes, - sapling::{keys::OutgoingViewingKey, Diversifier, Note, PaymentAddress, Rseed, SaplingIvk}, - transaction::components::{amount::Amount, sapling::OutputDescription}, -}; - -pub const KDF_SAPLING_PERSONALIZATION: &[u8; 16] = b"Zcash_SaplingKDF"; -pub const PRF_OCK_PERSONALIZATION: &[u8; 16] = b"Zcash_Derive_ock"; - -/// Sapling key agreement for note encryption. -/// -/// Implements section 5.4.4.3 of the Zcash Protocol Specification. -pub fn sapling_ka_agree(esk: &jubjub::Fr, pk_d: &jubjub::ExtendedPoint) -> jubjub::SubgroupPoint { - // [8 esk] pk_d - // ::clear_cofactor is implemented using - // ExtendedPoint::mul_by_cofactor in the jubjub crate. - - let mut wnaf = group::Wnaf::new(); - wnaf.scalar(esk).base(*pk_d).clear_cofactor() -} - -/// Sapling KDF for note encryption. -/// -/// Implements section 5.4.4.4 of the Zcash Protocol Specification. -fn kdf_sapling(dhsecret: jubjub::SubgroupPoint, ephemeral_key: &EphemeralKeyBytes) -> Blake2bHash { - Blake2bParams::new() - .hash_length(32) - .personal(KDF_SAPLING_PERSONALIZATION) - .to_state() - .update(&dhsecret.to_bytes()) - .update(ephemeral_key.as_ref()) - .finalize() -} - -/// Sapling PRF^ock. -/// -/// Implemented per section 5.4.2 of the Zcash Protocol Specification. -pub fn prf_ock( - ovk: &OutgoingViewingKey, - cv: &jubjub::ExtendedPoint, - cmu: &bls12_381::Scalar, - ephemeral_key: &EphemeralKeyBytes, -) -> OutgoingCipherKey { - OutgoingCipherKey( - Blake2bParams::new() - .hash_length(32) - .personal(PRF_OCK_PERSONALIZATION) - .to_state() - .update(&ovk.0) - .update(&cv.to_bytes()) - .update(&cmu.to_repr()) - .update(ephemeral_key.as_ref()) - .finalize() - .as_bytes() - .try_into() - .unwrap(), - ) -} - -fn epk_bytes(epk: &jubjub::ExtendedPoint) -> EphemeralKeyBytes { - EphemeralKeyBytes(epk.to_bytes()) -} - -fn sapling_parse_note_plaintext_without_memo( - domain: &SaplingDomain

, - plaintext: &[u8], - get_validated_pk_d: F, -) -> Option<(Note, PaymentAddress)> -where - F: FnOnce(&Diversifier) -> Option, -{ - assert!(plaintext.len() >= COMPACT_NOTE_SIZE); - - // Check note plaintext version - if !plaintext_version_is_valid(&domain.params, domain.height, plaintext[0]) { - return None; - } - - // The unwraps below are guaranteed to succeed by the assertion above - let diversifier = Diversifier(plaintext[1..12].try_into().unwrap()); - let value = Amount::from_u64_le_bytes(plaintext[12..20].try_into().unwrap()).ok()?; - let r: [u8; 32] = plaintext[20..COMPACT_NOTE_SIZE].try_into().unwrap(); - - let rseed = if plaintext[0] == 0x01 { - let rcm = jubjub::Fr::from_repr(r)?; - Rseed::BeforeZip212(rcm) - } else { - Rseed::AfterZip212(r) - }; - - let pk_d = get_validated_pk_d(&diversifier)?; - - let to = PaymentAddress::from_parts(diversifier, pk_d)?; - let note = to.create_note(value.into(), rseed)?; - Some((note, to)) -} - -pub struct SaplingDomain { - params: P, - height: BlockHeight, -} - -impl Domain for SaplingDomain

{ - type EphemeralSecretKey = jubjub::Scalar; - // It is acceptable for this to be a point because we enforce by consensus that - // points must not be small-order, and all points with non-canonical serialization - // are small-order. - type EphemeralPublicKey = jubjub::ExtendedPoint; - type SharedSecret = jubjub::SubgroupPoint; - type SymmetricKey = Blake2bHash; - type Note = Note; - type Recipient = PaymentAddress; - type DiversifiedTransmissionKey = jubjub::SubgroupPoint; - type IncomingViewingKey = SaplingIvk; - type OutgoingViewingKey = OutgoingViewingKey; - type ValueCommitment = jubjub::ExtendedPoint; - type ExtractedCommitment = bls12_381::Scalar; - type ExtractedCommitmentBytes = [u8; 32]; - type Memo = MemoBytes; - - fn derive_esk(note: &Self::Note) -> Option { - note.derive_esk() - } - - fn get_pk_d(note: &Self::Note) -> Self::DiversifiedTransmissionKey { - note.pk_d - } - - fn ka_derive_public( - note: &Self::Note, - esk: &Self::EphemeralSecretKey, - ) -> Self::EphemeralPublicKey { - // epk is an element of jubjub's prime-order subgroup, - // but Self::EphemeralPublicKey is a full group element - // for efficency of encryption. The conversion here is fine - // because the output of this function is only used for - // encoding and the byte encoding is unaffected by the conversion. - (note.g_d * esk).into() - } - - fn ka_agree_enc( - esk: &Self::EphemeralSecretKey, - pk_d: &Self::DiversifiedTransmissionKey, - ) -> Self::SharedSecret { - sapling_ka_agree(esk, pk_d.into()) - } - - fn ka_agree_dec( - ivk: &Self::IncomingViewingKey, - epk: &Self::EphemeralPublicKey, - ) -> Self::SharedSecret { - sapling_ka_agree(&ivk.0, epk) - } - - /// Sapling KDF for note encryption. - /// - /// Implements section 5.4.4.4 of the Zcash Protocol Specification. - fn kdf(dhsecret: jubjub::SubgroupPoint, epk: &EphemeralKeyBytes) -> Blake2bHash { - kdf_sapling(dhsecret, epk) - } - - fn note_plaintext_bytes( - note: &Self::Note, - to: &Self::Recipient, - memo: &Self::Memo, - ) -> NotePlaintextBytes { - // Note plaintext encoding is defined in section 5.5 of the Zcash Protocol - // Specification. - let mut input = [0; NOTE_PLAINTEXT_SIZE]; - input[0] = match note.rseed { - Rseed::BeforeZip212(_) => 1, - Rseed::AfterZip212(_) => 2, - }; - input[1..12].copy_from_slice(&to.diversifier().0); - (&mut input[12..20]) - .write_u64::(note.value) - .unwrap(); - - match note.rseed { - Rseed::BeforeZip212(rcm) => { - input[20..COMPACT_NOTE_SIZE].copy_from_slice(rcm.to_repr().as_ref()); - } - Rseed::AfterZip212(rseed) => { - input[20..COMPACT_NOTE_SIZE].copy_from_slice(&rseed); - } - } - - input[COMPACT_NOTE_SIZE..NOTE_PLAINTEXT_SIZE].copy_from_slice(&memo.as_array()[..]); - - NotePlaintextBytes(input) - } - - fn derive_ock( - ovk: &Self::OutgoingViewingKey, - cv: &Self::ValueCommitment, - cmu: &Self::ExtractedCommitment, - epk: &EphemeralKeyBytes, - ) -> OutgoingCipherKey { - prf_ock(ovk, cv, cmu, epk) - } - - fn outgoing_plaintext_bytes( - note: &Self::Note, - esk: &Self::EphemeralSecretKey, - ) -> OutPlaintextBytes { - let mut input = [0u8; OUT_PLAINTEXT_SIZE]; - input[0..32].copy_from_slice(¬e.pk_d.to_bytes()); - input[32..OUT_PLAINTEXT_SIZE].copy_from_slice(esk.to_repr().as_ref()); - - OutPlaintextBytes(input) - } - - fn epk_bytes(epk: &Self::EphemeralPublicKey) -> EphemeralKeyBytes { - epk_bytes(epk) - } - - fn check_epk_bytes NoteValidity>( - note: &Note, - check: F, - ) -> NoteValidity { - if let Some(derived_esk) = note.derive_esk() { - check(&derived_esk) - } else { - // Before ZIP 212 - NoteValidity::Valid - } - } - - fn parse_note_plaintext_without_memo_ivk( - &self, - ivk: &Self::IncomingViewingKey, - plaintext: &[u8], - ) -> Option<(Self::Note, Self::Recipient)> { - sapling_parse_note_plaintext_without_memo(&self, plaintext, |diversifier| { - Some(diversifier.g_d()? * ivk.0) - }) - } - - fn parse_note_plaintext_without_memo_ovk( - &self, - pk_d: &Self::DiversifiedTransmissionKey, - esk: &Self::EphemeralSecretKey, - epk: &Self::EphemeralPublicKey, - plaintext: &[u8], - ) -> Option<(Self::Note, Self::Recipient)> { - sapling_parse_note_plaintext_without_memo(&self, plaintext, |diversifier| { - if (diversifier.g_d()? * esk).to_bytes() == epk.to_bytes() { - Some(*pk_d) - } else { - None - } - }) - } - - fn cmstar(note: &Self::Note) -> Self::ExtractedCommitment { - note.cmu() - } - - fn extract_pk_d(op: &[u8; OUT_CIPHERTEXT_SIZE]) -> Option { - let pk_d = jubjub::SubgroupPoint::from_bytes( - op[0..32].try_into().expect("slice is the correct length"), - ); - - if pk_d.is_none().into() { - None - } else { - Some(pk_d.unwrap()) - } - } - - fn extract_esk(op: &[u8; OUT_CIPHERTEXT_SIZE]) -> Option { - jubjub::Fr::from_repr( - op[32..OUT_PLAINTEXT_SIZE] - .try_into() - .expect("slice is the correct length"), - ) - } - - fn extract_memo(&self, plaintext: &[u8]) -> Self::Memo { - MemoBytes::from_bytes(&plaintext[COMPACT_NOTE_SIZE..NOTE_PLAINTEXT_SIZE]).unwrap() - } -} - -/// Creates a new encryption context for the given note. -/// -/// Setting `ovk` to `None` represents the `ovk = ⊥` case, where the note cannot be -/// recovered by the sender. -pub fn sapling_note_encryption( - ovk: Option, - note: Note, - to: PaymentAddress, - memo: MemoBytes, - rng: &mut R, -) -> NoteEncryption> { - let esk = note.generate_or_derive_esk_internal(rng); - NoteEncryption::new_with_esk(esk, ovk, note, to, memo) -} - -#[allow(clippy::if_same_then_else)] -#[allow(clippy::needless_bool)] -pub fn plaintext_version_is_valid( - params: &P, - height: BlockHeight, - leadbyte: u8, -) -> bool { - if params.is_nu_active(Canopy, height) { - let grace_period_end_height = - params.activation_height(Canopy).unwrap() + ZIP212_GRACE_PERIOD; - - if height < grace_period_end_height && leadbyte != 0x01 && leadbyte != 0x02 { - // non-{0x01,0x02} received after Canopy activation and before grace period has elapsed - false - } else if height >= grace_period_end_height && leadbyte != 0x02 { - // non-0x02 received past (Canopy activation height + grace period) - false - } else { - true - } - } else { - // return false if non-0x01 received when Canopy is not active - leadbyte == 0x01 - } -} - -pub fn try_sapling_note_decryption< - P: consensus::Parameters, - Output: ShieldedOutput>, ->( - params: &P, - height: BlockHeight, - ivk: &SaplingIvk, - output: &Output, -) -> Option<(Note, PaymentAddress, MemoBytes)> { - let domain = SaplingDomain { - params: params.clone(), - height, - }; - try_note_decryption(&domain, ivk, output) -} - -pub fn try_sapling_compact_note_decryption< - P: consensus::Parameters, - Output: ShieldedOutput>, ->( - params: &P, - height: BlockHeight, - ivk: &SaplingIvk, - output: &Output, -) -> Option<(Note, PaymentAddress)> { - let domain = SaplingDomain { - params: params.clone(), - height, - }; - - try_compact_note_decryption(&domain, ivk, output) -} - -/// Recovery of the full note plaintext by the sender. -/// -/// Attempts to decrypt and validate the given `enc_ciphertext` using the given `ock`. -/// If successful, the corresponding Sapling note and memo are returned, along with the -/// `PaymentAddress` to which the note was sent. -/// -/// Implements part of section 4.19.3 of the Zcash Protocol Specification. -/// For decryption using a Full Viewing Key see [`try_sapling_output_recovery`]. -pub fn try_sapling_output_recovery_with_ock( - params: &P, - height: BlockHeight, - ock: &OutgoingCipherKey, - output: &OutputDescription, -) -> Option<(Note, PaymentAddress, MemoBytes)> { - let domain = SaplingDomain { - params: params.clone(), - height, - }; - - try_output_recovery_with_ock(&domain, ock, output, &output.out_ciphertext) -} - -/// Recovery of the full note plaintext by the sender. -/// -/// Attempts to decrypt and validate the given `enc_ciphertext` using the given `ovk`. -/// If successful, the corresponding Sapling note and memo are returned, along with the -/// `PaymentAddress` to which the note was sent. -/// -/// Implements section 4.19.3 of the Zcash Protocol Specification. -#[allow(clippy::too_many_arguments)] -pub fn try_sapling_output_recovery( - params: &P, - height: BlockHeight, - ovk: &OutgoingViewingKey, - output: &OutputDescription, -) -> Option<(Note, PaymentAddress, MemoBytes)> { - try_sapling_output_recovery_with_ock( - params, - height, - &prf_ock( - &ovk, - &output.cv, - &output.cmu, - &epk_bytes(&output.ephemeral_key), - ), - output, - ) -} - -#[cfg(test)] -mod tests { - use crypto_api_chachapoly::ChachaPolyIetf; - use ff::{Field, PrimeField}; - use group::Group; - use group::{cofactor::CofactorGroup, GroupEncoding}; - use rand_core::OsRng; - use rand_core::{CryptoRng, RngCore}; - use std::convert::TryInto; - - use zcash_note_encryption::{ - NoteEncryption, OutgoingCipherKey, ENC_CIPHERTEXT_SIZE, NOTE_PLAINTEXT_SIZE, - OUT_CIPHERTEXT_SIZE, OUT_PLAINTEXT_SIZE, - }; - - use super::{ - epk_bytes, kdf_sapling, prf_ock, sapling_ka_agree, sapling_note_encryption, - try_sapling_compact_note_decryption, try_sapling_note_decryption, - try_sapling_output_recovery, try_sapling_output_recovery_with_ock, SaplingDomain, - }; - - use crate::{ - consensus::{ - BlockHeight, - NetworkUpgrade::{Canopy, Sapling}, - Parameters, TestNetwork, TEST_NETWORK, ZIP212_GRACE_PERIOD, - }, - memo::MemoBytes, - sapling::util::generate_random_rseed, - sapling::{ - keys::OutgoingViewingKey, Diversifier, PaymentAddress, Rseed, SaplingIvk, - ValueCommitment, - }, - transaction::components::{ - amount::Amount, - sapling::{CompactOutputDescription, OutputDescription}, - GROTH_PROOF_SIZE, - }, - }; - - fn random_enc_ciphertext( - height: BlockHeight, - mut rng: &mut R, - ) -> ( - OutgoingViewingKey, - OutgoingCipherKey, - SaplingIvk, - OutputDescription, - ) { - let ivk = SaplingIvk(jubjub::Fr::random(&mut rng)); - - let (ovk, ock, output) = random_enc_ciphertext_with(height, &ivk, rng); - - assert!(try_sapling_note_decryption(&TEST_NETWORK, height, &ivk, &output).is_some()); - assert!(try_sapling_compact_note_decryption( - &TEST_NETWORK, - height, - &ivk, - &CompactOutputDescription::from(output.clone()), - ) - .is_some()); - - let ovk_output_recovery = try_sapling_output_recovery(&TEST_NETWORK, height, &ovk, &output); - - let ock_output_recovery = - try_sapling_output_recovery_with_ock(&TEST_NETWORK, height, &ock, &output); - assert!(ovk_output_recovery.is_some()); - assert!(ock_output_recovery.is_some()); - assert_eq!(ovk_output_recovery, ock_output_recovery); - - (ovk, ock, ivk, output) - } - - fn random_enc_ciphertext_with( - height: BlockHeight, - ivk: &SaplingIvk, - mut rng: &mut R, - ) -> (OutgoingViewingKey, OutgoingCipherKey, OutputDescription) { - let diversifier = Diversifier([0; 11]); - let pk_d = diversifier.g_d().unwrap() * ivk.0; - let pa = PaymentAddress::from_parts_unchecked(diversifier, pk_d); - - // Construct the value commitment for the proof instance - let value = Amount::from_u64(100).unwrap(); - let value_commitment = ValueCommitment { - value: value.into(), - randomness: jubjub::Fr::random(&mut rng), - }; - let cv = value_commitment.commitment().into(); - - let rseed = generate_random_rseed(&TEST_NETWORK, height, &mut rng); - - let note = pa.create_note(value.into(), rseed).unwrap(); - let cmu = note.cmu(); - - let ovk = OutgoingViewingKey([0; 32]); - let ne = sapling_note_encryption::<_, TestNetwork>( - Some(ovk), - note, - pa, - MemoBytes::empty(), - &mut rng, - ); - let epk = *ne.epk(); - let ock = prf_ock(&ovk, &cv, &cmu, &epk_bytes(&epk)); - - let output = OutputDescription { - cv, - cmu, - ephemeral_key: epk, - enc_ciphertext: ne.encrypt_note_plaintext(), - out_ciphertext: ne.encrypt_outgoing_plaintext(&cv, &cmu, &mut rng), - zkproof: [0u8; GROTH_PROOF_SIZE], - }; - - (ovk, ock, output) - } - - fn reencrypt_enc_ciphertext( - ovk: &OutgoingViewingKey, - cv: &jubjub::ExtendedPoint, - cmu: &bls12_381::Scalar, - epk: &jubjub::ExtendedPoint, - enc_ciphertext: &mut [u8; ENC_CIPHERTEXT_SIZE], - out_ciphertext: &[u8; OUT_CIPHERTEXT_SIZE], - modify_plaintext: impl Fn(&mut [u8; NOTE_PLAINTEXT_SIZE]), - ) { - let ock = prf_ock(&ovk, &cv, &cmu, &epk_bytes(epk)); - - let mut op = [0; OUT_CIPHERTEXT_SIZE]; - assert_eq!( - ChachaPolyIetf::aead_cipher() - .open_to(&mut op, out_ciphertext, &[], ock.as_ref(), &[0u8; 12]) - .unwrap(), - OUT_PLAINTEXT_SIZE - ); - - let pk_d = jubjub::SubgroupPoint::from_bytes(&op[0..32].try_into().unwrap()).unwrap(); - - let esk = jubjub::Fr::from_repr(op[32..OUT_PLAINTEXT_SIZE].try_into().unwrap()).unwrap(); - - let shared_secret = sapling_ka_agree(&esk, &pk_d.into()); - let key = kdf_sapling(shared_secret, &epk_bytes(&epk)); - - let mut plaintext = { - let mut buf = [0; ENC_CIPHERTEXT_SIZE]; - assert_eq!( - ChachaPolyIetf::aead_cipher() - .open_to(&mut buf, enc_ciphertext, &[], key.as_bytes(), &[0u8; 12]) - .unwrap(), - NOTE_PLAINTEXT_SIZE - ); - let mut pt = [0; NOTE_PLAINTEXT_SIZE]; - pt.copy_from_slice(&buf[..NOTE_PLAINTEXT_SIZE]); - pt - }; - - modify_plaintext(&mut plaintext); - - assert_eq!( - ChachaPolyIetf::aead_cipher() - .seal_to(enc_ciphertext, &plaintext, &[], &key.as_bytes(), &[0u8; 12]) - .unwrap(), - ENC_CIPHERTEXT_SIZE - ); - } - - fn find_invalid_diversifier() -> Diversifier { - // Find an invalid diversifier - let mut d = Diversifier([0; 11]); - loop { - for k in 0..11 { - d.0[k] = d.0[k].wrapping_add(1); - if d.0[k] != 0 { - break; - } - } - if d.g_d().is_none() { - break; - } - } - d - } - - fn find_valid_diversifier() -> Diversifier { - // Find a different valid diversifier - let mut d = Diversifier([0; 11]); - loop { - for k in 0..11 { - d.0[k] = d.0[k].wrapping_add(1); - if d.0[k] != 0 { - break; - } - } - if d.g_d().is_some() { - break; - } - } - d - } - - #[test] - fn decryption_with_invalid_ivk() { - let mut rng = OsRng; - let heights = [ - TEST_NETWORK.activation_height(Sapling).unwrap(), - TEST_NETWORK.activation_height(Canopy).unwrap(), - ]; - - for &height in heights.iter() { - let (_, _, _, output) = random_enc_ciphertext(height, &mut rng); - - assert_eq!( - try_sapling_note_decryption( - &TEST_NETWORK, - height, - &SaplingIvk(jubjub::Fr::random(&mut rng)), - &output - ), - None - ); - } - } - - #[test] - fn decryption_with_invalid_epk() { - let mut rng = OsRng; - let heights = [ - TEST_NETWORK.activation_height(Sapling).unwrap(), - TEST_NETWORK.activation_height(Canopy).unwrap(), - ]; - - for &height in heights.iter() { - let (_, _, ivk, mut output) = random_enc_ciphertext(height, &mut rng); - - output.ephemeral_key = jubjub::ExtendedPoint::random(&mut rng); - - assert_eq!( - try_sapling_note_decryption(&TEST_NETWORK, height, &ivk, &output,), - None - ); - } - } - - #[test] - fn decryption_with_invalid_cmu() { - let mut rng = OsRng; - let heights = [ - TEST_NETWORK.activation_height(Sapling).unwrap(), - TEST_NETWORK.activation_height(Canopy).unwrap(), - ]; - - for &height in heights.iter() { - let (_, _, ivk, mut output) = random_enc_ciphertext(height, &mut rng); - output.cmu = bls12_381::Scalar::random(&mut rng); - - assert_eq!( - try_sapling_note_decryption(&TEST_NETWORK, height, &ivk, &output), - None - ); - } - } - - #[test] - fn decryption_with_invalid_tag() { - let mut rng = OsRng; - let heights = [ - TEST_NETWORK.activation_height(Sapling).unwrap(), - TEST_NETWORK.activation_height(Canopy).unwrap(), - ]; - - for &height in heights.iter() { - let (_, _, ivk, mut output) = random_enc_ciphertext(height, &mut rng); - output.enc_ciphertext[ENC_CIPHERTEXT_SIZE - 1] ^= 0xff; - - assert_eq!( - try_sapling_note_decryption(&TEST_NETWORK, height, &ivk, &output), - None - ); - } - } - - #[test] - fn decryption_with_invalid_version_byte() { - let mut rng = OsRng; - let canopy_activation_height = TEST_NETWORK.activation_height(Canopy).unwrap(); - let heights = [ - canopy_activation_height - 1, - canopy_activation_height, - canopy_activation_height + ZIP212_GRACE_PERIOD, - ]; - let leadbytes = [0x02, 0x03, 0x01]; - - for (&height, &leadbyte) in heights.iter().zip(leadbytes.iter()) { - let (ovk, _, ivk, mut output) = random_enc_ciphertext(height, &mut rng); - - reencrypt_enc_ciphertext( - &ovk, - &output.cv, - &output.cmu, - &output.ephemeral_key, - &mut output.enc_ciphertext, - &output.out_ciphertext, - |pt| pt[0] = leadbyte, - ); - assert_eq!( - try_sapling_note_decryption(&TEST_NETWORK, height, &ivk, &output), - None - ); - } - } - - #[test] - fn decryption_with_invalid_diversifier() { - let mut rng = OsRng; - let heights = [ - TEST_NETWORK.activation_height(Sapling).unwrap(), - TEST_NETWORK.activation_height(Canopy).unwrap(), - ]; - - for &height in heights.iter() { - let (ovk, _, ivk, mut output) = random_enc_ciphertext(height, &mut rng); - - reencrypt_enc_ciphertext( - &ovk, - &output.cv, - &output.cmu, - &output.ephemeral_key, - &mut output.enc_ciphertext, - &output.out_ciphertext, - |pt| pt[1..12].copy_from_slice(&find_invalid_diversifier().0), - ); - assert_eq!( - try_sapling_note_decryption(&TEST_NETWORK, height, &ivk, &output), - None - ); - } - } - - #[test] - fn decryption_with_incorrect_diversifier() { - let mut rng = OsRng; - let heights = [ - TEST_NETWORK.activation_height(Sapling).unwrap(), - TEST_NETWORK.activation_height(Canopy).unwrap(), - ]; - - for &height in heights.iter() { - let (ovk, _, ivk, mut output) = random_enc_ciphertext(height, &mut rng); - - reencrypt_enc_ciphertext( - &ovk, - &output.cv, - &output.cmu, - &output.ephemeral_key, - &mut output.enc_ciphertext, - &output.out_ciphertext, - |pt| pt[1..12].copy_from_slice(&find_valid_diversifier().0), - ); - - assert_eq!( - try_sapling_note_decryption(&TEST_NETWORK, height, &ivk, &output), - None - ); - } - } - - #[test] - fn compact_decryption_with_invalid_ivk() { - let mut rng = OsRng; - let heights = [ - TEST_NETWORK.activation_height(Sapling).unwrap(), - TEST_NETWORK.activation_height(Canopy).unwrap(), - ]; - - for &height in heights.iter() { - let (_, _, _, output) = random_enc_ciphertext(height, &mut rng); - - assert_eq!( - try_sapling_compact_note_decryption( - &TEST_NETWORK, - height, - &SaplingIvk(jubjub::Fr::random(&mut rng)), - &CompactOutputDescription::from(output) - ), - None - ); - } - } - - #[test] - fn compact_decryption_with_invalid_epk() { - let mut rng = OsRng; - let heights = [ - TEST_NETWORK.activation_height(Sapling).unwrap(), - TEST_NETWORK.activation_height(Canopy).unwrap(), - ]; - - for &height in heights.iter() { - let (_, _, ivk, mut output) = random_enc_ciphertext(height, &mut rng); - output.ephemeral_key = jubjub::ExtendedPoint::random(&mut rng); - - assert_eq!( - try_sapling_compact_note_decryption( - &TEST_NETWORK, - height, - &ivk, - &CompactOutputDescription::from(output) - ), - None - ); - } - } - - #[test] - fn compact_decryption_with_invalid_cmu() { - let mut rng = OsRng; - let heights = [ - TEST_NETWORK.activation_height(Sapling).unwrap(), - TEST_NETWORK.activation_height(Canopy).unwrap(), - ]; - - for &height in heights.iter() { - let (_, _, ivk, mut output) = random_enc_ciphertext(height, &mut rng); - output.cmu = bls12_381::Scalar::random(&mut rng); - - assert_eq!( - try_sapling_compact_note_decryption( - &TEST_NETWORK, - height, - &ivk, - &CompactOutputDescription::from(output) - ), - None - ); - } - } - - #[test] - fn compact_decryption_with_invalid_version_byte() { - let mut rng = OsRng; - let canopy_activation_height = TEST_NETWORK.activation_height(Canopy).unwrap(); - let heights = [ - canopy_activation_height - 1, - canopy_activation_height, - canopy_activation_height + ZIP212_GRACE_PERIOD, - ]; - let leadbytes = [0x02, 0x03, 0x01]; - - for (&height, &leadbyte) in heights.iter().zip(leadbytes.iter()) { - let (ovk, _, ivk, mut output) = random_enc_ciphertext(height, &mut rng); - - reencrypt_enc_ciphertext( - &ovk, - &output.cv, - &output.cmu, - &output.ephemeral_key, - &mut output.enc_ciphertext, - &output.out_ciphertext, - |pt| pt[0] = leadbyte, - ); - assert_eq!( - try_sapling_compact_note_decryption( - &TEST_NETWORK, - height, - &ivk, - &CompactOutputDescription::from(output) - ), - None - ); - } - } - - #[test] - fn compact_decryption_with_invalid_diversifier() { - let mut rng = OsRng; - let heights = [ - TEST_NETWORK.activation_height(Sapling).unwrap(), - TEST_NETWORK.activation_height(Canopy).unwrap(), - ]; - - for &height in heights.iter() { - let (ovk, _, ivk, mut output) = random_enc_ciphertext(height, &mut rng); - - reencrypt_enc_ciphertext( - &ovk, - &output.cv, - &output.cmu, - &output.ephemeral_key, - &mut output.enc_ciphertext, - &output.out_ciphertext, - |pt| pt[1..12].copy_from_slice(&find_invalid_diversifier().0), - ); - assert_eq!( - try_sapling_compact_note_decryption( - &TEST_NETWORK, - height, - &ivk, - &CompactOutputDescription::from(output) - ), - None - ); - } - } - - #[test] - fn compact_decryption_with_incorrect_diversifier() { - let mut rng = OsRng; - let heights = [ - TEST_NETWORK.activation_height(Sapling).unwrap(), - TEST_NETWORK.activation_height(Canopy).unwrap(), - ]; - - for &height in heights.iter() { - let (ovk, _, ivk, mut output) = random_enc_ciphertext(height, &mut rng); - - reencrypt_enc_ciphertext( - &ovk, - &output.cv, - &output.cmu, - &output.ephemeral_key, - &mut output.enc_ciphertext, - &output.out_ciphertext, - |pt| pt[1..12].copy_from_slice(&find_valid_diversifier().0), - ); - assert_eq!( - try_sapling_compact_note_decryption( - &TEST_NETWORK, - height, - &ivk, - &CompactOutputDescription::from(output) - ), - None - ); - } - } - - #[test] - fn recovery_with_invalid_ovk() { - let mut rng = OsRng; - let heights = [ - TEST_NETWORK.activation_height(Sapling).unwrap(), - TEST_NETWORK.activation_height(Canopy).unwrap(), - ]; - - for &height in heights.iter() { - let (mut ovk, _, _, output) = random_enc_ciphertext(height, &mut rng); - - ovk.0[0] ^= 0xff; - assert_eq!( - try_sapling_output_recovery(&TEST_NETWORK, height, &ovk, &output,), - None - ); - } - } - - #[test] - fn recovery_with_invalid_ock() { - let mut rng = OsRng; - let heights = [ - TEST_NETWORK.activation_height(Sapling).unwrap(), - TEST_NETWORK.activation_height(Canopy).unwrap(), - ]; - - for &height in heights.iter() { - let (_, _, _, output) = random_enc_ciphertext(height, &mut rng); - - assert_eq!( - try_sapling_output_recovery_with_ock( - &TEST_NETWORK, - height, - &OutgoingCipherKey([0u8; 32]), - &output, - ), - None - ); - } - } - - #[test] - fn recovery_with_invalid_cv() { - let mut rng = OsRng; - let heights = [ - TEST_NETWORK.activation_height(Sapling).unwrap(), - TEST_NETWORK.activation_height(Canopy).unwrap(), - ]; - - for &height in heights.iter() { - let (ovk, _, _, mut output) = random_enc_ciphertext(height, &mut rng); - output.cv = jubjub::ExtendedPoint::random(&mut rng); - - assert_eq!( - try_sapling_output_recovery(&TEST_NETWORK, height, &ovk, &output,), - None - ); - } - } - - #[test] - fn recovery_with_invalid_cmu() { - let mut rng = OsRng; - let heights = [ - TEST_NETWORK.activation_height(Sapling).unwrap(), - TEST_NETWORK.activation_height(Canopy).unwrap(), - ]; - - for &height in heights.iter() { - let (ovk, ock, _, mut output) = random_enc_ciphertext(height, &mut rng); - output.cmu = bls12_381::Scalar::random(&mut rng); - - assert_eq!( - try_sapling_output_recovery(&TEST_NETWORK, height, &ovk, &output,), - None - ); - - assert_eq!( - try_sapling_output_recovery_with_ock(&TEST_NETWORK, height, &ock, &output,), - None - ); - } - } - - #[test] - fn recovery_with_invalid_epk() { - let mut rng = OsRng; - let heights = [ - TEST_NETWORK.activation_height(Sapling).unwrap(), - TEST_NETWORK.activation_height(Canopy).unwrap(), - ]; - - for &height in heights.iter() { - let (ovk, ock, _, mut output) = random_enc_ciphertext(height, &mut rng); - output.ephemeral_key = jubjub::ExtendedPoint::random(&mut rng); - - assert_eq!( - try_sapling_output_recovery(&TEST_NETWORK, height, &ovk, &output,), - None - ); - - assert_eq!( - try_sapling_output_recovery_with_ock(&TEST_NETWORK, height, &ock, &output,), - None - ); - } - } - - #[test] - fn recovery_with_invalid_enc_tag() { - let mut rng = OsRng; - let heights = [ - TEST_NETWORK.activation_height(Sapling).unwrap(), - TEST_NETWORK.activation_height(Canopy).unwrap(), - ]; - - for &height in heights.iter() { - let (ovk, ock, _, mut output) = random_enc_ciphertext(height, &mut rng); - - output.enc_ciphertext[ENC_CIPHERTEXT_SIZE - 1] ^= 0xff; - assert_eq!( - try_sapling_output_recovery(&TEST_NETWORK, height, &ovk, &output,), - None - ); - assert_eq!( - try_sapling_output_recovery_with_ock(&TEST_NETWORK, height, &ock, &output,), - None - ); - } - } - - #[test] - fn recovery_with_invalid_out_tag() { - let mut rng = OsRng; - let heights = [ - TEST_NETWORK.activation_height(Sapling).unwrap(), - TEST_NETWORK.activation_height(Canopy).unwrap(), - ]; - - for &height in heights.iter() { - let (ovk, ock, _, mut output) = random_enc_ciphertext(height, &mut rng); - - output.out_ciphertext[OUT_CIPHERTEXT_SIZE - 1] ^= 0xff; - assert_eq!( - try_sapling_output_recovery(&TEST_NETWORK, height, &ovk, &output,), - None - ); - assert_eq!( - try_sapling_output_recovery_with_ock(&TEST_NETWORK, height, &ock, &output,), - None - ); - } - } - - #[test] - fn recovery_with_invalid_version_byte() { - let mut rng = OsRng; - let canopy_activation_height = TEST_NETWORK.activation_height(Canopy).unwrap(); - let heights = [ - canopy_activation_height - 1, - canopy_activation_height, - canopy_activation_height + ZIP212_GRACE_PERIOD, - ]; - let leadbytes = [0x02, 0x03, 0x01]; - - for (&height, &leadbyte) in heights.iter().zip(leadbytes.iter()) { - let (ovk, ock, _, mut output) = random_enc_ciphertext(height, &mut rng); - - reencrypt_enc_ciphertext( - &ovk, - &output.cv, - &output.cmu, - &output.ephemeral_key, - &mut output.enc_ciphertext, - &output.out_ciphertext, - |pt| pt[0] = leadbyte, - ); - assert_eq!( - try_sapling_output_recovery(&TEST_NETWORK, height, &ovk, &output,), - None - ); - assert_eq!( - try_sapling_output_recovery_with_ock(&TEST_NETWORK, height, &ock, &output,), - None - ); - } - } - - #[test] - fn recovery_with_invalid_diversifier() { - let mut rng = OsRng; - let heights = [ - TEST_NETWORK.activation_height(Sapling).unwrap(), - TEST_NETWORK.activation_height(Canopy).unwrap(), - ]; - - for &height in heights.iter() { - let (ovk, ock, _, mut output) = random_enc_ciphertext(height, &mut rng); - - reencrypt_enc_ciphertext( - &ovk, - &output.cv, - &output.cmu, - &output.ephemeral_key, - &mut output.enc_ciphertext, - &output.out_ciphertext, - |pt| pt[1..12].copy_from_slice(&find_invalid_diversifier().0), - ); - assert_eq!( - try_sapling_output_recovery(&TEST_NETWORK, height, &ovk, &output,), - None - ); - assert_eq!( - try_sapling_output_recovery_with_ock(&TEST_NETWORK, height, &ock, &output,), - None - ); - } - } - - #[test] - fn recovery_with_incorrect_diversifier() { - let mut rng = OsRng; - let heights = [ - TEST_NETWORK.activation_height(Sapling).unwrap(), - TEST_NETWORK.activation_height(Canopy).unwrap(), - ]; - - for &height in heights.iter() { - let (ovk, ock, _, mut output) = random_enc_ciphertext(height, &mut rng); - - reencrypt_enc_ciphertext( - &ovk, - &output.cv, - &output.cmu, - &output.ephemeral_key, - &mut output.enc_ciphertext, - &output.out_ciphertext, - |pt| pt[1..12].copy_from_slice(&find_valid_diversifier().0), - ); - assert_eq!( - try_sapling_output_recovery(&TEST_NETWORK, height, &ovk, &output,), - None - ); - assert_eq!( - try_sapling_output_recovery_with_ock(&TEST_NETWORK, height, &ock, &output,), - None - ); - } - } - - #[test] - fn recovery_with_invalid_pk_d() { - let mut rng = OsRng; - let heights = [ - TEST_NETWORK.activation_height(Sapling).unwrap(), - TEST_NETWORK.activation_height(Canopy).unwrap(), - ]; - - for &height in heights.iter() { - let ivk = SaplingIvk(jubjub::Fr::zero()); - let (ovk, ock, output) = random_enc_ciphertext_with(height, &ivk, &mut rng); - - assert_eq!( - try_sapling_output_recovery(&TEST_NETWORK, height, &ovk, &output,), - None - ); - assert_eq!( - try_sapling_output_recovery_with_ock(&TEST_NETWORK, height, &ock, &output,), - None - ); - } - } - - #[test] - fn test_vectors() { - let test_vectors = crate::test_vectors::note_encryption::make_test_vectors(); - - macro_rules! read_bls12_381_scalar { - ($field:expr) => {{ - bls12_381::Scalar::from_repr($field[..].try_into().unwrap()).unwrap() - }}; - } - - macro_rules! read_jubjub_scalar { - ($field:expr) => {{ - jubjub::Fr::from_repr($field[..].try_into().unwrap()).unwrap() - }}; - } - - macro_rules! read_point { - ($field:expr) => { - jubjub::ExtendedPoint::from_bytes(&$field).unwrap() - }; - } - - let height = TEST_NETWORK.activation_height(Sapling).unwrap(); - - for tv in test_vectors { - // - // Load the test vector components - // - - let ivk = SaplingIvk(read_jubjub_scalar!(tv.ivk)); - let pk_d = read_point!(tv.default_pk_d).into_subgroup().unwrap(); - let rcm = read_jubjub_scalar!(tv.rcm); - let cv = read_point!(tv.cv); - let cmu = read_bls12_381_scalar!(tv.cmu); - let esk = read_jubjub_scalar!(tv.esk); - let epk = read_point!(tv.epk); - - // - // Test the individual components - // - - let shared_secret = sapling_ka_agree(&esk, &pk_d.into()); - assert_eq!(shared_secret.to_bytes(), tv.shared_secret); - - let k_enc = kdf_sapling(shared_secret, &epk_bytes(&epk)); - assert_eq!(k_enc.as_bytes(), tv.k_enc); - - let ovk = OutgoingViewingKey(tv.ovk); - let ock = prf_ock(&ovk, &cv, &cmu, &epk_bytes(&epk)); - assert_eq!(ock.as_ref(), tv.ock); - - let to = PaymentAddress::from_parts(Diversifier(tv.default_d), pk_d).unwrap(); - let note = to.create_note(tv.v, Rseed::BeforeZip212(rcm)).unwrap(); - assert_eq!(note.cmu(), cmu); - - let output = OutputDescription { - cv, - cmu, - ephemeral_key: epk, - enc_ciphertext: tv.c_enc, - out_ciphertext: tv.c_out, - zkproof: [0u8; GROTH_PROOF_SIZE], - }; - - // - // Test decryption - // (Tested first because it only requires immutable references.) - // - - match try_sapling_note_decryption(&TEST_NETWORK, height, &ivk, &output) { - Some((decrypted_note, decrypted_to, decrypted_memo)) => { - assert_eq!(decrypted_note, note); - assert_eq!(decrypted_to, to); - assert_eq!(&decrypted_memo.as_array()[..], &tv.memo[..]); - } - None => panic!("Note decryption failed"), - } - - match try_sapling_compact_note_decryption( - &TEST_NETWORK, - height, - &ivk, - &CompactOutputDescription::from(output.clone()), - ) { - Some((decrypted_note, decrypted_to)) => { - assert_eq!(decrypted_note, note); - assert_eq!(decrypted_to, to); - } - None => panic!("Compact note decryption failed"), - } - - match try_sapling_output_recovery(&TEST_NETWORK, height, &ovk, &output) { - Some((decrypted_note, decrypted_to, decrypted_memo)) => { - assert_eq!(decrypted_note, note); - assert_eq!(decrypted_to, to); - assert_eq!(&decrypted_memo.as_array()[..], &tv.memo[..]); - } - None => panic!("Output recovery failed"), - } - - // - // Test encryption - // - - let ne = NoteEncryption::>::new_with_esk( - esk, - Some(ovk), - note, - to, - MemoBytes::from_bytes(&tv.memo).unwrap(), - ); - - assert_eq!(ne.encrypt_note_plaintext().as_ref(), &tv.c_enc[..]); - assert_eq!( - &ne.encrypt_outgoing_plaintext(&cv, &cmu, &mut OsRng)[..], - &tv.c_out[..] - ); - } - } -} diff --git a/zcash_primitives/src/sapling/pedersen_hash.rs b/zcash_primitives/src/sapling/pedersen_hash.rs deleted file mode 100644 index 6d92b8e3b6..0000000000 --- a/zcash_primitives/src/sapling/pedersen_hash.rs +++ /dev/null @@ -1,160 +0,0 @@ -//! Implementation of the Pedersen hash function used in Sapling. - -#[cfg(test)] -pub(crate) mod test_vectors; - -use byteorder::{ByteOrder, LittleEndian}; -use ff::PrimeField; -use group::Group; -use std::ops::{AddAssign, Neg}; - -use crate::constants::{ - PEDERSEN_HASH_CHUNKS_PER_GENERATOR, PEDERSEN_HASH_EXP_TABLE, PEDERSEN_HASH_EXP_WINDOW_SIZE, -}; - -#[derive(Copy, Clone)] -pub enum Personalization { - NoteCommitment, - MerkleTree(usize), -} - -impl Personalization { - pub fn get_bits(&self) -> Vec { - match *self { - Personalization::NoteCommitment => vec![true, true, true, true, true, true], - Personalization::MerkleTree(num) => { - assert!(num < 63); - - (0..6).map(|i| (num >> i) & 1 == 1).collect() - } - } - } -} - -pub fn pedersen_hash(personalization: Personalization, bits: I) -> jubjub::SubgroupPoint -where - I: IntoIterator, -{ - let mut bits = personalization - .get_bits() - .into_iter() - .chain(bits.into_iter()); - - let mut result = jubjub::SubgroupPoint::identity(); - let mut generators = PEDERSEN_HASH_EXP_TABLE.iter(); - - loop { - let mut acc = jubjub::Fr::zero(); - let mut cur = jubjub::Fr::one(); - let mut chunks_remaining = PEDERSEN_HASH_CHUNKS_PER_GENERATOR; - let mut encountered_bits = false; - - // Grab three bits from the input - while let Some(a) = bits.next() { - encountered_bits = true; - - let b = bits.next().unwrap_or(false); - let c = bits.next().unwrap_or(false); - - // Start computing this portion of the scalar - let mut tmp = cur; - if a { - tmp.add_assign(&cur); - } - cur = cur.double(); // 2^1 * cur - if b { - tmp.add_assign(&cur); - } - - // conditionally negate - if c { - tmp = tmp.neg(); - } - - acc.add_assign(&tmp); - - chunks_remaining -= 1; - - if chunks_remaining == 0 { - break; - } else { - cur = cur.double().double().double(); // 2^4 * cur - } - } - - if !encountered_bits { - break; - } - - let mut table: &[Vec] = - &generators.next().expect("we don't have enough generators"); - let window = PEDERSEN_HASH_EXP_WINDOW_SIZE as usize; - let window_mask = (1u64 << window) - 1; - - let acc = acc.to_repr(); - let num_limbs: usize = acc.as_ref().len() / 8; - let mut limbs = vec![0u64; num_limbs + 1]; - LittleEndian::read_u64_into(acc.as_ref(), &mut limbs[..num_limbs]); - - let mut tmp = jubjub::SubgroupPoint::identity(); - - let mut pos = 0; - while pos < jubjub::Fr::NUM_BITS as usize { - let u64_idx = pos / 64; - let bit_idx = pos % 64; - let i = (if bit_idx + window < 64 { - // This window's bits are contained in a single u64. - limbs[u64_idx] >> bit_idx - } else { - // Combine the current u64's bits with the bits from the next u64. - (limbs[u64_idx] >> bit_idx) | (limbs[u64_idx + 1] << (64 - bit_idx)) - } & window_mask) as usize; - - tmp += table[0][i]; - - pos += window; - table = &table[1..]; - } - - result += tmp; - } - - result -} - -#[cfg(test)] -pub mod test { - use group::Curve; - - use super::*; - - pub struct TestVector<'a> { - pub personalization: Personalization, - pub input_bits: Vec, - pub hash_u: &'a str, - pub hash_v: &'a str, - } - - #[test] - fn test_pedersen_hash_points() { - let test_vectors = test_vectors::get_vectors(); - - assert!(!test_vectors.is_empty()); - - for v in test_vectors.iter() { - let input_bools: Vec = v.input_bits.iter().map(|&i| i == 1).collect(); - - // The 6 bits prefix is handled separately - assert_eq!(v.personalization.get_bits(), &input_bools[..6]); - - let p = jubjub::ExtendedPoint::from(pedersen_hash( - v.personalization, - input_bools.into_iter().skip(6), - )) - .to_affine(); - - assert_eq!(p.get_u().to_string(), v.hash_u); - assert_eq!(p.get_v().to_string(), v.hash_v); - } - } -} diff --git a/zcash_primitives/src/sapling/pedersen_hash/test_vectors.rs b/zcash_primitives/src/sapling/pedersen_hash/test_vectors.rs deleted file mode 100644 index 095f6c2a60..0000000000 --- a/zcash_primitives/src/sapling/pedersen_hash/test_vectors.rs +++ /dev/null @@ -1,715 +0,0 @@ -//! Test vectors from https://github.com/zcash-hackworks/zcash-test-vectors/blob/master/sapling_pedersen.py - -use super::{test::TestVector, Personalization}; - -pub fn get_vectors<'a>() -> Vec> { - return vec![ - TestVector { - personalization: Personalization::NoteCommitment, - input_bits: vec![1, 1, 1, 1, 1, 1], - hash_u: "0x06b1187c11ca4fb4383b2e0d0dbbde3ad3617338b5029187ec65a5eaed5e4d0b", - hash_v: "0x3ce70f536652f0dea496393a1e55c4e08b9d55508e16d11e5db40d4810cbc982", - }, - TestVector { - personalization: Personalization::NoteCommitment, - input_bits: vec![1, 1, 1, 1, 1, 1, 0], - hash_u: "0x2fc3bc454c337f71d4f04f86304262fcbfc9ecd808716b92fc42cbe6827f7f1a", - hash_v: "0x46d0d25bf1a654eedc6a9b1e5af398925113959feac31b7a2c036ff9b9ec0638", - }, - TestVector { - personalization: Personalization::NoteCommitment, - input_bits: vec![1, 1, 1, 1, 1, 1, 1], - hash_u: "0x4f8ce0e0a9e674b3ab9606a7d7aefba386e81583d81918127814cde41d209d97", - hash_v: "0x312b5ab93b14c9b9af334fe1fe3c50fffb53fbd074fa40ca600febde7c97e346", - }, - TestVector { - personalization: Personalization::NoteCommitment, - input_bits: vec![1, 1, 1, 1, 1, 1, 1, 0, 0], - hash_u: "0x4f8ce0e0a9e674b3ab9606a7d7aefba386e81583d81918127814cde41d209d97", - hash_v: "0x312b5ab93b14c9b9af334fe1fe3c50fffb53fbd074fa40ca600febde7c97e346", - }, - TestVector { - personalization: Personalization::NoteCommitment, - input_bits: vec![ - 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, - 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, - 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, - 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, - 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, - 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, - 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, - ], - hash_u: "0x599ab788360ae8c6d5bb7618aec37056d6227408d857fdc394078a3d7afdfe0f", - hash_v: "0x4320c373da670e28d168f4ffd72b43208e8c815f40841682c57a3ee1d005a527", - }, - TestVector { - personalization: Personalization::NoteCommitment, - input_bits: vec![ - 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, - 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, - 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, - 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, - 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, - 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, - 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, - ], - hash_u: "0x2da510317620f5dfdce1f31db6019f947eedcf02ff2972cff597a5c3ad21f5dd", - hash_v: "0x198789969c0c33e6c359b9da4a51771f4d50863f36beef90436944fe568399f2", - }, - TestVector { - personalization: Personalization::NoteCommitment, - input_bits: vec![ - 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, - 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, - 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, - 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, - 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, - 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, - 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, - ], - hash_u: "0x601247c7e640992d193dfb51df6ed93446687a7f2bcd0e4a598e6feb1ef20c40", - hash_v: "0x371931733b73e7b95c2cad55a6cebd15c83619f697c64283e54e5ef61442a743", - }, - TestVector { - personalization: Personalization::NoteCommitment, - input_bits: vec![ - 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, - 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, - 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, - 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, - 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, - 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, - 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, - 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, - 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, - 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, - 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, - 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, - 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, - 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, - 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, - 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, - 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, - 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, - 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, - 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, - 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, - 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, - 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, - 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, - 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, - 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, - 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, - ], - hash_u: "0x314192ecb1f2d8806a8108704c875a25d9fb7e444f9f373919adedebe8f2ae27", - hash_v: "0x6b12b32f1372ad574799dee9eb591d961b704bf611f55fcc71f7e82cd3330b74", - }, - TestVector { - personalization: Personalization::NoteCommitment, - input_bits: vec![ - 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, - 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, - 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, - 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, - 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, - 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, - 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, - 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, - 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, - 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, - 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, - 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, - 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, - 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, - 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, - 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, - 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, - 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, - 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, - 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, - 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, - 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, - 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, - 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, - 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, - 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, - 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, - 0, - ], - hash_u: "0x0666c2bce7f362a2b807d212e9a577f116891a932affd7addec39fbf372c494e", - hash_v: "0x6758bccfaf2e47c07756b96edea23aa8d10c33b38220bd1c411af612eeec18ab", - }, - TestVector { - personalization: Personalization::NoteCommitment, - input_bits: vec![ - 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, - 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, - 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, - 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, - 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, - 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, - 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, - 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, - 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, - 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, - 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, - 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, - 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, - 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, - 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, - 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, - 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, - 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, - 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, - 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, - 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, - 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, - 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, - 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, - 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, - 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, - 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, - 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, - 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, - 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, - 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, - 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, - 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, - 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, - ], - hash_u: "0x130afe02b99375484efb0998f5331d2178e1d00e803049bb0769099420624f5f", - hash_v: "0x5e2fc6970554ffe358652aa7968ac4fcf3de0c830e6ea492e01a38fafb68cd71", - }, - TestVector { - personalization: Personalization::NoteCommitment, - input_bits: vec![ - 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, - 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, - 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, - 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, - 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, - 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, - 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, - 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, - 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, - 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, - 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, - 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, - 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, - 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, - 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, - 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, - 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, - 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, - 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, - 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, - 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, - 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, - 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, - 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, - 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, - 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, - 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, - 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, - 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, - 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, - 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, - 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, - 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, - 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, - ], - hash_u: "0x67914ebd539961b70f468fa23d4cb42133693a8ac57cd35a1e6369fe34fbedf7", - hash_v: "0x44770870c0f0cfe59a10df95d6c21e6f1514a2f464b66377599438c126052d9f", - }, - TestVector { - personalization: Personalization::MerkleTree(0), - input_bits: vec![0, 0, 0, 0, 0, 0], - hash_u: "0x62454a957289b3930d10f3def0d512cfe0ef3de06421321221af3558de9d481d", - hash_v: "0x0279f0aebfb66e53ff69fba16b6608dbf4319b944432f45c6e69a3dbd1f7b330", - }, - TestVector { - personalization: Personalization::MerkleTree(0), - input_bits: vec![0, 0, 0, 0, 0, 0, 0], - hash_u: "0x283c7880f35179e201161402d9c4556b255917dbbf0142ae60519787d36d4dea", - hash_v: "0x648224408b4b83297cd0feb4cdc4eeb224237734931145432793bcd414228dc4", - }, - TestVector { - personalization: Personalization::MerkleTree(0), - input_bits: vec![0, 0, 0, 0, 0, 0, 1], - hash_u: "0x1f1086b287636a20063c9614db2de66bb7d49242e88060956a5e5845057f6f5d", - hash_v: "0x6b1b395421dde74d53341caa9e01f39d7a3138efb9b57fc0381f98f4868df622", - }, - TestVector { - personalization: Personalization::MerkleTree(0), - input_bits: vec![0, 0, 0, 0, 0, 0, 1, 0, 0], - hash_u: "0x1f1086b287636a20063c9614db2de66bb7d49242e88060956a5e5845057f6f5d", - hash_v: "0x6b1b395421dde74d53341caa9e01f39d7a3138efb9b57fc0381f98f4868df622", - }, - TestVector { - personalization: Personalization::MerkleTree(0), - input_bits: vec![ - 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, - 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, - 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, - 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, - 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, - 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, - 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, - ], - hash_u: "0x20d2b1b0551efe511755d564f8da4f5bf285fd6051331fa5f129ad95b318f6cd", - hash_v: "0x2834d96950de67ae80e85545f8333c6e14b5cf5be7325dac768f401e6edd9544", - }, - TestVector { - personalization: Personalization::MerkleTree(0), - input_bits: vec![ - 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, - 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, - 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, - 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, - 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, - 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, - 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, - ], - hash_u: "0x01f4850a0f40e07186fee1f0a276f52fb12cffe05c18eb2aa18170330a93c555", - hash_v: "0x19b0807358e7c8cba9168815ec54c4cd76997c34c592607d172151c48d5377cb", - }, - TestVector { - personalization: Personalization::MerkleTree(0), - input_bits: vec![ - 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, - 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, - 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, - 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, - 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, - 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, - 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, - ], - hash_u: "0x26dd81a3ffa37452c6a932d41eb4f2e0fedd531e9af8c2a7935b91dff653879d", - hash_v: "0x2fc7aebb729ef5cabf0fb3f883bc2eb2603093850b0ec19c1a3c08b653e7f27f", - }, - TestVector { - personalization: Personalization::MerkleTree(0), - input_bits: vec![ - 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, - 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, - 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, - 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, - 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, - 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, - 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, - 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, - 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, - 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, - 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, - 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, - 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, - 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, - 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, - 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, - 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, - 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, - 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, - 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, - 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, - 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, - 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, - 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, - 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, - 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, - 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, - ], - hash_u: "0x1111740552773b00aa6a2334575aa94102cfbd084290a430c90eb56d6db65b85", - hash_v: "0x6560c44b11683c20030626f89456f78a53ae8a89f565956a98ffc554b48fbb1a", - }, - TestVector { - personalization: Personalization::MerkleTree(0), - input_bits: vec![ - 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, - 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, - 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, - 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, - 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, - 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, - 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, - 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, - 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, - 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, - 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, - 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, - 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, - 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, - 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, - 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, - 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, - 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, - 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, - 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, - 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, - 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, - 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, - 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, - 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, - 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, - 0, - ], - hash_u: "0x429349ea9b5f8163bcda3014b3e15554df5173353fd73f315a49360c97265f68", - hash_v: "0x188774bb6de41eba669be5d368942783f937acf2f418385fc5c78479b0a405ee", - }, - TestVector { - personalization: Personalization::MerkleTree(0), - input_bits: vec![ - 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, - 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, - 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, - 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, - 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, - 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, - 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, - 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, - 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, - 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, - 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, - 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, - 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, - 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, - 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, - 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, - 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, - 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, - 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, - 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, - 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, - 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, - 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, - 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, - 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, - 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, - 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, - 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, - 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, - 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, - 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, - 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, - 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, - ], - hash_u: "0x00e827f3ed136f3c91c61c97ab9b7cca0ea53c20e47abb5e226ede297bdd5f37", - hash_v: "0x315cc00a54972df6a19f650d3fab5f2ad0fb07397bacb6944568618f2aa76bf6", - }, - TestVector { - personalization: Personalization::MerkleTree(0), - input_bits: vec![ - 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, - 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, - 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, - 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, - 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, - 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, - 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, - 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, - 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, - 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, - 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, - 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, - 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, - 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, - 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, - 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, - 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, - 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, - 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, - 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, - 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, - 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, - 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, - 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, - 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, - 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, - 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, - 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, - 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, - 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, - 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, - 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, - 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, - ], - hash_u: "0x3ee50557c4aa9158c4bb9d5961208e6c62f55c73ad7c7695a0eba0bcb6d83d05", - hash_v: "0x1b1a2be6e47688828aeadf2d37db298eac0c2736c2722b227871fdeeee29de33", - }, - TestVector { - personalization: Personalization::MerkleTree(34), - input_bits: vec![0, 1, 0, 0, 0, 1], - hash_u: "0x61f8e2cb8e945631677b450d5e5669bc6b5f2ec69b321ac550dbe74525d7ac9a", - hash_v: "0x4e11951ab9c9400ee38a18bd98cdb9453f1f67141ee9d9bf0c1c157d4fb34f9a", - }, - TestVector { - personalization: Personalization::MerkleTree(34), - input_bits: vec![0, 1, 0, 0, 0, 1, 0], - hash_u: "0x27fa1e296c37dde8448483ce5485c2604d1d830e53812246299773a02ecd519c", - hash_v: "0x08e499113675202cb42b4b681a31430814edebd72c5bb3bc3bfedf91fb0605df", - }, - TestVector { - personalization: Personalization::MerkleTree(34), - input_bits: vec![0, 1, 0, 0, 0, 1, 1], - hash_u: "0x52112dd7a4293d049bb011683244a0f957e6ba95e1d1cf2fb6654d449a6d3fbc", - hash_v: "0x2ae14ecd81bb5b4489d2d64b5d2eb92a684087b28dd9a4950ecdb78c014e178c", - }, - TestVector { - personalization: Personalization::MerkleTree(34), - input_bits: vec![0, 1, 0, 0, 0, 1, 1, 0, 0], - hash_u: "0x52112dd7a4293d049bb011683244a0f957e6ba95e1d1cf2fb6654d449a6d3fbc", - hash_v: "0x2ae14ecd81bb5b4489d2d64b5d2eb92a684087b28dd9a4950ecdb78c014e178c", - }, - TestVector { - personalization: Personalization::MerkleTree(34), - input_bits: vec![ - 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, - 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, - 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, - 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, - 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, - 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, - 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, - ], - hash_u: "0x544a0b44c35dca64ee806d1af70b7c44134e5d86efed413947657ffd71adf9b2", - hash_v: "0x5ddc5dbf12abbbc5561defd3782a32f450b3c398f52ff4629677e59e86e3ab31", - }, - TestVector { - personalization: Personalization::MerkleTree(34), - input_bits: vec![ - 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, - 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, - 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, - 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, - 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, - 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, - 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, - ], - hash_u: "0x6cb6490ccb0ca9ccd657146f58a7b800bc4fb2556ee37861227ee8fda724acfb", - hash_v: "0x05c6fe100926f5cc441e54e72f024b6b12c907f2ec5680335057896411984c9f", - }, - TestVector { - personalization: Personalization::MerkleTree(34), - input_bits: vec![ - 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, - 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, - 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, - 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, - 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, - 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, - 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, - ], - hash_u: "0x40901e2175cb7f06a00c676d54d90e59fd448f11cbbc5eb517f9fea74b795ce2", - hash_v: "0x42d512891f91087310c9bc630c8d0ecc014596f884fd6df55dada8195ed726de", - }, - TestVector { - personalization: Personalization::MerkleTree(34), - input_bits: vec![ - 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, - 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, - 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, - 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, - 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, - 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, - 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, - 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, - 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, - 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, - 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, - 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, - 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, - 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, - 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, - 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, - 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, - 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, - 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, - 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, - 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, - 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, - 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, - 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, - 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, - 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, - 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, - ], - hash_u: "0x66a433542419f1a086ed0663b0e8df2ece9a04065f147896976baba1a916b6dc", - hash_v: "0x203bd3672522e1d3c86fa6b9f3b58f20199a4216adfd40982add13a856f6f3de", - }, - TestVector { - personalization: Personalization::MerkleTree(34), - input_bits: vec![ - 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, - 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, - 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, - 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, - 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, - 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, - 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, - 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, - 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, - 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, - 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, - 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, - 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, - 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, - 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, - 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, - 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, - 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, - 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, - 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, - 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, - 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, - 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, - 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, - 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, - 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, - 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, - 1, - ], - hash_u: "0x119db3b38086c1a3c6c6f53c529ee62d9311d69c2d8aeeafa6e172e650d3afda", - hash_v: "0x72287540be7d2b0f58f5c73eaa53c55bea6b79dd79873b4e47cc11787bb9a15d", - }, - TestVector { - personalization: Personalization::MerkleTree(34), - input_bits: vec![ - 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, - 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, - 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, - 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, - 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, - 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, - 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, - 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, - 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, - 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, - 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, - 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, - 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, - 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, - 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, - 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, - 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, - 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, - 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, - 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, - 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, - 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, - 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, - 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, - 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, - 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, - 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, - 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, - 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, - 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, - 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, - 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, - 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, - 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, - ], - hash_u: "0x446efdcf89b70ba2b03427a0893008181d0fc4e76b84b1a500d7ee523c8e3666", - hash_v: "0x125ee0048efb0372b92c3c15d51a7c5c77a712054cc4fdd0774563da46ec7289", - }, - TestVector { - personalization: Personalization::MerkleTree(34), - input_bits: vec![ - 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, - 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, - 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, - 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, - 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, - 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, - 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, - 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, - 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, - 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, - 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, - 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, - 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, - 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, - 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, - 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, - 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, - 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, - 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, - 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, - 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, - 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, - 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, - 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, - 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, - 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, - 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, - 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, - 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, - 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, - 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, - 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, - 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, - ], - hash_u: "0x72723bf0573bcb4b72d4184cfeb707d9556b7f705f56a4652707a36f2edf10f7", - hash_v: "0x3a7f0999a6a1393bd49fc82302e7352e01176fbebb0192bf5e6ef39eb8c585ad", - }, - TestVector { - personalization: Personalization::MerkleTree(27), - input_bits: vec![ - 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, - 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, - 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, - 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, - 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, - 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, - 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, - ], - hash_u: "0x414f6ba05f6b92da1f9051950769e1083d05615def32b016ae424309828a11f4", - hash_v: "0x471d2109656afcb96d0609b371b132b97efcf72c6051064dd19fdc004799bfa9", - }, - TestVector { - personalization: Personalization::MerkleTree(36), - input_bits: vec![ - 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, - 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, - 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, - 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, - 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, - 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, - 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, - ], - hash_u: "0x62d6fe1e373225a5695f3115aed8265c59e2d6275ceef6bbc53fde3fc6594024", - hash_v: "0x407275be7d5a4c48204c8d83f5b211d09a2f285d4f0f87a928d4de9a6338e1d1", - }, - TestVector { - personalization: Personalization::MerkleTree(0), - input_bits: vec![ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ], - hash_u: "0x1116a934f26b57a2c9daa6f25ac9b1a8f9dacddba30f65433ac021bf39a6bfdd", - hash_v: "0x407275be7d5a4c48204c8d83f5b211d09a2f285d4f0f87a928d4de9a6338e1d1", - }, - TestVector { - personalization: Personalization::NoteCommitment, - input_bits: vec![ - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - ], - hash_u: "0x329e3bb2ca31ea6e13a986730237f6fd16b842a510cbabe851bdbcf57d75ee0d", - hash_v: "0x471d2109656afcb96d0609b371b132b97efcf72c6051064dd19fdc004799bfa9", - }, - ]; -} diff --git a/zcash_primitives/src/sapling/prover.rs b/zcash_primitives/src/sapling/prover.rs deleted file mode 100644 index 1c4ae9ee1d..0000000000 --- a/zcash_primitives/src/sapling/prover.rs +++ /dev/null @@ -1,144 +0,0 @@ -//! Abstractions over the proving system and parameters. - -use crate::{ - merkle_tree::MerklePath, - sapling::{ - redjubjub::{PublicKey, Signature}, - Node, - }, - transaction::components::{Amount, GROTH_PROOF_SIZE}, -}; - -use super::{Diversifier, PaymentAddress, ProofGenerationKey, Rseed}; - -/// Interface for creating zero-knowledge proofs for shielded transactions. -pub trait TxProver { - /// Type for persisting any necessary context across multiple Sapling proofs. - type SaplingProvingContext; - - /// Instantiate a new Sapling proving context. - fn new_sapling_proving_context(&self) -> Self::SaplingProvingContext; - - /// Create the value commitment, re-randomized key, and proof for a Sapling - /// [`SpendDescription`], while accumulating its value commitment randomness inside - /// the context for later use. - /// - /// [`SpendDescription`]: crate::transaction::components::SpendDescription - #[allow(clippy::too_many_arguments)] - fn spend_proof( - &self, - ctx: &mut Self::SaplingProvingContext, - proof_generation_key: ProofGenerationKey, - diversifier: Diversifier, - rseed: Rseed, - ar: jubjub::Fr, - value: u64, - anchor: bls12_381::Scalar, - merkle_path: MerklePath, - ) -> Result<([u8; GROTH_PROOF_SIZE], jubjub::ExtendedPoint, PublicKey), ()>; - - /// Create the value commitment and proof for a Sapling [`OutputDescription`], - /// while accumulating its value commitment randomness inside the context for later - /// use. - /// - /// [`OutputDescription`]: crate::transaction::components::OutputDescription - fn output_proof( - &self, - ctx: &mut Self::SaplingProvingContext, - esk: jubjub::Fr, - payment_address: PaymentAddress, - rcm: jubjub::Fr, - value: u64, - ) -> ([u8; GROTH_PROOF_SIZE], jubjub::ExtendedPoint); - - /// Create the `bindingSig` for a Sapling transaction. All calls to - /// [`TxProver::spend_proof`] and [`TxProver::output_proof`] must be completed before - /// calling this function. - fn binding_sig( - &self, - ctx: &mut Self::SaplingProvingContext, - value_balance: Amount, - sighash: &[u8; 32], - ) -> Result; -} - -#[cfg(any(test, feature = "test-dependencies"))] -pub mod mock { - use ff::Field; - use rand_core::OsRng; - - use crate::{ - constants::SPENDING_KEY_GENERATOR, - merkle_tree::MerklePath, - sapling::{ - redjubjub::{PublicKey, Signature}, - Diversifier, Node, PaymentAddress, ProofGenerationKey, Rseed, ValueCommitment, - }, - transaction::components::{Amount, GROTH_PROOF_SIZE}, - }; - - use super::TxProver; - - pub struct MockTxProver; - - impl TxProver for MockTxProver { - type SaplingProvingContext = (); - - fn new_sapling_proving_context(&self) -> Self::SaplingProvingContext {} - - fn spend_proof( - &self, - _ctx: &mut Self::SaplingProvingContext, - proof_generation_key: ProofGenerationKey, - _diversifier: Diversifier, - _rcm: Rseed, - ar: jubjub::Fr, - value: u64, - _anchor: bls12_381::Scalar, - _merkle_path: MerklePath, - ) -> Result<([u8; GROTH_PROOF_SIZE], jubjub::ExtendedPoint, PublicKey), ()> { - let mut rng = OsRng; - - let cv = ValueCommitment { - value, - randomness: jubjub::Fr::random(&mut rng), - } - .commitment() - .into(); - - let rk = PublicKey(proof_generation_key.ak.clone().into()) - .randomize(ar, SPENDING_KEY_GENERATOR); - - Ok(([0u8; GROTH_PROOF_SIZE], cv, rk)) - } - - fn output_proof( - &self, - _ctx: &mut Self::SaplingProvingContext, - _esk: jubjub::Fr, - _payment_address: PaymentAddress, - _rcm: jubjub::Fr, - value: u64, - ) -> ([u8; GROTH_PROOF_SIZE], jubjub::ExtendedPoint) { - let mut rng = OsRng; - - let cv = ValueCommitment { - value, - randomness: jubjub::Fr::random(&mut rng), - } - .commitment() - .into(); - - ([0u8; GROTH_PROOF_SIZE], cv) - } - - fn binding_sig( - &self, - _ctx: &mut Self::SaplingProvingContext, - _value_balance: Amount, - _sighash: &[u8; 32], - ) -> Result { - Err(()) - } - } -} diff --git a/zcash_primitives/src/sapling/redjubjub.rs b/zcash_primitives/src/sapling/redjubjub.rs deleted file mode 100644 index 8d5939a84b..0000000000 --- a/zcash_primitives/src/sapling/redjubjub.rs +++ /dev/null @@ -1,359 +0,0 @@ -//! Implementation of [RedJubjub], a specialization of RedDSA to the Jubjub -//! curve. -//! -//! [RedJubjub]: https://zips.z.cash/protocol/protocol.pdf#concretereddsa - -use ff::{Field, PrimeField}; -use group::GroupEncoding; -use jubjub::{ExtendedPoint, SubgroupPoint}; -use rand_core::RngCore; -use std::io::{self, Read, Write}; -use std::ops::{AddAssign, MulAssign, Neg}; - -use super::util::hash_to_scalar; - -fn read_scalar(mut reader: R) -> io::Result { - let mut s_repr = [0u8; 32]; - reader.read_exact(s_repr.as_mut())?; - - jubjub::Fr::from_repr(s_repr) - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "scalar is not in field")) -} - -fn write_scalar(s: &jubjub::Fr, mut writer: W) -> io::Result<()> { - writer.write_all(s.to_repr().as_ref()) -} - -fn h_star(a: &[u8], b: &[u8]) -> jubjub::Fr { - hash_to_scalar(b"Zcash_RedJubjubH", a, b) -} - -#[derive(Copy, Clone, Debug)] -pub struct Signature { - rbar: [u8; 32], - sbar: [u8; 32], -} - -pub struct PrivateKey(pub jubjub::Fr); - -#[derive(Debug, Clone)] -pub struct PublicKey(pub ExtendedPoint); - -impl Signature { - pub fn read(mut reader: R) -> io::Result { - let mut rbar = [0u8; 32]; - let mut sbar = [0u8; 32]; - reader.read_exact(&mut rbar)?; - reader.read_exact(&mut sbar)?; - Ok(Signature { rbar, sbar }) - } - - pub fn write(&self, mut writer: W) -> io::Result<()> { - writer.write_all(&self.rbar)?; - writer.write_all(&self.sbar) - } -} - -impl PrivateKey { - pub fn randomize(&self, alpha: jubjub::Fr) -> Self { - let mut tmp = self.0; - tmp.add_assign(&alpha); - PrivateKey(tmp) - } - - pub fn read(reader: R) -> io::Result { - let pk = read_scalar::(reader)?; - Ok(PrivateKey(pk)) - } - - pub fn write(&self, writer: W) -> io::Result<()> { - write_scalar::(&self.0, writer) - } - - pub fn sign(&self, msg: &[u8], rng: &mut R, p_g: SubgroupPoint) -> Signature { - // T = (l_H + 128) bits of randomness - // For H*, l_H = 512 bits - let mut t = [0u8; 80]; - rng.fill_bytes(&mut t[..]); - - // r = H*(T || M) - let r = h_star(&t[..], msg); - - // R = r . P_G - let r_g = p_g * r; - let rbar = r_g.to_bytes(); - - // S = r + H*(Rbar || M) . sk - let mut s = h_star(&rbar[..], msg); - s.mul_assign(&self.0); - s.add_assign(&r); - let mut sbar = [0u8; 32]; - write_scalar::<&mut [u8]>(&s, &mut sbar[..]) - .expect("Jubjub scalars should serialize to 32 bytes"); - - Signature { rbar, sbar } - } -} - -impl PublicKey { - pub fn from_private(privkey: &PrivateKey, p_g: SubgroupPoint) -> Self { - PublicKey((p_g * privkey.0).into()) - } - - pub fn randomize(&self, alpha: jubjub::Fr, p_g: SubgroupPoint) -> Self { - PublicKey(ExtendedPoint::from(p_g * alpha) + self.0) - } - - pub fn read(mut reader: R) -> io::Result { - let mut bytes = [0u8; 32]; - reader.read_exact(&mut bytes)?; - let p = ExtendedPoint::from_bytes(&bytes).map(PublicKey); - if p.is_some().into() { - Ok(p.unwrap()) - } else { - Err(io::Error::new( - io::ErrorKind::InvalidInput, - "invalid RedJubjub public key", - )) - } - } - - pub fn write(&self, mut writer: W) -> io::Result<()> { - writer.write_all(&self.0.to_bytes()) - } - - pub fn verify(&self, msg: &[u8], sig: &Signature, p_g: SubgroupPoint) -> bool { - // c = H*(Rbar || M) - let c = h_star(&sig.rbar[..], msg); - - // Signature checks: - // R != invalid - let r = { - let r = ExtendedPoint::from_bytes(&sig.rbar); - if r.is_none().into() { - return false; - } - r.unwrap() - }; - // S < order(G) - // (jubjub::Scalar guarantees its representation is in the field) - let s = match read_scalar::<&[u8]>(&sig.sbar[..]) { - Ok(s) => s, - Err(_) => return false, - }; - // 0 = h_G(-S . P_G + R + c . vk) - ((self.0 * c) + r - (p_g * s)) - .mul_by_cofactor() - .is_identity() - .into() - } -} - -pub struct BatchEntry<'a> { - vk: PublicKey, - msg: &'a [u8], - sig: Signature, -} - -// TODO: #82: This is a naive implementation currently, -// and doesn't use multiexp. -pub fn batch_verify<'a, R: RngCore>( - mut rng: &mut R, - batch: &[BatchEntry<'a>], - p_g: SubgroupPoint, -) -> bool { - let mut acc = ExtendedPoint::identity(); - - for entry in batch { - let mut r = { - let r = ExtendedPoint::from_bytes(&entry.sig.rbar); - if r.is_none().into() { - return false; - } - r.unwrap() - }; - let mut s = match read_scalar::<&[u8]>(&entry.sig.sbar[..]) { - Ok(s) => s, - Err(_) => return false, - }; - - let mut c = h_star(&entry.sig.rbar[..], entry.msg); - - let z = jubjub::Fr::random(&mut rng); - - s.mul_assign(&z); - s = s.neg(); - - r *= z; - - c.mul_assign(&z); - - acc = acc + r + (entry.vk.0 * c) + (p_g * s); - } - - acc.mul_by_cofactor().is_identity().into() -} - -#[cfg(test)] -mod tests { - use group::Group; - use rand_core::SeedableRng; - use rand_xorshift::XorShiftRng; - - use super::*; - use crate::constants::SPENDING_KEY_GENERATOR; - - #[test] - fn test_batch_verify() { - let mut rng = XorShiftRng::from_seed([ - 0x59, 0x62, 0xbe, 0x5d, 0x76, 0x3d, 0x31, 0x8d, 0x17, 0xdb, 0x37, 0x32, 0x54, 0x06, - 0xbc, 0xe5, - ]); - let p_g = SPENDING_KEY_GENERATOR; - - let sk1 = PrivateKey(jubjub::Fr::random(&mut rng)); - let vk1 = PublicKey::from_private(&sk1, p_g); - let msg1 = b"Foo bar"; - let sig1 = sk1.sign(msg1, &mut rng, p_g); - assert!(vk1.verify(msg1, &sig1, p_g)); - - let sk2 = PrivateKey(jubjub::Fr::random(&mut rng)); - let vk2 = PublicKey::from_private(&sk2, p_g); - let msg2 = b"Foo bar"; - let sig2 = sk2.sign(msg2, &mut rng, p_g); - assert!(vk2.verify(msg2, &sig2, p_g)); - - let mut batch = vec![ - BatchEntry { - vk: vk1, - msg: msg1, - sig: sig1, - }, - BatchEntry { - vk: vk2, - msg: msg2, - sig: sig2, - }, - ]; - - assert!(batch_verify(&mut rng, &batch, p_g)); - - batch[0].sig = sig2; - - assert!(!batch_verify(&mut rng, &batch, p_g)); - } - - #[test] - fn cofactor_check() { - let mut rng = XorShiftRng::from_seed([ - 0x59, 0x62, 0xbe, 0x5d, 0x76, 0x3d, 0x31, 0x8d, 0x17, 0xdb, 0x37, 0x32, 0x54, 0x06, - 0xbc, 0xe5, - ]); - let zero = jubjub::ExtendedPoint::identity(); - let p_g = SPENDING_KEY_GENERATOR; - - let jubjub_modulus_bytes = [ - 0xb7, 0x2c, 0xf7, 0xd6, 0x5e, 0x0e, 0x97, 0xd0, 0x82, 0x10, 0xc8, 0xcc, 0x93, 0x20, - 0x68, 0xa6, 0x00, 0x3b, 0x34, 0x01, 0x01, 0x3b, 0x67, 0x06, 0xa9, 0xaf, 0x33, 0x65, - 0xea, 0xb4, 0x7d, 0x0e, - ]; - - // Get a point of order 8 - let p8 = loop { - let r = jubjub::ExtendedPoint::random(&mut rng) - .to_niels() - .multiply_bits(&jubjub_modulus_bytes); - - let r2 = r.double(); - let r4 = r2.double(); - let r8 = r4.double(); - - if r2 != zero && r4 != zero && r8 == zero { - break r; - } - }; - - let sk = PrivateKey(jubjub::Fr::random(&mut rng)); - let vk = PublicKey::from_private(&sk, p_g); - - // TODO: This test will need to change when #77 is fixed - let msg = b"Foo bar"; - let sig = sk.sign(msg, &mut rng, p_g); - assert!(vk.verify(msg, &sig, p_g)); - - let vktorsion = PublicKey(vk.0 + p8); - assert!(vktorsion.verify(msg, &sig, p_g)); - } - - #[test] - fn round_trip_serialization() { - let mut rng = XorShiftRng::from_seed([ - 0x59, 0x62, 0xbe, 0x5d, 0x76, 0x3d, 0x31, 0x8d, 0x17, 0xdb, 0x37, 0x32, 0x54, 0x06, - 0xbc, 0xe5, - ]); - let p_g = SPENDING_KEY_GENERATOR; - - for _ in 0..1000 { - let sk = PrivateKey(jubjub::Fr::random(&mut rng)); - let vk = PublicKey::from_private(&sk, p_g); - let msg = b"Foo bar"; - let sig = sk.sign(msg, &mut rng, p_g); - - let mut sk_bytes = [0u8; 32]; - let mut vk_bytes = [0u8; 32]; - let mut sig_bytes = [0u8; 64]; - sk.write(&mut sk_bytes[..]).unwrap(); - vk.write(&mut vk_bytes[..]).unwrap(); - sig.write(&mut sig_bytes[..]).unwrap(); - - let sk_2 = PrivateKey::read(&sk_bytes[..]).unwrap(); - let vk_2 = PublicKey::from_private(&sk_2, p_g); - let mut vk_2_bytes = [0u8; 32]; - vk_2.write(&mut vk_2_bytes[..]).unwrap(); - assert!(vk_bytes == vk_2_bytes); - - let vk_2 = PublicKey::read(&vk_bytes[..]).unwrap(); - let sig_2 = Signature::read(&sig_bytes[..]).unwrap(); - assert!(vk.verify(msg, &sig_2, p_g)); - assert!(vk_2.verify(msg, &sig, p_g)); - assert!(vk_2.verify(msg, &sig_2, p_g)); - } - } - - #[test] - fn random_signatures() { - let mut rng = XorShiftRng::from_seed([ - 0x59, 0x62, 0xbe, 0x5d, 0x76, 0x3d, 0x31, 0x8d, 0x17, 0xdb, 0x37, 0x32, 0x54, 0x06, - 0xbc, 0xe5, - ]); - let p_g = SPENDING_KEY_GENERATOR; - - for _ in 0..1000 { - let sk = PrivateKey(jubjub::Fr::random(&mut rng)); - let vk = PublicKey::from_private(&sk, p_g); - - let msg1 = b"Foo bar"; - let msg2 = b"Spam eggs"; - - let sig1 = sk.sign(msg1, &mut rng, p_g); - let sig2 = sk.sign(msg2, &mut rng, p_g); - - assert!(vk.verify(msg1, &sig1, p_g)); - assert!(vk.verify(msg2, &sig2, p_g)); - assert!(!vk.verify(msg1, &sig2, p_g)); - assert!(!vk.verify(msg2, &sig1, p_g)); - - let alpha = jubjub::Fr::random(&mut rng); - let rsk = sk.randomize(alpha); - let rvk = vk.randomize(alpha, p_g); - - let sig1 = rsk.sign(msg1, &mut rng, p_g); - let sig2 = rsk.sign(msg2, &mut rng, p_g); - - assert!(rvk.verify(msg1, &sig1, p_g)); - assert!(rvk.verify(msg2, &sig2, p_g)); - assert!(!rvk.verify(msg1, &sig2, p_g)); - assert!(!rvk.verify(msg2, &sig1, p_g)); - } - } -} diff --git a/zcash_primitives/src/sapling/util.rs b/zcash_primitives/src/sapling/util.rs deleted file mode 100644 index 294ebdf16f..0000000000 --- a/zcash_primitives/src/sapling/util.rs +++ /dev/null @@ -1,37 +0,0 @@ -use blake2b_simd::Params; -use ff::Field; -use rand_core::{CryptoRng, RngCore}; - -use crate::consensus::{self, BlockHeight, NetworkUpgrade}; - -use super::Rseed; - -pub fn hash_to_scalar(persona: &[u8], a: &[u8], b: &[u8]) -> jubjub::Fr { - let mut hasher = Params::new().hash_length(64).personal(persona).to_state(); - hasher.update(a); - hasher.update(b); - let ret = hasher.finalize(); - jubjub::Fr::from_bytes_wide(ret.as_array()) -} - -pub fn generate_random_rseed( - params: &P, - height: BlockHeight, - rng: &mut R, -) -> Rseed { - generate_random_rseed_internal(params, height, rng) -} - -pub(crate) fn generate_random_rseed_internal( - params: &P, - height: BlockHeight, - rng: &mut R, -) -> Rseed { - if params.is_nu_active(NetworkUpgrade::Canopy, height) { - let mut buffer = [0u8; 32]; - rng.fill_bytes(&mut buffer); - Rseed::AfterZip212(buffer) - } else { - Rseed::BeforeZip212(jubjub::Fr::random(rng)) - } -} diff --git a/zcash_primitives/src/serialize.rs b/zcash_primitives/src/serialize.rs deleted file mode 100644 index e09fc44d12..0000000000 --- a/zcash_primitives/src/serialize.rs +++ /dev/null @@ -1,231 +0,0 @@ -use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; -use std::io::{self, Read, Write}; - -const MAX_SIZE: usize = 0x02000000; - -pub(crate) struct CompactSize; - -impl CompactSize { - pub(crate) fn read(mut reader: R) -> io::Result { - let flag = reader.read_u8()?; - match if flag < 253 { - Ok(flag as usize) - } else if flag == 253 { - match reader.read_u16::()? { - n if n < 253 => Err(io::Error::new( - io::ErrorKind::InvalidInput, - "non-canonical CompactSize", - )), - n => Ok(n as usize), - } - } else if flag == 254 { - match reader.read_u32::()? { - n if n < 0x10000 => Err(io::Error::new( - io::ErrorKind::InvalidInput, - "non-canonical CompactSize", - )), - n => Ok(n as usize), - } - } else { - match reader.read_u64::()? { - n if n < 0x100000000 => Err(io::Error::new( - io::ErrorKind::InvalidInput, - "non-canonical CompactSize", - )), - n => Ok(n as usize), - } - }? { - s if s > MAX_SIZE => Err(io::Error::new( - io::ErrorKind::InvalidInput, - "CompactSize too large", - )), - s => Ok(s), - } - } - - pub(crate) fn write(mut writer: W, size: usize) -> io::Result<()> { - match size { - s if s < 253 => writer.write_u8(s as u8), - s if s <= 0xFFFF => { - writer.write_u8(253)?; - writer.write_u16::(s as u16) - } - s if s <= 0xFFFFFFFF => { - writer.write_u8(254)?; - writer.write_u32::(s as u32) - } - s => { - writer.write_u8(255)?; - writer.write_u64::(s as u64) - } - } - } -} - -pub struct Vector; - -impl Vector { - pub fn read(mut reader: R, func: F) -> io::Result> - where - F: Fn(&mut R) -> io::Result, - { - let count = CompactSize::read(&mut reader)?; - (0..count).map(|_| func(&mut reader)).collect() - } - - pub fn write(mut writer: W, vec: &[E], func: F) -> io::Result<()> - where - F: Fn(&mut W, &E) -> io::Result<()>, - { - CompactSize::write(&mut writer, vec.len())?; - vec.iter().try_for_each(|e| func(&mut writer, e)) - } -} - -pub struct Optional; - -impl Optional { - pub fn read(mut reader: R, func: F) -> io::Result> - where - F: Fn(&mut R) -> io::Result, - { - match reader.read_u8()? { - 0 => Ok(None), - 1 => Ok(Some(func(&mut reader)?)), - _ => Err(io::Error::new( - io::ErrorKind::InvalidInput, - "non-canonical Option", - )), - } - } - - pub fn write(mut writer: W, val: &Option, func: F) -> io::Result<()> - where - F: Fn(&mut W, &T) -> io::Result<()>, - { - match val { - None => writer.write_u8(0), - Some(e) => { - writer.write_u8(1)?; - func(&mut writer, e) - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn compact_size() { - macro_rules! eval { - ($value:expr, $expected:expr) => { - let mut data = vec![]; - CompactSize::write(&mut data, $value).unwrap(); - assert_eq!(&data[..], &$expected[..]); - match CompactSize::read(&data[..]) { - Ok(n) => assert_eq!(n, $value), - Err(e) => panic!("Unexpected error: {:?}", e), - } - }; - } - - eval!(0, [0]); - eval!(1, [1]); - eval!(252, [252]); - eval!(253, [253, 253, 0]); - eval!(254, [253, 254, 0]); - eval!(255, [253, 255, 0]); - eval!(256, [253, 0, 1]); - eval!(256, [253, 0, 1]); - eval!(65535, [253, 255, 255]); - eval!(65536, [254, 0, 0, 1, 0]); - eval!(65537, [254, 1, 0, 1, 0]); - - eval!(33554432, [254, 0, 0, 0, 2]); - - { - let value = 33554433; - let encoded = &[254, 1, 0, 0, 2][..]; - let mut data = vec![]; - CompactSize::write(&mut data, value).unwrap(); - assert_eq!(&data[..], encoded); - assert!(CompactSize::read(encoded).is_err()); - } - } - - #[allow(clippy::useless_vec)] - #[test] - fn vector() { - macro_rules! eval { - ($value:expr, $expected:expr) => { - let mut data = vec![]; - Vector::write(&mut data, &$value, |w, e| w.write_u8(*e)).unwrap(); - assert_eq!(&data[..], &$expected[..]); - match Vector::read(&data[..], |r| r.read_u8()) { - Ok(v) => assert_eq!(v, $value), - Err(e) => panic!("Unexpected error: {:?}", e), - } - }; - } - - eval!(vec![], [0]); - eval!(vec![0], [1, 0]); - eval!(vec![1], [1, 1]); - eval!(vec![5; 8], [8, 5, 5, 5, 5, 5, 5, 5, 5]); - - { - // expected = [253, 4, 1, 7, 7, 7, ...] - let mut expected = vec![7; 263]; - expected[0] = 253; - expected[1] = 4; - expected[2] = 1; - - eval!(vec![7; 260], expected); - } - } - - #[test] - fn optional() { - macro_rules! eval { - ($value:expr, $expected:expr, $write:expr, $read:expr) => { - let mut data = vec![]; - Optional::write(&mut data, &$value, $write).unwrap(); - assert_eq!(&data[..], &$expected[..]); - match Optional::read(&data[..], $read) { - Ok(v) => assert_eq!(v, $value), - Err(e) => panic!("Unexpected error: {:?}", e), - } - }; - } - - macro_rules! eval_u8 { - ($value:expr, $expected:expr) => { - eval!($value, $expected, |w, e| w.write_u8(*e), |r| r.read_u8()) - }; - } - - macro_rules! eval_vec { - ($value:expr, $expected:expr) => { - eval!( - $value, - $expected, - |w, v| Vector::write(w, v, |w, e| w.write_u8(*e)), - |r| Vector::read(r, |r| r.read_u8()) - ) - }; - } - - eval_u8!(None, [0]); - eval_u8!(Some(0), [1, 0]); - eval_u8!(Some(1), [1, 1]); - eval_u8!(Some(5), [1, 5]); - - eval_vec!(None as Option>, [0]); - eval_vec!(Some(vec![]), [1, 0]); - eval_vec!(Some(vec![0]), [1, 1, 0]); - eval_vec!(Some(vec![1]), [1, 1, 1]); - eval_vec!(Some(vec![5; 8]), [1, 8, 5, 5, 5, 5, 5, 5, 5, 5]); - } -} diff --git a/zcash_primitives/src/test_vectors.rs b/zcash_primitives/src/test_vectors.rs deleted file mode 100644 index 403fbc962f..0000000000 --- a/zcash_primitives/src/test_vectors.rs +++ /dev/null @@ -1 +0,0 @@ -pub(crate) mod note_encryption; diff --git a/zcash_primitives/src/test_vectors/note_encryption.rs b/zcash_primitives/src/test_vectors/note_encryption.rs deleted file mode 100644 index 03a1b4c82d..0000000000 --- a/zcash_primitives/src/test_vectors/note_encryption.rs +++ /dev/null @@ -1,2046 +0,0 @@ -pub(crate) struct TestVector { - pub ovk: [u8; 32], - pub ivk: [u8; 32], - pub default_d: [u8; 11], - pub default_pk_d: [u8; 32], - pub v: u64, - pub rcm: [u8; 32], - pub memo: [u8; 512], - pub cv: [u8; 32], - pub cmu: [u8; 32], - pub esk: [u8; 32], - pub epk: [u8; 32], - pub shared_secret: [u8; 32], - pub k_enc: [u8; 32], - pub p_enc: [u8; 564], - pub c_enc: [u8; 580], - pub ock: [u8; 32], - pub op: [u8; 64], - pub c_out: [u8; 80], -} - -pub(crate) fn make_test_vectors() -> Vec { - // From https://github.com/zcash-hackworks/zcash-test-vectors/blob/master/sapling_note_encryption.py - vec![ - TestVector { - ovk: [ - 0x98, 0xd1, 0x69, 0x13, 0xd9, 0x9b, 0x04, 0x17, 0x7c, 0xab, 0xa4, 0x4f, 0x6e, 0x4d, - 0x22, 0x4e, 0x03, 0xb5, 0xac, 0x03, 0x1d, 0x7c, 0xe4, 0x5e, 0x86, 0x51, 0x38, 0xe1, - 0xb9, 0x96, 0xd6, 0x3b, - ], - ivk: [ - 0xb7, 0x0b, 0x7c, 0xd0, 0xed, 0x03, 0xcb, 0xdf, 0xd7, 0xad, 0xa9, 0x50, 0x2e, 0xe2, - 0x45, 0xb1, 0x3e, 0x56, 0x9d, 0x54, 0xa5, 0x71, 0x9d, 0x2d, 0xaa, 0x0f, 0x5f, 0x14, - 0x51, 0x47, 0x92, 0x04, - ], - default_d: [ - 0xf1, 0x9d, 0x9b, 0x79, 0x7e, 0x39, 0xf3, 0x37, 0x44, 0x58, 0x39, - ], - default_pk_d: [ - 0xdb, 0x4c, 0xd2, 0xb0, 0xaa, 0xc4, 0xf7, 0xeb, 0x8c, 0xa1, 0x31, 0xf1, 0x65, 0x67, - 0xc4, 0x45, 0xa9, 0x55, 0x51, 0x26, 0xd3, 0xc2, 0x9f, 0x14, 0xe3, 0xd7, 0x76, 0xe8, - 0x41, 0xae, 0x74, 0x15, - ], - v: 100000000, - rcm: [ - 0x39, 0x17, 0x6d, 0xac, 0x39, 0xac, 0xe4, 0x98, 0x0e, 0xcc, 0x8d, 0x77, 0x8e, 0x89, - 0x86, 0x02, 0x55, 0xec, 0x36, 0x15, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - ], - memo: [ - 0xf6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ], - cv: [ - 0xa9, 0xcb, 0x0d, 0x13, 0x72, 0x32, 0xff, 0x84, 0x48, 0xd0, 0xf0, 0x78, 0xb6, 0x81, - 0x4c, 0x66, 0xcb, 0x33, 0x1b, 0x0f, 0x2d, 0x3d, 0x8a, 0x08, 0x5b, 0xed, 0xba, 0x81, - 0x5f, 0x00, 0xa8, 0xdb, - ], - cmu: [ - 0x63, 0x55, 0x72, 0xf5, 0x72, 0xa8, 0xa1, 0xa0, 0xb7, 0xac, 0xbc, 0x0a, 0xfc, 0x6d, - 0x66, 0xf1, 0x4a, 0x02, 0xef, 0xac, 0xde, 0x7b, 0xdf, 0x03, 0x44, 0x3e, 0xd4, 0xc3, - 0xe5, 0x51, 0xd4, 0x70, - ], - esk: [ - 0x81, 0xc7, 0xb2, 0x17, 0x1f, 0xf4, 0x41, 0x52, 0x50, 0xca, 0xc0, 0x1f, 0x59, 0x82, - 0xfd, 0x8f, 0x49, 0x61, 0x9d, 0x61, 0xad, 0x78, 0xf6, 0x83, 0x0b, 0x3c, 0x60, 0x61, - 0x45, 0x96, 0x2a, 0x0e, - ], - epk: [ - 0xde, 0xd6, 0x8f, 0x05, 0xc6, 0x58, 0xfc, 0xae, 0x5a, 0xe2, 0x18, 0x64, 0x6f, 0xf8, - 0x44, 0x40, 0x6f, 0x84, 0x42, 0x67, 0x84, 0x04, 0x0d, 0x0b, 0xef, 0x2b, 0x09, 0xcb, - 0x38, 0x48, 0xc4, 0xdc, - ], - shared_secret: [ - 0x67, 0xf9, 0x61, 0x34, 0x04, 0xd9, 0xe9, 0x27, 0x1f, 0x16, 0x74, 0x01, 0x1b, 0x03, - 0x9b, 0x3d, 0x43, 0x81, 0xa4, 0xd7, 0x0c, 0x58, 0x6c, 0x8a, 0x13, 0x42, 0x28, 0x3f, - 0xd5, 0xfc, 0x3a, 0xde, - ], - k_enc: [ - 0xe5, 0xbf, 0x8a, 0xb2, 0xf9, 0x41, 0xe9, 0xb9, 0xd2, 0xc7, 0x4a, 0xce, 0x2d, 0xf6, - 0xb3, 0x3c, 0x3c, 0x32, 0x29, 0xfa, 0x0b, 0x91, 0x26, 0xf9, 0xdd, 0xdb, 0x43, 0x29, - 0x66, 0x10, 0x00, 0x69, - ], - p_enc: [ - 0x01, 0xf1, 0x9d, 0x9b, 0x79, 0x7e, 0x39, 0xf3, 0x37, 0x44, 0x58, 0x39, 0x00, 0xe1, - 0xf5, 0x05, 0x00, 0x00, 0x00, 0x00, 0x39, 0x17, 0x6d, 0xac, 0x39, 0xac, 0xe4, 0x98, - 0x0e, 0xcc, 0x8d, 0x77, 0x8e, 0x89, 0x86, 0x02, 0x55, 0xec, 0x36, 0x15, 0x06, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf6, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - ], - c_enc: [ - 0x8d, 0x6b, 0x27, 0xe7, 0xef, 0xf5, 0x9b, 0xfb, 0xa0, 0x1d, 0x65, 0x88, 0xba, 0xdd, - 0x36, 0x6c, 0xe5, 0x9b, 0x4d, 0x5b, 0x0e, 0xf9, 0x3b, 0xeb, 0xcb, 0xf2, 0x11, 0x41, - 0x7c, 0x56, 0xae, 0x70, 0x0a, 0xe1, 0x82, 0x44, 0xba, 0xc2, 0xfb, 0x64, 0x37, 0xdb, - 0x01, 0xf8, 0x3d, 0xc1, 0x49, 0xe2, 0x78, 0x6e, 0xc4, 0xec, 0x32, 0xc1, 0x1b, 0x05, - 0x4a, 0x4c, 0x0e, 0x2b, 0xdb, 0xe3, 0x43, 0x78, 0x8b, 0xb9, 0xc3, 0x3f, 0xf4, 0x2f, - 0xae, 0x99, 0x32, 0x32, 0x13, 0xe0, 0x96, 0x3e, 0x6f, 0x97, 0x6d, 0x6f, 0xff, 0xb8, - 0xc9, 0xfc, 0xf5, 0x21, 0x95, 0x74, 0xc7, 0xa9, 0x4c, 0x0e, 0x72, 0xf6, 0x09, 0x3a, - 0xed, 0xaf, 0xe3, 0x80, 0x62, 0x1b, 0x3b, 0xa8, 0x15, 0xd2, 0xb9, 0x72, 0x40, 0xf6, - 0x77, 0xd3, 0x90, 0xf5, 0xfc, 0x5d, 0x45, 0xee, 0xff, 0x16, 0x68, 0x8e, 0x40, 0xb9, - 0xee, 0xe8, 0xee, 0x1d, 0x39, 0x3b, 0x00, 0x97, 0x50, 0xcb, 0x73, 0xdf, 0x7a, 0x47, - 0xfd, 0x07, 0xa2, 0x81, 0x41, 0xdb, 0x49, 0xbd, 0x9c, 0xca, 0xb1, 0xf1, 0x8d, 0x0b, - 0x6a, 0x55, 0xed, 0x10, 0x1c, 0xa1, 0x6f, 0x73, 0x45, 0xbc, 0xb0, 0xbe, 0xaf, 0x7c, - 0xd7, 0x9a, 0x3d, 0x2b, 0xf2, 0x88, 0xf1, 0xd8, 0x8e, 0xbb, 0x1e, 0x4b, 0x74, 0x21, - 0x99, 0xd3, 0x30, 0xc3, 0x0a, 0x9f, 0xee, 0x1b, 0x44, 0xc6, 0x86, 0xa1, 0xff, 0x5c, - 0xc3, 0x3d, 0x46, 0x27, 0xf8, 0x3d, 0x61, 0xce, 0x34, 0xd6, 0xf1, 0x34, 0x4e, 0x2b, - 0x11, 0xa5, 0xf7, 0x17, 0x24, 0x42, 0x29, 0x60, 0x75, 0x91, 0x90, 0x05, 0x43, 0x4a, - 0x57, 0x4e, 0xd4, 0xe4, 0xc9, 0x8e, 0x23, 0x8e, 0xdd, 0x53, 0x67, 0xe8, 0xf5, 0x75, - 0x24, 0xb6, 0x38, 0xdd, 0x2d, 0x58, 0x30, 0xe8, 0x3f, 0x7f, 0x32, 0x08, 0x0d, 0x2d, - 0x51, 0xa0, 0x8a, 0xe8, 0x4e, 0x37, 0x42, 0x9c, 0x84, 0x38, 0xfa, 0xae, 0x15, 0x40, - 0x86, 0x7b, 0x12, 0xac, 0x2c, 0xf6, 0xa7, 0x7d, 0xa7, 0x80, 0xd9, 0x2c, 0xfa, 0x50, - 0x0c, 0x19, 0x5a, 0x07, 0x1c, 0xe8, 0xae, 0x3f, 0x10, 0x2c, 0xe0, 0x95, 0x01, 0xec, - 0xda, 0xc0, 0x8a, 0x79, 0x52, 0xa0, 0x8d, 0x53, 0xf3, 0x62, 0xd3, 0x7b, 0x64, 0x94, - 0x8c, 0x99, 0x15, 0xcb, 0xfc, 0x9f, 0x2d, 0x3c, 0x4e, 0x82, 0x22, 0xd3, 0x9a, 0x34, - 0x84, 0x21, 0x44, 0x7f, 0xab, 0xe4, 0xd5, 0xf0, 0x87, 0x80, 0x9a, 0x79, 0xe8, 0x49, - 0xb2, 0x8d, 0xff, 0xbc, 0x97, 0xfb, 0xbf, 0x64, 0x7f, 0xf3, 0x4f, 0x79, 0xff, 0x64, - 0xe7, 0x37, 0xeb, 0xf0, 0x3d, 0x8a, 0xdd, 0x44, 0xc1, 0x54, 0x32, 0x5f, 0x2b, 0xff, - 0x14, 0xc6, 0xe9, 0xe9, 0x0b, 0x0f, 0x98, 0x89, 0xf3, 0x25, 0xa9, 0x26, 0xa3, 0x68, - 0x56, 0x41, 0xa7, 0xa2, 0x19, 0xec, 0xe6, 0xfb, 0x2b, 0x4d, 0xee, 0xbf, 0x31, 0x09, - 0xd7, 0xee, 0x0f, 0x03, 0x9d, 0xac, 0x42, 0x74, 0x44, 0x99, 0x34, 0x85, 0x84, 0x84, - 0x44, 0xcc, 0xaf, 0xda, 0x5e, 0xa3, 0x28, 0x74, 0x06, 0x66, 0xdd, 0x75, 0xc3, 0x23, - 0xce, 0x7b, 0x92, 0x0e, 0xe0, 0xf3, 0xdc, 0x3a, 0xbc, 0xe6, 0xbd, 0x09, 0xc1, 0x3c, - 0x95, 0x7c, 0x5e, 0xa8, 0x95, 0x28, 0x27, 0x11, 0x6b, 0xb5, 0xbd, 0x0e, 0x5c, 0x27, - 0xf8, 0x20, 0xf2, 0xcf, 0x72, 0xa5, 0x10, 0x5d, 0x95, 0x55, 0xbe, 0x1e, 0x1e, 0x5e, - 0x68, 0xff, 0xfb, 0x71, 0x33, 0xdc, 0x39, 0x00, 0x19, 0x4e, 0x3b, 0x73, 0x1c, 0x7d, - 0x39, 0x11, 0x70, 0xad, 0x6d, 0x4a, 0xf1, 0x3a, 0x78, 0xa0, 0x6c, 0x25, 0xcf, 0xbb, - 0x0d, 0x09, 0x91, 0xd5, 0xa8, 0x83, 0xcf, 0xf5, 0x1c, 0xb6, 0xf5, 0x91, 0xc7, 0x92, - 0xd9, 0x9d, 0xcc, 0x55, 0x9c, 0xde, 0x9b, 0x7b, 0x39, 0xc4, 0xf5, 0x4a, 0x6b, 0xfb, - 0x29, 0xf1, 0xf8, 0x5e, 0x13, 0x5d, 0x17, 0x33, 0xb4, 0x9d, 0x5d, 0xd6, 0x70, 0x18, - 0xe6, 0x2e, 0x8c, 0x1a, 0xb0, 0xc1, 0x9a, 0x25, 0x41, 0x87, 0x26, 0xcc, 0xf2, 0xf5, - 0xe8, 0x8b, 0x97, 0x69, 0x21, 0x12, 0x92, 0x4b, 0xda, 0x2f, 0xde, 0x73, 0x48, 0xba, - 0xd7, 0x29, 0x52, 0x41, 0x72, 0x9d, 0xb4, 0xf3, 0x87, 0x11, 0xc7, 0xea, 0x98, 0xc5, - 0xd4, 0x19, 0x7c, 0x66, 0xfd, 0x23, - ], - ock: [ - 0x6c, 0xe6, 0x1e, 0xad, 0x78, 0x49, 0x20, 0x42, 0x93, 0x34, 0x9e, 0x83, 0x2e, 0x95, - 0xca, 0x3a, 0xc6, 0x42, 0x2e, 0xc4, 0xfe, 0x21, 0xe5, 0xd1, 0x53, 0x86, 0x55, 0x8e, - 0x4d, 0x37, 0x79, 0x6d, - ], - op: [ - 0xdb, 0x4c, 0xd2, 0xb0, 0xaa, 0xc4, 0xf7, 0xeb, 0x8c, 0xa1, 0x31, 0xf1, 0x65, 0x67, - 0xc4, 0x45, 0xa9, 0x55, 0x51, 0x26, 0xd3, 0xc2, 0x9f, 0x14, 0xe3, 0xd7, 0x76, 0xe8, - 0x41, 0xae, 0x74, 0x15, 0x81, 0xc7, 0xb2, 0x17, 0x1f, 0xf4, 0x41, 0x52, 0x50, 0xca, - 0xc0, 0x1f, 0x59, 0x82, 0xfd, 0x8f, 0x49, 0x61, 0x9d, 0x61, 0xad, 0x78, 0xf6, 0x83, - 0x0b, 0x3c, 0x60, 0x61, 0x45, 0x96, 0x2a, 0x0e, - ], - c_out: [ - 0x0e, 0xb2, 0xb0, 0x1b, 0xe8, 0x88, 0x0f, 0xc0, 0x46, 0x98, 0x42, 0x27, 0x14, 0x18, - 0xb5, 0x2b, 0xad, 0x40, 0x19, 0x89, 0x2c, 0xde, 0x53, 0xee, 0xca, 0xcd, 0xb2, 0xe4, - 0x5f, 0x5f, 0x33, 0x75, 0x85, 0xf7, 0xf6, 0x17, 0x5d, 0x88, 0x8f, 0x6e, 0x2c, 0x4e, - 0xd1, 0x35, 0x71, 0xcd, 0x96, 0xfd, 0x17, 0x7a, 0x01, 0xab, 0x10, 0x19, 0x08, 0xd7, - 0xca, 0x4a, 0x6d, 0x81, 0xd9, 0x16, 0x62, 0x2f, 0x5f, 0xf0, 0x77, 0xb1, 0x3f, 0x34, - 0x55, 0x90, 0xe2, 0x27, 0xc1, 0x0e, 0x08, 0x95, 0xe2, 0x04, - ], - }, - TestVector { - ovk: [ - 0x3b, 0x94, 0x62, 0x10, 0xce, 0x6d, 0x1b, 0x16, 0x92, 0xd7, 0x39, 0x2a, 0xc8, 0x4a, - 0x8b, 0xc8, 0xf0, 0x3b, 0x72, 0x72, 0x3c, 0x7d, 0x36, 0x72, 0x1b, 0x80, 0x9a, 0x79, - 0xc9, 0xd6, 0xe4, 0x5b, - ], - ivk: [ - 0xc5, 0x18, 0x38, 0x44, 0x66, 0xb2, 0x69, 0x88, 0xb5, 0x10, 0x90, 0x67, 0x41, 0x8d, - 0x19, 0x2d, 0x9d, 0x6b, 0xd0, 0xd9, 0x23, 0x22, 0x05, 0xd7, 0x74, 0x18, 0xc2, 0x40, - 0xfc, 0x68, 0xa4, 0x06, - ], - default_d: [ - 0xae, 0xf1, 0x80, 0xf6, 0xe3, 0x4e, 0x35, 0x4b, 0x88, 0x8f, 0x81, - ], - default_pk_d: [ - 0xa6, 0xb1, 0x3e, 0xa3, 0x36, 0xdd, 0xb7, 0xa6, 0x7b, 0xb0, 0x9a, 0x0e, 0x68, 0xe9, - 0xd3, 0xcf, 0xb3, 0x92, 0x10, 0x83, 0x1e, 0xa3, 0xa2, 0x96, 0xba, 0x09, 0xa9, 0x22, - 0x06, 0x0f, 0xd3, 0x8b, - ], - v: 200000000, - rcm: [ - 0x47, 0x8b, 0xa0, 0xee, 0x6e, 0x1a, 0x75, 0xb6, 0x00, 0x03, 0x6f, 0x26, 0xf1, 0x8b, - 0x70, 0x15, 0xab, 0x55, 0x6b, 0xed, 0xdf, 0x8b, 0x96, 0x02, 0x38, 0x86, 0x9f, 0x89, - 0xdd, 0x80, 0x4e, 0x06, - ], - memo: [ - 0xf6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ], - cv: [ - 0xfc, 0x54, 0x31, 0x9a, 0x39, 0xbe, 0x49, 0xc0, 0x48, 0x0c, 0x4d, 0xf3, 0x3b, 0x8f, - 0x77, 0xca, 0x67, 0x3a, 0x42, 0xbf, 0xde, 0xdf, 0xb8, 0x0e, 0xe4, 0x6b, 0x8f, 0x70, - 0xfc, 0x0d, 0xcd, 0x3d, - ], - cmu: [ - 0x0c, 0x87, 0x41, 0x75, 0x77, 0x48, 0x0b, 0x69, 0x77, 0xba, 0x92, 0xc5, 0x54, 0x25, - 0xd6, 0x2b, 0x03, 0xb1, 0xe5, 0xf3, 0xc3, 0x82, 0x9c, 0xac, 0x49, 0xbf, 0xe5, 0x15, - 0xae, 0x72, 0x29, 0x45, - ], - esk: [ - 0xad, 0x4a, 0xd6, 0x24, 0x77, 0xc2, 0xc8, 0x83, 0xc8, 0xba, 0xbf, 0xed, 0x5d, 0x38, - 0x5b, 0x51, 0xab, 0xdc, 0xc6, 0x98, 0xe9, 0x36, 0xe7, 0x8d, 0xc2, 0x26, 0x71, 0x72, - 0x91, 0x55, 0x62, 0x0b, - ], - epk: [ - 0xf0, 0x6c, 0xba, 0xf8, 0xcb, 0x5c, 0x84, 0x82, 0x38, 0x47, 0xa1, 0x20, 0x10, 0x4c, - 0x85, 0xad, 0x70, 0x72, 0x28, 0xad, 0xba, 0x87, 0x6c, 0x6d, 0x83, 0x7e, 0xfd, 0x41, - 0x4e, 0x1c, 0x1d, 0xb4, - ], - shared_secret: [ - 0xb9, 0x8a, 0x2c, 0x3b, 0xf0, 0xdc, 0x56, 0xb2, 0xbf, 0x65, 0xf5, 0xbd, 0x15, 0x25, - 0x05, 0x5e, 0xed, 0x22, 0xac, 0x0d, 0xcc, 0x2c, 0x11, 0xe3, 0x00, 0xc4, 0x67, 0x80, - 0x2b, 0x85, 0x88, 0x97, - ], - k_enc: [ - 0xb2, 0xef, 0x45, 0xb0, 0xf7, 0x25, 0x36, 0xa6, 0xc0, 0x22, 0xdd, 0xce, 0xe6, 0x2e, - 0xa7, 0x02, 0x7a, 0x49, 0x36, 0x2a, 0xa2, 0xdd, 0x3b, 0x54, 0x36, 0xd8, 0x89, 0x75, - 0xe0, 0x2a, 0xd0, 0xca, - ], - p_enc: [ - 0x01, 0xae, 0xf1, 0x80, 0xf6, 0xe3, 0x4e, 0x35, 0x4b, 0x88, 0x8f, 0x81, 0x00, 0xc2, - 0xeb, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x47, 0x8b, 0xa0, 0xee, 0x6e, 0x1a, 0x75, 0xb6, - 0x00, 0x03, 0x6f, 0x26, 0xf1, 0x8b, 0x70, 0x15, 0xab, 0x55, 0x6b, 0xed, 0xdf, 0x8b, - 0x96, 0x02, 0x38, 0x86, 0x9f, 0x89, 0xdd, 0x80, 0x4e, 0x06, 0xf6, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - ], - c_enc: [ - 0x8a, 0x3f, 0x60, 0x25, 0x2f, 0x4d, 0xf9, 0x96, 0x39, 0x2e, 0x55, 0xaf, 0xee, 0x07, - 0x22, 0xf1, 0x24, 0xb1, 0xa1, 0x34, 0xe8, 0xa1, 0xfb, 0x1e, 0xaa, 0x88, 0x88, 0x9e, - 0x6a, 0xd4, 0x89, 0xcf, 0x1b, 0xa9, 0x12, 0x55, 0xee, 0x56, 0xfa, 0x1a, 0x09, 0xdb, - 0x71, 0x56, 0xc3, 0x55, 0x1a, 0xed, 0x29, 0x69, 0xa6, 0xff, 0x37, 0xf2, 0xa7, 0x7a, - 0x60, 0xb3, 0xea, 0x43, 0x75, 0xfa, 0xff, 0x04, 0x9e, 0x85, 0xc2, 0x72, 0x21, 0xcc, - 0x2b, 0xa9, 0x89, 0xbd, 0x18, 0xff, 0x96, 0x98, 0x00, 0x0a, 0xf1, 0xa7, 0x64, 0x3f, - 0x87, 0x85, 0xd6, 0x5e, 0xbb, 0x04, 0xc8, 0x5b, 0x24, 0x75, 0xdf, 0x62, 0x5b, 0x47, - 0xe3, 0xe9, 0xc7, 0xac, 0xa8, 0x4c, 0x13, 0x17, 0x23, 0x77, 0x6b, 0xd8, 0xc2, 0x9f, - 0x9d, 0x1f, 0x5f, 0xd2, 0x57, 0xe5, 0x8f, 0x72, 0xb6, 0x04, 0xf9, 0xb5, 0x7b, 0x1c, - 0x2d, 0x05, 0x31, 0xeb, 0xbb, 0x19, 0xcf, 0xc2, 0x73, 0x68, 0x89, 0x0d, 0x25, 0x6e, - 0x9a, 0xba, 0x30, 0x8d, 0xb9, 0xd8, 0x85, 0x6f, 0x49, 0xd4, 0x66, 0x3a, 0xfe, 0x55, - 0x50, 0x72, 0xed, 0x64, 0xc8, 0x19, 0x8e, 0x6a, 0xd1, 0x5c, 0x0c, 0x43, 0xbb, 0x16, - 0x85, 0x49, 0xa5, 0xbe, 0x38, 0xc5, 0xb4, 0x6d, 0xc1, 0x2f, 0x0c, 0x2a, 0x96, 0x1f, - 0xf3, 0xcf, 0xe3, 0x2a, 0x1c, 0x3e, 0xfe, 0x80, 0xb1, 0x5e, 0x37, 0xe4, 0xce, 0xbe, - 0x2a, 0x7a, 0xbe, 0x03, 0xeb, 0x17, 0xf4, 0xbb, 0xad, 0x22, 0x31, 0xcb, 0x52, 0x55, - 0xe2, 0x9c, 0xd0, 0x3c, 0xb9, 0x61, 0x33, 0x2c, 0xf5, 0xe5, 0x5e, 0x60, 0x53, 0xcd, - 0x40, 0x65, 0xc3, 0x78, 0x56, 0x06, 0xb2, 0x18, 0x5f, 0x18, 0xc4, 0xa3, 0xa2, 0x26, - 0x23, 0xd2, 0x59, 0xcd, 0x20, 0xdb, 0xe1, 0x54, 0xc4, 0xaf, 0x6b, 0x2b, 0xdc, 0xf3, - 0xb9, 0xc0, 0xff, 0x13, 0xce, 0x27, 0xe3, 0x95, 0x05, 0xa9, 0xf1, 0xb8, 0x2f, 0x6f, - 0xce, 0xea, 0xc0, 0x95, 0x38, 0x47, 0x17, 0xe8, 0x97, 0x0e, 0xe0, 0x29, 0xde, 0x96, - 0x4e, 0x80, 0x4a, 0xbd, 0x32, 0xd4, 0xda, 0x93, 0xbb, 0x8d, 0xc2, 0xb6, 0xbd, 0x60, - 0x44, 0xd8, 0xdf, 0xd7, 0x9d, 0xf7, 0x20, 0x7e, 0xa0, 0x3b, 0xdf, 0x03, 0x6f, 0xa6, - 0x26, 0x3f, 0x21, 0xbc, 0x1b, 0xfd, 0x4a, 0x6d, 0x9c, 0xb5, 0xf2, 0xd8, 0xbb, 0x6e, - 0x74, 0xb6, 0xdd, 0x04, 0x7a, 0xe1, 0xaa, 0xb8, 0xc1, 0xa7, 0x23, 0xb4, 0x78, 0x7c, - 0x54, 0xe2, 0x53, 0x96, 0x7f, 0xa9, 0x44, 0x0b, 0x73, 0x61, 0x83, 0x50, 0x65, 0x74, - 0x35, 0x03, 0x55, 0x26, 0x9b, 0x2b, 0x66, 0xb7, 0x48, 0xe8, 0x8f, 0xe9, 0xb8, 0xd1, - 0x23, 0xe9, 0x4b, 0x5f, 0xa5, 0xd0, 0x72, 0xb8, 0xc3, 0x96, 0x52, 0xe9, 0x20, 0x2b, - 0x16, 0xf1, 0x65, 0x46, 0x0e, 0x4b, 0x97, 0x0f, 0x63, 0xee, 0x7d, 0x63, 0x8f, 0x48, - 0xe4, 0x90, 0x17, 0xea, 0x64, 0x1c, 0xd3, 0x70, 0x09, 0xd4, 0x4b, 0x77, 0x24, 0x18, - 0x25, 0x44, 0xdb, 0x92, 0xbd, 0x0c, 0x4a, 0x7e, 0x9d, 0x93, 0x93, 0xd4, 0x6f, 0xcb, - 0x7b, 0xdd, 0xf9, 0x6f, 0x02, 0xcb, 0xf4, 0x7f, 0xa0, 0xf5, 0x28, 0x04, 0x09, 0x8e, - 0xcb, 0xbb, 0x7a, 0x13, 0xf3, 0xa2, 0xa5, 0xf1, 0x63, 0x8e, 0x77, 0xf8, 0xa8, 0x2f, - 0x6c, 0x3d, 0xec, 0xb7, 0x60, 0x7f, 0x09, 0x51, 0xc5, 0x7c, 0x7f, 0x27, 0x76, 0x04, - 0x22, 0x14, 0xf9, 0x0a, 0x3b, 0x6e, 0x00, 0xed, 0x16, 0x05, 0x9d, 0xff, 0x45, 0x55, - 0xbd, 0x47, 0x1d, 0x78, 0xaf, 0xe7, 0xaa, 0x3d, 0xc7, 0x91, 0x41, 0xa0, 0x87, 0x2d, - 0x19, 0xc8, 0x1c, 0x35, 0x1c, 0xaf, 0x54, 0xa2, 0xfc, 0x6d, 0xe8, 0xfd, 0x76, 0x86, - 0xc4, 0xf2, 0xc5, 0x34, 0xef, 0xac, 0x77, 0x51, 0x5e, 0x30, 0xf2, 0x50, 0x7b, 0xa0, - 0xb2, 0x3b, 0x1e, 0xe3, 0x7c, 0xa9, 0x08, 0x94, 0x3d, 0xfe, 0xf3, 0x80, 0x9a, 0x7e, - 0x9b, 0xec, 0xf1, 0xb9, 0x69, 0x10, 0x49, 0xf7, 0x87, 0x6a, 0x59, 0x2e, 0xe7, 0xed, - 0x64, 0x74, 0x0f, 0x1b, 0xe7, 0xe3, 0x06, 0x6e, 0xf7, 0x6f, 0x81, 0x47, 0x0f, 0x43, - 0x54, 0x33, 0x1a, 0xa1, 0xbc, 0x49, 0x57, 0x96, 0x99, 0x69, 0x77, 0x82, 0xbb, 0x07, - 0x5c, 0xbf, 0x82, 0xd3, 0xa8, 0xc0, - ], - ock: [ - 0x6f, 0xce, 0x27, 0xbf, 0x1a, 0x62, 0xf0, 0x78, 0xe7, 0xe3, 0xcb, 0x5d, 0x8b, 0xf2, - 0x4c, 0xa7, 0xe4, 0xa5, 0x82, 0x1d, 0x45, 0x5f, 0x0f, 0xa8, 0x2c, 0xd5, 0x44, 0xec, - 0xb4, 0x20, 0x91, 0xfa, - ], - op: [ - 0xa6, 0xb1, 0x3e, 0xa3, 0x36, 0xdd, 0xb7, 0xa6, 0x7b, 0xb0, 0x9a, 0x0e, 0x68, 0xe9, - 0xd3, 0xcf, 0xb3, 0x92, 0x10, 0x83, 0x1e, 0xa3, 0xa2, 0x96, 0xba, 0x09, 0xa9, 0x22, - 0x06, 0x0f, 0xd3, 0x8b, 0xad, 0x4a, 0xd6, 0x24, 0x77, 0xc2, 0xc8, 0x83, 0xc8, 0xba, - 0xbf, 0xed, 0x5d, 0x38, 0x5b, 0x51, 0xab, 0xdc, 0xc6, 0x98, 0xe9, 0x36, 0xe7, 0x8d, - 0xc2, 0x26, 0x71, 0x72, 0x91, 0x55, 0x62, 0x0b, - ], - c_out: [ - 0x88, 0x24, 0x58, 0x30, 0x2c, 0x0a, 0xba, 0x55, 0xed, 0x8d, 0x67, 0x18, 0xca, 0x26, - 0xd8, 0xc2, 0x8a, 0x12, 0x7a, 0x01, 0xe7, 0x7c, 0x2a, 0xe5, 0xbf, 0x15, 0xc6, 0x96, - 0x73, 0x91, 0x81, 0x77, 0xf9, 0x24, 0x77, 0xa2, 0x18, 0xa7, 0xf6, 0xcf, 0x12, 0x17, - 0x80, 0x22, 0xc9, 0xdd, 0xc7, 0x18, 0x5c, 0x18, 0xd0, 0x87, 0x6c, 0x3c, 0x29, 0x65, - 0x83, 0xe0, 0xbc, 0x54, 0x79, 0x3b, 0xf1, 0xe2, 0x6a, 0x85, 0x4a, 0x41, 0xab, 0x61, - 0x7f, 0x20, 0x52, 0x71, 0xba, 0x6c, 0x14, 0x29, 0xbd, 0xf4, - ], - }, - TestVector { - ovk: [ - 0x8b, 0xf4, 0x39, 0x0e, 0x28, 0xdd, 0xc9, 0x5b, 0x83, 0x02, 0xc3, 0x81, 0xd5, 0x81, - 0x0b, 0x84, 0xba, 0x8e, 0x60, 0x96, 0xe5, 0xa7, 0x68, 0x22, 0x77, 0x4f, 0xd4, 0x9f, - 0x49, 0x1e, 0x8f, 0x49, - ], - ivk: [ - 0x47, 0x1c, 0x24, 0xa3, 0xdc, 0x87, 0x30, 0xe7, 0x50, 0x36, 0xc0, 0xa9, 0x5f, 0x3e, - 0x2f, 0x7d, 0xd1, 0xbe, 0x6f, 0xb9, 0x3a, 0xd2, 0x95, 0x92, 0x20, 0x3d, 0xef, 0x30, - 0x41, 0x95, 0x45, 0x05, - ], - default_d: [ - 0x75, 0x99, 0xf0, 0xbf, 0x9b, 0x57, 0xcd, 0x2d, 0xc2, 0x99, 0xb6, - ], - default_pk_d: [ - 0x66, 0x14, 0x17, 0x39, 0x51, 0x4b, 0x28, 0xf0, 0x5d, 0xef, 0x8a, 0x18, 0xee, 0xee, - 0x5e, 0xed, 0x4d, 0x44, 0xc6, 0x22, 0x5c, 0x3c, 0x65, 0xd8, 0x8d, 0xd9, 0x90, 0x77, - 0x08, 0x01, 0x2f, 0x5a, - ], - v: 300000000, - rcm: [ - 0x14, 0x7c, 0xf2, 0xb5, 0x1b, 0x4c, 0x7c, 0x63, 0xcb, 0x77, 0xb9, 0x9e, 0x8b, 0x78, - 0x3e, 0x5b, 0x51, 0x11, 0xdb, 0x0a, 0x7c, 0xa0, 0x4d, 0x6c, 0x01, 0x4a, 0x1d, 0x7d, - 0xa8, 0x3b, 0xae, 0x0a, - ], - memo: [ - 0xf6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ], - cv: [ - 0x5c, 0xc9, 0xea, 0x16, 0x8e, 0x79, 0xff, 0x0d, 0x08, 0x3a, 0xf4, 0x21, 0xd3, 0x2d, - 0x27, 0xfb, 0xa1, 0xc8, 0xa6, 0x38, 0xc0, 0xc3, 0x52, 0xcf, 0x59, 0xdc, 0xb1, 0xca, - 0x84, 0xc3, 0xfb, 0x1b, - ], - cmu: [ - 0xb3, 0xb4, 0xe7, 0xab, 0x08, 0x0b, 0x9b, 0x0f, 0xe4, 0x73, 0xcf, 0xc5, 0xa3, 0x10, - 0x5e, 0x9a, 0x06, 0x2a, 0x4e, 0xe4, 0x9e, 0xdd, 0x70, 0x95, 0xa6, 0x71, 0x63, 0x7e, - 0x00, 0x57, 0x24, 0x2b, - ], - esk: [ - 0x99, 0xaa, 0x10, 0xc0, 0x57, 0x88, 0x08, 0x1c, 0x0d, 0xa7, 0xd8, 0x79, 0xcd, 0x95, - 0x43, 0xec, 0x18, 0x92, 0x15, 0x72, 0x92, 0x40, 0x2e, 0x96, 0x0b, 0x06, 0x99, 0x5a, - 0x08, 0x96, 0x4c, 0x03, - ], - epk: [ - 0x6a, 0x92, 0x02, 0x60, 0x43, 0xfa, 0x93, 0x0e, 0xeb, 0x2b, 0x28, 0xfd, 0x7b, 0xbd, - 0xc5, 0xa7, 0x05, 0x00, 0xbe, 0xb8, 0x4c, 0x67, 0x11, 0x36, 0x23, 0x8e, 0x5e, 0xfd, - 0xb0, 0x17, 0xd9, 0x9c, - ], - shared_secret: [ - 0x50, 0x78, 0x28, 0x7f, 0xf1, 0x7b, 0x1d, 0x92, 0x9b, 0x6a, 0x99, 0xb5, 0xe2, 0x82, - 0x68, 0xa1, 0x92, 0x93, 0x95, 0x73, 0xda, 0xc4, 0xe8, 0x4d, 0x51, 0x1b, 0x53, 0x93, - 0xd7, 0x2a, 0x6d, 0x68, - ], - k_enc: [ - 0xa4, 0x3c, 0xaa, 0xd6, 0x25, 0x30, 0xde, 0x86, 0xdf, 0x57, 0xe9, 0xde, 0x03, 0x47, - 0xa2, 0xd8, 0x06, 0x40, 0x53, 0x0a, 0x4c, 0xa9, 0x7b, 0x82, 0x92, 0xa5, 0xa5, 0x25, - 0x0f, 0x1b, 0xf2, 0x40, - ], - p_enc: [ - 0x01, 0x75, 0x99, 0xf0, 0xbf, 0x9b, 0x57, 0xcd, 0x2d, 0xc2, 0x99, 0xb6, 0x00, 0xa3, - 0xe1, 0x11, 0x00, 0x00, 0x00, 0x00, 0x14, 0x7c, 0xf2, 0xb5, 0x1b, 0x4c, 0x7c, 0x63, - 0xcb, 0x77, 0xb9, 0x9e, 0x8b, 0x78, 0x3e, 0x5b, 0x51, 0x11, 0xdb, 0x0a, 0x7c, 0xa0, - 0x4d, 0x6c, 0x01, 0x4a, 0x1d, 0x7d, 0xa8, 0x3b, 0xae, 0x0a, 0xf6, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - ], - c_enc: [ - 0x4c, 0xac, 0xe5, 0x2f, 0x2d, 0xa8, 0x2a, 0x34, 0xe3, 0x0d, 0xe8, 0xfb, 0x2e, 0x25, - 0x6b, 0xef, 0xd9, 0x2d, 0xd3, 0x0e, 0xf7, 0x86, 0x85, 0xa5, 0x08, 0xe4, 0x41, 0x0c, - 0x79, 0x33, 0x6f, 0x0a, 0xf1, 0xb2, 0x64, 0x84, 0x82, 0x33, 0x59, 0x24, 0x78, 0xd2, - 0x2d, 0xf7, 0x91, 0xab, 0x8d, 0x4c, 0x7d, 0x32, 0x3c, 0xd8, 0x4d, 0x6b, 0x2e, 0x4d, - 0xcf, 0x66, 0x49, 0x5b, 0x46, 0xc5, 0x31, 0xa3, 0x21, 0x67, 0x66, 0xfc, 0x8b, 0x6f, - 0x65, 0xfe, 0x57, 0x6c, 0x44, 0xef, 0x88, 0xc4, 0x44, 0xfa, 0x95, 0x7f, 0xbd, 0x87, - 0xaf, 0x7a, 0x30, 0xf5, 0x2b, 0xd3, 0xf2, 0x33, 0x8c, 0xbb, 0x0b, 0x7e, 0xe6, 0x68, - 0x5c, 0x51, 0xec, 0xef, 0xb5, 0xfd, 0x17, 0xd7, 0x53, 0x0b, 0xb6, 0x14, 0x52, 0x28, - 0xbb, 0x97, 0x6a, 0x56, 0xa1, 0xc9, 0xb2, 0xc8, 0xd2, 0x86, 0x4c, 0x43, 0xd3, 0xcd, - 0x64, 0x0b, 0xd7, 0xe0, 0x1f, 0x08, 0xaa, 0xc4, 0x16, 0xd2, 0x25, 0x0d, 0xf7, 0xf4, - 0xb1, 0xb9, 0xeb, 0xd9, 0xbd, 0x10, 0x3f, 0xd4, 0x17, 0xfd, 0xbe, 0x57, 0x13, 0x2e, - 0xab, 0xfc, 0x52, 0xc3, 0x79, 0x8e, 0x98, 0xc3, 0x7c, 0x1a, 0xf3, 0x4d, 0x28, 0x91, - 0x2c, 0x1d, 0x11, 0x64, 0xb5, 0x27, 0x71, 0x07, 0xc4, 0x7d, 0x6b, 0xd5, 0xf3, 0xc0, - 0xb3, 0x0f, 0x4e, 0xfa, 0xb7, 0xef, 0x04, 0x15, 0x8e, 0x11, 0x9d, 0x7c, 0x40, 0x79, - 0x4a, 0xb0, 0xd4, 0x23, 0x19, 0x49, 0xe7, 0xf8, 0x0f, 0x43, 0xd7, 0x63, 0x64, 0x56, - 0xfe, 0xe2, 0xe1, 0x27, 0x2e, 0xa1, 0xe2, 0xec, 0x3e, 0x8f, 0xf3, 0x06, 0x98, 0xb8, - 0x32, 0x64, 0x71, 0xeb, 0xa9, 0x40, 0x95, 0x0d, 0x55, 0x83, 0x62, 0x4d, 0xfd, 0xab, - 0xe8, 0x7d, 0x7c, 0x52, 0xa4, 0xd0, 0x0e, 0xf2, 0x00, 0x42, 0x38, 0x1c, 0x9e, 0x6f, - 0x03, 0xd3, 0x29, 0xbb, 0xf4, 0x20, 0x43, 0xf2, 0xf3, 0xb4, 0xfd, 0x77, 0x54, 0x16, - 0x32, 0x40, 0x2e, 0x06, 0x11, 0xb2, 0x44, 0xb0, 0xc2, 0x80, 0x3c, 0xd5, 0x12, 0x50, - 0x81, 0x4c, 0xff, 0xdd, 0x7e, 0xeb, 0x17, 0x35, 0xbe, 0xba, 0x8e, 0xa8, 0xa5, 0x8e, - 0xbc, 0xc3, 0x23, 0xf4, 0x24, 0xfc, 0xd5, 0xa7, 0x3d, 0xcc, 0xa2, 0xf5, 0x06, 0xfc, - 0xa4, 0x03, 0x19, 0x9f, 0x0c, 0xc7, 0xb1, 0xe9, 0x7b, 0x92, 0x0b, 0xa2, 0x72, 0x35, - 0xcd, 0x39, 0xe5, 0x27, 0x38, 0x2b, 0xad, 0x3a, 0x48, 0x3b, 0x9f, 0x1e, 0xbb, 0xf2, - 0x91, 0x77, 0xae, 0x94, 0xd8, 0xfa, 0x63, 0xbe, 0xeb, 0x45, 0x6d, 0x12, 0x78, 0xb9, - 0xd2, 0x28, 0x59, 0x44, 0x31, 0x99, 0x04, 0xdd, 0xe4, 0x2a, 0xdc, 0x70, 0x62, 0xb5, - 0x50, 0xb1, 0xff, 0x47, 0xb7, 0x0d, 0x3c, 0x78, 0xc2, 0x4c, 0x55, 0x06, 0x9f, 0x72, - 0x0f, 0xea, 0x60, 0x23, 0xf2, 0x19, 0x4a, 0x72, 0x91, 0xff, 0xb8, 0x11, 0xf6, 0x8a, - 0x16, 0xd6, 0xc1, 0x15, 0xf4, 0xd8, 0xc6, 0x85, 0xe0, 0x9a, 0x44, 0xda, 0x84, 0x11, - 0xe1, 0xb9, 0xb5, 0x3f, 0x39, 0xd5, 0x18, 0x46, 0x14, 0x7d, 0xdb, 0x62, 0x08, 0x98, - 0xe0, 0x80, 0xb7, 0xa6, 0x5f, 0xe8, 0xe2, 0xe1, 0x31, 0x2b, 0x0b, 0x81, 0x52, 0x13, - 0x8a, 0x8b, 0xa9, 0xe0, 0x86, 0x67, 0x90, 0x57, 0x17, 0x9f, 0xf0, 0x9f, 0x7b, 0x3c, - 0xbf, 0x58, 0xbf, 0x59, 0xe3, 0x3f, 0x83, 0xde, 0x2c, 0x70, 0x35, 0x0a, 0xb5, 0x7c, - 0x82, 0xbe, 0x9e, 0xc9, 0x5c, 0xcc, 0x95, 0xe2, 0xbe, 0x29, 0x4e, 0xc5, 0x38, 0x3f, - 0xa3, 0xbb, 0xd7, 0xa7, 0x59, 0x31, 0x5c, 0xc2, 0x5d, 0xea, 0x38, 0x53, 0xe7, 0xb5, - 0x36, 0x6b, 0xaa, 0xe0, 0x5a, 0xca, 0x8b, 0xc9, 0x56, 0xf1, 0xd5, 0xbd, 0xdc, 0xbd, - 0xa2, 0x95, 0xa5, 0xca, 0x7c, 0x2e, 0x26, 0xfb, 0x4e, 0x26, 0xf7, 0xeb, 0xdf, 0x62, - 0x44, 0xb7, 0x8a, 0x59, 0x1e, 0xfa, 0xa3, 0xa6, 0xf4, 0x8c, 0xc4, 0x10, 0x59, 0x78, - 0xc9, 0x68, 0xdd, 0x85, 0x88, 0x79, 0x5a, 0x9a, 0x65, 0x71, 0x17, 0x93, 0xf1, 0x98, - 0x04, 0xf8, 0x81, 0x4b, 0x4a, 0x9d, 0xb0, 0xbf, 0xa1, 0x57, 0x76, 0x9a, 0xaf, 0xda, - 0x2d, 0xb0, 0xee, 0xf0, 0x2b, 0x9a, 0x81, 0x16, 0x3b, 0x7c, 0x23, 0x56, 0x97, 0x62, - 0x0c, 0x72, 0xd8, 0x24, 0xe3, 0x2b, - ], - ock: [ - 0x24, 0x11, 0xa0, 0xf9, 0x31, 0xa8, 0xd3, 0x51, 0x6c, 0xdb, 0x71, 0x93, 0xc9, 0x41, - 0xcf, 0x0e, 0x49, 0xc3, 0x66, 0xae, 0x72, 0xc9, 0x79, 0xc4, 0x90, 0x49, 0xc9, 0x4b, - 0xd3, 0xc7, 0x5c, 0xf4, - ], - op: [ - 0x66, 0x14, 0x17, 0x39, 0x51, 0x4b, 0x28, 0xf0, 0x5d, 0xef, 0x8a, 0x18, 0xee, 0xee, - 0x5e, 0xed, 0x4d, 0x44, 0xc6, 0x22, 0x5c, 0x3c, 0x65, 0xd8, 0x8d, 0xd9, 0x90, 0x77, - 0x08, 0x01, 0x2f, 0x5a, 0x99, 0xaa, 0x10, 0xc0, 0x57, 0x88, 0x08, 0x1c, 0x0d, 0xa7, - 0xd8, 0x79, 0xcd, 0x95, 0x43, 0xec, 0x18, 0x92, 0x15, 0x72, 0x92, 0x40, 0x2e, 0x96, - 0x0b, 0x06, 0x99, 0x5a, 0x08, 0x96, 0x4c, 0x03, - ], - c_out: [ - 0x9d, 0xcf, 0xab, 0x0d, 0x20, 0x54, 0xd2, 0xbd, 0xf4, 0x06, 0xc3, 0x1b, 0x41, 0x78, - 0x46, 0x5d, 0xe6, 0x50, 0x5d, 0xb3, 0xbe, 0x9b, 0x69, 0x36, 0xf7, 0x8d, 0x2e, 0x29, - 0x37, 0x57, 0x9b, 0x58, 0x2e, 0x83, 0x28, 0x61, 0x92, 0x9a, 0x75, 0x17, 0x88, 0x04, - 0xb6, 0x57, 0x12, 0x6a, 0xdd, 0x74, 0x2e, 0x06, 0xcb, 0x84, 0x36, 0x86, 0x42, 0xdb, - 0x9b, 0xf4, 0x7a, 0xc6, 0xe4, 0xdc, 0x1a, 0xf1, 0x78, 0x19, 0x8b, 0x22, 0xd6, 0x26, - 0x23, 0x45, 0x37, 0x3b, 0x0f, 0x56, 0x2e, 0xf2, 0x7b, 0xb0, - ], - }, - TestVector { - ovk: [ - 0x14, 0x76, 0x78, 0xe0, 0x55, 0x3b, 0x97, 0x82, 0x93, 0x47, 0x64, 0x7c, 0x5b, 0xc7, - 0xda, 0xb4, 0xcc, 0x22, 0x02, 0xb5, 0x4e, 0xc2, 0x9f, 0xd3, 0x1a, 0x3d, 0xe6, 0xbe, - 0x08, 0x25, 0xfc, 0x5e, - ], - ivk: [ - 0x63, 0x6a, 0xa9, 0x64, 0xbf, 0xc2, 0x3c, 0xe4, 0xb1, 0xfc, 0xf7, 0xdf, 0xc9, 0x91, - 0x79, 0xdd, 0xc4, 0x06, 0xff, 0x55, 0x40, 0x0c, 0x92, 0x95, 0xac, 0xfc, 0x14, 0xf0, - 0x31, 0xc7, 0x26, 0x00, - ], - default_d: [ - 0x1b, 0x81, 0x61, 0x4f, 0x1d, 0xad, 0xea, 0x0f, 0x8d, 0x0a, 0x58, - ], - default_pk_d: [ - 0x25, 0xeb, 0x55, 0xfc, 0xcf, 0x76, 0x1f, 0xc6, 0x4e, 0x85, 0xa5, 0x88, 0xef, 0xe6, - 0xea, 0xd7, 0x83, 0x2f, 0xb1, 0xf0, 0xf7, 0xa8, 0x31, 0x65, 0x89, 0x5b, 0xdf, 0xf9, - 0x42, 0x92, 0x5f, 0x5c, - ], - v: 400000000, - rcm: [ - 0x34, 0xa4, 0xb2, 0xa9, 0x14, 0x4f, 0xf5, 0xea, 0x54, 0xef, 0xee, 0x87, 0xcf, 0x90, - 0x1b, 0x5b, 0xed, 0x5e, 0x35, 0xd2, 0x1f, 0xbb, 0xd7, 0x88, 0xd5, 0xbd, 0x9d, 0x83, - 0x3e, 0x11, 0x28, 0x04, - ], - memo: [ - 0xf6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ], - cv: [ - 0x6d, 0x6e, 0xf8, 0xce, 0x97, 0x92, 0x74, 0x09, 0x4f, 0x19, 0x1a, 0xef, 0x64, 0x3f, - 0x3f, 0xcb, 0xd1, 0xac, 0x9d, 0x98, 0xd6, 0x07, 0xe2, 0xbc, 0xfe, 0xf6, 0xfd, 0x51, - 0xba, 0x4b, 0xb4, 0xb9, - ], - cmu: [ - 0x51, 0xfd, 0xdd, 0x70, 0x8c, 0xd1, 0x51, 0xd3, 0xca, 0x47, 0x17, 0xe3, 0xc9, 0x9e, - 0xeb, 0x8f, 0x64, 0xf1, 0x04, 0x49, 0x5f, 0x26, 0xde, 0x05, 0x7b, 0x68, 0x10, 0x63, - 0xb9, 0xc9, 0x78, 0x2d, - ], - esk: [ - 0xbd, 0xde, 0x13, 0x81, 0xec, 0x9f, 0xf4, 0x21, 0xca, 0xfd, 0x1e, 0x31, 0xcc, 0x5d, - 0xe2, 0x55, 0x59, 0x88, 0x1f, 0x6b, 0x21, 0xb2, 0x17, 0x5d, 0x0d, 0xce, 0x94, 0x08, - 0x59, 0x7e, 0xa1, 0x03, - ], - epk: [ - 0x04, 0xa1, 0x0a, 0x3e, 0xa0, 0xe4, 0xb1, 0xa1, 0xd1, 0x3a, 0x67, 0xbc, 0xb2, 0x7d, - 0xe6, 0x34, 0xe1, 0x94, 0xb2, 0x08, 0x01, 0x62, 0x61, 0x9f, 0xbc, 0xa7, 0x66, 0x2d, - 0x42, 0xb8, 0xa5, 0x5f, - ], - shared_secret: [ - 0xdd, 0x88, 0x05, 0x9f, 0xd9, 0x05, 0x90, 0x13, 0xf2, 0xb9, 0xfa, 0xa2, 0x3a, 0x6b, - 0xa1, 0x49, 0xb2, 0xff, 0x0e, 0x37, 0x79, 0x3a, 0x3e, 0x8d, 0x92, 0x70, 0xff, 0x71, - 0x67, 0xfd, 0x7a, 0x8d, - ], - k_enc: [ - 0xab, 0xa4, 0xd4, 0xa5, 0xb5, 0x1a, 0x8b, 0xf5, 0x2e, 0x29, 0xd6, 0x80, 0x3a, 0xb9, - 0x33, 0x0c, 0xf9, 0xc8, 0x2b, 0x1e, 0xb1, 0xfe, 0xe6, 0xa1, 0xa5, 0x54, 0x4a, 0x82, - 0xc7, 0xb3, 0x16, 0x82, - ], - p_enc: [ - 0x01, 0x1b, 0x81, 0x61, 0x4f, 0x1d, 0xad, 0xea, 0x0f, 0x8d, 0x0a, 0x58, 0x00, 0x84, - 0xd7, 0x17, 0x00, 0x00, 0x00, 0x00, 0x34, 0xa4, 0xb2, 0xa9, 0x14, 0x4f, 0xf5, 0xea, - 0x54, 0xef, 0xee, 0x87, 0xcf, 0x90, 0x1b, 0x5b, 0xed, 0x5e, 0x35, 0xd2, 0x1f, 0xbb, - 0xd7, 0x88, 0xd5, 0xbd, 0x9d, 0x83, 0x3e, 0x11, 0x28, 0x04, 0xf6, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - ], - c_enc: [ - 0x9d, 0xb8, 0xb2, 0x4a, 0x05, 0x6f, 0x99, 0x6d, 0x39, 0x2d, 0x4d, 0x96, 0x3e, 0xa3, - 0x89, 0x76, 0xd0, 0xf3, 0x5e, 0x85, 0xd8, 0xaa, 0x84, 0x7a, 0x08, 0x96, 0x16, 0x4e, - 0x39, 0xd8, 0x69, 0x7a, 0xe1, 0x80, 0xc4, 0xdc, 0xc1, 0x70, 0x61, 0xd5, 0xf3, 0x99, - 0xe0, 0xac, 0x4e, 0xcb, 0x5f, 0x02, 0xd4, 0xd9, 0xa3, 0xca, 0x5b, 0x33, 0x51, 0x8c, - 0x58, 0xb1, 0xa0, 0x73, 0xbc, 0xa7, 0xee, 0x67, 0x41, 0x01, 0x03, 0x05, 0xdb, 0xb8, - 0xc7, 0x38, 0x38, 0x35, 0xb9, 0xc7, 0x80, 0xa9, 0x42, 0x78, 0x5c, 0x57, 0xa3, 0x09, - 0x8a, 0x81, 0xae, 0xf5, 0xd7, 0x06, 0x1f, 0xda, 0xba, 0xcf, 0x52, 0x72, 0x15, 0x30, - 0xef, 0x32, 0xdf, 0xfc, 0x01, 0x10, 0x19, 0xeb, 0xd3, 0x60, 0x97, 0xe8, 0x4d, 0xf2, - 0x03, 0x63, 0xcf, 0x18, 0x22, 0xb1, 0x15, 0x0c, 0x24, 0x73, 0x58, 0x2b, 0x01, 0xf8, - 0xd8, 0x67, 0x99, 0xc1, 0x73, 0xf7, 0xfe, 0xf8, 0xca, 0x93, 0x8e, 0x4c, 0xde, 0x71, - 0x85, 0xa1, 0x9d, 0x70, 0xad, 0x38, 0x61, 0x47, 0x9e, 0x7d, 0x43, 0x81, 0x0d, 0xc5, - 0x64, 0x24, 0x71, 0x03, 0x33, 0x49, 0x28, 0x6b, 0xaf, 0x71, 0x4f, 0x7f, 0xdc, 0x22, - 0xb3, 0x81, 0xd9, 0xe3, 0xad, 0xf3, 0xbc, 0x10, 0x49, 0x87, 0x8e, 0x18, 0x6d, 0x53, - 0x2d, 0x8c, 0x98, 0x70, 0xf6, 0x01, 0x80, 0xd6, 0x54, 0x72, 0x45, 0x5d, 0x22, 0xd2, - 0x59, 0x24, 0xb9, 0x92, 0xc0, 0x2f, 0x94, 0xea, 0x6e, 0xaf, 0x75, 0xb9, 0xdc, 0x88, - 0x3d, 0xe7, 0x37, 0x6d, 0xa6, 0x01, 0x8e, 0x55, 0x45, 0x1e, 0x23, 0xf2, 0x38, 0xe1, - 0x09, 0xa6, 0x40, 0x07, 0x89, 0xf9, 0x30, 0x52, 0x57, 0x9b, 0xbb, 0x18, 0x40, 0x19, - 0xf3, 0x09, 0xb3, 0xd0, 0x6d, 0x07, 0x67, 0xa1, 0x07, 0xe4, 0xb7, 0x9a, 0x2b, 0xfc, - 0x84, 0x25, 0xd8, 0xb0, 0x70, 0x62, 0x7f, 0x2d, 0x55, 0xc9, 0xa2, 0x6b, 0x22, 0x82, - 0x3a, 0x21, 0xe1, 0xca, 0xf6, 0xfb, 0xc2, 0xa5, 0x7d, 0xce, 0x78, 0x4b, 0x25, 0x30, - 0x34, 0x5a, 0x5f, 0x8b, 0x0c, 0xea, 0x3f, 0xce, 0x3b, 0x7f, 0xf4, 0xf5, 0xbb, 0x88, - 0x4f, 0x68, 0xb7, 0xd1, 0x36, 0x06, 0x92, 0x33, 0xad, 0xe4, 0xd6, 0xbd, 0xda, 0xf3, - 0x40, 0xde, 0xe1, 0x43, 0x72, 0x33, 0x2e, 0xc3, 0x76, 0xf5, 0x93, 0x5d, 0x62, 0x79, - 0xc3, 0x74, 0x91, 0x1d, 0x95, 0x40, 0xfa, 0xcc, 0x75, 0x11, 0x5b, 0x20, 0xc5, 0x53, - 0x32, 0x9b, 0x43, 0xee, 0x57, 0xa8, 0xbb, 0x58, 0xa3, 0xf7, 0x46, 0x06, 0xa7, 0xf3, - 0xfa, 0x87, 0xe4, 0x6a, 0xaf, 0x72, 0xad, 0xae, 0x90, 0x48, 0xb9, 0x43, 0xe4, 0x64, - 0x89, 0x85, 0xad, 0xaa, 0x99, 0x0d, 0x78, 0x20, 0xfb, 0xb2, 0xb1, 0x24, 0x65, 0xa1, - 0x61, 0x7d, 0x01, 0xca, 0xf4, 0x14, 0x36, 0xa4, 0x94, 0x6e, 0xa0, 0x95, 0x96, 0x23, - 0x96, 0x40, 0xdc, 0x95, 0xe5, 0x86, 0x81, 0x9e, 0x6c, 0x00, 0x69, 0xee, 0xe0, 0x7a, - 0x72, 0x42, 0xb9, 0x4a, 0xfd, 0x69, 0xce, 0x35, 0x43, 0xb8, 0x87, 0x7b, 0x31, 0x94, - 0xcd, 0xb9, 0xe7, 0x07, 0xc0, 0x83, 0x8b, 0x15, 0x43, 0x46, 0x03, 0x57, 0x50, 0x46, - 0x35, 0x2c, 0x1b, 0xf4, 0xcf, 0xc2, 0x7f, 0x4e, 0xdf, 0x61, 0x91, 0xd8, 0xec, 0xf5, - 0x52, 0xb8, 0xf6, 0x98, 0x70, 0x2d, 0x3a, 0x8f, 0x6f, 0xda, 0x58, 0xb5, 0xcf, 0x16, - 0x1f, 0xed, 0x6e, 0x6f, 0xdb, 0x14, 0x9a, 0x79, 0xdb, 0x0a, 0x6b, 0x02, 0xc3, 0x27, - 0xe9, 0x62, 0x9c, 0x94, 0x8f, 0x66, 0x5d, 0x13, 0x28, 0x3f, 0x65, 0xe5, 0x4b, 0xe5, - 0x5a, 0xc1, 0xae, 0x82, 0x75, 0x35, 0xff, 0x7a, 0xc1, 0x43, 0xcc, 0x72, 0xd9, 0x2b, - 0xc4, 0xf4, 0x6e, 0xf4, 0xad, 0x88, 0xc7, 0x66, 0xab, 0x4b, 0xff, 0x1e, 0x1d, 0x11, - 0x5c, 0x85, 0x1e, 0x59, 0x85, 0x41, 0x10, 0x5d, 0x6e, 0xbb, 0x36, 0x7c, 0xe0, 0x54, - 0x93, 0x20, 0xa2, 0x30, 0x83, 0x53, 0x11, 0x47, 0x8b, 0xdd, 0x9f, 0x6c, 0x53, 0x85, - 0x03, 0xf3, 0x62, 0xe5, 0xf6, 0xc2, 0x7d, 0x15, 0xb5, 0x6c, 0x41, 0x43, 0xd4, 0x57, - 0x69, 0xc2, 0x54, 0x6e, 0x53, 0xfb, 0x45, 0x01, 0xf9, 0xba, 0x5e, 0xd4, 0x55, 0xd2, - 0x49, 0x86, 0xb4, 0xdf, 0xf7, 0xcd, - ], - ock: [ - 0xf6, 0xbd, 0x5d, 0x10, 0x80, 0xfc, 0xa6, 0x46, 0x00, 0xee, 0x92, 0x17, 0xb0, 0x9e, - 0xf1, 0x98, 0x4c, 0x9a, 0x8b, 0x98, 0xe0, 0x6e, 0xe5, 0xd8, 0x36, 0xce, 0x0e, 0x6c, - 0x89, 0xab, 0x56, 0xfd, - ], - op: [ - 0x25, 0xeb, 0x55, 0xfc, 0xcf, 0x76, 0x1f, 0xc6, 0x4e, 0x85, 0xa5, 0x88, 0xef, 0xe6, - 0xea, 0xd7, 0x83, 0x2f, 0xb1, 0xf0, 0xf7, 0xa8, 0x31, 0x65, 0x89, 0x5b, 0xdf, 0xf9, - 0x42, 0x92, 0x5f, 0x5c, 0xbd, 0xde, 0x13, 0x81, 0xec, 0x9f, 0xf4, 0x21, 0xca, 0xfd, - 0x1e, 0x31, 0xcc, 0x5d, 0xe2, 0x55, 0x59, 0x88, 0x1f, 0x6b, 0x21, 0xb2, 0x17, 0x5d, - 0x0d, 0xce, 0x94, 0x08, 0x59, 0x7e, 0xa1, 0x03, - ], - c_out: [ - 0x25, 0x4f, 0x12, 0x2c, 0xfe, 0x94, 0x98, 0xad, 0xd7, 0x57, 0xcf, 0x0b, 0x61, 0x0d, - 0xa8, 0xcb, 0xae, 0xda, 0x05, 0x3e, 0x26, 0xcb, 0x72, 0x30, 0x6f, 0x36, 0x23, 0x08, - 0x55, 0x28, 0x53, 0xff, 0x02, 0x3c, 0x23, 0xc2, 0x6f, 0x3a, 0xb4, 0x41, 0xb8, 0x1e, - 0xa2, 0x5c, 0xe0, 0xae, 0x57, 0xd1, 0xa9, 0x49, 0x83, 0xbb, 0x45, 0xab, 0x8a, 0x86, - 0xda, 0x68, 0xef, 0x63, 0xf1, 0x58, 0x16, 0xc1, 0x43, 0x32, 0x7a, 0x1e, 0x46, 0x0c, - 0x51, 0x0c, 0x63, 0x1c, 0xc6, 0x9f, 0x39, 0x60, 0xfb, 0x5a, - ], - }, - TestVector { - ovk: [ - 0x1b, 0x6e, 0x75, 0xec, 0xe3, 0xac, 0xe8, 0xdb, 0xa6, 0xa5, 0x41, 0x0d, 0x9a, 0xd4, - 0x75, 0x56, 0x68, 0xe4, 0xb3, 0x95, 0x85, 0xd6, 0x35, 0xec, 0x1d, 0xa7, 0xc8, 0xdc, - 0xfd, 0x5f, 0xc4, 0xed, - ], - ivk: [ - 0x67, 0xfa, 0x2b, 0xf7, 0xc6, 0x7d, 0x46, 0x58, 0x24, 0x3c, 0x31, 0x7c, 0x0c, 0xb4, - 0x1f, 0xd3, 0x20, 0x64, 0xdf, 0xd3, 0x70, 0x9f, 0xe0, 0xdc, 0xb7, 0x24, 0xf1, 0x4b, - 0xb0, 0x1a, 0x1d, 0x04, - ], - default_d: [ - 0xfc, 0xfb, 0x68, 0xa4, 0x0d, 0x4b, 0xc6, 0xa0, 0x4b, 0x09, 0xc4, - ], - default_pk_d: [ - 0x8b, 0x2a, 0x33, 0x7f, 0x03, 0x62, 0x2c, 0x24, 0xff, 0x38, 0x1d, 0x4c, 0x54, 0x6f, - 0x69, 0x77, 0xf9, 0x05, 0x22, 0xe9, 0x2f, 0xde, 0x44, 0xc9, 0xd1, 0xbb, 0x09, 0x97, - 0x14, 0xb9, 0xdb, 0x2b, - ], - v: 500000000, - rcm: [ - 0xe5, 0x57, 0x85, 0x13, 0x55, 0x74, 0x7c, 0x09, 0xac, 0x59, 0x01, 0x3c, 0xbd, 0xe8, - 0x59, 0x80, 0x96, 0x4e, 0xc1, 0x84, 0x4d, 0x9c, 0x69, 0x67, 0xca, 0x0c, 0x02, 0x9c, - 0x84, 0x57, 0xbb, 0x04, - ], - memo: [ - 0xf6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ], - cv: [ - 0xce, 0x42, 0xf9, 0xd0, 0x89, 0xba, 0x9d, 0x9e, 0x62, 0xe3, 0xf6, 0x56, 0x33, 0x62, - 0xf0, 0xfd, 0xc7, 0xce, 0xde, 0x8a, 0xb3, 0x59, 0x43, 0x9e, 0x21, 0x4e, 0x26, 0x52, - 0xdb, 0xf0, 0x5a, 0x0c, - ], - cmu: [ - 0xc2, 0xb5, 0xf3, 0x57, 0x11, 0x7a, 0x40, 0x03, 0x62, 0x9e, 0x05, 0xca, 0x6f, 0x56, - 0xa6, 0x23, 0xa3, 0xc4, 0x8a, 0xa5, 0xeb, 0x79, 0x7c, 0xdd, 0x32, 0x2d, 0x48, 0x57, - 0xa0, 0xfb, 0xa4, 0x4e, - ], - esk: [ - 0x3d, 0xc1, 0x66, 0xd5, 0x6a, 0x1d, 0x62, 0xf5, 0xa8, 0xd7, 0x55, 0x1d, 0xb5, 0xfd, - 0x93, 0x13, 0xe8, 0xc7, 0x20, 0x3d, 0x99, 0x6a, 0xf7, 0xd4, 0x77, 0x08, 0x37, 0x56, - 0xd5, 0x9a, 0xf8, 0x0d, - ], - epk: [ - 0x5b, 0x54, 0xe5, 0xd4, 0x13, 0xa8, 0x07, 0xdf, 0x36, 0x42, 0x6d, 0x5c, 0x8c, 0x09, - 0x81, 0x0a, 0xc2, 0x45, 0x95, 0xb1, 0x52, 0xcd, 0x89, 0x41, 0xa2, 0x34, 0x3c, 0x96, - 0x30, 0x3d, 0x24, 0x6b, - ], - shared_secret: [ - 0x40, 0x64, 0xc2, 0xb7, 0xc1, 0x82, 0xd1, 0x80, 0x52, 0x50, 0xd3, 0x59, 0xfb, 0xa1, - 0xa5, 0x32, 0x54, 0x56, 0xb0, 0x12, 0x94, 0x4d, 0x7d, 0x92, 0x9f, 0x40, 0x9c, 0x6d, - 0xe5, 0x70, 0x5d, 0xc5, - ], - k_enc: [ - 0xc5, 0xfc, 0xf8, 0x13, 0xb1, 0xbb, 0xef, 0x20, 0xa6, 0x2a, 0xce, 0x7a, 0x47, 0xf3, - 0x7f, 0x26, 0x1f, 0xbb, 0x2d, 0xfa, 0xd8, 0x88, 0x66, 0xb4, 0x32, 0xff, 0x0d, 0xfa, - 0xee, 0xc5, 0xb2, 0xcf, - ], - p_enc: [ - 0x01, 0xfc, 0xfb, 0x68, 0xa4, 0x0d, 0x4b, 0xc6, 0xa0, 0x4b, 0x09, 0xc4, 0x00, 0x65, - 0xcd, 0x1d, 0x00, 0x00, 0x00, 0x00, 0xe5, 0x57, 0x85, 0x13, 0x55, 0x74, 0x7c, 0x09, - 0xac, 0x59, 0x01, 0x3c, 0xbd, 0xe8, 0x59, 0x80, 0x96, 0x4e, 0xc1, 0x84, 0x4d, 0x9c, - 0x69, 0x67, 0xca, 0x0c, 0x02, 0x9c, 0x84, 0x57, 0xbb, 0x04, 0xf6, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - ], - c_enc: [ - 0xd7, 0xe7, 0x06, 0x31, 0x7c, 0x78, 0x95, 0x06, 0x2d, 0x89, 0xab, 0x5f, 0x10, 0x52, - 0x15, 0x5a, 0xc3, 0xd2, 0xa1, 0xe3, 0x43, 0x97, 0x3e, 0x5a, 0xab, 0x1c, 0xce, 0x53, - 0x59, 0xc6, 0xbc, 0x11, 0x1b, 0x9a, 0x7b, 0xb6, 0x68, 0xb6, 0xc7, 0xd0, 0x21, 0xb1, - 0x23, 0x35, 0x77, 0xe8, 0x2b, 0xaf, 0x33, 0x00, 0x5c, 0xd0, 0x34, 0xa9, 0x75, 0x4b, - 0x1e, 0x12, 0xdf, 0x03, 0x6b, 0x7b, 0xc7, 0x82, 0x98, 0x79, 0xca, 0x8c, 0x6b, 0x54, - 0x37, 0x8f, 0xcd, 0x5f, 0x18, 0x2f, 0x65, 0x16, 0x0e, 0xa7, 0x24, 0x3b, 0x7d, 0xfc, - 0xac, 0xfb, 0x6d, 0xac, 0xee, 0x02, 0x26, 0x34, 0x14, 0x9d, 0x8f, 0xb2, 0xf0, 0xca, - 0x51, 0xa8, 0x26, 0x72, 0xa5, 0x63, 0xd5, 0x36, 0xba, 0xf1, 0xaf, 0x88, 0x1a, 0x7a, - 0x8d, 0x25, 0xc5, 0xcf, 0x78, 0x61, 0x89, 0x53, 0x03, 0x2e, 0xf5, 0x65, 0xb0, 0xf3, - 0x98, 0xe3, 0x4b, 0xee, 0x2c, 0x30, 0x95, 0xa7, 0xbd, 0x0b, 0x7d, 0x09, 0x7a, 0x3d, - 0x26, 0x4d, 0x65, 0x46, 0xd0, 0x0c, 0x85, 0x83, 0x04, 0x43, 0x78, 0xd1, 0x48, 0x94, - 0x04, 0xa3, 0x1e, 0xec, 0xa8, 0x8f, 0x8f, 0x42, 0xeb, 0xfb, 0x82, 0x18, 0xd4, 0x9f, - 0xde, 0xd8, 0x2a, 0x9b, 0xa6, 0x23, 0x2c, 0xcc, 0x47, 0x94, 0x5d, 0x6f, 0x7d, 0x6e, - 0x39, 0xe0, 0xe8, 0x39, 0x29, 0x34, 0x1a, 0xcf, 0x88, 0xdb, 0x5a, 0x27, 0x73, 0xdc, - 0x55, 0x8a, 0x9d, 0xc1, 0x1d, 0xcd, 0xa1, 0xba, 0xb3, 0xcb, 0x21, 0xbf, 0x5c, 0x29, - 0x51, 0x83, 0xbf, 0x9a, 0x93, 0xee, 0x02, 0x5e, 0xb4, 0x60, 0xf7, 0xd7, 0x41, 0x20, - 0x42, 0xce, 0x5a, 0x84, 0x3a, 0x79, 0x0c, 0x3a, 0x94, 0xda, 0x2d, 0xb7, 0xf6, 0x12, - 0x03, 0x2f, 0xbf, 0x56, 0x4e, 0xfc, 0xf2, 0x04, 0xaf, 0xed, 0x0f, 0xf2, 0xab, 0x2b, - 0xc1, 0xb3, 0x77, 0xca, 0x41, 0x0f, 0x12, 0x7f, 0xaf, 0x98, 0x76, 0x62, 0x7f, 0xbd, - 0xb2, 0x26, 0x2a, 0xe6, 0x56, 0x23, 0x08, 0x84, 0x48, 0x00, 0xb5, 0xcd, 0x52, 0x74, - 0x3e, 0x7f, 0x7b, 0xca, 0xe3, 0xc7, 0xb2, 0x70, 0x34, 0xc5, 0xf2, 0x1d, 0x4f, 0xef, - 0xb5, 0x9b, 0xd2, 0x3b, 0xc6, 0xea, 0x0c, 0x39, 0x39, 0x87, 0x1a, 0xb4, 0x34, 0xb3, - 0xa5, 0xcb, 0x71, 0x03, 0x85, 0x1a, 0x24, 0x78, 0xc5, 0xf6, 0x13, 0x8f, 0x8f, 0xd9, - 0x91, 0x3f, 0xa7, 0xaf, 0x5a, 0x4a, 0xa2, 0x0e, 0xf9, 0x59, 0x40, 0x84, 0x0b, 0xcd, - 0x17, 0x4c, 0xa3, 0xe1, 0x06, 0x5a, 0xea, 0xee, 0x5f, 0x6c, 0x7d, 0x94, 0x34, 0x2c, - 0x68, 0x5f, 0x13, 0xa8, 0x1e, 0x7b, 0x53, 0xad, 0x42, 0x89, 0x0b, 0xa8, 0x10, 0x3a, - 0xc8, 0x34, 0xa4, 0xeb, 0x1f, 0x10, 0xb0, 0xa7, 0x0e, 0x76, 0x89, 0x1d, 0xbe, 0x18, - 0xf5, 0x80, 0x47, 0x2f, 0x5b, 0xdc, 0x3f, 0xc9, 0x55, 0x0f, 0x15, 0x6b, 0x31, 0x21, - 0xa8, 0x44, 0xd6, 0xc7, 0x7b, 0x22, 0x4b, 0x8d, 0x04, 0xf1, 0xfe, 0x8e, 0xa7, 0xb9, - 0x88, 0xd8, 0x78, 0xbf, 0xc0, 0x6d, 0xac, 0x33, 0x2a, 0x10, 0x6a, 0x6e, 0xad, 0x47, - 0xf8, 0x2b, 0xd8, 0xcb, 0x7c, 0x25, 0xae, 0x9e, 0x1d, 0x75, 0xbb, 0x76, 0x2a, 0xfe, - 0xe3, 0x49, 0x30, 0xf4, 0xa9, 0x98, 0xf2, 0x68, 0xd8, 0x76, 0x3c, 0xae, 0x7b, 0x32, - 0x15, 0x20, 0x5e, 0x58, 0x9c, 0x48, 0x11, 0x13, 0xb5, 0xa4, 0xcd, 0xb2, 0x09, 0xbe, - 0xce, 0x2f, 0x09, 0x4f, 0x33, 0x9f, 0x03, 0xfb, 0x39, 0xa1, 0x6e, 0xf1, 0x67, 0x2e, - 0x00, 0x89, 0x27, 0xfd, 0x97, 0x09, 0x8e, 0x00, 0x12, 0xbe, 0xca, 0xa0, 0x0f, 0x62, - 0xc6, 0xbf, 0xd9, 0x45, 0xa0, 0x16, 0xbe, 0x8b, 0x18, 0x66, 0xd9, 0x2b, 0x1d, 0x85, - 0x88, 0xae, 0x26, 0xc6, 0x35, 0x70, 0xd7, 0xe2, 0xa6, 0xb2, 0xee, 0x6e, 0xc2, 0xe6, - 0xb0, 0xbe, 0x22, 0x19, 0x38, 0x0e, 0x4e, 0xea, 0x6a, 0xf0, 0x9b, 0xf5, 0x85, 0xf2, - 0x85, 0x38, 0xd8, 0xb7, 0x89, 0x32, 0x6e, 0x6a, 0x3d, 0xe3, 0xbf, 0x45, 0x06, 0x80, - 0x28, 0xac, 0x80, 0xb1, 0x92, 0x25, 0x5f, 0x27, 0x33, 0x64, 0xda, 0x88, 0xdc, 0x1a, - 0x6f, 0x00, 0xe0, 0xcc, 0x32, 0xbb, 0x47, 0x5e, 0xcc, 0xbe, 0x09, 0x7a, 0x69, 0xf6, - 0x49, 0x2b, 0xdb, 0xa2, 0xad, 0xf0, - ], - ock: [ - 0xf9, 0x8d, 0x6e, 0x55, 0xff, 0x78, 0x3a, 0x13, 0x13, 0x14, 0x0f, 0xb8, 0x8b, 0x7f, - 0x3a, 0x4d, 0xb2, 0x81, 0x86, 0x37, 0x86, 0x88, 0xbe, 0xc6, 0x19, 0x56, 0x23, 0x2e, - 0x42, 0xb7, 0x0a, 0xba, - ], - op: [ - 0x8b, 0x2a, 0x33, 0x7f, 0x03, 0x62, 0x2c, 0x24, 0xff, 0x38, 0x1d, 0x4c, 0x54, 0x6f, - 0x69, 0x77, 0xf9, 0x05, 0x22, 0xe9, 0x2f, 0xde, 0x44, 0xc9, 0xd1, 0xbb, 0x09, 0x97, - 0x14, 0xb9, 0xdb, 0x2b, 0x3d, 0xc1, 0x66, 0xd5, 0x6a, 0x1d, 0x62, 0xf5, 0xa8, 0xd7, - 0x55, 0x1d, 0xb5, 0xfd, 0x93, 0x13, 0xe8, 0xc7, 0x20, 0x3d, 0x99, 0x6a, 0xf7, 0xd4, - 0x77, 0x08, 0x37, 0x56, 0xd5, 0x9a, 0xf8, 0x0d, - ], - c_out: [ - 0x3b, 0xfc, 0x13, 0x67, 0x3c, 0x24, 0xac, 0x5e, 0xaf, 0x0b, 0xc2, 0x44, 0x6c, 0x38, - 0xa7, 0x92, 0xae, 0x42, 0xd9, 0x6b, 0xaf, 0x05, 0x53, 0xce, 0xe4, 0x36, 0xb6, 0x34, - 0xb5, 0x73, 0x89, 0xb3, 0x62, 0x1d, 0xdb, 0xba, 0x22, 0xe6, 0x84, 0x89, 0x0a, 0x7b, - 0x64, 0x5d, 0x63, 0xc4, 0xbc, 0x8c, 0x26, 0xdb, 0x54, 0x62, 0x8c, 0xef, 0x4d, 0xed, - 0x98, 0x0f, 0x60, 0x8f, 0x00, 0x20, 0xbb, 0xb5, 0xa2, 0xf6, 0x55, 0x22, 0xa6, 0x1f, - 0x89, 0xdf, 0x82, 0x18, 0x18, 0x67, 0x04, 0x01, 0x1e, 0x91, - ], - }, - TestVector { - ovk: [ - 0xc6, 0xbc, 0x1f, 0x39, 0xf0, 0xd7, 0x86, 0x31, 0x4c, 0xb2, 0x0b, 0xf9, 0xab, 0x22, - 0x85, 0x40, 0x91, 0x35, 0x55, 0xf9, 0x70, 0x69, 0x6b, 0x6d, 0x7c, 0x77, 0xbb, 0x33, - 0x23, 0x28, 0x37, 0x2a, - ], - ivk: [ - 0xea, 0x3f, 0x1d, 0x80, 0xe4, 0x30, 0x7c, 0xa7, 0x3b, 0x9f, 0x37, 0x80, 0x1f, 0x91, - 0xfb, 0xa8, 0x10, 0xcc, 0x41, 0xd2, 0x79, 0xfc, 0x29, 0xf5, 0x64, 0x23, 0x56, 0x54, - 0xa2, 0x17, 0x8e, 0x03, - ], - default_d: [ - 0xeb, 0x51, 0x98, 0x82, 0xad, 0x1e, 0x5c, 0xc6, 0x54, 0xcd, 0x59, - ], - default_pk_d: [ - 0x6b, 0x27, 0xda, 0xcc, 0xb5, 0xa8, 0x20, 0x7f, 0x53, 0x2d, 0x10, 0xca, 0x23, 0x8f, - 0x97, 0x86, 0x64, 0x8a, 0x11, 0xb5, 0x96, 0x6e, 0x51, 0xa2, 0xf7, 0xd8, 0x9e, 0x15, - 0xd2, 0x9b, 0x8f, 0xdf, - ], - v: 600000000, - rcm: [ - 0x68, 0xf0, 0x61, 0x04, 0x60, 0x6b, 0x0c, 0x54, 0x49, 0x84, 0x5f, 0xf4, 0xc6, 0x5f, - 0x73, 0xe9, 0x0f, 0x45, 0xef, 0x5a, 0x43, 0xc9, 0xd7, 0x4c, 0xb2, 0xc8, 0x5c, 0xf5, - 0x6c, 0x94, 0xc0, 0x02, - ], - memo: [ - 0xf6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ], - cv: [ - 0x30, 0x27, 0xd7, 0xb7, 0x47, 0x64, 0xca, 0xf7, 0x2b, 0x73, 0x87, 0x28, 0x9b, 0x12, - 0x8f, 0x43, 0x9f, 0xd0, 0x42, 0xc2, 0x1d, 0x81, 0x36, 0x4b, 0xc2, 0xae, 0x7b, 0xd2, - 0x9e, 0xab, 0x51, 0x23, - ], - cmu: [ - 0x38, 0x2c, 0x7d, 0x68, 0x8b, 0xdf, 0x34, 0xb9, 0x4d, 0x40, 0x1c, 0x41, 0x22, 0x79, - 0x52, 0xa2, 0xb9, 0x31, 0xc5, 0x7b, 0x00, 0x5c, 0x82, 0xf2, 0xc3, 0x63, 0x15, 0xf6, - 0x1c, 0x35, 0x02, 0x4e, - ], - esk: [ - 0x4e, 0x41, 0x8c, 0x3c, 0x54, 0x3d, 0x6b, 0xf0, 0x15, 0x31, 0x74, 0xa0, 0x4e, 0x85, - 0x44, 0xae, 0x7c, 0x58, 0x09, 0x2a, 0x2e, 0x4e, 0x5d, 0x7d, 0x9c, 0x67, 0x2a, 0x3a, - 0x79, 0x11, 0x09, 0x03, - ], - epk: [ - 0xe0, 0xc2, 0x9b, 0x43, 0x5d, 0xae, 0xdb, 0xc9, 0x8d, 0x46, 0x5f, 0x38, 0x9b, 0x1b, - 0x60, 0xd7, 0xdf, 0xac, 0x0e, 0x45, 0x9b, 0x1e, 0x62, 0x8f, 0xa0, 0x18, 0x4e, 0x92, - 0xf2, 0x64, 0x79, 0xca, - ], - shared_secret: [ - 0x34, 0xdd, 0x16, 0x13, 0xa8, 0x57, 0x75, 0x2a, 0xa9, 0x07, 0x26, 0xff, 0xf0, 0x7d, - 0x42, 0x9d, 0xcb, 0x52, 0xd2, 0xca, 0x27, 0x7d, 0x84, 0xeb, 0x7a, 0x12, 0xfa, 0x9a, - 0xfc, 0x99, 0xa7, 0x35, - ], - k_enc: [ - 0x03, 0x25, 0xb3, 0x12, 0x63, 0x58, 0x57, 0x3c, 0x09, 0x90, 0xa3, 0x62, 0xb8, 0xf2, - 0x7c, 0xd0, 0x0c, 0xe0, 0xdc, 0x4b, 0x4d, 0x00, 0xcc, 0x8d, 0x8d, 0x3b, 0xa2, 0xce, - 0x6e, 0xa9, 0xc2, 0x97, - ], - p_enc: [ - 0x01, 0xeb, 0x51, 0x98, 0x82, 0xad, 0x1e, 0x5c, 0xc6, 0x54, 0xcd, 0x59, 0x00, 0x46, - 0xc3, 0x23, 0x00, 0x00, 0x00, 0x00, 0x68, 0xf0, 0x61, 0x04, 0x60, 0x6b, 0x0c, 0x54, - 0x49, 0x84, 0x5f, 0xf4, 0xc6, 0x5f, 0x73, 0xe9, 0x0f, 0x45, 0xef, 0x5a, 0x43, 0xc9, - 0xd7, 0x4c, 0xb2, 0xc8, 0x5c, 0xf5, 0x6c, 0x94, 0xc0, 0x02, 0xf6, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - ], - c_enc: [ - 0x3f, 0x92, 0x6f, 0x4c, 0x93, 0xff, 0x12, 0x5b, 0xd1, 0xfa, 0x04, 0xc9, 0x1e, 0xf5, - 0x9e, 0x07, 0x14, 0x33, 0xf5, 0x7c, 0x60, 0x6e, 0xe1, 0xbc, 0x91, 0x2d, 0x54, 0x62, - 0x8d, 0x14, 0x07, 0x40, 0xa1, 0xab, 0x8a, 0x34, 0x51, 0x6a, 0xde, 0xfb, 0xe6, 0x48, - 0x00, 0x7f, 0x86, 0xf1, 0x31, 0xf4, 0x99, 0x3b, 0x99, 0xae, 0xbd, 0x18, 0x99, 0x63, - 0x48, 0xf4, 0xec, 0x85, 0x34, 0x1d, 0xf3, 0x35, 0x42, 0x2b, 0x12, 0x61, 0x8f, 0x63, - 0xaa, 0x80, 0x4b, 0x30, 0x6c, 0x6c, 0x6b, 0x23, 0x7a, 0x0c, 0x04, 0x4f, 0x79, 0x03, - 0x3d, 0x02, 0x8d, 0x13, 0xcf, 0x1f, 0x3d, 0x6e, 0x38, 0xac, 0xf3, 0x90, 0xf5, 0x54, - 0xa8, 0xd4, 0xe4, 0x64, 0x94, 0x8f, 0xb5, 0xa7, 0xf9, 0x8d, 0x16, 0x1e, 0x3a, 0x8a, - 0x15, 0x7a, 0xf4, 0xc8, 0x94, 0xca, 0x2d, 0xa4, 0x64, 0x7c, 0x53, 0x22, 0x35, 0x4f, - 0x26, 0x19, 0xfd, 0x6c, 0xcc, 0x3c, 0xab, 0xef, 0x03, 0x71, 0xba, 0x42, 0x2f, 0x3d, - 0x6d, 0x92, 0x16, 0x99, 0x6e, 0x49, 0xe6, 0x93, 0x87, 0x1c, 0x56, 0x3f, 0xfb, 0xf4, - 0xc6, 0xd1, 0xd1, 0xc4, 0x73, 0x9f, 0x73, 0x26, 0xda, 0x4c, 0x66, 0x97, 0x61, 0x84, - 0xf0, 0x13, 0x64, 0x96, 0x71, 0x2a, 0x7e, 0xed, 0x56, 0xea, 0x4c, 0xa1, 0xd0, 0x78, - 0x4c, 0x7f, 0xa2, 0xc5, 0x56, 0xd6, 0xa9, 0x64, 0x0b, 0x55, 0x45, 0xd2, 0x14, 0x0a, - 0xd7, 0x45, 0xf1, 0xfc, 0xda, 0xb6, 0xb1, 0xf9, 0xee, 0x59, 0x35, 0x6b, 0xed, 0x24, - 0x93, 0x38, 0xa5, 0xc6, 0xc1, 0xc6, 0x37, 0xea, 0x9b, 0x77, 0x9b, 0x83, 0x11, 0xa5, - 0x32, 0x3a, 0x15, 0xd6, 0x1f, 0x1a, 0x0f, 0xfc, 0x7b, 0x2f, 0xc9, 0xe0, 0xbe, 0x58, - 0xc5, 0xfc, 0xbd, 0xbe, 0x57, 0xa2, 0xe4, 0xd3, 0xbf, 0x21, 0x84, 0x5b, 0x90, 0x16, - 0x54, 0x1c, 0x8c, 0xb4, 0x4a, 0x59, 0xec, 0xa7, 0xf2, 0xb4, 0x18, 0x3b, 0xfb, 0xbc, - 0xda, 0x57, 0xeb, 0x54, 0x24, 0xe8, 0x9d, 0xc3, 0xb0, 0x67, 0x14, 0xe2, 0x0e, 0xdf, - 0x78, 0x46, 0xd6, 0x8a, 0x5f, 0x8a, 0x18, 0x4a, 0x7f, 0x7c, 0x5a, 0x08, 0xfc, 0xcc, - 0x79, 0x84, 0x12, 0x2e, 0x8c, 0x63, 0x63, 0x03, 0xd0, 0x3b, 0x52, 0xb5, 0x1e, 0xc8, - 0xcd, 0x97, 0x68, 0x88, 0x97, 0x6a, 0xc5, 0x9f, 0xe4, 0xeb, 0xda, 0x53, 0x95, 0x53, - 0x8d, 0xbe, 0xa3, 0xd0, 0x09, 0x7b, 0xe5, 0x54, 0x6e, 0x1e, 0x0a, 0xb1, 0xba, 0x4c, - 0xbb, 0x47, 0xf6, 0x20, 0x3d, 0xca, 0xb8, 0x4b, 0x12, 0x9c, 0x52, 0x99, 0xe3, 0xe9, - 0x9d, 0x65, 0xeb, 0xcb, 0xe4, 0x0f, 0xd0, 0x5b, 0x87, 0x36, 0x9c, 0x30, 0xdb, 0x29, - 0x38, 0x37, 0xdb, 0xd0, 0x4e, 0x7a, 0x71, 0x08, 0xab, 0x74, 0x4b, 0x4f, 0xb3, 0xda, - 0x1f, 0x8a, 0x7d, 0x2c, 0xba, 0x6a, 0x5f, 0x01, 0x4f, 0x0d, 0x70, 0x5e, 0xce, 0x11, - 0x9a, 0xe9, 0x80, 0xe9, 0x99, 0x3d, 0xa3, 0xdd, 0xaa, 0x3b, 0xf1, 0x89, 0x9a, 0x74, - 0x74, 0xd6, 0x0b, 0x72, 0xed, 0x1e, 0x39, 0x0d, 0xfe, 0x4a, 0x3a, 0x07, 0x1a, 0xce, - 0xfb, 0x02, 0xcc, 0xca, 0x0b, 0xa9, 0x39, 0x8c, 0x86, 0x1b, 0xed, 0x45, 0x21, 0x61, - 0x79, 0xee, 0x2a, 0x08, 0x53, 0x36, 0x1c, 0x7d, 0xea, 0x89, 0xac, 0x1c, 0xd7, 0xe2, - 0xb4, 0xef, 0xa6, 0xad, 0x82, 0x15, 0xf5, 0xf7, 0x6a, 0xc2, 0x8a, 0x73, 0x1d, 0x27, - 0x79, 0xc1, 0xff, 0xeb, 0xe9, 0xab, 0x6f, 0x51, 0x3d, 0x9b, 0x5e, 0xe0, 0x08, 0x13, - 0x5f, 0xf6, 0x0b, 0xb8, 0x6f, 0x8e, 0x13, 0x97, 0x87, 0xc6, 0xc3, 0x46, 0x8d, 0x31, - 0x29, 0x8f, 0x25, 0x91, 0x76, 0x48, 0xf0, 0x72, 0xa1, 0x1c, 0x0b, 0x8a, 0xf4, 0x0f, - 0x92, 0xa8, 0xb5, 0x04, 0x2c, 0xd4, 0xaf, 0x4f, 0x5a, 0x2a, 0x55, 0x27, 0x31, 0x54, - 0x61, 0x90, 0x44, 0x8d, 0xf1, 0x07, 0x86, 0x37, 0xf4, 0x2e, 0x97, 0x54, 0x5a, 0x86, - 0x64, 0x3a, 0xa4, 0x10, 0x37, 0xc5, 0x34, 0xbc, 0x3e, 0x2e, 0x44, 0xa8, 0x85, 0x34, - 0x10, 0xa0, 0x6e, 0x91, 0x25, 0x31, 0x8a, 0x96, 0x56, 0x55, 0xf3, 0x3f, 0xed, 0x8e, - 0xba, 0x35, 0x62, 0x93, 0xd7, 0xcc, 0xfb, 0x97, 0xa2, 0x33, 0x20, 0xbc, 0x35, 0x39, - 0x70, 0xaa, 0xa1, 0x18, 0xe7, 0x43, - ], - ock: [ - 0x95, 0x9a, 0x28, 0x02, 0x17, 0xb9, 0xef, 0x54, 0xab, 0x44, 0x3b, 0x8d, 0x0f, 0xea, - 0x5a, 0x11, 0x75, 0x86, 0xae, 0x8a, 0xdd, 0x64, 0x99, 0x7d, 0x02, 0xec, 0xb8, 0xb5, - 0xcb, 0xac, 0x14, 0x87, - ], - op: [ - 0x6b, 0x27, 0xda, 0xcc, 0xb5, 0xa8, 0x20, 0x7f, 0x53, 0x2d, 0x10, 0xca, 0x23, 0x8f, - 0x97, 0x86, 0x64, 0x8a, 0x11, 0xb5, 0x96, 0x6e, 0x51, 0xa2, 0xf7, 0xd8, 0x9e, 0x15, - 0xd2, 0x9b, 0x8f, 0xdf, 0x4e, 0x41, 0x8c, 0x3c, 0x54, 0x3d, 0x6b, 0xf0, 0x15, 0x31, - 0x74, 0xa0, 0x4e, 0x85, 0x44, 0xae, 0x7c, 0x58, 0x09, 0x2a, 0x2e, 0x4e, 0x5d, 0x7d, - 0x9c, 0x67, 0x2a, 0x3a, 0x79, 0x11, 0x09, 0x03, - ], - c_out: [ - 0x65, 0x9d, 0xef, 0x25, 0x08, 0x34, 0x84, 0x6f, 0x85, 0xeb, 0x9e, 0x39, 0x5b, 0xef, - 0xe1, 0x5e, 0x1d, 0x4d, 0x2a, 0xb4, 0x36, 0x2d, 0x1a, 0xa7, 0xde, 0x84, 0x24, 0x3f, - 0x74, 0x45, 0xd5, 0xd2, 0x8f, 0x47, 0x92, 0x92, 0x4d, 0x60, 0xc7, 0x60, 0x53, 0x3c, - 0xef, 0x05, 0x10, 0x47, 0xe5, 0x4d, 0x52, 0x1e, 0x2b, 0x07, 0x2d, 0x13, 0x30, 0xb2, - 0x68, 0x5e, 0xb8, 0x70, 0x10, 0x6c, 0x66, 0x1f, 0x1f, 0x07, 0xb7, 0x6f, 0xdb, 0xb5, - 0x14, 0xaa, 0x9b, 0x94, 0xad, 0x41, 0x91, 0xbc, 0x0d, 0x2d, - ], - }, - TestVector { - ovk: [ - 0xf6, 0x2c, 0x05, 0xe8, 0x48, 0xa8, 0x73, 0xef, 0x88, 0x5e, 0x12, 0xb0, 0x8c, 0x5e, - 0x7c, 0xa2, 0xf3, 0x24, 0x24, 0xba, 0xcc, 0x75, 0x4c, 0xb6, 0x97, 0x50, 0x44, 0x4d, - 0x35, 0x5f, 0x51, 0x06, - ], - ivk: [ - 0xb5, 0xc5, 0x89, 0x49, 0x43, 0x95, 0x69, 0x33, 0xc0, 0xe5, 0xc1, 0x2d, 0x31, 0x1f, - 0xc1, 0x2c, 0xba, 0x58, 0x35, 0x4b, 0x5c, 0x38, 0x9e, 0xdc, 0x03, 0xda, 0x55, 0x08, - 0x4f, 0x74, 0xc2, 0x05, - ], - default_d: [ - 0xbe, 0xbb, 0x0f, 0xb4, 0x6b, 0x8a, 0xaf, 0xf8, 0x90, 0x40, 0xf6, - ], - default_pk_d: [ - 0xd1, 0x1d, 0xa0, 0x1f, 0x0b, 0x43, 0xbd, 0xd5, 0x28, 0x8d, 0x32, 0x38, 0x5b, 0x87, - 0x71, 0xd2, 0x23, 0x49, 0x3c, 0x69, 0x80, 0x25, 0x44, 0x04, 0x3f, 0x77, 0xcf, 0x1d, - 0x71, 0xc1, 0xcb, 0x8c, - ], - v: 700000000, - rcm: [ - 0x49, 0xf9, 0x0b, 0x47, 0xfd, 0x52, 0xfe, 0xe7, 0xc1, 0xc8, 0x1f, 0x0d, 0xcb, 0x5b, - 0x74, 0xc3, 0xfb, 0x9b, 0x3e, 0x03, 0x97, 0x6f, 0x8b, 0x75, 0x24, 0xea, 0xba, 0xd0, - 0x08, 0x89, 0x21, 0x07, - ], - memo: [ - 0xf6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ], - cv: [ - 0x77, 0x08, 0x94, 0xc7, 0xa5, 0x45, 0x8b, 0x16, 0x7d, 0x85, 0x18, 0xa5, 0x47, 0xbc, - 0x62, 0xb4, 0x6b, 0xa1, 0x89, 0x80, 0x7e, 0xb9, 0x7c, 0x08, 0x28, 0x4e, 0x1b, 0x92, - 0xb6, 0xda, 0x35, 0x2a, - ], - cmu: [ - 0x0d, 0xd4, 0x2d, 0x63, 0xff, 0x38, 0xee, 0x4c, 0x46, 0x65, 0x1e, 0x4d, 0x1d, 0xd5, - 0x22, 0x7d, 0xc5, 0x97, 0x33, 0x9f, 0x7d, 0x70, 0x4c, 0x51, 0x8e, 0xf4, 0x02, 0xf8, - 0xcd, 0x6f, 0x37, 0x44, - ], - esk: [ - 0x6d, 0xa9, 0x45, 0xd3, 0x03, 0x81, 0xc2, 0xee, 0xd2, 0xb8, 0x1d, 0x27, 0x08, 0x6d, - 0x22, 0x48, 0xe7, 0xc4, 0x49, 0xfe, 0x50, 0x9b, 0x38, 0xe2, 0x76, 0x79, 0x11, 0x89, - 0xea, 0xbc, 0x46, 0x02, - ], - epk: [ - 0xa5, 0x2f, 0x0b, 0x5a, 0xe4, 0xa9, 0x4f, 0xa8, 0x8a, 0xa7, 0xcb, 0x7e, 0x5f, 0x0f, - 0x34, 0x3c, 0xa2, 0xfa, 0x66, 0xb3, 0x94, 0x41, 0xba, 0x66, 0x28, 0x20, 0xe4, 0x6a, - 0x9b, 0xbb, 0xa3, 0xb5, - ], - shared_secret: [ - 0x81, 0xc7, 0xc5, 0xd5, 0xff, 0x63, 0xe9, 0xe6, 0x1f, 0xe3, 0x5a, 0x4b, 0x39, 0x6e, - 0xa7, 0xf1, 0x9e, 0x48, 0x07, 0x6f, 0x22, 0x09, 0x0a, 0xe7, 0x29, 0xa4, 0x11, 0x79, - 0x2f, 0x08, 0x58, 0x4a, - ], - k_enc: [ - 0xb4, 0xf9, 0xa7, 0xff, 0x9c, 0x60, 0x80, 0x6e, 0xc7, 0xf5, 0x5c, 0xee, 0xbe, 0xc2, - 0xba, 0x54, 0x76, 0x19, 0x8e, 0x29, 0x1d, 0xf7, 0x57, 0x8c, 0x2b, 0xef, 0x87, 0xe6, - 0x4a, 0x71, 0x6a, 0xe7, - ], - p_enc: [ - 0x01, 0xbe, 0xbb, 0x0f, 0xb4, 0x6b, 0x8a, 0xaf, 0xf8, 0x90, 0x40, 0xf6, 0x00, 0x27, - 0xb9, 0x29, 0x00, 0x00, 0x00, 0x00, 0x49, 0xf9, 0x0b, 0x47, 0xfd, 0x52, 0xfe, 0xe7, - 0xc1, 0xc8, 0x1f, 0x0d, 0xcb, 0x5b, 0x74, 0xc3, 0xfb, 0x9b, 0x3e, 0x03, 0x97, 0x6f, - 0x8b, 0x75, 0x24, 0xea, 0xba, 0xd0, 0x08, 0x89, 0x21, 0x07, 0xf6, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - ], - c_enc: [ - 0x20, 0x77, 0xdf, 0x43, 0x25, 0x24, 0x61, 0x4c, 0x07, 0x6e, 0x77, 0x93, 0x02, 0x41, - 0x91, 0xaa, 0xc9, 0xe4, 0x93, 0xf5, 0xc8, 0xa9, 0x87, 0x45, 0xae, 0x65, 0x31, 0x0c, - 0xfc, 0xb5, 0x75, 0x56, 0x4a, 0x93, 0xf1, 0x27, 0x2b, 0xce, 0x90, 0x07, 0x77, 0xb8, - 0x50, 0x49, 0x7e, 0x84, 0x54, 0x0c, 0xb1, 0x92, 0x03, 0x85, 0x65, 0x88, 0x2f, 0xa4, - 0xf3, 0x71, 0x21, 0x3e, 0xb5, 0x09, 0x00, 0x41, 0xff, 0xd9, 0x24, 0x7b, 0xee, 0x2b, - 0xb1, 0x53, 0x21, 0x22, 0x83, 0xb2, 0x7e, 0x36, 0xe2, 0x84, 0x60, 0x3c, 0x0b, 0xc4, - 0x0c, 0x46, 0x5f, 0xc6, 0xab, 0x8f, 0x88, 0x98, 0x5e, 0xf5, 0x0e, 0x2a, 0xb0, 0xeb, - 0x66, 0xa6, 0x34, 0x30, 0x9b, 0xb9, 0x02, 0xc6, 0xcd, 0xd6, 0xa5, 0x55, 0xb8, 0xc3, - 0x71, 0x48, 0x9f, 0x57, 0xc7, 0xea, 0x3b, 0x54, 0x37, 0xf2, 0x87, 0xc7, 0x4e, 0x35, - 0xe0, 0x34, 0xcc, 0x68, 0x08, 0xe2, 0xc9, 0xf2, 0xc9, 0x73, 0xfa, 0xc9, 0x6e, 0x84, - 0x9d, 0x31, 0xde, 0x76, 0xf8, 0x06, 0x63, 0xa5, 0x82, 0xb2, 0x3a, 0xfc, 0x36, 0x45, - 0x5e, 0xc4, 0x6e, 0x23, 0x8c, 0xb2, 0x84, 0xda, 0xf1, 0x11, 0x4a, 0x6e, 0x5b, 0xd0, - 0x28, 0x9a, 0xef, 0xb7, 0x46, 0x94, 0x31, 0xb8, 0xb8, 0x60, 0x89, 0xb9, 0xd3, 0x6f, - 0xfd, 0x67, 0x45, 0xbd, 0x86, 0x7b, 0xaa, 0x6b, 0x58, 0xfb, 0x30, 0xaf, 0xa0, 0x97, - 0xab, 0x9e, 0x57, 0x38, 0x8f, 0x4f, 0xdf, 0xc0, 0xfd, 0x48, 0x3d, 0xc6, 0x7f, 0x02, - 0xbc, 0x07, 0x99, 0x0e, 0x1a, 0x39, 0x7b, 0x11, 0x2d, 0x5d, 0xbc, 0xf2, 0x2f, 0x9b, - 0x64, 0xf5, 0xf5, 0x43, 0x10, 0x24, 0x63, 0xe3, 0x0f, 0x46, 0x81, 0x72, 0x85, 0x39, - 0xc0, 0xc5, 0xc5, 0xe0, 0x0a, 0x25, 0x35, 0xae, 0xf7, 0x68, 0xe3, 0xaf, 0x7d, 0x47, - 0xa0, 0x8d, 0xdb, 0x99, 0xea, 0x2e, 0xd0, 0x0c, 0x52, 0xbf, 0x4b, 0x5e, 0xb3, 0x14, - 0x05, 0x85, 0xb0, 0xf9, 0x0e, 0xcf, 0x7d, 0x21, 0x5b, 0x4c, 0xc1, 0x8a, 0xf9, 0xae, - 0xc8, 0x17, 0x0c, 0x6d, 0xb6, 0xc6, 0x69, 0x98, 0xb8, 0xda, 0x0f, 0x09, 0x17, 0xf1, - 0x38, 0x0c, 0x87, 0xa4, 0x18, 0x1b, 0x86, 0xc6, 0xcd, 0xfe, 0x6f, 0x2d, 0xb2, 0x21, - 0x41, 0xe7, 0x98, 0x4b, 0x1a, 0xac, 0xf7, 0xce, 0xc5, 0xe7, 0xd0, 0x76, 0xaa, 0xc5, - 0x47, 0x9e, 0xd7, 0x14, 0x40, 0xb2, 0xd4, 0x60, 0x18, 0x5b, 0xa3, 0xdb, 0xea, 0x03, - 0xc8, 0xfc, 0xca, 0xc0, 0x9a, 0xec, 0xd3, 0x3a, 0x3f, 0xdd, 0xa9, 0xa1, 0x34, 0xea, - 0x42, 0xa1, 0xa9, 0x78, 0xc4, 0x05, 0x17, 0x99, 0xe6, 0xcc, 0x69, 0x6f, 0x8a, 0x49, - 0x40, 0x0a, 0xea, 0xd6, 0x65, 0x2f, 0x93, 0xa2, 0x58, 0x22, 0x0c, 0x63, 0x38, 0xb9, - 0xe7, 0x3b, 0x10, 0xa0, 0x1c, 0xd2, 0xec, 0x39, 0x72, 0x86, 0x1c, 0x7b, 0x62, 0x69, - 0x5a, 0xda, 0xa5, 0x41, 0x4a, 0x78, 0x74, 0x50, 0xe7, 0xa5, 0xf8, 0x21, 0xe4, 0xf2, - 0x45, 0xdd, 0x97, 0x2c, 0x08, 0x92, 0xe8, 0x6f, 0xa1, 0x26, 0xba, 0x59, 0x5c, 0x12, - 0x25, 0x73, 0x8e, 0x2f, 0x8b, 0xe3, 0x6f, 0x11, 0xdc, 0xc5, 0x2c, 0xed, 0x4f, 0x78, - 0x75, 0xdf, 0x5b, 0xbb, 0xd8, 0x3a, 0xec, 0x8d, 0x43, 0x13, 0x07, 0x2d, 0x7e, 0xc9, - 0x47, 0xaf, 0x86, 0xb5, 0x6b, 0x65, 0xfc, 0xb1, 0xbd, 0x32, 0xf0, 0xdb, 0x0c, 0xb3, - 0x7d, 0xea, 0xa6, 0xcd, 0xe0, 0xdf, 0xe4, 0xbd, 0xb8, 0x09, 0x16, 0x1e, 0xda, 0x03, - 0x4a, 0x94, 0x9a, 0x3a, 0x03, 0x9a, 0xf9, 0xbb, 0xe0, 0x9e, 0xaf, 0xb3, 0x5b, 0x7c, - 0xd8, 0xb5, 0x32, 0x83, 0x42, 0xc3, 0x93, 0x22, 0x1a, 0x4f, 0x13, 0x4b, 0x15, 0xa4, - 0x16, 0x3c, 0x05, 0x3b, 0x32, 0xeb, 0xa8, 0x5e, 0x59, 0x36, 0x06, 0xda, 0x67, 0xa1, - 0x1c, 0xe1, 0x74, 0xb7, 0x7b, 0xbe, 0xfd, 0x50, 0xef, 0x10, 0x25, 0xe9, 0x4a, 0x06, - 0xc5, 0xe0, 0x98, 0x8d, 0xb7, 0xf9, 0xda, 0x54, 0x0a, 0xa3, 0xb1, 0xc0, 0x33, 0x09, - 0xb4, 0xb1, 0x40, 0x01, 0xe2, 0xc4, 0x5a, 0xa9, 0x99, 0x65, 0x0b, 0x01, 0xaa, 0x3b, - 0xef, 0x5f, 0xb2, 0xd3, 0x38, 0x0c, 0xbf, 0x33, 0xc5, 0x5d, 0x45, 0x70, 0x25, 0x9f, - 0x1e, 0x3e, 0xd7, 0xe0, 0x0c, 0xa9, - ], - ock: [ - 0x54, 0xce, 0xb1, 0x1b, 0xb0, 0xe8, 0xf8, 0x54, 0x86, 0x10, 0xd1, 0x1f, 0xf1, 0xab, - 0x14, 0x92, 0xd1, 0x8d, 0x5c, 0x85, 0x3c, 0x8f, 0x2f, 0x0c, 0xd5, 0xd1, 0x9d, 0x6d, - 0x34, 0xcf, 0x7c, 0x2d, - ], - op: [ - 0xd1, 0x1d, 0xa0, 0x1f, 0x0b, 0x43, 0xbd, 0xd5, 0x28, 0x8d, 0x32, 0x38, 0x5b, 0x87, - 0x71, 0xd2, 0x23, 0x49, 0x3c, 0x69, 0x80, 0x25, 0x44, 0x04, 0x3f, 0x77, 0xcf, 0x1d, - 0x71, 0xc1, 0xcb, 0x8c, 0x6d, 0xa9, 0x45, 0xd3, 0x03, 0x81, 0xc2, 0xee, 0xd2, 0xb8, - 0x1d, 0x27, 0x08, 0x6d, 0x22, 0x48, 0xe7, 0xc4, 0x49, 0xfe, 0x50, 0x9b, 0x38, 0xe2, - 0x76, 0x79, 0x11, 0x89, 0xea, 0xbc, 0x46, 0x02, - ], - c_out: [ - 0xe7, 0x72, 0xe0, 0x1d, 0x61, 0x09, 0xb6, 0xf9, 0x85, 0xb1, 0x77, 0x2e, 0xd1, 0x55, - 0x0a, 0x94, 0x7b, 0x35, 0xa8, 0x4b, 0x3e, 0x71, 0x12, 0x33, 0x31, 0xa3, 0xd6, 0x1f, - 0x1b, 0xf5, 0x96, 0x4e, 0x97, 0x42, 0x54, 0x42, 0xe5, 0xc8, 0xef, 0x2b, 0x9d, 0x84, - 0xab, 0x3d, 0xcb, 0xab, 0x9c, 0x96, 0xfe, 0x6a, 0x89, 0xce, 0x1d, 0x5e, 0x8a, 0x9b, - 0x83, 0xb5, 0x09, 0x0b, 0xb0, 0x7c, 0x50, 0x45, 0x0b, 0xbb, 0xfc, 0x8a, 0x74, 0x64, - 0xa7, 0x7c, 0x33, 0x97, 0x16, 0x33, 0xb2, 0x13, 0x68, 0xf0, - ], - }, - TestVector { - ovk: [ - 0xe9, 0xe0, 0xdc, 0x1e, 0xd3, 0x11, 0xda, 0xed, 0x64, 0xbd, 0x74, 0xda, 0x5d, 0x94, - 0xfe, 0x88, 0xa6, 0xea, 0x41, 0x4b, 0x73, 0x12, 0xde, 0x3d, 0x2a, 0x78, 0xf6, 0x46, - 0x32, 0xbb, 0xe3, 0x73, - ], - ivk: [ - 0x87, 0x16, 0xc8, 0x28, 0x80, 0xe1, 0x36, 0x83, 0xe1, 0xbb, 0x05, 0x9d, 0xd0, 0x6c, - 0x80, 0xc9, 0x01, 0x34, 0xa9, 0x6d, 0x5a, 0xfc, 0xa8, 0xaa, 0xc2, 0xbb, 0xf6, 0x8b, - 0xb0, 0x5f, 0x84, 0x02, - ], - default_d: [ - 0xad, 0x6e, 0x2e, 0x18, 0x5a, 0x31, 0x00, 0xe3, 0xa6, 0xa8, 0xb3, - ], - default_pk_d: [ - 0x32, 0xcb, 0x28, 0x06, 0xb8, 0x82, 0xf1, 0x36, 0x8b, 0x0d, 0x4a, 0x89, 0x8f, 0x72, - 0xc4, 0xc8, 0xf7, 0x28, 0x13, 0x2c, 0xc1, 0x24, 0x56, 0x94, 0x6e, 0x7f, 0x4c, 0xb0, - 0xfb, 0x05, 0x8d, 0xa9, - ], - v: 800000000, - rcm: [ - 0x51, 0x65, 0xaf, 0xf2, 0x2d, 0xd4, 0xed, 0x56, 0xb4, 0xd8, 0x1d, 0x1f, 0x17, 0x1c, - 0xc3, 0xd6, 0x43, 0x2f, 0xed, 0x1b, 0xeb, 0xf2, 0x0a, 0x7b, 0xea, 0xb1, 0x2d, 0xb1, - 0x42, 0xf9, 0x4a, 0x0c, - ], - memo: [ - 0xf6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ], - cv: [ - 0x29, 0x54, 0xcc, 0x7f, 0x9f, 0x9d, 0xfe, 0xb1, 0x4f, 0x02, 0xee, 0xbf, 0xf3, 0xf8, - 0x48, 0xd5, 0xd0, 0xe3, 0xd2, 0xe0, 0x1f, 0xeb, 0xc9, 0x16, 0x41, 0xf4, 0x12, 0x6c, - 0x60, 0x34, 0x33, 0x0c, - ], - cmu: [ - 0x09, 0x90, 0xcd, 0xb9, 0xa5, 0x2e, 0x5c, 0xd1, 0xba, 0x54, 0xd9, 0x20, 0x4c, 0x26, - 0x69, 0x1c, 0xb0, 0x36, 0xb1, 0x30, 0x12, 0x21, 0x26, 0xeb, 0x14, 0x12, 0x9c, 0xdf, - 0x0f, 0xc5, 0x18, 0x3c, - ], - esk: [ - 0xab, 0x2a, 0xff, 0x03, 0x32, 0xd5, 0x43, 0xfd, 0x1d, 0x80, 0x23, 0x18, 0x5b, 0x8e, - 0xcb, 0x5f, 0x22, 0xa2, 0x9c, 0x32, 0xef, 0x74, 0x16, 0x33, 0x31, 0x6e, 0xee, 0x51, - 0x4f, 0xc2, 0x23, 0x09, - ], - epk: [ - 0xd0, 0x04, 0x99, 0x7c, 0x79, 0xd0, 0x07, 0xa5, 0x3b, 0xf2, 0xfd, 0x2f, 0x6a, 0x66, - 0xc0, 0xaf, 0xd9, 0xf8, 0x79, 0xb5, 0x5f, 0xec, 0xdc, 0x15, 0x8a, 0x90, 0x12, 0x32, - 0xb7, 0x88, 0x48, 0x09, - ], - shared_secret: [ - 0xa8, 0xde, 0xa9, 0xbe, 0x94, 0xdc, 0xca, 0xc8, 0x15, 0x75, 0xb4, 0x4f, 0x4b, 0xe8, - 0x53, 0xe8, 0xc0, 0xf7, 0xe6, 0xba, 0x7f, 0x0b, 0xf8, 0xf2, 0xb3, 0xa1, 0xb8, 0x9c, - 0x6a, 0xc8, 0x92, 0x39, - ], - k_enc: [ - 0x14, 0x1b, 0x55, 0x0a, 0xd3, 0xc2, 0xe7, 0xdf, 0xdc, 0xd4, 0x2d, 0x4a, 0xba, 0x31, - 0x39, 0x97, 0x42, 0xa9, 0x29, 0xbb, 0x23, 0x10, 0x0a, 0x7c, 0x51, 0xed, 0x32, 0xf9, - 0xcb, 0x45, 0x96, 0xc6, - ], - p_enc: [ - 0x01, 0xad, 0x6e, 0x2e, 0x18, 0x5a, 0x31, 0x00, 0xe3, 0xa6, 0xa8, 0xb3, 0x00, 0x08, - 0xaf, 0x2f, 0x00, 0x00, 0x00, 0x00, 0x51, 0x65, 0xaf, 0xf2, 0x2d, 0xd4, 0xed, 0x56, - 0xb4, 0xd8, 0x1d, 0x1f, 0x17, 0x1c, 0xc3, 0xd6, 0x43, 0x2f, 0xed, 0x1b, 0xeb, 0xf2, - 0x0a, 0x7b, 0xea, 0xb1, 0x2d, 0xb1, 0x42, 0xf9, 0x4a, 0x0c, 0xf6, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - ], - c_enc: [ - 0x6d, 0x3e, 0xff, 0x72, 0x8a, 0x28, 0x8e, 0x35, 0x59, 0xd8, 0x96, 0x06, 0xa4, 0x50, - 0xce, 0x14, 0x86, 0x4d, 0xf9, 0x03, 0x23, 0xcb, 0x2f, 0x41, 0xfb, 0xa2, 0x68, 0x84, - 0x3c, 0xec, 0x77, 0x75, 0x48, 0xbc, 0xc4, 0x25, 0xf5, 0xed, 0x1e, 0x6e, 0x8c, 0x75, - 0xe2, 0xda, 0xe3, 0x56, 0x16, 0x84, 0x56, 0x39, 0x1b, 0x87, 0xb5, 0xc6, 0xcd, 0x55, - 0x50, 0x3f, 0x12, 0xc3, 0x4f, 0x94, 0xb0, 0xd8, 0x24, 0xa7, 0x7a, 0xe6, 0x21, 0x3f, - 0xf4, 0x3f, 0x12, 0xa3, 0x4f, 0x2c, 0x66, 0x8e, 0xa1, 0x6b, 0xd1, 0xf0, 0x4a, 0x91, - 0xd3, 0x9a, 0x7b, 0x60, 0x19, 0x7c, 0x7b, 0x58, 0x62, 0x90, 0x36, 0xa8, 0x8f, 0xa7, - 0x0a, 0x8d, 0x5b, 0xf8, 0x3e, 0xd4, 0xdb, 0x40, 0x63, 0xb1, 0xea, 0xce, 0x10, 0x95, - 0xf9, 0x06, 0x62, 0xce, 0x9f, 0x6a, 0xc0, 0x26, 0x73, 0xf7, 0xb9, 0xa3, 0x6e, 0xbc, - 0x52, 0xf4, 0x98, 0x4b, 0xd7, 0x11, 0x53, 0xb3, 0xe2, 0xed, 0xca, 0x80, 0x3d, 0x86, - 0x90, 0x26, 0xee, 0x2f, 0xf0, 0x22, 0x8a, 0xfa, 0x7b, 0x61, 0xd0, 0xd3, 0x8c, 0x9b, - 0xcc, 0xb3, 0x00, 0x8b, 0x32, 0xc6, 0xa0, 0x59, 0x84, 0x2e, 0xe8, 0xa0, 0x7b, 0xa1, - 0x2c, 0x63, 0x08, 0x43, 0x6b, 0x64, 0x89, 0x85, 0x35, 0x3d, 0x7d, 0xd5, 0x8b, 0x20, - 0x92, 0xb5, 0xac, 0x2e, 0xd7, 0xe7, 0x20, 0x65, 0xec, 0xad, 0xa6, 0x50, 0xae, 0xe6, - 0xcd, 0x00, 0xfd, 0x34, 0xd5, 0x8c, 0x2b, 0x58, 0xd4, 0x1a, 0x48, 0xaa, 0xc7, 0xbf, - 0x4b, 0x45, 0xc9, 0x6c, 0x53, 0xa1, 0x0b, 0x04, 0xdb, 0x73, 0xcc, 0x83, 0x27, 0x1b, - 0xa6, 0x71, 0x17, 0xd6, 0x42, 0xe4, 0xd8, 0x19, 0xc3, 0x02, 0xd7, 0x18, 0x5e, 0xcc, - 0xbf, 0xa5, 0x40, 0x5b, 0x80, 0xc5, 0xb3, 0xe4, 0xb2, 0xc5, 0x52, 0x43, 0x28, 0x60, - 0x80, 0x81, 0x78, 0xcb, 0x8f, 0xce, 0x40, 0x5b, 0x73, 0xfe, 0xf2, 0xb3, 0x46, 0xc4, - 0x1b, 0xb2, 0xb2, 0xfa, 0xd7, 0x1a, 0x80, 0x31, 0x3b, 0xe3, 0xcf, 0x01, 0xec, 0xfd, - 0x88, 0x8f, 0x25, 0x72, 0xed, 0xcf, 0x57, 0xe4, 0xd7, 0x1e, 0x47, 0xcf, 0x8d, 0x52, - 0xdb, 0xa4, 0xc6, 0x44, 0x0d, 0x0d, 0x4a, 0x9b, 0x19, 0x3f, 0x57, 0x74, 0x8d, 0x20, - 0xf8, 0x9a, 0xb5, 0xd6, 0xda, 0x16, 0x14, 0x36, 0x2a, 0x5f, 0xb8, 0x5f, 0x6a, 0xb2, - 0xbe, 0x35, 0xc7, 0x2f, 0xd6, 0x28, 0x7a, 0xe5, 0x5c, 0xd2, 0x77, 0x79, 0x19, 0x44, - 0xdf, 0x24, 0xa3, 0x76, 0x46, 0x71, 0xdd, 0xd4, 0x06, 0x0a, 0x9b, 0x9c, 0xab, 0x01, - 0x4a, 0xbe, 0x14, 0x35, 0x09, 0x31, 0x64, 0xa6, 0x9f, 0x61, 0xbf, 0x29, 0x24, 0x8c, - 0x35, 0x9c, 0xb6, 0x90, 0xab, 0x25, 0xe9, 0x93, 0xce, 0x39, 0x72, 0xd6, 0xee, 0x36, - 0x78, 0x5e, 0xf0, 0x61, 0x87, 0x20, 0x50, 0xf5, 0x26, 0xf7, 0xdb, 0x7f, 0xf1, 0x98, - 0xfb, 0xac, 0xff, 0x29, 0x85, 0x81, 0xb7, 0x33, 0x06, 0xef, 0xc0, 0x2b, 0xb9, 0xd4, - 0xab, 0x32, 0xdf, 0x26, 0x4f, 0x14, 0xa8, 0x0e, 0x7f, 0x0c, 0x76, 0xe5, 0xf1, 0x4d, - 0xa2, 0x9a, 0xb1, 0xea, 0x04, 0xa3, 0xe3, 0xf5, 0xba, 0x5e, 0x35, 0x05, 0x5d, 0xba, - 0xd2, 0x76, 0xe1, 0x20, 0x1c, 0xce, 0x0a, 0xec, 0x14, 0x82, 0xcb, 0xec, 0x1d, 0x3f, - 0xa4, 0xa1, 0x3d, 0x3e, 0x16, 0x51, 0x1b, 0x0d, 0xee, 0x35, 0x58, 0xc5, 0xae, 0xef, - 0x27, 0xe3, 0xe6, 0x1b, 0x91, 0x51, 0xe5, 0x5a, 0x5a, 0xe1, 0x57, 0x03, 0x0c, 0xe5, - 0x97, 0xf8, 0x21, 0x82, 0x89, 0x3e, 0xe4, 0xd6, 0xbd, 0x4f, 0xb0, 0x87, 0x29, 0xbb, - 0xc3, 0x01, 0x41, 0x9c, 0xe0, 0x66, 0x41, 0x45, 0xba, 0x7a, 0xb8, 0xcb, 0xc0, 0x65, - 0x48, 0xe1, 0xf7, 0xfd, 0xf5, 0x3d, 0x06, 0x05, 0xa7, 0x7b, 0xe6, 0xe4, 0x0c, 0x54, - 0x00, 0x90, 0xf9, 0x8c, 0x25, 0xb1, 0x25, 0xbe, 0x74, 0x99, 0xf1, 0x76, 0xbb, 0x85, - 0x01, 0x49, 0x33, 0x53, 0xcf, 0x90, 0x5f, 0x72, 0x25, 0x00, 0x62, 0xd6, 0xcf, 0x01, - 0x88, 0x14, 0x82, 0x46, 0xee, 0x94, 0xef, 0x9b, 0x21, 0xad, 0xb7, 0xae, 0x1a, 0xe7, - 0x3b, 0xb6, 0xe6, 0x8f, 0xa9, 0x1d, 0x7f, 0xb4, 0x98, 0x28, 0xd6, 0x57, 0xd8, 0x19, - 0x5f, 0x6e, 0x95, 0x08, 0x2f, 0xad, - ], - ock: [ - 0xda, 0xb4, 0x26, 0x26, 0x9e, 0x8d, 0x33, 0x09, 0x55, 0x23, 0x7a, 0x9f, 0xed, 0x86, - 0x83, 0xa9, 0x27, 0x7c, 0x61, 0x82, 0xa8, 0x08, 0xcc, 0x53, 0xa1, 0xbe, 0xdd, 0xd2, - 0x03, 0x68, 0xb1, 0x0a, - ], - op: [ - 0x32, 0xcb, 0x28, 0x06, 0xb8, 0x82, 0xf1, 0x36, 0x8b, 0x0d, 0x4a, 0x89, 0x8f, 0x72, - 0xc4, 0xc8, 0xf7, 0x28, 0x13, 0x2c, 0xc1, 0x24, 0x56, 0x94, 0x6e, 0x7f, 0x4c, 0xb0, - 0xfb, 0x05, 0x8d, 0xa9, 0xab, 0x2a, 0xff, 0x03, 0x32, 0xd5, 0x43, 0xfd, 0x1d, 0x80, - 0x23, 0x18, 0x5b, 0x8e, 0xcb, 0x5f, 0x22, 0xa2, 0x9c, 0x32, 0xef, 0x74, 0x16, 0x33, - 0x31, 0x6e, 0xee, 0x51, 0x4f, 0xc2, 0x23, 0x09, - ], - c_out: [ - 0xaf, 0x4d, 0x97, 0xfb, 0x72, 0x28, 0xf0, 0x1f, 0x6d, 0x9e, 0x2f, 0x79, 0xa1, 0xa1, - 0xba, 0x45, 0xa2, 0x3d, 0x60, 0x90, 0x59, 0x78, 0x4e, 0xa9, 0x35, 0x0f, 0x1e, 0xb0, - 0x92, 0xb0, 0x54, 0xa3, 0x26, 0x8c, 0xc0, 0x26, 0xd3, 0xd7, 0x37, 0xef, 0x35, 0xad, - 0xc2, 0x86, 0xd1, 0x95, 0xea, 0xa4, 0x14, 0x49, 0x3e, 0xd2, 0xa5, 0x1f, 0x2f, 0x61, - 0x09, 0x9a, 0x34, 0x51, 0xf9, 0x55, 0x5b, 0xab, 0x1a, 0x5e, 0xf3, 0xe3, 0xfb, 0xbe, - 0x8e, 0xc6, 0x41, 0x6b, 0xd3, 0x3d, 0x50, 0xdf, 0xf9, 0x8f, - ], - }, - TestVector { - ovk: [ - 0x14, 0x7d, 0xd1, 0x1d, 0x77, 0xeb, 0xa1, 0xb1, 0x63, 0x6f, 0xd6, 0x19, 0x0c, 0x62, - 0xb9, 0xa5, 0xd0, 0x48, 0x1b, 0xee, 0x7e, 0x91, 0x7f, 0xab, 0x02, 0xe2, 0x18, 0x58, - 0x06, 0x3a, 0xb5, 0x04, - ], - ivk: [ - 0x99, 0xc9, 0xb4, 0xb8, 0x4f, 0x4b, 0x4e, 0x35, 0x0f, 0x78, 0x7d, 0x1c, 0xf7, 0x05, - 0x1d, 0x50, 0xec, 0xc3, 0x4b, 0x1a, 0x5b, 0x20, 0xd2, 0xd2, 0x13, 0x9b, 0x4a, 0xf1, - 0xf1, 0x60, 0xe0, 0x01, - ], - default_d: [ - 0x21, 0xc9, 0x0e, 0x1c, 0x65, 0x8b, 0x3e, 0xfe, 0x86, 0xaf, 0x58, - ], - default_pk_d: [ - 0x9e, 0x64, 0x17, 0x4b, 0x4a, 0xb9, 0x81, 0x40, 0x5c, 0x32, 0x3b, 0x5e, 0x12, 0x47, - 0x59, 0x45, 0xa4, 0x6d, 0x4f, 0xed, 0xf8, 0x06, 0x08, 0x28, 0x04, 0x1c, 0xd2, 0x0e, - 0x62, 0xfd, 0x2c, 0xef, - ], - v: 900000000, - rcm: [ - 0x8c, 0x3e, 0x56, 0x44, 0x9d, 0xc8, 0x63, 0x54, 0xd3, 0x3b, 0x02, 0x5e, 0xf2, 0x79, - 0x34, 0x60, 0xbc, 0xb1, 0x69, 0xf3, 0x32, 0x4e, 0x4a, 0x6b, 0x64, 0xba, 0xa6, 0x08, - 0x32, 0x31, 0x57, 0x04, - ], - memo: [ - 0xf6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ], - cv: [ - 0x4a, 0x85, 0xeb, 0x3f, 0x25, 0x3f, 0x3b, 0xaa, 0xf6, 0xb5, 0x5a, 0x99, 0x49, 0x51, - 0xb2, 0xca, 0x82, 0x48, 0xcb, 0xd6, 0x79, 0xf7, 0xa5, 0x77, 0xe3, 0x3b, 0xcd, 0x66, - 0x46, 0xb2, 0x13, 0x51, - ], - cmu: [ - 0x56, 0x90, 0xcd, 0x51, 0xa4, 0x5c, 0xe8, 0x9a, 0x51, 0xac, 0xbe, 0x01, 0x60, 0x60, - 0xf0, 0xdf, 0xee, 0x0d, 0x2f, 0xc9, 0xb8, 0x97, 0x58, 0x5f, 0x97, 0x4a, 0x40, 0x2e, - 0x53, 0x7f, 0xe2, 0x18, - ], - esk: [ - 0xa5, 0x3d, 0x19, 0xf5, 0x69, 0x45, 0x95, 0xd5, 0xae, 0x63, 0x02, 0x27, 0x67, 0x3c, - 0x80, 0x24, 0x9c, 0xe1, 0x24, 0x41, 0x9f, 0x46, 0xdf, 0x4e, 0x7b, 0x3f, 0xc1, 0x04, - 0x61, 0x28, 0xcd, 0x0b, - ], - epk: [ - 0x4d, 0xfc, 0x8a, 0x70, 0xb2, 0x10, 0xdf, 0xd4, 0x48, 0x37, 0xaa, 0x52, 0xd6, 0x3b, - 0xd5, 0xd8, 0x1a, 0x5e, 0x40, 0xd8, 0xb4, 0xc1, 0x7a, 0x2d, 0xca, 0x25, 0xa5, 0xf7, - 0x5f, 0xe5, 0x20, 0x2e, - ], - shared_secret: [ - 0x1f, 0xf7, 0x5f, 0x5e, 0x7a, 0x51, 0x4b, 0x3c, 0xf5, 0xb3, 0x3c, 0xa3, 0x1a, 0x67, - 0x1f, 0xc5, 0x0c, 0x26, 0x8c, 0xf1, 0xa3, 0x16, 0xb2, 0x1b, 0x98, 0x67, 0x4b, 0xaa, - 0x45, 0x00, 0x85, 0xcf, - ], - k_enc: [ - 0x3c, 0x52, 0xd9, 0xc8, 0x32, 0x07, 0xee, 0x14, 0xf5, 0x62, 0x0d, 0x16, 0x21, 0x82, - 0xa6, 0xb9, 0xca, 0xbe, 0xfd, 0xba, 0x9e, 0x7a, 0x74, 0xf5, 0xba, 0x2f, 0x81, 0xb8, - 0x71, 0x40, 0x1f, 0x08, - ], - p_enc: [ - 0x01, 0x21, 0xc9, 0x0e, 0x1c, 0x65, 0x8b, 0x3e, 0xfe, 0x86, 0xaf, 0x58, 0x00, 0xe9, - 0xa4, 0x35, 0x00, 0x00, 0x00, 0x00, 0x8c, 0x3e, 0x56, 0x44, 0x9d, 0xc8, 0x63, 0x54, - 0xd3, 0x3b, 0x02, 0x5e, 0xf2, 0x79, 0x34, 0x60, 0xbc, 0xb1, 0x69, 0xf3, 0x32, 0x4e, - 0x4a, 0x6b, 0x64, 0xba, 0xa6, 0x08, 0x32, 0x31, 0x57, 0x04, 0xf6, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - ], - c_enc: [ - 0x84, 0xd3, 0x61, 0x09, 0xbd, 0xd2, 0x1c, 0x67, 0x8e, 0x84, 0x47, 0xf8, 0x89, 0xe5, - 0x60, 0xef, 0x6d, 0x07, 0xa8, 0x27, 0xaa, 0xab, 0x78, 0x9b, 0x46, 0xc3, 0xf9, 0xeb, - 0x32, 0x2e, 0xea, 0x21, 0x4c, 0x20, 0xf7, 0xe9, 0xfa, 0x7f, 0x7a, 0xa5, 0xe0, 0x44, - 0xa4, 0xed, 0x4c, 0xb1, 0x5d, 0xa9, 0xc5, 0x6c, 0x32, 0xf3, 0x7e, 0x4c, 0xbe, 0x7d, - 0x1e, 0xd1, 0xf6, 0x85, 0xa8, 0x74, 0x8d, 0xbf, 0x78, 0x12, 0x90, 0xf9, 0x7a, 0xc1, - 0x41, 0x40, 0xaa, 0x8b, 0x50, 0x93, 0x2a, 0x3f, 0x66, 0xc2, 0x08, 0x22, 0x6f, 0x8d, - 0x8e, 0xc0, 0xde, 0xb7, 0xbb, 0x58, 0x35, 0x72, 0xc9, 0xe9, 0x70, 0xbc, 0xd0, 0xc6, - 0x44, 0x67, 0x26, 0xaa, 0x5b, 0x6a, 0x5f, 0x81, 0xcf, 0x18, 0xc6, 0x7a, 0x99, 0x2d, - 0x6c, 0x86, 0x03, 0x86, 0xab, 0xbb, 0x5b, 0x90, 0xbe, 0x58, 0x64, 0x34, 0x4f, 0xc8, - 0xbf, 0x3e, 0xbb, 0x75, 0x41, 0xaa, 0x9b, 0x9e, 0x1e, 0x3f, 0x96, 0x25, 0xac, 0xce, - 0x7f, 0x4b, 0xf1, 0x58, 0x39, 0xa0, 0x81, 0x70, 0x68, 0xe9, 0x15, 0x1b, 0x63, 0x7f, - 0xa2, 0xa2, 0xca, 0x09, 0xb9, 0xbe, 0x28, 0x5f, 0xea, 0x7e, 0x0a, 0x03, 0x31, 0x7c, - 0x29, 0x8a, 0xd7, 0xff, 0xfe, 0x40, 0xc5, 0xf0, 0xf6, 0xe9, 0xfb, 0x44, 0xe8, 0xf0, - 0x6e, 0x19, 0x2f, 0x1a, 0xc2, 0x10, 0x8f, 0x3f, 0x11, 0xf7, 0x76, 0x3c, 0xf2, 0x1e, - 0x96, 0x62, 0x4d, 0x52, 0xf3, 0xe7, 0x2a, 0xaf, 0x15, 0x7f, 0x3b, 0xc7, 0xc5, 0xd1, - 0x8f, 0x1e, 0xba, 0x3d, 0x82, 0x7f, 0x71, 0x9c, 0x27, 0x9f, 0xd9, 0x66, 0xc2, 0x7d, - 0x94, 0xd7, 0x47, 0x23, 0xc5, 0x31, 0x1b, 0x86, 0x65, 0xbd, 0x29, 0xb3, 0xa1, 0x00, - 0xbb, 0x21, 0x11, 0xaa, 0x42, 0x16, 0xf0, 0x66, 0x5b, 0x16, 0x9e, 0xc0, 0x94, 0x17, - 0x68, 0xa9, 0x57, 0x4a, 0xe5, 0x0c, 0x2b, 0xc7, 0x90, 0x05, 0x53, 0xf5, 0xc4, 0x50, - 0xee, 0x98, 0x82, 0xaf, 0x44, 0x55, 0xd1, 0xd8, 0xce, 0x35, 0x18, 0x49, 0xd7, 0x8d, - 0xbb, 0xe6, 0x1e, 0xd1, 0xdb, 0x7a, 0x2f, 0xd6, 0x57, 0x75, 0xd5, 0x50, 0x6d, 0xfd, - 0x02, 0xa9, 0x4d, 0x9d, 0x42, 0x85, 0xa2, 0x3a, 0x3c, 0xab, 0x8a, 0xa3, 0x32, 0x14, - 0x22, 0xa4, 0xaa, 0xa5, 0x49, 0x27, 0x4a, 0x25, 0xf7, 0xf1, 0x2f, 0xf7, 0xa5, 0x19, - 0x5e, 0x51, 0x55, 0x73, 0x9f, 0x31, 0x8c, 0x30, 0xc0, 0x24, 0x8c, 0x3a, 0x21, 0x9a, - 0x7a, 0xde, 0x72, 0x98, 0x38, 0x0a, 0x59, 0x5c, 0x5c, 0x88, 0x5b, 0x42, 0x06, 0x69, - 0xcd, 0x6d, 0xeb, 0x2e, 0x5c, 0x80, 0x49, 0x78, 0xcb, 0x42, 0xd2, 0x06, 0x02, 0x74, - 0x57, 0x33, 0x60, 0x7c, 0xef, 0x4e, 0x26, 0xa5, 0xc9, 0x7c, 0xca, 0x1c, 0xc5, 0x2b, - 0x7f, 0xdc, 0x10, 0x69, 0x01, 0x70, 0x18, 0x07, 0x6c, 0xac, 0x62, 0xe5, 0xc4, 0xdb, - 0xf9, 0x07, 0x48, 0x72, 0x05, 0x0a, 0x42, 0x22, 0x19, 0x51, 0x3b, 0xca, 0x27, 0xa8, - 0x35, 0xf4, 0x82, 0x4f, 0x47, 0xba, 0x33, 0x7d, 0xeb, 0x74, 0x40, 0xf3, 0xf2, 0xca, - 0xce, 0x9e, 0x33, 0x16, 0x70, 0xdd, 0x98, 0xe3, 0x28, 0xab, 0x0a, 0x16, 0xac, 0x4a, - 0xb6, 0x62, 0x76, 0xd1, 0xe1, 0x01, 0x8b, 0x2c, 0xf1, 0x79, 0x43, 0x62, 0x66, 0xa4, - 0x08, 0xda, 0x8d, 0xda, 0xfc, 0x44, 0xb2, 0x27, 0x6b, 0x11, 0x68, 0x52, 0xd4, 0xcc, - 0xb3, 0x52, 0x89, 0xb4, 0x21, 0x30, 0x09, 0x12, 0x5d, 0x2d, 0x87, 0x84, 0x5d, 0x6e, - 0xb7, 0x8e, 0x55, 0x03, 0x15, 0x3d, 0x92, 0xfb, 0xd4, 0x93, 0xd1, 0x9e, 0xf0, 0x1f, - 0x37, 0x00, 0x26, 0xba, 0xf1, 0x72, 0x30, 0x7b, 0x3f, 0xe2, 0xc4, 0x56, 0x96, 0xfb, - 0xce, 0xda, 0x3b, 0x6e, 0xab, 0x05, 0xe2, 0xb0, 0x68, 0x5c, 0x72, 0x79, 0x04, 0x98, - 0x23, 0x3a, 0xbb, 0xbd, 0x6e, 0x05, 0xb0, 0xf4, 0x4a, 0x72, 0x98, 0xae, 0x0a, 0x25, - 0xaf, 0x08, 0xd7, 0x95, 0x74, 0x61, 0x4c, 0xf2, 0xd8, 0x3e, 0xa7, 0x9c, 0x2b, 0x79, - 0x53, 0xf8, 0x6c, 0xf5, 0xd0, 0x49, 0x27, 0xf0, 0x9c, 0x0d, 0x7d, 0xf8, 0x12, 0xf1, - 0xcf, 0x18, 0xa4, 0x53, 0xa0, 0x49, 0x70, 0xaf, 0x0d, 0x72, 0x9c, 0xe7, 0xd9, 0xc8, - 0xd6, 0xa2, 0x4d, 0x7e, 0xed, 0x3d, - ], - ock: [ - 0xc9, 0x72, 0x1e, 0x9e, 0x65, 0xa2, 0x61, 0x85, 0x10, 0x07, 0xcd, 0x81, 0x46, 0x7b, - 0xa5, 0xf3, 0x58, 0x05, 0xba, 0x78, 0x5a, 0x2c, 0x92, 0xa9, 0xaa, 0x62, 0x32, 0xb0, - 0x55, 0x1c, 0xf3, 0xf4, - ], - op: [ - 0x9e, 0x64, 0x17, 0x4b, 0x4a, 0xb9, 0x81, 0x40, 0x5c, 0x32, 0x3b, 0x5e, 0x12, 0x47, - 0x59, 0x45, 0xa4, 0x6d, 0x4f, 0xed, 0xf8, 0x06, 0x08, 0x28, 0x04, 0x1c, 0xd2, 0x0e, - 0x62, 0xfd, 0x2c, 0xef, 0xa5, 0x3d, 0x19, 0xf5, 0x69, 0x45, 0x95, 0xd5, 0xae, 0x63, - 0x02, 0x27, 0x67, 0x3c, 0x80, 0x24, 0x9c, 0xe1, 0x24, 0x41, 0x9f, 0x46, 0xdf, 0x4e, - 0x7b, 0x3f, 0xc1, 0x04, 0x61, 0x28, 0xcd, 0x0b, - ], - c_out: [ - 0xbc, 0x16, 0xaf, 0xa8, 0xaa, 0xb2, 0x38, 0x06, 0x26, 0x01, 0x8c, 0xe2, 0x75, 0x58, - 0x67, 0x55, 0x8f, 0x9d, 0x59, 0x85, 0x73, 0x93, 0xa1, 0xf3, 0x48, 0xb2, 0x1c, 0xb5, - 0x0f, 0x53, 0xea, 0xba, 0xe7, 0xf6, 0xe4, 0x7b, 0x45, 0x24, 0x1f, 0x6b, 0x7b, 0x3d, - 0x68, 0x94, 0x5d, 0xd4, 0x0c, 0xad, 0xc5, 0x7a, 0x9a, 0xde, 0x6a, 0xf9, 0x69, 0xae, - 0x07, 0x4f, 0xf2, 0x89, 0xbc, 0xb6, 0x61, 0x0a, 0xe3, 0x8c, 0x82, 0x10, 0xa5, 0xcb, - 0xd7, 0x47, 0xb8, 0x31, 0x15, 0x1c, 0x56, 0xef, 0x02, 0xc9, - ], - }, - TestVector { - ovk: [ - 0x57, 0x34, 0x67, 0xa7, 0xb3, 0x0e, 0xad, 0x6c, 0xcc, 0x50, 0x47, 0x44, 0xca, 0x9e, - 0x1a, 0x28, 0x1a, 0x0d, 0x1a, 0x08, 0x73, 0x8b, 0x06, 0xa0, 0x68, 0x4f, 0xea, 0xcd, - 0x1e, 0x9d, 0x12, 0x6d, - ], - ivk: [ - 0xdb, 0x95, 0xea, 0x8b, 0xd9, 0xf9, 0x3d, 0x41, 0xb5, 0xab, 0x2b, 0xeb, 0xc9, 0x1a, - 0x38, 0xed, 0xd5, 0x27, 0x08, 0x3e, 0x2a, 0x6e, 0xf9, 0xf3, 0xc2, 0x97, 0x02, 0xd5, - 0xff, 0x89, 0xed, 0x00, - ], - default_d: [ - 0x23, 0x3c, 0x4a, 0xb8, 0x86, 0xa5, 0x5e, 0x3b, 0xa3, 0x74, 0xc0, - ], - default_pk_d: [ - 0xb6, 0x8e, 0x9e, 0xe0, 0xc0, 0x67, 0x8d, 0x7b, 0x30, 0x36, 0x93, 0x1c, 0x83, 0x1a, - 0x25, 0x25, 0x5f, 0x7e, 0xe4, 0x87, 0x38, 0x5a, 0x30, 0x31, 0x6e, 0x15, 0xf6, 0x48, - 0x2b, 0x87, 0x4f, 0xda, - ], - v: 1000000000, - rcm: [ - 0x6e, 0xbb, 0xed, 0x74, 0x36, 0x19, 0xa2, 0x56, 0xf9, 0xad, 0x2e, 0x85, 0x88, 0x0c, - 0xfa, 0xa9, 0x09, 0x8a, 0x5f, 0xdb, 0x16, 0x29, 0x99, 0x0d, 0x9a, 0x7d, 0x3b, 0xb9, - 0x3f, 0xc9, 0x00, 0x03, - ], - memo: [ - 0xf6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ], - cv: [ - 0x2a, 0x54, 0x7d, 0x97, 0x8c, 0x7c, 0x90, 0xa8, 0xd0, 0xa5, 0x47, 0x4e, 0x29, 0xdb, - 0xff, 0xf3, 0x4b, 0xae, 0x81, 0xe6, 0x40, 0x8e, 0xc1, 0xfe, 0x2d, 0x56, 0xa2, 0x52, - 0x41, 0xa8, 0xe3, 0x29, - ], - cmu: [ - 0xf4, 0xba, 0x4e, 0xf0, 0x40, 0xf8, 0x0d, 0x00, 0x08, 0x0d, 0x29, 0xa6, 0xb3, 0x99, - 0xdc, 0x40, 0x32, 0x40, 0x33, 0x61, 0xe0, 0x59, 0x1e, 0xd6, 0x14, 0x99, 0xbc, 0x06, - 0x8e, 0x41, 0xed, 0x38, - ], - esk: [ - 0x29, 0x95, 0x89, 0x80, 0x69, 0x4f, 0x7f, 0x67, 0x08, 0x09, 0x97, 0xc2, 0x66, 0x47, - 0x02, 0x89, 0x0c, 0xd1, 0xb5, 0x03, 0xdd, 0xa4, 0x2d, 0x33, 0xa8, 0x99, 0xce, 0x99, - 0x1f, 0xe0, 0xf8, 0x00, - ], - epk: [ - 0xea, 0x6b, 0x3c, 0x98, 0x5f, 0x33, 0xb2, 0xa2, 0x2d, 0x0d, 0xbf, 0x7c, 0xd9, 0x30, - 0x19, 0xfd, 0x9e, 0x57, 0x31, 0x6c, 0x85, 0xb7, 0x67, 0x49, 0x54, 0x62, 0x9c, 0x77, - 0xdf, 0xae, 0xc0, 0x66, - ], - shared_secret: [ - 0xc0, 0x64, 0x58, 0x25, 0xdf, 0xc4, 0x4d, 0x54, 0x82, 0x83, 0xf6, 0xe8, 0x88, 0x25, - 0x3b, 0xf5, 0xc3, 0x2a, 0x90, 0xde, 0xbb, 0x92, 0x8e, 0x89, 0x67, 0x86, 0xac, 0x0b, - 0x16, 0xd5, 0xf6, 0x56, - ], - k_enc: [ - 0x33, 0xd2, 0xda, 0x8d, 0x80, 0xe0, 0xce, 0xd8, 0xb4, 0xbe, 0xec, 0x94, 0x3a, 0x0f, - 0xc9, 0xc9, 0x60, 0xad, 0x7c, 0xcc, 0x59, 0x77, 0x43, 0x74, 0x4c, 0x18, 0xc9, 0xc2, - 0xa5, 0x62, 0xf6, 0x3a, - ], - p_enc: [ - 0x01, 0x23, 0x3c, 0x4a, 0xb8, 0x86, 0xa5, 0x5e, 0x3b, 0xa3, 0x74, 0xc0, 0x00, 0xca, - 0x9a, 0x3b, 0x00, 0x00, 0x00, 0x00, 0x6e, 0xbb, 0xed, 0x74, 0x36, 0x19, 0xa2, 0x56, - 0xf9, 0xad, 0x2e, 0x85, 0x88, 0x0c, 0xfa, 0xa9, 0x09, 0x8a, 0x5f, 0xdb, 0x16, 0x29, - 0x99, 0x0d, 0x9a, 0x7d, 0x3b, 0xb9, 0x3f, 0xc9, 0x00, 0x03, 0xf6, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - ], - c_enc: [ - 0x14, 0x9a, 0x52, 0xf8, 0xf5, 0x34, 0x2b, 0x44, 0x84, 0x88, 0x91, 0xf8, 0x85, 0xd3, - 0xcd, 0x09, 0x9a, 0xbe, 0x80, 0x5a, 0xa5, 0x09, 0x1f, 0xe1, 0x71, 0x0e, 0xb7, 0x35, - 0x02, 0xde, 0x38, 0x7d, 0xf3, 0xf9, 0x64, 0x67, 0x22, 0xe8, 0xb8, 0x5c, 0x37, 0x7c, - 0x82, 0x2a, 0x71, 0x03, 0x34, 0x7c, 0x81, 0x01, 0xe9, 0xae, 0x8c, 0x31, 0x82, 0xca, - 0x36, 0xda, 0xfd, 0x75, 0x8d, 0x96, 0xce, 0xba, 0x48, 0x32, 0x7a, 0x09, 0x82, 0x86, - 0xa4, 0xe8, 0x32, 0x1d, 0x1e, 0x74, 0xfe, 0x3d, 0x61, 0x59, 0xc0, 0x29, 0x48, 0x3d, - 0xe9, 0xee, 0xf3, 0xb2, 0x4d, 0x85, 0xe4, 0xd5, 0x16, 0xb8, 0x70, 0x4f, 0x8e, 0x7d, - 0x93, 0xe7, 0x44, 0x42, 0xed, 0x00, 0x7a, 0xd7, 0x9a, 0x61, 0x52, 0xf2, 0xb6, 0x64, - 0x2f, 0xbe, 0xe6, 0x04, 0x35, 0xe1, 0x92, 0x09, 0xd8, 0x11, 0xc6, 0x6c, 0x17, 0xb7, - 0xdf, 0x3d, 0xfd, 0x76, 0x9f, 0xb5, 0xc7, 0xd0, 0x06, 0xb3, 0x67, 0x42, 0xbb, 0xe7, - 0x26, 0x92, 0x9e, 0x87, 0x9b, 0x11, 0x6d, 0x36, 0x13, 0x57, 0x1a, 0xa6, 0x3a, 0xc2, - 0xcc, 0xca, 0x43, 0xf8, 0x90, 0x0b, 0x89, 0x3e, 0x64, 0xdd, 0x0b, 0x8f, 0xf9, 0x1e, - 0xc5, 0x11, 0x40, 0x82, 0xe6, 0xd0, 0x0c, 0xf9, 0x3a, 0x7c, 0xfa, 0x75, 0x18, 0xbb, - 0x7f, 0xb6, 0x4a, 0x7f, 0x34, 0x64, 0x20, 0xb6, 0x44, 0x78, 0xd7, 0x18, 0x69, 0xe9, - 0x1d, 0x47, 0x97, 0x90, 0x1f, 0xa8, 0x6e, 0x70, 0xb2, 0x20, 0x1a, 0xfe, 0x4b, 0xd3, - 0xea, 0x55, 0x03, 0x81, 0x6f, 0xac, 0x68, 0x7d, 0x81, 0x25, 0x2f, 0x65, 0x61, 0x6e, - 0x7f, 0xb2, 0x68, 0x46, 0x52, 0x1e, 0x39, 0xff, 0x94, 0xbe, 0x73, 0xb8, 0xac, 0xa8, - 0x04, 0xc6, 0x5c, 0xf9, 0x4e, 0x32, 0x56, 0xbd, 0x3c, 0x69, 0xad, 0x31, 0x8e, 0x6b, - 0x28, 0x55, 0x19, 0x48, 0x77, 0x93, 0xee, 0x29, 0x88, 0x51, 0x40, 0xf0, 0xbc, 0x00, - 0x84, 0x5f, 0x67, 0x41, 0x5f, 0x67, 0x0f, 0x04, 0xca, 0x81, 0x8c, 0x5f, 0x32, 0x49, - 0xd3, 0xfb, 0x70, 0xbf, 0xea, 0x10, 0xc6, 0x25, 0xeb, 0x8c, 0xf2, 0xca, 0xb3, 0xf5, - 0x83, 0x62, 0x2a, 0x21, 0xa3, 0x8b, 0x8f, 0xe5, 0x1a, 0x5f, 0xf2, 0x91, 0x9e, 0xf4, - 0xc1, 0xbd, 0x98, 0x30, 0xa9, 0xf2, 0x48, 0x6a, 0xbd, 0x88, 0x5d, 0xd9, 0x43, 0xb9, - 0x4e, 0xdc, 0x8f, 0x88, 0xc8, 0xb7, 0x8a, 0x5e, 0xb0, 0x31, 0xf3, 0x4b, 0x7d, 0x93, - 0x1c, 0x87, 0x53, 0xaf, 0xd9, 0x76, 0x8d, 0x0f, 0xa8, 0xd2, 0x6e, 0x88, 0xc9, 0x56, - 0x7a, 0xd5, 0x89, 0x23, 0xe7, 0xb0, 0xaf, 0xbd, 0xaa, 0xdf, 0x47, 0x7b, 0xd1, 0xd2, - 0x3f, 0xc4, 0x0a, 0x42, 0xc2, 0x9b, 0x4d, 0x5f, 0xe1, 0x08, 0x76, 0x45, 0xdd, 0xfd, - 0xeb, 0xa0, 0xc7, 0xd5, 0x67, 0x15, 0xcd, 0x57, 0xf0, 0xd1, 0x74, 0x1a, 0x3d, 0x9c, - 0xb3, 0x8d, 0x88, 0xd6, 0x47, 0xb1, 0xc5, 0xb2, 0x4a, 0xdd, 0xba, 0xd1, 0xac, 0xfa, - 0x3a, 0x8d, 0xa3, 0x7a, 0x74, 0x26, 0x05, 0x55, 0xec, 0x0d, 0xea, 0x88, 0xed, 0x2c, - 0x7f, 0x46, 0xdd, 0x87, 0xb3, 0xf2, 0x79, 0xa9, 0x6a, 0x0e, 0x78, 0x54, 0xec, 0x4a, - 0x79, 0xce, 0xad, 0xc7, 0x4a, 0x68, 0x0f, 0xc8, 0x2d, 0x75, 0xae, 0xc7, 0xf2, 0xd1, - 0x3d, 0xfb, 0x62, 0x23, 0x50, 0x57, 0xe4, 0xf7, 0xdc, 0x5b, 0x07, 0xc6, 0xba, 0xba, - 0x82, 0xb3, 0x2f, 0xe9, 0x0b, 0x5c, 0x6e, 0x9d, 0xc6, 0xb2, 0xfb, 0x33, 0xbe, 0xac, - 0x88, 0x0d, 0x3a, 0x60, 0xba, 0x08, 0x48, 0xfa, 0xc6, 0x61, 0x9d, 0xa8, 0xca, 0x33, - 0xa6, 0x32, 0x94, 0xeb, 0x63, 0xd0, 0xf2, 0x4c, 0xbb, 0x1e, 0x03, 0x17, 0x82, 0x88, - 0x0f, 0xfa, 0x18, 0x35, 0x6c, 0x98, 0x76, 0x2c, 0xcd, 0xd3, 0xaf, 0xab, 0x81, 0xf1, - 0x9a, 0xbf, 0x3b, 0xdd, 0x2b, 0xc4, 0x3c, 0xb1, 0xf2, 0x15, 0x5c, 0xaf, 0x64, 0x98, - 0x89, 0x4e, 0x06, 0x8b, 0xa7, 0x49, 0xc9, 0x76, 0xec, 0x23, 0xf2, 0x11, 0x62, 0x26, - 0x14, 0x60, 0x78, 0x56, 0xd8, 0x7b, 0x74, 0x16, 0x24, 0xf7, 0xf8, 0x34, 0x95, 0xd7, - 0xde, 0x4d, 0x6d, 0xe2, 0x08, 0xe1, 0x35, 0x74, 0xc8, 0x2a, 0x1b, 0x8b, 0x1c, 0xfe, - 0x87, 0xe9, 0x18, 0xe7, 0xb3, 0x96, - ], - ock: [ - 0xdb, 0x5b, 0xa6, 0xb9, 0xdb, 0xb1, 0x1f, 0x7c, 0xe8, 0x12, 0xeb, 0x1b, 0xf3, 0x29, - 0x8c, 0xca, 0x55, 0x71, 0xee, 0xcc, 0x69, 0xb7, 0x22, 0xa0, 0xa3, 0xb8, 0x67, 0x50, - 0x72, 0x92, 0x99, 0xa0, - ], - op: [ - 0xb6, 0x8e, 0x9e, 0xe0, 0xc0, 0x67, 0x8d, 0x7b, 0x30, 0x36, 0x93, 0x1c, 0x83, 0x1a, - 0x25, 0x25, 0x5f, 0x7e, 0xe4, 0x87, 0x38, 0x5a, 0x30, 0x31, 0x6e, 0x15, 0xf6, 0x48, - 0x2b, 0x87, 0x4f, 0xda, 0x29, 0x95, 0x89, 0x80, 0x69, 0x4f, 0x7f, 0x67, 0x08, 0x09, - 0x97, 0xc2, 0x66, 0x47, 0x02, 0x89, 0x0c, 0xd1, 0xb5, 0x03, 0xdd, 0xa4, 0x2d, 0x33, - 0xa8, 0x99, 0xce, 0x99, 0x1f, 0xe0, 0xf8, 0x00, - ], - c_out: [ - 0xe2, 0x7a, 0x46, 0x4d, 0x6f, 0x44, 0xcc, 0x44, 0xf6, 0x17, 0xe2, 0x3c, 0x9f, 0xb1, - 0xb7, 0x1f, 0xff, 0xd4, 0x6a, 0xeb, 0xf0, 0x36, 0x77, 0xcf, 0x7d, 0xd2, 0x4d, 0x71, - 0x1b, 0xa0, 0xc6, 0xca, 0x38, 0x53, 0x09, 0x7b, 0x24, 0x7a, 0xb7, 0x4c, 0x15, 0xbb, - 0x93, 0x8e, 0xd6, 0x02, 0xfb, 0xcd, 0x30, 0xf4, 0xa6, 0x59, 0x56, 0x43, 0x0f, 0x47, - 0xa0, 0xfb, 0xcb, 0xe8, 0xe0, 0x8a, 0xad, 0xa3, 0x86, 0x30, 0x78, 0x5a, 0x80, 0x57, - 0x53, 0xba, 0x33, 0xb3, 0x34, 0xcd, 0x2a, 0x4b, 0xfc, 0x3d, - ], - }, - ] -} diff --git a/zcash_primitives/src/transaction/builder.rs b/zcash_primitives/src/transaction/builder.rs index db58da4ed3..b18d086c07 100644 --- a/zcash_primitives/src/transaction/builder.rs +++ b/zcash_primitives/src/transaction/builder.rs @@ -1,1002 +1,1099 @@ //! Structs for building transactions. -#[cfg(feature = "zfuture")] -use std::boxed::Box; +use core::cmp::Ordering; +use core::fmt; +use rand::{CryptoRng, RngCore}; + +use ::sapling::{builder::SaplingMetadata, Note, PaymentAddress}; +use ::transparent::{address::TransparentAddress, builder::TransparentBuilder, bundle::TxOut}; +use zcash_protocol::{ + consensus::{self, BlockHeight, BranchId, NetworkUpgrade, Parameters}, + memo::MemoBytes, + value::{BalanceError, ZatBalance, Zatoshis}, +}; -use std::error; -use std::fmt; -use std::marker::PhantomData; +use crate::transaction::{ + fees::{ + transparent::{InputView, OutputView}, + FeeRule, + }, + Transaction, TxVersion, +}; -use ff::Field; -use rand::{rngs::OsRng, seq::SliceRandom, CryptoRng, RngCore}; +#[cfg(feature = "std")] +use std::sync::mpsc::Sender; -use crate::{ - consensus::{self, BlockHeight}, - legacy::TransparentAddress, - memo::MemoBytes, - merkle_tree::MerklePath, - sapling::{ - keys::OutgoingViewingKey, note_encryption::sapling_note_encryption, prover::TxProver, - redjubjub::PrivateKey, spend_sig_internal, util::generate_random_rseed_internal, - Diversifier, Node, Note, PaymentAddress, +#[cfg(feature = "circuits")] +use { + crate::transaction::{ + sighash::{signature_hash, SignableInput}, + txid::TxIdDigester, + TransactionData, Unauthorized, }, + ::sapling::prover::{OutputProver, SpendProver}, + ::transparent::builder::TransparentSigningSet, + alloc::vec::Vec, +}; + +#[cfg(feature = "transparent-inputs")] +use ::transparent::builder::TransparentInputInfo; + +#[cfg(not(feature = "transparent-inputs"))] +use core::convert::Infallible; + +#[cfg(zcash_unstable = "zfuture")] +use crate::{ + extensions::transparent::{ExtensionTxBuilder, ToPayload}, transaction::{ components::{ - amount::{Amount, DEFAULT_FEE}, - OutputDescription, SpendDescription, TxOut, + tze::builder::TzeBuilder, + tze::{self, TzeOut}, }, - signature_hash_data, SignableInput, Transaction, TransactionData, SIGHASH_ALL, + fees::FutureFeeRule, }, - zip32::ExtendedSpendingKey, }; -#[cfg(feature = "transparent-inputs")] -use crate::{ - legacy::Script, - transaction::components::{OutPoint, TxIn}, -}; +use super::components::sapling::zip212_enforcement; -#[cfg(feature = "zfuture")] -use crate::{ - extensions::transparent::{self as tze, ExtensionTxBuilder, ToPayload}, - transaction::components::{TzeIn, TzeOut, TzeOutPoint}, -}; +/// Since Blossom activation, the default transaction expiry delta should be 40 blocks. +/// +pub const DEFAULT_TX_EXPIRY_DELTA: u32 = 40; -#[cfg(any(test, feature = "test-dependencies"))] -use crate::sapling::prover::mock::MockTxProver; - -const DEFAULT_TX_EXPIRY_DELTA: u32 = 20; - -/// If there are any shielded inputs, always have at least two shielded outputs, padding -/// with dummy outputs if necessary. See . -const MIN_SHIELDED_OUTPUTS: usize = 2; - -#[derive(Debug, PartialEq)] -pub enum Error { - AnchorMismatch, - BindingSig, - ChangeIsNegative(Amount), - InvalidAddress, - InvalidAmount, - NoChangeAddress, - SpendProof, - TzeWitnessModeMismatch(u32, u32), +/// Errors that can occur during fee calculation. +#[derive(Debug)] +pub enum FeeError { + FeeRule(FE), + Bundle(&'static str), } -impl fmt::Display for Error { +impl fmt::Display for FeeError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - Error::AnchorMismatch => { - write!(f, "Anchor mismatch (anchors for all spends must be equal)") - } - Error::BindingSig => write!(f, "Failed to create bindingSig"), - Error::ChangeIsNegative(amount) => { - write!(f, "Change is negative ({:?} zatoshis)", amount) - } - Error::InvalidAddress => write!(f, "Invalid address"), - Error::InvalidAmount => write!(f, "Invalid amount"), - Error::NoChangeAddress => write!(f, "No change address specified or discoverable"), - Error::SpendProof => write!(f, "Failed to create Sapling spend proof"), - Error::TzeWitnessModeMismatch(expected, actual) => - write!(f, "TZE witness builder returned a mode that did not match the mode with which the input was initially constructed: expected = {:?}, actual = {:?}", expected, actual), + FeeError::FeeRule(e) => write!(f, "An error occurred in fee calculation: {}", e), + FeeError::Bundle(b) => write!(f, "Bundle structure invalid in fee calculation: {}", b), } } } -impl error::Error for Error {} - -struct SpendDescriptionInfo { - extsk: ExtendedSpendingKey, - diversifier: Diversifier, - note: Note, - alpha: jubjub::Fr, - merkle_path: MerklePath, -} - -pub struct SaplingOutput { - /// `None` represents the `ovk = ⊥` case. - ovk: Option, - to: PaymentAddress, - note: Note, - memo: MemoBytes, - _params: PhantomData

, +/// Errors that can occur during transaction construction. +#[derive(Debug)] +pub enum Error { + /// Insufficient funds were provided to the transaction builder; the given + /// additional amount is required in order to construct the transaction. + InsufficientFunds(ZatBalance), + /// The transaction has inputs in excess of outputs and fees; the user must + /// add a change output. + ChangeRequired(ZatBalance), + /// An error occurred in computing the fees for a transaction. + Fee(FeeError), + /// An overflow or underflow occurred when computing value balances + Balance(BalanceError), + /// An error occurred in constructing the transparent parts of a transaction. + TransparentBuild(transparent::builder::Error), + /// An error occurred in constructing the Sapling parts of a transaction. + SaplingBuild(sapling::builder::Error), + /// An error occurred in constructing the Orchard parts of a transaction. + OrchardBuild(orchard::builder::BuildError), + /// An error occurred in adding an Orchard Spend to a transaction. + OrchardSpend(orchard::builder::SpendError), + /// An error occurred in adding an Orchard Output to a transaction. + OrchardRecipient(orchard::builder::OutputError), + /// The builder was constructed without support for the Sapling pool, but a Sapling + /// spend or output was added. + SaplingBuilderNotAvailable, + /// The builder was constructed with a target height before NU5 activation, but an Orchard + /// spend or output was added. + OrchardBuilderNotAvailable, + /// An error occurred in constructing the TZE parts of a transaction. + #[cfg(zcash_unstable = "zfuture")] + TzeBuild(tze::builder::Error), } -impl SaplingOutput

{ - pub fn new( - params: &P, - height: BlockHeight, - rng: &mut R, - ovk: Option, - to: PaymentAddress, - value: Amount, - memo: Option, - ) -> Result { - Self::new_internal(params, height, rng, ovk, to, value, memo) - } - - fn new_internal( - params: &P, - height: BlockHeight, - rng: &mut R, - ovk: Option, - to: PaymentAddress, - value: Amount, - memo: Option, - ) -> Result { - let g_d = to.g_d().ok_or(Error::InvalidAddress)?; - if value.is_negative() { - return Err(Error::InvalidAmount); +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Error::InsufficientFunds(amount) => write!( + f, + "Insufficient funds for transaction construction; need an additional {:?} zatoshis", + amount + ), + Error::ChangeRequired(amount) => write!( + f, + "The transaction requires an additional change output of {:?} zatoshis", + amount + ), + Error::Balance(e) => write!(f, "Invalid amount {:?}", e), + Error::Fee(e) => write!(f, "An error occurred in fee calculation: {}", e), + Error::TransparentBuild(err) => err.fmt(f), + Error::SaplingBuild(err) => err.fmt(f), + Error::OrchardBuild(err) => write!(f, "{:?}", err), + Error::OrchardSpend(err) => write!(f, "Could not add Orchard spend: {}", err), + Error::OrchardRecipient(err) => write!(f, "Could not add Orchard recipient: {}", err), + Error::SaplingBuilderNotAvailable => write!( + f, + "Cannot create Sapling transactions without a Sapling anchor" + ), + Error::OrchardBuilderNotAvailable => write!( + f, + "Cannot create Orchard transactions without an Orchard anchor, or before NU5 activation" + ), + #[cfg(zcash_unstable = "zfuture")] + Error::TzeBuild(err) => err.fmt(f), } + } +} - let rseed = generate_random_rseed_internal(params, height, rng); - - let note = Note { - g_d, - pk_d: *to.pk_d(), - value: value.into(), - rseed, - }; +#[cfg(feature = "std")] +impl std::error::Error for Error {} - Ok(SaplingOutput { - ovk, - to, - note, - memo: memo.unwrap_or_else(MemoBytes::empty), - _params: PhantomData::default(), - }) +impl From for Error { + fn from(e: BalanceError) -> Self { + Error::Balance(e) } +} - pub fn build( - self, - prover: &Pr, - ctx: &mut Pr::SaplingProvingContext, - rng: &mut R, - ) -> OutputDescription { - self.build_internal(prover, ctx, rng) +impl From> for Error { + fn from(e: FeeError) -> Self { + Error::Fee(e) } +} - fn build_internal( - self, - prover: &Pr, - ctx: &mut Pr::SaplingProvingContext, - rng: &mut R, - ) -> OutputDescription { - let encryptor = sapling_note_encryption::( - self.ovk, - self.note.clone(), - self.to.clone(), - self.memo, - rng, - ); - - let (zkproof, cv) = prover.output_proof( - ctx, - *encryptor.esk(), - self.to, - self.note.rcm(), - self.note.value, - ); - - let cmu = self.note.cmu(); - - let enc_ciphertext = encryptor.encrypt_note_plaintext(); - let out_ciphertext = encryptor.encrypt_outgoing_plaintext(&cv, &cmu, rng); - let ephemeral_key = *encryptor.epk(); - - OutputDescription { - cv, - cmu, - ephemeral_key, - enc_ciphertext, - out_ciphertext, - zkproof, - } +impl From for Error { + fn from(e: sapling::builder::Error) -> Self { + Error::SaplingBuild(e) } } -#[cfg(feature = "transparent-inputs")] -struct TransparentInputInfo { - sk: secp256k1::SecretKey, - pubkey: [u8; secp256k1::constants::PUBLIC_KEY_SIZE], - coin: TxOut, +impl From for Error { + fn from(e: orchard::builder::SpendError) -> Self { + Error::OrchardSpend(e) + } } -#[cfg(feature = "transparent-inputs")] -struct TransparentInputs { - secp: secp256k1::Secp256k1, - inputs: Vec, +/// Reports on the progress made by the builder towards building a transaction. +pub struct Progress { + /// The number of steps completed. + cur: u32, + /// The expected total number of steps (as of this progress update), if known. + end: Option, } -#[cfg(feature = "transparent-inputs")] -impl Default for TransparentInputs { - fn default() -> Self { - TransparentInputs { - secp: secp256k1::Secp256k1::gen_new(), - inputs: Default::default(), +impl From<(u32, u32)> for Progress { + fn from((cur, end): (u32, u32)) -> Self { + Self { + cur, + end: Some(end), } } } -#[cfg(not(feature = "transparent-inputs"))] -#[derive(Default)] -struct TransparentInputs; - -impl TransparentInputs { - #[cfg(feature = "transparent-inputs")] - fn push(&mut self, sk: secp256k1::SecretKey, coin: TxOut) -> Result<(), Error> { - if coin.value.is_negative() { - return Err(Error::InvalidAmount); - } - - // Ensure that the RIPEMD-160 digest of the public key associated with the - // provided secret key matches that of the address to which the provided - // output may be spent. - let pubkey = secp256k1::PublicKey::from_secret_key(&self.secp, &sk).serialize(); - match coin.script_pubkey.address() { - Some(TransparentAddress::PublicKey(hash)) => { - use ripemd160::Ripemd160; - use sha2::{Digest, Sha256}; - - if hash[..] != Ripemd160::digest(&Sha256::digest(&pubkey))[..] { - return Err(Error::InvalidAddress); - } - } - _ => return Err(Error::InvalidAddress), - } - - self.inputs.push(TransparentInputInfo { sk, pubkey, coin }); +impl Progress { + /// Returns the number of steps completed so far while building the transaction. + /// + /// Note that each step may not be of the same complexity/duration. + pub fn cur(&self) -> u32 { + self.cur + } - Ok(()) + /// Returns the total expected number of steps before this transaction will be ready, + /// or `None` if the end is unknown as of this progress update. + /// + /// Note that each step may not be of the same complexity/duration. + pub fn end(&self) -> Option { + self.end } +} - fn value_sum(&self) -> Amount { - #[cfg(feature = "transparent-inputs")] - { - self.inputs - .iter() - .map(|input| input.coin.value) - .sum::() - } +/// Rules for how the builder should be configured for each shielded pool. +#[derive(Clone, Copy)] +pub enum BuildConfig { + Standard { + sapling_anchor: Option, + orchard_anchor: Option, + }, + Coinbase, +} - #[cfg(not(feature = "transparent-inputs"))] - { - Amount::zero() +impl BuildConfig { + /// Returns the Sapling bundle type and anchor for this configuration. + pub fn sapling_builder_config( + &self, + ) -> Option<(sapling::builder::BundleType, sapling::Anchor)> { + match self { + BuildConfig::Standard { sapling_anchor, .. } => sapling_anchor + .as_ref() + .map(|a| (sapling::builder::BundleType::DEFAULT, *a)), + BuildConfig::Coinbase => Some(( + sapling::builder::BundleType::Coinbase, + sapling::Anchor::empty_tree(), + )), } } - #[cfg(feature = "transparent-inputs")] - fn apply_signatures( + /// Returns the Orchard bundle type and anchor for this configuration. + pub fn orchard_builder_config( &self, - mtx: &mut TransactionData, - consensus_branch_id: consensus::BranchId, - ) { - let mut sighash = [0u8; 32]; - for (i, info) in self.inputs.iter().enumerate() { - sighash.copy_from_slice(&signature_hash_data( - mtx, - consensus_branch_id, - SIGHASH_ALL, - SignableInput::transparent(i, &info.coin.script_pubkey, info.coin.value), - )); - - let msg = secp256k1::Message::from_slice(&sighash).expect("32 bytes"); - let sig = self.secp.sign(&msg, &info.sk); - - // Signature has to have "SIGHASH_ALL" appended to it - let mut sig_bytes: Vec = sig.serialize_der()[..].to_vec(); - sig_bytes.extend(&[SIGHASH_ALL as u8]); - - // P2PKH scriptSig - mtx.vin[i].script_sig = Script::default() << &sig_bytes[..] << &info.pubkey[..]; + ) -> Option<(orchard::builder::BundleType, orchard::Anchor)> { + match self { + BuildConfig::Standard { orchard_anchor, .. } => orchard_anchor + .as_ref() + .map(|a| (orchard::builder::BundleType::DEFAULT, *a)), + BuildConfig::Coinbase => Some(( + orchard::builder::BundleType::Coinbase, + orchard::Anchor::empty_tree(), + )), } } - - #[cfg(not(feature = "transparent-inputs"))] - fn apply_signatures(&self, _: &mut TransactionData, _: consensus::BranchId) {} } -#[cfg(feature = "zfuture")] -#[allow(clippy::type_complexity)] -struct TzeInputInfo<'a, BuildCtx> { - prevout: TzeOut, - builder: Box Result<(u32, Vec), Error> + 'a>, +/// The result of a transaction build operation, which includes the resulting transaction along +/// with metadata describing how spends and outputs were shuffled in creating the transaction's +/// shielded bundles. +#[derive(Debug)] +pub struct BuildResult { + transaction: Transaction, + sapling_meta: SaplingMetadata, + orchard_meta: orchard::builder::BundleMetadata, } -#[cfg(feature = "zfuture")] -struct TzeInputs<'a, BuildCtx> { - builders: Vec>, -} +impl BuildResult { + /// Returns the transaction that was constructed by the builder. + pub fn transaction(&self) -> &Transaction { + &self.transaction + } -#[cfg(feature = "zfuture")] -impl<'a, BuildCtx> TzeInputs<'a, BuildCtx> { - fn default() -> Self { - TzeInputs { builders: vec![] } + /// Returns the mapping from Sapling inputs and outputs to their randomized positions in the + /// Sapling bundle in the newly constructed transaction. + pub fn sapling_meta(&self) -> &SaplingMetadata { + &self.sapling_meta } - fn push(&mut self, tzeout: TzeOut, builder: WBuilder) - where - WBuilder: 'a + FnOnce(&BuildCtx) -> Result, - { - self.builders.push(TzeInputInfo { - prevout: tzeout, - builder: Box::new(move |ctx| builder(&ctx).map(|x| x.to_payload())), - }); + /// Returns the mapping from Orchard inputs and outputs to the randomized positions of the + /// Actions that contain them in the Orchard bundle in the newly constructed transaction. + pub fn orchard_meta(&self) -> &orchard::builder::BundleMetadata { + &self.orchard_meta } } -/// Metadata about a transaction created by a [`Builder`]. -#[derive(Debug, PartialEq)] -pub struct TransactionMetadata { - spend_indices: Vec, - output_indices: Vec, +/// The result of [`Builder::build_for_pczt`]. +/// +/// It includes the PCZT components along with metadata describing how spends and outputs +/// were shuffled in creating the transaction's shielded bundles. +#[derive(Debug)] +pub struct PcztResult { + pub pczt_parts: PcztParts

, + pub sapling_meta: SaplingMetadata, + pub orchard_meta: orchard::builder::BundleMetadata, } -impl TransactionMetadata { - fn new() -> Self { - TransactionMetadata { - spend_indices: vec![], - output_indices: vec![], - } +/// The components of a PCZT. +#[derive(Debug)] +pub struct PcztParts { + pub params: P, + pub version: TxVersion, + pub consensus_branch_id: BranchId, + pub lock_time: u32, + pub expiry_height: BlockHeight, + pub transparent: Option, + pub sapling: Option, + pub orchard: Option, +} + +/// Generates a [`Transaction`] from its inputs and outputs. +pub struct Builder<'a, P, U: sapling::builder::ProverProgress> { + params: P, + build_config: BuildConfig, + target_height: BlockHeight, + expiry_height: BlockHeight, + transparent_builder: TransparentBuilder, + sapling_builder: Option, + orchard_builder: Option, + #[cfg(zcash_unstable = "zfuture")] + tze_builder: TzeBuilder<'a, TransactionData>, + #[cfg(not(zcash_unstable = "zfuture"))] + tze_builder: core::marker::PhantomData<&'a ()>, + _progress_notifier: U, +} + +impl Builder<'_, P, U> { + /// Returns the network parameters that the builder has been configured for. + pub fn params(&self) -> &P { + &self.params } - /// Returns the index within the transaction of the [`SpendDescription`] corresponding - /// to the `n`-th call to [`Builder::add_sapling_spend`]. - /// - /// Note positions are randomized when building transactions for indistinguishability. - /// This means that the transaction consumer cannot assume that e.g. the first spend - /// they added (via the first call to [`Builder::add_sapling_spend`]) is the first - /// [`SpendDescription`] in the transaction. - pub fn spend_index(&self, n: usize) -> Option { - self.spend_indices.get(n).copied() + /// Returns the target height of the transaction under construction. + pub fn target_height(&self) -> BlockHeight { + self.target_height } - /// Returns the index within the transaction of the [`OutputDescription`] corresponding - /// to the `n`-th call to [`Builder::add_sapling_output`]. - /// - /// Note positions are randomized when building transactions for indistinguishability. - /// This means that the transaction consumer cannot assume that e.g. the first output - /// they added (via the first call to [`Builder::add_sapling_output`]) is the first - /// [`OutputDescription`] in the transaction. - pub fn output_index(&self, n: usize) -> Option { - self.output_indices.get(n).copied() + /// Returns the set of transparent inputs currently committed to be consumed + /// by the transaction. + #[cfg(feature = "transparent-inputs")] + pub fn transparent_inputs(&self) -> &[TransparentInputInfo] { + self.transparent_builder.inputs() } -} -/// Generates a [`Transaction`] from its inputs and outputs. -pub struct Builder<'a, P: consensus::Parameters, R: RngCore> { - params: P, - rng: R, - height: BlockHeight, - mtx: TransactionData, - fee: Amount, - anchor: Option, - spends: Vec, - outputs: Vec>, - transparent_inputs: TransparentInputs, - #[cfg(feature = "zfuture")] - tze_inputs: TzeInputs<'a, TransactionData>, - change_address: Option<(OutgoingViewingKey, PaymentAddress)>, - _phantom: &'a PhantomData

, -} + /// Returns the set of transparent outputs currently set to be produced by + /// the transaction. + pub fn transparent_outputs(&self) -> &[TxOut] { + self.transparent_builder.outputs() + } -impl<'a, P: consensus::Parameters> Builder<'a, P, OsRng> { - /// Creates a new `Builder` targeted for inclusion in the block with the given height, - /// using default values for general transaction fields and the default OS random. - /// - /// # Default values - /// - /// The expiry height will be set to the given height plus the default transaction - /// expiry delta (20 blocks). - /// - /// The fee will be set to the default fee (0.0001 ZEC). - pub fn new(params: P, height: BlockHeight) -> Self { - Builder::new_with_rng(params, height, OsRng) + /// Returns the set of Sapling inputs currently committed to be consumed + /// by the transaction. + pub fn sapling_inputs(&self) -> &[sapling::builder::SpendInfo] { + self.sapling_builder + .as_ref() + .map_or_else(|| &[][..], |b| b.inputs()) } - /// Creates a new `Builder` targeted for inclusion in the block with the given height, - /// using default values for general transaction fields and the default OS random, - /// and the `ZFUTURE_TX_VERSION` and `ZFUTURE_VERSION_GROUP_ID` version identifiers. - /// - /// # Default values - /// - /// The expiry height will be set to the given height plus the default transaction - /// expiry delta (20 blocks). - /// - /// The fee will be set to the default fee (0.0001 ZEC). - /// - /// The transaction will be constructed and serialized according to the - /// NetworkUpgrade::ZFuture rules. This is intended only for use in - /// integration testing of new features. - #[cfg(feature = "zfuture")] - pub fn new_zfuture(params: P, height: BlockHeight) -> Self { - Builder::new_with_rng_zfuture(params, height, OsRng) + /// Returns the set of Sapling outputs currently set to be produced by + /// the transaction. + pub fn sapling_outputs(&self) -> &[sapling::builder::OutputInfo] { + self.sapling_builder + .as_ref() + .map_or_else(|| &[][..], |b| b.outputs()) } } -impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> Builder<'a, P, R> { - /// Creates a new `Builder` targeted for inclusion in the block with the given height - /// and randomness source, using default values for general transaction fields. +impl<'a, P: consensus::Parameters> Builder<'a, P, ()> { + /// Creates a new `Builder` targeted for inclusion in the block with the given height, + /// using default values for general transaction fields. /// /// # Default values /// /// The expiry height will be set to the given height plus the default transaction /// expiry delta (20 blocks). - /// - /// The fee will be set to the default fee (0.0001 ZEC). - pub fn new_with_rng(params: P, height: BlockHeight, rng: R) -> Builder<'a, P, R> { - Self::new_with_mtx(params, height, rng, TransactionData::new()) + pub fn new(params: P, target_height: BlockHeight, build_config: BuildConfig) -> Self { + let orchard_builder = if params.is_nu_active(NetworkUpgrade::Nu5, target_height) { + build_config + .orchard_builder_config() + .map(|(bundle_type, anchor)| orchard::builder::Builder::new(bundle_type, anchor)) + } else { + None + }; + + let sapling_builder = build_config + .sapling_builder_config() + .map(|(bundle_type, anchor)| { + sapling::builder::Builder::new( + zip212_enforcement(¶ms, target_height), + bundle_type, + anchor, + ) + }); + + Builder { + params, + build_config, + target_height, + expiry_height: target_height + DEFAULT_TX_EXPIRY_DELTA, + transparent_builder: TransparentBuilder::empty(), + sapling_builder, + orchard_builder, + #[cfg(zcash_unstable = "zfuture")] + tze_builder: TzeBuilder::empty(), + #[cfg(not(zcash_unstable = "zfuture"))] + tze_builder: core::marker::PhantomData, + _progress_notifier: (), + } } - /// Creates a new `Builder` targeted for inclusion in the block with the given height, - /// and randomness source, using default values for general transaction fields - /// and the `ZFUTURE_TX_VERSION` and `ZFUTURE_VERSION_GROUP_ID` version identifiers. - /// - /// # Default values - /// - /// The expiry height will be set to the given height plus the default transaction - /// expiry delta (20 blocks). + /// Sets the notifier channel, where progress of building the transaction is sent. /// - /// The fee will be set to the default fee (0.0001 ZEC). - /// - /// The transaction will be constructed and serialized according to the - /// NetworkUpgrade::ZFuture rules. This is intended only for use in - /// integration testing of new features. - #[cfg(feature = "zfuture")] - pub fn new_with_rng_zfuture(params: P, height: BlockHeight, rng: R) -> Builder<'a, P, R> { - Self::new_with_mtx(params, height, rng, TransactionData::zfuture()) + /// An update is sent after every Sapling Spend or Output is computed, and the `u32` + /// sent represents the total steps completed so far. It will eventually send number + /// of spends + outputs. If there's an error building the transaction, the channel is + /// closed. + #[cfg(feature = "std")] + pub fn with_progress_notifier( + self, + _progress_notifier: Sender, + ) -> Builder<'a, P, Sender> { + Builder { + params: self.params, + build_config: self.build_config, + target_height: self.target_height, + expiry_height: self.expiry_height, + transparent_builder: self.transparent_builder, + sapling_builder: self.sapling_builder, + orchard_builder: self.orchard_builder, + tze_builder: self.tze_builder, + _progress_notifier, + } } } -impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { - /// Common utility function for builder construction. +impl Builder<'_, P, U> { + /// Adds an Orchard note to be spent in this bundle. /// - /// WARNING: THIS MUST REMAIN PRIVATE AS IT ALLOWS CONSTRUCTION - /// OF BUILDERS WITH NON-CryptoRng RNGs - fn new_with_mtx( - params: P, - height: BlockHeight, - rng: R, - mut mtx: TransactionData, - ) -> Builder<'a, P, R> { - mtx.expiry_height = height + DEFAULT_TX_EXPIRY_DELTA; - - Builder { - params, - rng, - height, - mtx, - fee: DEFAULT_FEE, - anchor: None, - spends: vec![], - outputs: vec![], - transparent_inputs: TransparentInputs::default(), - #[cfg(feature = "zfuture")] - tze_inputs: TzeInputs::default(), - change_address: None, - _phantom: &PhantomData, + /// Returns an error if the given Merkle path does not have the required anchor for + /// the given note. + pub fn add_orchard_spend( + &mut self, + fvk: orchard::keys::FullViewingKey, + note: orchard::Note, + merkle_path: orchard::tree::MerklePath, + ) -> Result<(), Error> { + if let Some(builder) = self.orchard_builder.as_mut() { + builder.add_spend(fvk, note, merkle_path)?; + Ok(()) + } else { + Err(Error::OrchardBuilderNotAvailable) } } + /// Adds an Orchard recipient to the transaction. + pub fn add_orchard_output( + &mut self, + ovk: Option, + recipient: orchard::Address, + value: u64, + memo: MemoBytes, + ) -> Result<(), Error> { + self.orchard_builder + .as_mut() + .ok_or(Error::OrchardBuilderNotAvailable)? + .add_output( + ovk, + recipient, + orchard::value::NoteValue::from_raw(value), + Some(*memo.as_array()), + ) + .map_err(Error::OrchardRecipient) + } + /// Adds a Sapling note to be spent in this transaction. /// /// Returns an error if the given Merkle path does not have the same anchor as the /// paths for previous Sapling notes. - pub fn add_sapling_spend( + pub fn add_sapling_spend( &mut self, - extsk: ExtendedSpendingKey, - diversifier: Diversifier, + fvk: sapling::keys::FullViewingKey, note: Note, - merkle_path: MerklePath, - ) -> Result<(), Error> { - // Consistency check: all anchors must equal the first one - let cmu = Node::new(note.cmu().into()); - if let Some(anchor) = self.anchor { - let path_root: bls12_381::Scalar = merkle_path.root(cmu).into(); - if path_root != anchor { - return Err(Error::AnchorMismatch); - } + merkle_path: sapling::MerklePath, + ) -> Result<(), Error> { + if let Some(builder) = self.sapling_builder.as_mut() { + builder.add_spend(fvk, note, merkle_path)?; + Ok(()) } else { - self.anchor = Some(merkle_path.root(cmu).into()) + Err(Error::SaplingBuilderNotAvailable) } - - let alpha = jubjub::Fr::random(&mut self.rng); - - self.mtx.value_balance += Amount::from_u64(note.value).map_err(|_| Error::InvalidAmount)?; - - self.spends.push(SpendDescriptionInfo { - extsk, - diversifier, - note, - alpha, - merkle_path, - }); - - Ok(()) } /// Adds a Sapling address to send funds to. - pub fn add_sapling_output( + pub fn add_sapling_output( &mut self, - ovk: Option, + ovk: Option, to: PaymentAddress, - value: Amount, - memo: Option, - ) -> Result<(), Error> { - let output = SaplingOutput::new_internal( - &self.params, - self.height, - &mut self.rng, - ovk, - to, - value, - memo, - )?; - - self.mtx.value_balance -= value; - - self.outputs.push(output); - - Ok(()) + value: Zatoshis, + memo: MemoBytes, + ) -> Result<(), Error> { + self.sapling_builder + .as_mut() + .ok_or(Error::SaplingBuilderNotAvailable)? + .add_output( + ovk, + to, + sapling::value::NoteValue::from_raw(value.into()), + Some(*memo.as_array()), + ) + .map_err(Error::SaplingBuild) } /// Adds a transparent coin to be spent in this transaction. #[cfg(feature = "transparent-inputs")] - #[cfg_attr(docsrs, doc(cfg(feature = "transparent-inputs")))] pub fn add_transparent_input( &mut self, - sk: secp256k1::SecretKey, - utxo: OutPoint, + pubkey: secp256k1::PublicKey, + utxo: transparent::bundle::OutPoint, coin: TxOut, - ) -> Result<(), Error> { - self.transparent_inputs.push(sk, coin)?; - self.mtx.vin.push(TxIn::new(utxo)); - Ok(()) + ) -> Result<(), transparent::builder::Error> { + self.transparent_builder.add_input(pubkey, utxo, coin) } /// Adds a transparent address to send funds to. pub fn add_transparent_output( &mut self, to: &TransparentAddress, - value: Amount, - ) -> Result<(), Error> { - if value.is_negative() { - return Err(Error::InvalidAmount); - } - - self.mtx.vout.push(TxOut { - value, - script_pubkey: to.script(), - }); + value: Zatoshis, + ) -> Result<(), transparent::builder::Error> { + self.transparent_builder.add_output(to, value) + } - Ok(()) + /// Returns the sum of the transparent, Sapling, Orchard, and TZE value balances. + fn value_balance(&self) -> Result { + let value_balances = [ + self.transparent_builder.value_balance()?, + self.sapling_builder + .as_ref() + .map_or_else(ZatBalance::zero, |builder| { + builder.value_balance::() + }), + self.orchard_builder.as_ref().map_or_else( + || Ok(ZatBalance::zero()), + |builder| { + builder + .value_balance::() + .map_err(|_| BalanceError::Overflow) + }, + )?, + #[cfg(zcash_unstable = "zfuture")] + self.tze_builder.value_balance()?, + ]; + + value_balances + .into_iter() + .sum::>() + .ok_or(BalanceError::Overflow) } - /// Sets the Sapling address to which any change will be sent. + /// Reports the calculated fee given the specified fee rule. /// - /// By default, change is sent to the Sapling address corresponding to the first note - /// being spent (i.e. the first call to [`Builder::add_sapling_spend`]). - pub fn send_change_to(&mut self, ovk: OutgoingViewingKey, to: PaymentAddress) { - self.change_address = Some((ovk, to)); + /// This fee is a function of the spends and outputs that have been added to the builder, + /// pursuant to the specified [`FeeRule`]. + pub fn get_fee(&self, fee_rule: &FR) -> Result> { + #[cfg(feature = "transparent-inputs")] + let transparent_inputs = self.transparent_builder.inputs(); + + #[cfg(not(feature = "transparent-inputs"))] + let transparent_inputs: &[Infallible] = &[]; + + let sapling_spends = self + .sapling_builder + .as_ref() + .map_or(0, |builder| builder.inputs().len()); + + fee_rule + .fee_required( + &self.params, + self.target_height, + transparent_inputs.iter().map(|i| i.serialized_size()), + self.transparent_builder + .outputs() + .iter() + .map(|i| i.serialized_size()), + sapling_spends, + self.sapling_builder + .as_ref() + .zip(self.build_config.sapling_builder_config()) + .map_or(Ok(0), |(builder, (bundle_type, _))| { + bundle_type + .num_outputs(sapling_spends, builder.outputs().len()) + .map_err(FeeError::Bundle) + })?, + self.orchard_builder + .as_ref() + .zip(self.build_config.orchard_builder_config()) + .map_or(Ok(0), |(builder, (bundle_type, _))| { + bundle_type + .num_actions(builder.spends().len(), builder.outputs().len()) + .map_err(FeeError::Bundle) + })?, + ) + .map_err(FeeError::FeeRule) + } + + #[cfg(zcash_unstable = "zfuture")] + pub fn get_fee_zfuture( + &self, + fee_rule: &FR, + ) -> Result> { + #[cfg(feature = "transparent-inputs")] + let transparent_inputs = self.transparent_builder.inputs(); + + #[cfg(not(feature = "transparent-inputs"))] + let transparent_inputs: &[Infallible] = &[]; + + let sapling_spends = self + .sapling_builder + .as_ref() + .map_or(0, |builder| builder.inputs().len()); + + fee_rule + .fee_required_zfuture( + &self.params, + self.target_height, + transparent_inputs.iter().map(|i| i.serialized_size()), + self.transparent_builder + .outputs() + .iter() + .map(|i| i.serialized_size()), + sapling_spends, + self.sapling_builder + .as_ref() + .zip(self.build_config.sapling_builder_config()) + .map_or(Ok(0), |(builder, (bundle_type, _))| { + bundle_type + .num_outputs(sapling_spends, builder.outputs().len()) + .map_err(FeeError::Bundle) + })?, + self.orchard_builder + .as_ref() + .zip(self.build_config.orchard_builder_config()) + .map_or(Ok(0), |(builder, (bundle_type, _))| { + bundle_type + .num_actions(builder.spends().len(), builder.outputs().len()) + .map_err(FeeError::Bundle) + })?, + self.tze_builder.inputs(), + self.tze_builder.outputs(), + ) + .map_err(FeeError::FeeRule) } /// Builds a transaction from the configured spends and outputs. /// /// Upon success, returns a tuple containing the final transaction, and the - /// [`TransactionMetadata`] generated during the build process. + /// [`SaplingMetadata`] generated during the build process. + #[allow(clippy::too_many_arguments)] + #[cfg(feature = "circuits")] + pub fn build( + self, + transparent_signing_set: &TransparentSigningSet, + sapling_extsks: &[sapling::zip32::ExtendedSpendingKey], + orchard_saks: &[orchard::keys::SpendAuthorizingKey], + rng: R, + spend_prover: &SP, + output_prover: &OP, + fee_rule: &FR, + ) -> Result> { + let fee = self.get_fee(fee_rule).map_err(Error::Fee)?; + self.build_internal( + transparent_signing_set, + sapling_extsks, + orchard_saks, + rng, + spend_prover, + output_prover, + fee, + ) + } + + /// Builds a transaction from the configured spends and outputs. /// - /// `consensus_branch_id` must be valid for the block height that this transaction is - /// targeting. An invalid `consensus_branch_id` will *not* result in an error from - /// this function, and instead will generate a transaction that will be rejected by - /// the network. - pub fn build( - mut self, - consensus_branch_id: consensus::BranchId, - prover: &impl TxProver, - ) -> Result<(Transaction, TransactionMetadata), Error> { - let mut tx_metadata = TransactionMetadata::new(); + /// Upon success, returns a tuple containing the final transaction, and the + /// [`SaplingMetadata`] generated during the build process. + #[cfg(zcash_unstable = "zfuture")] + pub fn build_zfuture< + R: RngCore + CryptoRng, + SP: SpendProver, + OP: OutputProver, + FR: FutureFeeRule, + >( + self, + transparent_signing_set: &TransparentSigningSet, + sapling_extsks: &[sapling::zip32::ExtendedSpendingKey], + orchard_saks: &[orchard::keys::SpendAuthorizingKey], + rng: R, + spend_prover: &SP, + output_prover: &OP, + fee_rule: &FR, + ) -> Result> { + let fee = self.get_fee_zfuture(fee_rule).map_err(Error::Fee)?; + self.build_internal( + transparent_signing_set, + sapling_extsks, + orchard_saks, + rng, + spend_prover, + output_prover, + fee, + ) + } + + #[allow(clippy::too_many_arguments)] + #[cfg(feature = "circuits")] + fn build_internal( + self, + transparent_signing_set: &TransparentSigningSet, + sapling_extsks: &[sapling::zip32::ExtendedSpendingKey], + orchard_saks: &[orchard::keys::SpendAuthorizingKey], + mut rng: R, + spend_prover: &SP, + output_prover: &OP, + fee: Zatoshis, + ) -> Result> { + let consensus_branch_id = BranchId::for_height(&self.params, self.target_height); + + // determine transaction version + let version = TxVersion::suggested_for_branch(consensus_branch_id); // // Consistency checks // - // Valid change - let change = self.mtx.value_balance - self.fee + self.transparent_inputs.value_sum() - - self.mtx.vout.iter().map(|vo| vo.value).sum::(); - - #[cfg(feature = "zfuture")] - let change = change - + self - .tze_inputs - .builders - .iter() - .map(|ein| ein.prevout.value) - .sum::() - - self - .mtx - .tze_outputs - .iter() - .map(|tzo| tzo.value) - .sum::(); - - if change.is_negative() { - return Err(Error::ChangeIsNegative(change)); - } + // After fees are accounted for, the value balance of the transaction must be zero. + let balance_after_fees = + (self.value_balance()? - fee.into()).ok_or(BalanceError::Underflow)?; - // - // Change output - // + match balance_after_fees.cmp(&ZatBalance::zero()) { + Ordering::Less => { + return Err(Error::InsufficientFunds(-balance_after_fees)); + } + Ordering::Greater => { + return Err(Error::ChangeRequired(balance_after_fees)); + } + Ordering::Equal => (), + }; - if change.is_positive() { - // Send change to the specified change address. If no change address - // was set, send change to the first Sapling address given as input. - let change_address = if let Some(change_address) = self.change_address.take() { - change_address - } else if !self.spends.is_empty() { - ( - self.spends[0].extsk.expsk.ovk, - PaymentAddress::from_parts( - self.spends[0].diversifier, - self.spends[0].note.pk_d, - ) - .ok_or(Error::InvalidAddress)?, - ) - } else { - return Err(Error::NoChangeAddress); - }; + let transparent_bundle = self.transparent_builder.build(); + + let (sapling_bundle, sapling_meta) = match self + .sapling_builder + .and_then(|builder| { + builder + .build::(sapling_extsks, &mut rng) + .map_err(Error::SaplingBuild) + .transpose() + .map(|res| { + res.map(|(bundle, sapling_meta)| { + // We need to create proofs before signatures, because we still support + // creating V4 transactions, which commit to the Sapling proofs in the + // transaction digest. + ( + bundle.create_proofs( + spend_prover, + output_prover, + &mut rng, + self._progress_notifier, + ), + sapling_meta, + ) + }) + }) + }) + .transpose()? + { + Some((bundle, meta)) => (Some(bundle), meta), + None => (None, SaplingMetadata::empty()), + }; - self.add_sapling_output(Some(change_address.0), change_address.1, change, None)?; - } + let (orchard_bundle, orchard_meta) = match self + .orchard_builder + .and_then(|builder| { + builder + .build(&mut rng) + .map_err(Error::OrchardBuild) + .transpose() + }) + .transpose()? + { + Some((bundle, meta)) => (Some(bundle), meta), + None => (None, orchard::builder::BundleMetadata::empty()), + }; - // - // Record initial positions of spends and outputs - // - let mut spends: Vec<_> = self.spends.into_iter().enumerate().collect(); - let mut outputs: Vec<_> = self - .outputs - .into_iter() - .enumerate() - .map(|(i, o)| Some((i, o))) - .collect(); + #[cfg(zcash_unstable = "zfuture")] + let (tze_bundle, tze_signers) = self.tze_builder.build(); + + let unauthed_tx: TransactionData = TransactionData { + version, + consensus_branch_id: BranchId::for_height(&self.params, self.target_height), + lock_time: 0, + expiry_height: self.expiry_height, + transparent_bundle, + sprout_bundle: None, + sapling_bundle, + orchard_bundle, + #[cfg(zcash_unstable = "zfuture")] + tze_bundle, + }; // - // Sapling spends and outputs + // Signatures -- everything but the signatures must already have been added. // + let txid_parts = unauthed_tx.digest(TxIdDigester); + + let transparent_bundle = unauthed_tx + .transparent_bundle + .clone() + .map(|b| { + b.apply_signatures( + |input| { + *signature_hash( + &unauthed_tx, + &SignableInput::Transparent(input), + &txid_parts, + ) + .as_ref() + }, + transparent_signing_set, + ) + }) + .transpose() + .map_err(Error::TransparentBuild)?; + + #[cfg(zcash_unstable = "zfuture")] + let tze_bundle = unauthed_tx + .tze_bundle + .clone() + .map(|b| b.into_authorized(&unauthed_tx, tze_signers)) + .transpose() + .map_err(Error::TzeBuild)?; + + // the commitment being signed is shared across all Sapling inputs; once + // V4 transactions are deprecated this should just be the txid, but + // for now we need to continue to compute it here. + let shielded_sig_commitment = + signature_hash(&unauthed_tx, &SignableInput::Shielded, &txid_parts); + + let sapling_asks = sapling_extsks + .iter() + .map(|extsk| extsk.expsk.ask.clone()) + .collect::>(); + let sapling_bundle = unauthed_tx + .sapling_bundle + .map(|b| b.apply_signatures(&mut rng, *shielded_sig_commitment.as_ref(), &sapling_asks)) + .transpose() + .map_err(Error::SaplingBuild)?; + + let orchard_bundle = unauthed_tx + .orchard_bundle + .map(|b| { + b.create_proof(&orchard::circuit::ProvingKey::build(), &mut rng) + .and_then(|b| { + b.apply_signatures( + &mut rng, + *shielded_sig_commitment.as_ref(), + orchard_saks, + ) + }) + }) + .transpose() + .map_err(Error::OrchardBuild)?; + + let authorized_tx = TransactionData { + version: unauthed_tx.version, + consensus_branch_id: unauthed_tx.consensus_branch_id, + lock_time: unauthed_tx.lock_time, + expiry_height: unauthed_tx.expiry_height, + transparent_bundle, + sprout_bundle: unauthed_tx.sprout_bundle, + sapling_bundle, + orchard_bundle, + #[cfg(zcash_unstable = "zfuture")] + tze_bundle, + }; - let mut ctx = prover.new_sapling_proving_context(); - - // Pad Sapling outputs - let orig_outputs_len = outputs.len(); - if !spends.is_empty() { - while outputs.len() < MIN_SHIELDED_OUTPUTS { - outputs.push(None); - } - } + // The unwrap() here is safe because the txid hashing + // of freeze() should be infalliable. + Ok(BuildResult { + transaction: authorized_tx.freeze().unwrap(), + sapling_meta, + orchard_meta, + }) + } - // Randomize order of inputs and outputs - spends.shuffle(&mut self.rng); - outputs.shuffle(&mut self.rng); - tx_metadata.spend_indices.resize(spends.len(), 0); - tx_metadata.output_indices.resize(orig_outputs_len, 0); - - // Record if we'll need a binding signature - let binding_sig_needed = !spends.is_empty() || !outputs.is_empty(); - - // Create Sapling SpendDescriptions - if !spends.is_empty() { - let anchor = self.anchor.expect("anchor was set if spends were added"); - - for (i, (pos, spend)) in spends.iter().enumerate() { - let proof_generation_key = spend.extsk.expsk.proof_generation_key(); - - let nullifier = spend.note.nf( - &proof_generation_key.to_viewing_key(), - spend.merkle_path.position, - ); - - let (zkproof, cv, rk) = prover - .spend_proof( - &mut ctx, - proof_generation_key, - spend.diversifier, - spend.note.rseed, - spend.alpha, - spend.note.value, - anchor, - spend.merkle_path.clone(), - ) - .map_err(|()| Error::SpendProof)?; - - self.mtx.shielded_spends.push(SpendDescription { - cv, - anchor, - nullifier, - rk, - zkproof, - spend_auth_sig: None, - }); - - // Record the post-randomized spend location - tx_metadata.spend_indices[*pos] = i; - } - } + /// Builds a PCZT from the configured spends and outputs. + /// + /// Upon success, returns a struct containing the PCZT components, and the + /// [`SaplingMetadata`] and [`orchard::builder::BundleMetadata`] generated during the + /// build process. + pub fn build_for_pczt( + self, + mut rng: R, + fee_rule: &FR, + ) -> Result, Error> { + let fee = self.get_fee(fee_rule).map_err(Error::Fee)?; + let consensus_branch_id = BranchId::for_height(&self.params, self.target_height); - // Create Sapling OutputDescriptions - for (i, output) in outputs.into_iter().enumerate() { - let output_desc = if let Some((pos, output)) = output { - // Record the post-randomized output location - tx_metadata.output_indices[pos] = i; - - output.build_internal(prover, &mut ctx, &mut self.rng) - } else { - // This is a dummy output - let (dummy_to, dummy_note) = { - let (diversifier, g_d) = { - let mut diversifier; - let g_d; - loop { - let mut d = [0; 11]; - self.rng.fill_bytes(&mut d); - diversifier = Diversifier(d); - if let Some(val) = diversifier.g_d() { - g_d = val; - break; - } - } - (diversifier, g_d) - }; - - let (pk_d, payment_address) = loop { - let dummy_ivk = jubjub::Fr::random(&mut self.rng); - let pk_d = g_d * dummy_ivk; - if let Some(addr) = PaymentAddress::from_parts(diversifier, pk_d) { - break (pk_d, addr); - } - }; - - let rseed = - generate_random_rseed_internal(&self.params, self.height, &mut self.rng); - - ( - payment_address, - Note { - g_d, - pk_d, - rseed, - value: 0, - }, - ) - }; - - let esk = dummy_note.generate_or_derive_esk_internal(&mut self.rng); - let epk = dummy_note.g_d * esk; - - let (zkproof, cv) = prover.output_proof( - &mut ctx, - esk, - dummy_to, - dummy_note.rcm(), - dummy_note.value, - ); - - let cmu = dummy_note.cmu(); - - let mut enc_ciphertext = [0u8; 580]; - let mut out_ciphertext = [0u8; 80]; - self.rng.fill_bytes(&mut enc_ciphertext[..]); - self.rng.fill_bytes(&mut out_ciphertext[..]); - - OutputDescription { - cv, - cmu, - ephemeral_key: epk.into(), - enc_ciphertext, - out_ciphertext, - zkproof, - } - }; + // determine transaction version + let version = TxVersion::suggested_for_branch(consensus_branch_id); - self.mtx.shielded_outputs.push(output_desc); - } + let consensus_branch_id = BranchId::for_height(&self.params, self.target_height); // - // Signatures -- everything but the signatures must already have been added. + // Consistency checks // - let mut sighash = [0u8; 32]; - sighash.copy_from_slice(&signature_hash_data( - &self.mtx, - consensus_branch_id, - SIGHASH_ALL, - SignableInput::Shielded, - )); - - // Create Sapling spendAuth and binding signatures - for (i, (_, spend)) in spends.into_iter().enumerate() { - self.mtx.shielded_spends[i].spend_auth_sig = Some(spend_sig_internal( - PrivateKey(spend.extsk.expsk.ask), - spend.alpha, - &sighash, - &mut self.rng, - )); - } + // After fees are accounted for, the value balance of the transaction must be zero. + let balance_after_fees = + (self.value_balance()? - fee.into()).ok_or(BalanceError::Underflow)?; - // Add a binding signature if needed - self.mtx.binding_sig = if binding_sig_needed { - Some( - prover - .binding_sig(&mut ctx, self.mtx.value_balance, &sighash) - .map_err(|_| Error::BindingSig)?, - ) - } else { - None + match balance_after_fees.cmp(&ZatBalance::zero()) { + Ordering::Less => { + return Err(Error::InsufficientFunds(-balance_after_fees)); + } + Ordering::Greater => { + return Err(Error::ChangeRequired(balance_after_fees)); + } + Ordering::Equal => (), }; - // Create TZE input witnesses - #[cfg(feature = "zfuture")] - for (i, tze_in) in self.tze_inputs.builders.into_iter().enumerate() { - // The witness builder function should have cached/closed over whatever data was necessary for the - // witness to commit to at the time it was added to the transaction builder; here, it then computes those - // commitments. - let (mode, payload) = (tze_in.builder)(&self.mtx)?; - let mut current = self.mtx.tze_inputs.get_mut(i).unwrap(); - if mode != current.witness.mode { - return Err(Error::TzeWitnessModeMismatch(current.witness.mode, mode)); - } + let transparent_bundle = self.transparent_builder.build_for_pczt(); - current.witness.payload = payload; - } + let (sapling_bundle, sapling_meta) = match self + .sapling_builder + .map(|builder| { + builder + .build_for_pczt(&mut rng) + .map_err(Error::SaplingBuild) + }) + .transpose()? + { + Some((bundle, meta)) => (Some(bundle), meta), + None => (None, SaplingMetadata::empty()), + }; - // Transparent signatures - self.transparent_inputs - .apply_signatures(&mut self.mtx, consensus_branch_id); + let (orchard_bundle, orchard_meta) = match self + .orchard_builder + .map(|builder| { + builder + .build_for_pczt(&mut rng) + .map_err(Error::OrchardBuild) + }) + .transpose()? + { + Some((bundle, meta)) => (Some(bundle), meta), + None => (None, orchard::builder::BundleMetadata::empty()), + }; - Ok(( - self.mtx.freeze().expect("Transaction should be complete"), - tx_metadata, - )) + Ok(PcztResult { + pczt_parts: PcztParts { + params: self.params, + version, + consensus_branch_id, + lock_time: 0, + expiry_height: self.expiry_height, + transparent: transparent_bundle, + sapling: sapling_bundle, + orchard: orchard_bundle, + }, + sapling_meta, + orchard_meta, + }) } } -#[cfg(feature = "zfuture")] -impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> ExtensionTxBuilder<'a> - for Builder<'a, P, R> +#[cfg(zcash_unstable = "zfuture")] +impl<'a, P: consensus::Parameters, U: sapling::builder::ProverProgress> ExtensionTxBuilder<'a> + for Builder<'a, P, U> { - type BuildCtx = TransactionData; - type BuildError = Error; + type BuildCtx = TransactionData; + type BuildError = tze::builder::Error; fn add_tze_input( &mut self, extension_id: u32, mode: u32, - (outpoint, prevout): (TzeOutPoint, TzeOut), + prevout: (tze::OutPoint, TzeOut), witness_builder: WBuilder, ) -> Result<(), Self::BuildError> where - WBuilder: 'a + (FnOnce(&Self::BuildCtx) -> Result), + WBuilder: 'a + (FnOnce(&Self::BuildCtx) -> Result), { - self.mtx - .tze_inputs - .push(TzeIn::new(outpoint, extension_id, mode)); - self.tze_inputs.push(prevout, witness_builder); + self.tze_builder + .add_input(extension_id, mode, prevout, witness_builder); + Ok(()) } fn add_tze_output( &mut self, extension_id: u32, - value: Amount, + value: Zatoshis, guarded_by: &G, ) -> Result<(), Self::BuildError> { - if value.is_negative() { - return Err(Error::InvalidAmount); - } - - let (mode, payload) = guarded_by.to_payload(); - self.mtx.tze_outputs.push(TzeOut { - value, - precondition: tze::Precondition { - extension_id, - mode, - payload, - }, - }); - + self.tze_builder.add_output(extension_id, value, guarded_by); Ok(()) } } #[cfg(any(test, feature = "test-dependencies"))] -impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { - /// Creates a new `Builder` targeted for inclusion in the block with the given height - /// and randomness source, using default values for general transaction fields. - /// - /// # Default values - /// - /// The expiry height will be set to the given height plus the default transaction - /// expiry delta (20 blocks). - /// - /// The fee will be set to the default fee (0.0001 ZEC). - /// - /// WARNING: DO NOT USE IN PRODUCTION - pub fn test_only_new_with_rng(params: P, height: BlockHeight, rng: R) -> Builder<'a, P, R> { - Self::new_with_mtx(params, height, rng, TransactionData::new()) - } +mod testing { + use rand::RngCore; + use rand_core::CryptoRng; + + use ::sapling::prover::mock::{MockOutputProver, MockSpendProver}; + use ::transparent::builder::TransparentSigningSet; + use zcash_protocol::consensus; + + use super::{BuildResult, Builder, Error}; + use crate::transaction::fees::zip317; + + impl Builder<'_, P, U> { + /// Build the transaction using mocked randomness and proving capabilities. + /// DO NOT USE EXCEPT FOR UNIT TESTING. + pub fn mock_build( + self, + transparent_signing_set: &TransparentSigningSet, + sapling_extsks: &[sapling::zip32::ExtendedSpendingKey], + orchard_saks: &[orchard::keys::SpendAuthorizingKey], + rng: R, + ) -> Result> { + struct FakeCryptoRng(R); + impl CryptoRng for FakeCryptoRng {} + impl RngCore for FakeCryptoRng { + fn next_u32(&mut self) -> u32 { + self.0.next_u32() + } - /// Creates a new `Builder` targeted for inclusion in the block with the given height, - /// and randomness source, using default values for general transaction fields - /// and the `ZFUTURE_TX_VERSION` and `ZFUTURE_VERSION_GROUP_ID` version identifiers. - /// - /// # Default values - /// - /// The expiry height will be set to the given height plus the default transaction - /// expiry delta (20 blocks). - /// - /// The fee will be set to the default fee (0.0001 ZEC). - /// - /// The transaction will be constructed and serialized according to the - /// NetworkUpgrade::ZFuture rules. This is intended only for use in - /// integration testing of new features. - /// - /// WARNING: DO NOT USE IN PRODUCTION - #[cfg(feature = "zfuture")] - pub fn test_only_new_with_rng_zfuture( - params: P, - height: BlockHeight, - rng: R, - ) -> Builder<'a, P, R> { - Self::new_with_mtx(params, height, rng, TransactionData::zfuture()) - } + fn next_u64(&mut self) -> u64 { + self.0.next_u64() + } - pub fn mock_build( - self, - consensus_branch_id: consensus::BranchId, - ) -> Result<(Transaction, TransactionMetadata), Error> { - self.build(consensus_branch_id, &MockTxProver) + fn fill_bytes(&mut self, dest: &mut [u8]) { + self.0.fill_bytes(dest) + } + + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> { + self.0.try_fill_bytes(dest) + } + } + + self.build( + transparent_signing_set, + sapling_extsks, + orchard_saks, + FakeCryptoRng(rng), + &MockSpendProver, + &MockOutputProver, + #[allow(deprecated)] + &zip317::FeeRule::standard(), + ) + } } } #[cfg(test)] mod tests { - use ff::{Field, PrimeField}; + use core::convert::Infallible; + + use assert_matches::assert_matches; + use ff::Field; + use incrementalmerkletree::{frontier::CommitmentTree, witness::IncrementalWitness}; use rand_core::OsRng; - use std::marker::PhantomData; - - use crate::{ - consensus::{self, Parameters, H0, TEST_NETWORK}, - legacy::TransparentAddress, - merkle_tree::{CommitmentTree, IncrementalWitness}, - sapling::{prover::mock::MockTxProver, Node, Rseed}, - transaction::components::{amount::Amount, amount::DEFAULT_FEE}, - zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, - }; use super::{Builder, Error}; + use crate::transaction::builder::BuildConfig; + + use ::sapling::{zip32::ExtendedSpendingKey, Node, Rseed}; + use ::transparent::{address::TransparentAddress, builder::TransparentSigningSet}; + use zcash_protocol::{ + consensus::{NetworkUpgrade, Parameters, TEST_NETWORK}, + memo::MemoBytes, + value::{BalanceError, ZatBalance, Zatoshis}, + }; - #[cfg(feature = "zfuture")] - use super::TzeInputs; + #[cfg(zcash_unstable = "zfuture")] + #[cfg(feature = "transparent-inputs")] + use super::TzeBuilder; - #[test] - fn fails_on_negative_output() { - let extsk = ExtendedSpendingKey::master(&[]); - let extfvk = ExtendedFullViewingKey::from(&extsk); - let ovk = extfvk.fvk.ovk; - let to = extfvk.default_address().unwrap().1; - - let mut builder = Builder::new(TEST_NETWORK, H0); - assert_eq!( - builder.add_sapling_output(Some(ovk), to, Amount::from_i64(-1).unwrap(), None), - Err(Error::InvalidAmount) - ); - } + #[cfg(feature = "transparent-inputs")] + use { + crate::transaction::{builder::DEFAULT_TX_EXPIRY_DELTA, OutPoint, TxOut}, + ::transparent::keys::{AccountPrivKey, IncomingViewingKey}, + zip32::AccountId, + }; + // This test only works with the transparent_inputs feature because we have to + // be able to create a tx with a valid balance, without using Sapling inputs. #[test] + #[cfg(feature = "transparent-inputs")] fn binding_sig_absent_if_no_shielded_spend_or_output() { - use crate::consensus::NetworkUpgrade; - use crate::transaction::{ - builder::{self, TransparentInputs}, - TransactionData, - }; + use crate::transaction::builder::{self, TransparentBuilder}; + use ::transparent::{builder::TransparentSigningSet, keys::NonHardenedChildIndex}; + use zcash_protocol::consensus::NetworkUpgrade; let sapling_activation_height = TEST_NETWORK .activation_height(NetworkUpgrade::Sapling) @@ -1005,207 +1102,277 @@ mod tests { // Create a builder with 0 fee, so we can construct t outputs let mut builder = builder::Builder { params: TEST_NETWORK, - rng: OsRng, - height: sapling_activation_height, - mtx: TransactionData::new(), - fee: Amount::zero(), - anchor: None, - spends: vec![], - outputs: vec![], - transparent_inputs: TransparentInputs::default(), - #[cfg(feature = "zfuture")] - tze_inputs: TzeInputs::default(), - change_address: None, - _phantom: &PhantomData, + build_config: BuildConfig::Standard { + sapling_anchor: Some(sapling::Anchor::empty_tree()), + orchard_anchor: Some(orchard::Anchor::empty_tree()), + }, + target_height: sapling_activation_height, + expiry_height: sapling_activation_height + DEFAULT_TX_EXPIRY_DELTA, + transparent_builder: TransparentBuilder::empty(), + sapling_builder: None, + #[cfg(zcash_unstable = "zfuture")] + tze_builder: TzeBuilder::empty(), + #[cfg(not(zcash_unstable = "zfuture"))] + tze_builder: core::marker::PhantomData, + _progress_notifier: (), + orchard_builder: None, }; + let mut transparent_signing_set = TransparentSigningSet::new(); + let tsk = AccountPrivKey::from_seed(&TEST_NETWORK, &[0u8; 32], AccountId::ZERO).unwrap(); + let sk = tsk + .derive_external_secret_key(NonHardenedChildIndex::ZERO) + .unwrap(); + let pubkey = transparent_signing_set.add_key(sk); + let prev_coin = TxOut { + value: Zatoshis::const_from_u64(50000), + script_pubkey: tsk + .to_account_pubkey() + .derive_external_ivk() + .unwrap() + .derive_address(NonHardenedChildIndex::ZERO) + .unwrap() + .script(), + }; + builder + .add_transparent_input(pubkey, OutPoint::fake(), prev_coin) + .unwrap(); + // Create a tx with only t output. No binding_sig should be present builder - .add_transparent_output(&TransparentAddress::PublicKey([0; 20]), Amount::zero()) + .add_transparent_output( + &TransparentAddress::PublicKeyHash([0; 20]), + Zatoshis::const_from_u64(40000), + ) .unwrap(); - let (tx, _) = builder - .build(consensus::BranchId::Sapling, &MockTxProver) + let res = builder + .mock_build(&transparent_signing_set, &[], &[], OsRng) .unwrap(); // No binding signature, because only t input and outputs - assert!(tx.binding_sig.is_none()); + assert!(res.transaction().sapling_bundle.is_none()); } #[test] fn binding_sig_present_if_shielded_spend() { let extsk = ExtendedSpendingKey::master(&[]); - let extfvk = ExtendedFullViewingKey::from(&extsk); - let to = extfvk.default_address().unwrap().1; + let dfvk = extsk.to_diversifiable_full_viewing_key(); + let to = dfvk.default_address().1; let mut rng = OsRng; - let note1 = to - .create_note(50000, Rseed::BeforeZip212(jubjub::Fr::random(&mut rng))) - .unwrap(); - let cmu1 = Node::new(note1.cmu().to_repr()); - let mut tree = CommitmentTree::empty(); + let note1 = to.create_note( + sapling::value::NoteValue::from_raw(50000), + Rseed::BeforeZip212(jubjub::Fr::random(&mut rng)), + ); + let cmu1 = Node::from_cmu(¬e1.cmu()); + let mut tree = CommitmentTree::::empty(); tree.append(cmu1).unwrap(); - let witness1 = IncrementalWitness::from_tree(&tree); + let witness1 = IncrementalWitness::from_tree(tree).unwrap(); - let mut builder = Builder::new(TEST_NETWORK, H0); + let tx_height = TEST_NETWORK + .activation_height(NetworkUpgrade::Sapling) + .unwrap(); + + let build_config = BuildConfig::Standard { + sapling_anchor: Some(witness1.root().into()), + orchard_anchor: None, + }; + let mut builder = Builder::new(TEST_NETWORK, tx_height, build_config); // Create a tx with a sapling spend. binding_sig should be present builder - .add_sapling_spend(extsk, *to.diversifier(), note1, witness1.path().unwrap()) + .add_sapling_spend::(dfvk.fvk().clone(), note1, witness1.path().unwrap()) .unwrap(); builder - .add_transparent_output(&TransparentAddress::PublicKey([0; 20]), Amount::zero()) + .add_transparent_output( + &TransparentAddress::PublicKeyHash([0; 20]), + Zatoshis::const_from_u64(35000), + ) .unwrap(); - // Expect a binding signature error, because our inputs aren't valid, but this shows - // that a binding signature was attempted - assert_eq!( - builder.build(consensus::BranchId::Sapling, &MockTxProver), - Err(Error::BindingSig) - ); - } - - #[test] - fn fails_on_negative_transparent_output() { - let mut builder = Builder::new(TEST_NETWORK, H0); - assert_eq!( - builder.add_transparent_output( - &TransparentAddress::PublicKey([0; 20]), - Amount::from_i64(-1).unwrap(), - ), - Err(Error::InvalidAmount) - ); + // A binding signature (and bundle) is present because there is a Sapling spend. + let res = builder + .mock_build(&TransparentSigningSet::new(), &[extsk], &[], OsRng) + .unwrap(); + assert!(res.transaction().sapling_bundle().is_some()); } #[test] fn fails_on_negative_change() { + use crate::transaction::fees::zip317::MINIMUM_FEE; + let mut rng = OsRng; // Just use the master key as the ExtendedSpendingKey for this test let extsk = ExtendedSpendingKey::master(&[]); + let tx_height = TEST_NETWORK + .activation_height(NetworkUpgrade::Sapling) + .unwrap(); // Fails with no inputs or outputs // 0.0001 t-ZEC fee { - let builder = Builder::new(TEST_NETWORK, H0); - assert_eq!( - builder.build(consensus::BranchId::Sapling, &MockTxProver), - Err(Error::ChangeIsNegative(Amount::zero() - DEFAULT_FEE)) + let build_config = BuildConfig::Standard { + sapling_anchor: None, + orchard_anchor: None, + }; + let builder = Builder::new(TEST_NETWORK, tx_height, build_config); + assert_matches!( + builder.mock_build(&TransparentSigningSet::new(), &[], &[], OsRng), + Err(Error::InsufficientFunds(expected)) if expected == MINIMUM_FEE.into() ); } - let extfvk = ExtendedFullViewingKey::from(&extsk); - let ovk = Some(extfvk.fvk.ovk); - let to = extfvk.default_address().unwrap().1; + let dfvk = extsk.to_diversifiable_full_viewing_key(); + let ovk = Some(dfvk.fvk().ovk); + let to = dfvk.default_address().1; + + let extsks = &[extsk]; // Fail if there is only a Sapling output - // 0.0005 z-ZEC out, 0.00001 t-ZEC fee + // 0.0005 z-ZEC out, 0.0001 t-ZEC fee { - let mut builder = Builder::new(TEST_NETWORK, H0); + let build_config = BuildConfig::Standard { + sapling_anchor: Some(sapling::Anchor::empty_tree()), + orchard_anchor: Some(orchard::Anchor::empty_tree()), + }; + let mut builder = Builder::new(TEST_NETWORK, tx_height, build_config); builder - .add_sapling_output(ovk, to.clone(), Amount::from_u64(50000).unwrap(), None) + .add_sapling_output::( + ovk, + to, + Zatoshis::const_from_u64(50000), + MemoBytes::empty(), + ) .unwrap(); - assert_eq!( - builder.build(consensus::BranchId::Sapling, &MockTxProver), - Err(Error::ChangeIsNegative( - Amount::from_i64(-50000).unwrap() - DEFAULT_FEE - )) + assert_matches!( + builder.mock_build(&TransparentSigningSet::new(), extsks, &[], OsRng), + Err(Error::InsufficientFunds(expected)) if + expected == (Zatoshis::const_from_u64(50000) + MINIMUM_FEE).unwrap().into() ); } // Fail if there is only a transparent output - // 0.0005 t-ZEC out, 0.00001 t-ZEC fee + // 0.0005 t-ZEC out, 0.0001 t-ZEC fee { - let mut builder = Builder::new(TEST_NETWORK, H0); + let build_config = BuildConfig::Standard { + sapling_anchor: Some(sapling::Anchor::empty_tree()), + orchard_anchor: Some(orchard::Anchor::empty_tree()), + }; + let mut builder = Builder::new(TEST_NETWORK, tx_height, build_config); builder .add_transparent_output( - &TransparentAddress::PublicKey([0; 20]), - Amount::from_u64(50000).unwrap(), + &TransparentAddress::PublicKeyHash([0; 20]), + Zatoshis::const_from_u64(50000), ) .unwrap(); - assert_eq!( - builder.build(consensus::BranchId::Sapling, &MockTxProver), - Err(Error::ChangeIsNegative( - Amount::from_i64(-50000).unwrap() - DEFAULT_FEE - )) + assert_matches!( + builder.mock_build(&TransparentSigningSet::new(), extsks, &[], OsRng), + Err(Error::InsufficientFunds(expected)) if expected == + (Zatoshis::const_from_u64(50000) + MINIMUM_FEE).unwrap().into() ); } - let note1 = to - .create_note(50999, Rseed::BeforeZip212(jubjub::Fr::random(&mut rng))) - .unwrap(); - let cmu1 = Node::new(note1.cmu().to_repr()); - let mut tree = CommitmentTree::empty(); + let note1 = to.create_note( + sapling::value::NoteValue::from_raw(59999), + Rseed::BeforeZip212(jubjub::Fr::random(&mut rng)), + ); + let cmu1 = Node::from_cmu(¬e1.cmu()); + let mut tree = CommitmentTree::::empty(); tree.append(cmu1).unwrap(); - let mut witness1 = IncrementalWitness::from_tree(&tree); + let mut witness1 = IncrementalWitness::from_tree(tree.clone()).unwrap(); // Fail if there is insufficient input - // 0.0003 z-ZEC out, 0.0002 t-ZEC out, 0.00001 t-ZEC fee, 0.00050999 z-ZEC in + // 0.0003 z-ZEC out, 0.0002 t-ZEC out, 0.0001 t-ZEC fee, 0.00059999 z-ZEC in { - let mut builder = Builder::new(TEST_NETWORK, H0); + let build_config = BuildConfig::Standard { + sapling_anchor: Some(witness1.root().into()), + orchard_anchor: Some(orchard::Anchor::empty_tree()), + }; + let mut builder = Builder::new(TEST_NETWORK, tx_height, build_config); builder - .add_sapling_spend( - extsk.clone(), - *to.diversifier(), + .add_sapling_spend::( + dfvk.fvk().clone(), note1.clone(), witness1.path().unwrap(), ) .unwrap(); builder - .add_sapling_output(ovk, to.clone(), Amount::from_u64(30000).unwrap(), None) + .add_sapling_output::( + ovk, + to, + Zatoshis::const_from_u64(30000), + MemoBytes::empty(), + ) .unwrap(); builder .add_transparent_output( - &TransparentAddress::PublicKey([0; 20]), - Amount::from_u64(20000).unwrap(), + &TransparentAddress::PublicKeyHash([0; 20]), + Zatoshis::const_from_u64(15000), ) .unwrap(); - assert_eq!( - builder.build(consensus::BranchId::Sapling, &MockTxProver), - Err(Error::ChangeIsNegative(Amount::from_i64(-1).unwrap())) + assert_matches!( + builder.mock_build(&TransparentSigningSet::new(), extsks, &[], OsRng), + Err(Error::InsufficientFunds(expected)) if expected == ZatBalance::const_from_i64(1) ); } - let note2 = to - .create_note(1, Rseed::BeforeZip212(jubjub::Fr::random(&mut rng))) - .unwrap(); - let cmu2 = Node::new(note2.cmu().to_repr()); + let note2 = to.create_note( + sapling::value::NoteValue::from_raw(1), + Rseed::BeforeZip212(jubjub::Fr::random(&mut rng)), + ); + let cmu2 = Node::from_cmu(¬e2.cmu()); tree.append(cmu2).unwrap(); witness1.append(cmu2).unwrap(); - let witness2 = IncrementalWitness::from_tree(&tree); + let witness2 = IncrementalWitness::from_tree(tree).unwrap(); // Succeeds if there is sufficient input - // 0.0003 z-ZEC out, 0.0002 t-ZEC out, 0.0001 t-ZEC fee, 0.0006 z-ZEC in - // - // (Still fails because we are using a MockTxProver which doesn't correctly - // compute bindingSig.) + // 0.0003 z-ZEC out, 0.00015 t-ZEC out, 0.00015 t-ZEC fee, 0.0006 z-ZEC in { - let mut builder = Builder::new(TEST_NETWORK, H0); + let build_config = BuildConfig::Standard { + sapling_anchor: Some(witness1.root().into()), + orchard_anchor: Some(orchard::Anchor::empty_tree()), + }; + let mut builder = Builder::new(TEST_NETWORK, tx_height, build_config); builder - .add_sapling_spend( - extsk.clone(), - *to.diversifier(), + .add_sapling_spend::( + dfvk.fvk().clone(), note1, witness1.path().unwrap(), ) .unwrap(); builder - .add_sapling_spend(extsk, *to.diversifier(), note2, witness2.path().unwrap()) + .add_sapling_spend::( + dfvk.fvk().clone(), + note2, + witness2.path().unwrap(), + ) .unwrap(); builder - .add_sapling_output(ovk, to, Amount::from_u64(30000).unwrap(), None) + .add_sapling_output::( + ovk, + to, + Zatoshis::const_from_u64(30000), + MemoBytes::empty(), + ) .unwrap(); builder .add_transparent_output( - &TransparentAddress::PublicKey([0; 20]), - Amount::from_u64(20000).unwrap(), + &TransparentAddress::PublicKeyHash([0; 20]), + Zatoshis::const_from_u64(15000), ) .unwrap(); + let res = builder + .mock_build(&TransparentSigningSet::new(), extsks, &[], OsRng) + .unwrap(); assert_eq!( - builder.build(consensus::BranchId::Sapling, &MockTxProver), - Err(Error::BindingSig) - ) + res.transaction() + .fee_paid(|_| Err(BalanceError::Overflow)) + .unwrap(), + ZatBalance::const_from_i64(15_000) + ); } } } diff --git a/zcash_primitives/src/transaction/components.rs b/zcash_primitives/src/transaction/components.rs index 258381ab0d..944f45e9d9 100644 --- a/zcash_primitives/src/transaction/components.rs +++ b/zcash_primitives/src/transaction/components.rs @@ -1,19 +1,61 @@ //! Structs representing the components within Zcash transactions. - -pub mod amount; +pub mod orchard; pub mod sapling; pub mod sprout; -pub mod transparent; +#[cfg(zcash_unstable = "zfuture")] pub mod tze; -pub use self::{ - amount::Amount, - sapling::{OutputDescription, SpendDescription}, - sprout::JsDescription, - transparent::{OutPoint, TxIn, TxOut}, -}; - -#[cfg(feature = "zfuture")] -pub use self::tze::{TzeIn, TzeOut, TzeOutPoint}; + +pub use self::sprout::JsDescription; + +#[deprecated(note = "This module is deprecated; use `::zcash_protocol::value` instead.")] +pub mod amount { + #[deprecated(note = "Use `::zcash_protocol::value::BalanceError` instead.")] + pub type BalanceError = zcash_protocol::value::BalanceError; + #[deprecated(note = "Use `::zcash_protocol::value::ZatBalance` instead.")] + pub type Amount = zcash_protocol::value::ZatBalance; + #[deprecated(note = "Use `::zcash_protocol::value::Zatoshis` instead.")] + pub type NonNegativeAmount = zcash_protocol::value::Zatoshis; + #[deprecated(note = "Use `::zcash_protocol::value::COIN` instead.")] + pub const COIN: u64 = zcash_protocol::value::COIN; + + #[cfg(any(test, feature = "test-dependencies"))] + #[deprecated(note = "Use `::zcash_protocol::value::testing` instead.")] + pub mod testing { + pub use zcash_protocol::value::testing::arb_positive_zat_balance as arb_positive_amount; + pub use zcash_protocol::value::testing::arb_zat_balance as arb_amount; + pub use zcash_protocol::value::testing::arb_zatoshis as arb_nonnegative_amount; + } +} + +#[deprecated(note = "This module is deprecated; use the `zcash_transparent` crate instead.")] +pub mod transparent { + #[deprecated(note = "This module is deprecated; use `::zcash_transparent::builder` instead.")] + pub mod builder { + pub use ::transparent::builder::*; + } + pub use ::transparent::bundle::*; + #[deprecated(note = "This module is deprecated; use `::zcash_transparent::pczt` instead.")] + pub mod pczt { + pub use ::transparent::pczt::*; + } +} + +#[deprecated(note = "use `::zcash_transparent::bundle::OutPoint` instead.")] +pub type OutPoint = ::transparent::bundle::OutPoint; +#[deprecated(note = "use `::zcash_transparent::bundle::TxIn` instead.")] +pub type TxIn = ::transparent::bundle::TxIn; +#[deprecated(note = "use `::zcash_transparent::bundle::TxIn` instead.")] +pub type TxOut = ::transparent::bundle::TxOut; +#[deprecated(note = "use `::zcash_protocol::value::ZatBalance` instead.")] +pub type Amount = zcash_protocol::value::ZatBalance; + +#[deprecated(note = "Use `::sapling_crypto::bundle::OutputDescription` instead.")] +pub type OutputDescription = ::sapling::bundle::OutputDescription; +#[deprecated(note = "Use `::sapling_crypto::bundle::SpendDescription` instead.")] +pub type SpendDescription = ::sapling::bundle::SpendDescription; + +#[cfg(zcash_unstable = "zfuture")] +pub use self::tze::{TzeIn, TzeOut}; // π_A + π_B + π_C pub const GROTH_PROOF_SIZE: usize = 48 + 96 + 48; diff --git a/zcash_primitives/src/transaction/components/amount.rs b/zcash_primitives/src/transaction/components/amount.rs deleted file mode 100644 index f47b485f74..0000000000 --- a/zcash_primitives/src/transaction/components/amount.rs +++ /dev/null @@ -1,242 +0,0 @@ -use std::iter::Sum; -use std::ops::{Add, AddAssign, Sub, SubAssign}; - -pub const COIN: i64 = 1_0000_0000; -pub const MAX_MONEY: i64 = 21_000_000 * COIN; - -pub const DEFAULT_FEE: Amount = Amount(1000); - -/// A type-safe representation of some quantity of Zcash. -/// -/// An Amount can only be constructed from an integer that is within the valid monetary -/// range of `{-MAX_MONEY..MAX_MONEY}` (where `MAX_MONEY` = 21,000,000 × 10⁸ zatoshis). -/// However, this range is not preserved as an invariant internally; it is possible to -/// add two valid Amounts together to obtain an invalid Amount. It is the user's -/// responsibility to handle the result of serializing potentially-invalid Amounts. In -/// particular, a [`Transaction`] containing serialized invalid Amounts will be rejected -/// by the network consensus rules. -/// -/// [`Transaction`]: crate::transaction::Transaction -#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Eq, Ord)] -pub struct Amount(i64); - -impl Amount { - /// Returns a zero-valued Amount. - pub const fn zero() -> Self { - Amount(0) - } - - /// Creates an Amount from an i64. - /// - /// Returns an error if the amount is outside the range `{-MAX_MONEY..MAX_MONEY}`. - pub fn from_i64(amount: i64) -> Result { - if -MAX_MONEY <= amount && amount <= MAX_MONEY { - Ok(Amount(amount)) - } else { - Err(()) - } - } - - /// Creates a non-negative Amount from an i64. - /// - /// Returns an error if the amount is outside the range `{0..MAX_MONEY}`. - pub fn from_nonnegative_i64(amount: i64) -> Result { - if (0..=MAX_MONEY).contains(&amount) { - Ok(Amount(amount)) - } else { - Err(()) - } - } - - /// Creates an Amount from a u64. - /// - /// Returns an error if the amount is outside the range `{0..MAX_MONEY}`. - pub fn from_u64(amount: u64) -> Result { - if amount <= MAX_MONEY as u64 { - Ok(Amount(amount as i64)) - } else { - Err(()) - } - } - - /// Reads an Amount from a signed 64-bit little-endian integer. - /// - /// Returns an error if the amount is outside the range `{-MAX_MONEY..MAX_MONEY}`. - pub fn from_i64_le_bytes(bytes: [u8; 8]) -> Result { - let amount = i64::from_le_bytes(bytes); - Amount::from_i64(amount) - } - - /// Reads a non-negative Amount from a signed 64-bit little-endian integer. - /// - /// Returns an error if the amount is outside the range `{0..MAX_MONEY}`. - pub fn from_nonnegative_i64_le_bytes(bytes: [u8; 8]) -> Result { - let amount = i64::from_le_bytes(bytes); - Amount::from_nonnegative_i64(amount) - } - - /// Reads an Amount from an unsigned 64-bit little-endian integer. - /// - /// Returns an error if the amount is outside the range `{0..MAX_MONEY}`. - pub fn from_u64_le_bytes(bytes: [u8; 8]) -> Result { - let amount = u64::from_le_bytes(bytes); - Amount::from_u64(amount) - } - - /// Returns the Amount encoded as a signed 64-bit little-endian integer. - pub fn to_i64_le_bytes(self) -> [u8; 8] { - self.0.to_le_bytes() - } - - /// Returns `true` if `self` is positive and `false` if the Amount is zero or - /// negative. - pub const fn is_positive(self) -> bool { - self.0.is_positive() - } - - /// Returns `true` if `self` is negative and `false` if the Amount is zero or - /// positive. - pub const fn is_negative(self) -> bool { - self.0.is_negative() - } -} - -impl From for i64 { - fn from(amount: Amount) -> i64 { - amount.0 - } -} - -impl From for u64 { - fn from(amount: Amount) -> u64 { - amount.0 as u64 - } -} - -impl Add for Amount { - type Output = Amount; - - fn add(self, rhs: Amount) -> Amount { - Amount::from_i64(self.0 + rhs.0).expect("addition should remain in range") - } -} - -impl AddAssign for Amount { - fn add_assign(&mut self, rhs: Amount) { - *self = *self + rhs - } -} - -impl Sub for Amount { - type Output = Amount; - - fn sub(self, rhs: Amount) -> Amount { - Amount::from_i64(self.0 - rhs.0).expect("subtraction should remain in range") - } -} - -impl SubAssign for Amount { - fn sub_assign(&mut self, rhs: Amount) { - *self = *self - rhs - } -} - -impl Sum for Amount { - fn sum>(iter: I) -> Amount { - iter.fold(Amount::zero(), Add::add) - } -} - -#[cfg(any(test, feature = "test-dependencies"))] -pub mod testing { - use proptest::prelude::prop_compose; - - use super::{Amount, MAX_MONEY}; - - prop_compose! { - pub fn arb_nonnegative_amount()(amt in 0i64..MAX_MONEY) -> Amount { - Amount::from_i64(amt).unwrap() - } - } -} - -#[cfg(test)] -mod tests { - use super::{Amount, MAX_MONEY}; - - #[test] - fn amount_in_range() { - let zero = b"\x00\x00\x00\x00\x00\x00\x00\x00"; - assert_eq!(Amount::from_u64_le_bytes(*zero).unwrap(), Amount(0)); - assert_eq!( - Amount::from_nonnegative_i64_le_bytes(*zero).unwrap(), - Amount(0) - ); - assert_eq!(Amount::from_i64_le_bytes(*zero).unwrap(), Amount(0)); - - let neg_one = b"\xff\xff\xff\xff\xff\xff\xff\xff"; - assert!(Amount::from_u64_le_bytes(*neg_one).is_err()); - assert!(Amount::from_nonnegative_i64_le_bytes(*neg_one).is_err()); - assert_eq!(Amount::from_i64_le_bytes(*neg_one).unwrap(), Amount(-1)); - - let max_money = b"\x00\x40\x07\x5a\xf0\x75\x07\x00"; - assert_eq!( - Amount::from_u64_le_bytes(*max_money).unwrap(), - Amount(MAX_MONEY) - ); - assert_eq!( - Amount::from_nonnegative_i64_le_bytes(*max_money).unwrap(), - Amount(MAX_MONEY) - ); - assert_eq!( - Amount::from_i64_le_bytes(*max_money).unwrap(), - Amount(MAX_MONEY) - ); - - let max_money_p1 = b"\x01\x40\x07\x5a\xf0\x75\x07\x00"; - assert!(Amount::from_u64_le_bytes(*max_money_p1).is_err()); - assert!(Amount::from_nonnegative_i64_le_bytes(*max_money_p1).is_err()); - assert!(Amount::from_i64_le_bytes(*max_money_p1).is_err()); - - let neg_max_money = b"\x00\xc0\xf8\xa5\x0f\x8a\xf8\xff"; - assert!(Amount::from_u64_le_bytes(*neg_max_money).is_err()); - assert!(Amount::from_nonnegative_i64_le_bytes(*neg_max_money).is_err()); - assert_eq!( - Amount::from_i64_le_bytes(*neg_max_money).unwrap(), - Amount(-MAX_MONEY) - ); - - let neg_max_money_m1 = b"\xff\xbf\xf8\xa5\x0f\x8a\xf8\xff"; - assert!(Amount::from_u64_le_bytes(*neg_max_money_m1).is_err()); - assert!(Amount::from_nonnegative_i64_le_bytes(*neg_max_money_m1).is_err()); - assert!(Amount::from_i64_le_bytes(*neg_max_money_m1).is_err()); - } - - #[test] - #[should_panic] - fn add_panics_on_overflow() { - let v = Amount(MAX_MONEY); - let _sum = v + Amount(1); - } - - #[test] - #[should_panic] - fn add_assign_panics_on_overflow() { - let mut a = Amount(MAX_MONEY); - a += Amount(1); - } - - #[test] - #[should_panic] - fn sub_panics_on_underflow() { - let v = Amount(-MAX_MONEY); - let _diff = v - Amount(1); - } - - #[test] - #[should_panic] - fn sub_assign_panics_on_underflow() { - let mut a = Amount(-MAX_MONEY); - a -= Amount(1); - } -} diff --git a/zcash_primitives/src/transaction/components/orchard.rs b/zcash_primitives/src/transaction/components/orchard.rs new file mode 100644 index 0000000000..281ebaa11b --- /dev/null +++ b/zcash_primitives/src/transaction/components/orchard.rs @@ -0,0 +1,291 @@ +//! Functions for parsing & serialization of Orchard transaction components. +use crate::encoding::ReadBytesExt; + +use alloc::vec::Vec; +use core::convert::TryFrom; +use core2::io::{self, Read, Write}; + +use nonempty::NonEmpty; + +use orchard::{ + bundle::{Authorization, Authorized, Flags}, + note::{ExtractedNoteCommitment, Nullifier, TransmittedNoteCiphertext}, + primitives::redpallas::{self, SigType, Signature, SpendAuth, VerificationKey}, + value::ValueCommitment, + Action, Anchor, +}; +use zcash_encoding::{Array, CompactSize, Vector}; +use zcash_protocol::value::ZatBalance; + +use crate::transaction::Transaction; + +pub const FLAG_SPENDS_ENABLED: u8 = 0b0000_0001; +pub const FLAG_OUTPUTS_ENABLED: u8 = 0b0000_0010; +pub const FLAGS_EXPECTED_UNSET: u8 = !(FLAG_SPENDS_ENABLED | FLAG_OUTPUTS_ENABLED); + +pub trait MapAuth { + fn map_spend_auth(&self, s: A::SpendAuth) -> B::SpendAuth; + fn map_authorization(&self, a: A) -> B; +} + +/// The identity map. +/// +/// This can be used with [`TransactionData::map_authorization`] when you want to map the +/// authorization of a subset of the transaction's bundles. +/// +/// [`TransactionData::map_authorization`]: crate::transaction::TransactionData::map_authorization +impl MapAuth for () { + fn map_spend_auth( + &self, + s: ::SpendAuth, + ) -> ::SpendAuth { + s + } + + fn map_authorization(&self, a: Authorized) -> Authorized { + a + } +} + +/// Reads an [`orchard::Bundle`] from a v5 transaction format. +pub fn read_v5_bundle( + mut reader: R, +) -> io::Result>> { + #[allow(clippy::redundant_closure)] + let actions_without_auth = Vector::read(&mut reader, |r| read_action_without_auth(r))?; + if actions_without_auth.is_empty() { + Ok(None) + } else { + let flags = read_flags(&mut reader)?; + let value_balance = Transaction::read_amount(&mut reader)?; + let anchor = read_anchor(&mut reader)?; + let proof_bytes = Vector::read(&mut reader, |r| r.read_u8())?; + let actions = NonEmpty::from_vec( + actions_without_auth + .into_iter() + .map(|act| act.try_map(|_| read_signature::<_, redpallas::SpendAuth>(&mut reader))) + .collect::, _>>()?, + ) + .expect("A nonzero number of actions was read from the transaction data."); + let binding_signature = read_signature::<_, redpallas::Binding>(&mut reader)?; + + let authorization = orchard::bundle::Authorized::from_parts( + orchard::Proof::new(proof_bytes), + binding_signature, + ); + + Ok(Some(orchard::Bundle::from_parts( + actions, + flags, + value_balance, + anchor, + authorization, + ))) + } +} + +pub fn read_value_commitment(mut reader: R) -> io::Result { + let mut bytes = [0u8; 32]; + reader.read_exact(&mut bytes)?; + let cv = ValueCommitment::from_bytes(&bytes); + + if cv.is_none().into() { + Err(io::Error::new( + io::ErrorKind::InvalidInput, + "invalid Pallas point for value commitment", + )) + } else { + Ok(cv.unwrap()) + } +} + +pub fn read_nullifier(mut reader: R) -> io::Result { + let mut bytes = [0u8; 32]; + reader.read_exact(&mut bytes)?; + let nullifier_ctopt = Nullifier::from_bytes(&bytes); + if nullifier_ctopt.is_none().into() { + Err(io::Error::new( + io::ErrorKind::InvalidInput, + "invalid Pallas point for nullifier", + )) + } else { + Ok(nullifier_ctopt.unwrap()) + } +} + +pub fn read_verification_key(mut reader: R) -> io::Result> { + let mut bytes = [0u8; 32]; + reader.read_exact(&mut bytes)?; + VerificationKey::try_from(bytes) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid verification key")) +} + +pub fn read_cmx(mut reader: R) -> io::Result { + let mut bytes = [0u8; 32]; + reader.read_exact(&mut bytes)?; + let cmx = ExtractedNoteCommitment::from_bytes(&bytes); + Option::from(cmx).ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidInput, + "invalid Pallas base for field cmx", + ) + }) +} + +pub fn read_note_ciphertext(mut reader: R) -> io::Result { + let mut tnc = TransmittedNoteCiphertext { + epk_bytes: [0u8; 32], + enc_ciphertext: [0u8; 580], + out_ciphertext: [0u8; 80], + }; + + reader.read_exact(&mut tnc.epk_bytes)?; + reader.read_exact(&mut tnc.enc_ciphertext)?; + reader.read_exact(&mut tnc.out_ciphertext)?; + + Ok(tnc) +} + +pub fn read_action_without_auth(mut reader: R) -> io::Result> { + let cv_net = read_value_commitment(&mut reader)?; + let nf_old = read_nullifier(&mut reader)?; + let rk = read_verification_key(&mut reader)?; + let cmx = read_cmx(&mut reader)?; + let encrypted_note = read_note_ciphertext(&mut reader)?; + + Ok(Action::from_parts( + nf_old, + rk, + cmx, + encrypted_note, + cv_net, + (), + )) +} + +pub fn read_flags(mut reader: R) -> io::Result { + let mut byte = [0u8; 1]; + reader.read_exact(&mut byte)?; + Flags::from_byte(byte[0]) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid Orchard flags")) +} + +pub fn read_anchor(mut reader: R) -> io::Result { + let mut bytes = [0u8; 32]; + reader.read_exact(&mut bytes)?; + Option::from(Anchor::from_bytes(bytes)) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid Orchard anchor")) +} + +pub fn read_signature(mut reader: R) -> io::Result> { + let mut bytes = [0u8; 64]; + reader.read_exact(&mut bytes)?; + Ok(Signature::from(bytes)) +} + +/// Writes an [`orchard::Bundle`] in the v5 transaction format. +pub fn write_v5_bundle( + bundle: Option<&orchard::Bundle>, + mut writer: W, +) -> io::Result<()> { + if let Some(bundle) = &bundle { + Vector::write_nonempty(&mut writer, bundle.actions(), |w, a| { + write_action_without_auth(w, a) + })?; + + writer.write_all(&[bundle.flags().to_byte()])?; + writer.write_all(&bundle.value_balance().to_i64_le_bytes())?; + writer.write_all(&bundle.anchor().to_bytes())?; + Vector::write( + &mut writer, + bundle.authorization().proof().as_ref(), + |w, b| w.write_all(&[*b]), + )?; + Array::write( + &mut writer, + bundle.actions().iter().map(|a| a.authorization()), + |w, auth| w.write_all(&<[u8; 64]>::from(*auth)), + )?; + writer.write_all(&<[u8; 64]>::from( + bundle.authorization().binding_signature(), + ))?; + } else { + CompactSize::write(&mut writer, 0)?; + } + + Ok(()) +} + +pub fn write_value_commitment(mut writer: W, cv: &ValueCommitment) -> io::Result<()> { + writer.write_all(&cv.to_bytes()) +} + +pub fn write_nullifier(mut writer: W, nf: &Nullifier) -> io::Result<()> { + writer.write_all(&nf.to_bytes()) +} + +pub fn write_verification_key( + mut writer: W, + rk: &redpallas::VerificationKey, +) -> io::Result<()> { + writer.write_all(&<[u8; 32]>::from(rk)) +} + +pub fn write_cmx(mut writer: W, cmx: &ExtractedNoteCommitment) -> io::Result<()> { + writer.write_all(&cmx.to_bytes()) +} + +pub fn write_note_ciphertext( + mut writer: W, + nc: &TransmittedNoteCiphertext, +) -> io::Result<()> { + writer.write_all(&nc.epk_bytes)?; + writer.write_all(&nc.enc_ciphertext)?; + writer.write_all(&nc.out_ciphertext) +} + +pub fn write_action_without_auth( + mut writer: W, + act: &Action<::SpendAuth>, +) -> io::Result<()> { + write_value_commitment(&mut writer, act.cv_net())?; + write_nullifier(&mut writer, act.nullifier())?; + write_verification_key(&mut writer, act.rk())?; + write_cmx(&mut writer, act.cmx())?; + write_note_ciphertext(&mut writer, act.encrypted_note())?; + Ok(()) +} + +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing { + use proptest::prelude::*; + + use orchard::bundle::{ + testing::{self as t_orch}, + Authorized, Bundle, + }; + use zcash_protocol::value::{testing::arb_zat_balance, ZatBalance}; + + use crate::transaction::TxVersion; + + prop_compose! { + pub fn arb_bundle(n_actions: usize)( + orchard_value_balance in arb_zat_balance(), + bundle in t_orch::arb_bundle(n_actions) + ) -> Bundle { + // overwrite the value balance, as we can't guarantee that the + // value doesn't exceed the MAX_MONEY bounds. + bundle.try_map_value_balance::<_, (), _>(|_| Ok(orchard_value_balance)).unwrap() + } + } + + pub fn arb_bundle_for_version( + v: TxVersion, + ) -> impl Strategy>> { + if v.has_orchard() { + Strategy::boxed((1usize..100).prop_flat_map(|n| prop::option::of(arb_bundle(n)))) + } else { + Strategy::boxed(Just(None)) + } + } +} diff --git a/zcash_primitives/src/transaction/components/sapling.rs b/zcash_primitives/src/transaction/components/sapling.rs index f008b75b3a..943b11a387 100644 --- a/zcash_primitives/src/transaction/components/sapling.rs +++ b/zcash_primitives/src/transaction/components/sapling.rs @@ -1,245 +1,527 @@ +use alloc::vec::Vec; +use core2::io::{self, Read, Write}; use ff::PrimeField; -use group::GroupEncoding; -use std::io::{self, Read, Write}; - -use zcash_note_encryption::ShieldedOutput; - -use crate::{ - consensus, - sapling::{ - note_encryption::SaplingDomain, - redjubjub::{PublicKey, Signature}, - Nullifier, +use ::sapling::{ + bundle::{ + Authorization, Authorized, Bundle, GrothProofBytes, OutputDescription, OutputDescriptionV5, + SpendDescription, SpendDescriptionV5, }, + note::ExtractedNoteCommitment, + note_encryption::Zip212Enforcement, + value::ValueCommitment, + Nullifier, +}; +use redjubjub::SpendAuth; +use zcash_encoding::{Array, CompactSize, Vector}; +use zcash_note_encryption::{EphemeralKeyBytes, ENC_CIPHERTEXT_SIZE, OUT_CIPHERTEXT_SIZE}; +use zcash_protocol::{ + consensus::{BlockHeight, NetworkUpgrade, Parameters, ZIP212_GRACE_PERIOD}, + value::ZatBalance, }; - -use zcash_note_encryption::COMPACT_NOTE_SIZE; use super::GROTH_PROOF_SIZE; - -#[derive(Clone)] -pub struct SpendDescription { - pub cv: jubjub::ExtendedPoint, - pub anchor: bls12_381::Scalar, - pub nullifier: Nullifier, - pub rk: PublicKey, - pub zkproof: [u8; GROTH_PROOF_SIZE], - pub spend_auth_sig: Option, -} - -impl std::fmt::Debug for SpendDescription { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - write!( - f, - "SpendDescription(cv = {:?}, anchor = {:?}, nullifier = {:?}, rk = {:?}, spend_auth_sig = {:?})", - self.cv, self.anchor, self.nullifier, self.rk, self.spend_auth_sig - ) +use crate::transaction::Transaction; + +/// Returns the enforcement policy for ZIP 212 at the given height. +pub fn zip212_enforcement(params: &impl Parameters, height: BlockHeight) -> Zip212Enforcement { + if params.is_nu_active(NetworkUpgrade::Canopy, height) { + let grace_period_end_height = + params.activation_height(NetworkUpgrade::Canopy).unwrap() + ZIP212_GRACE_PERIOD; + + if height < grace_period_end_height { + Zip212Enforcement::GracePeriod + } else { + Zip212Enforcement::On + } + } else { + Zip212Enforcement::Off } } -impl SpendDescription { - pub fn read(mut reader: &mut R) -> io::Result { - // Consensus rules (§4.4): - // - Canonical encoding is enforced here. - // - "Not small order" is enforced in SaplingVerificationContext::check_spend() - // (located in zcash_proofs::sapling::verifier). - let cv = { - let mut bytes = [0u8; 32]; - reader.read_exact(&mut bytes)?; - let cv = jubjub::ExtendedPoint::from_bytes(&bytes); - if cv.is_none().into() { - return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid cv")); - } - cv.unwrap() - }; - - // Consensus rule (§7.3): Canonical encoding is enforced here - let anchor = { - let mut f = [0u8; 32]; - reader.read_exact(&mut f)?; - bls12_381::Scalar::from_repr(f) - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "anchor not in field"))? - }; - - let mut nullifier = Nullifier([0u8; 32]); - reader.read_exact(&mut nullifier.0)?; - - // Consensus rules (§4.4): - // - Canonical encoding is enforced here. - // - "Not small order" is enforced in SaplingVerificationContext::check_spend() - let rk = PublicKey::read(&mut reader)?; - - // Consensus rules (§4.4): - // - Canonical encoding is enforced by the API of SaplingVerificationContext::check_spend() - // due to the need to parse this into a bellman::groth16::Proof. - // - Proof validity is enforced in SaplingVerificationContext::check_spend() - let mut zkproof = [0u8; GROTH_PROOF_SIZE]; - reader.read_exact(&mut zkproof)?; - - // Consensus rules (§4.4): - // - Canonical encoding is enforced here. - // - Signature validity is enforced in SaplingVerificationContext::check_spend() - let spend_auth_sig = Some(Signature::read(&mut reader)?); - - Ok(SpendDescription { - cv, - anchor, - nullifier, - rk, - zkproof, - spend_auth_sig, - }) - } +/// A map from one bundle authorization to another. +/// +/// For use with [`TransactionData::map_authorization`]. +/// +/// [`TransactionData::map_authorization`]: crate::transaction::TransactionData::map_authorization +pub trait MapAuth { + fn map_spend_proof(&mut self, p: A::SpendProof) -> B::SpendProof; + fn map_output_proof(&mut self, p: A::OutputProof) -> B::OutputProof; + fn map_auth_sig(&mut self, s: A::AuthSig) -> B::AuthSig; + fn map_authorization(&mut self, a: A) -> B; +} - pub fn write(&self, mut writer: W) -> io::Result<()> { - writer.write_all(&self.cv.to_bytes())?; - writer.write_all(self.anchor.to_repr().as_ref())?; - writer.write_all(&self.nullifier.0)?; - self.rk.write(&mut writer)?; - writer.write_all(&self.zkproof)?; - match self.spend_auth_sig { - Some(sig) => sig.write(&mut writer), - None => Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Missing spend auth signature", - )), - } +/// The identity map. +/// +/// This can be used with [`TransactionData::map_authorization`] when you want to map the +/// authorization of a subset of a transaction's bundles. +/// +/// [`TransactionData::map_authorization`]: crate::transaction::TransactionData::map_authorization +impl MapAuth for () { + fn map_spend_proof( + &mut self, + p: ::SpendProof, + ) -> ::SpendProof { + p } -} -#[derive(Clone)] -pub struct OutputDescription { - pub cv: jubjub::ExtendedPoint, - pub cmu: bls12_381::Scalar, - pub ephemeral_key: jubjub::ExtendedPoint, - pub enc_ciphertext: [u8; 580], - pub out_ciphertext: [u8; 80], - pub zkproof: [u8; GROTH_PROOF_SIZE], -} + fn map_output_proof( + &mut self, + p: ::OutputProof, + ) -> ::OutputProof { + p + } -impl ShieldedOutput> for OutputDescription { - fn epk(&self) -> &jubjub::ExtendedPoint { - &self.ephemeral_key + fn map_auth_sig( + &mut self, + s: ::AuthSig, + ) -> ::AuthSig { + s } - fn cmstar_bytes(&self) -> [u8; 32] { - self.cmu.to_repr() + fn map_authorization(&mut self, a: Authorized) -> Authorized { + a } +} - fn enc_ciphertext(&self) -> &[u8] { - &self.enc_ciphertext +/// Consensus rules (§4.4) & (§4.5): +/// - Canonical encoding is enforced here. +/// - "Not small order" is enforced here. +fn read_value_commitment(mut reader: R) -> io::Result { + let mut bytes = [0u8; 32]; + reader.read_exact(&mut bytes)?; + let cv = ValueCommitment::from_bytes_not_small_order(&bytes); + + if cv.is_none().into() { + Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid cv")) + } else { + Ok(cv.unwrap()) } } -impl std::fmt::Debug for OutputDescription { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - write!( - f, - "OutputDescription(cv = {:?}, cmu = {:?}, ephemeral_key = {:?})", - self.cv, self.cmu, self.ephemeral_key +/// Consensus rules (§7.3) & (§7.4): +/// - Canonical encoding is enforced here +fn read_cmu(mut reader: R) -> io::Result { + let mut f = [0u8; 32]; + reader.read_exact(&mut f)?; + Option::from(ExtractedNoteCommitment::from_bytes(&f)) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "cmu not in field")) +} + +/// Consensus rules (§7.3) & (§7.4): +/// - Canonical encoding is enforced here +pub fn read_base(mut reader: R, _field: &str) -> io::Result { + let mut f = [0u8; 32]; + reader.read_exact(&mut f)?; + Option::from(jubjub::Base::from_repr(f)).ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidInput, + "base value not a valid field element", ) - } + }) } -impl OutputDescription { - pub fn read(reader: &mut R) -> io::Result { - // Consensus rules (§4.5): - // - Canonical encoding is enforced here. - // - "Not small order" is enforced in SaplingVerificationContext::check_output() - // (located in zcash_proofs::sapling::verifier). - let cv = { - let mut bytes = [0u8; 32]; - reader.read_exact(&mut bytes)?; - let cv = jubjub::ExtendedPoint::from_bytes(&bytes); - if cv.is_none().into() { - return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid cv")); - } - cv.unwrap() - }; - - // Consensus rule (§7.4): Canonical encoding is enforced here - let cmu = { - let mut f = [0u8; 32]; - reader.read_exact(&mut f)?; - bls12_381::Scalar::from_repr(f) - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "cmu not in field"))? - }; - - // Consensus rules (§4.5): - // - Canonical encoding is enforced here. - // - "Not small order" is enforced in SaplingVerificationContext::check_output() - let ephemeral_key = { - let mut bytes = [0u8; 32]; - reader.read_exact(&mut bytes)?; - let ephemeral_key = jubjub::ExtendedPoint::from_bytes(&bytes); - if ephemeral_key.is_none().into() { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "invalid ephemeral_key", - )); - } - ephemeral_key.unwrap() - }; - - let mut enc_ciphertext = [0u8; 580]; - let mut out_ciphertext = [0u8; 80]; - reader.read_exact(&mut enc_ciphertext)?; - reader.read_exact(&mut out_ciphertext)?; - - // Consensus rules (§4.5): - // - Canonical encoding is enforced by the API of SaplingVerificationContext::check_output() - // due to the need to parse this into a bellman::groth16::Proof. - // - Proof validity is enforced in SaplingVerificationContext::check_output() - let mut zkproof = [0u8; GROTH_PROOF_SIZE]; - reader.read_exact(&mut zkproof)?; - - Ok(OutputDescription { - cv, - cmu, - ephemeral_key, - enc_ciphertext, - out_ciphertext, - zkproof, - }) - } +/// Consensus rules (§4.4) & (§4.5): +/// - Canonical encoding is enforced by the API of SaplingVerificationContext::check_spend() +/// and SaplingVerificationContext::check_output() due to the need to parse this into a +/// bellman::groth16::Proof. +/// - Proof validity is enforced in SaplingVerificationContext::check_spend() +/// and SaplingVerificationContext::check_output() +pub fn read_zkproof(mut reader: R) -> io::Result { + let mut zkproof = [0u8; GROTH_PROOF_SIZE]; + reader.read_exact(&mut zkproof)?; + Ok(zkproof) +} + +fn read_nullifier(mut reader: R) -> io::Result { + let mut nullifier = Nullifier([0u8; 32]); + reader.read_exact(&mut nullifier.0)?; + Ok(nullifier) +} + +/// Consensus rules (§4.4): +/// - Canonical encoding is enforced here. +/// - "Not small order" is enforced in SaplingVerificationContext::check_spend() +fn read_rk(mut reader: R) -> io::Result> { + let mut bytes = [0; 32]; + reader.read_exact(&mut bytes)?; + redjubjub::VerificationKey::try_from(bytes) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "verification key is invalid")) +} + +/// Consensus rules (§4.4): +/// - Canonical encoding is enforced here. +/// - Signature validity is enforced in SaplingVerificationContext::check_spend() +fn read_spend_auth_sig(mut reader: R) -> io::Result> { + let mut sig = [0; 64]; + reader.read_exact(&mut sig)?; + Ok(redjubjub::Signature::from(sig)) +} + +#[cfg(feature = "temporary-zcashd")] +pub fn temporary_zcashd_read_spend_v4( + reader: R, +) -> io::Result> { + read_spend_v4(reader) +} + +fn read_spend_v4(mut reader: R) -> io::Result> { + // Consensus rules (§4.4) & (§4.5): + // - Canonical encoding is enforced here. + // - "Not small order" is enforced in SaplingVerificationContext::(check_spend()/check_output()) + // (located in zcash_proofs::sapling::verifier). + let cv = read_value_commitment(&mut reader)?; + // Consensus rules (§7.3) & (§7.4): + // - Canonical encoding is enforced here + let anchor = read_base(&mut reader, "anchor")?; + let nullifier = read_nullifier(&mut reader)?; + let rk = read_rk(&mut reader)?; + let zkproof = read_zkproof(&mut reader)?; + let spend_auth_sig = read_spend_auth_sig(&mut reader)?; + + Ok(SpendDescription::from_parts( + cv, + anchor, + nullifier, + rk, + zkproof, + spend_auth_sig, + )) +} + +fn write_spend_v4(mut writer: W, spend: &SpendDescription) -> io::Result<()> { + writer.write_all(&spend.cv().to_bytes())?; + writer.write_all(spend.anchor().to_repr().as_ref())?; + writer.write_all(&spend.nullifier().0)?; + writer.write_all(&<[u8; 32]>::from(*spend.rk()))?; + writer.write_all(spend.zkproof())?; + writer.write_all(&<[u8; 64]>::from(*spend.spend_auth_sig())) +} - pub fn write(&self, mut writer: W) -> io::Result<()> { - writer.write_all(&self.cv.to_bytes())?; - writer.write_all(self.cmu.to_repr().as_ref())?; - writer.write_all(&self.ephemeral_key.to_bytes())?; - writer.write_all(&self.enc_ciphertext)?; - writer.write_all(&self.out_ciphertext)?; - writer.write_all(&self.zkproof) +fn write_spend_v5_without_witness_data( + mut writer: W, + spend: &SpendDescription, +) -> io::Result<()> { + writer.write_all(&spend.cv().to_bytes())?; + writer.write_all(&spend.nullifier().0)?; + writer.write_all(&<[u8; 32]>::from(*spend.rk())) +} + +fn read_spend_v5(mut reader: &mut R) -> io::Result { + let cv = read_value_commitment(&mut reader)?; + let nullifier = read_nullifier(&mut reader)?; + let rk = read_rk(&mut reader)?; + + Ok(SpendDescriptionV5::from_parts(cv, nullifier, rk)) +} + +#[cfg(feature = "temporary-zcashd")] +pub fn temporary_zcashd_read_output_v4( + mut reader: R, +) -> io::Result> { + read_output_v4(&mut reader) +} + +fn read_output_v4(mut reader: &mut R) -> io::Result> { + // Consensus rules (§4.5): + // - Canonical encoding is enforced here. + // - "Not small order" is enforced in SaplingVerificationContext::check_output() + // (located in zcash_proofs::sapling::verifier). + let cv = read_value_commitment(&mut reader)?; + + // Consensus rule (§7.4): Canonical encoding is enforced here + let cmu = read_cmu(&mut reader)?; + + // Consensus rules (§4.5): + // - Canonical encoding is enforced in librustzcash_sapling_check_output by zcashd + // - "Not small order" is enforced in SaplingVerificationContext::check_output() + let mut ephemeral_key = EphemeralKeyBytes([0u8; 32]); + reader.read_exact(&mut ephemeral_key.0)?; + + let mut enc_ciphertext = [0u8; ENC_CIPHERTEXT_SIZE]; + let mut out_ciphertext = [0u8; OUT_CIPHERTEXT_SIZE]; + reader.read_exact(&mut enc_ciphertext)?; + reader.read_exact(&mut out_ciphertext)?; + + let zkproof = read_zkproof(&mut reader)?; + + Ok(OutputDescription::from_parts( + cv, + cmu, + ephemeral_key, + enc_ciphertext, + out_ciphertext, + zkproof, + )) +} + +#[cfg(feature = "temporary-zcashd")] +pub fn temporary_zcashd_write_output_v4( + writer: W, + output: &OutputDescription, +) -> io::Result<()> { + write_output_v4(writer, output) +} + +pub(crate) fn write_output_v4( + mut writer: W, + output: &OutputDescription, +) -> io::Result<()> { + writer.write_all(&output.cv().to_bytes())?; + writer.write_all(output.cmu().to_bytes().as_ref())?; + writer.write_all(output.ephemeral_key().as_ref())?; + writer.write_all(output.enc_ciphertext())?; + writer.write_all(output.out_ciphertext())?; + writer.write_all(output.zkproof()) +} + +fn write_output_v5_without_proof( + mut writer: W, + output: &OutputDescription, +) -> io::Result<()> { + writer.write_all(&output.cv().to_bytes())?; + writer.write_all(output.cmu().to_bytes().as_ref())?; + writer.write_all(output.ephemeral_key().as_ref())?; + writer.write_all(output.enc_ciphertext())?; + writer.write_all(output.out_ciphertext()) +} + +fn read_output_v5(mut reader: &mut R) -> io::Result { + let cv = read_value_commitment(&mut reader)?; + let cmu = read_cmu(&mut reader)?; + + // Consensus rules (§4.5): + // - Canonical encoding is enforced in librustzcash_sapling_check_output by zcashd + // - "Not small order" is enforced in SaplingVerificationContext::check_output() + let mut ephemeral_key = EphemeralKeyBytes([0u8; 32]); + reader.read_exact(&mut ephemeral_key.0)?; + + let mut enc_ciphertext = [0u8; 580]; + let mut out_ciphertext = [0u8; 80]; + reader.read_exact(&mut enc_ciphertext)?; + reader.read_exact(&mut out_ciphertext)?; + + Ok(OutputDescriptionV5::from_parts( + cv, + cmu, + ephemeral_key, + enc_ciphertext, + out_ciphertext, + )) +} + +/// Reads the Sapling components of a v4 transaction. +#[cfg(feature = "temporary-zcashd")] +#[allow(clippy::type_complexity)] +pub fn temporary_zcashd_read_v4_components( + reader: R, + tx_has_sapling: bool, +) -> io::Result<( + ZatBalance, + Vec>, + Vec>, +)> { + read_v4_components(reader, tx_has_sapling) +} + +/// Reads the Sapling components of a v4 transaction. +#[allow(clippy::type_complexity)] +pub(crate) fn read_v4_components( + mut reader: R, + tx_has_sapling: bool, +) -> io::Result<( + ZatBalance, + Vec>, + Vec>, +)> { + if tx_has_sapling { + let vb = Transaction::read_amount(&mut reader)?; + #[allow(clippy::redundant_closure)] + let ss: Vec> = + Vector::read(&mut reader, |r| read_spend_v4(r))?; + #[allow(clippy::redundant_closure)] + let so: Vec> = + Vector::read(&mut reader, |r| read_output_v4(r))?; + Ok((vb, ss, so)) + } else { + Ok((ZatBalance::zero(), vec![], vec![])) } } -pub struct CompactOutputDescription { - pub epk: jubjub::ExtendedPoint, - pub cmu: bls12_381::Scalar, - pub enc_ciphertext: Vec, +/// Writes the Sapling components of a v4 transaction. +#[cfg(feature = "temporary-zcashd")] +pub fn temporary_zcashd_write_v4_components( + writer: W, + bundle: Option<&Bundle>, + tx_has_sapling: bool, +) -> io::Result<()> { + write_v4_components(writer, bundle, tx_has_sapling) } -impl From for CompactOutputDescription { - fn from(out: OutputDescription) -> CompactOutputDescription { - CompactOutputDescription { - epk: out.ephemeral_key, - cmu: out.cmu, - enc_ciphertext: out.enc_ciphertext[..COMPACT_NOTE_SIZE].to_vec(), - } +/// Writes the Sapling components of a v4 transaction. +pub(crate) fn write_v4_components( + mut writer: W, + bundle: Option<&Bundle>, + tx_has_sapling: bool, +) -> io::Result<()> { + if tx_has_sapling { + writer.write_all( + &bundle + .map_or(ZatBalance::zero(), |b| *b.value_balance()) + .to_i64_le_bytes(), + )?; + Vector::write( + &mut writer, + bundle.map_or(&[], |b| b.shielded_spends()), + |w, e| write_spend_v4(w, e), + )?; + Vector::write( + &mut writer, + bundle.map_or(&[], |b| b.shielded_outputs()), + |w, e| write_output_v4(w, e), + )?; + } else if bundle.is_some() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Sapling components may not be present if Sapling is not active.", + )); } + + Ok(()) +} + +/// Reads a [`Bundle`] from a v5 transaction format. +#[allow(clippy::redundant_closure)] +pub(crate) fn read_v5_bundle( + mut reader: R, +) -> io::Result>> { + let sd_v5s = Vector::read(&mut reader, read_spend_v5)?; + let od_v5s = Vector::read(&mut reader, read_output_v5)?; + let n_spends = sd_v5s.len(); + let n_outputs = od_v5s.len(); + let value_balance = if n_spends > 0 || n_outputs > 0 { + Transaction::read_amount(&mut reader)? + } else { + ZatBalance::zero() + }; + + let anchor = if n_spends > 0 { + Some(read_base(&mut reader, "anchor")?) + } else { + None + }; + + let v_spend_proofs = Array::read(&mut reader, n_spends, |r| read_zkproof(r))?; + let v_spend_auth_sigs = Array::read(&mut reader, n_spends, |r| read_spend_auth_sig(r))?; + let v_output_proofs = Array::read(&mut reader, n_outputs, |r| read_zkproof(r))?; + + let binding_sig = if n_spends > 0 || n_outputs > 0 { + let mut sig = [0; 64]; + reader.read_exact(&mut sig)?; + Some(redjubjub::Signature::from(sig)) + } else { + None + }; + + let shielded_spends = sd_v5s + .into_iter() + .zip(v_spend_proofs.into_iter().zip(v_spend_auth_sigs)) + .map(|(sd_5, (zkproof, spend_auth_sig))| { + // the following `unwrap` is safe because we know n_spends > 0. + sd_5.into_spend_description(anchor.unwrap(), zkproof, spend_auth_sig) + }) + .collect(); + + let shielded_outputs = od_v5s + .into_iter() + .zip(v_output_proofs) + .map(|(od_5, zkproof)| od_5.into_output_description(zkproof)) + .collect(); + + Ok(binding_sig.and_then(|binding_sig| { + Bundle::from_parts( + shielded_spends, + shielded_outputs, + value_balance, + Authorized { binding_sig }, + ) + })) } -impl ShieldedOutput> for CompactOutputDescription { - fn epk(&self) -> &jubjub::ExtendedPoint { - &self.epk +/// Writes a [`Bundle`] in the v5 transaction format. +pub(crate) fn write_v5_bundle( + mut writer: W, + sapling_bundle: Option<&Bundle>, +) -> io::Result<()> { + if let Some(bundle) = sapling_bundle { + Vector::write(&mut writer, bundle.shielded_spends(), |w, e| { + write_spend_v5_without_witness_data(w, e) + })?; + + Vector::write(&mut writer, bundle.shielded_outputs(), |w, e| { + write_output_v5_without_proof(w, e) + })?; + + if !(bundle.shielded_spends().is_empty() && bundle.shielded_outputs().is_empty()) { + writer.write_all(&bundle.value_balance().to_i64_le_bytes())?; + } + if !bundle.shielded_spends().is_empty() { + writer.write_all(bundle.shielded_spends()[0].anchor().to_repr().as_ref())?; + } + + Array::write( + &mut writer, + bundle.shielded_spends().iter().map(|s| &s.zkproof()[..]), + |w, e| w.write_all(e), + )?; + Array::write( + &mut writer, + bundle.shielded_spends().iter().map(|s| s.spend_auth_sig()), + |w, e| w.write_all(&<[u8; 64]>::from(**e)), + )?; + + Array::write( + &mut writer, + bundle.shielded_outputs().iter().map(|s| &s.zkproof()[..]), + |w, e| w.write_all(e), + )?; + + if !(bundle.shielded_spends().is_empty() && bundle.shielded_outputs().is_empty()) { + writer.write_all(&<[u8; 64]>::from(bundle.authorization().binding_sig))?; + } + } else { + CompactSize::write(&mut writer, 0)?; + CompactSize::write(&mut writer, 0)?; } - fn cmstar_bytes(&self) -> [u8; 32] { - self.cmu.to_repr() + Ok(()) +} + +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing { + use proptest::prelude::*; + + use crate::transaction::TxVersion; + use ::sapling::bundle::{testing as t_sap, Authorized, Bundle}; + use zcash_protocol::value::{testing::arb_zat_balance, ZatBalance}; + + prop_compose! { + fn arb_bundle()( + value_balance in arb_zat_balance() + )( + bundle in t_sap::arb_bundle(value_balance) + ) -> Option> { + bundle + } } - fn enc_ciphertext(&self) -> &[u8] { - &self.enc_ciphertext + pub fn arb_bundle_for_version( + v: TxVersion, + ) -> impl Strategy>> { + if v.has_sapling() { + Strategy::boxed(arb_bundle()) + } else { + Strategy::boxed(Just(None)) + } } } diff --git a/zcash_primitives/src/transaction/components/sprout.rs b/zcash_primitives/src/transaction/components/sprout.rs index a143eafb3d..15e44b0aa0 100644 --- a/zcash_primitives/src/transaction/components/sprout.rs +++ b/zcash_primitives/src/transaction/components/sprout.rs @@ -1,8 +1,10 @@ //! Structs representing the components within Zcash transactions. -use std::io::{self, Read, Write}; +use alloc::vec::Vec; +use core2::io::{self, Read, Write}; -use super::{amount::Amount, GROTH_PROOF_SIZE}; +use super::GROTH_PROOF_SIZE; +use zcash_protocol::value::ZatBalance; // π_A + π_A' + π_B + π_B' + π_C + π_C' + π_K + π_H const PHGR_PROOF_SIZE: usize = 33 + 33 + 65 + 33 + 33 + 33 + 33 + 33; @@ -10,15 +12,34 @@ const PHGR_PROOF_SIZE: usize = 33 + 33 + 65 + 33 + 33 + 33 + 33 + 33; const ZC_NUM_JS_INPUTS: usize = 2; const ZC_NUM_JS_OUTPUTS: usize = 2; +#[derive(Debug, Clone)] +pub struct Bundle { + pub joinsplits: Vec, + pub joinsplit_pubkey: [u8; 32], + pub joinsplit_sig: [u8; 64], +} + +impl Bundle { + /// The value balance for the bundle. When this is positive, + /// its value is added to the transparent value pool; when it + /// is negative, its value is subtracted from the transparent + /// value pool. + pub fn value_balance(&self) -> Option { + self.joinsplits + .iter() + .try_fold(ZatBalance::zero(), |total, js| total + js.net_value()) + } +} + #[derive(Clone)] +#[allow(clippy::upper_case_acronyms)] pub(crate) enum SproutProof { Groth([u8; GROTH_PROOF_SIZE]), - #[allow(clippy::upper_case_acronyms)] PHGR([u8; PHGR_PROOF_SIZE]), } -impl std::fmt::Debug for SproutProof { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { +impl core::fmt::Debug for SproutProof { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { match self { SproutProof::Groth(_) => write!(f, "SproutProof::Groth"), SproutProof::PHGR(_) => write!(f, "SproutProof::PHGR"), @@ -28,8 +49,8 @@ impl std::fmt::Debug for SproutProof { #[derive(Clone)] pub struct JsDescription { - pub(crate) vpub_old: Amount, - pub(crate) vpub_new: Amount, + pub(crate) vpub_old: ZatBalance, + pub(crate) vpub_new: ZatBalance, pub(crate) anchor: [u8; 32], pub(crate) nullifiers: [[u8; 32]; ZC_NUM_JS_INPUTS], pub(crate) commitments: [[u8; 32]; ZC_NUM_JS_OUTPUTS], @@ -40,8 +61,8 @@ pub struct JsDescription { pub(crate) ciphertexts: [[u8; 601]; ZC_NUM_JS_OUTPUTS], } -impl std::fmt::Debug for JsDescription { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { +impl core::fmt::Debug for JsDescription { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { write!( f, "JSDescription( @@ -70,7 +91,7 @@ impl JsDescription { let vpub_old = { let mut tmp = [0u8; 8]; reader.read_exact(&mut tmp)?; - Amount::from_u64_le_bytes(tmp) + ZatBalance::from_u64_le_bytes(tmp) } .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "vpub_old out of range"))?; @@ -78,7 +99,7 @@ impl JsDescription { let vpub_new = { let mut tmp = [0u8; 8]; reader.read_exact(&mut tmp)?; - Amount::from_u64_le_bytes(tmp) + ZatBalance::from_u64_le_bytes(tmp) } .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "vpub_new out of range"))?; @@ -165,4 +186,12 @@ impl JsDescription { writer.write_all(&self.ciphertexts[0])?; writer.write_all(&self.ciphertexts[1]) } + + /// The net value for the JoinSplit. When this is positive, + /// its value is added to the transparent value pool; when it + /// is negative, its value is subtracted from the transparent + /// value pool. + pub fn net_value(&self) -> ZatBalance { + (self.vpub_new - self.vpub_old).expect("difference is in range [-MAX_MONEY..=MAX_MONEY]") + } } diff --git a/zcash_primitives/src/transaction/components/transparent.rs b/zcash_primitives/src/transaction/components/transparent.rs deleted file mode 100644 index 829ecec19b..0000000000 --- a/zcash_primitives/src/transaction/components/transparent.rs +++ /dev/null @@ -1,106 +0,0 @@ -//! Structs representing the components within Zcash transactions. - -use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; - -use std::io::{self, Read, Write}; - -use crate::legacy::Script; - -use super::amount::Amount; - -#[derive(Clone, Debug, PartialEq)] -pub struct OutPoint { - hash: [u8; 32], - n: u32, -} - -impl OutPoint { - pub fn new(hash: [u8; 32], n: u32) -> Self { - OutPoint { hash, n } - } - - pub fn read(mut reader: R) -> io::Result { - let mut hash = [0u8; 32]; - reader.read_exact(&mut hash)?; - let n = reader.read_u32::()?; - Ok(OutPoint { hash, n }) - } - - pub fn write(&self, mut writer: W) -> io::Result<()> { - writer.write_all(&self.hash)?; - writer.write_u32::(self.n) - } - - pub fn n(&self) -> u32 { - self.n - } - - pub fn hash(&self) -> &[u8; 32] { - &self.hash - } -} - -#[derive(Debug, Clone, PartialEq)] -pub struct TxIn { - pub prevout: OutPoint, - pub script_sig: Script, - pub sequence: u32, -} - -impl TxIn { - #[cfg(feature = "transparent-inputs")] - #[cfg_attr(docsrs, doc(cfg(feature = "transparent-inputs")))] - pub fn new(prevout: OutPoint) -> Self { - TxIn { - prevout, - script_sig: Script::default(), - sequence: std::u32::MAX, - } - } - - pub fn read(mut reader: &mut R) -> io::Result { - let prevout = OutPoint::read(&mut reader)?; - let script_sig = Script::read(&mut reader)?; - let sequence = reader.read_u32::()?; - - Ok(TxIn { - prevout, - script_sig, - sequence, - }) - } - - pub fn write(&self, mut writer: W) -> io::Result<()> { - self.prevout.write(&mut writer)?; - self.script_sig.write(&mut writer)?; - writer.write_u32::(self.sequence) - } -} - -#[derive(Clone, Debug, PartialEq)] -pub struct TxOut { - pub value: Amount, - pub script_pubkey: Script, -} - -impl TxOut { - pub fn read(mut reader: &mut R) -> io::Result { - let value = { - let mut tmp = [0u8; 8]; - reader.read_exact(&mut tmp)?; - Amount::from_nonnegative_i64_le_bytes(tmp) - } - .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "value out of range"))?; - let script_pubkey = Script::read(&mut reader)?; - - Ok(TxOut { - value, - script_pubkey, - }) - } - - pub fn write(&self, mut writer: W) -> io::Result<()> { - writer.write_all(&self.value.to_i64_le_bytes())?; - self.script_pubkey.write(&mut writer) - } -} diff --git a/zcash_primitives/src/transaction/components/tze.rs b/zcash_primitives/src/transaction/components/tze.rs index d26485825d..e62f1450f6 100644 --- a/zcash_primitives/src/transaction/components/tze.rs +++ b/zcash_primitives/src/transaction/components/tze.rs @@ -1,116 +1,183 @@ //! Structs representing the TZE components within Zcash transactions. -#![cfg(feature = "zfuture")] -use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; - -use std::io::{self, Read, Write}; - -use std::convert::TryFrom; +use alloc::vec::Vec; +use core::convert::TryFrom; +use core::fmt::Debug; +use core2::io::{self, Read, Write}; use crate::{ + encoding::{ReadBytesExt, WriteBytesExt}, extensions::transparent as tze, - serialize::{CompactSize, Vector}, + transaction::TxId, }; +use zcash_encoding::{CompactSize, Vector}; +use zcash_protocol::value::Zatoshis; -use super::amount::Amount; +pub mod builder; -fn to_io_error(_: std::num::TryFromIntError) -> io::Error { +fn to_io_error(_: core::num::TryFromIntError) -> io::Error { io::Error::new(io::ErrorKind::InvalidData, "value out of range") } -#[derive(Clone, Debug, PartialEq)] -pub struct TzeOutPoint { - hash: [u8; 32], +pub trait Authorization: Debug { + type Witness: Debug + Clone + PartialEq; +} + +impl Authorization for core::convert::Infallible { + type Witness = core::convert::Infallible; +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct Authorized; + +impl Authorization for Authorized { + type Witness = tze::AuthData; +} + +pub trait MapAuth { + fn map_witness(&self, s: A::Witness) -> B::Witness; + fn map_authorization(&self, s: A) -> B; +} + +/// The identity map. +/// +/// This can be used with [`TransactionData::map_authorization`] when you want to map the +/// authorization of a subset of the transaction's bundles. +/// +/// [`TransactionData::map_authorization`]: crate::transaction::TransactionData::map_authorization +impl MapAuth for () { + fn map_witness( + &self, + s: ::Witness, + ) -> ::Witness { + s + } + + fn map_authorization(&self, a: Authorized) -> Authorized { + a + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Bundle { + pub vin: Vec>, + pub vout: Vec, + pub authorization: A, +} + +impl Bundle { + pub fn map_authorization>(self, f: F) -> Bundle { + Bundle { + vin: self + .vin + .into_iter() + .map(|tzein| TzeIn { + prevout: tzein.prevout, + witness: tzein.witness.map_payload(|p| f.map_witness(p)), + }) + .collect(), + vout: self.vout, + authorization: f.map_authorization(self.authorization), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct OutPoint { + txid: TxId, n: u32, } -impl TzeOutPoint { - pub fn new(hash: [u8; 32], n: u32) -> Self { - TzeOutPoint { hash, n } +impl OutPoint { + pub fn new(txid: TxId, n: u32) -> Self { + OutPoint { txid, n } } pub fn read(mut reader: R) -> io::Result { - let mut hash = [0u8; 32]; - reader.read_exact(&mut hash)?; - let n = reader.read_u32::()?; - Ok(TzeOutPoint { hash, n }) + let txid = TxId::read(&mut reader)?; + let n = reader.read_u32_le()?; + Ok(OutPoint { txid, n }) } pub fn write(&self, mut writer: W) -> io::Result<()> { - writer.write_all(&self.hash)?; - writer.write_u32::(self.n) + self.txid.write(&mut writer)?; + writer.write_u32_le(self.n) } pub fn n(&self) -> u32 { self.n } - pub fn hash(&self) -> &[u8; 32] { - &self.hash + pub fn txid(&self) -> &TxId { + &self.txid } } -#[derive(Clone, Debug, PartialEq)] -pub struct TzeIn { - pub prevout: TzeOutPoint, - pub witness: tze::Witness, +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TzeIn { + pub prevout: OutPoint, + pub witness: tze::Witness, +} + +impl TzeIn { + /// Write without witness data (for signature hashing) + /// + /// This is also used as the prefix for the encoded form used + /// within a serialized transaction. + pub fn write_without_witness(&self, mut writer: W) -> io::Result<()> { + self.prevout.write(&mut writer)?; + + CompactSize::write( + &mut writer, + usize::try_from(self.witness.extension_id).map_err(to_io_error)?, + )?; + + CompactSize::write( + &mut writer, + usize::try_from(self.witness.mode).map_err(to_io_error)?, + ) + } } /// Transaction encoding and decoding functions conforming to [ZIP 222]. /// /// [ZIP 222]: https://zips.z.cash/zip-0222#encoding-in-transactions -impl TzeIn { +impl TzeIn<()> { /// Convenience constructor - pub fn new(prevout: TzeOutPoint, extension_id: u32, mode: u32) -> Self { + pub fn new(prevout: OutPoint, extension_id: u32, mode: u32) -> Self { TzeIn { prevout, witness: tze::Witness { extension_id, mode, - payload: vec![], + payload: (), }, } } +} +impl TzeIn<::Witness> { /// Read witness metadata & payload /// /// Used to decode the encoded form used within a serialized /// transaction. pub fn read(mut reader: &mut R) -> io::Result { - let prevout = TzeOutPoint::read(&mut reader)?; + let prevout = OutPoint::read(&mut reader)?; - let extension_id = CompactSize::read(&mut reader)?; - let mode = CompactSize::read(&mut reader)?; + let extension_id = CompactSize::read_t(&mut reader)?; + let mode = CompactSize::read_t(&mut reader)?; let payload = Vector::read(&mut reader, |r| r.read_u8())?; Ok(TzeIn { prevout, witness: tze::Witness { - extension_id: u32::try_from(extension_id).map_err(to_io_error)?, - mode: u32::try_from(mode).map_err(to_io_error)?, - payload, + extension_id, + mode, + payload: tze::AuthData(payload), }, }) } - /// Write without witness data (for signature hashing) - /// - /// This is also used as the prefix for the encoded form used - /// within a serialized transaction. - pub fn write_without_witness(&self, mut writer: W) -> io::Result<()> { - self.prevout.write(&mut writer)?; - - CompactSize::write( - &mut writer, - usize::try_from(self.witness.extension_id).map_err(to_io_error)?, - )?; - - CompactSize::write( - &mut writer, - usize::try_from(self.witness.mode).map_err(to_io_error)?, - ) - } - /// Write prevout, extension, and mode followed by witness data. /// /// This calls [`write_without_witness`] to serialize witness metadata, @@ -120,13 +187,13 @@ impl TzeIn { /// [`write_without_witness`]: TzeIn::write_without_witness pub fn write(&self, mut writer: W) -> io::Result<()> { self.write_without_witness(&mut writer)?; - Vector::write(&mut writer, &self.witness.payload, |w, b| w.write_u8(*b)) + Vector::write(&mut writer, &self.witness.payload.0, |w, b| w.write_u8(*b)) } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct TzeOut { - pub value: Amount, + pub value: Zatoshis, pub precondition: tze::Precondition, } @@ -135,19 +202,19 @@ impl TzeOut { let value = { let mut tmp = [0; 8]; reader.read_exact(&mut tmp)?; - Amount::from_nonnegative_i64_le_bytes(tmp) + Zatoshis::from_nonnegative_i64_le_bytes(tmp) } .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "value out of range"))?; - let extension_id = CompactSize::read(&mut reader)?; - let mode = CompactSize::read(&mut reader)?; + let extension_id = CompactSize::read_t(&mut reader)?; + let mode = CompactSize::read_t(&mut reader)?; let payload = Vector::read(&mut reader, |r| r.read_u8())?; Ok(TzeOut { value, precondition: tze::Precondition { - extension_id: u32::try_from(extension_id).map_err(to_io_error)?, - mode: u32::try_from(mode).map_err(to_io_error)?, + extension_id, + mode, payload, }, }) @@ -169,3 +236,61 @@ impl TzeOut { }) } } + +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing { + use proptest::collection::vec; + use proptest::prelude::*; + + use crate::{ + consensus::BranchId, + extensions::transparent::{AuthData, Precondition, Witness}, + transaction::components::amount::testing::arb_nonnegative_amount, + transaction::testing::arb_txid, + }; + + use super::{Authorized, Bundle, OutPoint, TzeIn, TzeOut}; + + prop_compose! { + pub fn arb_outpoint()(txid in arb_txid(), n in 0..100u32) -> OutPoint { + OutPoint::new(txid, n) + } + } + + prop_compose! { + pub fn arb_witness()(extension_id in 0..100u32, mode in 0..100u32, payload in vec(any::(), 32..256).prop_map(AuthData)) -> Witness { + Witness { extension_id, mode, payload } + } + } + + prop_compose! { + pub fn arb_tzein()(prevout in arb_outpoint(), witness in arb_witness()) -> TzeIn { + TzeIn { prevout, witness } + } + } + + prop_compose! { + pub fn arb_precondition()(extension_id in 0..100u32, mode in 0..100u32, payload in vec(any::(), 32..256)) -> Precondition { + Precondition { extension_id, mode, payload } + } + } + + prop_compose! { + pub fn arb_tzeout()(value in arb_nonnegative_amount(), precondition in arb_precondition()) -> TzeOut { + TzeOut { value: value.into(), precondition } + } + } + + prop_compose! { + pub fn arb_bundle(branch_id: BranchId)( + vin in vec(arb_tzein(), 0..10), + vout in vec(arb_tzeout(), 0..10), + ) -> Option> { + if branch_id != BranchId::ZFuture || (vin.is_empty() && vout.is_empty()) { + None + } else { + Some(Bundle { vin, vout, authorization: Authorized }) + } + } + } +} diff --git a/zcash_primitives/src/transaction/components/tze/builder.rs b/zcash_primitives/src/transaction/components/tze/builder.rs new file mode 100644 index 0000000000..1625b6d7d7 --- /dev/null +++ b/zcash_primitives/src/transaction/components/tze/builder.rs @@ -0,0 +1,194 @@ +//! Types and functions for building TZE transaction components + +use alloc::boxed::Box; +use alloc::vec::Vec; +use core::fmt; + +use crate::{ + extensions::transparent::{self as tze, ToPayload}, + transaction::{ + self as tx, + components::tze::{Authorization, Authorized, Bundle, OutPoint, TzeIn, TzeOut}, + }, +}; +use zcash_protocol::value::{BalanceError, ZatBalance, Zatoshis}; + +#[derive(Debug, PartialEq, Eq)] +pub enum Error { + InvalidAmount, + WitnessModeMismatch(u32, u32), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Error::InvalidAmount => write!(f, "Invalid amount"), + Error::WitnessModeMismatch(expected, actual) => + write!(f, "TZE witness builder returned a mode that did not match the mode with which the input was initially constructed: expected = {:?}, actual = {:?}", expected, actual), + } + } +} + +#[allow(clippy::type_complexity)] +pub struct TzeSigner<'a, BuildCtx> { + builder: Box Result<(u32, Vec), Error> + 'a>, +} + +#[derive(Clone)] +pub struct TzeBuildInput { + tzein: TzeIn<()>, + coin: TzeOut, +} + +impl TzeBuildInput { + pub fn outpoint(&self) -> &OutPoint { + &self.tzein.prevout + } + pub fn coin(&self) -> &TzeOut { + &self.coin + } +} + +pub struct TzeBuilder<'a, BuildCtx> { + signers: Vec>, + vin: Vec, + vout: Vec, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct Unauthorized; + +impl Authorization for Unauthorized { + type Witness = (); +} + +impl<'a, BuildCtx> TzeBuilder<'a, BuildCtx> { + pub fn empty() -> Self { + TzeBuilder { + signers: vec![], + vin: vec![], + vout: vec![], + } + } + + pub fn inputs(&self) -> &[TzeBuildInput] { + &self.vin + } + + pub fn outputs(&self) -> &[TzeOut] { + &self.vout + } + + pub fn add_input( + &mut self, + extension_id: u32, + mode: u32, + (outpoint, coin): (OutPoint, TzeOut), + witness_builder: WBuilder, + ) where + WBuilder: 'a + FnOnce(&BuildCtx) -> Result, + { + self.vin.push(TzeBuildInput { + tzein: TzeIn::new(outpoint, extension_id, mode), + coin, + }); + self.signers.push(TzeSigner { + builder: Box::new(move |ctx| witness_builder(ctx).map(|x| x.to_payload())), + }); + } + + pub fn add_output( + &mut self, + extension_id: u32, + value: Zatoshis, + guarded_by: &G, + ) -> () { + let (mode, payload) = guarded_by.to_payload(); + self.vout.push(TzeOut { + value, + precondition: tze::Precondition { + extension_id, + mode, + payload, + }, + }); + } + + pub fn value_balance(&self) -> Result { + let total_in = self + .vin + .iter() + .map(|tzi| tzi.coin.value) + .sum::>() + .ok_or(BalanceError::Overflow)?; + + let total_out = self + .vout + .iter() + .map(|tzo| tzo.value) + .sum::>() + .ok_or(BalanceError::Overflow)?; + + (ZatBalance::from(total_in) - ZatBalance::from(total_out)).ok_or(BalanceError::Underflow) + } + + pub fn build(self) -> (Option>, Vec>) { + if self.vin.is_empty() && self.vout.is_empty() { + (None, vec![]) + } else { + ( + Some(Bundle { + vin: self.vin.iter().map(|vin| vin.tzein.clone()).collect(), + vout: self.vout.clone(), + authorization: Unauthorized, + }), + self.signers, + ) + } + } +} + +impl Bundle { + pub fn into_authorized( + self, + unauthed_tx: &tx::TransactionData, + signers: Vec>>, + ) -> Result, Error> { + // Create TZE input witnesses + let payloads = signers + .into_iter() + .zip(self.vin.iter()) + .into_iter() + .map(|(signer, tzein)| { + // The witness builder function should have cached/closed over whatever data was + // necessary for the witness to commit to at the time it was added to the + // transaction builder; here, it then computes those commitments. + let (mode, payload) = (signer.builder)(unauthed_tx)?; + let input_mode = tzein.witness.mode; + if mode != input_mode { + return Err(Error::WitnessModeMismatch(input_mode, mode)); + } + + Ok(tze::AuthData(payload)) + }) + .collect::, Error>>()?; + + Ok(Bundle { + vin: self + .vin + .into_iter() + .zip(payloads.into_iter()) + .map(|(tzein, payload)| TzeIn { + prevout: tzein.prevout, + witness: tze::Witness { + extension_id: tzein.witness.extension_id, + mode: tzein.witness.mode, + payload, + }, + }) + .collect(), + vout: self.vout, + authorization: Authorized, + }) + } +} diff --git a/zcash_primitives/src/transaction/fees.rs b/zcash_primitives/src/transaction/fees.rs new file mode 100644 index 0000000000..353bce8eb4 --- /dev/null +++ b/zcash_primitives/src/transaction/fees.rs @@ -0,0 +1,62 @@ +//! Abstractions and types related to fee calculations. + +use crate::transaction::fees::transparent::InputSize; +use zcash_protocol::{ + consensus::{self, BlockHeight}, + value::Zatoshis, +}; + +#[cfg(feature = "non-standard-fees")] +pub mod fixed; +pub mod transparent; +pub mod zip317; + +#[cfg(zcash_unstable = "zfuture")] +pub mod tze; + +/// A trait that represents the ability to compute the fees that must be paid +/// by a transaction having a specified set of inputs and outputs. +pub trait FeeRule { + type Error; + + /// Computes the total fee required for a transaction given the provided inputs and outputs. + /// + /// Implementations of this method should compute the fee amount given exactly the inputs and + /// outputs specified, and should NOT compute speculative fees given any additional change + /// outputs that may need to be created in order for inputs and outputs to balance. + #[allow(clippy::too_many_arguments)] + fn fee_required( + &self, + params: &P, + target_height: BlockHeight, + transparent_input_sizes: impl IntoIterator, + transparent_output_sizes: impl IntoIterator, + sapling_input_count: usize, + sapling_output_count: usize, + orchard_action_count: usize, + ) -> Result; +} + +/// A trait that represents the ability to compute the fees that must be paid by a transaction +/// having a specified set of inputs and outputs, for use when experimenting with the TZE feature. +#[cfg(zcash_unstable = "zfuture")] +pub trait FutureFeeRule: FeeRule { + /// Computes the total fee required for a transaction given the provided inputs and outputs. + /// + /// Implementations of this method should compute the fee amount given exactly the inputs and + /// outputs specified, and should NOT compute speculative fees given any additional change + /// outputs that may need to be created in order for inputs and outputs to balance. + #[allow(clippy::too_many_arguments)] + fn fee_required_zfuture( + &self, + params: &P, + target_height: BlockHeight, + transparent_input_sizes: impl IntoIterator, + transparent_output_sizes: impl IntoIterator, + sapling_input_count: usize, + sapling_output_count: usize, + orchard_action_count: usize, + tze_inputs: &[impl tze::InputView], + tze_outputs: &[impl tze::OutputView], + ) -> Result; +} diff --git a/zcash_primitives/src/transaction/fees/fixed.rs b/zcash_primitives/src/transaction/fees/fixed.rs new file mode 100644 index 0000000000..7674e4cb29 --- /dev/null +++ b/zcash_primitives/src/transaction/fees/fixed.rs @@ -0,0 +1,63 @@ +use crate::transaction::fees::transparent; + +use zcash_protocol::{ + consensus::{self, BlockHeight}, + value::Zatoshis, +}; + +#[cfg(zcash_unstable = "zfuture")] +use crate::transaction::fees::tze; + +/// A fee rule that always returns a fixed fee, irrespective of the structure of +/// the transaction being constructed. +#[derive(Clone, Copy, Debug)] +pub struct FeeRule { + fixed_fee: Zatoshis, +} + +impl FeeRule { + /// Creates a new nonstandard fixed fee rule with the specified fixed fee. + pub fn non_standard(fixed_fee: Zatoshis) -> Self { + Self { fixed_fee } + } + + /// Returns the fixed fee amount which this rule was configured. + pub fn fixed_fee(&self) -> Zatoshis { + self.fixed_fee + } +} + +impl super::FeeRule for FeeRule { + type Error = core::convert::Infallible; + + fn fee_required( + &self, + _params: &P, + _target_height: BlockHeight, + _transparent_input_sizes: impl IntoIterator, + _transparent_output_sizes: impl IntoIterator, + _sapling_input_count: usize, + _sapling_output_count: usize, + _orchard_action_count: usize, + ) -> Result { + Ok(self.fixed_fee) + } +} + +#[cfg(zcash_unstable = "zfuture")] +impl super::FutureFeeRule for FeeRule { + fn fee_required_zfuture( + &self, + _params: &P, + _target_height: BlockHeight, + _transparent_input_sizes: impl IntoIterator, + _transparent_output_sizes: impl IntoIterator, + _sapling_input_count: usize, + _sapling_output_count: usize, + _orchard_action_count: usize, + _tze_inputs: &[impl tze::InputView], + _tze_outputs: &[impl tze::OutputView], + ) -> Result { + Ok(self.fixed_fee) + } +} diff --git a/zcash_primitives/src/transaction/fees/transparent.rs b/zcash_primitives/src/transaction/fees/transparent.rs new file mode 100644 index 0000000000..aa09c075e4 --- /dev/null +++ b/zcash_primitives/src/transaction/fees/transparent.rs @@ -0,0 +1,95 @@ +//! Types related to computation of fees and change related to the transparent components +//! of a transaction. + +use core::convert::Infallible; + +use crate::transaction::fees::zip317::P2PKH_STANDARD_INPUT_SIZE; +use transparent::{ + address::{Script, TransparentAddress}, + bundle::{OutPoint, TxOut}, +}; +use zcash_protocol::value::Zatoshis; + +#[cfg(feature = "transparent-inputs")] +use transparent::builder::TransparentInputInfo; + +/// The size of a transparent input, or the outpoint corresponding to the input +/// if the size of the script required to spend that input is unknown. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum InputSize { + /// The txin size is known. + Known(usize), + /// The size of the script required to spend this input (and therefore the txin size) + /// is unknown. + Unknown(OutPoint), +} + +impl InputSize { + /// An `InputSize` corresponding to the upper bound on the size of a P2PKH input used by ZIP 317. + pub const STANDARD_P2PKH: InputSize = InputSize::Known(P2PKH_STANDARD_INPUT_SIZE); +} + +/// This trait provides a minimized view of a transparent input suitable for use in +/// fee and change computation. +pub trait InputView: core::fmt::Debug { + /// The outpoint to which the input refers. + fn outpoint(&self) -> &OutPoint; + + /// The previous output being spent. + fn coin(&self) -> &TxOut; + + /// The size of the transparent script required to spend this input. + fn serialized_size(&self) -> InputSize { + match self.coin().script_pubkey.address() { + Some(TransparentAddress::PublicKeyHash(_)) => InputSize::STANDARD_P2PKH, + _ => InputSize::Unknown(self.outpoint().clone()), + } + } +} + +#[cfg(feature = "transparent-inputs")] +impl InputView for TransparentInputInfo { + fn outpoint(&self) -> &OutPoint { + self.outpoint() + } + + fn coin(&self) -> &TxOut { + self.coin() + } +} + +impl InputView for Infallible { + fn outpoint(&self) -> &OutPoint { + unreachable!() + } + fn coin(&self) -> &TxOut { + unreachable!() + } +} + +/// This trait provides a minimized view of a transparent output suitable for use in +/// fee and change computation. +pub trait OutputView: core::fmt::Debug { + /// Returns the value of the output being created. + fn value(&self) -> Zatoshis; + + /// Returns the script corresponding to the newly created output. + fn script_pubkey(&self) -> &Script; + + /// Returns the serialized size of the txout. + fn serialized_size(&self) -> usize { + // The serialized size of a transparent `TxOut` is the serialized size of an amount + // plus the serialized size of the script pubkey. + 8 + self.script_pubkey().serialized_size() + } +} + +impl OutputView for TxOut { + fn value(&self) -> Zatoshis { + self.value + } + + fn script_pubkey(&self) -> &Script { + &self.script_pubkey + } +} diff --git a/zcash_primitives/src/transaction/fees/tze.rs b/zcash_primitives/src/transaction/fees/tze.rs new file mode 100644 index 0000000000..457f1ae680 --- /dev/null +++ b/zcash_primitives/src/transaction/fees/tze.rs @@ -0,0 +1,44 @@ +//! Abstractions and types related to fee calculations for TZE components of a transaction. + +use crate::{ + extensions::transparent as tze, + transaction::components::tze::{builder::TzeBuildInput, OutPoint, TzeOut}, +}; +use zcash_protocol::value::Zatoshis; + +/// This trait provides a minimized view of a TZE input suitable for use in +/// fee computation. +pub trait InputView { + /// The outpoint to which the input refers. + fn outpoint(&self) -> &OutPoint; + /// The previous output being consumed. + fn coin(&self) -> &TzeOut; +} + +impl InputView for TzeBuildInput { + fn outpoint(&self) -> &OutPoint { + self.outpoint() + } + fn coin(&self) -> &TzeOut { + self.coin() + } +} + +/// This trait provides a minimized view of a TZE output suitable for use in +/// fee computation. +pub trait OutputView { + /// The value of the newly created output + fn value(&self) -> Zatoshis; + /// The precondition that must be satisfied in order to spend this output. + fn precondition(&self) -> &tze::Precondition; +} + +impl OutputView for TzeOut { + fn value(&self) -> Zatoshis { + self.value + } + + fn precondition(&self) -> &tze::Precondition { + &self.precondition + } +} diff --git a/zcash_primitives/src/transaction/fees/zip317.rs b/zcash_primitives/src/transaction/fees/zip317.rs new file mode 100644 index 0000000000..52ba98bfe9 --- /dev/null +++ b/zcash_primitives/src/transaction/fees/zip317.rs @@ -0,0 +1,196 @@ +//! Types related to implementing a [`FeeRule`] provides [ZIP 317] fee calculation. +//! +//! [`FeeRule`]: crate::transaction::fees::FeeRule +//! [ZIP 317]: https//zips.z.cash/zip-0317 +use alloc::vec::Vec; +use core::cmp::max; + +use ::transparent::bundle::OutPoint; +use zcash_protocol::{ + consensus::{self, BlockHeight}, + value::{BalanceError, Zatoshis}, +}; + +use crate::transaction::fees::transparent; + +/// The standard [ZIP 317] marginal fee. +/// +/// [ZIP 317]: https//zips.z.cash/zip-0317 +pub const MARGINAL_FEE: Zatoshis = Zatoshis::const_from_u64(5_000); + +/// The minimum number of logical actions that must be paid according to [ZIP 317]. +/// +/// [ZIP 317]: https//zips.z.cash/zip-0317 +pub const GRACE_ACTIONS: usize = 2; + +/// The standard size of a P2PKH input, in bytes, according to [ZIP 317]. +/// +/// [ZIP 317]: https//zips.z.cash/zip-0317 +pub const P2PKH_STANDARD_INPUT_SIZE: usize = 150; + +/// The standard size of a P2PKH output, in bytes, according to [ZIP 317]. +/// +/// [ZIP 317]: https//zips.z.cash/zip-0317 +pub const P2PKH_STANDARD_OUTPUT_SIZE: usize = 34; + +/// The minimum conventional fee computed from the standard [ZIP 317] constants. Equivalent to +/// `MARGINAL_FEE * GRACE_ACTIONS`. +/// +/// [ZIP 317]: https//zips.z.cash/zip-0317 +pub const MINIMUM_FEE: Zatoshis = Zatoshis::const_from_u64(10_000); + +/// A [`FeeRule`] implementation that implements the [ZIP 317] fee rule. +/// +/// This fee rule supports Orchard, Sapling, and (P2PKH only) transparent inputs. +/// Returns an error if a coin containing a non-P2PKH script is provided as an input. +/// +/// This fee rule may slightly overestimate fees in case where the user is attempting +/// to spend a large number of transparent inputs. This is intentional and is relied +/// on for the correctness of transaction construction algorithms in the +/// `zcash_client_backend` crate. +/// +/// [`FeeRule`]: crate::transaction::fees::FeeRule +/// [ZIP 317]: https//zips.z.cash/zip-0317 +#[derive(Clone, Debug)] +pub struct FeeRule { + marginal_fee: Zatoshis, + grace_actions: usize, + p2pkh_standard_input_size: usize, + p2pkh_standard_output_size: usize, +} + +impl FeeRule { + /// Construct a new FeeRule using the standard [ZIP 317] constants. + /// + /// [ZIP 317]: https//zips.z.cash/zip-0317 + pub fn standard() -> Self { + Self { + marginal_fee: MARGINAL_FEE, + grace_actions: GRACE_ACTIONS, + p2pkh_standard_input_size: P2PKH_STANDARD_INPUT_SIZE, + p2pkh_standard_output_size: P2PKH_STANDARD_OUTPUT_SIZE, + } + } + + /// Construct a new FeeRule instance with the specified parameter values. + /// + /// Using this fee rule with + /// ```compile_fail + /// marginal_fee < 5000 || grace_actions < 2 + /// || p2pkh_standard_input_size > P2PKH_STANDARD_INPUT_SIZE + /// || p2pkh_standard_output_size > P2PKH_STANDARD_OUTPUT_SIZE + /// ``` + /// violates ZIP 317, and might cause transactions built with it to fail. + /// + /// Returns `None` if either `p2pkh_standard_input_size` or `p2pkh_standard_output_size` are + /// zero. + #[cfg(feature = "non-standard-fees")] + pub fn non_standard( + marginal_fee: Zatoshis, + grace_actions: usize, + p2pkh_standard_input_size: usize, + p2pkh_standard_output_size: usize, + ) -> Option { + if p2pkh_standard_input_size == 0 || p2pkh_standard_output_size == 0 { + None + } else { + Some(Self { + marginal_fee, + grace_actions, + p2pkh_standard_input_size, + p2pkh_standard_output_size, + }) + } + } + + /// Returns the ZIP 317 marginal fee. + pub fn marginal_fee(&self) -> Zatoshis { + self.marginal_fee + } + /// Returns the ZIP 317 number of grace actions + pub fn grace_actions(&self) -> usize { + self.grace_actions + } + /// Returns the ZIP 317 standard P2PKH input size + pub fn p2pkh_standard_input_size(&self) -> usize { + self.p2pkh_standard_input_size + } + /// Returns the ZIP 317 standard P2PKH output size + pub fn p2pkh_standard_output_size(&self) -> usize { + self.p2pkh_standard_output_size + } +} + +/// Errors that can occur in ZIP 317 fee computation +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum FeeError { + /// An overflow or underflow of amount computation occurred. + Balance(BalanceError), + /// Transparent inputs provided to the fee calculation included coins that do not pay to + /// standard P2pkh scripts. + NonP2pkhInputs(Vec), +} + +impl From for FeeError { + fn from(err: BalanceError) -> Self { + FeeError::Balance(err) + } +} + +impl core::fmt::Display for FeeError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + match &self { + FeeError::Balance(e) => write!( + f, + "A balance calculation violated amount validity bounds: {}.", + e + ), + FeeError::NonP2pkhInputs(_) => write!(f, "Only P2PKH inputs are supported."), + } + } +} + +impl super::FeeRule for FeeRule { + type Error = FeeError; + + fn fee_required( + &self, + _params: &P, + _target_height: BlockHeight, + transparent_input_sizes: impl IntoIterator, + transparent_output_sizes: impl IntoIterator, + sapling_input_count: usize, + sapling_output_count: usize, + orchard_action_count: usize, + ) -> Result { + let mut t_in_total_size: usize = 0; + let mut non_p2pkh_outpoints = vec![]; + for sz in transparent_input_sizes.into_iter() { + match sz { + transparent::InputSize::Known(s) => { + t_in_total_size += s; + } + transparent::InputSize::Unknown(outpoint) => { + non_p2pkh_outpoints.push(outpoint.clone()); + } + } + } + + if !non_p2pkh_outpoints.is_empty() { + return Err(FeeError::NonP2pkhInputs(non_p2pkh_outpoints)); + } + + let t_out_total_size = transparent_output_sizes.into_iter().sum(); + + let ceildiv = |num: usize, den: usize| num.div_ceil(den); + + let logical_actions = max( + ceildiv(t_in_total_size, self.p2pkh_standard_input_size), + ceildiv(t_out_total_size, self.p2pkh_standard_output_size), + ) + max(sapling_input_count, sapling_output_count) + + orchard_action_count; + + (self.marginal_fee * max(self.grace_actions, logical_actions)) + .ok_or_else(|| BalanceError::Overflow.into()) + } +} diff --git a/zcash_primitives/src/transaction/mod.rs b/zcash_primitives/src/transaction/mod.rs index 78bdf9546b..4727537fa4 100644 --- a/zcash_primitives/src/transaction/mod.rs +++ b/zcash_primitives/src/transaction/mod.rs @@ -1,55 +1,65 @@ //! Structs and methods for handling Zcash transactions. - -use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; -use std::fmt; -use std::io::{self, Read, Write}; -use std::ops::Deref; - -use crate::{consensus::BlockHeight, sapling::redjubjub::Signature, serialize::Vector}; - -use self::util::sha256d::{HashReader, HashWriter}; - pub mod builder; pub mod components; -mod sighash; +pub mod fees; +pub mod sighash; +pub mod sighash_v4; +pub mod sighash_v5; +pub mod txid; pub mod util; #[cfg(test)] mod tests; -pub use self::sighash::{signature_hash, signature_hash_data, SignableInput, SIGHASH_ALL}; - -use self::components::{Amount, JsDescription, OutputDescription, SpendDescription, TxIn, TxOut}; - -#[cfg(feature = "zfuture")] -use self::components::{TzeIn, TzeOut}; +use crate::encoding::{ReadBytesExt, WriteBytesExt}; +use blake2b_simd::Hash as Blake2bHash; +use core::convert::TryFrom; +use core::fmt::Debug; +use core::ops::Deref; +use core2::io::{self, Read, Write}; + +use ::transparent::bundle::{self as transparent, OutPoint, TxIn, TxOut}; +use zcash_encoding::{CompactSize, Vector}; +use zcash_protocol::{ + consensus::{BlockHeight, BranchId}, + value::{BalanceError, ZatBalance}, +}; + +use self::{ + components::{ + orchard as orchard_serialization, sapling as sapling_serialization, + sprout::{self, JsDescription}, + }, + txid::{to_txid, BlockTxCommitmentDigester, TxIdDigester}, + util::sha256d::{HashReader, HashWriter}, +}; + +#[cfg(feature = "circuits")] +use ::sapling::builder as sapling_builder; + +#[cfg(zcash_unstable = "zfuture")] +use self::components::tze::{self, TzeIn, TzeOut}; const OVERWINTER_VERSION_GROUP_ID: u32 = 0x03C48270; const OVERWINTER_TX_VERSION: u32 = 3; const SAPLING_VERSION_GROUP_ID: u32 = 0x892F2085; const SAPLING_TX_VERSION: u32 = 4; +const V5_TX_VERSION: u32 = 5; +const V5_VERSION_GROUP_ID: u32 = 0x26A7270A; + /// These versions are used exclusively for in-development transaction /// serialization, and will never be active under the consensus rules. /// When new consensus transaction versions are added, all call sites /// using these constants should be inspected, and use of these constants /// should be removed as appropriate in favor of the new consensus /// transaction version and group. -#[cfg(feature = "zfuture")] +#[cfg(zcash_unstable = "zfuture")] const ZFUTURE_VERSION_GROUP_ID: u32 = 0xFFFFFFFF; -#[cfg(feature = "zfuture")] +#[cfg(zcash_unstable = "zfuture")] const ZFUTURE_TX_VERSION: u32 = 0x0000FFFF; -#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)] -pub struct TxId(pub [u8; 32]); - -impl fmt::Display for TxId { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut data = self.0; - data.reverse(); - formatter.write_str(&hex::encode(data)) - } -} +pub use zcash_protocol::TxId; /// The set of defined transaction format versions. /// @@ -63,21 +73,23 @@ pub enum TxVersion { Sprout(u32), Overwinter, Sapling, - #[cfg(feature = "zfuture")] + Zip225, + #[cfg(zcash_unstable = "zfuture")] ZFuture, } impl TxVersion { pub fn read(mut reader: R) -> io::Result { - let header = reader.read_u32::()?; + let header = reader.read_u32_le()?; let overwintered = (header >> 31) == 1; let version = header & 0x7FFFFFFF; if overwintered { - match (version, reader.read_u32::()?) { + match (version, reader.read_u32_le()?) { (OVERWINTER_TX_VERSION, OVERWINTER_VERSION_GROUP_ID) => Ok(TxVersion::Overwinter), (SAPLING_TX_VERSION, SAPLING_VERSION_GROUP_ID) => Ok(TxVersion::Sapling), - #[cfg(feature = "zfuture")] + (V5_TX_VERSION, V5_VERSION_GROUP_ID) => Ok(TxVersion::Zip225), + #[cfg(zcash_unstable = "zfuture")] (ZFUTURE_TX_VERSION, ZFUTURE_VERSION_GROUP_ID) => Ok(TxVersion::ZFuture), _ => Err(io::Error::new( io::ErrorKind::InvalidInput, @@ -106,7 +118,8 @@ impl TxVersion { TxVersion::Sprout(v) => *v, TxVersion::Overwinter => OVERWINTER_TX_VERSION, TxVersion::Sapling => SAPLING_TX_VERSION, - #[cfg(feature = "zfuture")] + TxVersion::Zip225 => V5_TX_VERSION, + #[cfg(zcash_unstable = "zfuture")] TxVersion::ZFuture => ZFUTURE_TX_VERSION, } } @@ -116,49 +129,129 @@ impl TxVersion { TxVersion::Sprout(_) => 0, TxVersion::Overwinter => OVERWINTER_VERSION_GROUP_ID, TxVersion::Sapling => SAPLING_VERSION_GROUP_ID, - #[cfg(feature = "zfuture")] + TxVersion::Zip225 => V5_VERSION_GROUP_ID, + #[cfg(zcash_unstable = "zfuture")] TxVersion::ZFuture => ZFUTURE_VERSION_GROUP_ID, } } pub fn write(&self, mut writer: W) -> io::Result<()> { - writer.write_u32::(self.header())?; + writer.write_u32_le(self.header())?; match self { TxVersion::Sprout(_) => Ok(()), - _ => writer.write_u32::(self.version_group_id()), + _ => writer.write_u32_le(self.version_group_id()), } } + /// Returns `true` if this transaction version supports the Sprout protocol. pub fn has_sprout(&self) -> bool { match self { TxVersion::Sprout(v) => *v >= 2u32, TxVersion::Overwinter | TxVersion::Sapling => true, - #[cfg(feature = "zfuture")] + TxVersion::Zip225 => false, + #[cfg(zcash_unstable = "zfuture")] TxVersion::ZFuture => true, } } - pub fn uses_groth_proofs(&self) -> bool { + pub fn has_overwinter(&self) -> bool { + !matches!(self, TxVersion::Sprout(_)) + } + + /// Returns `true` if this transaction version supports the Sapling protocol. + pub fn has_sapling(&self) -> bool { match self { TxVersion::Sprout(_) | TxVersion::Overwinter => false, TxVersion::Sapling => true, - #[cfg(feature = "zfuture")] + TxVersion::Zip225 => true, + #[cfg(zcash_unstable = "zfuture")] TxVersion::ZFuture => true, } } + + /// Returns `true` if this transaction version supports the Orchard protocol. + pub fn has_orchard(&self) -> bool { + match self { + TxVersion::Sprout(_) | TxVersion::Overwinter | TxVersion::Sapling => false, + TxVersion::Zip225 => true, + #[cfg(zcash_unstable = "zfuture")] + TxVersion::ZFuture => true, + } + } + + #[cfg(zcash_unstable = "zfuture")] + pub fn has_tze(&self) -> bool { + matches!(self, TxVersion::ZFuture) + } + + /// Suggests the transaction version that should be used in the given Zcash epoch. + pub fn suggested_for_branch(consensus_branch_id: BranchId) -> Self { + match consensus_branch_id { + BranchId::Sprout => TxVersion::Sprout(2), + BranchId::Overwinter => TxVersion::Overwinter, + BranchId::Sapling | BranchId::Blossom | BranchId::Heartwood | BranchId::Canopy => { + TxVersion::Sapling + } + BranchId::Nu5 => TxVersion::Zip225, + BranchId::Nu6 => TxVersion::Zip225, + #[cfg(zcash_unstable = "zfuture")] + BranchId::ZFuture => TxVersion::ZFuture, + } + } +} + +/// Authorization state for a bundle of transaction data. +pub trait Authorization { + type TransparentAuth: transparent::Authorization; + type SaplingAuth: sapling::bundle::Authorization; + type OrchardAuth: orchard::bundle::Authorization; + + #[cfg(zcash_unstable = "zfuture")] + type TzeAuth: tze::Authorization; +} + +/// [`Authorization`] marker type for fully-authorized transactions. +#[derive(Debug)] +pub struct Authorized; + +impl Authorization for Authorized { + type TransparentAuth = transparent::Authorized; + type SaplingAuth = sapling::bundle::Authorized; + type OrchardAuth = orchard::bundle::Authorized; + + #[cfg(zcash_unstable = "zfuture")] + type TzeAuth = tze::Authorized; +} + +/// [`Authorization`] marker type for transactions without authorization data. +/// +/// Currently this includes Sapling proofs because the types in this crate support v4 +/// transactions, which commit to the Sapling proofs in the transaction digest. +pub struct Unauthorized; + +#[cfg(feature = "circuits")] +impl Authorization for Unauthorized { + type TransparentAuth = ::transparent::builder::Unauthorized; + type SaplingAuth = + sapling_builder::InProgress; + type OrchardAuth = + orchard::builder::InProgress; + + #[cfg(zcash_unstable = "zfuture")] + type TzeAuth = tze::builder::Unauthorized; } /// A Zcash transaction. -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct Transaction { txid: TxId, - data: TransactionData, + data: TransactionData, } impl Deref for Transaction { - type Target = TransactionData; + type Target = TransactionData; - fn deref(&self) -> &TransactionData { + fn deref(&self) -> &TransactionData { &self.data } } @@ -169,502 +262,782 @@ impl PartialEq for Transaction { } } -#[derive(Clone)] -pub struct TransactionData { - pub version: TxVersion, - pub vin: Vec, - pub vout: Vec, - #[cfg(feature = "zfuture")] - pub tze_inputs: Vec, - #[cfg(feature = "zfuture")] - pub tze_outputs: Vec, - pub lock_time: u32, - pub expiry_height: BlockHeight, - pub value_balance: Amount, - pub shielded_spends: Vec, - pub shielded_outputs: Vec, - pub joinsplits: Vec, - pub joinsplit_pubkey: Option<[u8; 32]>, - pub joinsplit_sig: Option<[u8; 64]>, - pub binding_sig: Option, +/// The information contained in a Zcash transaction. +#[derive(Debug)] +pub struct TransactionData { + version: TxVersion, + consensus_branch_id: BranchId, + lock_time: u32, + expiry_height: BlockHeight, + transparent_bundle: Option>, + sprout_bundle: Option, + sapling_bundle: Option>, + orchard_bundle: Option>, + #[cfg(zcash_unstable = "zfuture")] + tze_bundle: Option>, } -impl std::fmt::Debug for TransactionData { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - write!( - f, - "TransactionData( - version = {:?}, - vin = {:?}, - vout = {:?},{} - lock_time = {:?}, - expiry_height = {:?}, - value_balance = {:?}, - shielded_spends = {:?}, - shielded_outputs = {:?}, - joinsplits = {:?}, - joinsplit_pubkey = {:?}, - binding_sig = {:?})", - self.version, - self.vin, - self.vout, - { - #[cfg(feature = "zfuture")] - { - format!( - " - tze_inputs = {:?}, - tze_outputs = {:?},", - self.tze_inputs, self.tze_outputs - ) - } - #[cfg(not(feature = "zfuture"))] - "" - }, - self.lock_time, - self.expiry_height, - self.value_balance, - self.shielded_spends, - self.shielded_outputs, - self.joinsplits, - self.joinsplit_pubkey, - self.binding_sig - ) +impl TransactionData { + /// Constructs a `TransactionData` from its constituent parts. + #[allow(clippy::too_many_arguments)] + pub fn from_parts( + version: TxVersion, + consensus_branch_id: BranchId, + lock_time: u32, + expiry_height: BlockHeight, + transparent_bundle: Option>, + sprout_bundle: Option, + sapling_bundle: Option>, + orchard_bundle: Option>, + ) -> Self { + TransactionData { + version, + consensus_branch_id, + lock_time, + expiry_height, + transparent_bundle, + sprout_bundle, + sapling_bundle, + orchard_bundle, + #[cfg(zcash_unstable = "zfuture")] + tze_bundle: None, + } + } + + /// Constructs a `TransactionData` from its constituent parts, including speculative + /// future parts that are not in the current Zcash consensus rules. + #[cfg(zcash_unstable = "zfuture")] + #[allow(clippy::too_many_arguments)] + pub fn from_parts_zfuture( + version: TxVersion, + consensus_branch_id: BranchId, + lock_time: u32, + expiry_height: BlockHeight, + transparent_bundle: Option>, + sprout_bundle: Option, + sapling_bundle: Option>, + orchard_bundle: Option>, + tze_bundle: Option>, + ) -> Self { + TransactionData { + version, + consensus_branch_id, + lock_time, + expiry_height, + transparent_bundle, + sprout_bundle, + sapling_bundle, + orchard_bundle, + tze_bundle, + } } -} -impl Default for TransactionData { - fn default() -> Self { - TransactionData::new() + /// Returns the transaction version. + pub fn version(&self) -> TxVersion { + self.version } -} -impl TransactionData { - pub fn new() -> Self { + /// Returns the Zcash epoch that this transaction can be mined in. + pub fn consensus_branch_id(&self) -> BranchId { + self.consensus_branch_id + } + + pub fn lock_time(&self) -> u32 { + self.lock_time + } + + pub fn expiry_height(&self) -> BlockHeight { + self.expiry_height + } + + pub fn transparent_bundle(&self) -> Option<&transparent::Bundle> { + self.transparent_bundle.as_ref() + } + + pub fn sprout_bundle(&self) -> Option<&sprout::Bundle> { + self.sprout_bundle.as_ref() + } + + pub fn sapling_bundle(&self) -> Option<&sapling::Bundle> { + self.sapling_bundle.as_ref() + } + + pub fn orchard_bundle(&self) -> Option<&orchard::Bundle> { + self.orchard_bundle.as_ref() + } + + #[cfg(zcash_unstable = "zfuture")] + pub fn tze_bundle(&self) -> Option<&tze::Bundle> { + self.tze_bundle.as_ref() + } + + /// Returns the total fees paid by the transaction, given a function that can be used to + /// retrieve the value of previous transactions' transparent outputs that are being spent in + /// this transaction. + pub fn fee_paid(&self, get_prevout: F) -> Result + where + E: From, + F: FnMut(&OutPoint) -> Result, + { + let value_balances = [ + self.transparent_bundle + .as_ref() + .map_or_else(|| Ok(ZatBalance::zero()), |b| b.value_balance(get_prevout))?, + self.sprout_bundle.as_ref().map_or_else( + || Ok(ZatBalance::zero()), + |b| b.value_balance().ok_or(BalanceError::Overflow), + )?, + self.sapling_bundle + .as_ref() + .map_or_else(ZatBalance::zero, |b| *b.value_balance()), + self.orchard_bundle + .as_ref() + .map_or_else(ZatBalance::zero, |b| *b.value_balance()), + ]; + + value_balances + .iter() + .sum::>() + .ok_or_else(|| BalanceError::Overflow.into()) + } + + pub fn digest>(&self, digester: D) -> D::Digest { + digester.combine( + digester.digest_header( + self.version, + self.consensus_branch_id, + self.lock_time, + self.expiry_height, + ), + digester.digest_transparent(self.transparent_bundle.as_ref()), + digester.digest_sapling(self.sapling_bundle.as_ref()), + digester.digest_orchard(self.orchard_bundle.as_ref()), + #[cfg(zcash_unstable = "zfuture")] + digester.digest_tze(self.tze_bundle.as_ref()), + ) + } + + /// Maps the bundles from one type to another. + /// + /// This shouldn't be necessary for most use cases; it is provided for handling the + /// cross-FFI builder logic in `zcashd`. + pub fn map_bundles( + self, + f_transparent: impl FnOnce( + Option>, + ) -> Option>, + f_sapling: impl FnOnce( + Option>, + ) -> Option>, + f_orchard: impl FnOnce( + Option>, + ) -> Option>, + #[cfg(zcash_unstable = "zfuture")] f_tze: impl FnOnce( + Option>, + ) + -> Option>, + ) -> TransactionData { TransactionData { - version: TxVersion::Sapling, - vin: vec![], - vout: vec![], - #[cfg(feature = "zfuture")] - tze_inputs: vec![], - #[cfg(feature = "zfuture")] - tze_outputs: vec![], - lock_time: 0, - expiry_height: 0u32.into(), - value_balance: Amount::zero(), - shielded_spends: vec![], - shielded_outputs: vec![], - joinsplits: vec![], - joinsplit_pubkey: None, - joinsplit_sig: None, - binding_sig: None, + version: self.version, + consensus_branch_id: self.consensus_branch_id, + lock_time: self.lock_time, + expiry_height: self.expiry_height, + transparent_bundle: f_transparent(self.transparent_bundle), + sprout_bundle: self.sprout_bundle, + sapling_bundle: f_sapling(self.sapling_bundle), + orchard_bundle: f_orchard(self.orchard_bundle), + #[cfg(zcash_unstable = "zfuture")] + tze_bundle: f_tze(self.tze_bundle), } } - #[cfg(feature = "zfuture")] - pub fn zfuture() -> Self { + /// Maps the bundles from one type to another with fallible closures. + /// + /// This shouldn't be necessary for most use cases; it is provided for handling the + /// transaction extraction logic in the `pczt` crate. + pub fn try_map_bundles( + self, + f_transparent: impl FnOnce( + Option>, + ) + -> Result>, E>, + f_sapling: impl FnOnce( + Option>, + ) + -> Result>, E>, + f_orchard: impl FnOnce( + Option>, + ) + -> Result>, E>, + #[cfg(zcash_unstable = "zfuture")] f_tze: impl FnOnce( + Option>, + ) -> Result< + Option>, + E, + >, + ) -> Result, E> { + Ok(TransactionData { + version: self.version, + consensus_branch_id: self.consensus_branch_id, + lock_time: self.lock_time, + expiry_height: self.expiry_height, + transparent_bundle: f_transparent(self.transparent_bundle)?, + sprout_bundle: self.sprout_bundle, + sapling_bundle: f_sapling(self.sapling_bundle)?, + orchard_bundle: f_orchard(self.orchard_bundle)?, + #[cfg(zcash_unstable = "zfuture")] + tze_bundle: f_tze(self.tze_bundle)?, + }) + } + + pub fn map_authorization( + self, + f_transparent: impl transparent::MapAuth, + mut f_sapling: impl sapling_serialization::MapAuth, + mut f_orchard: impl orchard_serialization::MapAuth, + #[cfg(zcash_unstable = "zfuture")] f_tze: impl tze::MapAuth, + ) -> TransactionData { TransactionData { - version: TxVersion::ZFuture, - vin: vec![], - vout: vec![], - tze_inputs: vec![], - tze_outputs: vec![], - lock_time: 0, - expiry_height: 0u32.into(), - value_balance: Amount::zero(), - shielded_spends: vec![], - shielded_outputs: vec![], - joinsplits: vec![], - joinsplit_pubkey: None, - joinsplit_sig: None, - binding_sig: None, + version: self.version, + consensus_branch_id: self.consensus_branch_id, + lock_time: self.lock_time, + expiry_height: self.expiry_height, + transparent_bundle: self + .transparent_bundle + .map(|b| b.map_authorization(f_transparent)), + sprout_bundle: self.sprout_bundle, + sapling_bundle: self.sapling_bundle.map(|b| { + b.map_authorization( + &mut f_sapling, + |f, p| f.map_spend_proof(p), + |f, p| f.map_output_proof(p), + |f, s| f.map_auth_sig(s), + |f, a| f.map_authorization(a), + ) + }), + orchard_bundle: self.orchard_bundle.map(|b| { + b.map_authorization( + &mut f_orchard, + |f, _, s| f.map_spend_auth(s), + |f, a| f.map_authorization(a), + ) + }), + #[cfg(zcash_unstable = "zfuture")] + tze_bundle: self.tze_bundle.map(|b| b.map_authorization(f_tze)), } } +} +impl TransactionData { + pub fn sapling_value_balance(&self) -> ZatBalance { + self.sapling_bundle + .as_ref() + .map_or(ZatBalance::zero(), |b| *b.value_balance()) + } +} + +impl TransactionData { pub fn freeze(self) -> io::Result { Transaction::from_data(self) } } impl Transaction { - fn from_data(data: TransactionData) -> io::Result { + fn from_data(data: TransactionData) -> io::Result { + match data.version { + TxVersion::Sprout(_) | TxVersion::Overwinter | TxVersion::Sapling => { + Self::from_data_v4(data) + } + TxVersion::Zip225 => Ok(Self::from_data_v5(data)), + #[cfg(zcash_unstable = "zfuture")] + TxVersion::ZFuture => Ok(Self::from_data_v5(data)), + } + } + + fn from_data_v4(data: TransactionData) -> io::Result { let mut tx = Transaction { - txid: TxId([0; 32]), + txid: TxId::from_bytes([0; 32]), data, }; let mut writer = HashWriter::default(); tx.write(&mut writer)?; - tx.txid.0.copy_from_slice(&writer.into_hash()); + tx.txid = TxId::from_bytes(writer.into_hash().into()); Ok(tx) } + fn from_data_v5(data: TransactionData) -> Self { + let txid = to_txid( + data.version, + data.consensus_branch_id, + &data.digest(TxIdDigester), + ); + + Transaction { txid, data } + } + + pub fn into_data(self) -> TransactionData { + self.data + } + pub fn txid(&self) -> TxId { self.txid } - pub fn read(reader: R) -> io::Result { + pub fn read(reader: R, consensus_branch_id: BranchId) -> io::Result { let mut reader = HashReader::new(reader); let version = TxVersion::read(&mut reader)?; - let is_overwinter_v3 = version == TxVersion::Overwinter; - let is_sapling_v4 = version == TxVersion::Sapling; - - #[cfg(feature = "zfuture")] - let has_tze = version == TxVersion::ZFuture; - #[cfg(not(feature = "zfuture"))] - let has_tze = false; - - let vin = Vector::read(&mut reader, TxIn::read)?; - let vout = Vector::read(&mut reader, TxOut::read)?; - - #[cfg(feature = "zfuture")] - let (tze_inputs, tze_outputs) = if has_tze { - let wi = Vector::read(&mut reader, TzeIn::read)?; - let wo = Vector::read(&mut reader, TzeOut::read)?; - (wi, wo) - } else { - (vec![], vec![]) - }; + match version { + TxVersion::Sprout(_) | TxVersion::Overwinter | TxVersion::Sapling => { + Self::read_v4(reader, version, consensus_branch_id) + } + TxVersion::Zip225 => Self::read_v5(reader.into_base_reader(), version), + #[cfg(zcash_unstable = "zfuture")] + TxVersion::ZFuture => Self::read_v5(reader.into_base_reader(), version), + } + } - let lock_time = reader.read_u32::()?; - let expiry_height: BlockHeight = if is_overwinter_v3 || is_sapling_v4 || has_tze { - reader.read_u32::()?.into() + #[allow(clippy::redundant_closure)] + fn read_v4( + mut reader: HashReader, + version: TxVersion, + consensus_branch_id: BranchId, + ) -> io::Result { + let transparent_bundle = Self::read_transparent(&mut reader)?; + + let lock_time = reader.read_u32_le()?; + let expiry_height: BlockHeight = if version.has_overwinter() { + reader.read_u32_le()?.into() } else { 0u32.into() }; - let (value_balance, shielded_spends, shielded_outputs) = if is_sapling_v4 || has_tze { - let vb = { - let mut tmp = [0; 8]; - reader.read_exact(&mut tmp)?; - Amount::from_i64_le_bytes(tmp) - } - .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "valueBalance out of range"))?; - let ss = Vector::read(&mut reader, SpendDescription::read)?; - let so = Vector::read(&mut reader, OutputDescription::read)?; - (vb, ss, so) - } else { - (Amount::zero(), vec![], vec![]) - }; + let (value_balance, shielded_spends, shielded_outputs) = + sapling_serialization::read_v4_components(&mut reader, version.has_sapling())?; - let (joinsplits, joinsplit_pubkey, joinsplit_sig) = if version.has_sprout() { - let jss = Vector::read(&mut reader, |r| { - JsDescription::read(r, version.uses_groth_proofs()) + let sprout_bundle = if version.has_sprout() { + let joinsplits = Vector::read(&mut reader, |r| { + JsDescription::read(r, version.has_sapling()) })?; - let (pubkey, sig) = if !jss.is_empty() { - let mut joinsplit_pubkey = [0; 32]; - let mut joinsplit_sig = [0; 64]; - reader.read_exact(&mut joinsplit_pubkey)?; - reader.read_exact(&mut joinsplit_sig)?; - (Some(joinsplit_pubkey), Some(joinsplit_sig)) + + if !joinsplits.is_empty() { + let mut bundle = sprout::Bundle { + joinsplits, + joinsplit_pubkey: [0; 32], + joinsplit_sig: [0; 64], + }; + reader.read_exact(&mut bundle.joinsplit_pubkey)?; + reader.read_exact(&mut bundle.joinsplit_sig)?; + Some(bundle) } else { - (None, None) - }; - (jss, pubkey, sig) + None + } } else { - (vec![], None, None) + None }; - let binding_sig = if (is_sapling_v4 || has_tze) + let binding_sig = if version.has_sapling() && !(shielded_spends.is_empty() && shielded_outputs.is_empty()) { - Some(Signature::read(&mut reader)?) + let mut sig = [0; 64]; + reader.read_exact(&mut sig)?; + Some(redjubjub::Signature::from(sig)) } else { None }; let mut txid = [0; 32]; - txid.copy_from_slice(&reader.into_hash()); + let hash_bytes = reader.into_hash(); + txid.copy_from_slice(&hash_bytes); Ok(Transaction { - txid: TxId(txid), + txid: TxId::from_bytes(txid), data: TransactionData { version, - vin, - vout, - #[cfg(feature = "zfuture")] - tze_inputs, - #[cfg(feature = "zfuture")] - tze_outputs, + consensus_branch_id, lock_time, expiry_height, - value_balance, - shielded_spends, - shielded_outputs, - joinsplits, - joinsplit_pubkey, - joinsplit_sig, - binding_sig, + transparent_bundle, + sprout_bundle, + sapling_bundle: binding_sig.and_then(|binding_sig| { + sapling::Bundle::from_parts( + shielded_spends, + shielded_outputs, + value_balance, + sapling::bundle::Authorized { binding_sig }, + ) + }), + orchard_bundle: None, + #[cfg(zcash_unstable = "zfuture")] + tze_bundle: None, }, }) } - pub fn write(&self, mut writer: W) -> io::Result<()> { - self.version.write(&mut writer)?; + fn read_transparent( + mut reader: R, + ) -> io::Result>> { + let vin = Vector::read(&mut reader, TxIn::read)?; + let vout = Vector::read(&mut reader, TxOut::read)?; + Ok(if vin.is_empty() && vout.is_empty() { + None + } else { + Some(transparent::Bundle { + vin, + vout, + authorization: transparent::Authorized, + }) + }) + } - let is_overwinter_v3 = self.version == TxVersion::Overwinter; - let is_sapling_v4 = self.version == TxVersion::Sapling; - #[cfg(feature = "zfuture")] - let has_tze = self.version == TxVersion::ZFuture; - #[cfg(not(feature = "zfuture"))] - let has_tze = false; - - Vector::write(&mut writer, &self.vin, |w, e| e.write(w))?; - Vector::write(&mut writer, &self.vout, |w, e| e.write(w))?; - #[cfg(feature = "zfuture")] - if has_tze { - Vector::write(&mut writer, &self.tze_inputs, |w, e| e.write(w))?; - Vector::write(&mut writer, &self.tze_outputs, |w, e| e.write(w))?; - } - writer.write_u32::(self.lock_time)?; - if is_overwinter_v3 || is_sapling_v4 || has_tze { - writer.write_u32::(u32::from(self.expiry_height))?; + fn read_amount(mut reader: R) -> io::Result { + let mut tmp = [0; 8]; + reader.read_exact(&mut tmp)?; + ZatBalance::from_i64_le_bytes(tmp) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "valueBalance out of range")) + } + + fn read_v5(mut reader: R, version: TxVersion) -> io::Result { + let (consensus_branch_id, lock_time, expiry_height) = + Self::read_v5_header_fragment(&mut reader)?; + let transparent_bundle = Self::read_transparent(&mut reader)?; + let sapling_bundle = sapling_serialization::read_v5_bundle(&mut reader)?; + let orchard_bundle = orchard_serialization::read_v5_bundle(&mut reader)?; + + #[cfg(zcash_unstable = "zfuture")] + let tze_bundle = if version.has_tze() { + Self::read_tze(&mut reader)? + } else { + None + }; + + let data = TransactionData { + version, + consensus_branch_id, + lock_time, + expiry_height, + transparent_bundle, + sprout_bundle: None, + sapling_bundle, + orchard_bundle, + #[cfg(zcash_unstable = "zfuture")] + tze_bundle, + }; + + Ok(Self::from_data_v5(data)) + } + + fn read_v5_header_fragment(mut reader: R) -> io::Result<(BranchId, u32, BlockHeight)> { + let consensus_branch_id = reader.read_u32_le().and_then(|value| { + BranchId::try_from(value).map_err(|_e| { + io::Error::new(io::ErrorKind::InvalidInput, "invalid consensus branch id") + }) + })?; + let lock_time = reader.read_u32_le()?; + let expiry_height: BlockHeight = reader.read_u32_le()?.into(); + Ok((consensus_branch_id, lock_time, expiry_height)) + } + + #[cfg(feature = "temporary-zcashd")] + pub fn temporary_zcashd_read_v5_sapling( + reader: R, + ) -> io::Result>> { + sapling_serialization::read_v5_bundle(reader) + } + + #[cfg(zcash_unstable = "zfuture")] + fn read_tze(mut reader: &mut R) -> io::Result>> { + let vin = Vector::read(&mut reader, TzeIn::read)?; + let vout = Vector::read(&mut reader, TzeOut::read)?; + Ok(if vin.is_empty() && vout.is_empty() { + None + } else { + Some(tze::Bundle { + vin, + vout, + authorization: tze::Authorized, + }) + }) + } + + pub fn write(&self, writer: W) -> io::Result<()> { + match self.version { + TxVersion::Sprout(_) | TxVersion::Overwinter | TxVersion::Sapling => { + self.write_v4(writer) + } + TxVersion::Zip225 => self.write_v5(writer), + #[cfg(zcash_unstable = "zfuture")] + TxVersion::ZFuture => self.write_v5(writer), } + } - if is_sapling_v4 || has_tze { - writer.write_all(&self.value_balance.to_i64_le_bytes())?; - Vector::write(&mut writer, &self.shielded_spends, |w, e| e.write(w))?; - Vector::write(&mut writer, &self.shielded_outputs, |w, e| e.write(w))?; + pub fn write_v4(&self, mut writer: W) -> io::Result<()> { + self.version.write(&mut writer)?; + + self.write_transparent(&mut writer)?; + writer.write_u32_le(self.lock_time)?; + if self.version.has_overwinter() { + writer.write_u32_le(u32::from(self.expiry_height))?; } + sapling_serialization::write_v4_components( + &mut writer, + self.sapling_bundle.as_ref(), + self.version.has_sapling(), + )?; + if self.version.has_sprout() { - Vector::write(&mut writer, &self.joinsplits, |w, e| e.write(w))?; - if !self.joinsplits.is_empty() { - match self.joinsplit_pubkey { - Some(pubkey) => writer.write_all(&pubkey)?, - None => { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Missing JoinSplit pubkey", - )); - } - } - match self.joinsplit_sig { - Some(sig) => writer.write_all(&sig)?, - None => { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Missing JoinSplit signature", - )); - } - } + if let Some(bundle) = self.sprout_bundle.as_ref() { + Vector::write(&mut writer, &bundle.joinsplits, |w, e| e.write(w))?; + writer.write_all(&bundle.joinsplit_pubkey)?; + writer.write_all(&bundle.joinsplit_sig)?; + } else { + CompactSize::write(&mut writer, 0)?; } } - if !self.version.has_sprout() || self.joinsplits.is_empty() { - if self.joinsplit_pubkey.is_some() { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "JoinSplit pubkey should not be present", - )); - } - if self.joinsplit_sig.is_some() { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "JoinSplit signature should not be present", - )); + if self.version.has_sapling() { + if let Some(bundle) = self.sapling_bundle.as_ref() { + writer.write_all(&<[u8; 64]>::from(bundle.authorization().binding_sig))?; } } - if (is_sapling_v4 || has_tze) - && !(self.shielded_spends.is_empty() && self.shielded_outputs.is_empty()) - { - match self.binding_sig { - Some(sig) => sig.write(&mut writer)?, - None => { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Missing binding signature", - )); - } - } - } else if self.binding_sig.is_some() { + if self.orchard_bundle.is_some() { return Err(io::Error::new( io::ErrorKind::InvalidInput, - "Binding signature should not be present", + "Orchard components cannot be present when serializing to the V4 transaction format." )); } Ok(()) } -} - -#[cfg(any(test, feature = "test-dependencies"))] -pub mod testing { - use proptest::collection::vec; - use proptest::prelude::*; - use proptest::sample::select; - - use crate::{consensus::BranchId, legacy::Script}; - - #[cfg(feature = "zfuture")] - use crate::extensions::transparent as tze; - - use super::{ - components::{amount::MAX_MONEY, Amount, OutPoint, TxIn, TxOut}, - Transaction, TransactionData, TxVersion, - }; - #[cfg(feature = "zfuture")] - use super::components::{TzeIn, TzeOut, TzeOutPoint}; - - pub const VALID_OPCODES: [u8; 8] = [ - 0x00, // OP_FALSE, - 0x51, // OP_1, - 0x52, // OP_2, - 0x53, // OP_3, - 0xac, // OP_CHECKSIG, - 0x63, // OP_IF, - 0x65, // OP_VERIF, - 0x6a, // OP_RETURN, - ]; - - prop_compose! { - pub fn arb_outpoint()(hash in prop::array::uniform32(1u8..), n in 1..100u32) -> OutPoint { - OutPoint::new(hash, n) + pub fn write_transparent(&self, mut writer: W) -> io::Result<()> { + if let Some(bundle) = &self.transparent_bundle { + Vector::write(&mut writer, &bundle.vin, |w, e| e.write(w))?; + Vector::write(&mut writer, &bundle.vout, |w, e| e.write(w))?; + } else { + CompactSize::write(&mut writer, 0)?; + CompactSize::write(&mut writer, 0)?; } - } - prop_compose! { - pub fn arb_script()(v in vec(select(&VALID_OPCODES[..]), 1..256)) -> Script { - Script(v) - } + Ok(()) } - prop_compose! { - pub fn arb_txin()(prevout in arb_outpoint(), script_sig in arb_script(), sequence in any::()) -> TxIn { - TxIn { prevout, script_sig, sequence } + pub fn write_v5(&self, mut writer: W) -> io::Result<()> { + if self.sprout_bundle.is_some() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Sprout components cannot be present when serializing to the V5 transaction format.", + )); } + self.write_v5_header(&mut writer)?; + self.write_transparent(&mut writer)?; + self.write_v5_sapling(&mut writer)?; + orchard_serialization::write_v5_bundle(self.orchard_bundle.as_ref(), &mut writer)?; + #[cfg(zcash_unstable = "zfuture")] + self.write_tze(&mut writer)?; + Ok(()) } - prop_compose! { - pub fn arb_amount()(value in 0..MAX_MONEY) -> Amount { - Amount::from_i64(value).unwrap() - } + pub fn write_v5_header(&self, mut writer: W) -> io::Result<()> { + self.version.write(&mut writer)?; + writer.write_u32_le(u32::from(self.consensus_branch_id))?; + writer.write_u32_le(self.lock_time)?; + writer.write_u32_le(u32::from(self.expiry_height))?; + Ok(()) } - prop_compose! { - pub fn arb_txout()(value in arb_amount(), script_pubkey in arb_script()) -> TxOut { - TxOut { value, script_pubkey } - } + #[cfg(feature = "temporary-zcashd")] + pub fn temporary_zcashd_write_v5_sapling( + sapling_bundle: Option<&sapling::Bundle>, + writer: W, + ) -> io::Result<()> { + sapling_serialization::write_v5_bundle(writer, sapling_bundle) } - #[cfg(feature = "zfuture")] - prop_compose! { - pub fn arb_tzeoutpoint()(hash in prop::array::uniform32(1u8..), n in 1..100u32) -> TzeOutPoint { - TzeOutPoint::new(hash, n) - } + pub fn write_v5_sapling(&self, writer: W) -> io::Result<()> { + sapling_serialization::write_v5_bundle(writer, self.sapling_bundle.as_ref()) } - #[cfg(feature = "zfuture")] - prop_compose! { - pub fn arb_witness()(extension_id in 0..100u32, mode in 0..100u32, payload in vec(any::(), 32..256)) -> tze::Witness { - tze::Witness { extension_id, mode, payload } + #[cfg(zcash_unstable = "zfuture")] + pub fn write_tze(&self, mut writer: W) -> io::Result<()> { + if let Some(bundle) = &self.tze_bundle { + Vector::write(&mut writer, &bundle.vin, |w, e| e.write(w))?; + Vector::write(&mut writer, &bundle.vout, |w, e| e.write(w))?; + } else { + CompactSize::write(&mut writer, 0)?; + CompactSize::write(&mut writer, 0)?; } - } - #[cfg(feature = "zfuture")] - prop_compose! { - pub fn arb_tzein()(prevout in arb_tzeoutpoint(), witness in arb_witness()) -> TzeIn { - TzeIn { prevout, witness } - } + Ok(()) } - #[cfg(feature = "zfuture")] - prop_compose! { - pub fn arb_precondition()(extension_id in 0..100u32, mode in 0..100u32, payload in vec(any::(), 32..256)) -> tze::Precondition { - tze::Precondition { extension_id, mode, payload } - } + // TODO: should this be moved to `from_data` and stored? + pub fn auth_commitment(&self) -> Blake2bHash { + self.data.digest(BlockTxCommitmentDigester) } +} - #[cfg(feature = "zfuture")] - prop_compose! { - fn arb_tzeout()(value in arb_amount(), precondition in arb_precondition()) -> TzeOut { - TzeOut { value, precondition } - } - } +#[derive(Clone, Debug)] +pub struct TransparentDigests { + pub prevouts_digest: A, + pub sequence_digest: A, + pub outputs_digest: A, +} + +#[derive(Clone, Debug)] +pub struct TzeDigests { + pub inputs_digest: A, + pub outputs_digest: A, + pub per_input_digest: Option, +} + +#[derive(Clone, Debug)] +pub struct TxDigests { + pub header_digest: A, + pub transparent_digests: Option>, + pub sapling_digest: Option, + pub orchard_digest: Option, + #[cfg(zcash_unstable = "zfuture")] + pub tze_digests: Option>, +} + +pub trait TransactionDigest { + type HeaderDigest; + type TransparentDigest; + type SaplingDigest; + type OrchardDigest; + + #[cfg(zcash_unstable = "zfuture")] + type TzeDigest; + + type Digest; + + fn digest_header( + &self, + version: TxVersion, + consensus_branch_id: BranchId, + lock_time: u32, + expiry_height: BlockHeight, + ) -> Self::HeaderDigest; + + fn digest_transparent( + &self, + transparent_bundle: Option<&transparent::Bundle>, + ) -> Self::TransparentDigest; + + fn digest_sapling( + &self, + sapling_bundle: Option<&sapling::Bundle>, + ) -> Self::SaplingDigest; + + fn digest_orchard( + &self, + orchard_bundle: Option<&orchard::Bundle>, + ) -> Self::OrchardDigest; + + #[cfg(zcash_unstable = "zfuture")] + fn digest_tze(&self, tze_bundle: Option<&tze::Bundle>) -> Self::TzeDigest; + + fn combine( + &self, + header_digest: Self::HeaderDigest, + transparent_digest: Self::TransparentDigest, + sapling_digest: Self::SaplingDigest, + orchard_digest: Self::OrchardDigest, + #[cfg(zcash_unstable = "zfuture")] tze_digest: Self::TzeDigest, + ) -> Self::Digest; +} + +pub enum DigestError { + NotSigned, +} + +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing { + use proptest::prelude::*; - pub fn arb_branch_id() -> impl Strategy { - select(vec![ - BranchId::Sprout, - BranchId::Overwinter, - BranchId::Sapling, - BranchId::Blossom, - BranchId::Heartwood, - BranchId::Canopy, - #[cfg(feature = "zfuture")] - BranchId::ZFuture, - ]) + use ::transparent::bundle::testing::{self as transparent}; + use zcash_protocol::consensus::BranchId; + + use super::{ + components::{ + orchard::testing::{self as orchard}, + sapling::testing::{self as sapling}, + }, + Authorized, Transaction, TransactionData, TxId, TxVersion, + }; + + #[cfg(zcash_unstable = "zfuture")] + use super::components::tze::testing::{self as tze}; + + pub fn arb_txid() -> impl Strategy { + prop::array::uniform32(any::()).prop_map(TxId::from_bytes) } - fn tx_versions(branch_id: BranchId) -> impl Strategy { + pub fn arb_tx_version(branch_id: BranchId) -> impl Strategy { match branch_id { BranchId::Sprout => (1..=2u32).prop_map(TxVersion::Sprout).boxed(), BranchId::Overwinter => Just(TxVersion::Overwinter).boxed(), - #[cfg(feature = "zfuture")] + BranchId::Sapling | BranchId::Blossom | BranchId::Heartwood | BranchId::Canopy => { + Just(TxVersion::Sapling).boxed() + } + BranchId::Nu5 => Just(TxVersion::Zip225).boxed(), + BranchId::Nu6 => Just(TxVersion::Zip225).boxed(), + #[cfg(zcash_unstable = "zfuture")] BranchId::ZFuture => Just(TxVersion::ZFuture).boxed(), - _otherwise => Just(TxVersion::Sapling).boxed(), } } - #[cfg(feature = "zfuture")] + #[cfg(not(zcash_unstable = "zfuture"))] prop_compose! { - pub fn arb_txdata(branch_id: BranchId)( - version in tx_versions(branch_id), - vin in vec(arb_txin(), 0..10), - vout in vec(arb_txout(), 0..10), - tze_inputs in vec(arb_tzein(), 0..10), - tze_outputs in vec(arb_tzeout(), 0..10), + pub fn arb_txdata(consensus_branch_id: BranchId)( + version in arb_tx_version(consensus_branch_id), + )( lock_time in any::(), expiry_height in any::(), - value_balance in arb_amount(), - ) -> TransactionData { + transparent_bundle in transparent::arb_bundle(), + sapling_bundle in sapling::arb_bundle_for_version(version), + orchard_bundle in orchard::arb_bundle_for_version(version), + version in Just(version) + ) -> TransactionData { TransactionData { version, - vin, vout, - tze_inputs: if branch_id == BranchId::ZFuture { tze_inputs } else { vec![] }, - tze_outputs: if branch_id == BranchId::ZFuture { tze_outputs } else { vec![] }, + consensus_branch_id, lock_time, expiry_height: expiry_height.into(), - value_balance: match version { - TxVersion::Sprout(_) | TxVersion::Overwinter => Amount::zero(), - _ => value_balance, - }, - shielded_spends: vec![], //FIXME - shielded_outputs: vec![], //FIXME - joinsplits: vec![], //FIXME - joinsplit_pubkey: None, //FIXME - joinsplit_sig: None, //FIXME - binding_sig: None, //FIXME + transparent_bundle, + sprout_bundle: None, + sapling_bundle, + orchard_bundle } } } - #[cfg(not(feature = "zfuture"))] + #[cfg(zcash_unstable = "zfuture")] prop_compose! { - pub fn arb_txdata(branch_id: BranchId)( - version in tx_versions(branch_id), - vin in vec(arb_txin(), 0..10), - vout in vec(arb_txout(), 0..10), + pub fn arb_txdata(consensus_branch_id: BranchId)( + version in arb_tx_version(consensus_branch_id), + )( lock_time in any::(), expiry_height in any::(), - value_balance in arb_amount(), - ) -> TransactionData { + transparent_bundle in transparent::arb_bundle(), + sapling_bundle in sapling::arb_bundle_for_version(version), + orchard_bundle in orchard::arb_bundle_for_version(version), + tze_bundle in tze::arb_bundle(consensus_branch_id), + version in Just(version) + ) -> TransactionData { TransactionData { version, - vin, vout, + consensus_branch_id, lock_time, expiry_height: expiry_height.into(), - value_balance: match version { - TxVersion::Sprout(_) | TxVersion::Overwinter => Amount::zero(), - _ => value_balance, - }, - shielded_spends: vec![], //FIXME - shielded_outputs: vec![], //FIXME - joinsplits: vec![], //FIXME - joinsplit_pubkey: None, //FIXME - joinsplit_sig: None, //FIXME - binding_sig: None, //FIXME + transparent_bundle, + sprout_bundle: None, + sapling_bundle, + orchard_bundle, + tze_bundle } } } diff --git a/zcash_primitives/src/transaction/sighash.rs b/zcash_primitives/src/transaction/sighash.rs index 604e33b2fa..3e0117f3d7 100644 --- a/zcash_primitives/src/transaction/sighash.rs +++ b/zcash_primitives/src/transaction/sighash.rs @@ -1,382 +1,79 @@ -#[cfg(feature = "zfuture")] -use std::convert::TryInto; - -use blake2b_simd::{Hash as Blake2bHash, Params as Blake2bParams}; -use byteorder::{LittleEndian, WriteBytesExt}; -use ff::PrimeField; -use group::GroupEncoding; - -use crate::{consensus, legacy::Script}; - -#[cfg(feature = "zfuture")] -use crate::{ - extensions::transparent::Precondition, - serialize::{CompactSize, Vector}, -}; +use blake2b_simd::Hash as Blake2bHash; use super::{ - components::{Amount, JsDescription, OutputDescription, SpendDescription, TxIn, TxOut}, - Transaction, TransactionData, TxVersion, + sighash_v4::v4_signature_hash, sighash_v5::v5_signature_hash, Authorization, TransactionData, + TxDigests, TxVersion, }; +use ::sapling::bundle::GrothProofBytes; -#[cfg(feature = "zfuture")] -use super::components::{TzeIn, TzeOut}; - -const ZCASH_SIGHASH_PERSONALIZATION_PREFIX: &[u8; 12] = b"ZcashSigHash"; -const ZCASH_PREVOUTS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashPrevoutHash"; -const ZCASH_SEQUENCE_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashSequencHash"; -const ZCASH_OUTPUTS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashOutputsHash"; -const ZCASH_JOINSPLITS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashJSplitsHash"; -const ZCASH_SHIELDED_SPENDS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashSSpendsHash"; -const ZCASH_SHIELDED_OUTPUTS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashSOutputHash"; - -#[cfg(feature = "zfuture")] -const ZCASH_TZE_INPUTS_HASH_PERSONALIZATION: &[u8; 16] = b"Zcash_TzeInsHash"; -#[cfg(feature = "zfuture")] -const ZCASH_TZE_OUTPUTS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashTzeOutsHash"; - -#[cfg(feature = "zfuture")] -const ZCASH_TZE_SIGNED_INPUT_TAG: &[u8; 1] = &[0x00]; -#[cfg(feature = "zfuture")] -const ZCASH_TRANSPARENT_SIGNED_INPUT_TAG: &[u8; 1] = &[0x01]; - -pub const SIGHASH_ALL: u32 = 1; -const SIGHASH_NONE: u32 = 2; -const SIGHASH_SINGLE: u32 = 3; -const SIGHASH_MASK: u32 = 0x1f; -const SIGHASH_ANYONECANPAY: u32 = 0x80; - -macro_rules! update_u32 { - ($h:expr, $value:expr, $tmp:expr) => { - (&mut $tmp[..4]).write_u32::($value).unwrap(); - $h.update(&$tmp[..4]); - }; -} - -macro_rules! update_hash { - ($h:expr, $cond:expr, $value:expr) => { - if $cond { - $h.update(&$value.as_ref()); - } else { - $h.update(&[0; 32]); - } - }; -} - -fn has_overwinter_components(version: &TxVersion) -> bool { - !matches!(version, TxVersion::Sprout(_)) -} - -fn has_sapling_components(version: &TxVersion) -> bool { - !matches!(version, TxVersion::Sprout(_) | TxVersion::Overwinter) -} - -#[cfg(feature = "zfuture")] -fn has_tze_components(version: &TxVersion) -> bool { - matches!(version, TxVersion::ZFuture) -} - -fn prevout_hash(vin: &[TxIn]) -> Blake2bHash { - let mut data = Vec::with_capacity(vin.len() * 36); - for t_in in vin { - t_in.prevout.write(&mut data).unwrap(); - } - Blake2bParams::new() - .hash_length(32) - .personal(ZCASH_PREVOUTS_HASH_PERSONALIZATION) - .hash(&data) -} - -fn sequence_hash(vin: &[TxIn]) -> Blake2bHash { - let mut data = Vec::with_capacity(vin.len() * 4); - for t_in in vin { - (&mut data) - .write_u32::(t_in.sequence) - .unwrap(); - } - Blake2bParams::new() - .hash_length(32) - .personal(ZCASH_SEQUENCE_HASH_PERSONALIZATION) - .hash(&data) -} - -fn outputs_hash(vout: &[TxOut]) -> Blake2bHash { - let mut data = Vec::with_capacity(vout.len() * (4 + 1)); - for t_out in vout { - t_out.write(&mut data).unwrap(); - } - Blake2bParams::new() - .hash_length(32) - .personal(ZCASH_OUTPUTS_HASH_PERSONALIZATION) - .hash(&data) -} +#[cfg(zcash_unstable = "zfuture")] +use {crate::extensions::transparent::Precondition, zcash_protocol::value::Zatoshis}; -fn single_output_hash(tx_out: &TxOut) -> Blake2bHash { - let mut data = vec![]; - tx_out.write(&mut data).unwrap(); - Blake2bParams::new() - .hash_length(32) - .personal(ZCASH_OUTPUTS_HASH_PERSONALIZATION) - .hash(&data) -} +#[deprecated(note = "use `::zcash_transparent::sighash::SIGHASH_ALL` instead.")] +pub const SIGHASH_ALL: u8 = ::transparent::sighash::SIGHASH_ALL; +#[deprecated(note = "use `::zcash_transparent::sighash::SIGHASH_NONE` instead.")] +pub const SIGHASH_NONE: u8 = ::transparent::sighash::SIGHASH_NONE; +#[deprecated(note = "use `::zcash_transparent::sighash::SIGHASH_SINGLE` instead.")] +pub const SIGHASH_SINGLE: u8 = ::transparent::sighash::SIGHASH_SINGLE; +#[deprecated(note = "use `::zcash_transparent::sighash::SIGHASH_MASK` instead.")] +pub const SIGHASH_MASK: u8 = ::transparent::sighash::SIGHASH_MASK; +#[deprecated(note = "use `::zcash_transparent::sighash::SIGHASH_ANYONECANPAY` instead.")] +pub const SIGHASH_ANYONECANPAY: u8 = ::transparent::sighash::SIGHASH_ANYONECANPAY; -fn joinsplits_hash( - txversion: TxVersion, - joinsplits: &[JsDescription], - joinsplit_pubkey: &[u8; 32], -) -> Blake2bHash { - let mut data = Vec::with_capacity( - joinsplits.len() - * if txversion.uses_groth_proofs() { - 1698 // JSDescription with Groth16 proof - } else { - 1802 // JSDescription with PHGR13 proof - }, - ); - for js in joinsplits { - js.write(&mut data).unwrap(); - } - data.extend_from_slice(joinsplit_pubkey); - Blake2bParams::new() - .hash_length(32) - .personal(ZCASH_JOINSPLITS_HASH_PERSONALIZATION) - .hash(&data) -} - -fn shielded_spends_hash(shielded_spends: &[SpendDescription]) -> Blake2bHash { - let mut data = Vec::with_capacity(shielded_spends.len() * 384); - for s_spend in shielded_spends { - data.extend_from_slice(&s_spend.cv.to_bytes()); - data.extend_from_slice(s_spend.anchor.to_repr().as_ref()); - data.extend_from_slice(&s_spend.nullifier.0); - s_spend.rk.write(&mut data).unwrap(); - data.extend_from_slice(&s_spend.zkproof); - } - Blake2bParams::new() - .hash_length(32) - .personal(ZCASH_SHIELDED_SPENDS_HASH_PERSONALIZATION) - .hash(&data) -} - -fn shielded_outputs_hash(shielded_outputs: &[OutputDescription]) -> Blake2bHash { - let mut data = Vec::with_capacity(shielded_outputs.len() * 948); - for s_out in shielded_outputs { - s_out.write(&mut data).unwrap(); - } - Blake2bParams::new() - .hash_length(32) - .personal(ZCASH_SHIELDED_OUTPUTS_HASH_PERSONALIZATION) - .hash(&data) -} - -#[cfg(feature = "zfuture")] -fn tze_inputs_hash(tze_inputs: &[TzeIn]) -> Blake2bHash { - let mut data = vec![]; - for tzein in tze_inputs { - tzein.write_without_witness(&mut data).unwrap(); - } - Blake2bParams::new() - .hash_length(32) - .personal(ZCASH_TZE_INPUTS_HASH_PERSONALIZATION) - .hash(&data) -} - -#[cfg(feature = "zfuture")] -fn tze_outputs_hash(tze_outputs: &[TzeOut]) -> Blake2bHash { - let mut data = vec![]; - for tzeout in tze_outputs { - tzeout.write(&mut data).unwrap(); - } - Blake2bParams::new() - .hash_length(32) - .personal(ZCASH_TZE_OUTPUTS_HASH_PERSONALIZATION) - .hash(&data) -} +#[deprecated(note = "use `::zcash_transparent::sighash::SighashType` instead.")] +pub type SighashType = ::transparent::sighash::SighashType; pub enum SignableInput<'a> { Shielded, - Transparent { - index: usize, - script_code: &'a Script, - value: Amount, - }, - #[cfg(feature = "zfuture")] + Transparent(transparent::sighash::SignableInput<'a>), + #[cfg(zcash_unstable = "zfuture")] Tze { index: usize, precondition: &'a Precondition, - value: Amount, + value: Zatoshis, }, } -impl<'a> SignableInput<'a> { - pub fn transparent(index: usize, script_code: &'a Script, value: Amount) -> Self { - SignableInput::Transparent { - index, - script_code, - value, - } - } - - #[cfg(feature = "zfuture")] - pub fn tze(index: usize, precondition: &'a Precondition, value: Amount) -> Self { - SignableInput::Tze { - index, - precondition, - value, +impl SignableInput<'_> { + pub fn hash_type(&self) -> u8 { + match self { + SignableInput::Shielded => ::transparent::sighash::SIGHASH_ALL, + SignableInput::Transparent(input) => input.hash_type().encode(), + #[cfg(zcash_unstable = "zfuture")] + SignableInput::Tze { .. } => ::transparent::sighash::SIGHASH_ALL, } } } -pub fn signature_hash_data( - tx: &TransactionData, - consensus_branch_id: consensus::BranchId, - hash_type: u32, - signable_input: SignableInput<'_>, -) -> Vec { - if has_overwinter_components(&tx.version) { - let mut personal = [0; 16]; - (&mut personal[..12]).copy_from_slice(ZCASH_SIGHASH_PERSONALIZATION_PREFIX); - (&mut personal[12..]) - .write_u32::(consensus_branch_id.into()) - .unwrap(); +pub struct SignatureHash(Blake2bHash); - let mut h = Blake2bParams::new() - .hash_length(32) - .personal(&personal) - .to_state(); - let mut tmp = [0; 8]; - - update_u32!(h, tx.version.header(), tmp); - update_u32!(h, tx.version.version_group_id(), tmp); - update_hash!( - h, - hash_type & SIGHASH_ANYONECANPAY == 0, - prevout_hash(&tx.vin) - ); - update_hash!( - h, - hash_type & SIGHASH_ANYONECANPAY == 0 - && (hash_type & SIGHASH_MASK) != SIGHASH_SINGLE - && (hash_type & SIGHASH_MASK) != SIGHASH_NONE, - sequence_hash(&tx.vin) - ); - - if (hash_type & SIGHASH_MASK) != SIGHASH_SINGLE - && (hash_type & SIGHASH_MASK) != SIGHASH_NONE - { - h.update(outputs_hash(&tx.vout).as_ref()); - } else if (hash_type & SIGHASH_MASK) == SIGHASH_SINGLE { - match signable_input { - SignableInput::Transparent { index, .. } if index < tx.vout.len() => { - h.update(single_output_hash(&tx.vout[index]).as_ref()) - } - _ => h.update(&[0; 32]), - }; - } else { - h.update(&[0; 32]); - }; - #[cfg(feature = "zfuture")] - if has_tze_components(&tx.version) { - update_hash!( - h, - !tx.tze_inputs.is_empty(), - tze_inputs_hash(&tx.tze_inputs) - ); - update_hash!( - h, - !tx.tze_outputs.is_empty(), - tze_outputs_hash(&tx.tze_outputs) - ); - } - update_hash!( - h, - !tx.joinsplits.is_empty(), - joinsplits_hash(tx.version, &tx.joinsplits, &tx.joinsplit_pubkey.unwrap()) - ); - if has_sapling_components(&tx.version) { - update_hash!( - h, - !tx.shielded_spends.is_empty(), - shielded_spends_hash(&tx.shielded_spends) - ); - update_hash!( - h, - !tx.shielded_outputs.is_empty(), - shielded_outputs_hash(&tx.shielded_outputs) - ); - } - update_u32!(h, tx.lock_time, tmp); - update_u32!(h, tx.expiry_height.into(), tmp); - if has_sapling_components(&tx.version) { - h.update(&tx.value_balance.to_i64_le_bytes()); - } - update_u32!(h, hash_type, tmp); - - match signable_input { - SignableInput::Transparent { - index, - script_code, - value, - } => { - #[cfg(feature = "zfuture")] - let mut data = if has_tze_components(&tx.version) { - // domain separation here is to avoid collision attacks - // between transparent and TZE inputs. - ZCASH_TRANSPARENT_SIGNED_INPUT_TAG.to_vec() - } else { - vec![] - }; - - #[cfg(not(feature = "zfuture"))] - let mut data = vec![]; - - tx.vin[index].prevout.write(&mut data).unwrap(); - script_code.write(&mut data).unwrap(); - data.extend_from_slice(&value.to_i64_le_bytes()); - (&mut data) - .write_u32::(tx.vin[index].sequence) - .unwrap(); - h.update(&data); - } - - #[cfg(feature = "zfuture")] - SignableInput::Tze { - index, - precondition, - value, - } if has_tze_components(&tx.version) => { - // domain separation here is to avoid collision attacks - // between transparent and TZE inputs. - let mut data = ZCASH_TZE_SIGNED_INPUT_TAG.to_vec(); - - tx.tze_inputs[index].prevout.write(&mut data).unwrap(); - CompactSize::write(&mut data, precondition.extension_id.try_into().unwrap()) - .unwrap(); - CompactSize::write(&mut data, precondition.mode.try_into().unwrap()).unwrap(); - Vector::write(&mut data, &precondition.payload, |w, e| w.write_u8(*e)).unwrap(); - data.extend_from_slice(&value.to_i64_le_bytes()); - h.update(&data); - } - - #[cfg(feature = "zfuture")] - SignableInput::Tze { .. } => { - panic!("A request has been made to sign a TZE input, but the signature hash version is not ZFuture"); - } +impl AsRef<[u8; 32]> for SignatureHash { + fn as_ref(&self) -> &[u8; 32] { + self.0.as_ref().try_into().unwrap() + } +} - _ => (), +/// Computes the signature hash for an input to a transaction, given +/// the full data of the transaction, the input being signed, and the +/// set of precomputed hashes produced in the construction of the +/// transaction ID. +pub fn signature_hash< + TA: ::transparent::sighash::TransparentAuthorizingContext, + SA: sapling::bundle::Authorization, + A: Authorization, +>( + tx: &TransactionData, + signable_input: &SignableInput, + txid_parts: &TxDigests, +) -> SignatureHash { + SignatureHash(match tx.version { + TxVersion::Sprout(_) | TxVersion::Overwinter | TxVersion::Sapling => { + v4_signature_hash(tx, signable_input) } - h.finalize().as_ref().to_vec() - } else { - unimplemented!() - } -} + TxVersion::Zip225 => v5_signature_hash(tx, signable_input, txid_parts), -pub fn signature_hash( - tx: &Transaction, - consensus_branch_id: consensus::BranchId, - hash_type: u32, - signable_input: SignableInput<'_>, -) -> Vec { - signature_hash_data(tx, consensus_branch_id, hash_type, signable_input) + #[cfg(zcash_unstable = "zfuture")] + TxVersion::ZFuture => v5_signature_hash(tx, signable_input, txid_parts), + }) } diff --git a/zcash_primitives/src/transaction/sighash_v4.rs b/zcash_primitives/src/transaction/sighash_v4.rs new file mode 100644 index 0000000000..55f10e798a --- /dev/null +++ b/zcash_primitives/src/transaction/sighash_v4.rs @@ -0,0 +1,259 @@ +use alloc::vec::Vec; +use blake2b_simd::{Hash as Blake2bHash, Params as Blake2bParams}; +use ff::PrimeField; + +use ::sapling::bundle::{GrothProofBytes, OutputDescription, SpendDescription}; +use ::transparent::{ + bundle::{self as transparent, TxIn, TxOut}, + sighash::{SIGHASH_ANYONECANPAY, SIGHASH_MASK, SIGHASH_NONE, SIGHASH_SINGLE}, +}; +use zcash_protocol::consensus::BranchId; + +use super::{ + components::{sapling as sapling_serialization, sprout::JsDescription}, + sighash::SignableInput, + Authorization, TransactionData, +}; + +const ZCASH_SIGHASH_PERSONALIZATION_PREFIX: &[u8; 12] = b"ZcashSigHash"; +const ZCASH_PREVOUTS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashPrevoutHash"; +const ZCASH_SEQUENCE_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashSequencHash"; +const ZCASH_OUTPUTS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashOutputsHash"; +const ZCASH_JOINSPLITS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashJSplitsHash"; +const ZCASH_SHIELDED_SPENDS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashSSpendsHash"; +const ZCASH_SHIELDED_OUTPUTS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashSOutputHash"; + +macro_rules! update_hash { + ($h:expr, $cond:expr, $value:expr) => { + if $cond { + $h.update(&$value.as_ref()); + } else { + $h.update(&[0; 32]); + } + }; +} + +fn prevout_hash(vin: &[TxIn]) -> Blake2bHash { + let mut data = Vec::with_capacity(vin.len() * 36); + for t_in in vin { + t_in.prevout.write(&mut data).unwrap(); + } + Blake2bParams::new() + .hash_length(32) + .personal(ZCASH_PREVOUTS_HASH_PERSONALIZATION) + .hash(&data) +} + +fn sequence_hash(vin: &[TxIn]) -> Blake2bHash { + let mut data = Vec::with_capacity(vin.len() * 4); + for t_in in vin { + data.extend_from_slice(&t_in.sequence.to_le_bytes()); + } + Blake2bParams::new() + .hash_length(32) + .personal(ZCASH_SEQUENCE_HASH_PERSONALIZATION) + .hash(&data) +} + +fn outputs_hash(vout: &[TxOut]) -> Blake2bHash { + let mut data = Vec::with_capacity(vout.len() * (4 + 1)); + for t_out in vout { + t_out.write(&mut data).unwrap(); + } + Blake2bParams::new() + .hash_length(32) + .personal(ZCASH_OUTPUTS_HASH_PERSONALIZATION) + .hash(&data) +} + +fn single_output_hash(tx_out: &TxOut) -> Blake2bHash { + let mut data = vec![]; + tx_out.write(&mut data).unwrap(); + Blake2bParams::new() + .hash_length(32) + .personal(ZCASH_OUTPUTS_HASH_PERSONALIZATION) + .hash(&data) +} + +fn joinsplits_hash( + consensus_branch_id: BranchId, + joinsplits: &[JsDescription], + joinsplit_pubkey: &[u8; 32], +) -> Blake2bHash { + let mut data = Vec::with_capacity( + joinsplits.len() + * if consensus_branch_id.sprout_uses_groth_proofs() { + 1698 // JSDescription with Groth16 proof + } else { + 1802 // JsDescription with PHGR13 proof + }, + ); + for js in joinsplits { + js.write(&mut data).unwrap(); + } + data.extend_from_slice(joinsplit_pubkey); + Blake2bParams::new() + .hash_length(32) + .personal(ZCASH_JOINSPLITS_HASH_PERSONALIZATION) + .hash(&data) +} + +fn shielded_spends_hash< + A: sapling::bundle::Authorization, +>( + shielded_spends: &[SpendDescription], +) -> Blake2bHash { + let mut data = Vec::with_capacity(shielded_spends.len() * 384); + for s_spend in shielded_spends { + data.extend_from_slice(&s_spend.cv().to_bytes()); + data.extend_from_slice(s_spend.anchor().to_repr().as_ref()); + data.extend_from_slice(s_spend.nullifier().as_ref()); + data.extend_from_slice(&<[u8; 32]>::from(*s_spend.rk())); + data.extend_from_slice(s_spend.zkproof()); + } + Blake2bParams::new() + .hash_length(32) + .personal(ZCASH_SHIELDED_SPENDS_HASH_PERSONALIZATION) + .hash(&data) +} + +fn shielded_outputs_hash(shielded_outputs: &[OutputDescription]) -> Blake2bHash { + let mut data = Vec::with_capacity(shielded_outputs.len() * 948); + for s_out in shielded_outputs { + sapling_serialization::write_output_v4(&mut data, s_out).unwrap(); + } + Blake2bParams::new() + .hash_length(32) + .personal(ZCASH_SHIELDED_OUTPUTS_HASH_PERSONALIZATION) + .hash(&data) +} + +pub fn v4_signature_hash< + SA: sapling::bundle::Authorization, + A: Authorization, +>( + tx: &TransactionData, + signable_input: &SignableInput<'_>, +) -> Blake2bHash { + let hash_type = signable_input.hash_type(); + if tx.version.has_overwinter() { + let mut personal = [0; 16]; + personal[..12].copy_from_slice(ZCASH_SIGHASH_PERSONALIZATION_PREFIX); + personal[12..].copy_from_slice(&u32::from(tx.consensus_branch_id).to_le_bytes()); + + let mut h = Blake2bParams::new() + .hash_length(32) + .personal(&personal) + .to_state(); + + h.update(&tx.version.header().to_le_bytes()); + h.update(&tx.version.version_group_id().to_le_bytes()); + update_hash!( + h, + hash_type & SIGHASH_ANYONECANPAY == 0, + prevout_hash( + tx.transparent_bundle + .as_ref() + .map_or(&[], |b| b.vin.as_slice()) + ) + ); + update_hash!( + h, + (hash_type & SIGHASH_ANYONECANPAY) == 0 + && (hash_type & SIGHASH_MASK) != SIGHASH_SINGLE + && (hash_type & SIGHASH_MASK) != SIGHASH_NONE, + sequence_hash( + tx.transparent_bundle + .as_ref() + .map_or(&[], |b| b.vin.as_slice()) + ) + ); + + if (hash_type & SIGHASH_MASK) != SIGHASH_SINGLE + && (hash_type & SIGHASH_MASK) != SIGHASH_NONE + { + h.update( + outputs_hash( + tx.transparent_bundle + .as_ref() + .map_or(&[], |b| b.vout.as_slice()), + ) + .as_bytes(), + ); + } else if (hash_type & SIGHASH_MASK) == SIGHASH_SINGLE { + match (tx.transparent_bundle.as_ref(), signable_input) { + (Some(b), SignableInput::Transparent(input)) if input.index() < &b.vout.len() => { + h.update(single_output_hash(&b.vout[*input.index()]).as_bytes()) + } + _ => h.update(&[0; 32]), + }; + } else { + h.update(&[0; 32]); + }; + + update_hash!( + h, + !tx.sprout_bundle + .as_ref() + .map_or(true, |b| b.joinsplits.is_empty()), + { + let bundle = tx.sprout_bundle.as_ref().unwrap(); + joinsplits_hash( + tx.consensus_branch_id, + &bundle.joinsplits, + &bundle.joinsplit_pubkey, + ) + } + ); + + if tx.version.has_sapling() { + update_hash!( + h, + !tx.sapling_bundle + .as_ref() + .map_or(true, |b| b.shielded_spends().is_empty()), + shielded_spends_hash(tx.sapling_bundle.as_ref().unwrap().shielded_spends()) + ); + update_hash!( + h, + !tx.sapling_bundle + .as_ref() + .map_or(true, |b| b.shielded_outputs().is_empty()), + shielded_outputs_hash(tx.sapling_bundle.as_ref().unwrap().shielded_outputs()) + ); + } + h.update(&tx.lock_time.to_le_bytes()); + h.update(&u32::from(tx.expiry_height).to_le_bytes()); + if tx.version.has_sapling() { + h.update(&tx.sapling_value_balance().to_i64_le_bytes()); + } + h.update(&u32::from(hash_type).to_le_bytes()); + + match signable_input { + SignableInput::Shielded => (), + SignableInput::Transparent(input) => { + if let Some(bundle) = tx.transparent_bundle.as_ref() { + let mut data = vec![]; + bundle.vin[*input.index()].prevout.write(&mut data).unwrap(); + input.script_code().write(&mut data).unwrap(); + data.extend_from_slice(&input.value().to_i64_le_bytes()); + data.extend_from_slice(&bundle.vin[*input.index()].sequence.to_le_bytes()); + h.update(&data); + } else { + panic!( + "A request has been made to sign a transparent input, but none are present." + ); + } + } + + #[cfg(zcash_unstable = "zfuture")] + SignableInput::Tze { .. } => { + panic!("A request has been made to sign a TZE input, but the transaction version is not ZFuture"); + } + } + + h.finalize() + } else { + panic!("Signature hashing for pre-overwinter transactions is not supported.") + } +} diff --git a/zcash_primitives/src/transaction/sighash_v5.rs b/zcash_primitives/src/transaction/sighash_v5.rs new file mode 100644 index 0000000000..08ad375727 --- /dev/null +++ b/zcash_primitives/src/transaction/sighash_v5.rs @@ -0,0 +1,208 @@ +use blake2b_simd::{Hash as Blake2bHash, Params}; +use core2::io::Write; + +use ::transparent::{ + bundle::{self as transparent, TxOut}, + sighash::{ + TransparentAuthorizingContext, SIGHASH_ANYONECANPAY, SIGHASH_MASK, SIGHASH_NONE, + SIGHASH_SINGLE, + }, +}; +use zcash_encoding::Array; + +use crate::{ + encoding::StateWrite, + transaction::{ + sighash::SignableInput, + txid::{ + hash_transparent_txid_data, to_hash, transparent_outputs_hash, + transparent_prevout_hash, transparent_sequence_hash, + ZCASH_TRANSPARENT_HASH_PERSONALIZATION, + }, + Authorization, TransactionData, TransparentDigests, TxDigests, + }, +}; + +#[cfg(zcash_unstable = "zfuture")] +use { + crate::{ + encoding::WriteBytesExt, + transaction::{components::tze, TzeDigests}, + }, + zcash_encoding::{CompactSize, Vector}, +}; + +const ZCASH_TRANSPARENT_INPUT_HASH_PERSONALIZATION: &[u8; 16] = b"Zcash___TxInHash"; +const ZCASH_TRANSPARENT_AMOUNTS_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxTrAmountsHash"; +const ZCASH_TRANSPARENT_SCRIPTS_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxTrScriptsHash"; + +#[cfg(zcash_unstable = "zfuture")] +const ZCASH_TZE_INPUT_HASH_PERSONALIZATION: &[u8; 16] = b"Zcash__TzeInHash"; + +fn hasher(personal: &[u8; 16]) -> StateWrite { + StateWrite(Params::new().hash_length(32).personal(personal).to_state()) +} + +/// Implements [ZIP 244 section S.2](https://zips.z.cash/zip-0244#s-2-transparent-sig-digest). +fn transparent_sig_digest( + tx_data: Option<(&transparent::Bundle, &TransparentDigests)>, + input: &SignableInput<'_>, +) -> Blake2bHash { + match tx_data { + // No transparent inputs or outputs. + None => hash_transparent_txid_data(None), + // No transparent inputs, or coinbase. + Some((bundle, txid_digests)) if bundle.is_coinbase() || bundle.vin.is_empty() => { + hash_transparent_txid_data(Some(txid_digests)) + } + // Some transparent inputs, and not coinbase. + Some((bundle, txid_digests)) => { + let hash_type = input.hash_type(); + let flag_anyonecanpay = hash_type & SIGHASH_ANYONECANPAY != 0; + let flag_single = hash_type & SIGHASH_MASK == SIGHASH_SINGLE; + let flag_none = hash_type & SIGHASH_MASK == SIGHASH_NONE; + + let prevouts_digest = if flag_anyonecanpay { + transparent_prevout_hash::(&[]) + } else { + txid_digests.prevouts_digest + }; + + let amounts_digest = { + let mut h = hasher(ZCASH_TRANSPARENT_AMOUNTS_HASH_PERSONALIZATION); + if !flag_anyonecanpay { + Array::write(&mut h, bundle.authorization.input_amounts(), |w, amount| { + w.write_all(&amount.to_i64_le_bytes()) + }) + .unwrap(); + } + h.finalize() + }; + + let scripts_digest = { + let mut h = hasher(ZCASH_TRANSPARENT_SCRIPTS_HASH_PERSONALIZATION); + if !flag_anyonecanpay { + Array::write( + &mut h, + bundle.authorization.input_scriptpubkeys(), + |w, script| script.write(w), + ) + .unwrap(); + } + h.finalize() + }; + + let sequence_digest = if flag_anyonecanpay { + transparent_sequence_hash::(&[]) + } else { + txid_digests.sequence_digest + }; + + let outputs_digest = if let SignableInput::Transparent(input) = input { + if flag_single { + if *input.index() < bundle.vout.len() { + transparent_outputs_hash(&[&bundle.vout[*input.index()]]) + } else { + transparent_outputs_hash::(&[]) + } + } else if flag_none { + transparent_outputs_hash::(&[]) + } else { + txid_digests.outputs_digest + } + } else { + txid_digests.outputs_digest + }; + + //S.2g.i: prevout (field encoding) + //S.2g.ii: value (8-byte signed little-endian) + //S.2g.iii: scriptPubKey (field encoding) + //S.2g.iv: nSequence (4-byte unsigned little-endian) + let mut ch = hasher(ZCASH_TRANSPARENT_INPUT_HASH_PERSONALIZATION); + if let SignableInput::Transparent(input) = input { + let txin = &bundle.vin[*input.index()]; + txin.prevout.write(&mut ch).unwrap(); + ch.write_all(&input.value().to_i64_le_bytes()).unwrap(); + input.script_pubkey().write(&mut ch).unwrap(); + ch.write_all(&txin.sequence.to_le_bytes()).unwrap(); + } + let txin_sig_digest = ch.finalize(); + + let mut h = hasher(ZCASH_TRANSPARENT_HASH_PERSONALIZATION); + h.write_all(&[hash_type]).unwrap(); + h.write_all(prevouts_digest.as_bytes()).unwrap(); + h.write_all(amounts_digest.as_bytes()).unwrap(); + h.write_all(scripts_digest.as_bytes()).unwrap(); + h.write_all(sequence_digest.as_bytes()).unwrap(); + h.write_all(outputs_digest.as_bytes()).unwrap(); + h.write_all(txin_sig_digest.as_bytes()).unwrap(); + h.finalize() + } + } +} + +#[cfg(zcash_unstable = "zfuture")] +fn tze_input_sigdigests( + bundle: &tze::Bundle, + input: &SignableInput<'_>, + txid_digests: &TzeDigests, +) -> TzeDigests { + let mut ch = hasher(ZCASH_TZE_INPUT_HASH_PERSONALIZATION); + if let SignableInput::Tze { + index, + precondition, + value, + } = input + { + let tzein = &bundle.vin[*index]; + tzein.prevout.write(&mut ch).unwrap(); + CompactSize::write(&mut ch, precondition.extension_id.try_into().unwrap()).unwrap(); + CompactSize::write(&mut ch, precondition.mode.try_into().unwrap()).unwrap(); + Vector::write(&mut ch, &precondition.payload, |w, e| w.write_u8(*e)).unwrap(); + ch.write_all(&value.to_i64_le_bytes()).unwrap(); + } + let per_input_digest = ch.finalize(); + + TzeDigests { + inputs_digest: txid_digests.inputs_digest, + outputs_digest: txid_digests.outputs_digest, + per_input_digest: Some(per_input_digest), + } +} + +/// Implements the [Signature Digest section of ZIP 244](https://zips.z.cash/zip-0244#signature-digest) +pub fn v5_signature_hash< + TA: TransparentAuthorizingContext, + A: Authorization, +>( + tx: &TransactionData, + signable_input: &SignableInput<'_>, + txid_parts: &TxDigests, +) -> Blake2bHash { + // The caller must provide the transparent digests if and only if the transaction has a + // transparent component. + assert_eq!( + tx.transparent_bundle.is_some(), + txid_parts.transparent_digests.is_some() + ); + + to_hash( + tx.version, + tx.consensus_branch_id, + txid_parts.header_digest, + transparent_sig_digest( + tx.transparent_bundle + .as_ref() + .zip(txid_parts.transparent_digests.as_ref()), + signable_input, + ), + txid_parts.sapling_digest, + txid_parts.orchard_digest, + #[cfg(zcash_unstable = "zfuture")] + tx.tze_bundle + .as_ref() + .zip(txid_parts.tze_digests.as_ref()) + .map(|(bundle, tze_digests)| tze_input_sigdigests(bundle, signable_input, tze_digests)) + .as_ref(), + ) +} diff --git a/zcash_primitives/src/transaction/tests.rs b/zcash_primitives/src/transaction/tests.rs index 4360cad3ed..1e1a8f3890 100644 --- a/zcash_primitives/src/transaction/tests.rs +++ b/zcash_primitives/src/transaction/tests.rs @@ -1,22 +1,31 @@ -use ff::Field; -use rand_core::OsRng; +use alloc::vec::Vec; +use blake2b_simd::Hash as Blake2bHash; +use core::ops::Deref; use proptest::prelude::*; -use crate::{constants::SPENDING_KEY_GENERATOR, sapling::redjubjub::PrivateKey}; +use ::transparent::{ + address::Script, sighash::SighashType, sighash::TransparentAuthorizingContext, +}; +use zcash_protocol::{consensus::BranchId, value::Zatoshis}; use super::{ - components::Amount, - sighash::{signature_hash, SignableInput}, - Transaction, TransactionData, + sighash::SignableInput, + sighash_v4::v4_signature_hash, + sighash_v5::v5_signature_hash, + testing::arb_tx, + transparent::{self}, + txid::TxIdDigester, + Authorization, Transaction, TransactionData, TxDigests, TxIn, }; -use super::testing::{arb_branch_id, arb_tx}; +#[cfg(zcash_unstable = "zfuture")] +use super::components::tze; #[test] fn tx_read_write() { let data = &self::data::tx_read_write::TX_READ_WRITE; - let tx = Transaction::read(&data[..]).unwrap(); + let tx = Transaction::read(&data[..], BranchId::Canopy).unwrap(); assert_eq!( format!("{}", tx.txid()), "64f0bd7fe30ce23753358fe3a2dc835b8fba9c0274c4e2c54a6f73114cb55639" @@ -27,94 +36,95 @@ fn tx_read_write() { assert_eq!(&data[..], &encoded[..]); } -#[test] -fn tx_write_rejects_unexpected_joinsplit_pubkey() { - // Succeeds without a JoinSplit pubkey - assert!(TransactionData::new().freeze().is_ok()); - - // Fails with an unexpected JoinSplit pubkey - { - let mut tx = TransactionData::new(); - tx.joinsplit_pubkey = Some([0; 32]); - assert!(tx.freeze().is_err()); +fn check_roundtrip(tx: Transaction) -> Result<(), TestCaseError> { + let mut txn_bytes = vec![]; + tx.write(&mut txn_bytes).unwrap(); + let txo = Transaction::read(&txn_bytes[..], tx.consensus_branch_id).unwrap(); + + prop_assert_eq!(tx.version, txo.version); + #[cfg(zcash_unstable = "zfuture")] + prop_assert_eq!(tx.tze_bundle.as_ref(), txo.tze_bundle.as_ref()); + prop_assert_eq!(tx.lock_time, txo.lock_time); + prop_assert_eq!( + tx.transparent_bundle.as_ref(), + txo.transparent_bundle.as_ref() + ); + prop_assert_eq!(tx.sapling_value_balance(), txo.sapling_value_balance()); + prop_assert_eq!( + tx.orchard_bundle.as_ref().map(|v| *v.value_balance()), + txo.orchard_bundle.as_ref().map(|v| *v.value_balance()) + ); + Ok(()) +} + +proptest! { + #[test] + #[ignore] + #[cfg(feature = "expensive-tests")] + fn tx_serialization_roundtrip_sprout(tx in arb_tx(BranchId::Sprout)) { + check_roundtrip(tx)?; } } -#[test] -fn tx_write_rejects_unexpected_joinsplit_sig() { - // Succeeds without a JoinSplit signature - assert!(TransactionData::new().freeze().is_ok()); - - // Fails with an unexpected JoinSplit signature - { - let mut tx = TransactionData::new(); - tx.joinsplit_sig = Some([0; 64]); - assert!(tx.freeze().is_err()); +proptest! { + #[test] + #[ignore] + #[cfg(feature = "expensive-tests")] + fn tx_serialization_roundtrip_overwinter(tx in arb_tx(BranchId::Overwinter)) { + check_roundtrip(tx)?; } } -#[test] -fn tx_write_rejects_unexpected_binding_sig() { - // Succeeds without a binding signature - assert!(TransactionData::new().freeze().is_ok()); - - // Fails with an unexpected binding signature - { - let mut rng = OsRng; - let sk = PrivateKey(jubjub::Fr::random(&mut rng)); - let sig = sk.sign(b"Foo bar", &mut rng, SPENDING_KEY_GENERATOR); - - let mut tx = TransactionData::new(); - tx.binding_sig = Some(sig); - assert!(tx.freeze().is_err()); +proptest! { + #[test] + #[ignore] + #[cfg(feature = "expensive-tests")] + fn tx_serialization_roundtrip_sapling(tx in arb_tx(BranchId::Sapling)) { + check_roundtrip(tx)?; } } proptest! { #[test] - fn tx_serialization_roundtrip(tx in arb_branch_id().prop_flat_map(arb_tx)) { - let mut txn_bytes = vec![]; - tx.write(&mut txn_bytes).unwrap(); - - let txo = Transaction::read(&txn_bytes[..]).unwrap(); - - assert_eq!(tx.version, txo.version); - assert_eq!(tx.vin, txo.vin); - assert_eq!(tx.vout, txo.vout); - #[cfg(feature = "zfuture")] - assert_eq!(tx.tze_inputs, txo.tze_inputs); - #[cfg(feature = "zfuture")] - assert_eq!(tx.tze_outputs, txo.tze_outputs); - assert_eq!(tx.lock_time, txo.lock_time); - assert_eq!(tx.value_balance, txo.value_balance); + #[ignore] + #[cfg(feature = "expensive-tests")] + fn tx_serialization_roundtrip_blossom(tx in arb_tx(BranchId::Blossom)) { + check_roundtrip(tx)?; } } -#[test] -#[cfg(feature = "zfuture")] -fn test_tze_tx_parse() { - let txn_bytes = vec![ - 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x01, 0x52, 0x52, 0x52, 0x52, - 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, - 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x20, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, - 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, - 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x30, 0x75, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x01, 0x20, 0xd9, 0x81, 0x80, 0x87, 0xde, 0x72, 0x44, 0xab, 0xc1, 0xb5, 0xfc, - 0xf2, 0x8e, 0x55, 0xe4, 0x2c, 0x7f, 0xf9, 0xc6, 0x78, 0xc0, 0x60, 0x51, 0x81, 0xf3, 0x7a, - 0xc5, 0xd7, 0x41, 0x4a, 0x7b, 0x95, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ]; - - let tx = Transaction::read(&txn_bytes[..]); - - match tx { - Ok(tx) => assert!(!tx.tze_inputs.is_empty()), - - Err(e) => panic!( - "An error occurred parsing a serialized TZE transaction: {}", - e - ), +proptest! { + #[test] + #[ignore] + #[cfg(feature = "expensive-tests")] + fn tx_serialization_roundtrip_heartwood(tx in arb_tx(BranchId::Heartwood)) { + check_roundtrip(tx)?; + } +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(10))] + #[test] + fn tx_serialization_roundtrip_canopy(tx in arb_tx(BranchId::Canopy)) { + check_roundtrip(tx)?; + } +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(10))] + #[test] + fn tx_serialization_roundtrip_nu5(tx in arb_tx(BranchId::Nu5)) { + check_roundtrip(tx)?; + } +} + +#[cfg(zcash_unstable = "zfuture")] +proptest! { + #[test] + #[ignore] + #[cfg(feature = "expensive-tests")] + fn tx_serialization_roundtrip_future(tx in arb_tx(BranchId::ZFuture)) { + check_roundtrip(tx)?; } } @@ -122,18 +132,22 @@ mod data; #[test] fn zip_0143() { for tv in self::data::zip_0143::make_test_vectors() { - let tx = Transaction::read(&tv.tx[..]).unwrap(); + let tx = Transaction::read(&tv.tx[..], tv.consensus_branch_id).unwrap(); let signable_input = match tv.transparent_input { - Some(n) => SignableInput::transparent( - n as usize, - &tv.script_code, - Amount::from_nonnegative_i64(tv.amount).unwrap(), - ), + Some(n) => { + SignableInput::Transparent(::transparent::sighash::SignableInput::from_parts( + SighashType::parse(tv.hash_type as u8).unwrap(), + n as usize, + &tv.script_code, + &tv.script_code, + Zatoshis::from_nonnegative_i64(tv.amount).unwrap(), + )) + } _ => SignableInput::Shielded, }; assert_eq!( - signature_hash(&tx, tv.consensus_branch_id, tv.hash_type, signable_input), + v4_signature_hash(tx.deref(), &signable_input).as_ref(), tv.sighash ); } @@ -142,19 +156,208 @@ fn zip_0143() { #[test] fn zip_0243() { for tv in self::data::zip_0243::make_test_vectors() { - let tx = Transaction::read(&tv.tx[..]).unwrap(); + let tx = Transaction::read(&tv.tx[..], tv.consensus_branch_id).unwrap(); let signable_input = match tv.transparent_input { - Some(n) => SignableInput::transparent( - n as usize, - &tv.script_code, - Amount::from_nonnegative_i64(tv.amount).unwrap(), - ), + Some(n) => { + SignableInput::Transparent(::transparent::sighash::SignableInput::from_parts( + SighashType::parse(tv.hash_type as u8).unwrap(), + n as usize, + &tv.script_code, + &tv.script_code, + Zatoshis::from_nonnegative_i64(tv.amount).unwrap(), + )) + } _ => SignableInput::Shielded, }; assert_eq!( - signature_hash(&tx, tv.consensus_branch_id, tv.hash_type, signable_input), + v4_signature_hash(tx.deref(), &signable_input).as_ref(), tv.sighash ); } } + +#[derive(Debug)] +struct TestTransparentAuth { + input_amounts: Vec, + input_scriptpubkeys: Vec