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 index e5a028af21..9d5816faad 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +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..951f91964f --- /dev/null +++ b/.github/actions/prepare/action.yml @@ -0,0 +1,54 @@ +name: 'Prepare CI' +description: 'Prepares feature flags' +inputs: + extra-features: + description: 'Extra feature flags to enable' + required: false + default: '' + all-pools: + description: 'Enable all pool-specific feature flags' + required: false + default: true + 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: pools + shell: bash + run: echo "features=orchard transparent-inputs" >> $GITHUB_OUTPUT + if: inputs.all-pools == 'true' + + - id: test + shell: bash + run: echo "feature=test-dependencies" >> $GITHUB_OUTPUT + if: inputs.test-dependencies == 'true' + + # `steps.pools.outputs.features` and `steps.test.outputs.feature` cannot + # expand into attacker-controllable code because the previous steps only + # enable them to have one of two fixed values. + - name: Prepare feature flags # zizmor: ignore[template-injection] + id: prepare + shell: bash + run: > + echo "flags=--features ' + bundled-prover + download-params + lightwalletd-tonic + sync + temporary-zcashd + unstable + unstable-serialization + unstable-spanning-tree + ${EXTRA_FEATURES} + ${{ steps.pools.outputs.features }} + ${{ steps.test.outputs.feature }} + '" >> $GITHUB_OUTPUT + env: + EXTRA_FEATURES: ${{ inputs.extra-features }} 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..ebcca15331 --- /dev/null +++ b/.github/workflows/audits.yml @@ -0,0 +1,50 @@ +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 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@stable + id: toolchain + - run: rustup override set "${TOOLCHAIN}" + env: + TOOLCHAIN: ${{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 + with: + persist-credentials: false + - 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 "${NEEDS}" | jq -e '[ .[] | .result == "success" ] | all' + env: + NEEDS: ${{ toJSON(needs) }} diff --git a/.github/workflows/book.yml b/.github/workflows/book.yml index f62273e552..1fbcbf27a9 100644 --- a/.github/workflows/book.yml +++ b/.github/workflows/book.yml @@ -3,19 +3,29 @@ name: librustzcash documentation on: push: branches: - - master + - main jobs: deploy: - runs-on: ubuntu-18.04 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + with: + persist-credentials: false + - id: prepare + uses: ./.github/actions/prepare - uses: dtolnay/rust-toolchain@nightly id: toolchain - - run: rustup override set ${{steps.toolchain.outputs.name}} + - run: rustup override set "${TOOLCHAIN}" + env: + TOOLCHAIN: ${{steps.toolchain.outputs.name}} - name: Build latest rustdocs - run: cargo doc --no-deps --workspace --all-features + run: > + cargo doc + --no-deps + --workspace + ${{ steps.prepare.outputs.feature-flags }} env: RUSTDOCFLAGS: -Z unstable-options --enable-index-page --cfg docsrs @@ -25,7 +35,7 @@ jobs: mv ./target/doc ./book/book/rustdoc/latest - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v3 + 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 0611a39c6c..c0b186901b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,98 +1,435 @@ -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: + - transparent + - Sapling-only + - Orchard + - all-pools + - NU7 + + include: + - target: Linux + os: ubuntu-latest-8cores + + - state: transparent + extra_flags: transparent-inputs + - state: Orchard + extra_flags: orchard + - state: all-pools + extra_flags: orchard transparent-inputs + - state: NU7 + extra_flags: orchard transparent-inputs + rustflags: '--cfg zcash_unstable="nu7"' + + env: + RUSTFLAGS: ${{ matrix.rustflags }} + RUSTDOCFLAGS: ${{ matrix.rustflags }} steps: - - uses: actions/checkout@v3 - - 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@v3.3.1 + - uses: actions/checkout@v4 + with: + persist-credentials: false + - id: prepare + uses: ./.github/actions/prepare 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 + all-pools: false + 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 + 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: + - transparent + - Sapling-only + - Orchard + - all-pools + - NU7 + + include: + - target: macOS + os: macOS-latest + - target: Windows + os: windows-latest-8cores + + - state: transparent + extra_flags: transparent-inputs + - state: Orchard + extra_flags: orchard + - state: all-pools + extra_flags: orchard transparent-inputs + - state: NU7 + extra_flags: orchard transparent-inputs + rustflags: '--cfg zcash_unstable="nu7"' + + exclude: + - target: macOS + state: NU7 + + env: + RUSTFLAGS: ${{ matrix.rustflags }} + RUSTDOCFLAGS: ${{ matrix.rustflags }} + + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - id: prepare + uses: ./.github/actions/prepare + with: + all-pools: false + 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 - run: cargo test --all-features --verbose --release --all + 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: + - transparent + - Sapling-only + - Orchard + - all-pools + - NU7 + + include: + - target: Linux + os: ubuntu-latest-8cores + + - state: transparent + extra_flags: transparent-inputs + - state: Orchard + extra_flags: orchard + - state: all-pools + extra_flags: orchard transparent-inputs + - state: NU7 + extra_flags: orchard transparent-inputs + rustflags: '--cfg zcash_unstable="nu7"' + + env: + RUSTFLAGS: ${{ matrix.rustflags }} + RUSTDOCFLAGS: ${{ matrix.rustflags }} + + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - id: prepare + uses: ./.github/actions/prepare + with: + all-pools: false + 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 - run: cargo test --all-features --verbose --release --all -- --ignored + 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 + with: + persist-credentials: false + - 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 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 + with: + persist-credentials: false + - 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 "${TOOLCHAIN}" + shell: sh + env: + TOOLCHAIN: ${{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-wasi + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + path: crates + # We use a synthetic crate to ensure no dev-dependencies are enabled, which can + # be incompatible with some of these targets. + - name: Copy Rust toolchain into the root for use in synthetic crate setup + run: cp crates/rust-toolchain.toml . + - name: Create synthetic crate for testing + run: cargo init --lib ci-build + - name: Move Rust toolchain file into synthetic crate + run: mv 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: 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@v3 + - uses: actions/checkout@v4 + with: + persist-credentials: false + path: crates + # We use a synthetic crate to ensure no dev-dependencies are enabled, which can + # be incompatible with some of these targets. + - name: Copy Rust toolchain into the root for use in synthetic crate setup + run: cp crates/rust-toolchain.toml . + - name: Create synthetic crate for testing + run: cargo init --lib ci-build + - name: Move Rust toolchain file into synthetic crate + run: mv 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 }} - - run: cargo 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 }} + - 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@v3 + - uses: actions/checkout@v4 + with: + persist-credentials: false # Build benchmarks to prevent bitrot - name: Build benchmarks run: cargo build --all --benches clippy: name: Clippy (MSRV) - timeout-minutes: 30 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + with: + persist-credentials: false + - id: prepare + uses: ./.github/actions/prepare - name: Run clippy uses: actions-rs/clippy-check@v1 with: 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-beta: name: Clippy (beta) - timeout-minutes: 30 runs-on: ubuntu-latest continue-on-error: true steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + with: + persist-credentials: false + - id: prepare + uses: ./.github/actions/prepare - uses: dtolnay/rust-toolchain@beta id: toolchain - - run: rustup override set ${{steps.toolchain.outputs.name}} + with: + components: clippy + - run: rustup override set "${TOOLCHAIN}" + shell: sh + env: + TOOLCHAIN: ${{steps.toolchain.outputs.name}} - name: Run Clippy (beta) uses: actions-rs/clippy-check@v1 - continue-on-error: true with: 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 @@ -102,42 +439,137 @@ jobs: options: --security-opt seccomp=unconfined steps: - - uses: actions/checkout@v3 - - 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@v3.3.1 + - uses: actions/checkout@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 - + persist-credentials: false + - 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: codecov-cargo-${{ hashFiles('**/Cargo.lock') }} - name: Generate coverage report - run: cargo tarpaulin --engine llvm --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@v3.1.3 + uses: codecov/codecov-action@v5.3.1 + with: + token: ${{ secrets.CODECOV_TOKEN }} doc-links: name: Intra-doc links runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + with: + persist-credentials: false + - id: prepare + uses: ./.github/actions/prepare - run: cargo fetch # Requires #![deny(rustdoc::broken_intra_doc_links)] in crates. - name: Check intra-doc links - run: cargo doc --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@v3 + - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Check formatting run: cargo fmt --all -- --check + + protobuf: + name: protobuf consistency + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - id: prepare + uses: ./.github/actions/prepare + - name: Install protoc + uses: supplypike/setup-bin@v4 + with: + 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 + + uuid: + name: UUID validity + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - 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 "${NEEDS}" | jq -e '[ .[] | .result == "success" ] | all' + env: + NEEDS: ${{ toJSON(needs) }} diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 0000000000..b49c466a05 --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,31 @@ +name: GitHub Actions Security Analysis with zizmor 🌈 + +on: + push: + branches: ["main"] + pull_request: + branches: ["*"] + +jobs: + zizmor: + name: zizmor latest via Cargo + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false + - name: Install the latest version of uv + uses: astral-sh/setup-uv@4db96194c378173c656ce18a155ffc14a9fc4355 # v5.2.2 + - name: Run zizmor 🌈 + run: uvx zizmor --format sarif . > results.sarif + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Upload SARIF file + uses: github/codeql-action/upload-sarif@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9 + with: + sarif_file: results.sarif + category: zizmor 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/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000000..1ef7f1dbe8 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,6728 @@ +# 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 = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + +[[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.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4734bde002bb3d52e27ab808faa971a143d48d11dbd836d5c02edd1756cdab06" +dependencies = [ + "async-trait", + "cfg-if", + "derive-deftly 1.0.1", + "derive_builder_fork_arti", + "derive_more", + "educe", + "fs-mistrust", + "futures", + "hostname-validator", + "humantime", + "humantime-serde", + "libc", + "once_cell", + "postage", + "rand", + "safelog", + "serde", + "thiserror 2.0.12", + "tor-async-utils", + "tor-basic-utils", + "tor-chanmgr", + "tor-circmgr", + "tor-config", + "tor-config-path", + "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 = "asn1-rs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.12", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[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-trait" +version = "0.1.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[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 = "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.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[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.100", +] + +[[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 = "bstr" +version = "1.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" +dependencies = [ + "memchr", + "regex-automata 0.4.9", + "serde", +] + +[[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.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[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.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" +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.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cookie-factory" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" +dependencies = [ + "futures", +] + +[[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.100", +] + +[[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.100", +] + +[[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.100", +] + +[[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 = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "cookie-factory", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", +] + +[[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-deftly" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f9bc3564f74be6c35d49a7efee54380d7946ccc631323067f33fabb9246027" +dependencies = [ + "derive-deftly-macros 0.14.2", + "heck", +] + +[[package]] +name = "derive-deftly" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0015cb20a284ec944852820598af3aef6309ea8dc317a0304441272ed620f196" +dependencies = [ + "derive-deftly-macros 1.0.1", + "heck", +] + +[[package]] +name = "derive-deftly-macros" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b84d32b18d9a256d81e4fec2e4cfd0ab6dde5e5ff49be1713ae0adbd0060c2" +dependencies = [ + "heck", + "indexmap 2.6.0", + "itertools 0.13.0", + "proc-macro-crate", + "proc-macro2", + "quote", + "sha3", + "strum", + "syn 2.0.100", + "void", +] + +[[package]] +name = "derive-deftly-macros" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b48e8e38a4aa565da767322b5ca55fb0f8347983c5bc7f7647db069405420479" +dependencies = [ + "heck", + "indexmap 2.6.0", + "itertools 0.14.0", + "proc-macro-crate", + "proc-macro2", + "quote", + "sha3", + "strum", + "syn 2.0.100", + "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 = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn 2.0.100", + "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 = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + +[[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 0.4.5", + "windows-sys 0.48.0", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.0", + "windows-sys 0.59.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.100", +] + +[[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 = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea8a8b81cacc08888170eef4d13b775126db426d0b348bee9d18c2c1eaf123cf" + +[[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.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "277b2cb52d2df4acece06bb16bc0bb0a006970c7bf504eac2d310927a6f65890" +dependencies = [ + "dynosaur_derive", + "trait-variant", +] + +[[package]] +name = "dynosaur_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a4102713839a8c01c77c165bc38ef2e83948f6397fa1e1dcfacec0f07b149d3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[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.100", +] + +[[package]] +name = "env_home" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "equihash" +version = "0.2.2" +dependencies = [ + "blake2b_simd", + "cc", + "core2", + "document-features", + "hex", +] + +[[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.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4cb1d3bd5de41d56855c3aa0b35fa49d7cf2ff987f25d92df10f0b8c53c3b0" +dependencies = [ + "derive_builder_fork_arti", + "dirs 6.0.0", + "libc", + "once_cell", + "pwd-grp", + "serde", + "thiserror 2.0.12", + "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.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[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.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[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.25", + "rustls-pki-types", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +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.100", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[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.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.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[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.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +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.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "libc", + "pin-project-lite", + "socket2", + "tokio", + "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.2", + "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.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" +dependencies = [ + "bitflags 2.9.0", + "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.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +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.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" + +[[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.9.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 = "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 = "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", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.52.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 = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943" +dependencies = [ + "bitflags 2.9.0", + "filetime", + "inotify", + "kqueue", + "libc", + "log", + "mio 1.0.3", + "notify-types", + "walkdir", + "windows-sys 0.59.0", +] + +[[package]] +name = "notify-types" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" + +[[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.100", +] + +[[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.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1ef66fcf99348242a20d582d7434da381a867df8dc155b3a980eca767c56137" +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 = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" +dependencies = [ + "memchr", +] + +[[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.2.1" +dependencies = [ + "blake2b_simd", + "bls12_381", + "document-features", + "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.100", +] + +[[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.100", +] + +[[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 1.0.63", +] + +[[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.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebbe2f8898beba44815fdc9e5a4ae9c929e21c5dc29b0c774a15555f7f58d6d0" +dependencies = [ + "aligned-vec", + "backtrace", + "cfg-if", + "criterion", + "findshlibs", + "inferno", + "libc", + "log", + "nix", + "once_cell", + "parking_lot", + "smallvec", + "symbolic-demangle", + "tempfile", + "thiserror 1.0.63", +] + +[[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.100", +] + +[[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.100", +] + +[[package]] +name = "proc-macro2" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +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.9.0", + "lazy_static", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax 0.8.5", + "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", + "itertools 0.13.0", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.100", + "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.100", +] + +[[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 = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94fdf3867b7f2889a736f0022ea9386766280d2cca4bdbe41629ada9e4f3b8f" +dependencies = [ + "derive-deftly 0.14.2", + "libc", + "paste", + "thiserror 1.0.63", +] + +[[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_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand", +] + +[[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 1.0.63", + "zeroize", +] + +[[package]] +name = "redjubjub" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b0ac1bc6bb3696d2c6f52cff8fba57238b81da8c0214ee6cd146eb8fde364e" +dependencies = [ + "rand_core", + "reddsa", + "thiserror 1.0.63", + "zeroize", +] + +[[package]] +name = "redox_syscall" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +dependencies = [ + "bitflags 2.9.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 1.0.63", +] + +[[package]] +name = "redox_users" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +dependencies = [ + "getrandom", + "libredox", + "thiserror 2.0.12", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "retry-error" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cd5db9deeb62617010191df02a0887c96cc15d91514d32c208d6b8f76b9f20e" + +[[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.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "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.9.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 = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.9.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.14", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.23.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c" +dependencies = [ + "log", + "once_cell", + "ring 0.17.14", + "rustls-pki-types", + "rustls-webpki 0.103.1", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" +dependencies = [ + "ring 0.17.14", + "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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba05ad561772e139a16a49088b2d332f659ef49953d56e09cf0f726784e5fdd" +dependencies = [ + "derive_more", + "educe", + "either", + "fluid-let", + "thiserror 2.0.12", +] + +[[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.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc984f4f9ceb736a7bb755c3e3bd17dc56370af2600c9780dcc48c66453da34d" +dependencies = [ + "regex", +] + +[[package]] +name = "sapling-crypto" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d3c081c83f1dc87403d9d71a06f52301c0aa9ea4c17da2a3435bbf493ffba4" +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 1.0.63", + "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.14", + "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.100", +] + +[[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.100", +] + +[[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.9.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 = [ + "bstr", + "dirs 5.0.1", + "os_str_bytes", +] + +[[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 = "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 = [ + "serde", + "version_check", +] + +[[package]] +name = "slotmap-careful" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e34c0f5a636bb33bf53ca356933c525a7758ddddb8d93f98eff866db966d5" +dependencies = [ + "paste", + "serde", + "slotmap", + "thiserror 2.0.12", + "void", +] + +[[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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +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.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.100", +] + +[[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.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[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 1.0.63", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[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.100", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[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.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[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 0.8.11", + "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.100", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls 0.23.25", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +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.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85839f0b32fd242bb3209262371d07feda6d780d16ee9d2bc88581b89da1549b" +dependencies = [ + "async-trait", + "base64", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "prost", + "socket2", + "tokio", + "tokio-rustls", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", + "webpki-roots 0.26.3", +] + +[[package]] +name = "tonic-build" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85f0383fadd15609306383a90e85eaed44169f931a5d2be1b42c76ceff1825e" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "tor-async-utils" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5294c85610f52bcbe36fddde04a3a994c4ec382ceed455cfdc8252be7046008" +dependencies = [ + "derive-deftly 1.0.1", + "educe", + "futures", + "oneshot-fused-workaround", + "pin-project", + "postage", + "thiserror 2.0.12", + "void", +] + +[[package]] +name = "tor-basic-utils" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e97f88c41653613190a717185e9e208575cb4df256ba404daff05721f27f10d" +dependencies = [ + "derive_more", + "hex", + "itertools 0.14.0", + "libc", + "paste", + "rand", + "rand_chacha", + "serde", + "slab", + "smallvec", + "thiserror 2.0.12", +] + +[[package]] +name = "tor-bytes" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357650fb5bff5e94e5ecc7ee26c6af3f584c2be178b45da8f5ab81cf9f9d4795" +dependencies = [ + "bytes", + "derive-deftly 1.0.1", + "digest 0.10.7", + "educe", + "getrandom", + "safelog", + "thiserror 2.0.12", + "tor-error", + "tor-llcrypto", + "zeroize", +] + +[[package]] +name = "tor-cell" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5341a132563ebeffa45ff60e6519394ee7ba58cb5cf65ba99e7ef879789d87b7" +dependencies = [ + "amplify", + "bitflags 2.9.0", + "bytes", + "caret", + "derive-deftly 1.0.1", + "derive_more", + "educe", + "paste", + "rand", + "smallvec", + "thiserror 2.0.12", + "tor-basic-utils", + "tor-bytes", + "tor-cert", + "tor-error", + "tor-linkspec", + "tor-llcrypto", + "tor-memquota", + "tor-units", + "void", +] + +[[package]] +name = "tor-cert" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a82064ea8ea2864e52e143c978d09570a78bc7e5c2b0056b77068b8893a23f" +dependencies = [ + "caret", + "derive_builder_fork_arti", + "derive_more", + "digest 0.10.7", + "thiserror 2.0.12", + "tor-bytes", + "tor-checkable", + "tor-llcrypto", +] + +[[package]] +name = "tor-chanmgr" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86fd8969d9a3e6cf289a7f95c15ec11dff339bc94a902f1edf962333bd83270e" +dependencies = [ + "async-trait", + "caret", + "derive_builder_fork_arti", + "derive_more", + "educe", + "futures", + "oneshot-fused-workaround", + "postage", + "rand", + "safelog", + "serde", + "thiserror 2.0.12", + "tor-async-utils", + "tor-basic-utils", + "tor-cell", + "tor-config", + "tor-error", + "tor-linkspec", + "tor-llcrypto", + "tor-memquota", + "tor-netdir", + "tor-proto", + "tor-rtcompat", + "tor-socksproto", + "tor-units", + "tracing", + "void", +] + +[[package]] +name = "tor-checkable" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1671c146d35ead4a350a50d7d2b25230635c0271539d310d92ea8d7c777313" +dependencies = [ + "humantime", + "signature", + "thiserror 2.0.12", + "tor-llcrypto", +] + +[[package]] +name = "tor-circmgr" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e42aaf170fb526b16ff38d18d2f2967da7e5fd934e94a565beee3716ec480a8" +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.14.0", + "once_cell", + "oneshot-fused-workaround", + "pin-project", + "rand", + "retry-error", + "safelog", + "serde", + "static_assertions", + "thiserror 2.0.12", + "tor-async-utils", + "tor-basic-utils", + "tor-chanmgr", + "tor-config", + "tor-error", + "tor-guardmgr", + "tor-linkspec", + "tor-memquota", + "tor-netdir", + "tor-netdoc", + "tor-persist", + "tor-proto", + "tor-protover", + "tor-relay-selection", + "tor-rtcompat", + "tor-units", + "tracing", + "void", + "weak-table", +] + +[[package]] +name = "tor-config" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca6cc0af790f5f02d8a06c8f692fa471207de2739d8b2921c04f9570af34d75" +dependencies = [ + "amplify", + "cfg-if", + "derive-deftly 1.0.1", + "derive_builder_fork_arti", + "educe", + "either", + "figment", + "fs-mistrust", + "futures", + "itertools 0.14.0", + "notify", + "once_cell", + "paste", + "postage", + "regex", + "serde", + "serde-value", + "serde_ignored", + "strum", + "thiserror 2.0.12", + "toml", + "tor-basic-utils", + "tor-error", + "tor-rtcompat", + "tracing", + "void", +] + +[[package]] +name = "tor-config-path" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27d35f2df5e5a8968069280e9170ae6c617c637e69d075baf582bd925d0e3902" +dependencies = [ + "directories", + "once_cell", + "serde", + "shellexpand", + "thiserror 2.0.12", + "tor-error", + "tor-general-addr", +] + +[[package]] +name = "tor-consdiff" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b9c48e1e8cc9c925ae5bdca8c71952886d2407f1f286cc4d8f4f7aad082d6a6" +dependencies = [ + "digest 0.10.7", + "hex", + "thiserror 2.0.12", + "tor-llcrypto", +] + +[[package]] +name = "tor-dirclient" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5acde3549254949099b072ae4c71c98b93fe4f059c8e5cba4b055df546c9fac" +dependencies = [ + "async-compression", + "base64ct", + "derive_more", + "futures", + "hex", + "http", + "httparse", + "httpdate", + "itertools 0.14.0", + "memchr", + "thiserror 2.0.12", + "tor-circmgr", + "tor-error", + "tor-linkspec", + "tor-llcrypto", + "tor-netdoc", + "tor-proto", + "tor-rtcompat", + "tracing", +] + +[[package]] +name = "tor-dirmgr" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "183418366135eeab826f01f0fc87fed2de389ac938d37575e3443075f17797dd" +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.14.0", + "memmap2", + "once_cell", + "oneshot-fused-workaround", + "paste", + "postage", + "rand", + "rusqlite", + "safelog", + "scopeguard", + "serde", + "signature", + "strum", + "thiserror 2.0.12", + "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.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5c23ce991df37473f65aef31df61f1b038f422ef7daf1d934eda9e533ef9843" +dependencies = [ + "derive_more", + "futures", + "once_cell", + "paste", + "retry-error", + "static_assertions", + "strum", + "thiserror 2.0.12", + "tracing", + "void", +] + +[[package]] +name = "tor-general-addr" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f35f8ecb457f99f655c805f6c5cc855c63e71fa84c24a48e11e9fc51a7d7ad4b" +dependencies = [ + "derive_more", + "thiserror 2.0.12", + "void", +] + +[[package]] +name = "tor-guardmgr" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29a8f3ddf135d23e2c5443e97fb30c635767daa44923b142915d22bdaf47e2ea" +dependencies = [ + "amplify", + "base64ct", + "derive-deftly 1.0.1", + "derive_builder_fork_arti", + "derive_more", + "dyn-clone", + "educe", + "futures", + "humantime", + "humantime-serde", + "itertools 0.14.0", + "num_enum", + "oneshot-fused-workaround", + "pin-project", + "postage", + "rand", + "safelog", + "serde", + "strum", + "thiserror 2.0.12", + "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.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34606b731a6c9b24f102124947e8f4be85cd86d90816f4c5ff62c43776d557c5" +dependencies = [ + "data-encoding", + "derive_more", + "digest 0.10.7", + "itertools 0.14.0", + "paste", + "rand", + "safelog", + "signature", + "subtle", + "thiserror 2.0.12", + "tor-basic-utils", + "tor-bytes", + "tor-error", + "tor-key-forge", + "tor-llcrypto", + "tor-units", + "void", +] + +[[package]] +name = "tor-key-forge" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b22ecf1c5b6bfa7849bf92cad3daab16bbc741ac62a61c9fea47c8be2f982e01" +dependencies = [ + "derive-deftly 1.0.1", + "derive_more", + "downcast-rs", + "paste", + "rand", + "signature", + "ssh-key", + "thiserror 2.0.12", + "tor-bytes", + "tor-cert", + "tor-checkable", + "tor-error", + "tor-llcrypto", +] + +[[package]] +name = "tor-keymgr" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06c16e10a2bd5888e0c22e92638aa4ac90cd9971f0bf302a10a6c907662323a4" +dependencies = [ + "amplify", + "arrayvec", + "cfg-if", + "derive-deftly 1.0.1", + "derive_builder_fork_arti", + "derive_more", + "downcast-rs", + "dyn-clone", + "fs-mistrust", + "glob-match", + "humantime", + "inventory", + "itertools 0.14.0", + "rand", + "serde", + "signature", + "ssh-key", + "thiserror 2.0.12", + "tor-basic-utils", + "tor-bytes", + "tor-config", + "tor-config-path", + "tor-error", + "tor-hscrypto", + "tor-key-forge", + "tor-llcrypto", + "tor-persist", + "tracing", + "walkdir", + "zeroize", +] + +[[package]] +name = "tor-linkspec" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "119c9fe01c6dace496dd137aba8ad6d92324a1dbdd4515a9348c499723c0f742" +dependencies = [ + "base64ct", + "by_address", + "caret", + "derive-deftly 1.0.1", + "derive_builder_fork_arti", + "derive_more", + "hex", + "itertools 0.14.0", + "safelog", + "serde", + "serde_with", + "strum", + "thiserror 2.0.12", + "tor-basic-utils", + "tor-bytes", + "tor-config", + "tor-llcrypto", + "tor-memquota", + "tor-protover", +] + +[[package]] +name = "tor-llcrypto" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85dbde770a588d073ae16dc743f51a135bd6d041943a82395b05365ed6c69a0f" +dependencies = [ + "aes", + "base64ct", + "ctr", + "curve25519-dalek", + "der-parser", + "derive-deftly 1.0.1", + "derive_more", + "digest 0.10.7", + "ed25519-dalek", + "educe", + "getrandom", + "hex", + "rand_core", + "rsa", + "safelog", + "serde", + "sha1", + "sha2 0.10.8", + "sha3", + "signature", + "subtle", + "thiserror 2.0.12", + "tor-memquota", + "visibility", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "tor-log-ratelim" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80aed29a8c53e55664e9dd4b99561dec2621dcedaa25116e58dd795fa6bf07f1" +dependencies = [ + "futures", + "humantime", + "once_cell", + "thiserror 2.0.12", + "tor-error", + "tor-rtcompat", + "tracing", + "weak-table", +] + +[[package]] +name = "tor-memquota" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d63eef6dd4d38b16199cf201de07b6de4a6af310f67bd71067d22ef746eb1a1d" +dependencies = [ + "derive-deftly 1.0.1", + "derive_more", + "dyn-clone", + "educe", + "futures", + "itertools 0.14.0", + "paste", + "pin-project", + "serde", + "slotmap-careful", + "static_assertions", + "thiserror 2.0.12", + "tor-async-utils", + "tor-basic-utils", + "tor-config", + "tor-error", + "tor-log-ratelim", + "tor-rtcompat", + "tracing", + "void", +] + +[[package]] +name = "tor-netdir" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e17883b3b2ef17a5f9ad4ae8a78de2c4b3d629ccfeb66c15c4cb33494384f08" +dependencies = [ + "async-trait", + "bitflags 2.9.0", + "derive_more", + "futures", + "humantime", + "itertools 0.14.0", + "num_enum", + "rand", + "serde", + "static_assertions", + "strum", + "thiserror 2.0.12", + "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.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aec11efe729e4ca9c5b03a8702f94b82dfd0ab450c0d58c4ca5ee9e4c49e6f89" +dependencies = [ + "amplify", + "base64ct", + "bitflags 2.9.0", + "cipher", + "derive_builder_fork_arti", + "derive_more", + "digest 0.10.7", + "educe", + "hex", + "humantime", + "itertools 0.14.0", + "once_cell", + "phf", + "serde", + "serde_with", + "signature", + "smallvec", + "subtle", + "thiserror 2.0.12", + "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.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be9958219e20477aef5645f99d0d3695e01bb230bbd36a0fd4c207f5428abe6b" +dependencies = [ + "derive-deftly 1.0.1", + "derive_more", + "filetime", + "fs-mistrust", + "fslock", + "futures", + "itertools 0.14.0", + "oneshot-fused-workaround", + "paste", + "sanitize-filename", + "serde", + "serde_json", + "thiserror 2.0.12", + "time", + "tor-async-utils", + "tor-basic-utils", + "tor-error", + "tracing", + "void", +] + +[[package]] +name = "tor-proto" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef488ff76f3cd9e6c8e4376c90dc526a27d74a53363a5f86af426d61c42edd9" +dependencies = [ + "amplify", + "asynchronous-codec", + "bitvec", + "bytes", + "caret", + "cipher", + "coarsetime", + "derive-deftly 1.0.1", + "derive_builder_fork_arti", + "derive_more", + "digest 0.10.7", + "educe", + "futures", + "futures-util", + "hkdf", + "hmac 0.12.1", + "oneshot-fused-workaround", + "pin-project", + "rand", + "rand_core", + "safelog", + "slotmap-careful", + "static_assertions", + "subtle", + "thiserror 2.0.12", + "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-memquota", + "tor-rtcompat", + "tor-rtmock", + "tor-units", + "tracing", + "typenum", + "void", + "zeroize", +] + +[[package]] +name = "tor-protover" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a7d228eda4c7e7c96fff6a5f6759d1bd03bad69b62b9d94f2ac409de3518b8a" +dependencies = [ + "caret", + "thiserror 2.0.12", +] + +[[package]] +name = "tor-relay-selection" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e41754428684bd62892df2c74c2d11128cfbf3f1a8a9aaa1b920fcb90e04961a" +dependencies = [ + "rand", + "serde", + "tor-basic-utils", + "tor-linkspec", + "tor-netdir", + "tor-netdoc", +] + +[[package]] +name = "tor-rtcompat" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4956b5e707d288e22f77809507e67a79a11db9054a29a44af00f93ff7cb6c2c7" +dependencies = [ + "async-trait", + "async_executors", + "asynchronous-codec", + "coarsetime", + "derive_more", + "dyn-clone", + "educe", + "futures", + "futures-rustls", + "libc", + "paste", + "pin-project", + "rustls-pki-types", + "thiserror 2.0.12", + "tokio", + "tokio-util", + "tor-error", + "tor-general-addr", + "tracing", + "void", + "x509-signature", +] + +[[package]] +name = "tor-rtmock" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9077af79aac5ad0c5336af1cc41a31c617bbc09261210a2427deb84f14356857" +dependencies = [ + "amplify", + "async-trait", + "derive-deftly 1.0.1", + "derive_more", + "educe", + "futures", + "humantime", + "itertools 0.14.0", + "oneshot-fused-workaround", + "pin-project", + "priority-queue", + "slotmap-careful", + "strum", + "thiserror 2.0.12", + "tor-error", + "tor-general-addr", + "tor-rtcompat", + "tracing", + "tracing-test", + "void", +] + +[[package]] +name = "tor-socksproto" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3892f6d0c323b87a2390f41e91c0294c6d5852f00e955e41e85a0116636e82d" +dependencies = [ + "amplify", + "caret", + "derive-deftly 1.0.1", + "educe", + "safelog", + "subtle", + "thiserror 2.0.12", + "tor-bytes", + "tor-error", +] + +[[package]] +name = "tor-units" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7388f506c9278d07421e6799aa8a912adee4ea6921b3dd08a1247a619de82124" +dependencies = [ + "derive-deftly 1.0.1", + "derive_more", + "thiserror 2.0.12", + "tor-memquota", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 2.6.0", + "pin-project-lite", + "slab", + "sync_wrapper", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "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.100", +] + +[[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.100", +] + +[[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.100", +] + +[[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.100", +] + +[[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.100", + "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.100", + "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 = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2774c861e1f072b3aadc02f8ba886c26ad6321567ecc294c935434cad06f1283" +dependencies = [ + "either", + "env_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.7.1" +dependencies = [ + "assert_matches", + "bech32", + "bs58", + "core2", + "f4jumble", + "proptest", + "zcash_encoding", + "zcash_protocol", +] + +[[package]] +name = "zcash_client_backend" +version = "0.18.0" +dependencies = [ + "ambassador", + "arti-client", + "assert_matches", + "async-trait", + "base64", + "bech32", + "bip32", + "bls12_381", + "bs58", + "byteorder", + "crossbeam-channel", + "document-features", + "dynosaur", + "fs-mistrust", + "futures-util", + "group", + "gumdrop", + "hex", + "http-body-util", + "hyper", + "hyper-util", + "incrementalmerkletree", + "jubjub", + "memuse", + "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", + "time-core", + "tokio", + "tokio-rustls", + "tonic", + "tonic-build", + "tor-rtcompat", + "tower", + "tracing", + "trait-variant", + "webpki-roots 0.26.3", + "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.16.2" +dependencies = [ + "ambassador", + "assert_matches", + "bip32", + "bitflags 2.9.0", + "bls12_381", + "bs58", + "byteorder", + "document-features", + "group", + "incrementalmerkletree", + "incrementalmerkletree-testing", + "jubjub", + "maybe-rayon", + "nonempty", + "orchard", + "pasta_curves", + "proptest", + "prost", + "rand", + "rand_chacha", + "rand_core", + "rand_distr", + "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.3.0" +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.8.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.22.0" +dependencies = [ + "assert_matches", + "bip32", + "blake2b_simd", + "block-buffer 0.11.0-rc.3", + "bs58", + "chacha20poly1305", + "core2", + "criterion", + "crypto-common 0.2.0-rc.1", + "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.22.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.5.1" +dependencies = [ + "core2", + "document-features", + "hex", + "incrementalmerkletree", + "incrementalmerkletree-testing", + "memuse", + "proptest", +] + +[[package]] +name = "zcash_spec" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded3f58b93486aa79b85acba1001f5298f27a46489859934954d262533ee2915" +dependencies = [ + "blake2b_simd", +] + +[[package]] +name = "zcash_transparent" +version = "0.2.3" +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.100", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" + +[[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.100", +] + +[[package]] +name = "zerovec" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e62113720e311984f461c56b00457ae9981c0bc7859d22306cc2ae2f95571c" +dependencies = [ + "zerofrom", +] + +[[package]] +name = "zip32" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13ff9ea444cdbce820211f91e6aa3d3a56bde7202d3c0961b7c38f793abf5637" +dependencies = [ + "blake2b_simd", + "memuse", + "subtle", + "zcash_spec", +] + +[[package]] +name = "zip321" +version = "0.3.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 044d879e93..92aae7321c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,16 +4,213 @@ members = [ "components/f4jumble", "components/zcash_address", "components/zcash_encoding", - "components/zcash_note_encryption", + "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.7", path = "components/zcash_address", default-features = false } +zcash_client_backend = { version = "0.18", path = "zcash_client_backend" } +zcash_encoding = { version = "0.3", path = "components/zcash_encoding", default-features = false } +zcash_keys = { version = "0.8", path = "zcash_keys" } +zcash_protocol = { version = "0.5.1", path = "components/zcash_protocol", default-features = false } +zip321 = { version = "0.3", path = "components/zip321" } + +zcash_note_encryption = "0.4.1" +zcash_primitives = { version = "0.22", path = "zcash_primitives", default-features = false } +zcash_proofs = { version = "0.22", path = "zcash_proofs", default-features = false } + +pczt = { version = "0.2", 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.2" + +# 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.8", default-features = false } +sapling = { package = "sapling-crypto", version = "0.5", default-features = false } + +# - Orchard +orchard = { version = "0.11", default-features = false } +pasta_curves = "0.5" + +# - Transparent +bip32 = { version = "=0.6.0-pre.1", default-features = false } +block-buffer = { version = "=0.11.0-rc.3" } # later RCs require edition2024 +crypto-common = { version = "=0.2.0-rc.1" } # later RCs require edition2024 +ripemd = { version = "0.1", default-features = false } +secp256k1 = { version = "0.29", default-features = false, features = ["alloc"] } +transparent = { package = "zcash_transparent", version = "0.2", 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 } +rand_distr = { version = "0.4", 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"] } +bitflags = "2" +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 = { version = "0.26", default-features = false } +webpki-roots = "0.26" + +# 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.13", default-features = false } +tonic-build = { version = "0.13", 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.28", default-features = false, features = ["compression", "rustls", "tokio"] } +dynosaur = "0.2" +fs-mistrust = "0.9" +tokio = "1" +tor-rtcompat = "0.28" +tower = "0.5" +trait-variant = "0.1" + +# ZIP 32 +aes = "0.8" +fpe = { version = "0.6", default-features = false, features = ["alloc"] } +zip32 = { version = "0.2", default-features = false } + +# Workaround for https://anonticket.torproject.org/user/projects/arti/issues/pending/4028/ +time-core = "=0.1.2" + +[workspace.metadata.release] +consolidate-commits = false +pre-release-commit-message = "{{crate_name}} {{version}}" +tag-message = "Release {{crate_name}} version {{version}}" +tag-name = "{{prefix}}{{version}}" +pre-release-replacements = [ + {file="CHANGELOG.md", search="## \\[Unreleased\\]", replace="## [Unreleased]\n\n## [{{version}}] - {{date}}"}, +] +pre-release-hook = ["cargo", "vet"] +# Remove the following options once we're happy to use `cargo-release` without review. +tag = false +publish = false +push = 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", "nu7"))'] } diff --git a/README.md b/README.md index c10053bd85..53f8c4bc93 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,154 @@ # 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. + +```mermaid +graph TB + subgraph librustzcash + direction TB + subgraph main + zcash_address + zcash_primitives + zcash_transparent + zcash_proofs + zcash_protocol + pczt + zcash_client_backend + zcash_client_sqlite + zcash_keys + zip321 + end + + subgraph standalone_components + equihash + f4jumble + zcash_encoding + end + end + + subgraph shielded_protocols + sapling[sapling-crypto] + orchard[orchard] + end + + subgraph protocol_components + zcash_note_encryption + zip32 + zcash_spec + end + + zcash_client_sqlite --> zcash_client_backend + zcash_client_backend --> zcash_primitives + zcash_client_backend --> zip321 + zcash_client_backend --> zcash_keys + pczt --> zcash_primitives + zcash_proofs --> zcash_primitives + zcash_primitives --> zcash_protocol + zcash_primitives --> equihash + zcash_primitives --> zcash_encoding + zcash_primitives --> zcash_address + zcash_primitives --> zcash_transparent + zcash_primitives --> sapling + zcash_primitives --> orchard + zcash_keys --> zcash_address + zcash_keys --> zcash_encoding + zcash_keys --> zip32 + zcash_keys --> zcash_transparent + zcash_keys --> orchard + zcash_keys --> sapling + zcash_transparent --> zcash_protocol + zcash_transparent --> zcash_address + zcash_transparent --> zip32 + zip321 --> zcash_address + zip321 --> zcash_protocol + zcash_address --> zcash_protocol + zcash_address --> f4jumble + zcash_address --> zcash_encoding + sapling --> zcash_note_encryption + sapling --> zip32 + sapling --> zcash_spec + orchard --> zcash_note_encryption + orchard --> zip32 + orchard --> zcash_spec + + main --> standalone_components + + librustzcash --> shielded_protocols + shielded_protocols --> protocol_components + + click zcash_address "https://docs.rs/zcash_address/" _blank + click zcash_primitives "https://docs.rs/zcash_primitives/" _blank + click zcash_transparent "https://docs.rs/zcash_transparent/" _blank + click zcash_proofs "https://docs.rs/zcash_proofs/" _blank + click zcash_protocol "https://docs.rs/zcash_protocol/" _blank + click zcash_keys "https://docs.rs/zcash_keys/" _blank + click zip321 "https://docs.rs/zip321/" _blank + click pczt "https://docs.rs/pczt/" _blank + click zcash_client_backend "https://docs.rs/zcash_client_backend/" _blank + click zcash_client_sqlite "https://docs.rs/zcash_client_sqlite/" _blank + click equihash "https://docs.rs/equihash/" _blank + click f4jumble "https://docs.rs/f4jumble/" _blank + click zcash_encoding "https://docs.rs/zcash_encoding/" _blank + click sapling "https://docs.rs/sapling-crypto/" _blank + click orchard "https://docs.rs/orchard/" _blank + click zcash_note_encryption "https://docs.rs/zcash_note_encryption/" _blank + click zip32 "https://docs.rs/zip32/" _blank + click zcash_spec "https://docs.rs/zcash_spec/" _blank +``` + +### Crates + +#### Zcash Protocol + +* `zcash_protocol`: Constants & common types + - consensus parameters + - bounded value types (Zatoshis, ZatBalance) + - memo types +* `zcash_transparent`: Bitcoin-derived transparent transaction components + - transparent addresses + - transparent input, output, and bundle types + - support for transparent parts of pczt construction +* `zcash_primitives`: Core utilities for working with Zcash transactions + - the primary transaction data type + - transaction builder(s) + - proving, signing, & serialization + - low-level fee types +* `zcash_proofs`: The Sprout circuit & proving system + +#### Keys, Addresses & Wallet Support + +* `zcash_address`: Parsing & serialization of Zcash addresses + - unified address, fvk & ivk containers + - no dependencies on protocol-specific types + - serialization API definitions +* `zip321`: Parsing & serizalization for ZIP 321 payment requests +* `zcash_keys`: Spending Keys, Viewing Keys, & Addresses + - protocol-specific & Unified address types + - ZIP 32 key & address derivation implementations + - Unified spending keys & viewing keys + - Sapling spending & viewing key types +* `pczt`: Data types & interfaces for PCZT construction + - partially constructed transaction types + - transaction construction role interfaces & partial implementations +* `zcash_client_backend`: A wallet framework for Zcash + - wallet data storage APIs + - chain scanning + - light client protocol support + - fee calculation + - transaction proposals & high-level transaction construction APIs +* `zcash_client_sqlite`: SQLite-based implementation of `zcash_client_backend` storage APIs + +#### Utilities & Common Dependencies + +* `f4jumble`: Encoding for Unified addresses +* `zcash_encoding`: Bitcoin-derived transaction encoding utilities for Zcash +* `equihash`: Proof-of-work protocol implementation + ## 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 @@ -16,20 +159,8 @@ All code in this workspace is licensed under either of at your option. -Downstream code forks should note that some (but not all) of these crates -and components depend on the 'orchard' crate, which is licensed under the -[Bootstrap Open Source License](https://github.com/zcash/orchard/blob/main/LICENSE-BOSL). -A license exception is provided allowing some derived works that are linked or -combined with the 'orchard' crate to be copied or distributed under the original -licenses (in this case MIT / Apache 2.0), provided that the included portions of -the 'orchard' code remain subject to BOSL. -See for details of which -derived works can make use of this exception, and the `README.md` files in -subdirectories for which crates and components this applies to. - ### 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 7bad997dd4..8e623898b3 100644 --- a/components/equihash/CHANGELOG.md +++ b/components/equihash/CHANGELOG.md @@ -7,6 +7,14 @@ and this library adheres to Rust's notion of ## [Unreleased] +## [0.2.2] - 2025-03-04 + +Documentation improvements and rendering fix; no code changes. + +## [0.2.1] - 2025-02-21 +### Added +- `equihash::tromp` module behind the experimental `solver` feature flag. + ## [0.2.0] - 2022-06-24 ### Changed - MSRV is now 1.56.1. diff --git a/components/equihash/Cargo.toml b/components/equihash/Cargo.toml index 2eb7c023d1..93f0d48245 100644 --- a/components/equihash/Cargo.toml +++ b/components/equihash/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "equihash" description = "The Equihash Proof-of-Work function" -version = "0.2.0" +version = "0.2.2" authors = ["Jack Grigg "] homepage = "https://github.com/zcash/librustzcash" repository = "https://github.com/zcash/librustzcash" @@ -9,9 +9,34 @@ license = "MIT OR Apache-2.0" edition = "2021" rust-version = "1.56.1" +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[features] +default = ["std"] +std = ["document-features"] + +## Experimental tromp solver support, builds the C++ tromp solver and Rust FFI layer. +solver = ["dep:cc", "std"] + [dependencies] -blake2b_simd = "1" -byteorder = "1" +core2.workspace = true +blake2b_simd.workspace = true + +# Dependencies used internally: +# (Breaking upgrades to these are usually backwards-compatible, but check MSRVs.) +# - Documentation +document-features = { workspace = true, optional = true } + +[build-dependencies] +cc = { version = "1", optional = true } + +[dev-dependencies] +hex = "0.4" [lib] bench = false + +[lints] +workspace = true diff --git a/components/equihash/build.rs b/components/equihash/build.rs new file mode 100644 index 0000000000..74122e450a --- /dev/null +++ b/components/equihash/build.rs @@ -0,0 +1,17 @@ +//! Build script for the equihash tromp solver in C. + +fn main() { + #[cfg(feature = "solver")] + build_tromp_solver(); +} + +#[cfg(feature = "solver")] +fn build_tromp_solver() { + cc::Build::new() + .include("tromp/") + .file("tromp/equi_miner.c") + .compile("equitromp"); + + // Tell Cargo to only rerun this build script if the tromp C files or headers change. + println!("cargo:rerun-if-changed=tromp"); +} diff --git a/components/equihash/src/blake2b.rs b/components/equihash/src/blake2b.rs new file mode 100644 index 0000000000..a54375c737 --- /dev/null +++ b/components/equihash/src/blake2b.rs @@ -0,0 +1,60 @@ +// Copyright (c) 2020-2022 The Zcash developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://www.opensource.org/licenses/mit-license.php . + +// This module uses unsafe code for FFI into blake2b. +#![allow(unsafe_code)] + +use blake2b_simd::{State, PERSONALBYTES}; + +use std::boxed::Box; +use std::ptr; +use std::slice; + +#[no_mangle] +pub(crate) extern "C" fn blake2b_init( + output_len: usize, + personalization: *const [u8; PERSONALBYTES], +) -> *mut State { + let personalization = unsafe { personalization.as_ref().unwrap() }; + + Box::into_raw(Box::new( + blake2b_simd::Params::new() + .hash_length(output_len) + .personal(personalization) + .to_state(), + )) +} + +#[no_mangle] +pub(crate) extern "C" fn blake2b_clone(state: *const State) -> *mut State { + unsafe { state.as_ref() } + .map(|state| Box::into_raw(Box::new(state.clone()))) + .unwrap_or(ptr::null_mut()) +} + +#[no_mangle] +pub(crate) extern "C" fn blake2b_free(state: *mut State) { + if !state.is_null() { + drop(unsafe { Box::from_raw(state) }); + } +} + +#[no_mangle] +pub(crate) extern "C" fn blake2b_update(state: *mut State, input: *const u8, input_len: usize) { + let state = unsafe { state.as_mut().unwrap() }; + let input = unsafe { slice::from_raw_parts(input, input_len) }; + + state.update(input); +} + +#[no_mangle] +pub(crate) extern "C" fn blake2b_finalize(state: *mut State, output: *mut u8, output_len: usize) { + let state = unsafe { state.as_mut().unwrap() }; + let output = unsafe { slice::from_raw_parts_mut(output, output_len) }; + + // Allow consuming only part of the output. + let hash = state.finalize(); + assert!(output_len <= hash.as_bytes().len()); + output.copy_from_slice(&hash.as_bytes()[..output_len]); +} diff --git a/components/equihash/src/lib.rs b/components/equihash/src/lib.rs index fc23642063..9eab390e5e 100644 --- a/components/equihash/src/lib.rs +++ b/components/equihash/src/lib.rs @@ -7,6 +7,9 @@ //! verify solutions for any valid `(n, k)` parameters, as long as the row indices are no //! larger than 32 bits (that is, `ceiling(((n / (k + 1)) + 1) / 8) <= 4`). //! +#![cfg_attr(feature = "std", doc = "## Feature flags")] +#![cfg_attr(feature = "std", doc = document_features::document_features!())] +//! //! References //! ========== //! - [Section 7.6.1: Equihash.] Zcash Protocol Specification, version 2020.1.10 or later. @@ -19,10 +22,26 @@ // Catch documentation errors caused by code changes. #![deny(rustdoc::broken_intra_doc_links)] +#![no_std] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] + +#[cfg(feature = "std")] +extern crate std; +#[macro_use] +extern crate alloc; + +mod minimal; +mod params; mod verify; #[cfg(test)] mod test_vectors; pub use verify::{is_valid_solution, Error}; + +#[cfg(feature = "solver")] +mod blake2b; +#[cfg(feature = "solver")] +pub mod tromp; diff --git a/components/equihash/src/minimal.rs b/components/equihash/src/minimal.rs new file mode 100644 index 0000000000..e2f6200a93 --- /dev/null +++ b/components/equihash/src/minimal.rs @@ -0,0 +1,265 @@ +use alloc::vec::Vec; +use core::mem::size_of; +use core2::io::{Cursor, Read}; + +use crate::params::Params; + +// Rough translation of CompressArray() from: +// https://github.com/zcash/zcash/blob/6fdd9f1b81d3b228326c9826fa10696fc516444b/src/crypto/equihash.cpp#L39-L76 +#[cfg(any(feature = "solver", test))] +fn compress_array(array: &[u8], bit_len: usize, byte_pad: usize) -> Vec { + let index_bytes = (u32::BITS / 8) as usize; + assert!(bit_len >= 8); + assert!(8 * index_bytes >= 7 + bit_len); + + let in_width: usize = (bit_len + 7) / 8 + byte_pad; + let out_len = bit_len * array.len() / (8 * in_width); + + let mut out = Vec::with_capacity(out_len); + let bit_len_mask: u32 = (1 << (bit_len as u32)) - 1; + + // The acc_bits least-significant bits of acc_value represent a bit sequence + // in big-endian order. + let mut acc_bits: usize = 0; + let mut acc_value: u32 = 0; + + let mut j: usize = 0; + for _i in 0..out_len { + // When we have fewer than 8 bits left in the accumulator, read the next + // input element. + if acc_bits < 8 { + acc_value <<= bit_len; + for x in byte_pad..in_width { + acc_value |= ( + // Apply bit_len_mask across byte boundaries + (array[j + x] & ((bit_len_mask >> (8 * (in_width - x - 1))) as u8)) as u32 + ) + .wrapping_shl(8 * (in_width - x - 1) as u32); // Big-endian + } + j += in_width; + acc_bits += bit_len; + } + + acc_bits -= 8; + out.push((acc_value >> acc_bits) as u8); + } + + out +} + +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 +} + +// Rough translation of GetMinimalFromIndices() from: +// https://github.com/zcash/zcash/blob/6fdd9f1b81d3b228326c9826fa10696fc516444b/src/crypto/equihash.cpp#L130-L145 +#[cfg(any(feature = "solver", test))] +pub(crate) fn minimal_from_indices(p: Params, indices: &[u32]) -> Vec { + let c_bit_len = p.collision_bit_length(); + let index_bytes = (u32::BITS / 8) as usize; + let digit_bytes = ((c_bit_len + 1) + 7) / 8; + assert!(digit_bytes <= index_bytes); + + let len_indices = indices.len() * index_bytes; + let byte_pad = index_bytes - digit_bytes; + + // Rough translation of EhIndexToArray(index, array_pointer) from: + // https://github.com/zcash/zcash/blob/6fdd9f1b81d3b228326c9826fa10696fc516444b/src/crypto/equihash.cpp#L123-L128 + // + // Big-endian so that lexicographic array comparison is equivalent to integer comparison. + let array: Vec = indices + .iter() + .flat_map(|index| index.to_be_bytes()) + .collect(); + assert_eq!(array.len(), len_indices); + + compress_array(&array, c_bit_len + 1, byte_pad) +} + +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 crate::minimal::minimal_from_indices; + + use super::{compress_array, expand_array, indices_from_minimal, Params}; + + #[test] + fn array_compression_and_expansion() { + let check_array = |(bit_len, byte_pad), compact, expanded| { + assert_eq!(compress_array(expanded, bit_len, byte_pad), compact); + 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| { + let p = Params { n: 80, k: 3 }; + assert_eq!(minimal_from_indices(p, indices), minimal); + assert_eq!(indices_from_minimal(p, 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/tromp.rs b/components/equihash/src/tromp.rs new file mode 100644 index 0000000000..88eac686ac --- /dev/null +++ b/components/equihash/src/tromp.rs @@ -0,0 +1,259 @@ +//! Rust interface to the tromp equihash solver. + +use std::marker::{PhantomData, PhantomPinned}; +use std::slice; +use std::vec::Vec; + +use blake2b_simd::State; + +use crate::{blake2b, minimal::minimal_from_indices, params::Params, verify}; + +#[repr(C)] +struct CEqui { + _f: [u8; 0], + _m: PhantomData<(*mut u8, PhantomPinned)>, +} + +#[link(name = "equitromp")] +extern "C" { + #[allow(improper_ctypes)] + fn equi_new( + blake2b_clone: extern "C" fn(state: *const State) -> *mut State, + blake2b_free: extern "C" fn(state: *mut State), + blake2b_update: extern "C" fn(state: *mut State, input: *const u8, input_len: usize), + blake2b_finalize: extern "C" fn(state: *mut State, output: *mut u8, output_len: usize), + ) -> *mut CEqui; + fn equi_free(eq: *mut CEqui); + #[allow(improper_ctypes)] + fn equi_setstate(eq: *mut CEqui, ctx: *const State); + fn equi_clearslots(eq: *mut CEqui); + fn equi_digit0(eq: *mut CEqui, id: u32); + fn equi_digitodd(eq: *mut CEqui, r: u32, id: u32); + fn equi_digiteven(eq: *mut CEqui, r: u32, id: u32); + fn equi_digitK(eq: *mut CEqui, id: u32); + fn equi_nsols(eq: *const CEqui) -> usize; + /// Returns `equi_nsols()` solutions of length `2^K`, in a single memory allocation. + fn equi_sols(eq: *const CEqui) -> *const u32; +} + +/// Performs a single equihash solver run with equihash parameters `p` and hash state `curr_state`. +/// Returns zero or more unique solutions. +/// +/// # SAFETY +/// +/// The parameters to this function must match the hard-coded parameters in the C++ code. +/// +/// This function uses unsafe code for FFI into the tromp solver. +#[allow(unsafe_code)] +#[allow(clippy::print_stdout)] +unsafe fn worker(eq: *mut CEqui, p: Params, curr_state: &State) -> Vec> { + // SAFETY: caller must supply a valid `eq` instance. + // + // Review Note: nsols is set to zero in C++ here + equi_setstate(eq, curr_state); + + // Initialization done, start algo driver. + equi_digit0(eq, 0); + equi_clearslots(eq); + // SAFETY: caller must supply a `p` instance that matches the hard-coded values in the C code. + for r in 1..p.k { + if (r & 1) != 0 { + equi_digitodd(eq, r, 0) + } else { + equi_digiteven(eq, r, 0) + }; + equi_clearslots(eq); + } + // Review Note: nsols is increased here, but only if the solution passes the strictly ordered check. + // With 256 nonces, we get to around 6/9 digits strictly ordered. + equi_digitK(eq, 0); + + let solutions = { + let nsols = equi_nsols(eq); + let sols = equi_sols(eq); + let solution_len = 1 << p.k; + //println!("{nsols} solutions of length {solution_len} at {sols:?}"); + + // SAFETY: + // - caller must supply a `p` instance that matches the hard-coded values in the C code. + // - `sols` is a single allocation containing at least `nsols` solutions. + // - this slice is a shared ref to the memory in a valid `eq` instance supplied by the caller. + let solutions: &[u32] = slice::from_raw_parts(sols, nsols * solution_len); + + /* + println!( + "{nsols} solutions of length {solution_len} as a slice of length {:?}", + solutions.len() + ); + */ + + let mut chunks = solutions.chunks_exact(solution_len); + + // SAFETY: + // - caller must supply a `p` instance that matches the hard-coded values in the C code. + // - each solution contains `solution_len` u32 values. + // - the temporary slices are shared refs to a valid `eq` instance supplied by the caller. + // - the bytes in the shared ref are copied before they are returned. + // - dropping `solutions: &[u32]` does not drop the underlying memory owned by `eq`. + let mut solutions = (&mut chunks) + .map(|solution| solution.to_vec()) + .collect::>(); + + assert_eq!(chunks.remainder().len(), 0); + + // Sometimes the solver returns identical solutions. + solutions.sort(); + solutions.dedup(); + + solutions + }; + + /* + println!( + "{} solutions as cloned vectors of length {:?}", + solutions.len(), + solutions + .iter() + .map(|solution| solution.len()) + .collect::>() + ); + */ + + solutions +} + +/// Performs multiple equihash solver runs with equihash parameters `200, 9`, initialising the hash with +/// the supplied partial `input`. Between each run, generates a new nonce of length `N` using the +/// `next_nonce` function. +/// +/// Returns zero or more unique solutions. +fn solve_200_9_uncompressed( + input: &[u8], + mut next_nonce: impl FnMut() -> Option<[u8; N]>, +) -> Vec> { + let p = Params::new(200, 9).expect("should be valid"); + let mut state = verify::initialise_state(p.n, p.k, p.hash_output()); + state.update(input); + + // Create solver and initialize it. + // + // # SAFETY + // - the parameters 200,9 match the hard-coded parameters in the C++ code. + // - tromp is compiled without multi-threading support, so each instance can only support 1 thread. + // - the blake2b functions are in the correct order in Rust and C++ initializers. + #[allow(unsafe_code)] + let eq = unsafe { + equi_new( + blake2b::blake2b_clone, + blake2b::blake2b_free, + blake2b::blake2b_update, + blake2b::blake2b_finalize, + ) + }; + + let solutions = loop { + let nonce = match next_nonce() { + Some(nonce) => nonce, + None => break vec![], + }; + + let mut curr_state = state.clone(); + // Review Note: these hashes are changing when the nonce changes + curr_state.update(&nonce); + + // SAFETY: + // - the parameters 200,9 match the hard-coded parameters in the C++ code. + // - the eq instance is initilized above. + #[allow(unsafe_code)] + let solutions = unsafe { worker(eq, p, &curr_state) }; + if !solutions.is_empty() { + break solutions; + } + }; + + // SAFETY: + // - the eq instance is initilized above, and not used after this point. + #[allow(unsafe_code)] + unsafe { + equi_free(eq) + }; + + solutions +} + +/// Performs multiple equihash solver runs with equihash parameters `200, 9`, initialising the hash with +/// the supplied partial `input`. Between each run, generates a new nonce of length `N` using the +/// `next_nonce` function. +/// +/// Returns zero or more unique compressed solutions. +pub fn solve_200_9( + input: &[u8], + next_nonce: impl FnMut() -> Option<[u8; N]>, +) -> Vec> { + let p = Params::new(200, 9).expect("should be valid"); + let solutions = solve_200_9_uncompressed(input, next_nonce); + + let mut solutions: Vec> = solutions + .iter() + .map(|solution| minimal_from_indices(p, solution)) + .collect(); + + // Just in case the solver returns solutions that become the same when compressed. + solutions.sort(); + solutions.dedup(); + + solutions +} + +#[cfg(test)] +mod tests { + use std::println; + + use super::solve_200_9; + + #[test] + #[allow(clippy::print_stdout)] + fn run_solver() { + let input = b"Equihash is an asymmetric PoW based on the Generalised Birthday problem."; + let mut nonce: [u8; 32] = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]; + let mut nonces = 0..=32_u32; + let nonce_count = nonces.clone().count(); + + let solutions = solve_200_9(input, || { + let variable_nonce = nonces.next()?; + println!("Using variable nonce [0..4] of {}", variable_nonce); + + let variable_nonce = variable_nonce.to_le_bytes(); + nonce[0] = variable_nonce[0]; + nonce[1] = variable_nonce[1]; + nonce[2] = variable_nonce[2]; + nonce[3] = variable_nonce[3]; + + Some(nonce) + }); + + if solutions.is_empty() { + // Expected solution rate is documented at: + // https://github.com/tromp/equihash/blob/master/README.md + panic!("Found no solutions after {nonce_count} runs, expected 1.88 solutions per run",); + } else { + println!("Found {} solutions:", solutions.len()); + for (sol_num, solution) in solutions.iter().enumerate() { + println!("Validating solution {sol_num}:-\n{}", hex::encode(solution)); + crate::is_valid_solution(200, 9, input, &nonce, solution).unwrap_or_else(|error| { + panic!( + "unexpected invalid equihash 200, 9 solution:\n\ + error: {error:?}\n\ + input: {input:?}\n\ + nonce: {nonce:?}\n\ + solution: {solution:?}" + ) + }); + println!("Solution {sol_num} is valid!\n"); + } + } + } +} diff --git a/components/equihash/src/verify.rs b/components/equihash/src/verify.rs index 2015008838..f8f5011847 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)] @@ -148,10 +116,10 @@ impl fmt::Display for Kind { } } -fn initialise_state(n: u32, k: u32, digest_len: u8) -> Blake2bState { +pub(crate) 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!(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 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 = 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) = csr.read_u32::() { - ret.push(i); - } - - Ok(ret) -} - fn has_collision(a: &Node, b: &Node, len: usize) -> bool { a.hash .iter() @@ -347,8 +247,8 @@ pub fn is_valid_solution( nonce: &[u8], soln: &[u8], ) -> 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 { diff --git a/components/equihash/tromp/blake2b.h b/components/equihash/tromp/blake2b.h new file mode 100644 index 0000000000..23a7409b74 --- /dev/null +++ b/components/equihash/tromp/blake2b.h @@ -0,0 +1,51 @@ +// Copyright (c) 2020-2022 The Zcash developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://www.opensource.org/licenses/mit-license.php . + +#ifndef ZCASH_RUST_INCLUDE_RUST_BLAKE2B_H +#define ZCASH_RUST_INCLUDE_RUST_BLAKE2B_H + +#include + +struct BLAKE2bState; +typedef struct BLAKE2bState BLAKE2bState; +#define BLAKE2bPersonalBytes 16U + +/// Initializes a BLAKE2b state with no key and no salt. +/// +/// `personalization` MUST be a pointer to a 16-byte array. +/// +/// Please free this with `blake2b_free` when you are done. +typedef BLAKE2bState* (*blake2b_init)( + size_t output_len, + const unsigned char* personalization); + +/// Clones the given BLAKE2b state. +/// +/// Both states need to be separately freed with `blake2b_free` when you are +/// done. +typedef BLAKE2bState* (*blake2b_clone)(const BLAKE2bState* state); + +/// Frees a BLAKE2b state returned by `blake2b_init`. +typedef void (*blake2b_free)(BLAKE2bState* state); + +/// Adds input to the hash. You can call this any number of times. +typedef void (*blake2b_update)( + BLAKE2bState* state, + const unsigned char* input, + size_t input_len); + +/// Finalizes the `state` and stores the result in `output`. +/// +/// `output_len` MUST be less than or equal to the value that was passed as the +/// first parameter to `blake2b_init`. +/// +/// This method is idempotent, and calling it multiple times will give the same +/// result. It's also possible to call `blake2b_update` with more input in +/// between. +typedef void (*blake2b_finalize)( + BLAKE2bState* state, + unsigned char* output, + size_t output_len); + +#endif // ZCASH_RUST_INCLUDE_RUST_BLAKE2B_H diff --git a/components/equihash/tromp/equi.h b/components/equihash/tromp/equi.h new file mode 100644 index 0000000000..7b3969f52f --- /dev/null +++ b/components/equihash/tromp/equi.h @@ -0,0 +1,47 @@ +// Equihash solver +// Copyright (c) 2016-2016 John Tromp, The Zcash developers + +#ifndef ZCASH_POW_TROMP_EQUI_H +#define ZCASH_POW_TROMP_EQUI_H + +#include // for type bool +#include // for types uint32_t,uint64_t +#include // for functions memset +#include // for function qsort + +#include "blake2b.h" + +typedef uint32_t u32; +typedef unsigned char uchar; + +// algorithm parameters, prefixed with W to reduce include file conflicts + +#ifndef WN +#define WN 200 +#endif + +#ifndef WK +#define WK 9 +#endif + +#define NDIGITS (WK+1) +#define DIGITBITS (WN/(NDIGITS)) + +#define PROOFSIZE (1<0 the leftmost leaf of its left subtree +// is less than the leftmost leaf of its right subtree + +// The algorithm below solves this by maintaining the trees +// in a graph of K layers, each split into buckets +// with buckets indexed by the first n-RESTBITS bits following +// the i*n 0s, each bucket having 4 * 2^RESTBITS slots, +// twice the number of subtrees expected to land there. + +#ifndef ZCASH_POW_TROMP_EQUI_MINER_H +#define ZCASH_POW_TROMP_EQUI_MINER_H + +#include "equi.h" + +// Provides htole32() on macOS and Windows +#include "portable_endian.h" + +#include +#include +#include + +typedef uint16_t u16; +typedef uint64_t u64; + +#ifdef EQUIHASH_TROMP_ATOMIC +#include +typedef atomic_uint au32; +#else +typedef u32 au32; +#endif + +#ifndef RESTBITS +#define RESTBITS 8 +#endif + +// 2_log of number of buckets +#define BUCKBITS (DIGITBITS-RESTBITS) + +#ifndef SAVEMEM +#if RESTBITS == 4 +// can't save memory in such small buckets +#define SAVEMEM 1 +#elif RESTBITS >= 8 +// take advantage of law of large numbers (sum of 2^8 random numbers) +// this reduces (200,9) memory to under 144MB, with negligible discarding +#define SAVEMEM 9/14 +#endif +#endif + +// number of buckets +#define NBUCKETS (1<bid_s0_s1; + } + u32 bucketid(const tree *t) { +#ifdef SLOTDIFF + return t->bid_s0_s1 >> (2 * SLOTBITS - 1); +#else + return t->bid_s0_s1 >> (2 * SLOTBITS); +#endif + } + u32 slotid0(const tree *t) { +#ifdef SLOTDIFF + return (t->bid_s0_s1 >> (SLOTBITS-1)) & SLOTMASK; +#else + return (t->bid_s0_s1 >> SLOTBITS) & SLOTMASK; +#endif + } + u32 slotid1(const tree *t) { +#ifdef SLOTDIFF + return (slotid0() + 1 + (t->bid_s0_s1 & (SLOTMASK>>1))) & SLOTMASK; +#else + return t->bid_s0_s1 & SLOTMASK; +#endif + } + +union hashunit { + u32 word; + uchar bytes[sizeof(u32)]; +}; +typedef union hashunit hashunit; + +#define WORDS(bits) ((bits + 31) / 32) +#define HASHWORDS0 WORDS(WN - DIGITBITS + RESTBITS) +#define HASHWORDS1 WORDS(WN - 2*DIGITBITS + RESTBITS) + +struct slot0 { + tree attr; + hashunit hash[HASHWORDS0]; +}; +typedef struct slot0 slot0; + +struct slot1 { + tree attr; + hashunit hash[HASHWORDS1]; +}; +typedef struct slot1 slot1; + +// a bucket is NSLOTS treenodes +typedef slot0 bucket0[NSLOTS]; +typedef slot1 bucket1[NSLOTS]; +// the N-bit hash consists of K+1 n-bit "digits" +// each of which corresponds to a layer of NBUCKETS buckets +typedef bucket0 digit0[NBUCKETS]; +typedef bucket1 digit1[NBUCKETS]; + +// size (in bytes) of hash in round 0 <= r < WK +u32 hashsize(const u32 r) { + const u32 hashbits = WN - (r+1) * DIGITBITS + RESTBITS; + return (hashbits + 7) / 8; +} + +u32 hashwords(u32 bytes) { + return (bytes + 3) / 4; +} + +// manages hash and tree data +struct htalloc { + u32 *heap0; + u32 *heap1; + bucket0 *trees0[(WK+1)/2]; + bucket1 *trees1[WK/2]; + u32 alloced; +}; +typedef struct htalloc htalloc; + htalloc htalloc_new() { + htalloc hta; + hta.alloced = 0; + return hta; + } + void *htalloc_alloc(htalloc *hta, const u32 n, const u32 sz); + void alloctrees(htalloc *hta) { +// optimize xenoncat's fixed memory layout, avoiding any waste +// digit trees hashes trees hashes +// 0 0 A A A A A A . . . . . . +// 1 0 A A A A A A 1 B B B B B +// 2 0 2 C C C C C 1 B B B B B +// 3 0 2 C C C C C 1 3 D D D D +// 4 0 2 4 E E E E 1 3 D D D D +// 5 0 2 4 E E E E 1 3 5 F F F +// 6 0 2 4 6 . G G 1 3 5 F F F +// 7 0 2 4 6 . G G 1 3 5 7 H H +// 8 0 2 4 6 8 . I 1 3 5 7 H H + assert(DIGITBITS >= 16); // ensures hashes shorten by 1 unit every 2 digits + hta->heap0 = (u32 *)htalloc_alloc(hta, 1, sizeof(digit0)); + hta->heap1 = (u32 *)htalloc_alloc(hta, 1, sizeof(digit1)); + for (int r=0; rtrees0[r/2] = (bucket0 *)(hta->heap0 + r/2); + else + hta->trees1[r/2] = (bucket1 *)(hta->heap1 + r/2); + } + void dealloctrees(htalloc *hta) { + if (hta == NULL) { + return; + } + + free(hta->heap0); + free(hta->heap1); + // Avoid use-after-free and double-free + hta->heap0 = NULL; + hta->heap1 = NULL; + + for (int r=0; rtrees0[r/2] = NULL; + else + hta->trees1[r/2] = NULL; + hta->alloced = 0; + } + void *htalloc_alloc(htalloc *hta, const u32 n, const u32 sz) { + void *mem = calloc(n, sz); + assert(mem); + hta->alloced += n * sz; + return mem; + } + +typedef au32 bsizes[NBUCKETS]; + +u32 minu32(const u32 a, const u32 b) { + return a < b ? a : b; +} + +struct equi { + BLAKE2bState* blake_ctx; + blake2b_clone blake2b_clone; + blake2b_free blake2b_free; + blake2b_update blake2b_update; + blake2b_finalize blake2b_finalize; + htalloc hta; + bsizes *nslots; // PUT IN BUCKET STRUCT + proof *sols; + au32 nsols; + u32 xfull; + u32 hfull; + u32 bfull; +}; +typedef struct equi equi; + void equi_clearslots(equi *eq); + equi *equi_new( + blake2b_clone blake2b_clone, + blake2b_free blake2b_free, + blake2b_update blake2b_update, + blake2b_finalize blake2b_finalize + ) { + assert(sizeof(hashunit) == 4); + equi *eq = malloc(sizeof(equi)); + eq->blake2b_clone = blake2b_clone; + eq->blake2b_free = blake2b_free; + eq->blake2b_update = blake2b_update; + eq->blake2b_finalize = blake2b_finalize; + + alloctrees(&eq->hta); + eq->nslots = (bsizes *)htalloc_alloc(&eq->hta, 2 * NBUCKETS, sizeof(au32)); + eq->sols = (proof *)htalloc_alloc(&eq->hta, MAXSOLS, sizeof(proof)); + + // C malloc() does not guarantee zero-initialized memory (but calloc() does) + eq->blake_ctx = NULL; + eq->nsols = 0; + equi_clearslots(eq); + + return eq; + } + void equi_free(equi *eq) { + if (eq == NULL) { + return; + } + + dealloctrees(&eq->hta); + + free(eq->nslots); + free(eq->sols); + eq->blake2b_free(eq->blake_ctx); + // Avoid use-after-free and double-free + eq->nslots = NULL; + eq->sols = NULL; + eq->blake_ctx = NULL; + + free(eq); + } + void equi_setstate(equi *eq, const BLAKE2bState *ctx) { + if (eq->blake_ctx) { + eq->blake2b_free(eq->blake_ctx); + } + + eq->blake_ctx = eq->blake2b_clone(ctx); + memset(eq->nslots, 0, NBUCKETS * sizeof(au32)); // only nslots[0] needs zeroing + equi_clearslots(eq); + eq->nsols = 0; + } + void equi_clearslots(equi *eq) { + eq->xfull = eq->bfull = eq->hfull = 0; + } + u32 getslot(equi *eq, const u32 r, const u32 bucketi) { +#ifdef EQUIHASH_TROMP_ATOMIC + return std::atomic_fetch_add_explicit(&eq->nslots[r&1][bucketi], 1U, std::memory_order_relaxed); +#else + return eq->nslots[r&1][bucketi]++; +#endif + } + u32 getnslots(equi *eq, const u32 r, const u32 bid) { // SHOULD BE METHOD IN BUCKET STRUCT + au32 *nslot = &eq->nslots[r&1][bid]; + const u32 n = minu32(*nslot, NSLOTS); + *nslot = 0; + return n; + } + void orderindices(u32 *indices, u32 size) { + if (indices[0] > indices[size]) { + for (u32 i=0; i < size; i++) { + const u32 tmp = indices[i]; + indices[i] = indices[size+i]; + indices[size+i] = tmp; + } + } + } + void listindices1(equi *eq, u32 r, const tree t, u32 *indices); + void listindices0(equi *eq, u32 r, const tree t, u32 *indices) { + if (r == 0) { + *indices = getindex(&t); + return; + } + const bucket1 *buck = &eq->hta.trees1[--r/2][bucketid(&t)]; + const u32 size = 1 << r; + u32 *indices1 = indices + size; + listindices1(eq, r, (*buck)[slotid0(&t)].attr, indices); + listindices1(eq, r, (*buck)[slotid1(&t)].attr, indices1); + orderindices(indices, size); + } + void listindices1(equi *eq, u32 r, const tree t, u32 *indices) { + const bucket0 *buck = &eq->hta.trees0[--r/2][bucketid(&t)]; + const u32 size = 1 << r; + u32 *indices1 = indices + size; + listindices0(eq, r, (*buck)[slotid0(&t)].attr, indices); + listindices0(eq, r, (*buck)[slotid1(&t)].attr, indices1); + orderindices(indices, size); + } + void candidate(equi *eq, const tree t) { + proof prf; + listindices1(eq, WK, t, prf); // assume WK odd + qsort(prf, PROOFSIZE, sizeof(u32), &compu32); + for (u32 i=1; i proof[%d], actual: %d <= %d\n", + i, i-1, prf[i], prf[i-1] + ); + */ + return; + } +#ifdef EQUIHASH_TROMP_ATOMIC + u32 soli = std::atomic_fetch_add_explicit(&eq->nsols, 1U, std::memory_order_relaxed); +#else + u32 soli = eq->nsols++; +#endif + if (soli < MAXSOLS) + listindices1(eq, WK, t, eq->sols[soli]); // assume WK odd + } +#ifdef EQUIHASH_SHOW_BUCKET_SIZES + void showbsizes(equi *eq, u32 r) { +#if defined(HIST) || defined(SPARK) || defined(LOGSPARK) + u32 binsizes[65]; + memset(binsizes, 0, 65 * sizeof(u32)); + for (u32 bucketid = 0; bucketid < NBUCKETS; bucketid++) { + u32 bsize = minu32(eq->nslots[r&1][bucketid], NSLOTS) >> (SLOTBITS-6); + binsizes[bsize]++; + } + for (u32 i=0; i < 65; i++) { +#ifdef HIST +// printf(" %d:%d", i, binsizes[i]); +#else +#ifdef SPARK + u32 sparks = binsizes[i] / SPARKSCALE; +#else + u32 sparks = 0; + for (u32 bs = binsizes[i]; bs; bs >>= 1) sparks++; + sparks = sparks * 7 / SPARKSCALE; +#endif +// printf("\342\226%c", '\201' + sparks); +#endif + } +// printf("\n"); +#endif + } +#endif + + struct htlayout { + htalloc hta; + u32 prevhashunits; + u32 nexthashunits; + u32 dunits; + u32 prevbo; + u32 nextbo; + }; + typedef struct htlayout htlayout; + + htlayout htlayout_new(equi *eq, u32 r) { + htlayout htl; + htl.hta = eq->hta; + htl.prevhashunits = 0; + htl.dunits = 0; + u32 nexthashbytes = hashsize(r); + htl.nexthashunits = hashwords(nexthashbytes); + htl.prevbo = 0; + htl.nextbo = htl.nexthashunits * sizeof(hashunit) - nexthashbytes; // 0-3 + if (r) { + u32 prevhashbytes = hashsize(r-1); + htl.prevhashunits = hashwords(prevhashbytes); + htl.prevbo = htl.prevhashunits * sizeof(hashunit) - prevhashbytes; // 0-3 + htl.dunits = htl.prevhashunits - htl.nexthashunits; + } + return htl; + } + u32 getxhash0(const htlayout *htl, const slot0* pslot) { +#if WN == 200 && RESTBITS == 4 + return pslot->hash->bytes[htl->prevbo] >> 4; +#elif WN == 200 && RESTBITS == 8 + return (pslot->hash->bytes[htl->prevbo] & 0xf) << 4 | pslot->hash->bytes[htl->prevbo+1] >> 4; +#elif WN == 200 && RESTBITS == 9 + return (pslot->hash->bytes[htl->prevbo] & 0x1f) << 4 | pslot->hash->bytes[htl->prevbo+1] >> 4; +#elif WN == 144 && RESTBITS == 4 + return pslot->hash->bytes[htl->prevbo] & 0xf; +#else +#error non implemented +#endif + } + u32 getxhash1(const htlayout *htl, const slot1* pslot) { +#if WN == 200 && RESTBITS == 4 + return pslot->hash->bytes[htl->prevbo] & 0xf; +#elif WN == 200 && RESTBITS == 8 + return pslot->hash->bytes[htl->prevbo]; +#elif WN == 200 && RESTBITS == 9 + return (pslot->hash->bytes[htl->prevbo]&1) << 8 | pslot->hash->bytes[htl->prevbo+1]; +#elif WN == 144 && RESTBITS == 4 + return pslot->hash->bytes[htl->prevbo] & 0xf; +#else +#error non implemented +#endif + } + bool htlayout_equal(const htlayout *htl, const hashunit *hash0, const hashunit *hash1) { + return hash0[htl->prevhashunits-1].word == hash1[htl->prevhashunits-1].word; + } + +#if RESTBITS <= 6 + typedef uchar xslot; +#else + typedef u16 xslot; +#endif + struct collisiondata { +#ifdef XBITMAP +#if NSLOTS > 64 +#error cant use XBITMAP with more than 64 slots +#endif + u64 xhashmap[NRESTS]; + u64 xmap; +#else + xslot nxhashslots[NRESTS]; + xslot xhashslots[NRESTS][XFULL]; + xslot *xx; + u32 n0; + u32 n1; +#endif + u32 s0; + }; + typedef struct collisiondata collisiondata; + + void collisiondata_clear(collisiondata *cd) { +#ifdef XBITMAP + memset(cd->xhashmap, 0, NRESTS * sizeof(u64)); +#else + memset(cd->nxhashslots, 0, NRESTS * sizeof(xslot)); +#endif + } + bool addslot(collisiondata *cd, u32 s1, u32 xh) { +#ifdef XBITMAP + xmap = xhashmap[xh]; + xhashmap[xh] |= (u64)1 << s1; + s0 = -1; + return true; +#else + cd->n1 = (u32)cd->nxhashslots[xh]++; + if (cd->n1 >= XFULL) + return false; + cd->xx = cd->xhashslots[xh]; + cd->xx[cd->n1] = s1; + cd->n0 = 0; + return true; +#endif + } + bool nextcollision(const collisiondata *cd) { +#ifdef XBITMAP + return cd->xmap != 0; +#else + return cd->n0 < cd->n1; +#endif + } + u32 slot(collisiondata *cd) { +#ifdef XBITMAP + const u32 ffs = __builtin_ffsll(cd->xmap); + s0 += ffs; cd->xmap >>= ffs; + return s0; +#else + return (u32)cd->xx[cd->n0++]; +#endif + } + + void equi_digit0(equi *eq, const u32 id) { + uchar hash[HASHOUT]; + BLAKE2bState* state; + htlayout htl = htlayout_new(eq, 0); + const u32 hashbytes = hashsize(0); + for (u32 block = id; block < NBLOCKS; block++) { + state = eq->blake2b_clone(eq->blake_ctx); + u32 leb = htole32(block); + eq->blake2b_update(state, (uchar *)&leb, sizeof(u32)); + eq->blake2b_finalize(state, hash, HASHOUT); + eq->blake2b_free(state); + // Avoid use-after-free and double-free + state = NULL; + + for (u32 i = 0; i> 4; +#elif BUCKBITS == 11 && RESTBITS == 9 + const u32 bucketid = ((u32)ph[0] << 3) | ph[1] >> 5; +#elif BUCKBITS == 20 && RESTBITS == 4 + const u32 bucketid = ((((u32)ph[0] << 8) | ph[1]) << 4) | ph[2] >> 4; +#elif BUCKBITS == 12 && RESTBITS == 4 + const u32 bucketid = ((u32)ph[0] << 4) | ph[1] >> 4; + const u32 xhash = ph[1] & 0xf; +#else +#error not implemented +#endif + const u32 slot = getslot(eq, 0, bucketid); + if (slot >= NSLOTS) { + eq->bfull++; + continue; + } + slot0 *s = &eq->hta.trees0[0][bucketid][slot]; + s->attr = tree_from_idx(block * HASHESPERBLAKE + i); + memcpy(s->hash->bytes+htl.nextbo, ph+WN/8-hashbytes, hashbytes); + } + } + } + + void equi_digitodd(equi *eq, const u32 r, const u32 id) { + htlayout htl = htlayout_new(eq, r); + collisiondata cd; + for (u32 bucketid=id; bucketid < NBUCKETS; bucketid++) { + collisiondata_clear(&cd); + slot0 *buck = htl.hta.trees0[(r-1)/2][bucketid]; // optimize by updating previous buck?! + u32 bsize = getnslots(eq, r-1, bucketid); // optimize by putting bucketsize with block?! + for (u32 s1 = 0; s1 < bsize; s1++) { + const slot0 *pslot1 = buck + s1; // optimize by updating previous pslot1?! + if (!addslot(&cd, s1, getxhash0(&htl, pslot1))) { + eq->xfull++; + continue; + } + for (; nextcollision(&cd); ) { + const u32 s0 = slot(&cd); + const slot0 *pslot0 = buck + s0; + if (htlayout_equal(&htl, pslot0->hash, pslot1->hash)) { + eq->hfull++; + continue; + } + u32 xorbucketid; + const uchar *bytes0 = pslot0->hash->bytes, *bytes1 = pslot1->hash->bytes; +#if WN == 200 && BUCKBITS == 12 && RESTBITS == 8 + xorbucketid = (((u32)(bytes0[htl.prevbo+1] ^ bytes1[htl.prevbo+1]) & 0xf) << 8) + | (bytes0[htl.prevbo+2] ^ bytes1[htl.prevbo+2]); +#elif WN == 200 && BUCKBITS == 11 && RESTBITS == 9 + xorbucketid = (((u32)(bytes0[htl.prevbo+1] ^ bytes1[htl.prevbo+1]) & 0xf) << 7) + | (bytes0[htl.prevbo+2] ^ bytes1[htl.prevbo+2]) >> 1; +#elif WN == 144 && BUCKBITS == 20 && RESTBITS == 4 + xorbucketid = ((((u32)(bytes0[htl.prevbo+1] ^ bytes1[htl.prevbo+1]) << 8) + | (bytes0[htl.prevbo+2] ^ bytes1[htl.prevbo+2])) << 4) + | (bytes0[htl.prevbo+3] ^ bytes1[htl.prevbo+3]) >> 4; +#elif WN == 96 && BUCKBITS == 12 && RESTBITS == 4 + xorbucketid = ((u32)(bytes0[htl.prevbo+1] ^ bytes1[htl.prevbo+1]) << 4) + | (bytes0[htl.prevbo+2] ^ bytes1[htl.prevbo+2]) >> 4; +#else +#error not implemented +#endif + const u32 xorslot = getslot(eq, r, xorbucketid); + if (xorslot >= NSLOTS) { + eq->bfull++; + continue; + } + slot1 *xs = &htl.hta.trees1[r/2][xorbucketid][xorslot]; + xs->attr = tree_from_bid(bucketid, s0, s1); + for (u32 i=htl.dunits; i < htl.prevhashunits; i++) + xs->hash[i-htl.dunits].word = pslot0->hash[i].word ^ pslot1->hash[i].word; + } + } + } + } + + void equi_digiteven(equi *eq, const u32 r, const u32 id) { + htlayout htl = htlayout_new(eq, r); + collisiondata cd; + for (u32 bucketid=id; bucketid < NBUCKETS; bucketid++) { + collisiondata_clear(&cd); + slot1 *buck = htl.hta.trees1[(r-1)/2][bucketid]; // OPTIMIZE BY UPDATING PREVIOUS + u32 bsize = getnslots(eq, r-1, bucketid); + for (u32 s1 = 0; s1 < bsize; s1++) { + const slot1 *pslot1 = buck + s1; // OPTIMIZE BY UPDATING PREVIOUS + if (!addslot(&cd, s1, getxhash1(&htl, pslot1))) { + eq->xfull++; + continue; + } + for (; nextcollision(&cd); ) { + const u32 s0 = slot(&cd); + const slot1 *pslot0 = buck + s0; + if (htlayout_equal(&htl, pslot0->hash, pslot1->hash)) { + eq->hfull++; + continue; + } + u32 xorbucketid; + const uchar *bytes0 = pslot0->hash->bytes, *bytes1 = pslot1->hash->bytes; +#if WN == 200 && BUCKBITS == 12 && RESTBITS == 8 + xorbucketid = ((u32)(bytes0[htl.prevbo+1] ^ bytes1[htl.prevbo+1]) << 4) + | (bytes0[htl.prevbo+2] ^ bytes1[htl.prevbo+2]) >> 4; +#elif WN == 200 && BUCKBITS == 11 && RESTBITS == 9 + xorbucketid = ((u32)(bytes0[htl.prevbo+2] ^ bytes1[htl.prevbo+2]) << 3) + | (bytes0[htl.prevbo+3] ^ bytes1[htl.prevbo+3]) >> 5; +#elif WN == 144 && BUCKBITS == 20 && RESTBITS == 4 + xorbucketid = ((((u32)(bytes0[htl.prevbo+1] ^ bytes1[htl.prevbo+1]) << 8) + | (bytes0[htl.prevbo+2] ^ bytes1[htl.prevbo+2])) << 4) + | (bytes0[htl.prevbo+3] ^ bytes1[htl.prevbo+3]) >> 4; +#elif WN == 96 && BUCKBITS == 12 && RESTBITS == 4 + xorbucketid = ((u32)(bytes0[htl.prevbo+1] ^ bytes1[htl.prevbo+1]) << 4) + | (bytes0[htl.prevbo+2] ^ bytes1[htl.prevbo+2]) >> 4; +#else +#error not implemented +#endif + const u32 xorslot = getslot(eq, r, xorbucketid); + if (xorslot >= NSLOTS) { + eq->bfull++; + continue; + } + slot0 *xs = &htl.hta.trees0[r/2][xorbucketid][xorslot]; + xs->attr = tree_from_bid(bucketid, s0, s1); + for (u32 i=htl.dunits; i < htl.prevhashunits; i++) + xs->hash[i-htl.dunits].word = pslot0->hash[i].word ^ pslot1->hash[i].word; + } + } + } + } + + void equi_digitK(equi *eq, const u32 id) { + collisiondata cd; + htlayout htl = htlayout_new(eq, WK); +u32 nc = 0; + for (u32 bucketid = id; bucketid < NBUCKETS; bucketid++) { + collisiondata_clear(&cd); + slot0 *buck = htl.hta.trees0[(WK-1)/2][bucketid]; + u32 bsize = getnslots(eq, WK-1, bucketid); + for (u32 s1 = 0; s1 < bsize; s1++) { + const slot0 *pslot1 = buck + s1; + if (!addslot(&cd, s1, getxhash0(&htl, pslot1))) // assume WK odd + continue; + for (; nextcollision(&cd); ) { + const u32 s0 = slot(&cd); + if (htlayout_equal(&htl, buck[s0].hash, pslot1->hash)) +nc++, candidate(eq, tree_from_bid(bucketid, s0, s1)); + } + } + } +//printf(" %d candidates\n", nc); + } + + size_t equi_nsols(const equi *eq) { + return eq->nsols; + } + proof *equi_sols(const equi *eq) { + return eq->sols; + } + +typedef struct { + u32 id; + equi *eq; +} thread_ctx; + +void *worker(void *vp) { + thread_ctx *tp = (thread_ctx *)vp; + equi *eq = tp->eq; + +// if (tp->id == 0) +// printf("Digit 0\n"); + if (tp->id == 0) { + equi_clearslots(eq); + } + equi_digit0(eq, tp->id); + if (tp->id == 0) { + equi_clearslots(eq); +#ifdef EQUIHASH_SHOW_BUCKET_SIZES + showbsizes(eq, 0); +#endif + } + for (u32 r = 1; r < WK; r++) { +// if (tp->id == 0) +// printf("Digit %d", r); + r&1 ? equi_digitodd(eq, r, tp->id) : equi_digiteven(eq, r, tp->id); + if (tp->id == 0) { +// printf(" x%d b%d h%d\n", eq->xfull, eq->bfull, eq->hfull); + equi_clearslots(eq); +#ifdef EQUIHASH_SHOW_BUCKET_SIZES + showbsizes(eq, r); +#endif + } + } +// if (tp->id == 0) +// printf("Digit %d\n", WK); + equi_digitK(eq, tp->id); + return 0; +} + +#endif // ZCASH_POW_TROMP_EQUI_MINER_H diff --git a/components/equihash/tromp/portable_endian.h b/components/equihash/tromp/portable_endian.h new file mode 100644 index 0000000000..4a71ce7a7a --- /dev/null +++ b/components/equihash/tromp/portable_endian.h @@ -0,0 +1,130 @@ +// +// endian.h +// +// https://gist.github.com/panzi/6856583 +// +// I, Mathias Panzenböck, place this file hereby into the public domain. Use +// it at your own risk for whatever you like. In case there are +// jurisdictions that don't support putting things in the public domain you +// can also consider it to be "dual licensed" under the BSD, MIT and Apache +// licenses, if you want to. This code is trivial anyway. Consider it an +// example on how to get the endian conversion functions on different +// platforms. + +// Downloaded from https://raw.githubusercontent.com/mikepb/endian.h/master/endian.h +// on 12 January 2024. + +#ifndef PORTABLE_ENDIAN_H__ +#define PORTABLE_ENDIAN_H__ + +#if (defined(_WIN16) || defined(_WIN32) || defined(_WIN64)) && !defined(__WINDOWS__) + +# define __WINDOWS__ + +#endif + +#if defined(__linux__) || defined(__CYGWIN__) + +# include + +#elif defined(__APPLE__) + +# include + +# define htobe16(x) OSSwapHostToBigInt16(x) +# define htole16(x) OSSwapHostToLittleInt16(x) +# define be16toh(x) OSSwapBigToHostInt16(x) +# define le16toh(x) OSSwapLittleToHostInt16(x) + +# define htobe32(x) OSSwapHostToBigInt32(x) +# define htole32(x) OSSwapHostToLittleInt32(x) +# define be32toh(x) OSSwapBigToHostInt32(x) +# define le32toh(x) OSSwapLittleToHostInt32(x) + +# define htobe64(x) OSSwapHostToBigInt64(x) +# define htole64(x) OSSwapHostToLittleInt64(x) +# define be64toh(x) OSSwapBigToHostInt64(x) +# define le64toh(x) OSSwapLittleToHostInt64(x) + +# define __BYTE_ORDER BYTE_ORDER +# define __BIG_ENDIAN BIG_ENDIAN +# define __LITTLE_ENDIAN LITTLE_ENDIAN +# define __PDP_ENDIAN PDP_ENDIAN + +#elif defined(__OpenBSD__) + +# include + +#elif defined(__NetBSD__) || defined(__FreeBSD__) || defined(__DragonFly__) + +# include + +# define be16toh(x) betoh16(x) +# define le16toh(x) letoh16(x) + +# define be32toh(x) betoh32(x) +# define le32toh(x) letoh32(x) + +# define be64toh(x) betoh64(x) +# define le64toh(x) letoh64(x) + +#elif defined(__WINDOWS__) + +# include + +// Not available in librustzcash CI +//# include + +# if BYTE_ORDER == LITTLE_ENDIAN + +# define htobe16(x) htons(x) +# define htole16(x) (x) +# define be16toh(x) ntohs(x) +# define le16toh(x) (x) + +# define htobe32(x) htonl(x) +# define htole32(x) (x) +# define be32toh(x) ntohl(x) +# define le32toh(x) (x) + +# define htobe64(x) htonll(x) +# define htole64(x) (x) +# define be64toh(x) ntohll(x) +# define le64toh(x) (x) + +# elif BYTE_ORDER == BIG_ENDIAN + + /* that would be xbox 360 */ +# define htobe16(x) (x) +# define htole16(x) __builtin_bswap16(x) +# define be16toh(x) (x) +# define le16toh(x) __builtin_bswap16(x) + +# define htobe32(x) (x) +# define htole32(x) __builtin_bswap32(x) +# define be32toh(x) (x) +# define le32toh(x) __builtin_bswap32(x) + +# define htobe64(x) (x) +# define htole64(x) __builtin_bswap64(x) +# define be64toh(x) (x) +# define le64toh(x) __builtin_bswap64(x) + +# else + +# error byte order not supported + +# endif + +# define __BYTE_ORDER BYTE_ORDER +# define __BIG_ENDIAN BIG_ENDIAN +# define __LITTLE_ENDIAN LITTLE_ENDIAN +# define __PDP_ENDIAN PDP_ENDIAN + +#else + +# error platform not supported + +#endif + +#endif diff --git a/components/f4jumble/CHANGELOG.md b/components/f4jumble/CHANGELOG.md index 0dd3e14c90..0d0e46fc84 100644 --- a/components/f4jumble/CHANGELOG.md +++ b/components/f4jumble/CHANGELOG.md @@ -7,6 +7,11 @@ and this library adheres to Rust's notion of ## [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 index e62afd713c..ffccee0351 100644 --- a/components/f4jumble/Cargo.toml +++ b/components/f4jumble/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "f4jumble" description = "Implementation of Zcash's F4Jumble algorithm" -version = "0.1.0" +version = "0.1.1" authors = [ "Jack Grigg ", "Kris Nuttycombe ", @@ -32,4 +32,11 @@ proptest = "1" [features] default = ["std"] -std = ["blake2b_simd/std"] +alloc = [] +std = [ + "alloc", + "blake2b_simd/std", +] + +[lints] +workspace = true diff --git a/components/f4jumble/src/lib.rs b/components/f4jumble/src/lib.rs index bc86767522..982e9d64ee 100644 --- a/components/f4jumble/src/lib.rs +++ b/components/f4jumble/src/lib.rs @@ -53,11 +53,15 @@ 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 = "std")] -use std::vec::Vec; + +#[cfg(feature = "alloc")] +use alloc::vec::Vec; #[cfg(test)] mod test_vectors; @@ -264,8 +268,8 @@ pub fn f4jumble_inv_mut(message: &mut [u8]) -> Result<(), Error> { /// "af1d55f2695aea02440867bbbfae3b08e8da55b625de3fa91432ab7b2c0a7dff9033ee666db1513ba5761ef482919fb8", /// ); /// ``` -#[cfg(feature = "std")] -#[cfg_attr(docsrs, doc(cfg(feature = "std")))] +#[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) @@ -291,8 +295,8 @@ pub fn f4jumble(message: &[u8]) -> Result, Error> { /// let message_b = f4jumble::f4jumble_inv(&encoded_b).unwrap(); /// assert_eq!(message_b, b"The package from Sarah arrives tomorrow morning."); /// ``` -#[cfg(feature = "std")] -#[cfg_attr(docsrs, doc(cfg(feature = "std")))] +#[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) diff --git a/components/f4jumble/src/test_vectors.rs b/components/f4jumble/src/test_vectors.rs index ac1474e3ae..fa99ae43dd 100644 --- a/components/f4jumble/src/test_vectors.rs +++ b/components/f4jumble/src/test_vectors.rs @@ -8,7 +8,7 @@ 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 +// From https://github.com/zcash/zcash-test-vectors/blob/master/zcash_test_vectors/f4jumble.py pub(crate) const TEST_VECTORS: &[TestVector] = &[ TestVector { normal: &[ diff --git a/components/zcash_address/CHANGELOG.md b/components/zcash_address/CHANGELOG.md index a7bf6f6385..10f5a55685 100644 --- a/components/zcash_address/CHANGELOG.md +++ b/components/zcash_address/CHANGELOG.md @@ -7,6 +7,61 @@ and this library adheres to Rust's notion of ## [Unreleased] +## [0.6.3, 0.7.1] - 2025-05-07 +### Added +- `zcash_address::Converter` +- `zcash_address::ZcashAddress::convert_with` + +## [0.7.0] - 2025-02-21 +### Added +- `zcash_address::unified::Item` to expose the opaque typed encoding of unified + items. + +### Changed +- Migrated to `zcash_encoding 0.3`, `zcash_protocol 0.5`. + +### 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`. diff --git a/components/zcash_address/Cargo.toml b/components/zcash_address/Cargo.toml index 3ccef17a46..5fb4a21d33 100644 --- a/components/zcash_address/Cargo.toml +++ b/components/zcash_address/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zcash_address" description = "Zcash address parsing and serialization" -version = "0.3.0" +version = "0.7.1" authors = [ "Jack Grigg ", ] @@ -14,18 +14,35 @@ 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 = "0.9" -bs58 = { version = "0.5", features = ["check"] } -f4jumble = { version = "0.1", path = "../f4jumble" } -zcash_encoding = { version = "0.2", path = "../zcash_encoding" } +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 = "1.3.0" -proptest = "1" +assert_matches.workspace = true +proptest.workspace = true [features] -test-dependencies = [] +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/src/convert.rs b/components/zcash_address/src/convert.rs index 38b04f374a..925ca6040e 100644 --- a/components/zcash_address/src/convert.rs +++ b/components/zcash_address/src/convert.rs @@ -1,8 +1,13 @@ -use std::{error::Error, fmt}; +use core::fmt; -use crate::{kind::*, AddressKind, Network, ZcashAddress}; +#[cfg(feature = "std")] +use std::error::Error; -/// An address type is not supported for conversion. +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); @@ -16,7 +21,10 @@ impl fmt::Display for UnsupportedAddress { #[derive(Debug)] pub enum ConversionError { /// The address is for the wrong network. - IncorrectNetwork { expected: Network, actual: 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. @@ -43,7 +51,9 @@ impl fmt::Display for ConversionError { } } +#[cfg(feature = "std")] impl Error for UnsupportedAddress {} +#[cfg(feature = "std")] impl Error for ConversionError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { @@ -55,7 +65,7 @@ impl Error for ConversionError { /// A helper trait for converting a [`ZcashAddress`] into a network-agnostic type. /// -/// A blanket implementation of [`TryFromAddress`] is provided for `(Network, T)` where +/// A blanket implementation of [`TryFromAddress`] is provided for `(NetworkType, T)` where /// `T: TryFromRawAddress`. /// /// [`ZcashAddress`]: crate::ZcashAddress @@ -63,7 +73,8 @@ impl Error for ConversionError { /// # Examples /// /// ``` -/// use zcash_address::{ConversionError, Network, TryFromRawAddress, UnsupportedAddress, ZcashAddress}; +/// use zcash_address::{ConversionError, TryFromRawAddress, UnsupportedAddress, ZcashAddress}; +/// use zcash_protocol::consensus::NetworkType; /// /// #[derive(Debug, PartialEq)] /// struct MySapling([u8; 43]); @@ -85,7 +96,7 @@ impl Error for ConversionError { /// /// // 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::(Network::Main); +/// let converted = addr.convert_if_network::(NetworkType::Main); /// assert!(converted.is_ok()); /// assert_eq!(converted.unwrap(), MySapling([0; 43])); /// @@ -93,7 +104,7 @@ impl Error for ConversionError { /// let addr: ZcashAddress = addr_string.parse().unwrap(); /// let converted = addr.convert::<(_, MySapling)>(); /// assert!(converted.is_ok()); -/// assert_eq!(converted.unwrap(), (Network::Main, MySapling([0; 43]))); +/// assert_eq!(converted.unwrap(), (NetworkType::Main, MySapling([0; 43]))); /// /// // For an unsupported address type, we get an error. /// let addr: ZcashAddress = "t1Hsc1LR8yKnbbe3twRp88p6vFfC5t7DLbs".parse().unwrap(); @@ -107,12 +118,12 @@ pub trait TryFromRawAddress: Sized { /// [`Self::try_from_raw_sapling`] as a valid Sapling address). type Error; - fn try_from_raw_sprout(data: sprout::Data) -> Result> { + fn try_from_raw_sprout(data: [u8; 64]) -> Result> { let _ = data; Err(ConversionError::Unsupported(UnsupportedAddress("Sprout"))) } - fn try_from_raw_sapling(data: sapling::Data) -> Result> { + fn try_from_raw_sapling(data: [u8; 43]) -> Result> { let _ = data; Err(ConversionError::Unsupported(UnsupportedAddress("Sapling"))) } @@ -123,7 +134,7 @@ pub trait TryFromRawAddress: Sized { } fn try_from_raw_transparent_p2pkh( - data: p2pkh::Data, + data: [u8; 20], ) -> Result> { let _ = data; Err(ConversionError::Unsupported(UnsupportedAddress( @@ -131,14 +142,19 @@ pub trait TryFromRawAddress: Sized { ))) } - fn try_from_raw_transparent_p2sh( - data: p2sh::Data, - ) -> Result> { + 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. @@ -148,7 +164,8 @@ pub trait TryFromRawAddress: Sized { /// # Examples /// /// ``` -/// use zcash_address::{ConversionError, Network, TryFromAddress, UnsupportedAddress, ZcashAddress}; +/// use zcash_address::{ConversionError, TryFromAddress, UnsupportedAddress, ZcashAddress}; +/// use zcash_protocol::consensus::NetworkType; /// /// #[derive(Debug)] /// struct MySapling([u8; 43]); @@ -161,7 +178,7 @@ pub trait TryFromRawAddress: Sized { /// type Error = &'static str; /// /// fn try_from_sapling( -/// net: Network, +/// net: NetworkType, /// data: [u8; 43], /// ) -> Result> { /// Ok(MySapling(data)) @@ -188,23 +205,23 @@ pub trait TryFromAddress: Sized { type Error; fn try_from_sprout( - net: Network, - data: sprout::Data, + net: NetworkType, + data: [u8; 64], ) -> Result> { let _ = (net, data); Err(ConversionError::Unsupported(UnsupportedAddress("Sprout"))) } fn try_from_sapling( - net: Network, - data: sapling::Data, + net: NetworkType, + data: [u8; 43], ) -> Result> { let _ = (net, data); Err(ConversionError::Unsupported(UnsupportedAddress("Sapling"))) } fn try_from_unified( - net: Network, + net: NetworkType, data: unified::Address, ) -> Result> { let _ = (net, data); @@ -212,8 +229,8 @@ pub trait TryFromAddress: Sized { } fn try_from_transparent_p2pkh( - net: Network, - data: p2pkh::Data, + net: NetworkType, + data: [u8; 20], ) -> Result> { let _ = (net, data); Err(ConversionError::Unsupported(UnsupportedAddress( @@ -222,53 +239,168 @@ pub trait TryFromAddress: Sized { } fn try_from_transparent_p2sh( - net: Network, - data: p2sh::Data, + 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 (Network, T) { +impl TryFromAddress for (NetworkType, T) { type Error = T::Error; fn try_from_sprout( - net: Network, - data: sprout::Data, + net: NetworkType, + data: [u8; 64], ) -> Result> { T::try_from_raw_sprout(data).map(|addr| (net, addr)) } fn try_from_sapling( - net: Network, - data: sapling::Data, + net: NetworkType, + data: [u8; 43], ) -> Result> { T::try_from_raw_sapling(data).map(|addr| (net, addr)) } fn try_from_unified( - net: Network, + net: NetworkType, data: unified::Address, ) -> Result> { T::try_from_raw_unified(data).map(|addr| (net, addr)) } fn try_from_transparent_p2pkh( - net: Network, - data: p2pkh::Data, + net: NetworkType, + data: [u8; 20], ) -> Result> { T::try_from_raw_transparent_p2pkh(data).map(|addr| (net, addr)) } fn try_from_transparent_p2sh( - net: Network, - data: p2sh::Data, + 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 trait for converter types that can project from a [`ZcashAddress`] into another type. +/// +/// [`ZcashAddress`]: crate::ZcashAddress +/// +/// # Examples +/// +/// ``` +/// use zcash_address::{ConversionError, Converter, UnsupportedAddress, ZcashAddress}; +/// use zcash_protocol::consensus::NetworkType; +/// +/// struct KeyFinder { } +/// +/// impl KeyFinder { +/// fn find_sapling_extfvk(&self, data: [u8; 43]) -> Option<[u8; 73]> { +/// todo!() +/// } +/// } +/// +/// // Makes it possible to use a KeyFinder to find the Sapling extfvk that corresponds +/// // to a given ZcashAddress. +/// impl Converter> for KeyFinder { +/// type Error = &'static str; +/// +/// fn convert_sapling( +/// &self, +/// net: NetworkType, +/// data: [u8; 43], +/// ) -> Result, ConversionError> { +/// Ok(self.find_sapling_extfvk(data)) +/// } +/// } +/// ``` +pub trait Converter { + /// Conversion errors for the user type (e.g. failing to parse the data passed to + /// [`Self::convert_sapling`] as a valid Sapling address). + type Error; + + fn convert_sprout( + &self, + net: NetworkType, + data: [u8; 64], + ) -> Result> { + let _ = (net, data); + Err(ConversionError::Unsupported(UnsupportedAddress("Sprout"))) + } + + fn convert_sapling( + &self, + net: NetworkType, + data: [u8; 43], + ) -> Result> { + let _ = (net, data); + Err(ConversionError::Unsupported(UnsupportedAddress("Sapling"))) + } + + fn convert_unified( + &self, + net: NetworkType, + data: unified::Address, + ) -> Result> { + let _ = (net, data); + Err(ConversionError::Unsupported(UnsupportedAddress("Unified"))) + } + + fn convert_transparent_p2pkh( + &self, + net: NetworkType, + data: [u8; 20], + ) -> Result> { + let _ = (net, data); + Err(ConversionError::Unsupported(UnsupportedAddress( + "transparent P2PKH", + ))) + } + + fn convert_transparent_p2sh( + &self, + net: NetworkType, + data: [u8; 20], + ) -> Result> { + let _ = (net, data); + Err(ConversionError::Unsupported(UnsupportedAddress( + "transparent P2SH", + ))) + } + + fn convert_tex( + &self, + net: NetworkType, + data: [u8; 20], + ) -> Result> { + let _ = (net, data); + Err(ConversionError::Unsupported(UnsupportedAddress( + "transparent-source restricted P2PKH", + ))) + } } /// A helper trait for converting another type into a [`ZcashAddress`]. @@ -283,42 +415,45 @@ impl TryFromAddress for (Network, T) { /// # Examples /// /// ``` -/// use zcash_address::{ToAddress, Network, ZcashAddress}; +/// 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: Network) -> ZcashAddress { +/// fn encode(&self, net: NetworkType) -> ZcashAddress { /// ZcashAddress::from_sapling(net, self.0) /// } /// } /// /// let addr = MySapling([0; 43]); -/// let encoded = addr.encode(Network::Main); +/// let encoded = addr.encode(NetworkType::Main); /// assert_eq!( /// encoded.to_string(), /// "zs1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpq6d8g", /// ); /// ``` pub trait ToAddress: private::Sealed { - fn from_sprout(net: Network, data: sprout::Data) -> Self; + fn from_sprout(net: NetworkType, data: [u8; 64]) -> Self; - fn from_sapling(net: Network, data: sapling::Data) -> Self; + fn from_sapling(net: NetworkType, data: [u8; 43]) -> Self; - fn from_unified(net: Network, data: unified::Address) -> Self; + fn from_unified(net: NetworkType, data: unified::Address) -> Self; - fn from_transparent_p2pkh(net: Network, data: p2pkh::Data) -> Self; + fn from_transparent_p2pkh(net: NetworkType, data: [u8; 20]) -> Self; - fn from_transparent_p2sh(net: Network, data: p2sh::Data) -> 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: Network, data: sprout::Data) -> Self { + fn from_sprout(net: NetworkType, data: [u8; 64]) -> Self { ZcashAddress { - net: if let Network::Regtest = net { - Network::Test + net: if let NetworkType::Regtest = net { + NetworkType::Test } else { net }, @@ -326,24 +461,24 @@ impl ToAddress for ZcashAddress { } } - fn from_sapling(net: Network, data: sapling::Data) -> Self { + fn from_sapling(net: NetworkType, data: [u8; 43]) -> Self { ZcashAddress { net, kind: AddressKind::Sapling(data), } } - fn from_unified(net: Network, data: unified::Address) -> Self { + fn from_unified(net: NetworkType, data: unified::Address) -> Self { ZcashAddress { net, kind: AddressKind::Unified(data), } } - fn from_transparent_p2pkh(net: Network, data: p2pkh::Data) -> Self { + fn from_transparent_p2pkh(net: NetworkType, data: [u8; 20]) -> Self { ZcashAddress { - net: if let Network::Regtest = net { - Network::Test + net: if let NetworkType::Regtest = net { + NetworkType::Test } else { net }, @@ -351,16 +486,23 @@ impl ToAddress for ZcashAddress { } } - fn from_transparent_p2sh(net: Network, data: p2sh::Data) -> Self { + fn from_transparent_p2sh(net: NetworkType, data: [u8; 20]) -> Self { ZcashAddress { - net: if let Network::Regtest = net { - Network::Test + 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 { diff --git a/components/zcash_address/src/encoding.rs b/components/zcash_address/src/encoding.rs index 9e5e422ce6..b514c21e32 100644 --- a/components/zcash_address/src/encoding.rs +++ b/components/zcash_address/src/encoding.rs @@ -1,9 +1,18 @@ -use std::{convert::TryInto, error::Error, fmt, str::FromStr}; +use alloc::string::String; +use alloc::vec::Vec; +use core::convert::TryInto; +use core::fmt; +use core::str::FromStr; -use bech32::{self, FromBase32, ToBase32, Variant}; +#[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, Network, ZcashAddress}; +use crate::{kind::*, AddressKind, ZcashAddress}; /// An error while attempting to parse a string as a Zcash address. #[derive(Debug, PartialEq, Eq)] @@ -36,6 +45,7 @@ impl fmt::Display for ParseError { } } +#[cfg(feature = "std")] impl Error for ParseError {} impl FromStr for ZcashAddress { @@ -54,8 +64,8 @@ impl FromStr for ZcashAddress { kind: AddressKind::Unified(data), }); } - Err(unified::ParseError::NotUnified) => { - // allow decoding to fall through to Sapling/Transparent + Err(unified::ParseError::NotUnified | unified::ParseError::UnknownPrefix(_)) => { + // allow decoding to fall through to Sapling/TEX/Transparent } Err(e) => { return Err(ParseError::from(e)); @@ -63,46 +73,78 @@ impl FromStr for ZcashAddress { } // Try decoding as a Sapling address (Bech32) - if let Ok((hrp, data, Variant::Bech32)) = bech32::decode(s) { - // If we reached this point, the encoding is supposed to be valid Bech32. - let data = Vec::::from_base32(&data).map_err(|_| ParseError::InvalidEncoding)?; - - let net = match hrp.as_str() { - sapling::MAINNET => Network::Main, - sapling::TESTNET => Network::Test, - sapling::REGTEST => Network::Regtest, + 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); } }; - return data[..] + 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() { - let net = match decoded[..2].try_into().unwrap() { - sprout::MAINNET | p2pkh::MAINNET | p2sh::MAINNET => Network::Main, - sprout::TESTNET | p2pkh::TESTNET | p2sh::TESTNET => Network::Test, - // We will not define new Base58Check address encodings. - _ => return Err(ParseError::NotZcash), - }; + 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 decoded[..2].try_into().unwrap() { - sprout::MAINNET | sprout::TESTNET => { - decoded[2..].try_into().map(AddressKind::Sprout) + 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!(), } - p2pkh::MAINNET | p2pkh::TESTNET => decoded[2..].try_into().map(AddressKind::P2pkh), - p2sh::MAINNET | p2sh::TESTNET => decoded[2..].try_into().map(AddressKind::P2sh), - _ => unreachable!(), + .map_err(|_| ParseError::InvalidEncoding) + .map(|kind| ZcashAddress { kind, net }); } - .map_err(|_| ParseError::InvalidEncoding) - .map(|kind| ZcashAddress { kind, net }); }; // If it's not valid Bech32, Bech32m, or Base58Check, it's not a Zcash address. @@ -110,8 +152,8 @@ impl FromStr for ZcashAddress { } } -fn encode_bech32(hrp: &str, data: &[u8]) -> String { - bech32::encode(hrp, data.to_base32(), Variant::Bech32).expect("hrp is invalid") +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 { @@ -124,36 +166,14 @@ fn encode_b58(prefix: [u8; 2], data: &[u8]) -> 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( - match self.net { - Network::Main => sprout::MAINNET, - Network::Test | Network::Regtest => sprout::TESTNET, - }, - data, - ), - AddressKind::Sapling(data) => encode_bech32( - match self.net { - Network::Main => sapling::MAINNET, - Network::Test => sapling::TESTNET, - Network::Regtest => sapling::REGTEST, - }, - data, - ), + 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( - match self.net { - Network::Main => p2pkh::MAINNET, - Network::Test | Network::Regtest => p2pkh::TESTNET, - }, - data, - ), - AddressKind::P2sh(data) => encode_b58( - match self.net { - Network::Main => p2sh::MAINNET, - Network::Test | Network::Regtest => p2sh::TESTNET, - }, - data, - ), + 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) } @@ -161,8 +181,13 @@ impl fmt::Display for ZcashAddress { #[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); @@ -173,11 +198,11 @@ mod tests { fn sprout() { encoding( "zc8E5gYid86n4bo2Usdq1cpr7PpfoJGzttwBHEEgGhGkLUg7SPPVFNB2AkRFXZ7usfphup5426dt1buMmY3fkYeRrQGLa8y", - ZcashAddress { net: Network::Main, kind: AddressKind::Sprout([0; 64]) }, + ZcashAddress { net: NetworkType::Main, kind: AddressKind::Sprout([0; 64]) }, ); encoding( "ztJ1EWLKcGwF2S4NA17pAJVdco8Sdkz4AQPxt1cLTEfNuyNswJJc2BbBqYrsRZsp31xbVZwhF7c7a2L9jsF3p3ZwRWpqqyS", - ZcashAddress { net: Network::Test, kind: AddressKind::Sprout([0; 64]) }, + ZcashAddress { net: NetworkType::Test, kind: AddressKind::Sprout([0; 64]) }, ); } @@ -186,21 +211,21 @@ mod tests { encoding( "zs1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpq6d8g", ZcashAddress { - net: Network::Main, + net: NetworkType::Main, kind: AddressKind::Sapling([0; 43]), }, ); encoding( "ztestsapling1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqfhgwqu", ZcashAddress { - net: Network::Test, + net: NetworkType::Test, kind: AddressKind::Sapling([0; 43]), }, ); encoding( "zregtestsapling1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqknpr3m", ZcashAddress { - net: Network::Regtest, + net: NetworkType::Regtest, kind: AddressKind::Sapling([0; 43]), }, ); @@ -211,21 +236,21 @@ mod tests { encoding( "u1qpatys4zruk99pg59gcscrt7y6akvl9vrhcfyhm9yxvxz7h87q6n8cgrzzpe9zru68uq39uhmlpp5uefxu0su5uqyqfe5zp3tycn0ecl", ZcashAddress { - net: Network::Main, + net: NetworkType::Main, kind: AddressKind::Unified(unified::Address(vec![unified::address::Receiver::Sapling([0; 43])])), }, ); encoding( "utest10c5kutapazdnf8ztl3pu43nkfsjx89fy3uuff8tsmxm6s86j37pe7uz94z5jhkl49pqe8yz75rlsaygexk6jpaxwx0esjr8wm5ut7d5s", ZcashAddress { - net: Network::Test, + net: NetworkType::Test, kind: AddressKind::Unified(unified::Address(vec![unified::address::Receiver::Sapling([0; 43])])), }, ); encoding( "uregtest15xk7vj4grjkay6mnfl93dhsflc2yeunhxwdh38rul0rq3dfhzzxgm5szjuvtqdha4t4p2q02ks0jgzrhjkrav70z9xlvq0plpcjkd5z3", ZcashAddress { - net: Network::Regtest, + net: NetworkType::Regtest, kind: AddressKind::Unified(unified::Address(vec![unified::address::Receiver::Sapling([0; 43])])), }, ); @@ -242,46 +267,114 @@ mod tests { encoding( "t1Hsc1LR8yKnbbe3twRp88p6vFfC5t7DLbs", ZcashAddress { - net: Network::Main, + net: NetworkType::Main, kind: AddressKind::P2pkh([0; 20]), }, ); encoding( "tm9iMLAuYMzJ6jtFLcA7rzUmfreGuKvr7Ma", ZcashAddress { - net: Network::Test, + net: NetworkType::Test, kind: AddressKind::P2pkh([0; 20]), }, ); encoding( "t3JZcvsuaXE6ygokL4XUiZSTrQBUoPYFnXJ", ZcashAddress { - net: Network::Main, + net: NetworkType::Main, kind: AddressKind::P2sh([0; 20]), }, ); encoding( "t26YoyZ1iPgiMEWL4zGUm74eVWfhyDMXzY2", ZcashAddress { - net: Network::Test, + 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: Network::Main, + net: NetworkType::Main, kind: AddressKind::P2pkh([0; 20]) }), ); assert_eq!( "t1Hsc1LR8yKnbbe3twRp88p6vFfC5t7DLbs ".parse(), Ok(ZcashAddress { - net: Network::Main, + net: NetworkType::Main, kind: AddressKind::P2pkh([0; 20]) }), ); diff --git a/components/zcash_address/src/kind.rs b/components/zcash_address/src/kind.rs index 5397c027f8..38b4557a6e 100644 --- a/components/zcash_address/src/kind.rs +++ b/components/zcash_address/src/kind.rs @@ -1,7 +1 @@ pub mod unified; - -pub(crate) mod sapling; -pub(crate) mod sprout; - -pub(crate) mod p2pkh; -pub(crate) mod p2sh; diff --git a/components/zcash_address/src/kind/p2pkh.rs b/components/zcash_address/src/kind/p2pkh.rs deleted file mode 100644 index 0120e2c39f..0000000000 --- a/components/zcash_address/src/kind/p2pkh.rs +++ /dev/null @@ -1,7 +0,0 @@ -/// The prefix for a Base58Check-encoded mainnet transparent P2PKH address. -pub(crate) const MAINNET: [u8; 2] = [0x1c, 0xb8]; - -/// The prefix for a Base58Check-encoded testnet transparent P2PKH address. -pub(crate) const TESTNET: [u8; 2] = [0x1d, 0x25]; - -pub(crate) type Data = [u8; 20]; diff --git a/components/zcash_address/src/kind/p2sh.rs b/components/zcash_address/src/kind/p2sh.rs deleted file mode 100644 index 5059513182..0000000000 --- a/components/zcash_address/src/kind/p2sh.rs +++ /dev/null @@ -1,7 +0,0 @@ -/// The prefix for a Base58Check-encoded mainnet transparent P2SH address. -pub(crate) const MAINNET: [u8; 2] = [0x1c, 0xbd]; - -/// The prefix for a Base58Check-encoded testnet transparent P2SH address. -pub(crate) const TESTNET: [u8; 2] = [0x1c, 0xba]; - -pub(crate) type Data = [u8; 20]; diff --git a/components/zcash_address/src/kind/sapling.rs b/components/zcash_address/src/kind/sapling.rs deleted file mode 100644 index 2cbf914d61..0000000000 --- a/components/zcash_address/src/kind/sapling.rs +++ /dev/null @@ -1,22 +0,0 @@ -/// The HRP for a Bech32-encoded mainnet Sapling address. -/// -/// Defined in the [Zcash Protocol Specification section 5.6.4][saplingpaymentaddrencoding]. -/// -/// [saplingpaymentaddrencoding]: https://zips.z.cash/protocol/protocol.pdf#saplingpaymentaddrencoding -pub(crate) const MAINNET: &str = "zs"; - -/// The HRP for a Bech32-encoded testnet Sapling address. -/// -/// Defined in the [Zcash Protocol Specification section 5.6.4][saplingpaymentaddrencoding]. -/// -/// [saplingpaymentaddrencoding]: https://zips.z.cash/protocol/protocol.pdf#saplingpaymentaddrencoding -pub(crate) const TESTNET: &str = "ztestsapling"; - -/// The HRP for a Bech32-encoded regtest Sapling address. -/// -/// It is defined in [the `zcashd` codebase]. -/// -/// [the `zcashd` codebase]: https://github.com/zcash/zcash/blob/128d863fb8be39ee294fda397c1ce3ba3b889cb2/src/chainparams.cpp#L493 -pub(crate) const REGTEST: &str = "zregtestsapling"; - -pub(crate) type Data = [u8; 43]; diff --git a/components/zcash_address/src/kind/sprout.rs b/components/zcash_address/src/kind/sprout.rs deleted file mode 100644 index fae74aab28..0000000000 --- a/components/zcash_address/src/kind/sprout.rs +++ /dev/null @@ -1,15 +0,0 @@ -/// 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(crate) const MAINNET: [u8; 2] = [0x16, 0x9a]; - -/// 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(crate) const TESTNET: [u8; 2] = [0x16, 0xb6]; - -pub(crate) type Data = [u8; 64]; diff --git a/components/zcash_address/src/kind/unified.rs b/components/zcash_address/src/kind/unified.rs index 98bfa54130..79861fe801 100644 --- a/components/zcash_address/src/kind/unified.rs +++ b/components/zcash_address/src/kind/unified.rs @@ -1,11 +1,18 @@ -use bech32::{self, FromBase32, ToBase32, Variant}; -use std::cmp; -use std::convert::{TryFrom, TryInto}; +//! 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 std::fmt; -use std::num::TryFromIntError; -use crate::Network; +use bech32::{primitives::decode::CheckedHrpstring, Bech32m, Checksum, Hrp}; + +use zcash_protocol::consensus::NetworkType; pub(crate) mod address; pub(crate) mod fvk; @@ -17,12 +24,23 @@ 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), } @@ -142,17 +160,19 @@ impl fmt::Display for ParseError { } } +#[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 crate::Network; - use std::{ - cmp, - convert::{TryFrom, TryInto}, - io::Write, - }; 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 { @@ -172,10 +192,21 @@ pub(crate) mod private { res => res, } } + + fn write_raw_encoding(&self, mut writer: W) { + let data = self.data(); + CompactSize::write( + &mut writer, + ::from(self.typecode()).try_into().unwrap(), + ) + .unwrap(); + CompactSize::write(&mut writer, data.len()).unwrap(); + writer.write_all(data).unwrap(); + } } /// A Unified Container containing addresses or viewing keys. - pub trait SealedContainer: super::Container + std::marker::Sized { + pub trait SealedContainer: super::Container + core::marker::Sized { const MAINNET: &'static str; const TESTNET: &'static str; const REGTEST: &'static str; @@ -185,21 +216,21 @@ pub(crate) mod private { /// general invariants that apply to all unified containers. fn from_inner(items: Vec) -> Self; - fn network_hrp(network: &Network) -> &'static str { + fn network_hrp(network: &NetworkType) -> &'static str { match network { - Network::Main => Self::MAINNET, - Network::Test => Self::TESTNET, - Network::Regtest => Self::REGTEST, + NetworkType::Main => Self::MAINNET, + NetworkType::Test => Self::TESTNET, + NetworkType::Regtest => Self::REGTEST, } } - fn hrp_network(hrp: &str) -> Option { + fn hrp_network(hrp: &str) -> Option { if hrp == Self::MAINNET { - Some(Network::Main) + Some(NetworkType::Main) } else if hrp == Self::TESTNET { - Some(Network::Test) + Some(NetworkType::Test) } else if hrp == Self::REGTEST { - Some(Network::Regtest) + Some(NetworkType::Regtest) } else { None } @@ -207,14 +238,7 @@ pub(crate) mod private { 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(); + item.write_raw_encoding(&mut writer); } } @@ -222,14 +246,13 @@ pub(crate) mod private { fn to_jumbled_bytes(&self, hrp: &str) -> Vec { assert!(hrp.len() <= PADDING_LEN); - let mut writer = std::io::Cursor::new(Vec::new()); - self.write_raw_encoding(&mut writer); + 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()); - writer.write_all(&padding).unwrap(); + padded.write_all(&padding).unwrap(); - let padded = writer.into_inner(); f4jumble::f4jumble(&padded) .unwrap_or_else(|e| panic!("f4jumble failed on {:?}: {}", padded, e)) } @@ -237,7 +260,7 @@ pub(crate) mod private { /// Parse the items of the unified container. fn parse_items>>(hrp: &str, buf: T) -> Result, ParseError> { fn read_receiver( - mut cursor: &mut std::io::Cursor<&[u8]>, + 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")) @@ -295,7 +318,7 @@ pub(crate) mod private { )), }?; - let mut cursor = std::io::Cursor::new(encoded); + 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)?); @@ -345,6 +368,22 @@ pub(crate) mod private { 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 @@ -365,15 +404,15 @@ pub trait Encoding: private::SealedContainer { /// 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<(Network, Self), ParseError> { - if let Ok((hrp, data, Variant::Bech32m)) = bech32::decode(s) { + 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 = Vec::::from_base32(&data) - .map_err(|e| ParseError::InvalidEncoding(e.to_string()))?; + let data = parsed.byte_iter().collect::>(); Self::parse_internal(hrp, data).map(|value| (net, value)) } else { @@ -385,21 +424,17 @@ pub trait Encoding: private::SealedContainer { /// 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: &Network) -> String { + fn encode(&self, network: &NetworkType) -> String { let hrp = Self::network_hrp(network); - bech32::encode( - hrp, - self.to_jumbled_bytes(hrp).to_base32(), - Variant::Bech32m, - ) - .expect("hrp is invalid") + bech32::encode::(Hrp::parse_unchecked(hrp), &self.to_jumbled_bytes(hrp)) + .expect("F4Jumble ensures length is short enough by construction") } } -/// Trait for for Unified containers, that exposes the items within them. +/// Trait for Unified containers, that exposes the items within them. pub trait Container { /// The type of item in this unified container. - type Item: SealedItem; + type Item: Item; /// Returns the items contained within this container, sorted in preference order. fn items(&self) -> Vec { @@ -415,3 +450,19 @@ pub trait Container { /// This API is for advanced usage; in most cases you should use `Self::items`. fn items_as_parsed(&self) -> &[Self::Item]; } + +/// Trait for unified items, exposing specific methods on them. +pub trait Item: SealedItem { + /// Returns the opaque typed encoding of this item. + /// + /// This is the same encoding used internally by [`Encoding::encode`]. + /// This API is for advanced usage; in most cases you should not depend + /// on the typed encoding of items. + fn typed_encoding(&self) -> Vec { + let mut ret = vec![]; + self.write_raw_encoding(&mut ret); + ret + } +} + +impl Item for T {} diff --git a/components/zcash_address/src/kind/unified/address.rs b/components/zcash_address/src/kind/unified/address.rs index addba7d186..41be7ab992 100644 --- a/components/zcash_address/src/kind/unified/address.rs +++ b/components/zcash_address/src/kind/unified/address.rs @@ -1,15 +1,17 @@ +use zcash_protocol::{constants, PoolType}; + use super::{private::SealedItem, ParseError, Typecode}; -use crate::kind; -use std::convert::{TryFrom, TryInto}; +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(kind::sapling::Data), - P2pkh(kind::p2pkh::Data), - P2sh(kind::p2sh::Data), + Sapling([u8; 43]), + P2pkh([u8; 20]), + P2sh([u8; 20]), Unknown { typecode: u32, data: Vec }, } @@ -56,26 +58,95 @@ impl SealedItem for Receiver { } /// 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 = "u"; + 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 = "utest"; + const TESTNET: &'static str = constants::testnet::HRP_UNIFIED_ADDRESS; /// The HRP for a Bech32m-encoded regtest Unified Address. - const REGTEST: &'static str = "uregtest"; + const REGTEST: &'static str = constants::regtest::HRP_UNIFIED_ADDRESS; fn from_inner(receivers: Vec) -> Self { Self(receivers) @@ -92,26 +163,20 @@ impl super::Container for Address { } #[cfg(any(test, feature = "test-dependencies"))] -pub mod test_vectors; - -#[cfg(test)] -mod tests { - use assert_matches::assert_matches; - use zcash_encoding::MAX_COMPACT_SIZE; - - use crate::{ - kind::unified::{private::SealedContainer, Container, Encoding}, - Network, - }; +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, ParseError, Receiver, Typecode}; + use super::{Address, Receiver}; + use crate::unified::Typecode; prop_compose! { fn uniform43()(a in uniform11(0u8..), b in uniform32(0u8..)) -> [u8; 43] { @@ -122,11 +187,13 @@ mod tests { } } - fn arb_transparent_typecode() -> impl Strategy { + /// A strategy to generate an arbitrary transparent typecode. + pub fn arb_transparent_typecode() -> impl Strategy { select(vec![Typecode::P2pkh, Typecode::P2sh]) } - fn arb_shielded_typecode() -> impl Strategy { + /// 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), @@ -137,7 +204,7 @@ mod tests { /// 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. - fn arb_typecodes() -> impl Strategy> { + 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(); @@ -147,7 +214,11 @@ mod tests { }) } - fn arb_unified_address_for_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 @@ -164,16 +235,40 @@ mod tests { .collect::>() } - fn arb_unified_address() -> impl Strategy { + /// 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![Network::Main, Network::Test, Network::Regtest]), + network in select(vec![NetworkType::Main, NetworkType::Test, NetworkType::Regtest]), ua in arb_unified_address(), ) { let encoded = ua.encode(&network); @@ -296,7 +391,7 @@ mod tests { #[test] fn only_transparent() { // Encoding of `Address(vec![Receiver::P2pkh([0; 20])])`. - let encoded = vec![ + 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, diff --git a/components/zcash_address/src/kind/unified/address/test_vectors.rs b/components/zcash_address/src/kind/unified/address/test_vectors.rs index 973bc09dcf..62bd610380 100644 --- a/components/zcash_address/src/kind/unified/address/test_vectors.rs +++ b/components/zcash_address/src/kind/unified/address/test_vectors.rs @@ -11,7 +11,7 @@ pub struct TestVector { pub diversifier_index: u32, } -// From https://github.com/zcash-hackworks/zcash-test-vectors/blob/master/unified_address.py +// From https://github.com/zcash/zcash-test-vectors/blob/master/zcash_test_vectors/unified_address.py #[cfg(any(test, feature = "test-dependencies"))] pub const TEST_VECTORS: &[TestVector] = &[ TestVector { diff --git a/components/zcash_address/src/kind/unified/fvk.rs b/components/zcash_address/src/kind/unified/fvk.rs index 2afc80de66..534d6c7834 100644 --- a/components/zcash_address/src/kind/unified/fvk.rs +++ b/components/zcash_address/src/kind/unified/fvk.rs @@ -1,4 +1,6 @@ -use std::convert::{TryFrom, TryInto}; +use alloc::vec::Vec; +use core::convert::{TryFrom, TryInto}; +use zcash_protocol::constants; use super::{ private::{SealedContainer, SealedItem}, @@ -78,6 +80,32 @@ impl SealedItem for Fvk { } /// 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); @@ -101,17 +129,21 @@ impl SealedContainer for Ufvk { /// Defined in [ZIP 316][zip-0316]. /// /// [zip-0316]: https://zips.z.cash/zip-0316 - const MAINNET: &'static str = "uview"; + 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 = "uviewtest"; + const TESTNET: &'static str = constants::testnet::HRP_UNIFIED_FVK; /// The HRP for a Bech32m-encoded regtest Unified FVK. - const REGTEST: &'static str = "uviewregtest"; + /// + /// 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) @@ -120,18 +152,19 @@ impl SealedContainer for Ufvk { #[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, - }, - Network, + 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] { @@ -196,7 +229,7 @@ mod tests { proptest! { #[test] fn ufvk_roundtrip( - network in select(vec![Network::Main, Network::Test, Network::Regtest]), + network in select(vec![NetworkType::Main, NetworkType::Test, NetworkType::Regtest]), ufvk in arb_unified_fvk(), ) { let encoded = ufvk.encode(&network); @@ -210,7 +243,7 @@ mod tests { // 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 = vec![ + 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, @@ -228,7 +261,7 @@ mod tests { ); // Short padding (padded to 15 bytes instead of 16) - let truncated_padding = vec![ + 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, @@ -276,7 +309,7 @@ mod tests { ); // - Truncated after the typecode of the Sapling fvk. - let truncated_after_sapling_typecode = vec![ + 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, @@ -306,7 +339,7 @@ mod tests { #[test] fn only_transparent() { // Raw encoding of `Ufvk(vec![Fvk::P2pkh([0; 65])])`. - let encoded = vec![ + 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, diff --git a/components/zcash_address/src/kind/unified/ivk.rs b/components/zcash_address/src/kind/unified/ivk.rs index 31c2ad56f0..7b776b0008 100644 --- a/components/zcash_address/src/kind/unified/ivk.rs +++ b/components/zcash_address/src/kind/unified/ivk.rs @@ -1,4 +1,6 @@ -use std::convert::{TryFrom, TryInto}; +use alloc::vec::Vec; +use core::convert::{TryFrom, TryInto}; +use zcash_protocol::constants; use super::{ private::{SealedContainer, SealedItem}, @@ -83,6 +85,32 @@ impl SealedItem for Ivk { } /// 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); @@ -106,17 +134,17 @@ impl SealedContainer for Uivk { /// Defined in [ZIP 316][zip-0316]. /// /// [zip-0316]: https://zips.z.cash/zip-0316 - const MAINNET: &'static str = "uivk"; + 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 = "uivktest"; + const TESTNET: &'static str = constants::testnet::HRP_UNIFIED_IVK; /// The HRP for a Bech32m-encoded regtest Unified IVK. - const REGTEST: &'static str = "uivkregtest"; + const REGTEST: &'static str = constants::regtest::HRP_UNIFIED_IVK; fn from_inner(ivks: Vec) -> Self { Self(ivks) @@ -125,6 +153,9 @@ impl SealedContainer for Uivk { #[cfg(test)] mod tests { + use alloc::borrow::ToOwned; + use alloc::vec::Vec; + use assert_matches::assert_matches; use proptest::{ @@ -134,13 +165,11 @@ mod tests { }; use super::{Ivk, ParseError, Typecode, Uivk}; - use crate::{ - kind::unified::{ - private::{SealedContainer, SealedItem}, - Container, Encoding, - }, - Network, + 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] { @@ -189,7 +218,7 @@ mod tests { proptest! { #[test] fn uivk_roundtrip( - network in select(vec![Network::Main, Network::Test, Network::Regtest]), + network in select(vec![NetworkType::Main, NetworkType::Test, NetworkType::Regtest]), uivk in arb_unified_ivk(), ) { let encoded = uivk.encode(&network); @@ -203,7 +232,7 @@ mod tests { // 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 = vec![ + 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, @@ -219,7 +248,7 @@ mod tests { ); // Short padding (padded to 15 bytes instead of 16) - let truncated_padding = vec![ + 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, @@ -242,7 +271,7 @@ mod tests { // with the ivk data truncated, but valid padding. // - Missing the last data byte of the Sapling ivk. - let truncated_sapling_data = vec![ + 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, @@ -261,7 +290,7 @@ mod tests { ); // - Truncated after the typecode of the Sapling ivk. - let truncated_after_sapling_typecode = vec![ + 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, @@ -278,7 +307,7 @@ mod tests { 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(&Network::Main); + let encoded = uivk.encode(&NetworkType::Main); assert_eq!( Uivk::decode(&encoded), Err(ParseError::DuplicateTypecode(Typecode::Sapling)) @@ -288,7 +317,7 @@ mod tests { #[test] fn only_transparent() { // Raw Encoding of `Uivk(vec![Ivk::P2pkh([0; 65])])`. - let encoded = vec![ + 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, diff --git a/components/zcash_address/src/lib.rs b/components/zcash_address/src/lib.rs index c0f7fbc62c..555da0f0f6 100644 --- a/components/zcash_address/src/lib.rs +++ b/components/zcash_address/src/lib.rs @@ -1,3 +1,144 @@ +//! *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; @@ -6,52 +147,45 @@ mod kind; pub mod test_vectors; pub use convert::{ - ConversionError, ToAddress, TryFromAddress, TryFromRawAddress, UnsupportedAddress, + ConversionError, Converter, 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: Network, + net: NetworkType, kind: AddressKind, } -/// The Zcash network for which an address is encoded. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum Network { - /// 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, -} - /// Known kinds of Zcash addresses. #[derive(Clone, Debug, PartialEq, Eq, Hash)] enum AddressKind { - Sprout(kind::sprout::Data), - Sapling(kind::sapling::Data), + Sprout([u8; 64]), + Sapling([u8; 43]), Unified(unified::Address), - P2pkh(kind::p2pkh::Data), - P2sh(kind::p2sh::Data), + 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 + /// [Zcash protocol specification](https://zips.z.cash/protocol/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 + /// [`Display` implementation]: core::fmt::Display + /// [`address.to_string()`]: alloc::string::ToString pub fn encode(&self) -> String { format!("{}", self) } @@ -60,7 +194,7 @@ impl ZcashAddress { /// /// This simply calls [`s.parse()`], leveraging the [`FromStr` implementation]. /// - /// [`s.parse()`]: std::primitive::str::parse + /// [`s.parse()`]: str::parse /// [`FromStr` implementation]: ZcashAddress#impl-FromStr /// /// # Errors @@ -95,8 +229,8 @@ impl ZcashAddress { /// method or the [`Display` implementation] via [`address.to_string()`] instead. /// /// [`encode`]: Self::encode - /// [`Display` implementation]: std::fmt::Display - /// [`address.to_string()`]: std::string::ToString + /// [`Display` implementation]: core::fmt::Display + /// [`address.to_string()`]: alloc::string::ToString pub fn convert(self) -> Result> { match self.kind { AddressKind::Sprout(data) => T::try_from_sprout(self.net, data), @@ -104,6 +238,7 @@ impl ZcashAddress { 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), } } @@ -119,17 +254,17 @@ impl ZcashAddress { /// method or the [`Display` implementation] via [`address.to_string()`] instead. /// /// [`encode`]: Self::encode - /// [`Display` implementation]: std::fmt::Display - /// [`address.to_string()`]: std::string::ToString + /// [`Display` implementation]: core::fmt::Display + /// [`address.to_string()`]: alloc::string::ToString pub fn convert_if_network( self, - net: Network, + 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 == Network::Test && net == Network::Regtest); + network_matches || (self.net == NetworkType::Test && net == NetworkType::Regtest); match self.kind { AddressKind::Sprout(data) if regtest_exception => T::try_from_raw_sprout(data), @@ -139,10 +274,142 @@ impl ZcashAddress { 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, }), } } + + /// Converts this address into another type using the specified converter. + /// + /// `convert` can convert into any type `T` for which an implementation of the [`Converter`] + /// trait exists. This enables conversion of [`ZcashAddress`] values into other types to rely + /// on additional context. + pub fn convert_with>( + self, + converter: C, + ) -> Result> { + match self.kind { + AddressKind::Sprout(data) => converter.convert_sprout(self.net, data), + AddressKind::Sapling(data) => converter.convert_sapling(self.net, data), + AddressKind::Unified(data) => converter.convert_unified(self.net, data), + AddressKind::P2pkh(data) => converter.convert_transparent_p2pkh(self.net, data), + AddressKind::P2sh(data) => converter.convert_transparent_p2sh(self.net, data), + AddressKind::Tex(data) => converter.convert_tex(self.net, data), + } + } + + /// 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 index 61cffcef90..a7278734eb 100644 --- a/components/zcash_address/src/test_vectors.rs +++ b/components/zcash_address/src/test_vectors.rs @@ -9,9 +9,11 @@ use { self, address::{test_vectors::TEST_VECTORS, Receiver}, }, - Network, ToAddress, ZcashAddress, + ToAddress, ZcashAddress, }, - std::iter, + alloc::string::ToString, + core::iter, + zcash_protocol::consensus::NetworkType, }; #[test] @@ -38,7 +40,8 @@ fn unified() { })) .collect(); - let expected_addr = ZcashAddress::from_unified(Network::Main, unified::Address(receivers)); + let expected_addr = + ZcashAddress::from_unified(NetworkType::Main, unified::Address(receivers)); // Test parsing let addr: ZcashAddress = tv.unified_addr.parse().unwrap(); diff --git a/components/zcash_encoding/CHANGELOG.md b/components/zcash_encoding/CHANGELOG.md index da71b50f7c..85367fb7f1 100644 --- a/components/zcash_encoding/CHANGELOG.md +++ b/components/zcash_encoding/CHANGELOG.md @@ -7,6 +7,19 @@ and this library adheres to Rust's notion of ## [Unreleased] +## [0.3.0] - 2025-02-21 +### 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 diff --git a/components/zcash_encoding/Cargo.toml b/components/zcash_encoding/Cargo.toml index b287a47656..caa88dfe13 100644 --- a/components/zcash_encoding/Cargo.toml +++ b/components/zcash_encoding/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zcash_encoding" description = "Binary encodings used throughout the Zcash ecosystem." -version = "0.2.0" +version = "0.3.0" authors = [ "Jack Grigg ", "Kris Nuttycombe ", @@ -16,8 +16,15 @@ categories = ["cryptography::cryptocurrencies", "encoding"] keywords = ["zcash"] [dependencies] -byteorder = "1" -nonempty = "0.7" +core2.workspace = true +nonempty.workspace = true + +[features] +default = ["std"] +std = ["core2/std"] [lib] bench = false + +[lints] +workspace = true diff --git a/components/zcash_encoding/src/lib.rs b/components/zcash_encoding/src/lib.rs index 8e23f7b68f..4e611e25a6 100644 --- a/components/zcash_encoding/src/lib.rs +++ b/components/zcash_encoding/src/lib.rs @@ -3,15 +3,21 @@ //! `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)] -use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; +#[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; -use std::io::{self, Read, Write}; -use std::iter::FromIterator; /// The maximum allowed value representable as a `[CompactSize]` pub const MAX_COMPACT_SIZE: u32 = 0x02000000; @@ -25,11 +31,16 @@ pub struct CompactSize; impl CompactSize { /// Reads an integer encoded in compact form. pub fn read(mut reader: R) -> io::Result { - let flag = reader.read_u8()?; + 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 { - match reader.read_u16::()? { + 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", @@ -37,7 +48,9 @@ impl CompactSize { n => Ok(n as u64), } } else if flag == 254 { - match reader.read_u32::()? { + 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", @@ -45,7 +58,9 @@ impl CompactSize { n => Ok(n as u64), } } else { - match reader.read_u64::()? { + 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", @@ -78,21 +93,31 @@ impl CompactSize { /// 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_u8(s as u8), + s if s < 253 => writer.write_all(&[s as u8]), s if s <= 0xFFFF => { - writer.write_u8(253)?; - writer.write_u16::(s as u16) + writer.write_all(&[253])?; + writer.write_all(&(s as u16).to_le_bytes()) } s if s <= 0xFFFFFFFF => { - writer.write_u8(254)?; - writer.write_u32::(s as u32) + writer.write_all(&[254])?; + writer.write_all(&(s as u32).to_le_bytes()) } s => { - writer.write_u8(255)?; - writer.write_u64::(s as u64) + 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. @@ -171,6 +196,12 @@ impl Vector { 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. @@ -240,7 +271,9 @@ impl Optional { where F: Fn(R) -> io::Result, { - match reader.read_u8()? { + 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( @@ -258,9 +291,9 @@ impl Optional { F: Fn(W, T) -> io::Result<()>, { match val { - None => writer.write_u8(0), + None => writer.write_all(&[0]), Some(e) => { - writer.write_u8(1)?; + writer.write_all(&[1])?; func(writer, e) } } @@ -270,7 +303,7 @@ impl Optional { #[cfg(test)] mod tests { use super::*; - use std::fmt::Debug; + use core::fmt::Debug; #[test] fn compact_size() { @@ -279,8 +312,11 @@ mod tests { >::Error: Debug, { let mut data = vec![]; - CompactSize::write(&mut data, value.try_into().unwrap()).unwrap(); + 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), @@ -308,6 +344,8 @@ mod tests { 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()); } } @@ -318,9 +356,14 @@ mod tests { macro_rules! eval { ($value:expr, $expected:expr) => { let mut data = vec![]; - Vector::write(&mut data, &$value, |w, e| w.write_u8(*e)).unwrap(); + Vector::write(&mut data, &$value, |w, e| w.write_all(&[*e])).unwrap(); assert_eq!(&data[..], &$expected[..]); - match Vector::read(&data[..], |r| r.read_u8()) { + 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), } @@ -359,7 +402,10 @@ mod tests { macro_rules! eval_u8 { ($value:expr, $expected:expr) => { - eval!($value, $expected, |w, e| w.write_u8(e), |mut r| r.read_u8()) + eval!($value, $expected, |w, e| w.write_all(&[e]), |mut r| { + let mut bytes = [0; 1]; + r.read_exact(&mut bytes).map(|_| bytes[0]) + }) }; } @@ -368,8 +414,11 @@ mod tests { eval!( $value, $expected, - |w, v| Vector::write(w, &v, |w, e| w.write_u8(*e)), - |r| Vector::read(r, |r| r.read_u8()) + |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]) + }) ) }; } diff --git a/components/zcash_note_encryption/CHANGELOG.md b/components/zcash_note_encryption/CHANGELOG.md deleted file mode 100644 index cedc180c69..0000000000 --- a/components/zcash_note_encryption/CHANGELOG.md +++ /dev/null @@ -1,50 +0,0 @@ -# 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.4.0] - 2023-06-06 -### Changed -- The `esk` and `ephemeral_key` arguments have been removed from - `Domain::parse_note_plaintext_without_memo_ovk`. It is therefore no longer - necessary (or possible) to ensure that `ephemeral_key` is derived from `esk` - and the diversifier within the note plaintext. We have analyzed the safety of - this change in the context of callers within `zcash_note_encryption` and - `orchard`. See https://github.com/zcash/librustzcash/pull/848 and the - associated issue https://github.com/zcash/librustzcash/issues/802 for - additional detail. - -## [0.3.0] - 2023-03-22 -### Changed -- The `recipient` parameter has been removed from `Domain::note_plaintext_bytes`. -- The `recipient` parameter has been removed from `NoteEncryption::new`. Since - the `Domain::Note` type is now expected to contain information about the - recipient of the note, there is no longer any need to pass this information - in via the encryption context. - -## [0.2.0] - 2022-10-13 -### Added -- `zcash_note_encryption::Domain`: - - `Domain::PreparedEphemeralPublicKey` associated type. - - `Domain::prepare_epk` method, which produces the above type. - -### Changed -- MSRV is now 1.56.1. -- `zcash_note_encryption::Domain` now requires `epk` to be converted to - `Domain::PreparedEphemeralPublicKey` before being passed to - `Domain::ka_agree_dec`. -- Changes to batch decryption APIs: - - The return types of `batch::try_note_decryption` and - `batch::try_compact_note_decryption` have changed. Now, instead of - returning entries corresponding to the cartesian product of the IVKs used for - decryption with the outputs being decrypted, this now returns a vector of - decryption results of the same length and in the same order as the `outputs` - argument to the function. Each successful result includes the index of the - entry in `ivks` used to decrypt the value. - -## [0.1.0] - 2021-12-17 -Initial release. diff --git a/components/zcash_note_encryption/Cargo.toml b/components/zcash_note_encryption/Cargo.toml deleted file mode 100644 index 34d359ef7f..0000000000 --- a/components/zcash_note_encryption/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "zcash_note_encryption" -description = "Note encryption for Zcash transactions" -version = "0.4.0" -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"] - -[package.metadata.docs.rs] -all-features = true -rustdoc-args = ["--cfg", "docsrs"] - -[dependencies] -cipher = { version = "0.4", default-features = false } -chacha20 = { version = "0.9", default-features = false } -chacha20poly1305 = { version = "0.10", default-features = false } -rand_core = { version = "0.6", default-features = false } -subtle = { version = "2.3", default-features = false } - -[features] -default = ["alloc"] -alloc = [] -pre-zip-212 = [] - -[lib] -bench = false diff --git a/components/zcash_note_encryption/README.md b/components/zcash_note_encryption/README.md deleted file mode 100644 index 612b7a64fb..0000000000 --- a/components/zcash_note_encryption/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# zcash_note_encryption - -This crate implements the [in-band secret distribution scheme] for the Sapling and -Orchard protocols. It provides reusable methods that implement common note encryption -and trial decryption logic, and enforce protocol-agnostic verification requirements. - -Protocol-specific logic is handled via the `Domain` trait. Implementations of this -trait are provided in the [`zcash_primitives`] (for Sapling) and [`orchard`] crates; -users with their own existing types can similarly implement the trait themselves. - -[in-band secret distribution scheme]: https://zips.z.cash/protocol/protocol.pdf#saplingandorchardinband -[`zcash_primitives`]: https://crates.io/crates/zcash_primitives -[`orchard`]: https://crates.io/crates/orchard - -## 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_note_encryption/src/batch.rs b/components/zcash_note_encryption/src/batch.rs deleted file mode 100644 index ad704167c2..0000000000 --- a/components/zcash_note_encryption/src/batch.rs +++ /dev/null @@ -1,86 +0,0 @@ -//! APIs for batch trial decryption. - -use alloc::vec::Vec; // module is alloc only - -use crate::{ - try_compact_note_decryption_inner, try_note_decryption_inner, BatchDomain, EphemeralKeyBytes, - ShieldedOutput, COMPACT_NOTE_SIZE, ENC_CIPHERTEXT_SIZE, -}; - -/// Trial decryption of a batch of notes with a set of recipients. -/// -/// This is the batched version of [`crate::try_note_decryption`]. -/// -/// Returns a vector containing the decrypted result for each output, -/// with the same length and in the same order as the outputs were -/// provided, along with the index in the `ivks` slice associated with -/// the IVK that successfully decrypted the output. -#[allow(clippy::type_complexity)] -pub fn try_note_decryption>( - ivks: &[D::IncomingViewingKey], - outputs: &[(D, Output)], -) -> Vec> { - batch_note_decryption(ivks, outputs, try_note_decryption_inner) -} - -/// Trial decryption of a batch of notes for light clients with a set of recipients. -/// -/// This is the batched version of [`crate::try_compact_note_decryption`]. -/// -/// Returns a vector containing the decrypted result for each output, -/// with the same length and in the same order as the outputs were -/// provided, along with the index in the `ivks` slice associated with -/// the IVK that successfully decrypted the output. -#[allow(clippy::type_complexity)] -pub fn try_compact_note_decryption>( - ivks: &[D::IncomingViewingKey], - outputs: &[(D, Output)], -) -> Vec> { - batch_note_decryption(ivks, outputs, try_compact_note_decryption_inner) -} - -fn batch_note_decryption, F, FR, const CS: usize>( - ivks: &[D::IncomingViewingKey], - outputs: &[(D, Output)], - decrypt_inner: F, -) -> Vec> -where - F: Fn(&D, &D::IncomingViewingKey, &EphemeralKeyBytes, &Output, &D::SymmetricKey) -> Option, -{ - if ivks.is_empty() { - return (0..outputs.len()).map(|_| None).collect(); - }; - - // Fetch the ephemeral keys for each output, and batch-parse and prepare them. - let ephemeral_keys = D::batch_epk(outputs.iter().map(|(_, output)| output.ephemeral_key())); - - // Derive the shared secrets for all combinations of (ivk, output). - // The scalar multiplications cannot benefit from batching. - let items = ephemeral_keys.iter().flat_map(|(epk, ephemeral_key)| { - ivks.iter().map(move |ivk| { - ( - epk.as_ref().map(|epk| D::ka_agree_dec(ivk, epk)), - ephemeral_key, - ) - }) - }); - - // Run the batch-KDF to obtain the symmetric keys from the shared secrets. - let keys = D::batch_kdf(items); - - // Finish the trial decryption! - keys.chunks(ivks.len()) - .zip(ephemeral_keys.iter().zip(outputs.iter())) - .map(|(key_chunk, ((_, ephemeral_key), (domain, output)))| { - key_chunk - .iter() - .zip(ivks.iter().enumerate()) - .filter_map(|(key, (i, ivk))| { - key.as_ref() - .and_then(|key| decrypt_inner(domain, ivk, ephemeral_key, output, key)) - .map(|out| (out, i)) - }) - .next() - }) - .collect::>>() -} diff --git a/components/zcash_note_encryption/src/lib.rs b/components/zcash_note_encryption/src/lib.rs deleted file mode 100644 index fb8049d40c..0000000000 --- a/components/zcash_note_encryption/src/lib.rs +++ /dev/null @@ -1,675 +0,0 @@ -//! Note encryption for Zcash transactions. -//! -//! This crate implements the [in-band secret distribution scheme] for the Sapling and -//! Orchard protocols. It provides reusable methods that implement common note encryption -//! and trial decryption logic, and enforce protocol-agnostic verification requirements. -//! -//! Protocol-specific logic is handled via the [`Domain`] trait. Implementations of this -//! trait are provided in the [`zcash_primitives`] (for Sapling) and [`orchard`] crates; -//! users with their own existing types can similarly implement the trait themselves. -//! -//! [in-band secret distribution scheme]: https://zips.z.cash/protocol/protocol.pdf#saplingandorchardinband -//! [`zcash_primitives`]: https://crates.io/crates/zcash_primitives -//! [`orchard`]: https://crates.io/crates/orchard - -#![no_std] -#![cfg_attr(docsrs, feature(doc_cfg))] -// Catch documentation errors caused by code changes. -#![deny(rustdoc::broken_intra_doc_links)] -#![deny(unsafe_code)] -// TODO: #![deny(missing_docs)] - -#[cfg(feature = "alloc")] -extern crate alloc; -#[cfg(feature = "alloc")] -use alloc::vec::Vec; - -use chacha20::{ - cipher::{StreamCipher, StreamCipherSeek}, - ChaCha20, -}; -use chacha20poly1305::{aead::AeadInPlace, ChaCha20Poly1305, KeyInit}; -use cipher::KeyIvInit; - -use rand_core::RngCore; -use subtle::{Choice, ConstantTimeEq}; - -#[cfg(feature = "alloc")] -#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] -pub mod batch; - -/// The size of a compact note. -pub const COMPACT_NOTE_SIZE: usize = 1 + // version - 11 + // diversifier - 8 + // value - 32; // rseed (or rcm prior to ZIP 212) -/// The size of [`NotePlaintextBytes`]. -pub const NOTE_PLAINTEXT_SIZE: usize = COMPACT_NOTE_SIZE + 512; -/// The size of [`OutPlaintextBytes`]. -pub const OUT_PLAINTEXT_SIZE: usize = 32 + // pk_d - 32; // esk -const AEAD_TAG_SIZE: usize = 16; -/// The size of an encrypted note plaintext. -pub const ENC_CIPHERTEXT_SIZE: usize = NOTE_PLAINTEXT_SIZE + AEAD_TAG_SIZE; -/// The size of an encrypted outgoing plaintext. -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 - } -} - -/// Newtype representing the byte encoding of an [`EphemeralPublicKey`]. -/// -/// [`EphemeralPublicKey`]: Domain::EphemeralPublicKey -#[derive(Clone, Debug)] -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) - } -} - -/// Newtype representing the byte encoding of a note plaintext. -pub struct NotePlaintextBytes(pub [u8; NOTE_PLAINTEXT_SIZE]); -/// Newtype representing the byte encoding of a outgoing plaintext. -pub struct OutPlaintextBytes(pub [u8; OUT_PLAINTEXT_SIZE]); - -#[derive(Copy, Clone, PartialEq, Eq)] -enum NoteValidity { - Valid, - Invalid, -} - -/// Trait that encapsulates protocol-specific note encryption types and logic. -/// -/// This trait enables most of the note encryption logic to be shared between Sapling and -/// Orchard, as well as between different implementations of those protocols. -pub trait Domain { - type EphemeralSecretKey: ConstantTimeEq; - type EphemeralPublicKey; - type PreparedEphemeralPublicKey; - type SharedSecret; - type SymmetricKey: AsRef<[u8]>; - type Note; - type Recipient; - type DiversifiedTransmissionKey; - type IncomingViewingKey; - type OutgoingViewingKey; - type ValueCommitment; - type ExtractedCommitment; - type ExtractedCommitmentBytes: Eq + for<'a> From<&'a Self::ExtractedCommitment>; - type Memo; - - /// Derives the `EphemeralSecretKey` corresponding to this note. - /// - /// Returns `None` if the note was created prior to [ZIP 212], and doesn't have a - /// deterministic `EphemeralSecretKey`. - /// - /// [ZIP 212]: https://zips.z.cash/zip-0212 - fn derive_esk(note: &Self::Note) -> Option; - - /// Extracts the `DiversifiedTransmissionKey` from the note. - fn get_pk_d(note: &Self::Note) -> Self::DiversifiedTransmissionKey; - - /// Prepare an ephemeral public key for more efficient scalar multiplication. - fn prepare_epk(epk: Self::EphemeralPublicKey) -> Self::PreparedEphemeralPublicKey; - - /// Derives `EphemeralPublicKey` from `esk` and the note's diversifier. - fn ka_derive_public( - note: &Self::Note, - esk: &Self::EphemeralSecretKey, - ) -> Self::EphemeralPublicKey; - - /// Derives the `SharedSecret` from the sender's information during note encryption. - fn ka_agree_enc( - esk: &Self::EphemeralSecretKey, - pk_d: &Self::DiversifiedTransmissionKey, - ) -> Self::SharedSecret; - - /// Derives the `SharedSecret` from the recipient's information during note trial - /// decryption. - fn ka_agree_dec( - ivk: &Self::IncomingViewingKey, - epk: &Self::PreparedEphemeralPublicKey, - ) -> Self::SharedSecret; - - /// Derives the `SymmetricKey` used to encrypt the note plaintext. - /// - /// `secret` is the `SharedSecret` obtained from [`Self::ka_agree_enc`] or - /// [`Self::ka_agree_dec`]. - /// - /// `ephemeral_key` is the byte encoding of the [`EphemeralPublicKey`] used to derive - /// `secret`. During encryption it is derived via [`Self::epk_bytes`]; during trial - /// decryption it is obtained from [`ShieldedOutput::ephemeral_key`]. - /// - /// [`EphemeralPublicKey`]: Self::EphemeralPublicKey - /// [`EphemeralSecretKey`]: Self::EphemeralSecretKey - fn kdf(secret: Self::SharedSecret, ephemeral_key: &EphemeralKeyBytes) -> Self::SymmetricKey; - - /// Encodes the given `Note` and `Memo` as a note plaintext. - fn note_plaintext_bytes(note: &Self::Note, memo: &Self::Memo) -> NotePlaintextBytes; - - /// Derives the [`OutgoingCipherKey`] for an encrypted note, given the note-specific - /// public data and an `OutgoingViewingKey`. - fn derive_ock( - ovk: &Self::OutgoingViewingKey, - cv: &Self::ValueCommitment, - cmstar_bytes: &Self::ExtractedCommitmentBytes, - ephemeral_key: &EphemeralKeyBytes, - ) -> OutgoingCipherKey; - - /// Encodes the outgoing plaintext for the given note. - fn outgoing_plaintext_bytes( - note: &Self::Note, - esk: &Self::EphemeralSecretKey, - ) -> OutPlaintextBytes; - - /// Returns the byte encoding of the given `EphemeralPublicKey`. - fn epk_bytes(epk: &Self::EphemeralPublicKey) -> EphemeralKeyBytes; - - /// Attempts to parse `ephemeral_key` as an `EphemeralPublicKey`. - /// - /// Returns `None` if `ephemeral_key` is not a valid byte encoding of an - /// `EphemeralPublicKey`. - fn epk(ephemeral_key: &EphemeralKeyBytes) -> Option; - - /// Derives the `ExtractedCommitment` for this note. - fn cmstar(note: &Self::Note) -> Self::ExtractedCommitment; - - /// Parses the given note plaintext from the recipient's perspective. - /// - /// The implementation of this method must check that: - /// - The note plaintext version is valid (for the given decryption domain's context, - /// which may be passed via `self`). - /// - The note plaintext contains valid encodings of its various fields. - /// - Any domain-specific requirements are satisfied. - /// - /// `&self` is passed here to enable the implementation to enforce contextual checks, - /// such as rules like [ZIP 212] that become active at a specific block height. - /// - /// [ZIP 212]: https://zips.z.cash/zip-0212 - /// - /// # Panics - /// - /// Panics if `plaintext` is shorter than [`COMPACT_NOTE_SIZE`]. - fn parse_note_plaintext_without_memo_ivk( - &self, - ivk: &Self::IncomingViewingKey, - plaintext: &[u8], - ) -> Option<(Self::Note, Self::Recipient)>; - - /// Parses the given note plaintext from the sender's perspective. - /// - /// The implementation of this method must check that: - /// - The note plaintext version is valid (for the given decryption domain's context, - /// which may be passed via `self`). - /// - The note plaintext contains valid encodings of its various fields. - /// - Any domain-specific requirements are satisfied. - /// - /// `&self` is passed here to enable the implementation to enforce contextual checks, - /// such as rules like [ZIP 212] that become active at a specific block height. - /// - /// [ZIP 212]: https://zips.z.cash/zip-0212 - fn parse_note_plaintext_without_memo_ovk( - &self, - pk_d: &Self::DiversifiedTransmissionKey, - plaintext: &NotePlaintextBytes, - ) -> Option<(Self::Note, Self::Recipient)>; - - /// Extracts the memo field from the given note plaintext. - /// - /// # Compatibility - /// - /// `&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: &NotePlaintextBytes) -> Self::Memo; - - /// Parses the `DiversifiedTransmissionKey` field of the outgoing plaintext. - /// - /// Returns `None` if `out_plaintext` does not contain a valid byte encoding of a - /// `DiversifiedTransmissionKey`. - fn extract_pk_d(out_plaintext: &OutPlaintextBytes) -> Option; - - /// Parses the `EphemeralSecretKey` field of the outgoing plaintext. - /// - /// Returns `None` if `out_plaintext` does not contain a valid byte encoding of an - /// `EphemeralSecretKey`. - fn extract_esk(out_plaintext: &OutPlaintextBytes) -> Option; -} - -/// Trait that encapsulates protocol-specific batch trial decryption logic. -/// -/// Each batchable operation has a default implementation that calls through to the -/// non-batched implementation. Domains can override whichever operations benefit from -/// batched logic. -#[cfg(feature = "alloc")] -#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] -pub trait BatchDomain: Domain { - /// Computes `Self::kdf` on a batch of items. - /// - /// For each item in the batch, if the shared secret is `None`, this returns `None` at - /// that position. - fn batch_kdf<'a>( - items: impl Iterator, &'a EphemeralKeyBytes)>, - ) -> Vec> { - // Default implementation: do the non-batched thing. - items - .map(|(secret, ephemeral_key)| secret.map(|secret| Self::kdf(secret, ephemeral_key))) - .collect() - } - - /// Computes `Self::epk` on a batch of ephemeral keys. - /// - /// This is useful for protocols where the underlying curve requires an inversion to - /// parse an encoded point. - /// - /// For usability, this returns tuples of the ephemeral keys and the result of parsing - /// them. - fn batch_epk( - ephemeral_keys: impl Iterator, - ) -> Vec<(Option, EphemeralKeyBytes)> { - // Default implementation: do the non-batched thing. - ephemeral_keys - .map(|ephemeral_key| { - ( - Self::epk(&ephemeral_key).map(Self::prepare_epk), - ephemeral_key, - ) - }) - .collect() - } -} - -/// Trait that provides access to the components of an encrypted transaction output. -/// -/// Implementations of this trait are required to define the length of their ciphertext -/// field. In order to use the trial decryption APIs in this crate, the length must be -/// either [`ENC_CIPHERTEXT_SIZE`] or [`COMPACT_NOTE_SIZE`]. -pub trait ShieldedOutput { - /// Exposes the `ephemeral_key` field of the output. - fn ephemeral_key(&self) -> EphemeralKeyBytes; - - /// Exposes the `cmu_bytes` or `cmx_bytes` field of the output. - fn cmstar_bytes(&self) -> D::ExtractedCommitmentBytes; - - /// Exposes the note ciphertext of the output. - fn enc_ciphertext(&self) -> &[u8; CIPHERTEXT_SIZE]; -} - -/// 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) -pub struct NoteEncryption { - epk: D::EphemeralPublicKey, - esk: D::EphemeralSecretKey, - note: D::Note, - 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, memo: D::Memo) -> Self { - let esk = D::derive_esk(¬e).expect("ZIP 212 is active."); - NoteEncryption { - epk: D::ka_derive_public(¬e, &esk), - esk, - note, - memo, - ovk, - } - } - - /// 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. - #[cfg(feature = "pre-zip-212")] - #[cfg_attr(docsrs, doc(cfg(feature = "pre-zip-212")))] - pub fn new_with_esk( - esk: D::EphemeralSecretKey, - ovk: Option, - note: D::Note, - memo: D::Memo, - ) -> Self { - NoteEncryption { - epk: D::ka_derive_public(¬e, &esk), - esk, - note, - 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.memo); - - let mut output = [0u8; ENC_CIPHERTEXT_SIZE]; - output[..NOTE_PLAINTEXT_SIZE].copy_from_slice(&input.0); - let tag = ChaCha20Poly1305::new(key.as_ref().into()) - .encrypt_in_place_detached( - [0u8; 12][..].into(), - &[], - &mut output[..NOTE_PLAINTEXT_SIZE], - ) - .unwrap(); - output[NOTE_PLAINTEXT_SIZE..].copy_from_slice(&tag); - - 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.into(), &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]; - output[..OUT_PLAINTEXT_SIZE].copy_from_slice(&input.0); - let tag = ChaCha20Poly1305::new(ock.as_ref().into()) - .encrypt_in_place_detached([0u8; 12][..].into(), &[], &mut output[..OUT_PLAINTEXT_SIZE]) - .unwrap(); - output[OUT_PLAINTEXT_SIZE..].copy_from_slice(&tag); - - output - } -} - -/// Trial decryption of the full note plaintext by the recipient. -/// -/// Attempts to decrypt and validate the given shielded output using the given `ivk`. -/// If successful, the corresponding note and memo are returned, along with the address 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)> { - let ephemeral_key = output.ephemeral_key(); - - let epk = D::prepare_epk(D::epk(&ephemeral_key)?); - let shared_secret = D::ka_agree_dec(ivk, &epk); - let key = D::kdf(shared_secret, &ephemeral_key); - - try_note_decryption_inner(domain, ivk, &ephemeral_key, output, &key) -} - -fn try_note_decryption_inner>( - domain: &D, - ivk: &D::IncomingViewingKey, - ephemeral_key: &EphemeralKeyBytes, - output: &Output, - key: &D::SymmetricKey, -) -> Option<(D::Note, D::Recipient, D::Memo)> { - let enc_ciphertext = output.enc_ciphertext(); - - let mut plaintext = - NotePlaintextBytes(enc_ciphertext[..NOTE_PLAINTEXT_SIZE].try_into().unwrap()); - - ChaCha20Poly1305::new(key.as_ref().into()) - .decrypt_in_place_detached( - [0u8; 12][..].into(), - &[], - &mut plaintext.0, - enc_ciphertext[NOTE_PLAINTEXT_SIZE..].into(), - ) - .ok()?; - - let (note, to) = parse_note_plaintext_without_memo_ivk( - domain, - ivk, - ephemeral_key, - &output.cmstar_bytes(), - &plaintext.0, - )?; - let memo = domain.extract_memo(&plaintext); - - Some((note, to, memo)) -} - -fn parse_note_plaintext_without_memo_ivk( - domain: &D, - ivk: &D::IncomingViewingKey, - ephemeral_key: &EphemeralKeyBytes, - 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, ephemeral_key, cmstar_bytes) { - Some((note, to)) - } else { - None - } -} - -fn check_note_validity( - note: &D::Note, - ephemeral_key: &EphemeralKeyBytes, - cmstar_bytes: &D::ExtractedCommitmentBytes, -) -> NoteValidity { - if &D::ExtractedCommitmentBytes::from(&D::cmstar(note)) == cmstar_bytes { - // In the case corresponding to specification section 4.19.3, we check that `esk` is equal - // to `D::derive_esk(note)` prior to calling this method. - if let Some(derived_esk) = D::derive_esk(note) { - if D::epk_bytes(&D::ka_derive_public(note, &derived_esk)) - .ct_eq(ephemeral_key) - .into() - { - NoteValidity::Valid - } else { - NoteValidity::Invalid - } - } else { - // Before ZIP 212 - NoteValidity::Valid - } - } 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 given compact shielded output using the -/// given `ivk`. If successful, the corresponding note is returned, along with the address -/// 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)> { - let ephemeral_key = output.ephemeral_key(); - - let epk = D::prepare_epk(D::epk(&ephemeral_key)?); - let shared_secret = D::ka_agree_dec(ivk, &epk); - let key = D::kdf(shared_secret, &ephemeral_key); - - try_compact_note_decryption_inner(domain, ivk, &ephemeral_key, output, &key) -} - -fn try_compact_note_decryption_inner>( - domain: &D, - ivk: &D::IncomingViewingKey, - ephemeral_key: &EphemeralKeyBytes, - output: &Output, - key: &D::SymmetricKey, -) -> Option<(D::Note, D::Recipient)> { - // Start from block 1 to skip over Poly1305 keying output - let mut plaintext = [0; COMPACT_NOTE_SIZE]; - plaintext.copy_from_slice(output.enc_ciphertext()); - let mut keystream = ChaCha20::new(key.as_ref().into(), [0u8; 12][..].into()); - keystream.seek(64); - keystream.apply_keystream(&mut plaintext); - - parse_note_plaintext_without_memo_ivk( - domain, - ivk, - ephemeral_key, - &output.cmstar_bytes(), - &plaintext, - ) -} - -/// Recovery of the full note plaintext by the sender. -/// -/// Attempts to decrypt and validate the given shielded output using the given `ovk`. -/// If successful, the corresponding note and memo are returned, along with the address to -/// which the note was sent. -/// -/// Implements [Zcash Protocol Specification section 4.19.3][decryptovk]. -/// -/// [decryptovk]: https://zips.z.cash/protocol/nu5.pdf#decryptovk -pub fn try_output_recovery_with_ovk>( - domain: &D, - ovk: &D::OutgoingViewingKey, - output: &Output, - cv: &D::ValueCommitment, - out_ciphertext: &[u8; OUT_CIPHERTEXT_SIZE], -) -> Option<(D::Note, D::Recipient, D::Memo)> { - let ock = D::derive_ock(ovk, cv, &output.cmstar_bytes(), &output.ephemeral_key()); - try_output_recovery_with_ock(domain, &ock, output, out_ciphertext) -} - -/// Recovery of the full note plaintext by the sender. -/// -/// Attempts to decrypt and validate the given shielded output using the given `ock`. -/// If successful, the corresponding note and memo are returned, along with the address 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_output_recovery_with_ovk`]. -pub fn try_output_recovery_with_ock>( - domain: &D, - ock: &OutgoingCipherKey, - output: &Output, - out_ciphertext: &[u8; OUT_CIPHERTEXT_SIZE], -) -> Option<(D::Note, D::Recipient, D::Memo)> { - let enc_ciphertext = output.enc_ciphertext(); - - let mut op = OutPlaintextBytes([0; OUT_PLAINTEXT_SIZE]); - op.0.copy_from_slice(&out_ciphertext[..OUT_PLAINTEXT_SIZE]); - - ChaCha20Poly1305::new(ock.as_ref().into()) - .decrypt_in_place_detached( - [0u8; 12][..].into(), - &[], - &mut op.0, - out_ciphertext[OUT_PLAINTEXT_SIZE..].into(), - ) - .ok()?; - - let pk_d = D::extract_pk_d(&op)?; - let esk = D::extract_esk(&op)?; - - let ephemeral_key = output.ephemeral_key(); - 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, &ephemeral_key); - - let mut plaintext = NotePlaintextBytes([0; NOTE_PLAINTEXT_SIZE]); - plaintext - .0 - .copy_from_slice(&enc_ciphertext[..NOTE_PLAINTEXT_SIZE]); - - ChaCha20Poly1305::new(key.as_ref().into()) - .decrypt_in_place_detached( - [0u8; 12][..].into(), - &[], - &mut plaintext.0, - enc_ciphertext[NOTE_PLAINTEXT_SIZE..].into(), - ) - .ok()?; - - let (note, to) = domain.parse_note_plaintext_without_memo_ovk(&pk_d, &plaintext)?; - let memo = domain.extract_memo(&plaintext); - - // ZIP 212: Check that the esk provided to this function is consistent with the esk we can - // derive from the note. This check corresponds to `ToScalar(PRF^{expand}_{rseed}([4]) = esk` - // in https://zips.z.cash/protocol/protocol.pdf#decryptovk. (`ρ^opt = []` for Sapling.) - if let Some(derived_esk) = D::derive_esk(¬e) { - if (!derived_esk.ct_eq(&esk)).into() { - return None; - } - } - - if let NoteValidity::Valid = - check_note_validity::(¬e, &ephemeral_key, &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..f028face54 --- /dev/null +++ b/components/zcash_protocol/CHANGELOG.md @@ -0,0 +1,137 @@ +# 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 +- `zcash_protocol::constants::` + - `V3_TX_VERSION` + - `V3_VERSION_GROUP_ID` + - `V4_TX_VERSION` + - `V4_VERSION_GROUP_ID` + - `V5_TX_VERSION` + - `V5_VERSION_GROUP_ID` + +## [0.5.1] - 2025-03-19 +### Added +- `impl zcash::consensus::Parameters for &P` + +## [0.5.0] - 2025-02-21 +### Added +- `zcash_protocol::memo::MemoBytes::into_bytes` + +### 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..b8828d553d --- /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.5.1" +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_note_encryption/LICENSE-APACHE b/components/zcash_protocol/LICENSE-APACHE similarity index 100% rename from components/zcash_note_encryption/LICENSE-APACHE rename to components/zcash_protocol/LICENSE-APACHE diff --git a/components/zcash_note_encryption/LICENSE-MIT b/components/zcash_protocol/LICENSE-MIT similarity index 95% rename from components/zcash_note_encryption/LICENSE-MIT rename to components/zcash_protocol/LICENSE-MIT index 9500c140cc..c869731ad4 100644 --- a/components/zcash_note_encryption/LICENSE-MIT +++ b/components/zcash_protocol/LICENSE-MIT @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2021 Electric Coin Company +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 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/zcash_primitives/src/consensus.rs b/components/zcash_protocol/src/consensus.rs similarity index 59% rename from zcash_primitives/src/consensus.rs rename to components/zcash_protocol/src/consensus.rs index 02ceffa165..b588b833d2 100644 --- a/zcash_primitives/src/consensus.rs +++ b/components/zcash_protocol/src/consensus.rs @@ -1,28 +1,39 @@ //! 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 std::cmp::{Ord, Ordering}; -use std::convert::TryFrom; -use std::fmt; -use std::ops::{Add, Bound, RangeBounds, Sub}; -use zcash_address; -use crate::constants; +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. +/// 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 { @@ -56,7 +67,7 @@ impl From for u32 { } impl TryFrom for BlockHeight { - type Error = std::num::TryFromIntError; + type Error = core::num::TryFromIntError; fn try_from(value: u64) -> Result { u32::try_from(value).map(BlockHeight) @@ -70,7 +81,7 @@ impl From for u64 { } impl TryFrom for BlockHeight { - type Error = std::num::TryFromIntError; + type Error = core::num::TryFromIntError; fn try_from(value: i32) -> Result { u32::try_from(value).map(BlockHeight) @@ -78,7 +89,7 @@ impl TryFrom for BlockHeight { } impl TryFrom for BlockHeight { - type Error = std::num::TryFromIntError; + type Error = core::num::TryFromIntError; fn try_from(value: i64) -> Result { u32::try_from(value).map(BlockHeight) @@ -95,15 +106,7 @@ 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 + BlockHeight(self.0.saturating_add(other)) } } @@ -111,248 +114,364 @@ 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) + BlockHeight(self.0.saturating_sub(other)) } } -impl Sub for BlockHeight { - type Output = Self; +impl Sub for BlockHeight { + type Output = u32; - fn sub(self, other: Self) -> Self { - self - other.0 + fn sub(self, other: BlockHeight) -> u32 { + self.0.saturating_sub(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) - } - +/// 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 standard network constant for address encoding. Returns - /// 'None' for nonstandard networks. - fn address_network(&self) -> Option; - /// Returns the human-readable prefix for Bech32-encoded Sapling extended spending keys - /// the network to which this Parameters value applies. + /// 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) -> &str; + /// [ZIP 32]: https://github.com/zcash/zips/blob/main/zips/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 Parameters value applies. + /// 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) -> &str; + fn hrp_sapling_extended_full_viewing_key(&self) -> &'static str; /// Returns the Bech32-encoded human-readable prefix for Sapling payment addresses - /// viewing keys for the network to which this Parameters value applies. + /// 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) -> &str; + /// [Zcash Protocol Specification]: https://github.com/zcash/zips/blob/main/rendered/protocol/protocol.pdf + fn hrp_sapling_payment_address(&self) -> &'static 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 + /// Returns the human-readable prefix for Base58Check-encoded Sprout + /// payment addresses for the network to which this NetworkConstants value /// applies. /// - /// [`TransparentAddress::PublicKey`]: zcash_primitives::legacy::TransparentAddress::PublicKey + /// 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. 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 + /// payment addresses for the network to which this NetworkConstants value applies. fn b58_script_address_prefix(&self) -> [u8; 2]; -} -/// Marker struct for the production network. -#[derive(PartialEq, Eq, Copy, Clone, Debug)] -pub struct MainNetwork; + /// 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; -memuse::impl_no_dynamic_usage!(MainNetwork); + /// 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; -pub const MAIN_NETWORK: MainNetwork = MainNetwork; + /// 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; -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)), - NetworkUpgrade::Nu5 => Some(BlockHeight(1_687_104)), - #[cfg(feature = "zfuture")] - NetworkUpgrade::ZFuture => None, - } - } + /// 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 { - constants::mainnet::COIN_TYPE + match self { + NetworkType::Main => mainnet::COIN_TYPE, + NetworkType::Test => testnet::COIN_TYPE, + NetworkType::Regtest => regtest::COIN_TYPE, + } } - fn address_network(&self) -> Option { - Some(zcash_address::Network::Main) + 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_spending_key(&self) -> &str { - constants::mainnet::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_extended_full_viewing_key(&self) -> &str { - constants::mainnet::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 hrp_sapling_payment_address(&self) -> &str { - constants::mainnet::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] { - constants::mainnet::B58_PUBKEY_ADDRESS_PREFIX + 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] { - constants::mainnet::B58_SCRIPT_ADDRESS_PREFIX + 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, + } } } -/// Marker struct for the test network. -#[derive(PartialEq, Eq, Copy, Clone, Debug)] -pub struct TestNetwork; +/// Zcash consensus parameters. +pub trait Parameters: Clone { + /// Returns the type of network configured by this set of consensus parameters. + fn network_type(&self) -> NetworkType; -memuse::impl_no_dynamic_usage!(TestNetwork); + /// Returns the activation height for a particular network upgrade, + /// if an activation height has been set. + fn activation_height(&self, nu: NetworkUpgrade) -> Option; -pub const TEST_NETWORK: TestNetwork = TestNetwork; + /// 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 Parameters for &P { + fn network_type(&self) -> NetworkType { + (*self).network_type() + } -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)), - NetworkUpgrade::Nu5 => Some(BlockHeight(1_842_420)), - #[cfg(feature = "zfuture")] - NetworkUpgrade::ZFuture => None, - } + (*self).activation_height(nu) } +} +impl NetworkConstants for P { fn coin_type(&self) -> u32 { - constants::testnet::COIN_TYPE + self.network_type().coin_type() } - fn address_network(&self) -> Option { - Some(zcash_address::Network::Test) + fn hrp_sapling_extended_spending_key(&self) -> &'static str { + self.network_type().hrp_sapling_extended_spending_key() } - fn hrp_sapling_extended_spending_key(&self) -> &str { - constants::testnet::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_extended_full_viewing_key(&self) -> &str { - constants::testnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY + fn hrp_sapling_payment_address(&self) -> &'static str { + self.network_type().hrp_sapling_payment_address() } - fn hrp_sapling_payment_address(&self) -> &str { - constants::testnet::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] { - constants::testnet::B58_PUBKEY_ADDRESS_PREFIX + self.network_type().b58_pubkey_address_prefix() } fn b58_script_address_prefix(&self) -> [u8; 2] { - constants::testnet::B58_SCRIPT_ADDRESS_PREFIX + self.network_type().b58_script_address_prefix() } -} -#[derive(PartialEq, Eq, Copy, Clone, Debug)] -pub enum Network { - MainNetwork, - TestNetwork, -} + fn hrp_tex_address(&self) -> &'static str { + self.network_type().hrp_tex_address() + } -memuse::impl_no_dynamic_usage!(Network); + fn hrp_unified_address(&self) -> &'static str { + self.network_type().hrp_unified_address() + } -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 hrp_unified_fvk(&self) -> &'static str { + self.network_type().hrp_unified_fvk() } - fn coin_type(&self) -> u32 { - match self { - Network::MainNetwork => MAIN_NETWORK.coin_type(), - Network::TestNetwork => TEST_NETWORK.coin_type(), - } + fn hrp_unified_ivk(&self) -> &'static str { + self.network_type().hrp_unified_ivk() } +} - fn address_network(&self) -> Option { - match self { - Network::MainNetwork => Some(zcash_address::Network::Main), - Network::TestNetwork => Some(zcash_address::Network::Test), - } +/// 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 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 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 = "nu7")] + NetworkUpgrade::Nu7 => None, + #[cfg(zcash_unstable = "zfuture")] + NetworkUpgrade::ZFuture => None, } } +} - 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(), - } +/// 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 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 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 = "nu7")] + NetworkUpgrade::Nu7 => None, + #[cfg(zcash_unstable = "zfuture")] + NetworkUpgrade::ZFuture => None, } } +} - fn b58_pubkey_address_prefix(&self) -> [u8; 2] { +/// 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 => MAIN_NETWORK.b58_pubkey_address_prefix(), - Network::TestNetwork => TEST_NETWORK.b58_pubkey_address_prefix(), + Network::MainNetwork => NetworkType::Main, + Network::TestNetwork => NetworkType::Test, } } - fn b58_script_address_prefix(&self) -> [u8; 2] { + fn activation_height(&self, nu: NetworkUpgrade) -> Option { match self { - Network::MainNetwork => MAIN_NETWORK.b58_script_address_prefix(), - Network::TestNetwork => TEST_NETWORK.b58_script_address_prefix(), + Network::MainNetwork => MAIN_NETWORK.activation_height(nu), + Network::TestNetwork => TEST_NETWORK.activation_height(nu), } } } @@ -361,7 +480,7 @@ impl Parameters for Network { /// consensus rules enforced by the network are altered. /// /// See [ZIP 200](https://zips.z.cash/zip-0200) for more details. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum NetworkUpgrade { /// The [Overwinter] network upgrade. /// @@ -387,15 +506,25 @@ pub enum NetworkUpgrade { /// /// [Nu5]: https://z.cash/upgrade/nu5/ Nu5, + /// The [Nu6] network upgrade. + /// + /// [Nu6]: https://z.cash/upgrade/nu6/ + Nu6, + /// The [Nu7 (proposed)] network upgrade. + /// + /// [Nu7 (proposed)]: https://z.cash/upgrade/nu7/ + #[cfg(zcash_unstable = "nu7")] + Nu7, /// 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")] + #[cfg(zcash_unstable = "zfuture")] ZFuture, } +#[cfg(feature = "std")] memuse::impl_no_dynamic_usage!(NetworkUpgrade); impl fmt::Display for NetworkUpgrade { @@ -407,7 +536,10 @@ impl fmt::Display for NetworkUpgrade { NetworkUpgrade::Heartwood => write!(f, "Heartwood"), NetworkUpgrade::Canopy => write!(f, "Canopy"), NetworkUpgrade::Nu5 => write!(f, "Nu5"), - #[cfg(feature = "zfuture")] + NetworkUpgrade::Nu6 => write!(f, "Nu6"), + #[cfg(zcash_unstable = "nu7")] + NetworkUpgrade::Nu7 => write!(f, "Nu7"), + #[cfg(zcash_unstable = "zfuture")] NetworkUpgrade::ZFuture => write!(f, "ZFUTURE"), } } @@ -422,7 +554,10 @@ impl NetworkUpgrade { NetworkUpgrade::Heartwood => BranchId::Heartwood, NetworkUpgrade::Canopy => BranchId::Canopy, NetworkUpgrade::Nu5 => BranchId::Nu5, - #[cfg(feature = "zfuture")] + NetworkUpgrade::Nu6 => BranchId::Nu6, + #[cfg(zcash_unstable = "nu7")] + NetworkUpgrade::Nu7 => BranchId::Nu7, + #[cfg(zcash_unstable = "zfuture")] NetworkUpgrade::ZFuture => BranchId::ZFuture, } } @@ -439,8 +574,14 @@ const UPGRADES_IN_ORDER: &[NetworkUpgrade] = &[ NetworkUpgrade::Heartwood, NetworkUpgrade::Canopy, NetworkUpgrade::Nu5, + NetworkUpgrade::Nu6, + #[cfg(zcash_unstable = "nu7")] + NetworkUpgrade::Nu7, ]; +/// 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. @@ -455,7 +596,7 @@ pub const ZIP212_GRACE_PERIOD: u32 = 32256; /// /// See [ZIP 200](https://zips.z.cash/zip-0200) for more details. /// -/// [`signature_hash`]: crate::transaction::sighash::signature_hash +/// [`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. @@ -472,12 +613,18 @@ pub enum BranchId { Canopy, /// The consensus rules deployed by [`NetworkUpgrade::Nu5`]. Nu5, + /// The consensus rules deployed by [`NetworkUpgrade::Nu6`]. + Nu6, + /// The consensus rules to be deployed by [`NetworkUpgrade::Nu7`]. + #[cfg(zcash_unstable = "nu7")] + Nu7, /// Candidates for future consensus rules; this branch will never /// activate on mainnet. - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] ZFuture, } +#[cfg(feature = "std")] memuse::impl_no_dynamic_usage!(BranchId); impl TryFrom for BranchId { @@ -492,7 +639,10 @@ impl TryFrom for BranchId { 0xf5b9_230b => Ok(BranchId::Heartwood), 0xe9ff_75a6 => Ok(BranchId::Canopy), 0xc2d6_d0b4 => Ok(BranchId::Nu5), - #[cfg(feature = "zfuture")] + 0xc8e7_1055 => Ok(BranchId::Nu6), + #[cfg(zcash_unstable = "nu7")] + 0xffff_ffff => Ok(BranchId::Nu7), + #[cfg(zcash_unstable = "zfuture")] 0xffff_ffff => Ok(BranchId::ZFuture), _ => Err("Unknown consensus branch ID"), } @@ -509,7 +659,10 @@ impl From for u32 { BranchId::Heartwood => 0xf5b9_230b, BranchId::Canopy => 0xe9ff_75a6, BranchId::Nu5 => 0xc2d6_d0b4, - #[cfg(feature = "zfuture")] + BranchId::Nu6 => 0xc8e7_1055, + #[cfg(zcash_unstable = "nu7")] + BranchId::Nu7 => 0xffff_ffff, + #[cfg(zcash_unstable = "zfuture")] BranchId::ZFuture => 0xffff_ffff, } } @@ -574,14 +727,23 @@ impl BranchId { BranchId::Canopy => params .activation_height(NetworkUpgrade::Canopy) .map(|lower| (lower, params.activation_height(NetworkUpgrade::Nu5))), - BranchId::Nu5 => params.activation_height(NetworkUpgrade::Nu5).map(|lower| { - #[cfg(feature = "zfuture")] + 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 = "nu7")] + let upper = params.activation_height(NetworkUpgrade::Nu7); + #[cfg(zcash_unstable = "zfuture")] let upper = params.activation_height(NetworkUpgrade::ZFuture); - #[cfg(not(feature = "zfuture"))] + #[cfg(not(any(zcash_unstable = "nu7", zcash_unstable = "zfuture")))] let upper = None; (lower, upper) }), - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "nu7")] + BranchId::Nu7 => params + .activation_height(NetworkUpgrade::Nu7) + .map(|lower| (lower, None)), + #[cfg(zcash_unstable = "zfuture")] BranchId::ZFuture => params .activation_height(NetworkUpgrade::ZFuture) .map(|lower| (lower, None)), @@ -609,7 +771,10 @@ pub mod testing { BranchId::Heartwood, BranchId::Canopy, BranchId::Nu5, - #[cfg(feature = "zfuture")] + BranchId::Nu6, + #[cfg(zcash_unstable = "nu7")] + BranchId::Nu7, + #[cfg(zcash_unstable = "zfuture")] BranchId::ZFuture, ]) } @@ -622,17 +787,21 @@ pub mod testing { .height_bounds(params) .map_or(Strategy::boxed(Just(None)), |(lower, upper)| { Strategy::boxed( - (lower.0..upper.map_or(std::u32::MAX, |u| u.0)) - .prop_map(|h| Some(BlockHeight(h))), + (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 std::convert::TryFrom; - use super::{ BlockHeight, BranchId, NetworkUpgrade, Parameters, MAIN_NETWORK, UPGRADES_IN_ORDER, }; @@ -697,8 +866,16 @@ mod tests { BranchId::Nu5, ); assert_eq!( - BranchId::for_height(&MAIN_NETWORK, BlockHeight(5_000_000)), + 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..39d97f4910 --- /dev/null +++ b/components/zcash_protocol/src/constants.rs @@ -0,0 +1,59 @@ +//! Network-specific Zcash constants. + +pub mod mainnet; +pub mod regtest; +pub mod testnet; + +// The `V_TX_VERSION` constants, although trivial, serve to clarify that a +// transaction version is meant in APIs that use a bare `u32`. Consider using +// `zcash_primitives::transaction::TxVersion` instead. + +/// Transaction version 3, which was introduced by the Overwinter network upgrade +/// and allowed until Sapling activation. It is specified in +/// [§ 7.1 Transaction Encoding and Consensus](https://zips.z.cash/protocol/protocol.pdf#txnencoding). +/// +/// This constant is called `OVERWINTER_TX_VERSION` in the zcashd source. +pub const V3_TX_VERSION: u32 = 3; +/// The version group ID for Zcash v3 transactions. +/// +/// This constant is called `OVERWINTER_VERSION_GROUP_ID` in the zcashd source. +pub const V3_VERSION_GROUP_ID: u32 = 0x03C48270; + +/// Transaction version 4, which was introduced by the Sapling network upgrade. +/// It is specified in [§ 7.1 Transaction Encoding and Consensus](https://zips.z.cash/protocol/protocol.pdf#txnencoding). +/// +/// This constant is called `SAPLING_TX_VERSION` in the zcashd source. +pub const V4_TX_VERSION: u32 = 4; +/// The version group ID for Zcash v4 transactions. +/// +/// This constant is called `SAPLING_VERSION_GROUP_ID` in the zcashd source. +pub const V4_VERSION_GROUP_ID: u32 = 0x892F2085; + +/// Transaction version 5, which was introduced by the NU5 network upgrade. +/// It is specified in [§ 7.1 Transaction Encoding and Consensus](https://zips.z.cash/protocol/protocol.pdf#txnencoding) +/// and [ZIP 225](https://zips.z.cash/zip-0225). +pub const V5_TX_VERSION: u32 = 5; +/// The version group ID for Zcash v5 transactions. +pub const V5_VERSION_GROUP_ID: u32 = 0x26A7270A; + +/// Transaction version 6, specified in [ZIP 230](https://zips.z.cash/zip-0230). +#[cfg(zcash_unstable = "nu7")] +pub const V6_TX_VERSION: u32 = 6; +/// The version group ID for Zcash v6 transactions. +#[cfg(zcash_unstable = "nu7")] +pub const V6_VERSION_GROUP_ID: u32 = 0xFFFFFFFF; + +/// This version is 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 this constant should be inspected, and uses should be +/// removed as appropriate in favor of the new transaction version. +#[cfg(zcash_unstable = "zfuture")] +pub const ZFUTURE_TX_VERSION: u32 = 0x0000FFFF; +/// This version group ID is used exclusively for in-development transaction +/// serialization, and will never be active under the consensus rules. +/// When new consensus version group IDs are added, all call sites +/// using this constant should be inspected, and uses should be +/// removed as appropriate in favor of the new version group ID. +#[cfg(zcash_unstable = "zfuture")] +pub const ZFUTURE_VERSION_GROUP_ID: u32 = 0xFFFFFFFF; diff --git a/components/zcash_protocol/src/constants/mainnet.rs b/components/zcash_protocol/src/constants/mainnet.rs new file mode 100644 index 0000000000..f487b7e6dc --- /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/main/zips/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/main/zips/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/main/rendered/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..40c3f64b1b --- /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/main/zips/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/main/zips/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/main/rendered/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..5621ba36ed --- /dev/null +++ b/components/zcash_protocol/src/local_consensus.rs @@ -0,0 +1,235 @@ +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 = "nu7")] + pub nu7: 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 = "nu7")] + NetworkUpgrade::Nu7 => self.nu7, + #[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 = "nu7")] + let expected_nu7 = BlockHeight::from_u32(8); + #[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 = "nu7")] + nu7: Some(expected_nu7), + #[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 = "nu7")] + assert!(!regtest.is_nu_active(NetworkUpgrade::Nu7, expected_nu6)); + #[cfg(zcash_unstable = "zfuture")] + assert!(!regtest.is_nu_active(NetworkUpgrade::ZFuture, expected_nu6)); + } + + #[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 = "nu7")] + let expected_nu7 = BlockHeight::from_u32(8); + #[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 = "nu7")] + nu7: Some(expected_nu7), + #[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 = "nu7")] + assert_eq!( + regtest.activation_height(NetworkUpgrade::Nu7), + Some(expected_nu7) + ); + #[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 = "nu7")] + let expected_nu7 = BlockHeight::from_u32(8); + #[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 = "nu7")] + nu7: Some(expected_nu7), + #[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 94% rename from zcash_primitives/src/memo.rs rename to components/zcash_protocol/src/memo.rs index 8143eec69d..28fae5db6f 100644 --- a/zcash_primitives/src/memo.rs +++ b/components/zcash_protocol/src/memo.rs @@ -1,10 +1,15 @@ //! Structs for handling encrypted memos. -use std::cmp::Ordering; +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. /// @@ -28,9 +33,9 @@ where } /// Errors that may result from attempting to construct an invalid memo. -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum Error { - InvalidUtf8(std::str::Utf8Error), + InvalidUtf8(core::str::Utf8Error), TooLong(usize), } @@ -43,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. @@ -109,6 +115,11 @@ impl MemoBytes { &self.0 } + /// Consumes this `MemoBytes` value and returns the underlying byte array. + pub fn into_bytes(self) -> [u8; 512] { + *self.0 + } + /// Returns a slice of the raw bytes, excluding null padding. pub fn as_slice(&self) -> &[u8] { let first_null = self @@ -144,9 +155,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), @@ -171,12 +183,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) { @@ -197,13 +203,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())), } } } @@ -274,7 +292,8 @@ impl str::FromStr for Memo { #[cfg(test)] mod tests { - 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..43f03f8eaa --- /dev/null +++ b/components/zip321/CHANGELOG.md @@ -0,0 +1,50 @@ +# 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.3.0] - 2025-02-21 +### Changed +- MSRV is now 1.81.0. +- Migrated to `zcash_protocol 0.5`, `zcash_address 0.7`. + +## [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..56548571c6 --- /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.3.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/zcash_client_backend/src/zip321.rs b/components/zip321/src/lib.rs similarity index 56% rename from zcash_client_backend/src/zip321.rs rename to components/zip321/src/lib.rs index a26c3cdf48..ff7a745f41 100644 --- a/zcash_client_backend/src/zip321.rs +++ b/components/zip321/src/lib.rs @@ -1,30 +1,30 @@ //! Reference implementation of the ZIP-321 standard for payment requests. //! -//! This module provides data structures, parsing, and rendering functions +//! 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::HashMap; +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_primitives::{ - consensus, + +use zcash_address::{ConversionError, ZcashAddress}; +use zcash_protocol::{ memo::{self, MemoBytes}, - transaction::components::Amount, + value::BalanceError, + value::Zatoshis, }; -#[cfg(any(test, feature = "test-dependencies"))] -use std::cmp::Ordering; - -use crate::address::RecipientAddress; - /// Errors that may be produced in decoding of payment requests. -#[derive(Debug)] +#[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), @@ -45,16 +45,67 @@ pub enum Zip321Error { 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_primitives::memo::MemoBytes +/// [`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_primitives::memo::MemoBytes +/// [`MemoBytes`]: zcash_protocol::memo::MemoBytes pub fn memo_from_base64(s: &str) -> Result { BASE64_URL_SAFE_NO_PAD .decode(s) @@ -63,56 +114,104 @@ pub fn memo_from_base64(s: &str) -> Result { } /// A single payment being requested. -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Payment { - /// The payment address to which the payment should be sent. - pub recipient_address: RecipientAddress, + /// The address to which the payment should be sent. + recipient_address: ZcashAddress, /// The amount of the payment that is being requested. - pub amount: Amount, + 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 - pub memo: Option, + memo: Option, /// A human-readable label for this payment within the larger structure /// of the transaction request. - pub label: Option, + label: Option, /// A human-readable message to be displayed to the user describing the /// purpose of this payment. - pub message: Option, + message: Option, /// A list of other arbitrary key/value pairs associated with this payment. - pub other_params: Vec<(String, String)>, + 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(); + /// 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 + } } - /// 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)) + /// 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. @@ -121,57 +220,88 @@ impl 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, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct TransactionRequest { - payments: Vec, + payments: BTreeMap, } impl TransactionRequest { /// Constructs a new empty transaction request. pub fn empty() -> Self { - Self { payments: vec![] } + Self { + payments: BTreeMap::new(), + } } - /// Constructs a new transaction request that obeys the ZIP-321 invariants + /// Constructs a new transaction request that obeys the ZIP-321 invariants. pub fn new(payments: Vec) -> Result { - let request = TransactionRequest { payments }; + // 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() { - // It doesn't matter what params we use here, as none of the validity - // requirements depend on them. - let params = consensus::MAIN_NETWORK; - TransactionRequest::from_uri(¶ms, &request.to_uri(¶ms).unwrap())?; + TransactionRequest::from_uri(&request.to_uri())?; } Ok(request) } - /// Returns the slice of payments that make up this request. - pub fn payments(&self) -> &[Payment] { - &self.payments[..] + /// 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(in crate::zip321) fn normalize(&mut self, params: &P) { - for p in &mut self.payments { + pub(crate) fn normalize(&mut self) { + for p in &mut self.payments.values_mut() { 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); + #[cfg(test)] + pub(crate) fn normalize_and_eq(a: &mut TransactionRequest, b: &mut TransactionRequest) -> bool { + a.normalize(); + b.normalize(); a == b } @@ -179,13 +309,13 @@ impl TransactionRequest { /// Convert this request to a URI string. /// /// Returns None if the payment request is empty. - pub fn to_uri(&self, params: &P) -> Option { + pub fn to_uri(&self) -> String { fn payment_params( payment: &Payment, payment_index: Option, ) -> impl IntoIterator + '_ { std::iter::empty() - .chain(render::amount_param(payment.amount, payment_index)) + .chain(Some(render::amount_param(payment.amount, payment_index))) .chain( payment .memo @@ -212,42 +342,44 @@ impl TransactionRequest { ) } - match &self.payments[..] { - [] => None, - [payment] => { + 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::>(); - Some(format!( - "zcash:{}?{}", - payment.recipient_address.encode(params), + format!( + "zcash:{}{}{}", + payment.recipient_address.encode(), + if query_params.is_empty() { "" } else { "?" }, query_params.join("&") - )) + ) } _ => { let query_params = self .payments .iter() - .enumerate() .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(params, &primary_address, Some(i)))) - .chain(payment_params(payment, Some(i))) + .chain(Some(render::addr_param(&primary_address, idx))) + .chain(payment_params(payment, idx)) }) .collect::>(); - Some(format!("zcash:?{}", query_params.join("&"))) + format!("zcash:?{}", query_params.join("&")) } } } /// Parse the provided URI to a payment request value. - pub fn from_uri(params: &P, uri: &str) -> Result { + pub fn from_uri(uri: &str) -> Result { // Parse the leading zcash:
- let (rest, primary_addr_param) = - parse::lead_addr(params)(uri).map_err(|e| Zip321Error::ParseError(e.to_string()))?; + 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() { @@ -255,13 +387,15 @@ impl TransactionRequest { } else { all_consuming(preceded( char('?'), - separated_list0(char('&'), parse::zcashparam(params)), + separated_list0(char('&'), parse::zcashparam), ))(rest) - .map_err(|e| Zip321Error::ParseError(e.to_string()))? + .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: HashMap> = HashMap::new(); + let mut params_by_index: BTreeMap> = BTreeMap::new(); // Add the primary address, if any, to the index. if let Some(p) = primary_addr_param { @@ -288,20 +422,21 @@ impl TransactionRequest { // Build the actual payment values from the index. params_by_index .into_iter() - .map(|(i, params)| parse::to_payment(params, i)) - .collect::, _>>() + .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_primitives::{ - consensus, transaction::components::amount::COIN, transaction::components::Amount, + use zcash_address::ZcashAddress; + use zcash_protocol::{ + memo::MemoBytes, + value::{Zatoshis, COIN}, }; - use super::{memo_to_base64, MemoBytes, RecipientAddress}; + 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 @@ -341,36 +476,28 @@ mod render { /// 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)) + pub fn addr_param(addr: &ZcashAddress, idx: Option) -> String { + format!("address{}={}", param_index(idx), addr.encode()) } - /// Converts an [`Amount`] value to a correctly formatted decimal ZEC + /// Converts a [`Zatoshis`] 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() - }) + 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 { - None + 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: Amount, idx: Option) -> Option { - amount_str(amount).map(|s| format!("amount{}={}", param_index(idx), s)) + 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 @@ -397,33 +524,48 @@ mod parse { use nom::{ bytes::complete::{tag, take_till}, character::complete::{alpha1, char, digit0, digit1, one_of}, - combinator::{map_opt, map_res, opt, recognize}, + combinator::{all_consuming, 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 zcash_address::ZcashAddress; + use zcash_protocol::value::BalanceError; + use zcash_protocol::{ + memo::MemoBytes, + value::{Zatoshis, COIN}, }; - use crate::address::RecipientAddress; - - use super::{memo_from_base64, MemoBytes, Payment, Zip321Error}; + 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, PartialEq, Eq)] + #[derive(Debug, Clone, PartialEq, Eq)] pub enum Param { - Addr(Box), - Amount(Amount), - Memo(MemoBytes), + 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)] + #[derive(Debug, Clone, PartialEq, Eq)] pub struct IndexedParam { pub param: Param, pub payment_index: usize, @@ -462,7 +604,7 @@ mod parse { let mut payment = Payment { recipient_address: *addr.ok_or(Zip321Error::RecipientMissing(i))?, - amount: Amount::zero(), + amount: Zatoshis::ZERO, memo: None, label: None, message: None, @@ -472,15 +614,13 @@ mod parse { for v in vs { match v { Param::Amount(a) => payment.amount = a, - Param::Memo(m) => match payment.recipient_address { - RecipientAddress::Shielded(_) | RecipientAddress::Unified(_) => { - payment.memo = Some(m) - } - RecipientAddress::Transparent(_) => { - return Err(Zip321Error::TransparentMemo(i)) + 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)), @@ -492,40 +632,34 @@ mod parse { } /// 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_till(|c| c == '?')), - |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| { + 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, }) }) - } - }, - )(input) - } + .ok() + } + }, + )(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) - } + /// 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 @@ -567,59 +701,55 @@ mod parse { } /// Parses a value in decimal ZEC. - pub fn parse_amount(input: &str) -> IResult<&str, Amount> { + pub fn parse_amount(input: &str) -> IResult<&str, Zatoshis> { map_res( - tuple(( + all_consuming(tuple(( digit1, opt(preceded( char('.'), - map_opt(digit0, |s: &str| if s.len() > 8 { None } else { Some(s) }), + map_opt(digit1, |s: &str| if s.len() > 8 { None } else { Some(s) }), )), - )), + ))), |(whole_s, decimal_s): (&str, Option<&str>)| { - let coins: i64 = whole_s + let coins: u64 = whole_s .to_string() - .parse::() + .parse::() .map_err(|e| e.to_string())?; - let zats: i64 = match decimal_s { + let zats: u64 = match decimal_s { Some(d) => format!("{:0<8}", d) - .parse::() + .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)) + 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<'a, P: consensus::Parameters>( - params: &'a P, + fn to_indexed_param( ((name, iopt), value): ((&str, Option<&str>), &str), ) -> Result { let param = match name { - "address" => RecipientAddress::decode(params, value) + "address" => ZcashAddress::try_from_encoded(value) .map(Box::new) .map(Param::Addr) - .ok_or(format!( - "Could not interpret {} as a valid Zcash address.", - value - )), + .map_err(|err| { + format!( + "Could not interpret {} as a valid Zcash address: {}", + value, err + ) + }), "amount" => parse_amount(value) - .map(|(_, a)| Param::Amount(a)) - .map_err(|e| e.to_string()), + .map_err(|e| e.to_string()) + .map(|(_, a)| Param::Amount(a)), "label" => percent_decode(value.as_bytes()) .decode_utf8() @@ -632,6 +762,7 @@ mod parse { .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)), @@ -657,40 +788,17 @@ mod parse { } } -#[cfg(feature = "test-dependencies")] +#[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, prop_oneof}; - use proptest::strategy::Strategy; - use zcash_primitives::{ - consensus::TEST_NETWORK, legacy::testing::arb_transparent_addr, - sapling::testing::arb_payment_address, - transaction::components::amount::testing::arb_nonnegative_amount, - }; + use proptest::prelude::{any, prop_compose}; - use crate::address::{RecipientAddress, UnifiedAddress}; + use zcash_address::testing::arb_address; + use zcash_protocol::{consensus::NetworkType, value::testing::arb_zatoshis}; use super::{MemoBytes, Payment, TransactionRequest}; - - prop_compose! { - fn arb_unified_addr()( - sapling in arb_payment_address(), - transparent in option::of(arb_transparent_addr()), - ) -> UnifiedAddress { - UnifiedAddress::from_receivers(None, Some(sapling), transparent).unwrap() - } - } - - pub fn arb_addr() -> impl Strategy { - prop_oneof![ - arb_payment_address().prop_map(RecipientAddress::Shielded), - arb_transparent_addr().prop_map(RecipientAddress::Transparent), - arb_unified_addr().prop_map(RecipientAddress::Unified), - ] - } - pub const VALID_PARAMNAME: &str = "[a-zA-Z][a-zA-Z0-9+-]*"; prop_compose! { @@ -700,25 +808,20 @@ pub mod testing { } prop_compose! { - pub fn arb_zip321_payment()( - recipient_address in arb_addr(), - amount in arb_nonnegative_amount(), + 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 is_shielded = match recipient_address { - RecipientAddress::Transparent(_) => false, - RecipientAddress::Shielded(_) | RecipientAddress::Unified(_) => true, - }; - + ) -> Payment { + let memo = memo.filter(|_| recipient_address.can_receive_memo()); Payment { recipient_address, amount, - memo: memo.filter(|_| is_shielded), + memo, label, message, other_params: other_params.into_iter().collect(), @@ -727,64 +830,64 @@ pub mod testing { } 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 + 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()(req in arb_zip321_request()) -> String { - req.to_uri(&TEST_NETWORK).unwrap() + pub fn arb_zip321_uri(network: NetworkType)(req in arb_zip321_request(network)) -> String { + req.to_uri() } } prop_compose! { - pub fn arb_addr_str()(addr in arb_addr()) -> String { - addr.encode(&TEST_NETWORK) + 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_primitives::{ - consensus::{Parameters, TEST_NETWORK}, - memo::Memo, - transaction::components::Amount, - }; - use crate::address::RecipientAddress; + 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, - 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}, + 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) { - 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); - } + let req_uri = req.to_uri(); + let parsed = TransactionRequest::from_uri(&req_uri).unwrap(); + assert_eq!(parsed, req); } #[test] @@ -792,8 +895,8 @@ mod tests { 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(); + let amt = Zatoshis::const_from_u64(amt_u64); + let amt_str = amount_str(amt); assert_eq!(amt, parse_amount(&amt_str).unwrap().1); } } @@ -802,27 +905,27 @@ mod tests { fn test_zip321_parse_empty_message() { let fragment = "message="; - let result = zcashparam(&TEST_NETWORK)(fragment).unwrap().1.param; + 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(&TEST_NETWORK, uri).unwrap(); + let parse_result = TransactionRequest::from_uri(uri).unwrap(); - let expected = TransactionRequest { - payments: vec![ + let expected = TransactionRequest::new( + vec![ Payment { - recipient_address: RecipientAddress::Shielded(decode_payment_address(TEST_NETWORK.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()), - amount: Amount::from_u64(376876902796286).unwrap(), + 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); } @@ -830,38 +933,38 @@ mod tests { #[test] fn test_zip321_parse_no_query_params() { let uri = "zcash:ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k"; - let parse_result = TransactionRequest::from_uri(&TEST_NETWORK, uri).unwrap(); + let parse_result = TransactionRequest::from_uri(uri).unwrap(); - let expected = TransactionRequest { - payments: vec![ + let expected = TransactionRequest::new( + vec![ Payment { - recipient_address: RecipientAddress::Shielded(decode_payment_address(TEST_NETWORK.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()), - amount: Amount::from_u64(0).unwrap(), + 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 { - payments: vec![ + let req = TransactionRequest::new( + vec![ Payment { - recipient_address: RecipientAddress::Shielded(decode_payment_address(TEST_NETWORK.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()), - amount: Amount::from_u64(0).unwrap(), + 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); } @@ -887,125 +990,152 @@ mod tests { #[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(&TEST_NETWORK, valid_1).unwrap(); + let v1r = TransactionRequest::from_uri(valid_1).unwrap(); assert_eq!( - v1r.payments.get(0).map(|p| p.amount), - Some(Amount::from_u64(100000000).unwrap()) + 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(&TEST_NETWORK, valid_2).unwrap(); - v2r.normalize(&TEST_NETWORK); + let mut v2r = TransactionRequest::from_uri(valid_2).unwrap(); + v2r.normalize(); assert_eq!( - v2r.payments.get(0).map(|p| p.amount), - Some(Amount::from_u64(12345600000).unwrap()) + 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(Amount::from_u64(78900000).unwrap()) + 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(&TEST_NETWORK, valid_3).unwrap(); + let v3r = TransactionRequest::from_uri(valid_3).unwrap(); assert_eq!( - v3r.payments.get(0).map(|p| p.amount), - Some(Amount::from_u64(2099999999999999u64).unwrap()) + 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(&TEST_NETWORK, valid_4).unwrap(); + let v4r = TransactionRequest::from_uri(valid_4).unwrap(); assert_eq!( - v4r.payments.get(0).map(|p| p.amount), - Some(Amount::from_u64(2100000000000000u64).unwrap()) + 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(&TEST_NETWORK, invalid_1); + 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(&TEST_NETWORK, invalid_2); + 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(&TEST_NETWORK, invalid_3); + 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(&TEST_NETWORK, invalid_4); + 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(&TEST_NETWORK, invalid_5); + 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(&TEST_NETWORK, invalid_6); + 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(&TEST_NETWORK, invalid_7); + 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(&TEST_NETWORK, invalid_7a); + 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(&TEST_NETWORK, invalid_8); + 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(&TEST_NETWORK, invalid_9); + 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(&TEST_NETWORK, invalid_10); + 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()); } - #[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)); + 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()) { - let addr = RecipientAddress::decode(&TEST_NETWORK, &a).unwrap(); - assert_eq!(addr.encode(&TEST_NETWORK), a); + 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_nonnegative_amount()) { - let amt_str = amount_str(amt).unwrap(); + fn prop_zip321_roundtrip_amount(amt in arb_zatoshis()) { + let amt_str = amount_str(amt); assert_eq!(amt, parse_amount(&amt_str).unwrap().1); } @@ -1013,7 +1143,7 @@ mod tests { 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(); + 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)); @@ -1023,28 +1153,25 @@ mod tests { 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(); + let (rest, iparam) = zcashparam(&fragment).unwrap(); assert_eq!(rest, ""); - assert_eq!(iparam.param, Param::Memo(memo)); + 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()) { - 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); - } + 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()) { - 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)) + 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..754c8c8823 --- /dev/null +++ b/deny.toml @@ -0,0 +1,63 @@ +# 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 = "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 = ["ISC", "LicenseRef-ring"] }, + { name = "rustls-webpki", allow = ["ISC"] }, + { name = "secp256k1", allow = ["CC0-1.0"] }, + { name = "secp256k1-sys", allow = ["CC0-1.0"] }, + { 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..82d1e29f7d --- /dev/null +++ b/pczt/CHANGELOG.md @@ -0,0 +1,46 @@ +# 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.2.1] - 2025-03-04 + +Documentation improvements and rendering fix; no code changes. + +## [0.2.0] - 2025-02-21 + +### 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 + - `prover::Prover::{requires_sapling_proofs, requires_orchard_proof}` + - `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`, `redjubjub 0.8`, `orchard 0.11`, + `sapling-crypto 0.5`, `zcash_protocol 0.5`, `zcash_transparent 0.2`, + `zcash_primitives 0.22`. + + +## [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..e80eaad181 --- /dev/null +++ b/pczt/Cargo.toml @@ -0,0 +1,133 @@ +[package] +name = "pczt" +version = "0.2.1" +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 + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +zcash_note_encryption = { workspace = true, optional = true } +zcash_primitives = { workspace = true, optional = true } +zcash_protocol = { workspace = true, default-features = false } + +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 } + +# 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 } + +[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] +default = ["std"] +std = ["document-features"] +zip-233 = ["zcash_primitives/zip-233"] + +## Enables functionality that requires Orchard protocol types. +orchard = [ + "dep:ff", + "dep:nonempty", + "dep:orchard", + "dep:pasta_curves", +] + +## Enables functionality that requires Sapling protocol types. +sapling = [ + "dep:bls12_381", + "dep:ff", + "dep:jubjub", + "dep:redjubjub", + "dep:sapling", + "dep:zcash_note_encryption", +] + +## Enables functionality that requires Zcash transparent protocol types. +transparent = ["dep:secp256k1", "dep:transparent"] + +## Enables building a PCZT from the output of `zcash_primitives`'s `Builder::build_for_pczt`. +zcp-builder = ["dep:zcash_primitives"] + +#! ### PCZT roles behind feature flags +#! +#! These roles require awareness of at least one payment protocol's types in order to +#! function. + +## Enables the I/O Finalizer role. +io-finalizer = ["dep:zcash_primitives", "orchard", "sapling", "transparent"] + +## Enables the Prover role. +prover = ["dep:rand_core", "sapling?/temporary-zcashd"] + +## Enables the Signer role. +signer = [ + "dep:blake2b_simd", + "dep:rand_core", + "dep:zcash_primitives", + "orchard", + "sapling", + "transparent", +] + +## Enables the Spend Finalizer role. +spend-finalizer = ["transparent"] + +## Enables the Transaction Extractor role. +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..60091074a9 --- /dev/null +++ b/pczt/README.md @@ -0,0 +1,28 @@ +# pczt + +This library implements the Partially Created Zcash Transaction (PCZT) format. +This format enables splitting up the logical steps of creating a Zcash transaction +across distinct entities. The entity roles roughly match those specified in +[BIP 174: Partially Signed Bitcoin Transaction Format] and [BIP 370: PSBT Version 2], +with additional Zcash-specific roles. + +[BIP 174: Partially Signed Bitcoin Transaction Format]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki +[BIP 370: PSBT Version 2]: https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki + +## 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..62792a00b2 --- /dev/null +++ b/pczt/src/common.rs @@ -0,0 +1,346 @@ +//! The common fields of a PCZT. + +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..4f65db190c --- /dev/null +++ b/pczt/src/lib.rs @@ -0,0 +1,120 @@ +//! The Partially Created Zcash Transaction (PCZT) format. +//! +//! This format enables splitting up the logical steps of creating a Zcash transaction +//! across distinct entities. The entity roles roughly match those specified in +//! [BIP 174: Partially Signed Bitcoin Transaction Format] and [BIP 370: PSBT Version 2], +//! with additional Zcash-specific roles. +//! +//! [BIP 174: Partially Signed Bitcoin Transaction Format]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki +//! [BIP 370: PSBT Version 2]: https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki +//! +#![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)] + +#[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; + +/// 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..d00d16d3db --- /dev/null +++ b/pczt/src/orchard.rs @@ -0,0 +1,575 @@ +//! The Orchard fields of a PCZT. + +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]>, +} + +/// Information about an Orchard action within a transaction. +#[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 the spend part of an Orchard action. +#[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 the output part of an Orchard action. +#[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..a2e9d58236 --- /dev/null +++ b/pczt/src/roles.rs @@ -0,0 +1,80 @@ +//! Implementations of the PCZT roles. +//! +//! The roles currently without an implementation are: +//! - 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. + +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..6c569a6b3c --- /dev/null +++ b/pczt/src/roles/combiner/mod.rs @@ -0,0 +1,107 @@ +//! The Combiner role (anyone can execute). +//! +//! - Combines several PCZTs that represent the same transaction into a single PCZT. + +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..3885f04888 --- /dev/null +++ b/pczt/src/roles/creator/mod.rs @@ -0,0 +1,183 @@ +//! The Creator role (single entity). +//! +//! - Creates the base PCZT with no information about spends or outputs. + +use alloc::collections::BTreeMap; + +use crate::{ + common::{ + FLAG_SHIELDED_MODIFIABLE, FLAG_TRANSPARENT_INPUTS_MODIFIABLE, + FLAG_TRANSPARENT_OUTPUTS_MODIFIABLE, + }, + Pczt, +}; + +use zcash_protocol::constants::{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, constants::V4_TX_VERSION}; + + use crate::common::FLAG_HAS_SIGHASH_SINGLE; + + #[cfg(zcash_unstable = "nu7")] + use zcash_protocol::constants::V6_TX_VERSION; + + let tx_version = match parts.version { + zcash_primitives::transaction::TxVersion::Sprout(_) + | zcash_primitives::transaction::TxVersion::V3 => None, + zcash_primitives::transaction::TxVersion::V4 => Some(V4_TX_VERSION), + zcash_primitives::transaction::TxVersion::V5 => Some(V5_TX_VERSION), + #[cfg(zcash_unstable = "nu7")] + zcash_primitives::transaction::TxVersion::V6 => Some(V6_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..c8193e6f81 --- /dev/null +++ b/pczt/src/roles/io_finalizer/mod.rs @@ -0,0 +1,126 @@ +//! The IO Finalizer role (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. + +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, +}; +use zcash_protocol::constants::{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..016bb77fc6 --- /dev/null +++ b/pczt/src/roles/low_level_signer/mod.rs @@ -0,0 +1,82 @@ +//! A low-level variant of the Signer role, for dependency-constrained environments. + +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..fcd458fc1d --- /dev/null +++ b/pczt/src/roles/prover/mod.rs @@ -0,0 +1,48 @@ +//! The Prover role (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. + +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 } + } + + /// Returns `true` if this PCZT contains Sapling Spends or Outputs that are missing + /// proofs. + pub fn requires_sapling_proofs(&self) -> bool { + let sapling_bundle = self.pczt.sapling(); + + sapling_bundle.spends().iter().any(|s| s.zkproof.is_none()) + || sapling_bundle.outputs().iter().any(|o| o.zkproof.is_none()) + } + + /// Returns `true` if this PCZT contains Orchard Actions but no Orchard proof. + pub fn requires_orchard_proof(&self) -> bool { + !self.pczt.orchard().actions().is_empty() && self.pczt.orchard().zkproof.is_none() + } + + /// 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..a05a3f9301 --- /dev/null +++ b/pczt/src/roles/redactor/mod.rs @@ -0,0 +1,52 @@ +//! The Redactor role (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. + +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..5c0e7b4f3d --- /dev/null +++ b/pczt/src/roles/signer/mod.rs @@ -0,0 +1,330 @@ +//! The Signer role (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. + +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; +#[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))] +use zcash_protocol::value::Zatoshis; + +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::V5), + (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(), + #[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))] + Zatoshis::ZERO, + 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..cf605d2563 --- /dev/null +++ b/pczt/src/roles/spend_finalizer/mod.rs @@ -0,0 +1,46 @@ +//! The Spend Finalizer role (anyone can execute). +//! +//! - Combines partial transparent signatures into `script_sig`s. + +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..84b1845789 --- /dev/null +++ b/pczt/src/roles/tx_extractor/mod.rs @@ -0,0 +1,186 @@ +//! The Transaction Extractor role (anyone can execute). +//! +//! - Creates bindingSig and extracts the final transaction. + +use core::marker::PhantomData; +use rand_core::OsRng; + +use zcash_primitives::transaction::{ + sighash::{signature_hash, SignableInput}, + txid::TxIdDigester, + Authorization, Transaction, TransactionData, TxVersion, +}; +#[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))] +use zcash_protocol::value::Zatoshis; +use zcash_protocol::{ + consensus::BranchId, + constants::{V5_TX_VERSION, V5_VERSION_GROUP_ID}, +}; + +use crate::{common::determine_lock_time, Pczt}; + +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::V5), + (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(), + #[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))] + Zatoshis::ZERO, + 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..6498db654a --- /dev/null +++ b/pczt/src/roles/updater/mod.rs @@ -0,0 +1,74 @@ +//! The Updater role (anyone can contribute). +//! +//! - Adds information necessary for subsequent entities to proceed, such as key paths +//! for signing spends. + +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..c96f4f1e95 --- /dev/null +++ b/pczt/src/roles/verifier/mod.rs @@ -0,0 +1,37 @@ +//! The Verifier role (anyone can inspect). +//! +//! This isn't a real role per se; it's instead a way for accessing the parsed +//! protocol-specific bundles for individual access and verification. + +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..caf67d6981 --- /dev/null +++ b/pczt/src/sapling.rs @@ -0,0 +1,602 @@ +//! The Sapling fields of a PCZT. + +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..725d03380b --- /dev/null +++ b/pczt/src/transparent.rs @@ -0,0 +1,459 @@ +//! The transparent fields of a PCZT. + +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, +} + +/// Information about a transparent input within a transaction. +#[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>, +} + +/// Information about a transparent output within a transaction. +#[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..90267e15a7 --- /dev/null +++ b/pczt/tests/end_to_end.rs @@ -0,0 +1,427 @@ +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::{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, + Memo::Empty.encode().into_bytes(), + ) + .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, Memo::Empty.encode().into_bytes()) + .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.toml b/rust-toolchain.toml index 5ecda6e495..95c5b61b4f 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.65.0" +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..3c9de75dfb --- /dev/null +++ b/supply-chain/audits.toml @@ -0,0 +1,1317 @@ + +# 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.arti-client]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.23.0 -> 0.28.0" +notes = """ +No `unsafe` changes. The introduction of a path resolver affects filesystem +access but is driven by API changes in dependencies; nothing looks untoward in +the changes to this crate (though the various macros make some of it harder to +reason about). +""" + +[[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.bytes]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.6.0 -> 1.6.1" +notes = """ +New `unsafe` function is a code-duplicate of an existing `unsafe` function, but +using the correct `Shared` type for `BytesMut` in order to fix a bug. +""" + +[[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.directories]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "5.0.1 -> 6.0.0" + +[[audits.dirs]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "5.0.1 -> 6.0.0" + +[[audits.dirs-sys]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.4.1 -> 0.5.0" +notes = """ +One change to an `unsafe` block, adapting to an API change in `windows_sys` +(`Win32::Foundation::HANDLE` changed from `isize` to `*mut c_void`). I confirmed +that the Windows documentation permits an argument of `std::ptr::null_mut()`. +""" + +[[audits.dynosaur]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.1.1 -> 0.2.0" + +[[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.futures-task]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.29 -> 0.3.26" + +[[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.pprof]] +who = "Jack Grigg " +criteria = "safe-to-run" +delta = "0.13.0 -> 0.14.0" +notes = """ +I did not audit the correctness of the new `unsafe` block (initializing an +`aligned_vec::AVec`), but the changes therein don't affect `safe-to-run`. +""" + +[[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.redjubjub]] +who = "Kris Nuttycombe " +criteria = "safe-to-deploy" +delta = "0.7.0 -> 0.8.0" +notes = "This release adds `no-std` compatibility." + +[[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.redox_users]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.4.5 -> 0.5.0" +notes = """ +Changes `Config` from using scheme prefixes (with a default of `file:`) to root +FS prefixes (with a default of `/`). The behaviour of `Config::scheme` changed +correspondingly but without being renamed. The effect on the rest of the crate +is that the passwd, shadow, and group files now default to UNIX-style paths +(`/etc/passwd`) instead of scheme syntax (`file:etc/passwd`). +""" + +[[audits.regex-automata]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.4.6 -> 0.4.7" + +[[audits.regex-automata]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.4.7 -> 0.4.9" + +[[audits.regex-syntax]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.8.3 -> 0.8.4" + +[[audits.retry-error]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.6.0 -> 0.6.3" + +[[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.safelog]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.4.0 -> 0.4.5" + +[[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-stream]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.1.15 -> 0.1.17" +notes = """ +No new `unsafe` code or powerful imports. The new async polling logic added as +`StreamMap::poll_next_many` looks plausible. +""" + +[[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.tonic-build]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.12.3 -> 0.13.0" +notes = "Changes to generated code look sensible (adapting to `tonic` API changes)." + +[[audits.tor-async-utils]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.23.0 -> 0.28.0" +notes = """ +Some macro complexity but it appears to only be used for defining error types; +no changes to `unsafe` code or powerful imports. +""" + +[[audits.tor-bytes]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.23.0 -> 0.28.0" + +[[audits.tor-cert]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.23.0 -> 0.28.0" +notes = """ +No new `unsafe` APIs, but does add a new API that could be used to violate crate +semantics; it is gated as an experimental feature and follows the Tor crate +naming convention of using a `dangerously_*` method prefix. +""" + +[[audits.tor-checkable]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.23.0 -> 0.28.0" + +[[audits.tor-consdiff]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.23.0 -> 0.28.0" + +[[audits.tor-dirclient]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.23.0 -> 0.28.0" + +[[audits.tor-dirmgr]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.23.0 -> 0.28.0" + +[[audits.tor-error]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.23.0 -> 0.28.0" + +[[audits.tor-log-ratelim]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.23.0 -> 0.28.0" + +[[audits.tor-persist]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.23.0 -> 0.28.0" +notes = "No new `unsafe` code, and three new `#![forbid(unsafe_code)]` annotations." + +[[audits.tor-protover]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.23.0 -> 0.28.0" + +[[audits.tor-relay-selection]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.23.0 -> 0.28.0" + +[[audits.tower-layer]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.2 -> 0.3.3" + +[[audits.tower-service]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.2 -> 0.3.3" + +[[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.equihash]] +criteria = "safe-to-deploy" +user-id = 169181 # Kris Nuttycombe (nuttycom) +start = "2025-02-21" +end = "2026-02-21" + +[[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 = 6289 # Jack Grigg (str4d) +start = "2024-10-08" +end = "2026-03-13" + +[[trusted.pczt]] +criteria = "safe-to-deploy" +user-id = 169181 # Kris Nuttycombe (nuttycom) +start = "2024-12-17" +end = "2025-12-17" + +[[trusted.redjubjub]] +criteria = "safe-to-deploy" +user-id = 199950 # Daira-Emma Hopwood (daira) +start = "2023-03-30" +end = "2026-02-21" + +[[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_encoding]] +criteria = "safe-to-deploy" +user-id = 169181 # Kris Nuttycombe (nuttycom) +start = "2024-08-19" +end = "2026-02-21" + +[[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" +user-id = 199950 # Daira-Emma Hopwood (daira) +start = "2025-02-20" +end = "2026-02-21" + +[[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.zip32]] +criteria = "safe-to-deploy" +user-id = 169181 # Kris Nuttycombe (nuttycom) +start = "2025-02-20" +end = "2026-02-21" + +[[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..2f41a19cec --- /dev/null +++ b/supply-chain/config.toml @@ -0,0 +1,1598 @@ + +# 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.aligned-vec]] +version = "0.6.4" +criteria = "safe-to-run" + +[[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.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.asn1-rs]] +version = "0.7.1" +criteria = "safe-to-deploy" + +[[exemptions.asn1-rs-derive]] +version = "0.6.0" +criteria = "safe-to-deploy" + +[[exemptions.asn1-rs-impl]] +version = "0.2.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.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.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.10.1" +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.2.17" +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.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.7.1" +criteria = "safe-to-deploy" + +[[exemptions.cookie-factory]] +version = "0.3.3" +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.der-parser]] +version = "10.0.0" +criteria = "safe-to-deploy" + +[[exemptions.derive-deftly]] +version = "0.14.2" +criteria = "safe-to-deploy" + +[[exemptions.derive-deftly]] +version = "1.0.1" +criteria = "safe-to-deploy" + +[[exemptions.derive-deftly-macros]] +version = "0.14.2" +criteria = "safe-to-deploy" + +[[exemptions.derive-deftly-macros]] +version = "1.0.1" +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 = "2.0.1" +criteria = "safe-to-deploy" + +[[exemptions.derive_more-impl]] +version = "2.0.1" +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 = "2.0.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.2.0" +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.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.env_home]] +version = "0.1.0" +criteria = "safe-to-deploy" + +[[exemptions.equator]] +version = "0.4.2" +criteria = "safe-to-run" + +[[exemptions.equator-macro]] +version = "0.4.2" +criteria = "safe-to-run" + +[[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.9.1" +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-rustls]] +version = "0.26.0" +criteria = "safe-to-deploy" + +[[exemptions.futures-task]] +version = "0.3.29" +criteria = "safe-to-deploy" + +[[exemptions.futures-util]] +version = "0.3.27" +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-run" + +[[exemptions.gumdrop_derive]] +version = "0.8.1" +criteria = "safe-to-run" + +[[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.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.10.1" +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.6.0" +criteria = "safe-to-deploy" + +[[exemptions.hyper-timeout]] +version = "0.4.1" +criteria = "safe-to-deploy" + +[[exemptions.hyper-util]] +version = "0.1.11" +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.11.0" +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.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.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.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.mio]] +version = "1.0.3" +criteria = "safe-to-deploy" + +[[exemptions.multimap]] +version = "0.8.3" +criteria = "safe-to-deploy" + +[[exemptions.notify]] +version = "8.0.0" +criteria = "safe-to-deploy" + +[[exemptions.notify-types]] +version = "2.0.0" +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.os_str_bytes]] +version = "6.6.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.pasta_curves]] +version = "0.5.1" +criteria = "safe-to-deploy" + +[[exemptions.paste]] +version = "1.0.15" +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 = "1.0.0" +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.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.11.1" +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.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.14" +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.rusticata-macros]] +version = "4.1.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.25" +criteria = "safe-to-deploy" + +[[exemptions.rustls-pki-types]] +version = "1.11.0" +criteria = "safe-to-deploy" + +[[exemptions.rustls-webpki]] +version = "0.101.7" +criteria = "safe-to-deploy" + +[[exemptions.rustls-webpki]] +version = "0.103.1" +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.6.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.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.slotmap-careful]] +version = "0.2.3" +criteria = "safe-to-deploy" + +[[exemptions.socket2]] +version = "0.5.9" +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.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.100" +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.thiserror]] +version = "2.0.12" +criteria = "safe-to-deploy" + +[[exemptions.thiserror-impl]] +version = "2.0.12" +criteria = "safe-to-deploy" + +[[exemptions.time]] +version = "0.3.23" +criteria = "safe-to-deploy" + +[[exemptions.tinystr]] +version = "0.8.1" +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.2" +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.13.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.28.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-bytes]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-cell]] +version = "0.28.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-cert]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-chanmgr]] +version = "0.28.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-checkable]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-circmgr]] +version = "0.28.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-config]] +version = "0.28.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-config-path]] +version = "0.28.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-general-addr]] +version = "0.28.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-guardmgr]] +version = "0.28.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-hscrypto]] +version = "0.28.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-key-forge]] +version = "0.28.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-keymgr]] +version = "0.28.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-linkspec]] +version = "0.28.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-llcrypto]] +version = "0.28.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-log-ratelim]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-memquota]] +version = "0.28.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-netdir]] +version = "0.28.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-netdoc]] +version = "0.28.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-persist]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-proto]] +version = "0.28.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.28.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-rtmock]] +version = "0.28.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-socksproto]] +version = "0.28.0" +criteria = "safe-to-deploy" + +[[exemptions.tor-units]] +version = "0.28.0" +criteria = "safe-to-deploy" + +[[exemptions.tower]] +version = "0.5.2" +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.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 = "7.0.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.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.zerovec]] +version = "0.11.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..0c072231c2 --- /dev/null +++ b/supply-chain/imports.lock @@ -0,0 +1,3566 @@ + +# 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.2" +when = "2025-03-05" +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.orchard]] +version = "0.11.0" +when = "2025-02-21" +user-id = 169181 +user-login = "nuttycom" +user-name = "Kris Nuttycombe" + +[[publisher.pczt]] +version = "0.2.1" +when = "2025-03-05" +user-id = 6289 +user-login = "str4d" +user-name = "Jack Grigg" + +[[publisher.sapling-crypto]] +version = "0.5.0" +when = "2025-02-21" +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.unicode-segmentation]] +version = "1.12.0" +when = "2024-09-13" +user-id = 1139 +user-login = "Manishearth" +user-name = "Manish Goregaokar" + +[[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.7.1" +when = "2025-05-07" +user-id = 169181 +user-login = "nuttycom" +user-name = "Kris Nuttycombe" + +[[publisher.zcash_client_backend]] +version = "0.18.0" +when = "2025-03-21" +user-id = 169181 +user-login = "nuttycom" +user-name = "Kris Nuttycombe" + +[[publisher.zcash_client_sqlite]] +version = "0.16.2" +when = "2025-04-03" +user-id = 169181 +user-login = "nuttycom" +user-name = "Kris Nuttycombe" + +[[publisher.zcash_encoding]] +version = "0.3.0" +when = "2025-02-21" +user-id = 169181 +user-login = "nuttycom" +user-name = "Kris Nuttycombe" + +[[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.8.0" +when = "2025-03-21" +user-id = 169181 +user-login = "nuttycom" +user-name = "Kris Nuttycombe" + +[[publisher.zcash_primitives]] +version = "0.22.0" +when = "2025-02-21" +user-id = 169181 +user-login = "nuttycom" +user-name = "Kris Nuttycombe" + +[[publisher.zcash_proofs]] +version = "0.22.0" +when = "2025-02-21" +user-id = 169181 +user-login = "nuttycom" +user-name = "Kris Nuttycombe" + +[[publisher.zcash_protocol]] +version = "0.5.1" +when = "2025-03-21" +user-id = 169181 +user-login = "nuttycom" +user-name = "Kris Nuttycombe" + +[[publisher.zcash_spec]] +version = "0.2.1" +when = "2025-02-20" +user-id = 199950 +user-login = "daira" +user-name = "Daira-Emma Hopwood" + +[[publisher.zcash_transparent]] +version = "0.2.3" +when = "2025-04-04" +user-id = 169181 +user-login = "nuttycom" +user-name = "Kris Nuttycombe" + +[[publisher.zip32]] +version = "0.2.0" +when = "2025-02-20" +user-id = 169181 +user-login = "nuttycom" +user-name = "Kris Nuttycombe" + +[[publisher.zip321]] +version = "0.3.0" +when = "2025-02-21" +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.bitflags]] +who = "Jamey Sharp " +criteria = "safe-to-deploy" +delta = "2.1.0 -> 2.2.1" +notes = """ +This version adds unsafe impls of traits from the bytemuck crate when built +with that library enabled, but I believe the impls satisfy the documented +safety requirements for bytemuck. The other changes are minor. +""" + +[[audits.bytecode-alliance.audits.bitflags]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "2.3.2 -> 2.3.3" +notes = """ +Nothing outside the realm of what one would expect from a bitflags generator, +all as expected. +""" + +[[audits.bytecode-alliance.audits.bitflags]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "2.4.1 -> 2.6.0" +notes = """ +Changes in how macros are invoked and various bits and pieces of macro-fu. +Otherwise no major changes and nothing dealing with `unsafe`. +""" + +[[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 = "Joel Dice " +criteria = "safe-to-deploy" +version = "0.3.31" + +[[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-core]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +delta = "0.3.28 -> 0.3.31" + +[[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 = "Joel Dice " +criteria = "safe-to-deploy" +version = "0.3.31" + +[[audits.bytecode-alliance.audits.futures-macro]] +who = "Joel Dice " +criteria = "safe-to-deploy" +version = "0.3.31" + +[[audits.bytecode-alliance.audits.futures-sink]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.3.27" + +[[audits.bytecode-alliance.audits.futures-sink]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +delta = "0.3.28 -> 0.3.31" + +[[audits.bytecode-alliance.audits.futures-task]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +delta = "0.3.27 -> 0.3.31" + +[[audits.bytecode-alliance.audits.futures-util]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +delta = "0.3.27 -> 0.3.31" +notes = "New waker_ref module contains \"FIXME: panics on Arc::clone / refcount changes could wreak havoc...\" comment, but this corner case feels low risk." + +[[audits.bytecode-alliance.audits.hashbrown]] +who = "Chris Fallin " +criteria = "safe-to-deploy" +delta = "0.14.5 -> 0.15.2" + +[[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.itertools]] +who = "Nick Fitzgerald " +criteria = "safe-to-deploy" +delta = "0.10.5 -> 0.12.1" +notes = """ +Minimal `unsafe` usage. Few blocks that existed looked reasonable. Does what it +says on the tin: lots of iterators. +""" + +[[audits.bytecode-alliance.audits.itertools]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "0.12.1 -> 0.14.0" +notes = """ +Lots of new iterators and shuffling some things around. Some new unsafe code but +it's well-documented and well-tested. Nothing suspicious. +""" + +[[audits.bytecode-alliance.audits.libc]] +who = "Dan Gohman " +criteria = "safe-to-deploy" +delta = "0.2.158 -> 0.2.161" + +[[audits.bytecode-alliance.audits.libc]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "0.2.161 -> 0.2.171" +notes = """ +Lots of unsafe, but that's par for the course with libc, it's all FFI type +definitions updates/adjustments/etc. +""" + +[[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.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.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" +delta = "2.6.0 -> 2.8.0" +notes = "No changes related to `unsafe impl ... bytemuck` pieces from `src/external.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.bitflags]] +who = "Daniel Cheng " +criteria = "safe-to-deploy" +delta = "2.8.0 -> 2.9.0" +notes = "Adds a straightforward clear() function, but no new unsafe code." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.bstr]] +who = "danakj " +criteria = "safe-to-deploy" +version = "1.10.0" +notes = """ +WARNING: This certification is a result of a **partial** audit. The +`unicode` feature has **not** been audited. The unicode feature has +soundness that depends on the correctness of regex automata that are +shipped as binary blobs. They have not been reviewed here.Ability to +track partial audits is tracked in +https://github.com/mozilla/cargo-vet/issues/380. +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.bstr]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "1.10.0 -> 1.11.0" +notes = "Changes two unsafe blocks to use core::mem::align_of instead of core::mem::size_of which shouldn't differ on mainstream platforms." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.bstr]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.11.0 -> 1.11.1" +notes = "This release just excludes Unicode data files from being published to crates.io" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.bstr]] +who = "Dustin J. Mitchell " +criteria = "safe-to-deploy" +delta = "1.11.1 -> 1.11.3" +notes = "No unsafe 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.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.ciborium]] +who = "Daniel Verkamp " +criteria = "safe-to-run" +version = "0.2.2" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.ciborium-io]] +who = "Daniel Verkamp " +criteria = "safe-to-run" +version = "0.2.2" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.ciborium-ll]] +who = "Daniel Verkamp " +criteria = "safe-to-run" +version = "0.2.2" +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.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.proc-macro2]] +who = "danakj " +criteria = "safe-to-deploy" +delta = "1.0.86 -> 1.0.87" +notes = "No new unsafe interactions." +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 = "Liza Burakova 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.smallvec]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +version = "1.13.2" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.stable_deref_trait]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +version = "1.2.0" +notes = "Purely a trait, crates using this should be carefully vetted since self-referential stuff can be super tricky around various unsafe rust edges." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/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.synstructure]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +version = "0.13.1" +notes = "Exposes unsafe codegen APIs but does not itself contain 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.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.google.audits.windows-core]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +version = "0.52.0" +notes = "Implements Windows system APIs" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.zerofrom]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +version = "0.1.5" +notes = "Contains no 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.zerofrom]] +who = "Daniel Cheng " +criteria = "safe-to-deploy" +delta = "0.1.5 -> 0.1.6" +notes = "Only minor cfg tweaks." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.zerovec]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +delta = "0.11.0 -> 0.11.1" +notes = """ +Some unsafe changed: + - VarZeroCow unsafe moved around but not changed much, comments improved. + - Added a ULE impl for () +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/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-segmentation]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +user-id = 1139 # Manish Goregaokar (Manishearth) +start = "2019-05-15" +end = "2026-02-01" +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.android-tzdata]] +who = "Mark Hammond " +criteria = "safe-to-deploy" +version = "0.1.1" +notes = "Small crate parsing a file. No unsafe code" +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.bitflags]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +delta = "1.3.2 -> 2.0.2" +notes = "Removal of some unsafe code/methods. No changes to externals, just some refactoring (mostly internal)." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bitflags]] +who = "Nicolas Silva " +criteria = "safe-to-deploy" +delta = "2.0.2 -> 2.1.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bitflags]] +who = "Teodor Tanasoaia " +criteria = "safe-to-deploy" +delta = "2.2.1 -> 2.3.2" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bitflags]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "2.3.3 -> 2.4.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bitflags]] +who = "Jan-Erik Rediger " +criteria = "safe-to-deploy" +delta = "2.4.0 -> 2.4.1" +notes = "Only allowing new clippy lints" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/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-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-sink]] +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.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.libc]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +delta = "0.2.154 -> 0.2.158" +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.rand_distr]] +who = "Ben Dean-Kawamura " +criteria = "safe-to-deploy" +version = "0.4.3" +notes = """ +Simple crate that extends `rand`. It has little unsafe code and uses Miri to test it. +As far as I can tell, it does not have any file IO or network access. +""" +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.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.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.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-task]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.26 -> 0.3.27" +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.redjubjub]] +who = "Daira Emma Hopwood " +criteria = "safe-to-deploy" +version = "0.7.0" +notes = """ +This crate is a thin wrapper around the `reddsa` crate, which I did not review. I also +did not review tests or verify test vectors. + +The comment on `batch::Verifier::verify` has an error in the batch verification equation, +filed as https://github.com/ZcashFoundation/redjubjub/issues/163 . It does not affect the +implementation which just delegates to `reddsa`. `reddsa` has the same comment bug filed as +https://github.com/ZcashFoundation/reddsa/issues/52 , but its batch verification implementation +is correct. (I checked the latter against https://zips.z.cash/protocol/protocol.pdf#reddsabatchvalidate +which has had previous cryptographic review by NCC group; see finding NCC-Zcash2018-009 in +https://research.nccgroup.com/wp-content/uploads/2020/07/NCC_Group_Zcash2018_Public_Report_2019-01-30_v1.3.pdf ). +""" +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-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.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.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" diff --git a/zcash/CHANGELOG.md b/zcash/CHANGELOG.md new file mode 100644 index 0000000000..376c8b0475 --- /dev/null +++ b/zcash/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] + +### Changed +- MSRV is now 1.81.0. +- Migrated to `zcash_primitives 0.22`. + +## [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..69599ff740 --- /dev/null +++ b/zcash/Cargo.toml @@ -0,0 +1,39 @@ +[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 + +[package.metadata.release] +release = false + +[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/CHANGELOG.md b/zcash_client_backend/CHANGELOG.md index c896b6d702..02c62f2ed0 100644 --- a/zcash_client_backend/CHANGELOG.md +++ b/zcash_client_backend/CHANGELOG.md @@ -6,24 +6,1051 @@ and this library adheres to Rust's notion of [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +### Added +- `zcash_client_backend::tor`: + - `Client::set_dormant` + - `DormantMode` + +### Changed +- `zcash_client_backend::data_api`: + - `TargetValue`: An intent of representing spendable value to reach a certain + targeted amount. + - `select_spendable_notes`: parameter `target_value` now is a `TargetValue`. + Existing calls to this function that used `Zatoshis` now use + `TargetValue::AtLeast(Zatoshis)` +- Migrated to `arti-client 0.28`, `dynosaur 0.2`, `tonic 0.13`. +- `zcash_client_backend::tor`: + - `Client::{connect_to_lightwalletd, get_latest_zec_to_usd_rate}` now ensure + that the inner Tor client is ready for traffic, and re-bootstrap it if + necessary. + - The exchanges in `http::cryptex::exchanges` will now retry queries once on + failure, and will use isolated circuits for the retry if the error looks + like a blocked Tor exit node. + +## [0.18.0] - 2025-03-19 + +### Added +- `zcash_client_backend::data_api`: + - `AddressInfo` +- `zcash_client_backend::data_api::testing`: + - `struct transparent::GapLimits` + - `transparent::gap_limits` high-level test for gap limit handling +- `zcash_client_backend::data_api::{TransactionStatusFilter, OutputStatusFilter}` + +### Changed +- Updated to `zcash_keys 0.8` +- `zcash_client_backend::data_api::WalletRead`: + - `get_transparent_receivers` now takes additional `include_change` and + `include_ephemeral` arguments. + - `get_known_ephemeral_addresses` now takes a + `Range` as its argument + instead of a `Range` + - Has added method `utxo_query_height` when the `transparent-inputs` feature + flag is active. + - has removed method `get_current_address`. It has been replaced by + added method `WalletRead::get_last_generated_address_matching` + - Has added method `list_addresses`. +- `zcash_client_backend::data_api::WalletWrite`: + - has added method `get_address_for_index`. Please note the WARNINGS section + in the documentation for use of this method. + - `get_next_available_address` now returns the diversifier index at which the + address was generated in addition to the address. In addition, the + `UnifiedAddressRequest` argument is now non-optional; use + `UnifiedAddressRequest::AllAvailableKeys` to indicate that all available + keys should be used to generate receivers instead of `None`. + - Arguments to `get_address_for_index` have changed: the + `UnifiedAddressRequest` argument is now non-optional; use + `UnifiedAddressRequest::AllAvailableKeys` to indicate that all available + keys should be used to generate receivers instead of `None`. + - `TransactionDataRequest::SpendsFromAddress` has been renamed to + `TransactionDataRequest::TransactionsInvolvingAddress` and has added struct + fields `request_at`, `tx_status_filter`, and `output_status_filter`. +- Arguments to `zcash_client_backend::decrypt::decrypt_transaction` have changed. + It now takes separate `mined_height` and `chain_tip_height` parameters; this + fixes https://github.com/zcash/librustzcash/issues/1746 as described in the + `Fixed` section below. + +### Removed +- `zcash_client_backend::data_api::GAP_LIMIT` gap limits are now configured + based upon the key scope that they're associated with; there is no longer a + globally applicable gap limit. + +### Fixed +- This release fixes https://github.com/zcash/librustzcash/issues/1746, which + made it possible for `zcash_client_backend::decrypt_and_store_transaction` + to incorrectly set a `mined_height` value for a mempool transaction. + +## [0.17.0] - 2025-02-21 + +### Added +- `zcash_client_backend::data_api::testing::TransactionSummary` has added + accessor methods `total_spent` and `total_received`. + +### Changed +- MSRV is now 1.81.0. +- Migrated to `bip32 =0.6.0-pre.1`, `nonempty 0.11`, `incrementalmerkletree 0.8`, + `shardtree 0.6`, `orchard 0.11`, `pczt 0.2`, `sapling-crypto 0.5`, `zcash_encoding 0.3`, + `zcash_protocol 0.5`, `zcash_address 0.7`, `zip321 0.3`, `zcash_transparent 0.2`, + `zcash_primitives 0.22`, `zcash_proofs 0.22`, `zcash_keys 0.7`. +- `zcash_client_backend::tor`: + - `tor::Client::create` now takes a `with_permissions` argument for configuring + `fs_mistrust::Mistrust`. If you don't need to configure it, pass `|_| ()` + (the empty closure). +- `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::testing::TransactionSummary::from_parts` + has been modified; it now requires additional `total_spent` and `total_received` + arguments. + +### 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 +- 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}` -- `data_api::NullifierQuery` for use with `WalletRead::get_sapling_nullifiers` +- `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.12`, `zcash_note_encryption 0.4`, - `incrementalmerkletree 0.4`, `orchard 0.5`, `bs58 0.5` -- `WalletRead::get_memo` now 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`. -- `wallet::SpendableNote` has been renamed to `wallet::ReceivedSaplingNote`. +- 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 -- `WalletRead::get_all_nullifiers` +- `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 @@ -165,8 +1192,8 @@ and this library adheres to Rust's notion of - `WalletWrite::get_next_available_address` - `WalletWrite::put_received_transparent_utxo` - `impl From for error::Error` - - `chain::error`: a module containing error types type that that can occur only - in chain validation and sync have been separated out from errors related to + - `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. @@ -199,7 +1226,7 @@ and this library adheres to Rust's notion of 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::keys::{UnifiedSpendingKey, UnifiedFullViewingKey, Era, DecodingError}` - `zcash_client_backend::encoding::AddressCodec` - `zcash_client_backend::encoding::encode_payment_address` - `zcash_client_backend::encoding::encode_transparent_address` diff --git a/zcash_client_backend/Cargo.toml b/zcash_client_backend/Cargo.toml index bcd0038e33..569d8f3159 100644 --- a/zcash_client_backend/Cargo.toml +++ b/zcash_client_backend/Cargo.toml @@ -1,17 +1,18 @@ [package] name = "zcash_client_backend" description = "APIs for creating shielded Zcash light clients" -version = "0.9.0" +version = "0.18.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 = "2021" -rust-version = "1.65" +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"] @@ -19,88 +20,241 @@ exclude = ["*.proto"] [package.metadata.cargo-udeps.ignore] development = ["zcash_proofs"] +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + [dependencies] -incrementalmerkletree = { version = "0.4", features = ["legacy-api"] } -zcash_address = { version = "0.3", path = "../components/zcash_address" } -zcash_encoding = { version = "0.2", path = "../components/zcash_encoding" } -zcash_note_encryption = "0.4" -zcash_primitives = { version = "0.12", path = "../zcash_primitives", default-features = false } +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.2" +time = "0.3.22" +nonempty.workspace = true + +# - CSPRNG +rand_core.workspace = true # - Encodings -base64 = "0.21" -bech32 = "0.9" -bs58 = { version = "0.5", features = ["check"] } +base64.workspace = true +bech32.workspace = true +bs58.workspace = true +postcard = { workspace = true, optional = true } # - Errors -hdwallet = { version = "0.4", optional = true } +bip32 = { workspace = true, optional = true } # - Logging and metrics -memuse = "0.2" -tracing = "0.1" +memuse.workspace = true +tracing.workspace = true # - Protobuf interfaces and gRPC bindings -prost = "0.11" -tonic = { version = "0.9", optional = true } +hex.workspace = true +prost.workspace = true +tonic = { workspace = true, optional = true, features = ["prost", "codegen"] } # - Secret management -secrecy = "0.8" -subtle = "2.2.3" +secrecy.workspace = true +subtle.workspace = true # - Shielded protocols -bls12_381 = "0.8" -group = "0.13" -orchard = { version = "0.5", default-features = false } +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 -proptest = { version = "1.0.0", optional = true } +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 types: `arti_client::DormantMode`, `fs_mistrust::MistrustBuilder`. +# -- 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 } +fs-mistrust = { 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 = { version = "1", optional = true } -percent-encoding = "2.1.0" +byteorder = { workspace = true, optional = true } +percent-encoding.workspace = true # - Scanning -crossbeam-channel = "0.5" -rayon = "1.5" +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 } + +# Workaround for https://anonticket.torproject.org/user/projects/arti/issues/pending/4028/ +time-core.workspace = true [build-dependencies] -tonic-build = "0.9" -which = "4" +tonic-build = { workspace = true, features = ["prost"] } +which = "7" [dev-dependencies] -assert_matches = "1.5" +ambassador.workspace = true +assert_matches.workspace = true gumdrop = "0.8" -hex = "0.4" -jubjub = "0.10" -proptest = "1.0.0" -rand_core = "0.6" -rand_xorshift = "0.3" -tempfile = "3.5.0" -zcash_proofs = { version = "0.12", path = "../zcash_proofs", default-features = false } -zcash_address = { version = "0.3", path = "../components/zcash_address", features = ["test-dependencies"] } +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] -lightwalletd-tonic = ["tonic"] -transparent-inputs = ["hdwallet", "zcash_primitives/transparent-inputs"] +## 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:fs-mistrust", + "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 = [ - "proptest", - "orchard/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", - "incrementalmerkletree/test-dependencies" + "zcash_proofs/bundled-prover", + "zcash_protocol/local-consensus", + "incrementalmerkletree/test-dependencies", ] -unstable = ["byteorder"] + +## 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 eb8a6cd613..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 @@ -13,16 +19,6 @@ Licensed under either of at your option. -Downstream code forks should note that 'zcash_client_backend' depends on the -'orchard' crate, which is licensed under the -[Bootstrap Open Source License](https://github.com/zcash/orchard/blob/main/LICENSE-BOSL). -A license exception is provided allowing some derived works that are linked or -combined with the 'orchard' crate to be copied or distributed under the original -licenses (in this case MIT / Apache 2.0), provided that the included portions of -the 'orchard' code remain subject to BOSL. -See https://github.com/zcash/orchard/blob/main/COPYING for details of which -derived works can make use of this exception. - ### Contribution Unless you explicitly state otherwise, any contribution intentionally diff --git a/zcash_client_backend/build.rs b/zcash_client_backend/build.rs index 271b0f781e..e2503554a3 100644 --- a/zcash_client_backend/build.rs +++ b/zcash_client_backend/build.rs @@ -5,7 +5,8 @@ use std::path::{Path, PathBuf}; const COMPACT_FORMATS_PROTO: &str = "proto/compact_formats.proto"; -#[cfg(feature = "lightwalletd-tonic")] +const PROPOSAL_PROTO: &str = "proto/proposal.proto"; + const SERVICE_PROTO: &str = "proto/service.proto"; fn main() -> io::Result<()> { @@ -40,38 +41,52 @@ fn build() -> io::Result<()> { "src/proto/compact_formats.rs", )?; - #[cfg(feature = "lightwalletd-tonic")] - { - // Build the gRPC types and client. - tonic_build::configure() - .build_server(false) - .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(&[SERVICE_PROTO], &["proto/"])?; + // 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/"])?; - // 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")?; - } + // 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 aa77e6e9ab..6e0b35ec21 100644 --- a/zcash_client_backend/examples/diversify-address.rs +++ b/zcash_client_backend/examples/diversify-address.rs @@ -1,9 +1,8 @@ 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) @@ -17,16 +16,11 @@ fn parse_viewing_key(s: &str) -> Result<(ExtendedFullViewingKey, bool), &'static 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)] diff --git a/zcash_client_backend/proto/compact_formats.proto b/zcash_client_backend/proto/compact_formats.proto index 077537c606..b00ab1291a 100644 --- a/zcash_client_backend/proto/compact_formats.proto +++ b/zcash_client_backend/proto/compact_formats.proto @@ -10,20 +10,31 @@ 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; // 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 + 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. @@ -46,25 +57,25 @@ message CompactTx { 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) + bytes nf = 1; // Nullifier (see the Zcash protocol specification) } -// output encodes the `cmu` field, `ephemeralKey` field, and a 52-byte prefix of the -// `encCiphertext` field of a Sapling Output Description. These fields are described in -// section 7.4 of the Zcash protocol spec: -// https://zips.z.cash/protocol/protocol.pdf#outputencodingandconsensus -// Total size is 116 bytes. +// 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 + bytes cmu = 1; // Note commitment u-coordinate. + bytes ephemeralKey = 2; // Ephemeral public key. + bytes ciphertext = 3; // First 52 bytes of ciphertext. } -// https://github.com/zcash/zips/blob/main/zip-0225.rst#orchard-action-description-orchardaction -// (but not all fields are needed) +// 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 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 index d7f11dcd69..ee819ecbe4 100644 --- a/zcash_client_backend/proto/service.proto +++ b/zcash_client_backend/proto/service.proto @@ -34,6 +34,9 @@ message TxFilter { // 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) @@ -70,6 +73,7 @@ message LightdInfo { 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/" + string donationAddress = 15; // Zcash donation UA address } // TransparentAddressBlockFilter restricts the results to the given address @@ -118,6 +122,22 @@ message TreeState { 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 { @@ -138,12 +158,16 @@ message GetAddressUtxosReplyList { } service CompactTxStreamer { - // Return the height of the tip of the best chain + // 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) {} @@ -177,6 +201,10 @@ service CompactTxStreamer { 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) {} diff --git a/zcash_client_backend/src/address.rs b/zcash_client_backend/src/address.rs deleted file mode 100644 index 87e1ac0fe6..0000000000 --- a/zcash_client_backend/src/address.rs +++ /dev/null @@ -1,314 +0,0 @@ -//! Structs for handling supported address types. - -use std::convert::TryFrom; - -use zcash_address::{ - unified::{self, Container, Encoding}, - ConversionError, Network, ToAddress, TryFromRawAddress, ZcashAddress, -}; -use zcash_primitives::{ - consensus, - legacy::TransparentAddress, - sapling::PaymentAddress, - zip32::{AccountId, DiversifierIndex}, -}; - -pub struct AddressMetadata { - account: AccountId, - diversifier_index: DiversifierIndex, -} - -impl AddressMetadata { - pub fn new(account: AccountId, diversifier_index: DiversifierIndex) -> Self { - Self { - account, - diversifier_index, - } - } - - pub fn account(&self) -> AccountId { - self.account - } - - pub fn diversifier_index(&self) -> &DiversifierIndex { - &self.diversifier_index - } -} - -/// A Unified Address. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct UnifiedAddress { - orchard: Option, - sapling: Option, - transparent: Option, - unknown: Vec<(u32, Vec)>, -} - -impl TryFrom for UnifiedAddress { - type Error = &'static str; - - fn try_from(ua: unified::Address) -> Result { - let mut orchard = None; - let mut sapling = None; - 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 = ua - .items_as_parsed() - .iter() - .filter_map(|receiver| match receiver { - unified::Receiver::Orchard(data) => { - Option::from(orchard::Address::from_raw_address_bytes(data)) - .ok_or("Invalid Orchard receiver in Unified Address") - .map(|addr| { - orchard = Some(addr); - None - }) - .transpose() - } - unified::Receiver::Sapling(data) => PaymentAddress::from_bytes(data) - .ok_or("Invalid Sapling receiver in Unified Address") - .map(|pa| { - sapling = Some(pa); - None - }) - .transpose(), - unified::Receiver::P2pkh(data) => { - transparent = Some(TransparentAddress::PublicKey(*data)); - None - } - unified::Receiver::P2sh(data) => { - transparent = Some(TransparentAddress::Script(*data)); - None - } - unified::Receiver::Unknown { typecode, data } => { - Some(Ok((*typecode, data.clone()))) - } - }) - .collect::>()?; - - Ok(Self { - orchard, - 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( - orchard: Option, - sapling: Option, - transparent: Option, - ) -> Option { - if orchard.is_some() || sapling.is_some() { - Some(Self { - orchard, - sapling, - transparent, - unknown: vec![], - }) - } else { - // UAs require at least one shielded receiver. - None - } - } - - /// Returns the Orchard receiver within this Unified Address, if any. - pub fn orchard(&self) -> Option<&orchard::Address> { - self.orchard.as_ref() - } - - /// Returns the Sapling receiver within this Unified Address, if any. - pub fn sapling(&self) -> Option<&PaymentAddress> { - self.sapling.as_ref() - } - - /// Returns the transparent receiver within this Unified Address, if any. - pub fn transparent(&self) -> Option<&TransparentAddress> { - self.transparent.as_ref() - } - - fn to_address(&self, net: Network) -> ZcashAddress { - let ua = unified::Address::try_from_items( - self.unknown - .iter() - .map(|(typecode, data)| unified::Receiver::Unknown { - typecode: *typecode, - data: data.clone(), - }) - .chain(self.transparent.as_ref().map(|taddr| match taddr { - TransparentAddress::PublicKey(data) => unified::Receiver::P2pkh(*data), - TransparentAddress::Script(data) => unified::Receiver::P2sh(*data), - })) - .chain( - self.sapling - .as_ref() - .map(|pa| pa.to_bytes()) - .map(unified::Receiver::Sapling), - ) - .chain( - self.orchard - .as_ref() - .map(|addr| addr.to_raw_address_bytes()) - .map(unified::Receiver::Orchard), - ) - .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.address_network().expect("Unrecognized network")) - .to_string() - } -} - -/// An address that funds can be sent to. -// TODO: rename to ParsedAddress -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum RecipientAddress { - Shielded(PaymentAddress), - Transparent(TransparentAddress), - Unified(UnifiedAddress), -} - -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 From for RecipientAddress { - fn from(addr: UnifiedAddress) -> Self { - RecipientAddress::Unified(addr) - } -} - -impl TryFromRawAddress for RecipientAddress { - type Error = &'static str; - - 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(RecipientAddress::from) - } - - fn try_from_raw_transparent_p2pkh( - data: [u8; 20], - ) -> Result> { - Ok(TransparentAddress::PublicKey(data).into()) - } - - fn try_from_raw_transparent_p2sh(data: [u8; 20]) -> Result> { - Ok(TransparentAddress::Script(data).into()) - } -} - -impl RecipientAddress { - pub fn decode(params: &P, s: &str) -> Option { - let addr = ZcashAddress::try_from_encoded(s).ok()?; - addr.convert_if_network(params.address_network().expect("Unrecognized network")) - .ok() - } - - pub fn encode(&self, params: &P) -> String { - let net = params.address_network().expect("Unrecognized network"); - - match self { - RecipientAddress::Shielded(pa) => ZcashAddress::from_sapling(net, pa.to_bytes()), - RecipientAddress::Transparent(addr) => match addr { - TransparentAddress::PublicKey(data) => { - ZcashAddress::from_transparent_p2pkh(net, *data) - } - TransparentAddress::Script(data) => ZcashAddress::from_transparent_p2sh(net, *data), - }, - RecipientAddress::Unified(ua) => ua.to_address(net), - } - .to_string() - } -} - -#[cfg(test)] -mod tests { - use zcash_address::test_vectors; - use zcash_primitives::consensus::MAIN_NETWORK; - - use super::{RecipientAddress, UnifiedAddress}; - use crate::keys::sapling; - - #[test] - fn ua_round_trip() { - let orchard = { - let sk = orchard::keys::SpendingKey::from_zip32_seed(&[0; 32], 0, 0).unwrap(); - let fvk = orchard::keys::FullViewingKey::from(&sk); - Some(fvk.address_at(0u32, orchard::keys::Scope::External)) - }; - - let sapling = { - let extsk = sapling::spending_key(&[0; 32], 0, 0.into()); - let dfvk = extsk.to_diversifiable_full_viewing_key(); - Some(dfvk.default_address().1) - }; - - let transparent = { None }; - - let ua = UnifiedAddress::from_receivers(orchard, sapling, transparent).unwrap(); - - let addr = RecipientAddress::Unified(ua); - let addr_str = addr.encode(&MAIN_NETWORK); - assert_eq!( - RecipientAddress::decode(&MAIN_NETWORK, &addr_str), - Some(addr) - ); - } - - #[test] - fn ua_parsing() { - for tv in test_vectors::UNIFIED { - match RecipientAddress::decode(&MAIN_NETWORK, tv.unified_addr) { - Some(RecipientAddress::Unified(ua)) => { - assert_eq!( - ua.transparent().is_some(), - tv.p2pkh_bytes.is_some() || tv.p2sh_bytes.is_some() - ); - assert_eq!(ua.sapling().is_some(), tv.sapling_raw_addr.is_some()); - assert_eq!(ua.orchard().is_some(), 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_client_backend/src/data_api.rs b/zcash_client_backend/src/data_api.rs index 1b3dff2a7c..8fca78a399 100644 --- a/zcash_client_backend/src/data_api.rs +++ b/zcash_client_backend/src/data_api.rs @@ -1,37 +1,1272 @@ -//! 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 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_keys::{ + address::{Address, UnifiedAddress}, + keys::{ + UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedIncomingViewingKey, UnifiedSpendingKey, + }, +}; +use zcash_primitives::{block::BlockHash, transaction::Transaction}; +use zcash_protocol::{ + consensus::BlockHeight, + memo::{Memo, MemoBytes}, + value::{BalanceError, Zatoshis}, + ShieldedProtocol, TxId, +}; +use zip32::{fingerprint::SeedFingerprint, DiversifierIndex}; + +use self::{ + chain::{ChainState, CommitmentTreeRoot}, + scanning::ScanRange, +}; +use crate::{ + decrypt::DecryptedOutput, + proto::service::TreeState, + wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput, WalletTx}, +}; + +#[cfg(feature = "transparent-inputs")] +use { + crate::wallet::TransparentAddressMetadata, + std::ops::Range, + std::time::SystemTime, + transparent::{ + address::TransparentAddress, + bundle::OutPoint, + keys::{NonHardenedChildIndex, TransparentKeyScope}, + }, +}; + +#[cfg(feature = "test-dependencies")] +use ambassador::delegatable_trait; + +#[cfg(any(test, feature = "test-dependencies"))] +use zcash_protocol::consensus::NetworkUpgrade; + +pub mod chain; +pub mod error; +pub mod scanning; +pub mod wallet; + +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing; + +/// The height of subtree roots in the Sapling note commitment tree. +/// +/// 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; + +/// 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; + +/// An enumeration of constraints that can be applied when querying for nullifiers for notes +/// belonging to the wallet. +pub enum NullifierQuery { + Unspent, + All, +} + +/// An intent of representing spendable value to reach a certain targeted +/// amount. `AtLeast(Zatoshis)` refers to the amount of `Zatoshis` that can cover +/// at minimum the given zatoshis that is conformed by the sum of spendable notes. +/// +/// Discussion: why not just use ``Zatoshis``? +/// +/// the `Zatoshis` value isn't enough to explain intent when seeking to match a +/// given a given amount. Is the value expressed in `Zatoshis` the ceiling value +/// or the minimum value of a given spend intent? How would you express that the +/// value spend intent is "as much as possible" without knowing the value upfront? +#[derive(Debug, Clone, Copy)] +pub enum TargetValue { + AtLeast(Zatoshis), +} + +/// 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 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 + } + + /// 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 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 + } + + /// 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(()) + } + + /// 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 + } + + /// 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 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") + } +} + +/// 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, + + /// The value of unspent Orchard outputs belonging to the account. + orchard_balance: Balance, + + /// The value of all unspent transparent outputs belonging to the account. + unshielded_balance: Balance, +} + +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, + }; + + 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") + } +} + +/// Source metadata for a ZIP 32-derived key. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Zip32Derivation { + seed_fingerprint: SeedFingerprint, + account_index: zip32::AccountId, +} + +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 + } +} + +/// 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, +} + +/// 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, + }, + + /// An account imported from a viewing key. + Imported { + purpose: AccountPurpose, + key_source: Option, + }, +} + +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, + } + } + + /// 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. + /// + /// 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. + /// + /// 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; +} + +#[cfg(any(test, feature = "test-dependencies"))] +impl Account for (A, UnifiedFullViewingKey) { + type AccountId = A; + + 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(any(test, feature = "test-dependencies"))] +impl Account for (A, UnifiedIncomingViewingKey) { + type AccountId = A; + + fn id(&self) -> A { + self.0 + } + + fn name(&self) -> Option<&str> { + None + } + + fn source(&self) -> &AccountSource { + &AccountSource::Imported { + purpose: AccountPurpose::ViewOnly, + key_source: None, + } + } + + fn ufvk(&self) -> Option<&UnifiedFullViewingKey> { + None + } + + fn uivk(&self) -> UnifiedIncomingViewingKey { + self.1.clone() + } +} + +/// Information about an address in the wallet. +pub struct AddressInfo { + address: Address, + diversifier_index: DiversifierIndex, + #[cfg(feature = "transparent-inputs")] + transparent_key_scope: Option, +} + +impl AddressInfo { + /// Constructs an `AddressInfo` from its constituent parts. + pub fn from_parts( + address: Address, + diversifier_index: DiversifierIndex, + #[cfg(feature = "transparent-inputs")] transparent_key_scope: Option, + ) -> Option { + // Only allow `transparent_key_scope` to be set for transparent addresses. + #[cfg(feature = "transparent-inputs")] + let valid = transparent_key_scope.is_none() + || matches!(address, Address::Transparent(_) | Address::Tex(_)); + #[cfg(not(feature = "transparent-inputs"))] + let valid = true; + + valid.then_some(Self { + address, + diversifier_index, + #[cfg(feature = "transparent-inputs")] + transparent_key_scope, + }) + } + + /// Returns the address this information is about. + pub fn address(&self) -> &Address { + &self.address + } + + /// Returns the diversifier index the address was derived at. + pub fn diversifier_index(&self) -> DiversifierIndex { + self.diversifier_index + } + + /// Returns the key scope if this is a transparent address. + #[cfg(feature = "transparent-inputs")] + pub fn transparent_key_scope(&self) -> Option { + self.transparent_key_scope + } +} + +/// A polymorphic ratio type, usually used for rational numbers. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Ratio { + numerator: T, + denominator: T, +} + +impl Ratio { + /// Constructs a new Ratio from a numerator and a denominator. + pub fn new(numerator: T, denominator: T) -> Self { + Self { + numerator, + denominator, + } + } + + /// Returns the numerator of the ratio. + pub fn numerator(&self) -> &T { + &self.numerator + } + + /// Returns the denominator of the ratio. + pub fn denominator(&self) -> &T { + &self.denominator + } +} + +/// 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>, +} + +impl Progress { + /// Constructs a new progress value from its constituent parts. + pub fn new(scan: Ratio, recovery: Option>) -> Self { + Self { scan, recovery } + } + + /// 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 + } + + /// 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 + } +} + +/// 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, +} + +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, + } + } + + /// Returns the balances of accounts in the wallet, keyed by account ID. + pub fn account_balances(&self) -> &HashMap { + &self.account_balances + } + + /// Returns the height of the current chain tip. + pub fn chain_tip_height(&self) -> BlockHeight { + self.chain_tip_height + } + + /// 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 + } + + /// 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 + } + + /// 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 + } + + /// 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 type describing the mined-ness of transactions that should be returned in response to a +/// [`TransactionDataRequest`]. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg(feature = "transparent-inputs")] +pub enum TransactionStatusFilter { + /// Only mined transactions should be returned. + Mined, + /// Only mempool transactions should be returned. + Mempool, + /// Both mined transactions and transactions in the mempool should be returned. + All, +} + +/// A type used to filter transactions to be returned in response to a [`TransactionDataRequest`], +/// in terms of the spentness of the transaction's transparent outputs. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg(feature = "transparent-inputs")] +pub enum OutputStatusFilter { + /// Only transactions that have currently-unspent transparent outputs should be returned. + Unspent, + /// All transactions corresponding to the data request should be returned, irrespective of + /// whether or not those transactions produce transparent outputs that are currently unspent. + All, +} + +/// 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")] + TransactionsInvolvingAddress { + /// The address to request transactions and/or UTXOs for. + address: TransparentAddress, + /// Only transactions mined at heights greater than or equal to this height should be + /// returned. + block_range_start: BlockHeight, + /// Only transactions mined at heights less than this height should be returned. + block_range_end: Option, + /// If a `request_at` time is set, the caller evaluating this request should attempt to + /// retrieve transaction data related to the specified address at a time that is as close + /// as practical to the specified instant, and in a fashion that decorrelates this request + /// to a light wallet server from other requests made by the same caller. + /// + /// This may be ignored by callers that are able to satisfy the request without exposing + /// correlations between addresses to untrusted parties; for example, a wallet application + /// that uses a private, trusted-for-privacy supplier of chain data can safely ignore this + /// field. + request_at: Option, + /// The caller should respond to this request only with transactions that conform to the + /// specified transaction status filter. + tx_status_filter: TransactionStatusFilter, + /// The caller should respond to this request only with transactions containing outputs + /// that conform to the specified output status filter. + output_status_filter: OutputStatusFilter, + }, +} + +/// 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![], + ) + } + + /// 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(), + } + } -use std::cmp; -use std::collections::HashMap; -use std::fmt::Debug; + /// 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) + } -use secrecy::SecretVec; -use zcash_primitives::{ - block::BlockHash, - consensus::BlockHeight, - legacy::TransparentAddress, - memo::{Memo, MemoBytes}, - sapling, - transaction::{ - components::{amount::Amount, OutPoint}, - Transaction, TxId, + 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, }, - zip32::{AccountId, ExtendedFullViewingKey}, -}; +} -use crate::{ - address::{AddressMetadata, UnifiedAddress}, - decrypt::DecryptedOutput, - keys::{UnifiedFullViewingKey, UnifiedSpendingKey}, - wallet::{ReceivedSaplingNote, WalletTransparentOutput, WalletTx}, -}; +impl NoteFilter { + /// Constructs a [`NoteFilter::Combine`] query node. + pub fn combine(l: NoteFilter, r: NoteFilter) -> Self { + Self::Combine(Box::new(l), Box::new(r)) + } -pub mod chain; -pub mod error; -pub mod wallet; + /// Constructs a [`NoteFilter::Attempt`] query node. + pub fn attempt(condition: NoteFilter, fallback: NoteFilter) -> Self { + Self::Attempt { + condition: Box::new(condition), + fallback: Box::new(fallback), + } + } +} -pub enum NullifierQuery { - Unspent, - All, +/// 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: TargetValue, + 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. @@ -39,219 +1274,733 @@ pub enum NullifierQuery { /// 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 produced by a wallet backend. - type Error; + /// The type of errors that may be generated when querying a wallet data store. + type Error: Debug; - /// Backend-specific note identifier. + /// The type of the account identifier. /// - /// For example, this might be a database identifier type - /// or a UUID. - type NoteRef: Copy + Debug + Eq + Ord; + /// 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; - /// Backend-specific transaction identifier. + /// 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. /// - /// 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 + Eq + Ord; + /// 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; - /// Returns the minimum and maximum block heights for stored blocks. + /// Checks whether the given seed is relevant to any of the derived accounts (where + /// [`Account::source`] is [`AccountSource::Derived`]) in the wallet. /// - /// This will return `Ok(None)` if no block data is present in the database. - fn block_height_extrema(&self) -> Result, Self::Error>; + /// 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 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. + /// Returns the account corresponding to a given [`UnifiedFullViewingKey`], if any. + fn get_account_for_ufvk( + &self, + ufvk: &UnifiedFullViewingKey, + ) -> Result, Self::Error>; + + /// Returns information about every address tracked for this account. + fn list_addresses(&self, account: Self::AccountId) -> Result, Self::Error>; + + /// Returns the most recently generated unified address for the specified account that conforms + /// to the specified address filter, if the account identifier specified refers to a valid + /// account for this wallet. /// - /// This will return `Ok(None)` if no block data is present in the database. - fn get_target_and_anchor_heights( + /// This will return `Ok(None)` if no previously generated address conforms to the specified + /// request. + fn get_last_generated_address_matching( + &self, + account: Self::AccountId, + address_filter: UnifiedAddressRequest, + ) -> 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> { - self.block_height_extrema().map(|heights| { - heights.map(|(min_height, max_height)| { - let target_height = max_height + 1; - - // Select an anchor min_confirmations 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(min_confirmations), - u32::from(min_height), - )); - - (target_height, anchor_height) - }) - }) - } + ) -> Result>, Self::Error>; - /// Returns the minimum block height corresponding to an unspent note in the wallet. - fn get_min_unspent_height(&self) -> 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 block hash for the block at the maximum height known - /// in stored data. + /// 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_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()) - } + 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 the most recently generated unified address for the specified account, if the - /// account identifier specified refers to a valid account for this wallet. + /// 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. /// - /// This will return `Ok(None)` if the account identifier does not correspond to a known - /// account. - fn get_current_address( + /// 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, - account: AccountId, - ) -> Result, Self::Error>; + query: NullifierQuery, + ) -> Result, Self::Error>; - /// Returns all unified full viewing keys known to this wallet. - fn get_unified_full_viewing_keys( + /// 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, - ) -> Result, Self::Error>; + query: NullifierQuery, + ) -> Result, Self::Error>; - /// Returns the account id corresponding to a given [`UnifiedFullViewingKey`], if any. - fn get_account_for_ufvk( + /// 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, - ufvk: &UnifiedFullViewingKey, - ) -> Result, Self::Error>; + _account: Self::AccountId, + _include_change: bool, + ) -> Result>, Self::Error> { + Ok(HashMap::new()) + } - /// Checks whether the specified extended full viewing key is associated with the account. - fn is_valid_account_extfvk( + /// 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: AccountId, - extfvk: &ExtendedFullViewingKey, - ) -> Result; + _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, true)?.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, true)?.get(address) { + result.clone() + } else { + self.get_known_ephemeral_addresses(account, None)? + .into_iter() + .find(|(known_addr, _)| known_addr == address) + .map(|(_, metadata)| metadata) + }, + ) + } + + /// Returns the maximum block height at which a transparent output belonging to the wallet has + /// been observed. + /// + /// We must start looking for UTXOs for addresses within the current gap limit as of the block + /// height at which they might have first been revealed. This would have occurred when the gap + /// advanced as a consequence of a transaction being mined. The address at the start of the current + /// gap was potentially first revealed after the address at index `gap_start - (gap_limit + 1)` + /// received an output in a mined transaction; therefore, we take that height to be where we + /// should start searching for UTXOs. + #[cfg(feature = "transparent-inputs")] + fn utxo_query_height(&self, account: Self::AccountId) -> Result; - /// Returns the wallet balance for an account as of the specified block height. + /// 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 the backend's configured gap limit worth + /// of 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). /// - /// 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( + /// - 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: AccountId, - anchor_height: BlockHeight, - ) -> Result; + _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 + } +} - /// 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. Returns `Ok(None)` if the note - /// is known to the wallet but memo data has not yet been populated for that - /// note. - fn get_memo(&self, id_note: Self::NoteRef) -> Result, Self::Error>; +/// 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)>, +} - /// Returns a transaction. - fn get_transaction(&self, id_tx: Self::TxRef) -> Result; +/// 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, +} - /// Returns the note commitment tree at the specified block height. - fn get_commitment_tree( - &self, +impl ScannedBlock { + /// Constructs a new `ScannedBlock` + pub(crate) fn from_parts( block_height: BlockHeight, - ) -> Result, Self::Error>; + 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 incremental witnesses as of the specified block height. - #[allow(clippy::type_complexity)] - fn get_witnesses( - &self, - block_height: BlockHeight, - ) -> Result, Self::Error>; + /// Returns the height of the block that was scanned. + pub fn height(&self) -> BlockHeight { + self.block_height + } - /// Returns the nullifiers for 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 block hash of the block that was scanned. + pub fn block_hash(&self) -> BlockHash { + self.block_hash + } - /// Return all unspent Sapling notes. - fn get_spendable_sapling_notes( - &self, - account: AccountId, - anchor_height: BlockHeight, - exclude: &[Self::NoteRef], - ) -> Result>, Self::Error>; + /// 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 a list of spendable Sapling notes sufficient to cover the specified target value, - /// if possible. - fn select_spendable_sapling_notes( - &self, - account: AccountId, - target_value: Amount, - anchor_height: BlockHeight, - exclude: &[Self::NoteRef], - ) -> Result>, Self::Error>; + /// Returns the list of transactions from this block that are relevant to the wallet. + pub fn transactions(&self) -> &[WalletTx] { + &self.transactions + } - /// Returns the set of all transparent receivers associated with the given account. - /// - /// The set contains all transparent receivers that are known to have been derived - /// under this account. Wallets should scan the chain for UTXOs sent to these - /// receivers. - fn get_transparent_receivers( - &self, - account: AccountId, - ) -> Result, Self::Error>; + /// Returns the Sapling note commitment tree and nullifier data for the block. + pub fn sapling(&self) -> &ScannedBundles { + &self.sapling + } - /// Returns a list of unspent transparent UTXOs that appear in the chain at heights up to and - /// including `max_height`. - fn get_unspent_transparent_outputs( + /// Returns the Orchard note commitment tree and nullifier data for the block. + #[cfg(feature = "orchard")] + pub fn orchard( &self, - address: &TransparentAddress, - max_height: BlockHeight, - exclude: &[OutPoint], - ) -> Result, Self::Error>; + ) -> &ScannedBundles { + &self.orchard + } - /// Returns a mapping from transparent receiver to not-yet-shielded UTXO balance, - /// for each address associated with a nonzero balance. - fn get_transparent_balances( - &self, - account: AccountId, - max_height: BlockHeight, - ) -> Result, Self::Error>; -} + /// 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, + } + } -/// 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 PrunedBlock<'a> { - pub block_height: BlockHeight, - pub block_hash: BlockHash, - pub block_time: u32, - pub commitment_tree: &'a sapling::CommitmentTree, - pub transactions: &'a Vec>, + /// 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 outputs. +/// 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> { - pub tx: &'a Transaction, - pub sapling_outputs: &'a Vec>, +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. @@ -259,63 +2008,116 @@ pub struct DecryptedTransaction<'a> { /// 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, - pub account: AccountId, - pub outputs: Vec, - pub fee_amount: Amount, +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")] - pub utxos_spent: Vec, + utxos_spent: &'a [OutPoint], } -/// A value pool to which the wallet supports sending transaction outputs. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum PoolType { - /// The transparent value pool - Transparent, - /// The Sapling value pool - Sapling, - // TODO: Orchard -} +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, + } + } -/// A type that represents the recipient of a transaction output; a recipient address (and, for -/// unified addresses, the pool to which the payment is sent) in the case of outgoing output, or an -/// internal account ID and the pool to which funds were sent in the case of a wallet-internal -/// output. -#[derive(Debug, Clone)] -pub enum Recipient { - Transparent(TransparentAddress), - Sapling(sapling::PaymentAddress), - Unified(UnifiedAddress, PoolType), - InternalAccount(AccountId, PoolType), + /// 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 + } } -/// A type that represents an output (either Sapling or transparent) that was sent by the wallet. -pub struct SentTransactionOutput { +/// 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: Amount, + recipient: Recipient, + value: Zatoshis, memo: Option, - sapling_change_to: Option<(AccountId, sapling::Note)>, } -impl SentTransactionOutput { +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: Amount, + recipient: Recipient, + value: Zatoshis, memo: Option, - sapling_change_to: Option<(AccountId, sapling::Note)>, ) -> Self { Self { output_index, recipient, value, memo, - sapling_change_to, } } + /// 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 @@ -325,324 +2127,579 @@ impl SentTransactionOutput { pub fn output_index(&self) -> usize { self.output_index } - /// Returns the recipient address of the transaction, or the account id for wallet-internal - /// transactions. - pub fn recipient(&self) -> &Recipient { + /// 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) -> Amount { + pub fn value(&self) -> Zatoshis { self.value } - /// Returns the memo that was attached to the output, if any. + /// 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 t decrypted note, if the sent output belongs to this wallet - pub fn sapling_change_to(&self) -> Option<&(AccountId, sapling::Note)> { - self.sapling_change_to.as_ref() + /// 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. +/// 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. + /// 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`]. + /// 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. /// - /// If `seed` was imported from a backup and this method is being used to restore a - /// previous wallet state, you should use this method to add all of the desired - /// accounts before scanning the chain from the seed's birthday height. + /// The ZIP-32 account index may be obtained by calling [`WalletRead::get_account`] + /// with the returned account identifier. /// - /// By convention, wallets should only allow a new account to be generated after funds - /// have been received by the currently-available account (in order to enable - /// 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. + /// - `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, - ) -> Result<(AccountId, UnifiedSpendingKey), Self::Error>; + account_index: zip32::AccountId, + birthday: &AccountBirthday, + key_source: Option<&str>, + ) -> Result<(Self::Account, UnifiedSpendingKey), Self::Error>; - /// Generates and persists the next available diversified address, given the current - /// addresses known to the wallet. + /// 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, persists, and marks as exposed the next available diversified address for the + /// specified account, given the current addresses known to the wallet. /// /// Returns `Ok(None)` if the account identifier does not correspond to a known /// account. fn get_next_available_address( &mut self, - account: AccountId, - ) -> Result, Self::Error>; - - /// Updates the state of the wallet database by persisting the provided - /// block information, along with the updated witness data that was - /// produced when scanning the block for transactions pertaining to - /// this wallet. - #[allow(clippy::type_complexity)] - fn advance_by_block( - &mut self, - block: &PrunedBlock, - updated_witnesses: &[(Self::NoteRef, sapling::IncrementalWitness)], - ) -> Result, Self::Error>; + account: Self::AccountId, + request: UnifiedAddressRequest, + ) -> Result, Self::Error>; - /// Caches a decrypted transaction in the persistent wallet store. - fn store_decrypted_tx( + /// Generates, persists, and marks as exposed a diversified address for the specified account + /// at the provided diversifier index. + /// + /// Returns `Ok(None)` in the case that it is not possible to generate an address conforming + /// to the provided request at the specified diversifier index. Such a result might arise from + /// the diversifier index not being valid for a [`ReceiverRequirement::Require`]'ed receiver. + /// Some implementations of this trait may return `Err(_)` in some cases to expose more + /// information, which is only accessible in a backend-specific context. + /// + /// Address generation should fail if an address has already been exposed for the given + /// diversifier index and the given request produced an address having different receivers than + /// what was originally exposed. + /// + /// # WARNINGS + /// If an address generated using this method has a transparent receiver and the + /// chosen diversifier index would be outside the wallet's internally-configured gap limit, + /// funds sent to these address are **likely to not be discovered on recovery from seed**. It + /// up to the caller of this method to either ensure that they only request transparent + /// receivers with indices within the range of a reasonable gap limit, or that they ensure that + /// their wallet provides backup facilities that can be used to ensure that funds sent to such + /// addresses are recoverable after a loss of wallet data. + /// + /// [`ReceiverRequirement::Require`]: zcash_keys::keys::ReceiverRequirement::Require + fn get_address_for_index( &mut self, - received_tx: DecryptedTransaction, - ) -> Result; - - /// Saves information about a transaction that was constructed and sent by the wallet to the - /// persistent wallet store. - fn store_sent_tx(&mut self, sent_tx: &SentTransaction) -> Result; + account: Self::AccountId, + diversifier_index: DiversifierIndex, + request: UnifiedAddressRequest, + ) -> Result, Self::Error>; - /// Truncates the wallet database to the specified height. + /// Updates the wallet's view of the blockchain. /// - /// 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. + /// 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. /// - /// 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. - /// - /// There may be restrictions on heights to which it is possible to truncate. - fn truncate_to_height(&mut self, block_height: BlockHeight) -> Result<(), Self::Error>; + /// ### 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; -} - -#[cfg(feature = "test-dependencies")] -pub mod testing { - use secrecy::{ExposeSecret, SecretVec}; - use std::collections::HashMap; - - use zcash_primitives::{ - block::BlockHash, - consensus::{BlockHeight, Network}, - legacy::TransparentAddress, - memo::Memo, - sapling, - transaction::{ - components::{Amount, OutPoint}, - Transaction, TxId, - }, - zip32::{AccountId, ExtendedFullViewingKey}, - }; - - use crate::{ - address::{AddressMetadata, UnifiedAddress}, - keys::{UnifiedFullViewingKey, UnifiedSpendingKey}, - wallet::{ReceivedSaplingNote, WalletTransparentOutput}, - }; - - use super::{ - DecryptedTransaction, NullifierQuery, PrunedBlock, SentTransaction, WalletRead, WalletWrite, - }; - - pub struct MockWalletDb { - pub network: Network, - } - - impl WalletRead for MockWalletDb { - type Error = (); - type NoteRef = u32; - type TxRef = TxId; - - fn block_height_extrema(&self) -> Result, Self::Error> { - Ok(None) - } - - fn get_min_unspent_height(&self) -> Result, Self::Error> { - Ok(None) - } - - fn get_block_hash( - &self, - _block_height: BlockHeight, - ) -> Result, Self::Error> { - Ok(None) - } - - fn get_tx_height(&self, _txid: TxId) -> Result, Self::Error> { - Ok(None) - } - - fn get_current_address( - &self, - _account: AccountId, - ) -> Result, Self::Error> { - Ok(None) - } - - fn get_unified_full_viewing_keys( - &self, - ) -> Result, Self::Error> { - Ok(HashMap::new()) - } - - fn get_account_for_ufvk( - &self, - _ufvk: &UnifiedFullViewingKey, - ) -> Result, Self::Error> { - Ok(None) - } - - fn is_valid_account_extfvk( - &self, - _account: AccountId, - _extfvk: &ExtendedFullViewingKey, - ) -> Result { - Ok(false) - } - - fn get_balance_at( - &self, - _account: AccountId, - _anchor_height: BlockHeight, - ) -> Result { - Ok(Amount::zero()) - } - - fn get_memo(&self, _id_note: Self::NoteRef) -> Result, Self::Error> { - Ok(None) - } - - fn get_transaction(&self, _id_tx: Self::TxRef) -> Result { - Err(()) - } - - fn get_commitment_tree( - &self, - _block_height: BlockHeight, - ) -> Result, Self::Error> { - Ok(None) - } - - #[allow(clippy::type_complexity)] - fn get_witnesses( - &self, - _block_height: BlockHeight, - ) -> Result, Self::Error> { - Ok(Vec::new()) - } - - fn get_sapling_nullifiers( - &self, - _query: NullifierQuery, - ) -> Result, Self::Error> { - Ok(Vec::new()) - } - - fn get_spendable_sapling_notes( - &self, - _account: AccountId, - _anchor_height: BlockHeight, - _exclude: &[Self::NoteRef], - ) -> Result>, Self::Error> { - Ok(Vec::new()) - } - - fn select_spendable_sapling_notes( - &self, - _account: AccountId, - _target_value: Amount, - _anchor_height: BlockHeight, - _exclude: &[Self::NoteRef], - ) -> Result>, Self::Error> { - Ok(Vec::new()) - } - fn get_transparent_receivers( - &self, - _account: AccountId, - ) -> Result, Self::Error> { - Ok(HashMap::new()) - } + /// Caches a decrypted transaction in the persistent wallet store. + fn store_decrypted_tx( + &mut self, + received_tx: DecryptedTransaction, + ) -> Result<(), Self::Error>; - fn get_unspent_transparent_outputs( - &self, - _address: &TransparentAddress, - _anchor_height: BlockHeight, - _exclude: &[OutPoint], - ) -> Result, Self::Error> { - Ok(Vec::new()) - } + /// 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>; - fn get_transparent_balances( - &self, - _account: AccountId, - _max_height: BlockHeight, - ) -> Result, Self::Error> { - Ok(HashMap::new()) - } + /// 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![]) } - impl WalletWrite for MockWalletDb { - type UtxoRef = u32; - - fn create_account( - &mut self, - seed: &SecretVec, - ) -> Result<(AccountId, UnifiedSpendingKey), Self::Error> { - let account = AccountId::from(0); - UnifiedSpendingKey::from_seed(&self.network, seed.expose_secret(), account) - .map(|k| (account, k)) - .map_err(|_| ()) - } - - fn get_next_available_address( - &mut self, - _account: AccountId, - ) -> Result, Self::Error> { - Ok(None) - } - - #[allow(clippy::type_complexity)] - fn advance_by_block( - &mut self, - _block: &PrunedBlock, - _updated_witnesses: &[(Self::NoteRef, sapling::IncrementalWitness)], - ) -> Result, Self::Error> { - Ok(vec![]) - } - - fn store_decrypted_tx( - &mut self, - _received_tx: DecryptedTransaction, - ) -> Result { - Ok(TxId::from_bytes([0u8; 32])) - } - - fn store_sent_tx( - &mut self, - _sent_tx: &SentTransaction, - ) -> Result { - Ok(TxId::from_bytes([0u8; 32])) - } - - fn truncate_to_height(&mut self, _block_height: BlockHeight) -> Result<(), Self::Error> { - Ok(()) - } + /// 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>; +} - /// Adds a transparent UTXO received by the wallet to the data store. - fn put_received_transparent_utxo( - &mut self, - _output: &WalletTransparentOutput, - ) -> Result { - Ok(0) - } - } +/// 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 44736228df..d64392e288 100644 --- a/zcash_client_backend/src/data_api/chain.rs +++ b/zcash_client_backend/src/data_api/chain.rs @@ -7,19 +7,20 @@ //! # #[cfg(feature = "test-dependencies")] //! # { //! use zcash_primitives::{ -//! consensus::{BlockHeight, Network, Parameters} +//! consensus::{BlockHeight, Network, Parameters}, //! }; //! //! use zcash_client_backend::{ //! data_api::{ -//! WalletRead, WalletWrite, +//! WalletRead, WalletWrite, WalletCommitmentTrees, //! chain::{ //! BlockSource, +//! CommitmentTreeRoot, //! error::Error, //! scan_cached_blocks, -//! validate_chain, //! testing as chain_testing, //! }, +//! scanning::ScanPriority, //! testing, //! }, //! }; @@ -30,81 +31,182 @@ //! # test(); //! # } //! # -//! # fn test() -> Result<(), Error<(), Infallible, u32>> { +//! # fn test() -> Result<(), Error<(), Infallible>> { //! let network = Network::TestNetwork; //! let block_source = chain_testing::MockBlockSource; -//! let mut db_data = testing::MockWalletDb { -//! network: Network::TestNetwork -//! }; +//! let mut wallet_db = testing::MockWalletDb::new(Network::TestNetwork); //! -//! // 1) Download new CompactBlocks into block_source. +//! // 1) Download note commitment tree data from lightwalletd +//! let roots: Vec> = unimplemented!(); //! -//! // 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. -//! let max_height_hash = db_data.get_max_height_hash().map_err(Error::Wallet)?; -//! if let Err(e) = validate_chain(&block_source, max_height_hash, None) { -//! match e { -//! Error::Chain(e) => { -//! // 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 = e.at_height() - 10; +//! // 2) Pass the commitment tree data to the database. +//! wallet_db.put_sapling_subtree_roots(0, &roots).unwrap(); //! -//! // b) Rewind scanned block information. -//! db_data.truncate_to_height(rewind_height); +//! // 3) Download chain tip metadata from lightwalletd +//! let tip_height: BlockHeight = unimplemented!(); //! -//! // 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. +//! // 4) Notify the wallet of the updated chain tip. +//! wallet_db.update_chain_tip(tip_height).map_err(Error::Wallet)?; //! -//! // d) If there is some separate thread or service downloading -//! // CompactBlocks, tell it to go back and download from rewind_height -//! // onwards. -//! }, -//! e => { -//! // handle or return other errors +//! // 5) Get the suggested scan ranges from the wallet database +//! let mut scan_ranges = wallet_db.suggest_scan_ranges().map_err(Error::Wallet)?; +//! +//! // 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)?;"); +//! +//! // 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() +//! ); +//! +//! // 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); +//! +//! // Rewind to the chosen height. +//! wallet_db.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. +//! unimplemented!(); +//! } +//! Err(other) => { +//! // Handle or return other errors +//! } +//! } //! +//! // In case we updated the suggested scan ranges, now re-request. +//! scan_ranges = wallet_db.suggest_scan_ranges().map_err(Error::Wallet)?; +//! } +//! _ => { +//! // 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, &block_source, &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::convert::Infallible; +use std::ops::Range; -use zcash_primitives::{ - block::BlockHash, - consensus::{self, BlockHeight}, - sapling::{self, note_encryption::PreparedIncomingViewingKey, Nullifier}, - zip32::Scope, -}; +use incrementalmerkletree::frontier::Frontier; +use subtle::ConditionallySelectable; +use zcash_primitives::block::BlockHash; +use zcash_protocol::consensus::{self, BlockHeight}; use crate::{ - data_api::{PrunedBlock, WalletWrite}, + data_api::{NullifierQuery, WalletWrite}, proto::compact_formats::CompactBlock, - scan::BatchRunner, - wallet::WalletTx, - welding_rig::{add_block_to_runner, scan_block_with_runner}, + scanning::{scan_block_with_runners, BatchRunners, Nullifiers, ScanningKeys}, +}; + +#[cfg(feature = "sync")] +use { + super::scanning::ScanPriority, crate::data_api::scanning::ScanRange, async_trait::async_trait, }; pub mod error; -use error::{ChainError, Error}; +use error::Error; -use super::NullifierQuery; +use super::WalletRead; + +/// A struct containing metadata about a subtree root of the note commitment tree. +/// +/// 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, +} + +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. @@ -119,266 +221,485 @@ pub trait BlockSource { /// 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( + fn with_blocks( &self, from_height: Option, - limit: Option, - with_row: F, - ) -> Result<(), error::Error> + limit: Option, + with_block: F, + ) -> Result<(), error::Error> where - F: FnMut(CompactBlock) -> Result<(), error::Error>; + F: FnMut(CompactBlock) -> Result<(), error::Error>; } -/// Checks that the scanned blocks in the data database, when combined with the recent -/// `CompactBlock`s in the block_source database, form a valid chain. +/// `BlockCache` is a trait that extends `BlockSource` and defines methods for managing +/// a cache of compact blocks. /// -/// This function is built on the core assumption that the information provided in the -/// block source 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. +/// # Examples /// -/// Arguments: -/// - `block_source` Source of compact blocks -/// - `validate_from` Height & hash of last validated block; -/// - `limit` specified number of blocks that will be valididated. Callers providing -/// a `limit` argument are responsible of making subsequent calls to `validate_chain()` -/// to complete validating the remaining blocks stored on the `block_source`. If `none` -/// is provided, there will be no limit set to the validation and upper bound of the -/// validation range will be the latest height present in the `block_source`. +/// ``` +/// 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; /// -/// Returns: -/// - `Ok(())` if the combined chain is valid up to the given height -/// and block hash. -/// - `Err(Error::Chain(cause))` if the combined chain is invalid. -/// - `Err(e)` if there was an error during validation unrelated to chain validity. -pub fn validate_chain( - block_source: &BlockSourceT, - mut validate_from: Option<(BlockHeight, BlockHash)>, - limit: Option, -) -> Result<(), Error> +/// struct ExampleBlockCache { +/// cached_blocks: Arc>>, +/// } +/// +/// # 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))) +/// } +/// +/// 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()) +/// } +/// +/// async fn insert(&self, mut compact_blocks: Vec) -> Result<(), Self::Error> { +/// self.cached_blocks +/// .lock() +/// .unwrap() +/// .append(&mut compact_blocks); +/// Ok(()) +/// } +/// +/// 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); +/// }); +/// +/// // 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)) +/// ); +/// +/// // 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); +/// ``` +#[cfg(feature = "sync")] +#[async_trait] +pub trait BlockCache: BlockSource + Send + Sync where - BlockSourceT: BlockSource, + Self::Error: Send, { - // The block source 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 block - // source at the `validate_from` height, which can then be used to verify chain integrity by - // comparing against the `validate_from` hash. - - block_source.with_blocks::<_, Infallible, Infallible>( - validate_from.map(|(h, _)| h), - limit, - move |block| { - if let Some((valid_height, valid_hash)) = validate_from { - if block.height() != valid_height + 1 { - return Err(ChainError::block_height_discontinuity( - valid_height + 1, - block.height(), - ) - .into()); - } else if block.prev_hash() != valid_hash { - return Err(ChainError::prev_hash_mismatch(block.height()).into()); - } - } + /// 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>; - validate_from = Some((block.height(), block.hash())); - Ok(()) - }, - ) + /// 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>; + + /// 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>; + + /// 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(()) + } + + /// 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>; } -/// Scans at most `limit` new blocks added to the block source for any transactions received by the -/// tracked accounts. -/// -/// 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. +/// 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, +} + +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 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. +/// 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. /// -/// 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`). +/// ## Panics /// -/// Scanned blocks are required to be height-sequential. If a block is missing from the block -/// source, an error will be returned with cause [`error::Cause::BlockHeightDiscontinuity`]. -#[tracing::instrument(skip(params, block_source, data_db))] +/// 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, - limit: Option, -) -> Result<(), Error> + from_height: BlockHeight, + from_state: &ChainState, + limit: usize, +) -> Result> where ParamsT: consensus::Parameters + Send + 'static, BlockSourceT: BlockSource, DbT: WalletWrite, + ::AccountId: ConditionallySelectable + Default + Send + 'static, { - // Recall where we synced up to previously. - let mut last_height = data_db - .block_height_extrema() - .map_err(Error::Wallet)? - .map(|(_, max)| max); + assert_eq!(from_height, from_state.block_height + 1); // Fetch the UnifiedFullViewingKeys we are tracking - let ufvks = data_db + let account_ufvks = data_db .get_unified_full_viewing_keys() .map_err(Error::Wallet)?; - // TODO: Change `scan_block` to also scan Orchard. - // https://github.com/zcash/librustzcash/issues/403 - let dfvks: Vec<_> = ufvks - .iter() - .filter_map(|(account, ufvk)| ufvk.sapling().map(move |k| (account, k))) - .collect(); - - // Get the most recent CommitmentTree - let mut tree = last_height.map_or_else( - || Ok(sapling::CommitmentTree::empty()), - |h| { - data_db - .get_commitment_tree(h) - .map(|t| t.unwrap_or_else(sapling::CommitmentTree::empty)) - .map_err(Error::Wallet) - }, - )?; - - // Get most recent incremental witnesses for the notes we are tracking - let mut witnesses = last_height.map_or_else( - || Ok(vec![]), - |h| data_db.get_witnesses(h).map_err(Error::Wallet), - )?; - - // Get the nullifiers for the notes we are tracking - let mut nullifiers = data_db - .get_sapling_nullifiers(NullifierQuery::Unspent) - .map_err(Error::Wallet)?; - - let mut batch_runner = BatchRunner::<_, _, _, ()>::new( - 100, - dfvks - .iter() - .flat_map(|(account, dfvk)| { - [ - ((**account, Scope::External), dfvk.to_ivk(Scope::External)), - ((**account, Scope::Internal), dfvk.to_ivk(Scope::Internal)), - ] - }) - .map(|(tag, ivk)| (tag, PreparedIncomingViewingKey::new(&ivk))), + let scanning_keys = ScanningKeys::from_account_ufvks(account_ufvks); + let mut runners = BatchRunners::<_, (), ()>::for_keys(100, &scanning_keys); + + 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 + }; + + // 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)?, ); - block_source.with_blocks::<_, DbT::Error, DbT::NoteRef>( - last_height, - limit, + 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| { - add_block_to_runner(params, block, &mut batch_runner); - Ok(()) - }, - )?; - - batch_runner.flush(); - - block_source.with_blocks::<_, DbT::Error, DbT::NoteRef>( - last_height, - limit, - |block: CompactBlock| { - let current_height = block.height(); - - // Scanned blocks MUST be height-sequential. - if let Some(h) = last_height { - if current_height != (h + 1) { - return Err( - ChainError::block_height_discontinuity(h + 1, current_height).into(), - ); + scan_summary.scanned_range.end = block.height() + 1; + let scanned_block = scan_block_with_runners::<_, _, _, (), ()>( + params, + block, + &scanning_keys, + &nullifiers, + prior_block_metadata.as_ref(), + Some(&mut runners), + ) + .map_err(Error::Scan)?; + + 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(); } } - let block_hash = BlockHash::from_slice(&block.hash); - let block_time = block.time; - - let txs: Vec> = { - let mut witness_refs: Vec<_> = witnesses.iter_mut().map(|w| &mut w.1).collect(); - - scan_block_with_runner( - params, - block, - &dfvks, - &nullifiers, - &mut tree, - &mut witness_refs[..], - Some(&mut batch_runner), - ) - }; - - // 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( - ChainError::invalid_witness_anchor(current_height, row.0).into() - ); - } - } - for tx in &txs { - for output in tx.sapling_outputs.iter() { - if output.witness().root() != cur_root { - return Err(ChainError::invalid_new_witness_anchor( - current_height, - tx.txid, - output.index(), - output.witness().root(), - ) - .into()); - } - } - } - } - - let new_witnesses = data_db - .advance_by_block( - &(PrunedBlock { - block_height: current_height, - block_hash, - block_time, - commitment_tree: &tree, - transactions: &txs, - }), - &witnesses, - ) - .map_err(Error::Wallet)?; - - let spent_nf: Vec<&Nullifier> = txs + let sapling_spent_nf: Vec<&sapling::Nullifier> = scanned_block + .transactions .iter() - .flat_map(|tx| tx.sapling_spends.iter().map(|spend| spend.nf())) + .flat_map(|tx| tx.sapling_spends().iter().map(|spend| spend.nf())) .collect(); - nullifiers.retain(|(_, nf)| !spent_nf.contains(&nf)); - nullifiers.extend(txs.iter().flat_map(|tx| { - tx.sapling_outputs + nullifiers.retain_sapling(|(_, nf)| !sapling_spent_nf.contains(&nf)); + nullifiers.extend_sapling(scanned_block.transactions.iter().flat_map(|tx| { + tx.sapling_outputs() .iter() - .map(|out| (out.account(), *out.nf())) + .flat_map(|out| out.nf().into_iter().map(|nf| (*out.account_id(), *nf))) })); - witnesses.extend(new_witnesses); + #[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))) + })); + } - last_height = Some(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_primitives::consensus::BlockHeight; + use zcash_protocol::consensus::BlockHeight; use crate::proto::compact_formats::CompactBlock; @@ -389,14 +710,14 @@ pub mod testing { impl BlockSource for MockBlockSource { type Error = Infallible; - fn with_blocks( + fn with_blocks( &self, _from_height: Option, - _limit: Option, + _limit: Option, _with_row: F, - ) -> Result<(), Error> + ) -> Result<(), Error> where - F: FnMut(CompactBlock) -> Result<(), Error>, + 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 index b35334c6ac..3a21884bc6 100644 --- a/zcash_client_backend/src/data_api/chain/error.rs +++ b/zcash_client_backend/src/data_api/chain/error.rs @@ -3,134 +3,11 @@ use std::error; use std::fmt::{self, Debug, Display}; -use zcash_primitives::{consensus::BlockHeight, sapling, transaction::TxId}; - -/// The underlying cause of a [`ChainError`]. -#[derive(Copy, Clone, Debug)] -pub enum Cause { - /// The hash of the parent block given by a proposed new chain tip does not match the hash of - /// the current chain tip. - PrevHashMismatch, - - /// 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), - - /// 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. - /// - /// This error is currently only produced when performing the slow checks that are enabled by - /// compiling with `-C debug-assertions`. - InvalidNewWitnessAnchor { - /// The id of the transaction containing the mismatched witness. - txid: TxId, - /// The index of the shielded output within the transaction where the witness root does not - /// match. - index: usize, - /// The root of the witness that failed to match the root of the current note commitment - /// tree. - node: sapling::Node, - }, - - /// The root of an output's witness tree in a previously stored transaction does not correspond - /// to root of the current commitment tree. - /// - /// This error is currently only produced when performing the slow checks that are enabled by - /// compiling with `-C debug-assertions`. - InvalidWitnessAnchor(NoteRef), -} - -/// Errors that may occur in chain scanning or validation. -#[derive(Copy, Clone, Debug)] -pub struct ChainError { - at_height: BlockHeight, - cause: Cause, -} - -impl fmt::Display for ChainError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match &self.cause { - Cause::PrevHashMismatch => write!( - f, - "The parent hash of proposed block does not correspond to the block hash at height {}.", - self.at_height - ), - Cause::BlockHeightDiscontinuity(h) => { - write!(f, "Block height discontinuity at height {}; next height is : {}", self.at_height, h) - } - Cause::InvalidNewWitnessAnchor { txid, index, node } => write!( - f, - "New witness for output {} in tx {} at height {} has incorrect anchor: {:?}", - index, txid, self.at_height, node, - ), - Cause::InvalidWitnessAnchor(id_note) => { - write!(f, "Witness for note {} has incorrect anchor for height {}", id_note, self.at_height) - } - } - } -} - -impl ChainError { - /// Constructs an error that indicates block hashes failed to chain. - /// - /// * `at_height` the height of the block whose parent hash does not match the hash of the - /// previous block - pub fn prev_hash_mismatch(at_height: BlockHeight) -> Self { - ChainError { - at_height, - cause: Cause::PrevHashMismatch, - } - } - - /// Constructs an error that indicates a gap in block heights. - /// - /// * `at_height` the height of the block being added to the chain. - /// * `prev_chain_tip` the height of the previous chain tip. - pub fn block_height_discontinuity(at_height: BlockHeight, prev_chain_tip: BlockHeight) -> Self { - ChainError { - at_height, - cause: Cause::BlockHeightDiscontinuity(prev_chain_tip), - } - } - - /// Constructs an error that indicates a mismatch between an updated note's witness and the - /// root of the current note commitment tree. - pub fn invalid_witness_anchor(at_height: BlockHeight, note_ref: NoteRef) -> Self { - ChainError { - at_height, - cause: Cause::InvalidWitnessAnchor(note_ref), - } - } - - /// Constructs an error that indicates a mismatch between a new note's witness and the root of - /// the current note commitment tree. - pub fn invalid_new_witness_anchor( - at_height: BlockHeight, - txid: TxId, - index: usize, - node: sapling::Node, - ) -> Self { - ChainError { - at_height, - cause: Cause::InvalidNewWitnessAnchor { txid, index, node }, - } - } - - /// Returns the block height at which this error was discovered. - pub fn at_height(&self) -> BlockHeight { - self.at_height - } - - /// Returns the cause of this error. - pub fn cause(&self) -> &Cause { - &self.cause - } -} +use crate::scanning::ScanError; /// Errors related to chain validation and scanning. #[derive(Debug)] -pub enum Error { +pub enum Error { /// An error that was produced by wallet operations in the course of scanning the chain. Wallet(WalletError), @@ -141,10 +18,10 @@ pub enum Error { /// 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. - Chain(ChainError), + Scan(ScanError), } -impl fmt::Display for Error { +impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self { Error::Wallet(e) => { @@ -161,18 +38,17 @@ impl fmt::Display for Error { - write!(f, "{}", err) + Error::Scan(e) => { + write!(f, "Scanning produced the following error: {}", e) } } } } -impl error::Error for Error +impl error::Error for Error where WE: Debug + Display + error::Error + 'static, BE: Debug + Display + error::Error + 'static, - N: Debug + Display, { fn source(&self) -> Option<&(dyn error::Error + 'static)> { match &self { @@ -183,8 +59,8 @@ where } } -impl From> for Error { - fn from(e: ChainError) -> Self { - Error::Chain(e) +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 0614a612d6..90284563ba 100644 --- a/zcash_client_backend/src/data_api/error.rs +++ b/zcash_client_backend/src/data_api/error.rs @@ -2,42 +2,67 @@ use std::error; use std::fmt::{self, Debug, Display}; -use zcash_primitives::{ - transaction::{ - builder, - components::{ - amount::{Amount, BalanceError}, - sapling, transparent, - }, - }, - zip32::AccountId, + +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; +use crate::{ + data_api::wallet::input_selection::InputSelectorError, fees::ChangeError, + proposal::ProposalError, wallet::NoteId, +}; #[cfg(feature = "transparent-inputs")] -use zcash_primitives::{legacy::TransparentAddress, zip32::DiversifierIndex}; +use ::transparent::address::TransparentAddress; /// Errors that can occur as a consequence of wallet operations. #[derive(Debug)] -pub enum Error { +pub enum Error +{ /// An error occurred retrieving data from the underlying data source DataSource(DataSourceError), + /// An error in computations involving the note commitment trees. + CommitmentTree(ShardTreeError), + /// An error in note selection NoteSelection(SelectionError), + /// An error in change selection during transaction proposal construction + Change(ChangeError), + + /// An error in transaction proposal construction + Proposal(ProposalError), + + /// 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, + + /// No account could be found corresponding to a provided ID. + AccountIdNotRecognized, + /// No account could be found corresponding to a provided spending key. KeyNotRecognized, - /// No account with the given identifier was found in the wallet. - AccountNotFound(AccountId), + /// 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: Amount, required: Amount }, + InsufficientFunds { + available: Zatoshis, + required: Zatoshis, + }, /// The wallet must first perform a scan of the blockchain before other /// operations can be performed. @@ -49,26 +74,84 @@ pub enum Error { /// It is forbidden to provide a memo when constructing a transparent output. MemoForbidden, + /// 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(NoteRef), + 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")] - ChildIndexOutOfRange(DiversifierIndex), + PaysEphemeralTransparentAddress(String), + + /// An error occurred while working with PCZTs. + #[cfg(feature = "pczt")] + Pczt(PcztError), } -impl fmt::Display for Error +/// Errors that can occur while working with PCZTs. +#[cfg(feature = "pczt")] +#[derive(Debug)] +pub enum PcztError { + /// An error occurred while building a PCZT. + Build, + + /// 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 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 { + use fmt::Write; + + match self { Error::DataSource(e) => { write!( f, @@ -76,17 +159,41 @@ where 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, + "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::KeyNotRecognized => { write!( f, "Wallet does not contain an account corresponding to the provided spending key" ) } - Error::AccountNotFound(account) => { - write!(f, "Wallet does not contain account {}", u32::from(*account)) + Error::AccountCannotSpend => { + write!( + f, + "The given account cannot be used for spending, because it is unable to maintain an accurate balance.", + ) + } + Error::AccountIdNotRecognized => { + write!( + f, + "Wallet does not contain an account corresponding to the provided ID" + ) } Error::BalanceError(e) => write!( f, @@ -96,64 +203,135 @@ where Error::InsufficientFunds { available, required } => write!( f, "Insufficient balance (have {}, need {} including fee)", - i64::from(*available), - i64::from(*required) + u64::from(*available), + u64::from(*required) ), Error::ScanRequired => write!(f, "Must scan blocks first"), 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::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::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::ChildIndexOutOfRange(i) => { + 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}"), + } + } +} + +#[cfg(feature = "pczt")] +impl fmt::Display for PcztError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + PcztError::Build => { write!( f, - "The diversifier index {:?} is out of range for transparent addresses.", - i + "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 +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, - N: Debug + Display, + 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), + #[cfg(feature = "pczt")] + Error::Pczt(e) => Some(e), _ => None, } } } -impl From> for Error { +#[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 { +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(e: InputSelectorError) -> Self { +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, @@ -161,18 +339,87 @@ impl From> for Error { available, required, }, + InputSelectorError::SyncRequired => Error::ScanRequired, + InputSelectorError::Address(e) => Error::Address(e), } } } -impl From for Error { +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 { +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..b35b8f95f9 --- /dev/null +++ b/zcash_client_backend/src/data_api/testing.rs @@ -0,0 +1,2902 @@ +//! 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, + AddressInfo, BlockMetadata, DecryptedTransaction, InputSource, NoteFilter, NullifierQuery, + ScannedBlock, SeedRelevance, SentTransaction, SpendableNotes, TransactionDataRequest, + TransactionStatus, WalletCommitmentTrees, WalletRead, WalletSummary, WalletTest, WalletWrite, + SAPLING_SHARD_HEIGHT, +}; +use crate::{ + data_api::TargetValue, + 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, + transparent::GapLimits, +}; + +#[cfg(feature = "orchard")] +use { + super::ORCHARD_SHARD_HEIGHT, crate::proto::compact_formats::CompactOrchardAction, + ::orchard::tree::MerkleHashOrchard, group::ff::PrimeField, pasta_curves::pallas, +}; + +pub mod pool; +pub mod sapling; + +#[cfg(feature = "orchard")] +pub mod orchard; +#[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, + total_spent: Zatoshis, + total_received: Zatoshis, + 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, + total_spent: Zatoshis, + total_received: Zatoshis, + 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, + total_spent, + total_received, + 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 total value of notes spent by the account in this transaction. + pub fn total_spent(&self) -> Zatoshis { + self.total_spent + } + + /// Returns the total value of notes received by the account in this transaction. + pub fn total_received(&self) -> Zatoshis { + self.total_received + } + + /// 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(); + + extract_and_store_transaction_from_pczt( + self.wallet_mut(), + pczt, + Some((&spend_vk, &output_vk)), + None, + ) + } + + /// 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, + #[cfg(feature = "transparent-inputs")] gap_limits: GapLimits, + ) -> 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, + #[cfg(feature = "transparent-inputs")] + gap_limits: GapLimits, +} + +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 = "nu7")] + nu7: 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, + #[cfg(feature = "transparent-inputs")] + gap_limits: GapLimits::new(10, 5, 5), + } + } +} + +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, + #[cfg(feature = "transparent-inputs")] + gap_limits: self.gap_limits, + } + } +} + +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, + #[cfg(feature = "transparent-inputs")] + gap_limits: self.gap_limits, + } + } +} + +impl TestBuilder { + #[cfg(feature = "transparent-inputs")] + pub fn with_gap_limits(self, gap_limits: GapLimits) -> TestBuilder { + TestBuilder { + rng: self.rng, + network: self.network, + cache: self.cache, + ds_factory: self.ds_factory, + initial_chain_state: self.initial_chain_state, + account_birthday: self.account_birthday, + account_index: self.account_index, + gap_limits, + } + } +} + +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, + #[cfg(feature = "transparent-inputs")] + self.gap_limits, + ) + .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().into_bytes(), 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: TargetValue, + _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 list_addresses(&self, _account: Self::AccountId) -> Result, Self::Error> { + Ok(vec![]) + } + + fn get_last_generated_address_matching( + &self, + _account: Self::AccountId, + _request: UnifiedAddressRequest, + ) -> 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, + _include_change: bool, + ) -> 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 utxo_query_height(&self, _account: Self::AccountId) -> Result { + Ok(BlockHeight::from(0u32)) + } + + #[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: UnifiedAddressRequest, + ) -> Result, Self::Error> { + Ok(None) + } + + fn get_address_for_index( + &mut self, + _account: Self::AccountId, + _diversifier_index: DiversifierIndex, + _request: UnifiedAddressRequest, + ) -> 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..c905e0c160 --- /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, + ShieldedProtocol, +}; + +use crate::{ + data_api::{ + chain::{CommitmentTreeRoot, ScanSummary}, + testing::{pool::ShieldedPoolTester, TestState}, + DecryptedTransaction, InputSource, TargetValue, 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: TargetValue, + 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..63f2096779 --- /dev/null +++ b/zcash_client_backend/src/data_api/testing/pool.rs @@ -0,0 +1,3194 @@ +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, TargetValue, 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, + 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: TargetValue, + 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(), None, Some(h), &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 tx_0 = &tx_history[0]; + assert_eq!(tx_0.total_spent(), Zatoshis::const_from_u64(0)); + assert_eq!(tx_0.total_received(), Zatoshis::const_from_u64(60000)); + } + + { + let tx_1 = &tx_history[1]; + assert_eq!(tx_1.total_spent(), Zatoshis::const_from_u64(60000)); + assert_eq!(tx_1.total_received(), Zatoshis::const_from_u64(40000)); + } + + 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(), None, Some(h), &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 tx_0 = &tx_history[0]; + assert_eq!(tx_0.total_spent(), Zatoshis::const_from_u64(0)); + assert_eq!(tx_0.total_received(), Zatoshis::const_from_u64(650_0000)); + } + + { + let tx_1 = &tx_history[1]; + assert_eq!(tx_1.total_spent(), Zatoshis::const_from_u64(650_0000)); + assert_eq!(tx_1.total_received(), Zatoshis::const_from_u64(548_5000)); + assert_eq!(tx_1.fee_paid(), Some(Zatoshis::const_from_u64(15000))); + } + + 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::{testing::transparent::GapLimits, OutputOfSentTx}; + + let gap_limits = GapLimits::new(10, 5, 3); + let mut st = TestBuilder::new() + .with_data_store_factory(ds_factory) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .with_gap_limits(gap_limits) + .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 + ); + 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, prior_balance| { + // Add funds to the wallet. + add_funds(st, value); + let initial_balance: Option = prior_balance + value; + assert_eq!( + st.get_spendable_balance(account_id, 1), + initial_balance.unwrap() + ); + + 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 = + (initial_balance - expected_ephemeral - expected_step0_fee).expect("sufficient funds"); + assert!(expected_step0_change.is_positive()); + + let total_sent = (expected_step0_fee + expected_step1_fee + transfer_amount).unwrap(); + + // 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(); + + // Mine the created transactions. + for txid in txids.iter() { + let (h, _) = st.generate_next_block_including(*txid); + st.scan_cached_blocks(h, 1); + } + + // 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), + ); + + let ending_balance = st.get_spendable_balance(account_id, 1); + assert_eq!(initial_balance - total_sent, ending_balance.into()); + + (ephemeral_address.unwrap().0, txids, ending_balance) + }; + + // Each transfer should use a different ephemeral address. + let (ephemeral0, _, bal_0) = run_test(&mut st, 0, Zatoshis::ZERO); + let (ephemeral1, _, _) = run_test(&mut st, 1, bal_0); + 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(), + usize::try_from(gap_limits.ephemeral() + 2).unwrap() + ); + + // 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[usize::try_from(gap_limits.ephemeral() - 1).unwrap()]; + 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(); + + // Now, store the transaction, pretending it has been mined (we will actually mine the block + // next). This will cause the the gap start to move & a new `gap_limits.ephemeral()` of + // addresses to be created. + let target_height = st.latest_cached_block().unwrap().height() + 1; + st.wallet_mut() + .store_decrypted_tx(DecryptedTransaction::new( + Some(target_height), + build_result.transaction(), + vec![], + #[cfg(feature = "orchard")] + vec![], + )) + .unwrap(); + + // Mine the transaction & scan it so that it is will be detected as mined. Note that + // `generate_next_block_including` does not actually do anything with fully-transparent + // transactions; we're doing this just to get the mined block that we added via + // `store_decrypted_tx` into the database. + let (h, _) = st.generate_next_block_including(txid); + st.scan_cached_blocks(h, 1); + assert_eq!(h, target_height); + + // At this point the start of the gap should be at index `gap_limits.ephemeral()` and the new + // size of the known address set should be `gap_limits.ephemeral() * 2`. + let new_known_addrs = st + .wallet() + .get_known_ephemeral_addresses(account_id, None) + .unwrap(); + assert_eq!( + new_known_addrs.len(), + usize::try_from(gap_limits.ephemeral() * 2).unwrap() + ); + assert!(new_known_addrs.starts_with(&known_addrs)); + + let reservation_should_succeed = |st: &mut TestState<_, DSF::DataStore, _>, n: u32| { + let reserved = st + .wallet_mut() + .reserve_next_n_ephemeral_addresses(account_id, n.try_into().unwrap()) + .unwrap(); + assert_eq!(reserved.len(), usize::try_from(n).unwrap()); + reserved + }; + let reservation_should_fail = + |st: &mut TestState<_, DSF::DataStore, _>, n: u32, expected_bad_index| { + assert_matches!(st + .wallet_mut() + .reserve_next_n_ephemeral_addresses(account_id, n.try_into().unwrap()), + 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[usize::try_from(gap_limits.ephemeral()).unwrap()] + ); + + // The range of address indices that are safe to reserve now is + // 0..(gap_limits.ephemeral() * 2 - 1)`, and we have already reserved or used + // `gap_limits.ephemeral() + 1`, addresses, so trying to reserve another + // `gap_limits.ephemeral()` should fail. + reservation_should_fail(&mut st, gap_limits.ephemeral(), gap_limits.ephemeral() * 2); + reservation_should_succeed(&mut st, gap_limits.ephemeral() - 1); + // Now we've reserved everything we can, we can't reserve one more + reservation_should_fail(&mut st, 1, gap_limits.ephemeral() * 2); +} + +#[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, +{ + use zcash_keys::keys::UnifiedAddressRequest; + + 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_last_generated_address_matching(account.id(), UnifiedAddressRequest::AllAvailableKeys) + .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, + TargetValue::AtLeast(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, + TargetValue::AtLeast(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(), + TargetValue::AtLeast(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..96d5325277 --- /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, + ShieldedProtocol, +}; +use zip32::Scope; + +use crate::{ + data_api::{ + chain::{CommitmentTreeRoot, ScanSummary}, + DecryptedTransaction, InputSource, TargetValue, 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: TargetValue, + 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..fce4f611f5 --- /dev/null +++ b/zcash_client_backend/src/data_api/testing/transparent.rs @@ -0,0 +1,519 @@ +use std::collections::BTreeMap; + +use crate::{ + data_api::{ + testing::{ + AddressType, DataStoreFactory, ShieldedProtocol, TestBuilder, TestCache, TestState, + }, + wallet::{decrypt_and_store_transaction, 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_keys::{address::Address, keys::UnifiedAddressRequest}; +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_last_generated_address_matching(account_id, UnifiedAddressRequest::AllAvailableKeys) + .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_last_generated_address_matching(account.id(), UnifiedAddressRequest::AllAvailableKeys) + .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_last_generated_address_matching(account.id(), UnifiedAddressRequest::AllAvailableKeys) + .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); +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct GapLimits { + external: u32, + internal: u32, + ephemeral: u32, +} + +impl GapLimits { + pub fn new(external: u32, internal: u32, ephemeral: u32) -> Self { + Self { + external, + internal, + ephemeral, + } + } + + pub fn external(&self) -> u32 { + self.external + } + + pub fn internal(&self) -> u32 { + self.internal + } + + pub fn ephemeral(&self) -> u32 { + self.ephemeral + } +} + +pub fn gap_limits(ds_factory: DSF, cache: impl TestCache, gap_limits: GapLimits) +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 test_account = st.test_account().cloned().unwrap(); + let account_uuid = test_account.account().id(); + let ufvk = test_account.account().ufvk().unwrap().clone(); + + let external_taddrs = st + .wallet() + .get_transparent_receivers(account_uuid, false) + .unwrap(); + assert_eq!( + u32::try_from(external_taddrs.len()).unwrap(), + gap_limits.external() + ); + let internal_taddrs = st + .wallet() + .get_transparent_receivers(account_uuid, true) + .unwrap(); + assert_eq!( + u32::try_from(internal_taddrs.len()).unwrap(), + gap_limits.external() + gap_limits.internal() + ); + let ephemeral_taddrs = st + .wallet() + .get_known_ephemeral_addresses(account_uuid, None) + .unwrap(); + assert_eq!( + u32::try_from(ephemeral_taddrs.len()).unwrap(), + gap_limits.ephemeral() + ); + + // Add some funds to the wallet + let (h0, _, _) = st.generate_next_block( + &ufvk.sapling().unwrap(), + AddressType::DefaultExternal, + Zatoshis::const_from_u64(1000000), + ); + st.scan_cached_blocks(h0, 1); + + // The previous operation was shielded-only, but unified address usage within the + // valid transparent child index range still count towards the gap limit, so this + // updates the gap limit by the index of the default Sapling receiver + let external_taddrs = st + .wallet() + .get_transparent_receivers(account_uuid, false) + .unwrap(); + assert_eq!( + u32::try_from(external_taddrs.len()).unwrap(), + gap_limits.external() + + (u32::try_from(ufvk.sapling().unwrap().default_address().0).unwrap() + 1) + ); + + // Pick an address half way through the set of external taddrs + let external_taddrs_sorted = external_taddrs + .into_iter() + .filter_map(|(addr, meta)| meta.map(|m| (m.address_index(), addr))) + .collect::>(); + let to = Address::from( + *external_taddrs_sorted + .get(&transparent::keys::NonHardenedChildIndex::from_index(4).unwrap()) + .expect("An address exists at index 4."), + ) + .to_zcash_address(st.network()); + + // Create a transaction & scan the block. Since the txid corresponds to one our wallet + // generated, this should cause the gap limit to be bumped (generating addresses with index + // 10..15) + let txids = st + .create_standard_transaction(&test_account, to, Zatoshis::const_from_u64(20000)) + .unwrap(); + let (h1, _) = st.generate_next_block_including(txids.head); + + // At this point, the transaction has been created, but since it has not been mined it does + // not cause an update to the gap limit; we have to wait for the transaction to actually be + // mined or we could bump the gap limit too soon and start generating addresses that will + // never be inspected on wallet recovery. + let external_taddrs = st + .wallet() + .get_transparent_receivers(account_uuid, false) + .unwrap(); + assert_eq!( + u32::try_from(external_taddrs.len()).unwrap(), + gap_limits.external() + + (u32::try_from(ufvk.sapling().unwrap().default_address().0).unwrap() + 1) + ); + + // Mine the block, then use `decrypt_and_store_transaction` to ensure that the wallet sees + // the transaction as mined (since transparent handling doesn't get this from + // `scan_cached_blocks`) + st.scan_cached_blocks(h1, 1); + let tx = st.wallet().get_transaction(txids.head).unwrap().unwrap(); + decrypt_and_store_transaction(&st.network().clone(), st.wallet_mut(), &tx, Some(h1)).unwrap(); + + // Now that the transaction has been mined, the gap limit should have increased. + let external_taddrs = st + .wallet() + .get_transparent_receivers(account_uuid, false) + .unwrap(); + assert_eq!( + u32::try_from(external_taddrs.len()).unwrap(), + gap_limits.external() + 5 + ); + + // The utxo query height should be equal to the minimum mined height among transactions + // sent to any of the set of {addresses in the gap limit range | address prior to the gap}. + let query_height = st.wallet().utxo_query_height(account_uuid).unwrap(); + assert_eq!(query_height, h0); +} diff --git a/zcash_client_backend/src/data_api/wallet.rs b/zcash_client_backend/src/data_api/wallet.rs index d96a47e284..87d502cac8 100644 --- a/zcash_client_backend/src/data_api/wallet.rs +++ b/zcash_client_backend/src/data_api/wallet.rs @@ -1,55 +1,179 @@ -use std::convert::Infallible; -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, NetworkUpgrade}, - memo::MemoBytes, - sapling::{ - self, - note_encryption::{try_sapling_note_decryption, PreparedIncomingViewingKey}, - prover::TxProver as SaplingProver, - Node, - }, - transaction::{ - builder::Builder, - components::amount::{Amount, BalanceError}, - fees::{fixed, FeeRule}, - Transaction, - }, - zip32::{sapling::DiversifiableFullViewingKey, sapling::ExtendedSpendingKey, AccountId, Scope}, -}; +[`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, wallet::input_selection::Proposal, DecryptedTransaction, PoolType, Recipient, - SentTransaction, SentTransactionOutput, WalletWrite, + error::Error, Account, SentTransaction, SentTransactionOutput, WalletCommitmentTrees, + WalletRead, WalletWrite, }, decrypt_transaction, - fees::{self, ChangeValue, DustOutputPolicy}, - keys::UnifiedSpendingKey, - wallet::{OvkPolicy, ReceivedSaplingNote}, - zip321::{self, Payment}, + fees::{ + standard::SingleOutputChangeStrategy, ChangeStrategy, DustOutputPolicy, StandardFeeRule, + }, + proposal::{Proposal, ProposalError, Step, StepOutputIndex}, + wallet::{Note, OvkPolicy, Recipient}, }; - -pub mod input_selection; -use input_selection::{GreedyInputSelector, GreedyInputSelectorError, InputSelector}; +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}, + memo::MemoBytes, + value::Zatoshis, + PoolType, ShieldedProtocol, +}; +use zip32::Scope; +use zip321::Payment; #[cfg(feature = "transparent-inputs")] use { - crate::wallet::WalletTransparentOutput, - zcash_primitives::{ - legacy::TransparentAddress, sapling::keys::OutgoingViewingKey, - transaction::components::amount::NonNegativeAmount, + 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: &ParamsT, data: &mut DbT, tx: &Transaction, + mined_height: Option, ) -> Result<(), DbT::Error> where ParamsT: consensus::Parameters, @@ -58,585 +182,1925 @@ where // 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)) - .expect("Sapling activation height must be known."); - - data.store_decrypted_tx(DecryptedTransaction { + data.store_decrypted_tx(decrypt_transaction( + params, + mined_height.map_or_else(|| data.get_tx_height(tx.txid()), |h| Ok(Some(h)))?, + data.chain_height()?, tx, - sapling_outputs: &decrypt_transaction(params, height, tx, &ufvks), - })?; + &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 -/// -/// Parameters: -/// * `wallet_db`: A read/write reference to the wallet database -/// * `params`: Consensus parameters -/// * `prover`: The [`sapling::TxProver`] to use in constructing the shielded transaction. -/// * `usk`: The unified spending key that controls the funds that will be spent -/// in the resulting transaction. This procedure will return an error if the -/// USK does not correspond to an account known to the wallet. -/// * `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. -/// * `ovk_policy`: The policy to use for constructing outgoing viewing keys that -/// can allow the sender to view the resulting notes on the blockchain. -/// * `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. -/// -/// # Examples -/// -/// ``` -/// # #[cfg(feature = "test-dependencies")] -/// # { -/// use tempfile::NamedTempFile; -/// use zcash_primitives::{ -/// consensus::{self, Network}, -/// constants::testnet::COIN_TYPE, -/// transaction::{TxId, components::Amount}, -/// zip32::AccountId, -/// }; -/// use zcash_proofs::prover::LocalTxProver; -/// use zcash_client_backend::{ -/// keys::UnifiedSpendingKey, -/// data_api::{wallet::create_spend_to_address, error::Error, testing}, -/// wallet::OvkPolicy, -/// }; -/// -/// # use std::convert::Infallible; -/// # use zcash_primitives::transaction::components::amount::BalanceError; -/// # use zcash_client_backend::{ -/// # data_api::wallet::input_selection::GreedyInputSelectorError, -/// # }; -/// # -/// # fn main() { -/// # test(); -/// # } -/// # -/// # #[allow(deprecated)] -/// # fn test() -> Result, Infallible, u32>> { -/// -/// 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."); -/// } -/// }; -/// -/// let account = AccountId::from(0); -/// let usk = UnifiedSpendingKey::from_seed(&Network::TestNetwork, &[0; 32][..], account).unwrap(); -/// let to = usk.to_unified_full_viewing_key().default_address().0.into(); -/// -/// let mut db_read = testing::MockWalletDb { -/// network: Network::TestNetwork -/// }; -/// -/// create_spend_to_address( -/// &mut db_read, -/// &Network::TestNetwork, -/// tx_prover, -/// &usk, -/// &to, -/// Amount::from_u64(1).unwrap(), -/// None, -/// OvkPolicy::Sender, -/// 10 -/// ) -/// -/// # } -/// # } -/// ``` -/// [`sapling::TxProver`]: zcash_primitives::sapling::prover::TxProver +/// 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)] -#[deprecated( - note = "Use `spend` instead. `create_spend_to_address` uses a fixed fee of 10000 zatoshis, which is not compliant with ZIP 317." -)] -pub fn create_spend_to_address( +pub fn propose_transfer( wallet_db: &mut DbT, params: &ParamsT, - prover: impl SaplingProver, - usk: &UnifiedSpendingKey, - to: &RecipientAddress, - amount: Amount, - memo: Option, - ovk_policy: OvkPolicy, - min_confirmations: u32, + spend_from_account: ::AccountId, + input_selector: &InputsT, + change_strategy: &ChangeT, + request: zip321::TransactionRequest, + min_confirmations: NonZeroU32, ) -> Result< - DbT::TxRef, - Error< - DbT::Error, - GreedyInputSelectorError, - Infallible, - DbT::NoteRef, - >, + Proposal::NoteRef>, + ProposeTransferErrT, > where + DbT: WalletRead + InputSource::Error>, + ::NoteRef: Copy + Eq + Ord, ParamsT: consensus::Parameters + Clone, - DbT: WalletWrite, - DbT::NoteRef: Copy + Eq + Ord, + InputsT: InputSelector, + ChangeT: ChangeStrategy, { - let req = zip321::TransactionRequest::new(vec![Payment { - recipient_address: to.clone(), - amount, - memo, - label: None, - message: None, - other_params: vec![], - }]) - .expect( - "It should not be possible for this to violate ZIP 321 request construction invariants.", - ); + 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))?; - #[allow(deprecated)] - let fee_rule = fixed::FeeRule::standard(); - let change_strategy = fees::fixed::SingleOutputChangeStrategy::new(fee_rule); - spend( - wallet_db, - params, - prover, - &GreedyInputSelector::::new(change_strategy, DustOutputPolicy::default()), - usk, - req, - ovk_policy, - min_confirmations, - ) + input_selector + .propose_transaction( + params, + wallet_db, + target_height, + anchor_height, + spend_from_account, + request, + change_strategy, + ) + .map_err(Error::from) } -/// Constructs a transaction that sends funds as specified by the `request` argument -/// and stores it to the wallet's "sent transactions" data store, and returns a -/// unique identifier for the transaction; this identifier is used only for internal -/// reference purposes and is not the same as the transaction's txid, although after v4 -/// transactions have been made invalid in a future network upgrade, the txid could -/// potentially be used for this type (as it is non-malleable for v5+ transactions). -/// -/// This procedure uses the wallet's underlying note selection algorithm to choose -/// inputs of sufficient value to satisfy the request, if possible. -/// -/// Do not call this multiple times in parallel, or you will generate transactions that -/// double-spend the same notes. +/// Proposes making a payment to the specified address from the given account. /// -/// # Transaction privacy +/// 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. /// -/// `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 +/// This method uses the basic [`GreedyInputSelector`] for input selection. /// /// Parameters: -/// * `wallet_db`: A read/write reference to the wallet database -/// * `params`: Consensus parameters -/// * `prover`: The [`sapling::TxProver`] to use in constructing the shielded transaction. -/// * `input_selector`: The [`InputSelector`] that will be used to select available -/// inputs from the wallet database, choose change amounts and compute required -/// transaction fees. -/// * `usk`: The unified spending key that controls the funds that will be spent +/// * `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 -/// USK does not correspond to an account known to the wallet. -/// * `request`: The ZIP-321 payment request specifying the recipients and amounts -/// for the transaction. -/// * `ovk_policy`: The policy to use for constructing outgoing viewing keys that -/// can allow the sender to view the resulting notes on the blockchain. +/// 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. -/// -/// [`sapling::TxProver`]: zcash_primitives::sapling::prover::TxProver +/// 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 spend( +pub fn propose_standard_transfer_to_address( wallet_db: &mut DbT, params: &ParamsT, - prover: impl SaplingProver, - input_selector: &InputsT, - usk: &UnifiedSpendingKey, - request: zip321::TransactionRequest, - ovk_policy: OvkPolicy, - min_confirmations: u32, + fee_rule: StandardFeeRule, + spend_from_account: ::AccountId, + min_confirmations: NonZeroU32, + to: &Address, + amount: Zatoshis, + memo: Option, + change_memo: Option, + fallback_change_pool: ShieldedProtocol, ) -> Result< - DbT::TxRef, - Error::Error, DbT::NoteRef>, + Proposal, + ProposeTransferErrT< + DbT, + CommitmentTreeErrT, + GreedyInputSelector, + SingleOutputChangeStrategy, + >, > where - DbT: WalletWrite, - DbT::TxRef: Copy + Debug, - DbT::NoteRef: Copy + Eq + Ord, ParamsT: consensus::Parameters + Clone, - InputsT: InputSelector, + DbT: InputSource, + DbT: WalletRead< + Error = ::Error, + AccountId = ::AccountId, + >, + DbT::NoteRef: Copy + Eq + Ord, { - let account = wallet_db - .get_account_for_ufvk(&usk.to_unified_full_viewing_key()) - .map_err(Error::DataSource)? - .ok_or(Error::KeyNotRecognized)?; + 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 proposal = propose_transfer( + let input_selector = GreedyInputSelector::::new(); + let change_strategy = SingleOutputChangeStrategy::::new( + fee_rule, + change_memo, + fallback_change_pool, + DustOutputPolicy::default(), + ); + + propose_transfer( wallet_db, params, - account, - input_selector, + spend_from_account, + &input_selector, + &change_strategy, request, min_confirmations, - )?; - - create_proposed_transaction(wallet_db, params, prover, usk, ovk_policy, proposal, None) -} - -/// Select transaction inputs, compute fees, and construct a proposal for a transaction -/// that can then be authorized and made ready for submission to the network with -/// [`create_proposed_transaction`]. -#[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, - request: zip321::TransactionRequest, - min_confirmations: u32, -) -> Result< - Proposal, - Error::Error, DbT::NoteRef>, -> -where - DbT: WalletWrite, - DbT::NoteRef: Copy + Eq + Ord, - ParamsT: consensus::Parameters + Clone, - InputsT: InputSelector, -{ - // Target the next block, assuming we are up-to-date. - let (target_height, anchor_height) = wallet_db - .get_target_and_anchor_heights(min_confirmations) - .map_err(Error::DataSource) - .and_then(|x| x.ok_or(Error::ScanRequired))?; - - input_selector - .propose_transaction( - params, - wallet_db, - spend_from_account, - anchor_height, - target_height, - request, - ) - .map_err(Error::from) + ) } +/// 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( +pub fn propose_shielding( wallet_db: &mut DbT, params: &ParamsT, input_selector: &InputsT, - shielding_threshold: NonNegativeAmount, + change_strategy: &ChangeT, + shielding_threshold: Zatoshis, from_addrs: &[TransparentAddress], + to_account: ::AccountId, min_confirmations: u32, ) -> Result< - Proposal, - Error::Error, DbT::NoteRef>, + Proposal, + ProposeShieldingErrT, > where ParamsT: consensus::Parameters, - DbT: WalletWrite, - DbT::NoteRef: Copy + Eq + Ord, - InputsT: InputSelector, + DbT: WalletRead + InputSource::Error>, + InputsT: ShieldingSelector, + ChangeT: ChangeStrategy, { - let (target_height, latest_anchor) = wallet_db - .get_target_and_anchor_heights(min_confirmations) - .map_err(Error::DataSource) - .and_then(|x| x.ok_or(Error::ScanRequired))?; + 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, - latest_anchor, - target_height, + to_account, + chain_tip_height + 1, + min_confirmations, ) .map_err(Error::from) } -/// Construct, prove, and sign a transaction using the inputs supplied by the given proposal, -/// and persist it to the wallet database. +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. /// -/// Returns the database identifier for the newly constructed transaction, or an error if +/// Returns the database identifier for each newly constructed transaction, or an error if /// an error occurs in transaction construction, proving, or signing. +/// +/// 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)] #[allow(clippy::type_complexity)] -pub fn create_proposed_transaction( +pub fn create_proposed_transactions( wallet_db: &mut DbT, params: &ParamsT, - prover: impl SaplingProver, + spend_prover: &impl SpendProver, + output_prover: &impl OutputProver, usk: &UnifiedSpendingKey, ovk_policy: OvkPolicy, - proposal: Proposal, - change_memo: Option, -) -> Result> + proposal: &Proposal, +) -> Result, CreateErrT> where - DbT: WalletWrite, - DbT::TxRef: Copy + Debug, - DbT::NoteRef: Copy + Eq + Ord, + DbT: WalletWrite + WalletCommitmentTrees, ParamsT: consensus::Parameters + Clone, FeeRuleT: FeeRule, { - let account = wallet_db + // 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)?; + .ok_or(Error::KeyNotRecognized)? + .id(); - let dfvk = usk.sapling().to_diversifiable_full_viewing_key(); + 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)); + } - // Apply the outgoing viewing key policy. - let external_ovk = match ovk_policy { - OvkPolicy::Sender => Some(dfvk.to_ovk(Scope::External)), - OvkPolicy::Custom(ovk) => Some(ovk), - OvkPolicy::Discard => None, - }; + // 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 internal_ovk = || { - #[cfg(feature = "transparent-inputs")] - return if proposal.is_shielding() { - Some(OutgoingViewingKey( - usk.transparent() - .to_account_pubkey() - .internal_ovk() - .as_bytes(), - )) - } else { - Some(dfvk.to_ovk(Scope::Internal)) - }; + 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"))] - Some(dfvk.to_ovk(Scope::Internal)) + 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 - let mut builder = Builder::new(params.clone(), proposal.target_height()); + // 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, + }, + ); - for selected in proposal.sapling_inputs() { - let (note, key, merkle_path) = select_key_for_note(selected, usk.sapling(), &dfvk) - .ok_or(Error::NoteMismatch(selected.note_id))?; + #[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()); - builder.add_sapling_spend(key, selected.diversifier, note, merkle_path)?; + 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 = "transparent-inputs")] - let utxos = { - let known_addrs = wallet_db - .get_transparent_receivers(account) - .map_err(Error::DataSource)?; + #[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(), + )?; + } - let mut utxos: Vec = vec![]; - for utxo in proposal.transparent_inputs() { - utxos.push(utxo.clone()); + #[cfg(feature = "transparent-inputs")] + let mut cache = HashMap::::new(); - let diversifier_index = known_addrs - .get(utxo.recipient_address()) - .ok_or_else(|| Error::AddressNotRecognized(*utxo.recipient_address()))? - .diversifier_index(); + #[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 child_index = u32::try_from(*diversifier_index) - .map_err(|_| Error::ChildIndexOutOfRange(*diversifier_index))?; + 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) + } + } + }; - let secret_key = usk + #[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() - .derive_external_secret_key(child_index) - .unwrap(); + .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)?; - builder.add_transparent_input( - secret_key, + 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(), )?; } - utxos + 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 }; - let mut sapling_output_meta = vec![]; - let mut transparent_output_meta = vec![]; - for payment in proposal.transaction_request().payments() { - match &payment.recipient_address { - RecipientAddress::Unified(ua) => { - builder.add_sapling_output( - external_ovk, - *ua.sapling().expect("TODO: Add Orchard support to builder"), - payment.amount, - payment.memo.clone().unwrap_or_else(MemoBytes::empty), + #[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 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, + }; + + 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)) + }; + + #[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![]; + + 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 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(()) + }; + + #[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(), )?; - sapling_output_meta.push(( - Recipient::Unified(ua.clone(), PoolType::Sapling), - payment.amount, - payment.memo.clone(), + orchard_output_meta.push(( + BuildRecipient::External { + recipient_address: recipient_address.clone(), + output_pool: PoolType::ORCHARD, + }, + payment.amount(), + Some(memo), )); - } - RecipientAddress::Shielded(addr) => { - builder.add_sapling_output( - external_ovk, - *addr, - payment.amount, - payment.memo.clone().unwrap_or_else(MemoBytes::empty), - )?; - sapling_output_meta.push(( - Recipient::Sapling(*addr), - payment.amount, - payment.memo.clone(), + 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)?; } - RecipientAddress::Transparent(to) => { - if payment.memo.is_some() { - return Err(Error::MemoForbidden); - } else { - builder.add_transparent_output(to, payment.amount)?; + 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()); } - transparent_output_meta.push((*to, payment.amount)); + let to = TransparentAddress::PublicKeyHash(data); + add_transparent_output(&mut builder, &mut transparent_output_meta, to)?; } } } - for change_value in proposal.balance().proposed_change() { - match change_value { - ChangeValue::Sapling(amount) => { + 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( - internal_ovk(), - dfvk.change_address().1, - *amount, - MemoBytes::empty(), + sapling_internal_ovk(), + ufvk.sapling() + .ok_or(Error::KeyNotAvailable(PoolType::SAPLING))? + .change_address() + .1, + change_value.value(), + memo.clone(), )?; sapling_output_meta.push(( - Recipient::InternalAccount(account, PoolType::Sapling), - *amount, - change_memo.clone(), + 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)); + } + } + } + + // 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), + )) } } + 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 - let (tx, sapling_build_meta) = builder.build(&prover, proposal.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, + )?; - let internal_ivk = PreparedIncomingViewingKey::new(&dfvk.to_ivk(Scope::Internal)); - let sapling_outputs = - sapling_output_meta - .into_iter() - .enumerate() - .map(|(i, (recipient, value, memo))| { - let output_index = sapling_build_meta - .output_index(i) - .expect("An output should exist in the transaction for each shielded payment."); - - let received_as = - if let Recipient::InternalAccount(account, PoolType::Sapling) = recipient { - tx.sapling_bundle().and_then(|bundle| { - try_sapling_note_decryption( - params, - proposal.target_height(), - &internal_ivk, - &bundle.shielded_outputs()[output_index], - ) - .map(|(note, _, _)| (account, note)) - }) - } else { - None - }; + #[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) + }, + ); + + 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."); - SentTransactionOutput::from_parts(output_index, recipient, value, memo, received_as) + 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") }); - let transparent_outputs = transparent_output_meta.into_iter().map(|(addr, value)| { - let script = addr.script(); - let output_index = tx + 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() - .and_then(|b| { - b.vout + .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() - .find(|(_, tx_out)| tx_out.script_pubkey == script) + .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. +/// +/// - `sapling_vk` is optional to allow the caller to check whether a PCZT has Sapling +/// with [`pczt::roles::prover::Prover::requires_sapling_proofs`], and avoid downloading +/// the Sapling parameters if they are not needed. If `sapling_vk` is `None`, and the +/// PCZT has a Sapling bundle, this function will return an error. +/// - `orchard_vk` is optional to allow the caller to control where the Orchard verifying +/// key is generated or cached. If `orchard_vk` is `None`, and the PCZT has an Orchard +/// bundle, an Orchard verifying key will be generated on the fly. +#[cfg(feature = "pczt")] +pub fn extract_and_store_transaction_from_pczt( + wallet_db: &mut DbT, + pczt: pczt::Pczt, + sapling_vk: Option<( + &sapling::circuit::SpendVerifyingKey, + &sapling::circuit::OutputVerifyingKey, + )>, + #[cfg(feature = "orchard")] orchard_vk: Option<&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 + )) }) - .map(|(index, _)| index) - .expect("An output should exist in the transaction for each transparent payment."); + })?; - SentTransactionOutput::from_parts( - output_index, - Recipient::Transparent(addr), - value, - None, - None, - ) - }); + 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() + })?; - wallet_db - .store_sent_tx(&SentTransaction { - tx: &tx, - created: time::OffsetDateTime::now_utc(), - account, - outputs: sapling_outputs.chain(transparent_outputs).collect(), - fee_amount: proposal.balance().fee_required(), + 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 mut tx_extractor = TransactionExtractor::new(finalized); + if let Some((spend_vk, output_vk)) = sapling_vk { + tx_extractor = tx_extractor.with_sapling(spend_vk, output_vk); + } + if let Some(orchard_vk) = orchard_vk { + tx_extractor = tx_extractor.with_orchard(orchard_vk); + } + let transaction = tx_extractor.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")] - utxos_spent: utxos.iter().map(|utxo| utxo.outpoint().clone()).collect(), + (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() + .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::, _>>() }) - .map_err(Error::DataSource) + .transpose()?; + + 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 default address for the provided Sapling -/// extended full viewing key. +/// 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. +/// 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 -/// * `prover`: The [`sapling::TxProver`] to use in constructing the shielded transaction. +/// * `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, @@ -648,82 +2112,53 @@ where /// * `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. -/// * `memo`: A memo to be included in the output to the (internal) recipient. -/// This can be used to take notes about auto-shielding operations internal -/// to the wallet that the wallet can use to improve how it represents those -/// shielding transactions to the user. /// * `min_confirmations`: The minimum number of confirmations that a previously -/// received UTXO must have in the blockchain in order to be considered for being -/// spent. +/// 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::TxProver`]: zcash_primitives::sapling::prover::TxProver +/// [`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( +pub fn shield_transparent_funds( wallet_db: &mut DbT, params: &ParamsT, - prover: impl SaplingProver, + spend_prover: &impl SpendProver, + output_prover: &impl OutputProver, input_selector: &InputsT, - shielding_threshold: NonNegativeAmount, + change_strategy: &ChangeT, + shielding_threshold: Zatoshis, usk: &UnifiedSpendingKey, from_addrs: &[TransparentAddress], - memo: &MemoBytes, + to_account: ::AccountId, min_confirmations: u32, -) -> Result< - DbT::TxRef, - Error::Error, DbT::NoteRef>, -> +) -> Result, ShieldErrT> where ParamsT: consensus::Parameters, - DbT: WalletWrite, - DbT::NoteRef: Copy + Eq + Ord, - InputsT: InputSelector, + 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_transaction( + create_proposed_transactions( wallet_db, params, - prover, + spend_prover, + output_prover, usk, OvkPolicy::Sender, - proposal, - Some(memo.clone()), + &proposal, ) } - -fn select_key_for_note( - selected: &ReceivedSaplingNote, - extsk: &ExtendedSpendingKey, - dfvk: &DiversifiableFullViewingKey, -) -> Option<(sapling::Note, ExtendedSpendingKey, sapling::MerklePath)> { - let merkle_path = selected.witness.path().expect("the tree is not empty"); - - // 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(selected.diversifier) - .map(|addr| addr.create_note(selected.note_value.into(), selected.rseed)); - let internal_note = dfvk - .diversified_change_address(selected.diversifier) - .map(|addr| addr.create_note(selected.note_value.into(), selected.rseed)); - - let expected_root = selected.witness.root(); - external_note - .filter(|n| expected_root == merkle_path.root(Node::from_cmu(&n.cmu()))) - .map(|n| (n, extsk.clone(), merkle_path.clone())) - .or_else(|| { - internal_note - .filter(|n| expected_root == merkle_path.root(Node::from_cmu(&n.cmu()))) - .map(|n| (n, extsk.derive_internal(), merkle_path)) - }) -} diff --git a/zcash_client_backend/src/data_api/wallet/input_selection.rs b/zcash_client_backend/src/data_api/wallet/input_selection.rs index 798b834501..dfb7c6b4d2 100644 --- a/zcash_client_backend/src/data_api/wallet/input_selection.rs +++ b/zcash_client_backend/src/data_api/wallet/input_selection.rs @@ -1,43 +1,72 @@ //! Types related to the process of selecting inputs to be spent given a transaction request. use core::marker::PhantomData; -use std::collections::BTreeSet; -use std::fmt; +use std::{ + collections::BTreeMap, + error, + fmt::{self, Debug, Display}, +}; -use zcash_primitives::{ +use ::transparent::bundle::TxOut; +use nonempty::NonEmpty; +use zcash_address::ConversionError; +use zcash_keys::address::{Address, UnifiedAddress}; +use zcash_protocol::{ consensus::{self, BlockHeight}, - legacy::TransparentAddress, - transaction::{ - components::{ - amount::{Amount, BalanceError, NonNegativeAmount}, - sapling::fees as sapling, - OutPoint, TxOut, - }, - fees::FeeRule, - }, - zip32::AccountId, + value::{BalanceError, Zatoshis}, + PoolType, ShieldedProtocol, }; +use zip321::TransactionRequest; use crate::{ - address::{RecipientAddress, UnifiedAddress}, - data_api::WalletRead, - fees::{ChangeError, ChangeStrategy, DustOutputPolicy, TransactionBalance}, - wallet::{ReceivedSaplingNote, WalletTransparentOutput}, - zip321::TransactionRequest, + data_api::{InputSource, SimpleNoteRetention, SpendableNotes, TargetValue}, + 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. -pub enum InputSelectorError { +#[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: Amount, required: Amount }, + 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 { +impl fmt::Display + for InputSelectorError +{ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self { InputSelectorError::DataSource(e) => { @@ -50,61 +79,68 @@ impl fmt::Display for InputSelectorError { 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)", - i64::from(*available), - i64::from(*required) + u64::from(*available), + u64::from(*required) ), + InputSelectorError::SyncRequired => { + write!(f, "Insufficient chain data is available, sync required.") + } } } } -/// A data structure that describes the inputs to be consumed and outputs to -/// be produced in a proposed transaction. -pub struct Proposal { - transaction_request: TransactionRequest, - transparent_inputs: Vec, - sapling_inputs: Vec>, - balance: TransactionBalance, - fee_rule: FeeRuleT, - target_height: BlockHeight, - is_shielding: bool, +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 Proposal { - /// Returns the transaction request that describes the payments to be made. - pub fn transaction_request(&self) -> &TransactionRequest { - &self.transaction_request - } - /// Returns the transparent inputs that have been selected to fund the transaction. - pub fn transparent_inputs(&self) -> &[WalletTransparentOutput] { - &self.transparent_inputs - } - /// Returns the Sapling inputs that have been selected to fund the transaction. - pub fn sapling_inputs(&self) -> &[ReceivedSaplingNote] { - &self.sapling_inputs +impl From> for InputSelectorError { + fn from(value: ConversionError<&'static str>) -> Self { + InputSelectorError::Address(value) } - /// Returns the change outputs to be added to the transaction and the fee to be paid. - pub fn balance(&self) -> &TransactionBalance { - &self.balance - } - /// 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. - pub fn target_height(&self) -> BlockHeight { - self.target_height - } - /// 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 +} + +impl From> for InputSelectorError { + fn from(err: ChangeError) -> Self { + InputSelectorError::Change(err) } } @@ -116,14 +152,13 @@ impl Proposal { 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 and - /// 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 `WalletRead` does - /// not provide sufficiently fine-grained operations for a particular backing store to - /// optimally perform input selection. - type DataSource: WalletRead; - /// The type of the fee rule that this input selector uses when computing fees. - type FeeRule: FeeRule; + + /// 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. @@ -139,22 +174,44 @@ pub trait InputSelector { /// /// If insufficient funds are available to satisfy the required outputs for the shielding /// request, this operation must fail and return [`InputSelectorError::InsufficientFunds`]. - #[allow(clippy::too_many_arguments)] #[allow(clippy::type_complexity)] - fn propose_transaction( + #[allow(clippy::too_many_arguments)] + fn propose_transaction( &self, params: &ParamsT, - wallet_db: &Self::DataSource, - account: AccountId, - anchor_height: BlockHeight, + wallet_db: &Self::InputSource, target_height: BlockHeight, + anchor_height: BlockHeight, + account: ::AccountId, transaction_request: TransactionRequest, + change_strategy: &ChangeT, ) -> Result< - Proposal::DataSource as WalletRead>::NoteRef>, - InputSelectorError<<::DataSource as WalletRead>::Error, Self::Error>, + Proposal<::FeeRule, ::NoteRef>, + InputSelectorError< + ::Error, + Self::Error, + ChangeT::Error, + ::NoteRef, + >, > where - ParamsT: consensus::Parameters; + 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. @@ -164,36 +221,44 @@ pub trait InputSelector { /// 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::too_many_arguments)] #[allow(clippy::type_complexity)] - fn propose_shielding( + #[allow(clippy::too_many_arguments)] + fn propose_shielding( &self, params: &ParamsT, - wallet_db: &Self::DataSource, - shielding_threshold: NonNegativeAmount, + wallet_db: &Self::InputSource, + change_strategy: &ChangeT, + shielding_threshold: Zatoshis, source_addrs: &[TransparentAddress], - confirmed_height: BlockHeight, + to_account: ::AccountId, target_height: BlockHeight, + min_confirmations: u32, ) -> Result< - Proposal::DataSource as WalletRead>::NoteRef>, - InputSelectorError<<::DataSource as WalletRead>::Error, Self::Error>, + Proposal<::FeeRule, Infallible>, + InputSelectorError< + ::Error, + Self::Error, + ChangeT::Error, + Infallible, + >, > where - ParamsT: consensus::Parameters; + ParamsT: consensus::Parameters, + ChangeT: ChangeStrategy; } /// Errors that can occur as a consequence of greedy input selection. #[derive(Debug, Clone, PartialEq, Eq)] -pub enum GreedyInputSelectorError { +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), - /// An error was encountered in change selection. - Change(ChangeError), + /// Support for transparent-source-only (TEX) addresses requires the transparent-inputs feature. + UnsupportedTexAddress, } -impl fmt::Display for GreedyInputSelectorError { +impl fmt::Display for GreedyInputSelectorError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self { GreedyInputSelectorError::Balance(e) => write!( @@ -206,49 +271,58 @@ impl fmt::Display for GreedyInputSelectorErro // don't have network parameters here write!(f, "Unified address contains no supported receivers.") } - GreedyInputSelectorError::Change(err) => { - write!(f, "An error occurred computing change and fees: {}", err) + GreedyInputSelectorError::UnsupportedTexAddress => { + write!(f, "Support for transparent-source-only (TEX) addresses requires the transparent-inputs feature.") } } } } -impl - From> - for InputSelectorError> +impl From + for InputSelectorError { - fn from(err: GreedyInputSelectorError) -> Self { + fn from(err: GreedyInputSelectorError) -> Self { InputSelectorError::Selection(err) } } -impl From> - for InputSelectorError> -{ - fn from(err: ChangeError) -> Self { - InputSelectorError::Selection(GreedyInputSelectorError::Change(err)) - } -} - -impl From - for InputSelectorError> +impl From + for InputSelectorError { fn from(err: BalanceError) -> Self { InputSelectorError::Selection(GreedyInputSelectorError::Balance(err)) } } -pub(crate) struct SaplingPayment(Amount); +pub(crate) struct SaplingPayment(Zatoshis); #[cfg(test)] impl SaplingPayment { - pub(crate) fn new(amount: Amount) -> Self { + pub(crate) fn new(amount: Zatoshis) -> Self { SaplingPayment(amount) } } impl sapling::OutputView for SaplingPayment { - fn value(&self) -> Amount { + 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 } } @@ -256,135 +330,433 @@ impl sapling::OutputView for SaplingPayment { /// An [`InputSelector`] implementation that uses a greedy strategy to select between available /// notes. /// -/// This implementation performs input selection using methods available via the [`WalletRead`] -/// interface. -pub struct GreedyInputSelector { - change_strategy: ChangeT, - dust_output_policy: DustOutputPolicy, +/// This implementation performs input selection using methods available via the +/// [`InputSource`] interface. +pub struct GreedyInputSelector { _ds_type: PhantomData, } -impl GreedyInputSelector { +impl GreedyInputSelector { /// Constructs a new greedy input selector that uses the provided change strategy to determine /// change values and fee amounts. - pub fn new(change_strategy: ChangeT, dust_output_policy: DustOutputPolicy) -> Self { + /// + /// 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 { - change_strategy, - dust_output_policy, _ds_type: PhantomData, } } } -impl InputSelector for GreedyInputSelector -where - DbT: WalletRead, - ChangeT: ChangeStrategy, - ChangeT::FeeRule: Clone, -{ - type Error = GreedyInputSelectorError; - type DataSource = DbT; - type FeeRule = ChangeT::FeeRule; +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( + fn propose_transaction( &self, params: &ParamsT, - wallet_db: &Self::DataSource, - account: AccountId, - anchor_height: BlockHeight, + wallet_db: &Self::InputSource, target_height: BlockHeight, + anchor_height: BlockHeight, + account: ::AccountId, transaction_request: TransactionRequest, - ) -> Result, InputSelectorError> + 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![]; - let mut output_total = Amount::zero(); - for payment in transaction_request.payments() { - output_total = (output_total + payment.amount).ok_or(BalanceError::Overflow)?; - - let mut push_transparent = |taddr: TransparentAddress| { - transparent_outputs.push(TxOut { - value: payment.amount, - script_pubkey: taddr.script(), - }); - }; - let mut push_sapling = || { - sapling_outputs.push(SaplingPayment(payment.amount)); - }; - - match &payment.recipient_address { - RecipientAddress::Transparent(addr) => { - push_transparent(*addr); + #[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, + )); } - RecipientAddress::Shielded(_) => { - push_sapling(); + Address::Sapling(_) => { + payment_pools.insert(*idx, PoolType::SAPLING); + sapling_outputs.push(SaplingPayment(payment.amount())); } - RecipientAddress::Unified(addr) => { - if addr.sapling().is_some() { - push_sapling(); - } else if let Some(addr) = addr.transparent() { - push_transparent(*addr); - } else { - return Err(InputSelectorError::Selection( - GreedyInputSelectorError::UnsupportedAddress(Box::new(addr.clone())), - )); + 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 sapling_inputs: Vec> = vec![]; - let mut prior_available = Amount::zero(); - let mut amount_required = Amount::zero(); + 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 { - let balance = self.change_strategy.compute_balance( + #[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, - &Vec::::new(), + &[] as &[WalletTransparentOutput], &transparent_outputs, - &sapling_inputs, - &sapling_outputs, - &self.dust_output_policy, + &( + ::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) => { - return Ok(Proposal { + // 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, - transparent_inputs: vec![], - sapling_inputs, + payment_pools, + vec![], + shielded_inputs, balance, - fee_rule: (*self.change_strategy.fee_rule()).clone(), + (*change_strategy.fee_rule()).clone(), target_height, - is_shielding: false, - }); + false, + ) + .map_err(InputSelectorError::Proposal); } - Err(ChangeError::DustInputs { mut sapling, .. }) => { + 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(other.into()), + Err(other) => return Err(InputSelectorError::Change(other)), } - sapling_inputs = wallet_db - .select_spendable_sapling_notes(account, amount_required, anchor_height, &exclude) + #[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, + TargetValue::AtLeast(amount_required), + selectable_pools, + anchor_height, + &exclude, + ) .map_err(InputSelectorError::DataSource)?; - let new_available = sapling_inputs - .iter() - .map(|n| n.note_value) - .sum::>() - .ok_or(BalanceError::Overflow)?; - + let new_available = shielded_inputs.total_value()?; if new_available <= prior_available { return Err(InputSelectorError::InsufficientFunds { required: amount_required, @@ -397,37 +769,57 @@ where } } } +} + +#[cfg(feature = "transparent-inputs")] +impl ShieldingSelector for GreedyInputSelector { + type Error = GreedyInputSelectorError; + type InputSource = DbT; #[allow(clippy::type_complexity)] - fn propose_shielding( + fn propose_shielding( &self, params: &ParamsT, - wallet_db: &Self::DataSource, - shielding_threshold: NonNegativeAmount, + wallet_db: &Self::InputSource, + change_strategy: &ChangeT, + shielding_threshold: Zatoshis, source_addrs: &[TransparentAddress], - confirmed_height: BlockHeight, + to_account: ::AccountId, target_height: BlockHeight, - ) -> Result, InputSelectorError> + 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_unspent_transparent_outputs(taddr, confirmed_height, &[])) + .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 trial_balance = self.change_strategy.compute_balance( + 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, - &Vec::::new(), - &Vec::>::new(), - &Vec::::new(), - &self.dust_output_policy, + &[] as &[TxOut], + &sapling::EmptyBundleView, + #[cfg(feature = "orchard")] + &orchard_fees::EmptyBundleView, + None, + &wallet_meta, ); let balance = match trial_balance { @@ -436,35 +828,37 @@ where let exclusions: BTreeSet = transparent.into_iter().collect(); transparent_inputs.retain(|i| !exclusions.contains(i.outpoint())); - self.change_strategy.compute_balance( + change_strategy.compute_balance( params, target_height, &transparent_inputs, - &Vec::::new(), - &Vec::>::new(), - &Vec::::new(), - &self.dust_output_policy, + &[] as &[TxOut], + &sapling::EmptyBundleView, + #[cfg(feature = "orchard")] + &orchard_fees::EmptyBundleView, + None, + &wallet_meta, )? } - Err(other) => { - return Err(other.into()); - } + Err(other) => return Err(InputSelectorError::Change(other)), }; - if balance.total() >= shielding_threshold.into() { - Ok(Proposal { - transaction_request: TransactionRequest::empty(), + if balance.total() >= shielding_threshold { + Proposal::single_step( + TransactionRequest::empty(), + BTreeMap::new(), transparent_inputs, - sapling_inputs: vec![], + None, balance, - fee_rule: (*self.change_strategy.fee_rule()).clone(), + (*change_strategy.fee_rule()).clone(), target_height, - is_shielding: true, - }) + true, + ) + .map_err(InputSelectorError::Proposal) } else { Err(InputSelectorError::InsufficientFunds { available: balance.total(), - required: shielding_threshold.into(), + required: shielding_threshold, }) } } diff --git a/zcash_client_backend/src/decrypt.rs b/zcash_client_backend/src/decrypt.rs index eb7e78e2df..df855b0284 100644 --- a/zcash_client_backend/src/decrypt.rs +++ b/zcash_client_backend/src/decrypt.rs @@ -1,19 +1,22 @@ 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::{ - consensus::{self, BlockHeight}, + transaction::components::sapling::zip212_enforcement, transaction::Transaction, +}; +use zcash_protocol::{ + consensus::{self, BlockHeight, NetworkUpgrade}, memo::MemoBytes, - sapling::{ - self, - note_encryption::{ - try_sapling_note_decryption, try_sapling_output_recovery, PreparedIncomingViewingKey, - }, - }, - transaction::Transaction, - zip32::{AccountId, Scope}, + value::Zatoshis, }; +use zip32::Scope; + +use crate::data_api::DecryptedTransaction; -use crate::keys::UnifiedFullViewingKey; +#[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)] @@ -30,41 +33,115 @@ pub enum TransferType { } /// 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, + 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::keys::OutgoingViewingKey - pub transfer_type: TransferType, + 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 /// [`UnifiedFullViewingKey`]s. -pub fn decrypt_transaction( +/// +/// # Parameters +/// - `params`: The network parameters corresponding to the network the transaction +/// was created for. +/// - `mined_height`: The height at which the transaction was mined, or `None` for +/// unmined transactions. +/// - `chain_tip_height`: The current chain tip height, if known. This parameter +/// will be unused if `mined_height.is_some()`. +/// - `tx`: The transaction to decrypt. +/// - `ufvks`: The [`UnifiedFullViewingKey`]s to use in trial decryption, keyed +/// by the identifiers for the wallet accounts they correspond to. +pub fn decrypt_transaction<'a, P: consensus::Parameters, AccountId: Copy>( params: &P, - height: BlockHeight, - tx: &Transaction, + mined_height: Option, + chain_tip_height: Option, + tx: &'a Transaction, ufvks: &HashMap, -) -> Vec> { - tx.sapling_bundle() +) -> DecryptedTransaction<'a, AccountId> { + let zip212_enforcement = zip212_enforcement( + params, + // Height is block height for mined transactions, and the "mempool height" (chain height + 1) + // for mempool transactions. We fall back to Sapling activation if we have no other + // information. + mined_height.unwrap_or_else(|| { + chain_tip_height + .map(|max_height| max_height + 1) // "mempool height" + .or_else(|| params.activation_height(NetworkUpgrade::Sapling)) + .expect("Sapling activation height must be known.") + }), + ); + let sapling_bundle = tx.sapling_bundle(); + let sapling_outputs = sapling_bundle .iter() .flat_map(|bundle| { ufvks .iter() - .flat_map(move |(account, ufvk)| { - ufvk.sapling().into_iter().map(|dfvk| (*account, dfvk)) - }) - .flat_map(move |(account, dfvk)| { + .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 = @@ -76,31 +153,97 @@ pub fn decrypt_transaction( .iter() .enumerate() .flat_map(move |(index, output)| { - try_sapling_note_decryption(params, height, &ivk_external, output) + try_note_decryption(&sapling_domain, &ivk_external, output) .map(|ret| (ret, TransferType::Incoming)) .or_else(|| { - try_sapling_note_decryption( - params, - height, - &ivk_internal, + 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, ) - .map(|ret| (ret, TransferType::WalletInternal)) }) + }) + }) + }) + .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_sapling_output_recovery(params, height, &ovk, output) - .map(|ret| (ret, TransferType::Outgoing)) + 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 { - index, - note, - account, - memo, - transfer_type, + .map(move |((note, _, memo), transfer_type)| { + DecryptedOutput::new( + index, + note, + account, + MemoBytes::from_bytes(&memo).expect("correct length"), + transfer_type, + ) }) }) }) }) - .collect() + .collect(); + + DecryptedTransaction::new( + mined_height, + tx, + sapling_outputs, + #[cfg(feature = "orchard")] + orchard_outputs, + ) } diff --git a/zcash_client_backend/src/fees.rs b/zcash_client_backend/src/fees.rs index b83477159e..4591ef740e 100644 --- a/zcash_client_backend/src/fees.rs +++ b/zcash_client_backend/src/fees.rs @@ -1,31 +1,148 @@ -use std::fmt; +use std::{ + fmt::{self, Debug, Display}, + num::{NonZeroU64, NonZeroUsize}, +}; -use zcash_primitives::{ +use ::transparent::bundle::OutPoint; +use zcash_primitives::transaction::fees::{ + transparent::{self, InputSize}, + zip317::{self as prim_zip317}, + FeeRule, +}; +use zcash_protocol::{ consensus::{self, BlockHeight}, - transaction::{ - components::{ - amount::{Amount, BalanceError}, - sapling::fees as sapling, - transparent::fees as transparent, - OutPoint, - }, - fees::FeeRule, - }, + 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; -/// A proposed change amount and output pool. +/// 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 enum ChangeValue { - Sapling(Amount), +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 { - pub fn value(&self) -> Amount { - match self { - ChangeValue::Sapling(value) => *value, + /// 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, } } } @@ -36,38 +153,43 @@ impl ChangeValue { #[derive(Clone, Debug, PartialEq, Eq)] pub struct TransactionBalance { proposed_change: Vec, - fee_required: Amount, - total: Amount, + 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: Amount) -> Option { - proposed_change + pub fn new(proposed_change: Vec, fee_required: Zatoshis) -> Result { + let total = proposed_change .iter() - .map(|v| v.value()) - .chain(Some(fee_required)) - .sum::>() - .map(|total| TransactionBalance { - proposed_change, - fee_required, - total, - }) + .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. + /// 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) -> Amount { + 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) -> Amount { + pub fn total(&self) -> Zatoshis { self.total } } @@ -79,22 +201,37 @@ pub enum ChangeError { /// required outputs and fees. InsufficientFunds { /// The total of the inputs provided to change selection - available: Amount, + available: Zatoshis, /// The total amount of input value required to fund the requested outputs, /// including the required fees. - required: Amount, + required: Zatoshis, }, - /// Some of the inputs provided to the transaction were determined to currently have no - /// economic value (i.e. their inclusion in a transaction causes fees to rise in an amount - /// greater than their value.) + /// 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 corresponding to transparent inputs having no current economic value. + /// 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 having not current economic value + /// 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 { @@ -106,48 +243,73 @@ impl fmt::Display for ChangeError { } => write!( f, "Insufficient funds: required {} zatoshis, but only {} zatoshis were available.", - i64::from(required), - i64::from(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()) + 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 From for ChangeError { - fn from(err: BalanceError) -> ChangeError { - ChangeError::StrategyError(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 tak when a transaction would potentially create dust -/// outputs (outputs that are likely to be without economic value due to fee rules.) +/// 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 + /// 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.) +/// 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, + dust_threshold: Option, } impl DustOutputPolicy { @@ -155,22 +317,22 @@ impl DustOutputPolicy { /// /// 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 - /// recommended, but an explicit value (including zero) may be provided to explicitly + /// 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 { + 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 + /// 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 { + pub fn dust_threshold(&self) -> Option { self.dust_threshold } } @@ -181,16 +343,177 @@ impl Default for DustOutputPolicy { } } +/// 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; + 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. /// @@ -198,6 +521,24 @@ pub trait ChangeStrategy { /// 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, @@ -205,20 +546,22 @@ pub trait ChangeStrategy { target_height: BlockHeight, transparent_inputs: &[impl transparent::InputView], transparent_outputs: &[impl transparent::OutputView], - sapling_inputs: &[impl sapling::InputView], - sapling_outputs: &[impl sapling::OutputView], - dust_output_policy: &DustOutputPolicy, + 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 zcash_primitives::transaction::components::{ - amount::Amount, - sapling::fees as sapling, - transparent::{fees as transparent, OutPoint, TxOut}, - }; + 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, @@ -235,14 +578,14 @@ pub(crate) mod tests { pub(crate) struct TestSaplingInput { pub note_id: u32, - pub value: Amount, + pub value: Zatoshis, } impl sapling::InputView for TestSaplingInput { fn note_id(&self) -> &u32 { &self.note_id } - fn value(&self) -> Amount { + 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..c516017998 --- /dev/null +++ b/zcash_client_backend/src/fees/common.rs @@ -0,0 +1,769 @@ +use core::cmp::{max, min, Ordering}; +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 + }, + }; + assert!(target_change_counts.total_shielded() == target_change_count); + + // We don't create a fully-transparent transaction if a change memo is used. + let fully_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 fully_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 subtotal_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) + } + }; + + 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), + ); + + // Once we calculate the balance with minimum fee (i.e. with no change), + // there are three cases: + // + // 1. Insufficient funds even with minimum fee. + // 2. The minimum fee exactly cancels out the net flow balance. + // 3. The minimum fee is smaller than the change. + // + // If case 2 happens for a transaction with any shielded flows, we want there + // to be a zero-value shielded change output anyway (i.e. treat this like case 3), + // 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 min_fee = 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 total_out_with_min_fee = (subtotal_out + min_fee).ok_or_else(overflow)?; + + #[allow(unused_mut)] + let (mut change, fee) = match total_in.cmp(&total_out_with_min_fee) { + Ordering::Less => { + // Case 1. Insufficient input value exists to pay the minimum fee; there's no way + // we can construct the transaction. + return Err(ChangeError::InsufficientFunds { + available: total_in, + required: total_out_with_min_fee, + }); + } + Ordering::Equal if fully_transparent => { + // Case 2 for a tx with all transparent flows and no change memo + // (e.g. the second transaction of a ZIP 320 pair). + (vec![], min_fee) + } + _ => { + let max_fee = max( + min_fee, + 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_with_max_fee = (subtotal_out + max_fee).ok_or_else(overflow)?; + + // 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 = usize::from(wallet_meta.map_or(NonZeroUsize::MIN, |wm| { + cfg.split_policy.split_count( + wm.total_note_count(), + wm.total_value(), + // We use a saturating subtraction here because there may be insufficient funds to pay + // the fee, *if* the requested number of split outputs are created. If there is no + // proposed change, the split policy should recommend only a single change output. + (total_in - total_out_with_max_fee).unwrap_or(Zatoshis::ZERO), + ) + })); + + // If we don't have as many change outputs as we expected, recompute the fee. + let total_fee = if split_count < target_change_count { + 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 { + split_count + } else { + 0 + })?, + orchard_action_count(if change_pool == ShieldedProtocol::Orchard { + split_count + } else { + 0 + })?, + ) + .map_err(|fee_error| ChangeError::StrategyError(E::from(fee_error)))? + } else { + max_fee + }; + + let total_out = (subtotal_out + total_fee).ok_or_else(overflow)?; + let total_change = + (total_in - total_out).ok_or_else(|| ChangeError::InsufficientFunds { + available: total_in, + required: total_out, + })?; + + let per_output_change = total_change.div_with_remainder( + NonZeroU64::new(u64::try_from(split_count).expect("usize fits into u64")).unwrap(), + ); + let simple_case = || { + ( + (0usize..split_count) + .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()) + .unwrap() + } else { + // For any other output, the change value will just be the + // quotient. + *per_output_change.quotient() + }, + change_memo.cloned(), + ) + }) + .collect(), + total_fee, + ) + }; + + let change_dust_threshold = cfg + .dust_output_policy + .dust_threshold() + .unwrap_or(cfg.default_dust_threshold); + + if total_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 total_change.is_zero() { + simple_case() + } else { + let shortfall = + (change_dust_threshold - total_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_change + total_fee).ok_or_else(overflow)?; + + let reasonable_fee = + (total_fee + (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 index 28b309bb5a..cf8d489d52 100644 --- a/zcash_client_backend/src/fees/fixed.rs +++ b/zcash_client_backend/src/fees/fixed.rs @@ -1,181 +1,150 @@ //! Change strategies designed for use with a fixed fee. -use std::cmp::Ordering; -use zcash_primitives::{ +use core::marker::PhantomData; + +use zcash_primitives::transaction::fees::{fixed::FeeRule as FixedFeeRule, transparent}; +use zcash_protocol::{ consensus::{self, BlockHeight}, - transaction::{ - components::{ - amount::{Amount, BalanceError}, - sapling::fees as sapling, - transparent::fees as transparent, - }, - fees::{fixed::FeeRule as FixedFeeRule, FeeRule}, - }, + memo::MemoBytes, + value::{BalanceError, Zatoshis}, + ShieldedProtocol, }; +use crate::data_api::InputSource; + use super::{ - ChangeError, ChangeStrategy, ChangeValue, DustAction, DustOutputPolicy, TransactionBalance, + common::{single_pool_output_balance, SinglePoolBalanceConfig}, + sapling as sapling_fees, ChangeError, ChangeStrategy, DustOutputPolicy, EphemeralBalance, + SplitPolicy, TransactionBalance, }; -/// A change strategy that and proposes change as a single output to the most current supported -/// shielded pool and delegates fee calculation to the provided fee rule. -pub struct SingleOutputChangeStrategy { +#[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. - pub fn new(fee_rule: FixedFeeRule) -> Self { - Self { fee_rule } +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 { +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_inputs: &[impl sapling::InputView], - sapling_outputs: &[impl sapling::OutputView], - dust_output_policy: &DustOutputPolicy, + sapling: &impl sapling_fees::BundleView, + #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView, + ephemeral_balance: Option<&EphemeralBalance>, + _wallet_meta: &Self::AccountMetaT, ) -> Result> { - let t_in = transparent_inputs - .iter() - .map(|t_in| t_in.coin().value) - .sum::>() - .ok_or(BalanceError::Overflow)?; - let t_out = transparent_outputs - .iter() - .map(|t_out| t_out.value()) - .sum::>() - .ok_or(BalanceError::Overflow)?; - let sapling_in = sapling_inputs - .iter() - .map(|s_in| s_in.value()) - .sum::>() - .ok_or(BalanceError::Overflow)?; - let sapling_out = sapling_outputs - .iter() - .map(|s_out| s_out.value()) - .sum::>() - .ok_or(BalanceError::Overflow)?; - - let fee_amount = self - .fee_rule - .fee_required( - params, - target_height, - transparent_inputs, - transparent_outputs, - sapling_inputs.len(), - sapling_outputs.len() + 1, - ) - .unwrap(); // fixed::FeeRule::fee_required is infallible. - - let total_in = (t_in + sapling_in).ok_or(BalanceError::Overflow)?; - - if (!transparent_inputs.is_empty() || !sapling_inputs.is_empty()) && fee_amount > total_in { - // For the fixed-fee selection rule, the only time we consider inputs dust is when the fee - // exceeds the value of all input values. - Err(ChangeError::DustInputs { - transparent: transparent_inputs - .iter() - .map(|i| i.outpoint()) - .cloned() - .collect(), - sapling: sapling_inputs - .iter() - .map(|i| i.note_id()) - .cloned() - .collect(), - }) - } else { - let total_out = [t_out, sapling_out, fee_amount] - .iter() - .sum::>() - .ok_or(BalanceError::Overflow)?; - - let proposed_change = (total_in - total_out).ok_or(BalanceError::Underflow)?; - match proposed_change.cmp(&Amount::zero()) { - Ordering::Less => Err(ChangeError::InsufficientFunds { - available: total_in, - required: total_out, - }), - Ordering::Equal => TransactionBalance::new(vec![], fee_amount) - .ok_or_else(|| BalanceError::Overflow.into()), - Ordering::Greater => { - let dust_threshold = dust_output_policy - .dust_threshold() - .unwrap_or_else(|| self.fee_rule.fixed_fee()); - - if dust_threshold > proposed_change { - match dust_output_policy.action() { - DustAction::Reject => { - let shortfall = (dust_threshold - proposed_change) - .ok_or(BalanceError::Underflow)?; - Err(ChangeError::InsufficientFunds { - available: total_in, - required: (total_in + shortfall) - .ok_or(BalanceError::Overflow)?, - }) - } - DustAction::AllowDustChange => TransactionBalance::new( - vec![ChangeValue::Sapling(proposed_change)], - fee_amount, - ) - .ok_or_else(|| BalanceError::Overflow.into()), - DustAction::AddDustToFee => TransactionBalance::new( - vec![], - (fee_amount + proposed_change).unwrap(), - ) - .ok_or_else(|| BalanceError::Overflow.into()), - } - } else { - TransactionBalance::new( - vec![ChangeValue::Sapling(proposed_change)], - fee_amount, - ) - .ok_or_else(|| BalanceError::Overflow.into()) - } - } - } - } + 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 zcash_primitives::{ + use ::transparent::bundle::TxOut; + use zcash_primitives::transaction::fees::{ + fixed::FeeRule as FixedFeeRule, zip317::MINIMUM_FEE, + }; + use zcash_protocol::{ consensus::{Network, NetworkUpgrade, Parameters}, - transaction::{ - components::{amount::Amount, transparent::TxOut}, - fees::fixed::FeeRule as FixedFeeRule, - }, + value::Zatoshis, + ShieldedProtocol, }; use super::SingleOutputChangeStrategy; use crate::{ - data_api::wallet::input_selection::SaplingPayment, + 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() { - #[allow(deprecated)] - let fee_rule = FixedFeeRule::standard(); - let change_strategy = SingleOutputChangeStrategy::new(fee_rule); + 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( @@ -183,28 +152,39 @@ mod tests { Network::TestNetwork .activation_height(NetworkUpgrade::Nu5) .unwrap(), - &Vec::::new(), - &Vec::::new(), - &[TestSaplingInput { - note_id: 0, - value: Amount::from_u64(60000).unwrap(), - }], - &[SaplingPayment::new(Amount::from_u64(40000).unwrap())], - &DustOutputPolicy::default(), + &[] 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(Amount::from_u64(10000).unwrap())] - && balance.fee_required() == Amount::from_u64(10000).unwrap() + Ok(balance) if + balance.proposed_change() == [ChangeValue::sapling(Zatoshis::const_from_u64(10000), None)] && + balance.fee_required() == MINIMUM_FEE ); } #[test] fn dust_change() { - #[allow(deprecated)] - let fee_rule = FixedFeeRule::standard(); - let change_strategy = SingleOutputChangeStrategy::new(fee_rule); + 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( @@ -212,27 +192,33 @@ mod tests { Network::TestNetwork .activation_height(NetworkUpgrade::Nu5) .unwrap(), - &Vec::::new(), - &Vec::::new(), - &[ - TestSaplingInput { - note_id: 0, - value: Amount::from_u64(40000).unwrap(), - }, - // enough to pay a fee, plus dust - TestSaplingInput { - note_id: 0, - value: Amount::from_u64(10100).unwrap(), - }, - ], - &[SaplingPayment::new(Amount::from_u64(40000).unwrap())], - &DustOutputPolicy::default(), + &[] 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 == Amount::from_u64(50100).unwrap() && required == Amount::from_u64(60000).unwrap() + 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 index 142bf7ce65..2fa63510ae 100644 --- a/zcash_client_backend/src/fees/zip317.rs +++ b/zcash_client_backend/src/fees/zip317.rs @@ -3,246 +3,303 @@ //! 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::cmp::Ordering; -use zcash_primitives::{ +use core::marker::PhantomData; + +use zcash_primitives::transaction::fees::{transparent, zip317 as prim_zip317, FeeRule}; +use zcash_protocol::{ consensus::{self, BlockHeight}, - transaction::{ - components::{ - amount::{Amount, BalanceError}, - sapling::fees as sapling, - transparent::fees as transparent, - }, - fees::{ - zip317::{FeeError as Zip317FeeError, FeeRule as Zip317FeeRule}, - FeeRule, - }, - }, + memo::MemoBytes, + value::{BalanceError, Zatoshis}, + ShieldedProtocol, +}; + +use crate::{ + data_api::{AccountMeta, InputSource, NoteFilter}, + fees::StandardFeeRule, }; use super::{ - ChangeError, ChangeStrategy, ChangeValue, DustAction, DustOutputPolicy, TransactionBalance, + common::{single_pool_output_balance, SinglePoolBalanceConfig}, + sapling as sapling_fees, ChangeError, ChangeStrategy, DustOutputPolicy, EphemeralBalance, + SplitPolicy, TransactionBalance, }; -/// A change strategy that and proposes change as a single output to the most current supported -/// shielded pool and delegates fee calculation to the provided fee rule. -pub struct SingleOutputChangeStrategy { - fee_rule: Zip317FeeRule, +#[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 SingleOutputChangeStrategy { +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. - pub fn new(fee_rule: Zip317FeeRule) -> Self { - Self { fee_rule } + /// 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 { - type FeeRule = Zip317FeeRule; - type Error = Zip317FeeError; +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_inputs: &[impl sapling::InputView], - sapling_outputs: &[impl sapling::OutputView], - dust_output_policy: &DustOutputPolicy, + sapling: &impl sapling_fees::BundleView, + #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView, + ephemeral_balance: Option<&EphemeralBalance>, + _wallet_meta: &Self::AccountMetaT, ) -> Result> { - let mut transparent_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 < self.fee_rule.marginal_fee() { - Some(i.outpoint().clone()) - } else { - None - } - }) - .collect(); - - let mut sapling_dust: Vec<_> = sapling_inputs - .iter() - .filter_map(|i| { - if i.value() < self.fee_rule.marginal_fee() { - Some(i.note_id().clone()) - } else { - None - } - }) - .collect(); - - // Depending on the shape of the transaction, we may be able to spend up to - // `grace_actions - 1` dust inputs. If we don't have any dust inputs though, - // we don't need to worry about any of that. - if !(transparent_dust.is_empty() && sapling_dust.is_empty()) { - let t_non_dust = transparent_inputs.len() - transparent_dust.len(); - let t_allowed_dust = transparent_outputs.len().saturating_sub(t_non_dust); - - // We add one to the sapling outputs for the (single) change output Note that this - // means that wallet-internal shielding transactions are an opportunity to spend a dust - // note. - let s_non_dust = sapling_inputs.len() - sapling_dust.len(); - let s_allowed_dust = (sapling_outputs.len() + 1).saturating_sub(s_non_dust); - - let available_grace_inputs = self - .fee_rule - .grace_actions() - .saturating_sub(t_non_dust) - .saturating_sub(s_non_dust); - - let mut t_disallowed_dust = transparent_dust.len().saturating_sub(t_allowed_dust); - let mut s_disallowed_dust = sapling_dust.len().saturating_sub(s_allowed_dust); - - if available_grace_inputs > 0 { - // If we have available grace inputs, allocate them first to transparent dust - // and then to Sapling dust. The caller has provided inputs that it is willing - // to spend, so we don't need to consider privacy effects at this layer. - let t_grace_dust = available_grace_inputs.saturating_sub(t_disallowed_dust); - t_disallowed_dust = t_disallowed_dust.saturating_sub(t_grace_dust); - - let s_grace_dust = available_grace_inputs - .saturating_sub(t_grace_dust) - .saturating_sub(s_disallowed_dust); - s_disallowed_dust = s_disallowed_dust.saturating_sub(s_grace_dust); - } - - // Truncate the lists of inputs to be disregarded in input selection to just the - // disallowed lengths. This has the effect of prioritizing inputs for inclusion by the - // order of the original input slices, with the most preferred inputs first. - transparent_dust.reverse(); - transparent_dust.truncate(t_disallowed_dust); - sapling_dust.reverse(); - sapling_dust.truncate(s_disallowed_dust); - - if !(transparent_dust.is_empty() && sapling_dust.is_empty()) { - return Err(ChangeError::DustInputs { - transparent: transparent_dust, - sapling: sapling_dust, - }); - } - } + 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(), + ); - let overflow = || ChangeError::StrategyError(Zip317FeeError::from(BalanceError::Overflow)); - let underflow = - || ChangeError::StrategyError(Zip317FeeError::from(BalanceError::Underflow)); - - let t_in = transparent_inputs - .iter() - .map(|t_in| t_in.coin().value) - .sum::>() - .ok_or_else(overflow)?; - let t_out = transparent_outputs - .iter() - .map(|t_out| t_out.value()) - .sum::>() - .ok_or_else(overflow)?; - let sapling_in = sapling_inputs - .iter() - .map(|s_in| s_in.value()) - .sum::>() - .ok_or_else(overflow)?; - let sapling_out = sapling_outputs - .iter() - .map(|s_out| s_out.value()) - .sum::>() - .ok_or_else(overflow)?; - - let fee_amount = self - .fee_rule - .fee_required( - params, - target_height, - transparent_inputs, - transparent_outputs, - sapling_inputs.len(), - // add one for Sapling change, then account for Sapling output padding performed by - // the transaction builder - std::cmp::max(sapling_outputs.len() + 1, 2), - ) - .map_err(ChangeError::StrategyError)?; - - let total_in = (t_in + sapling_in).ok_or_else(overflow)?; - - let total_out = [t_out, sapling_out, fee_amount] - .iter() - .sum::>() - .ok_or_else(overflow)?; - - let proposed_change = (total_in - total_out).ok_or_else(underflow)?; - match proposed_change.cmp(&Amount::zero()) { - Ordering::Less => Err(ChangeError::InsufficientFunds { - available: total_in, - required: total_out, - }), - Ordering::Equal => TransactionBalance::new(vec![], fee_amount).ok_or_else(overflow), - Ordering::Greater => { - let dust_threshold = dust_output_policy - .dust_threshold() - .unwrap_or_else(|| self.fee_rule.marginal_fee()); - - if dust_threshold > proposed_change { - match dust_output_policy.action() { - DustAction::Reject => { - let shortfall = - (dust_threshold - proposed_change).ok_or_else(underflow)?; - - Err(ChangeError::InsufficientFunds { - available: total_in, - required: (total_in + shortfall).ok_or_else(overflow)?, - }) - } - DustAction::AllowDustChange => TransactionBalance::new( - vec![ChangeValue::Sapling(proposed_change)], - fee_amount, - ) - .ok_or_else(overflow), - DustAction::AddDustToFee => { - TransactionBalance::new(vec![], (fee_amount + proposed_change).unwrap()) - .ok_or_else(overflow) - } - } - } else { - TransactionBalance::new(vec![ChangeValue::Sapling(proposed_change)], fee_amount) - .ok_or_else(overflow) - } - } + 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 zcash_primitives::{ + use ::transparent::{address::Script, bundle::TxOut}; + use zcash_primitives::transaction::fees::zip317::FeeRule as Zip317FeeRule; + use zcash_protocol::{ consensus::{Network, NetworkUpgrade, Parameters}, - legacy::Script, - transaction::{ - components::{amount::Amount, transparent::TxOut}, - fees::zip317::FeeRule as Zip317FeeRule, - }, + value::Zatoshis, + ShieldedProtocol, }; use super::SingleOutputChangeStrategy; use crate::{ - data_api::wallet::input_selection::SaplingPayment, + data_api::{ + testing::MockWalletDb, wallet::input_selection::SaplingPayment, AccountMeta, PoolMeta, + }, fees::{ tests::{TestSaplingInput, TestTransparentInput}, - ChangeError, ChangeStrategy, ChangeValue, DustOutputPolicy, + 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::new(Zip317FeeRule::standard()); + 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( @@ -250,26 +307,311 @@ mod tests { Network::TestNetwork .activation_height(NetworkUpgrade::Nu5) .unwrap(), - &Vec::::new(), - &Vec::::new(), - &[TestSaplingInput { - note_id: 0, - value: Amount::from_u64(55000).unwrap(), - }], - &[SaplingPayment::new(Amount::from_u64(40000).unwrap())], - &DustOutputPolicy::default(), + &[] 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_3750), None), + ChangeValue::sapling(Zatoshis::const_from_u64(124_3750), None), + ChangeValue::sapling(Zatoshis::const_from_u64(124_3750), None), + ChangeValue::sapling(Zatoshis::const_from_u64(124_3750), None), + ] && + balance.fee_required() == Zatoshis::const_from_u64(25000) + ); + } + + { + // spend a single Sapling note and produce no change outputs, as the value of outputs + // has been requested such that it exactly empties the wallet + 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(50000), + }][..], + &[SaplingPayment::new(Zatoshis::const_from_u64(40000))][..], + ), + #[cfg(feature = "orchard")] + &orchard_fees::EmptyBundleView, + None, + // after excluding the inputs we're spending, we have no notes in the wallet + &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::ZERO, None)] && + balance.fee_required() == Zatoshis::const_from_u64(10000) + ); + } + + { + // spend a single Sapling note, with insufficient funds to cover the minimum 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(50000), + }][..], + &[SaplingPayment::new(Zatoshis::const_from_u64(40001))][..], + ), + #[cfg(feature = "orchard")] + &orchard_fees::EmptyBundleView, + None, + // after excluding the inputs we're spending, we have no notes in the wallet + &AccountMeta::new( + Some(PoolMeta::new(0, Zatoshis::ZERO)), + Some(PoolMeta::new(0, Zatoshis::ZERO)), + ), + ); + + assert_matches!( + result, + Err(ChangeError::InsufficientFunds { available, required }) + if available == Zatoshis::const_from_u64(50000) + && required == Zatoshis::const_from_u64(50001) + ); + } + + { + // Spend a single Sapling note, creating two output notes that cause the transaction to + // balance exactly. This will fail, because even though there are enough funds in the + // wallet for the transaction to go through, and the fee is correct for a two-output + // transaction, we prohibit this case in order to prevent the transaction recipients + // from being able to reason about the value of the input note via knowledge that there + // is no change output. + 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(50000), + }][..], + &[ + SaplingPayment::new(Zatoshis::const_from_u64(30000)), + SaplingPayment::new(Zatoshis::const_from_u64(10000)), + ][..], + ), + #[cfg(feature = "orchard")] + &orchard_fees::EmptyBundleView, + None, + // after excluding the inputs we're spending, we have no notes in the wallet + &AccountMeta::new( + Some(PoolMeta::new(0, Zatoshis::ZERO)), + Some(PoolMeta::new(0, Zatoshis::ZERO)), + ), + ); + + assert_matches!( + result, + Err(ChangeError::InsufficientFunds { available, required }) + if available == Zatoshis::const_from_u64(50000) + && required == Zatoshis::const_from_u64(55000) + ); + } + } + + #[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::Sapling(Amount::from_u64(5000).unwrap())] - && balance.fee_required() == Amount::from_u64(10000).unwrap() + 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() { - let change_strategy = SingleOutputChangeStrategy::new(Zip317FeeRule::standard()); + 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( @@ -277,90 +619,277 @@ mod tests { Network::TestNetwork .activation_height(NetworkUpgrade::Nu5) .unwrap(), - &Vec::::new(), + &[] as &[TestTransparentInput], &[TxOut { - value: Amount::from_u64(40000).unwrap(), + value: Zatoshis::const_from_u64(40000), script_pubkey: Script(vec![]), }], - &[TestSaplingInput { - note_id: 0, - value: Amount::from_u64(55000).unwrap(), + &( + 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![]), }], - &Vec::::new(), - &DustOutputPolicy::default(), + &sapling_fees::EmptyBundleView, + #[cfg(feature = "orchard")] + &orchard_fees::EmptyBundleView, + None, + &(), ); assert_matches!( result, - Ok(balance) if balance.proposed_change().is_empty() - && balance.fee_required() == Amount::from_u64(15000).unwrap() + Ok(balance) if + balance.proposed_change().is_empty() && + balance.fee_required() == Zatoshis::const_from_u64(10000) ); } #[test] - fn change_with_allowable_dust() { - let change_strategy = SingleOutputChangeStrategy::new(Zip317FeeRule::standard()); + #[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 Sapling note that is sufficient to pay the fee - let result = change_strategy.compute_balance( + // 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(), - &Vec::::new(), - &Vec::::new(), - &[ - TestSaplingInput { - note_id: 0, - value: Amount::from_u64(49000).unwrap(), + &[TestTransparentInput { + outpoint: OutPoint::fake(), + coin: TxOut { + value: Zatoshis::const_from_u64(63000), + script_pubkey: TransparentAddress::PublicKeyHash([0u8; 20]).script(), }, - TestSaplingInput { - note_id: 1, - value: Amount::from_u64(1000).unwrap(), + }], + &[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(), }, - ], - &[SaplingPayment::new(Amount::from_u64(40000).unwrap())], - &DustOutputPolicy::default(), + }], + &[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().is_empty() - && balance.fee_required() == Amount::from_u64(10000).unwrap() + 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::new(Zip317FeeRule::standard()); + 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 + // 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(), - &Vec::::new(), - &Vec::::new(), - &[ - TestSaplingInput { - note_id: 0, - value: Amount::from_u64(29000).unwrap(), - }, - TestSaplingInput { - note_id: 1, - value: Amount::from_u64(20000).unwrap(), - }, - TestSaplingInput { - note_id: 2, - value: Amount::from_u64(1000).unwrap(), - }, - ], - &[SaplingPayment::new(Amount::from_u64(40000).unwrap())], - &DustOutputPolicy::default(), + &[] 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 now isn't free to add + // We will get an error here, because the dust input isn't free to add // to the transaction. assert_matches!( result, diff --git a/zcash_client_backend/src/keys.rs b/zcash_client_backend/src/keys.rs deleted file mode 100644 index 54a585dcb4..0000000000 --- a/zcash_client_backend/src/keys.rs +++ /dev/null @@ -1,764 +0,0 @@ -//! Helper functions for managing light client key material. -use orchard; -use zcash_address::unified::{self, Container, Encoding}; -use zcash_primitives::{ - consensus, - zip32::{AccountId, DiversifierIndex}, -}; - -use crate::address::UnifiedAddress; - -#[cfg(feature = "transparent-inputs")] -use { - std::convert::TryInto, - zcash_primitives::legacy::keys::{self as legacy, IncomingViewingKey}, -}; - -#[cfg(feature = "unstable")] -use { - byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}, - std::convert::TryFrom, - std::io::{Read, Write}, - zcash_address::unified::Typecode, - zcash_encoding::CompactSize, - zcash_primitives::consensus::BranchId, -}; - -pub mod sapling { - pub use zcash_primitives::zip32::sapling::{ - DiversifiableFullViewingKey, ExtendedFullViewingKey, ExtendedSpendingKey, - }; - use zcash_primitives::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_primitives::{ - /// constants::testnet::COIN_TYPE, - /// zip32::AccountId, - /// }; - /// use zcash_client_backend::{ - /// keys::sapling, - /// }; - /// - /// let extsk = sapling::spending_key(&[0; 32][..], COIN_TYPE, AccountId::from(0)); - /// ``` - /// [`ExtendedSpendingKey`]: zcash_primitives::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), - ChildIndex::Hardened(account.into()), - ], - ) - } -} - -#[cfg(feature = "transparent-inputs")] -fn to_transparent_child_index(j: DiversifierIndex) -> Option { - let (low_4_bytes, rest) = j.0.split_at(4); - let transparent_j = u32::from_le_bytes(low_4_bytes.try_into().unwrap()); - if transparent_j > (0x7FFFFFFF) || rest.iter().any(|b| b != &0) { - None - } else { - Some(transparent_j) - } -} - -#[derive(Debug)] -#[doc(hidden)] -pub enum DerivationError { - Orchard(orchard::zip32::Error), - #[cfg(feature = "transparent-inputs")] - Transparent(hdwallet::error::Error), -} - -/// 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. -#[cfg(feature = "unstable")] -#[derive(Debug, PartialEq, Eq)] -pub enum DecodingError { - ReadError(&'static str), - EraInvalid, - EraMismatch(Era), - TypecodeInvalid, - LengthInvalid, - LengthMismatch(Typecode, u32), - InsufficientData(Typecode), - KeyDataInvalid(Typecode), -} - -#[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 viewing keys that are all associated with a single -/// ZIP-0032 account identifier. -#[derive(Clone, Debug)] -#[doc(hidden)] -pub struct UnifiedSpendingKey { - #[cfg(feature = "transparent-inputs")] - transparent: legacy::AccountPrivKey, - sapling: sapling::ExtendedSpendingKey, - orchard: orchard::keys::SpendingKey, -} - -#[doc(hidden)] -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"); - } - - let orchard = - orchard::keys::SpendingKey::from_zip32_seed(seed, params.coin_type(), account.into()) - .map_err(DerivationError::Orchard)?; - - #[cfg(feature = "transparent-inputs")] - let transparent = legacy::AccountPrivKey::from_seed(params, seed, account) - .map_err(DerivationError::Transparent)?; - - Ok(UnifiedSpendingKey { - #[cfg(feature = "transparent-inputs")] - transparent, - sapling: sapling::spending_key(seed, params.coin_type(), account), - orchard, - }) - } - - pub fn to_unified_full_viewing_key(&self) -> UnifiedFullViewingKey { - UnifiedFullViewingKey { - #[cfg(feature = "transparent-inputs")] - transparent: Some(self.transparent.to_account_pubkey()), - sapling: Some(self.sapling.to_diversifiable_full_viewing_key()), - 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) -> &legacy::AccountPrivKey { - &self.transparent - } - - /// Returns the Sapling extended spending key component of this unified spending key. - pub fn sapling(&self) -> &sapling::ExtendedSpendingKey { - &self.sapling - } - - /// Returns the Orchard spending key component of this unified spending key. - pub fn orchard(&self) -> &orchard::keys::SpendingKey { - &self.orchard - } - - /// Returns a binary encoding of this key suitable for decoding with [`decode`]. - /// - /// 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(); - - // 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(); - - // 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(); - - // transparent - #[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 [`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 = std::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)); - } - - let mut orchard = None; - 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))?; - 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))?; - sapling = Some( - sapling::ExtendedSpendingKey::from_bytes(&key) - .map_err(|_| DecodingError::KeyDataInvalid(Typecode::Sapling))?, - ); - } - #[cfg(feature = "transparent-inputs")] - Typecode::P2pkh => { - if len != 64 { - return Err(DecodingError::LengthMismatch(Typecode::P2pkh, len)); - } - - let mut key = [0u8; 64]; - source - .read_exact(&mut key) - .map_err(|_| DecodingError::InsufficientData(Typecode::P2pkh))?; - transparent = Some( - legacy::AccountPrivKey::from_bytes(&key) - .ok_or(DecodingError::KeyDataInvalid(Typecode::P2pkh))?, - ); - } - _ => { - return Err(DecodingError::TypecodeInvalid); - } - } - - #[cfg(feature = "transparent-inputs")] - let has_transparent = transparent.is_some(); - #[cfg(not(feature = "transparent-inputs"))] - let has_transparent = true; - - if orchard.is_some() && sapling.is_some() && has_transparent { - return Ok(UnifiedSpendingKey { - orchard: orchard.unwrap(), - sapling: sapling.unwrap(), - #[cfg(feature = "transparent-inputs")] - transparent: transparent.unwrap(), - }); - } - } - } -} - -/// A [ZIP 316](https://zips.z.cash/zip-0316) unified full viewing key. -#[derive(Clone, Debug)] -#[doc(hidden)] -pub struct UnifiedFullViewingKey { - #[cfg(feature = "transparent-inputs")] - transparent: Option, - sapling: Option, - orchard: Option, - unknown: Vec<(u32, Vec)>, -} - -#[doc(hidden)] -impl UnifiedFullViewingKey { - /// Construct a new unified full viewing key, if the required components are present. - pub fn new( - #[cfg(feature = "transparent-inputs")] transparent: Option, - sapling: Option, - orchard: Option, - ) -> Option { - if sapling.is_none() { - None - } else { - Some(UnifiedFullViewingKey { - #[cfg(feature = "transparent-inputs")] - transparent, - sapling, - 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::Ufvk::decode(encoding).map_err(|e| e.to_string())?; - let expected_net = params.address_network().expect("Unrecognized network"); - if net != expected_net { - return Err(format!( - "UFVK is for network {:?} but we expected {:?}", - net, expected_net, - )); - } - - let mut orchard = None; - 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 { - unified::Fvk::Orchard(data) => orchard::keys::FullViewingKey::from_bytes(data) - .ok_or("Invalid Orchard FVK in Unified FVK") - .map(|addr| { - orchard = Some(addr); - None - }) - .transpose(), - unified::Fvk::Sapling(data) => { - sapling::DiversifiableFullViewingKey::from_bytes(data) - .ok_or("Invalid Sapling FVK in Unified FVK") - .map(|pa| { - sapling = Some(pa); - None - }) - .transpose() - } - #[cfg(feature = "transparent-inputs")] - unified::Fvk::P2pkh(data) => legacy::AccountPubKey::deserialize(data) - .map_err(|_| "Invalid transparent FVK in Unified FVK") - .map(|tfvk| { - transparent = Some(tfvk); - None - }) - .transpose(), - #[cfg(not(feature = "transparent-inputs"))] - unified::Fvk::P2pkh(data) => { - Some(Ok((unified::Typecode::P2pkh.into(), data.to_vec()))) - } - unified::Fvk::Unknown { typecode, data } => Some(Ok((*typecode, data.clone()))), - }) - .collect::>()?; - - Ok(Self { - #[cfg(feature = "transparent-inputs")] - transparent, - sapling, - orchard, - unknown, - }) - } - - /// Returns the string encoding of this `UnifiedFullViewingKey` for the given network. - pub fn encode(&self, params: &P) -> String { - let items = std::iter::empty() - .chain( - self.orchard - .as_ref() - .map(|fvk| fvk.to_bytes()) - .map(unified::Fvk::Orchard), - ) - .chain( - self.sapling - .as_ref() - .map(|dfvk| dfvk.to_bytes()) - .map(unified::Fvk::Sapling), - ) - .chain( - self.unknown - .iter() - .map(|(typecode, data)| unified::Fvk::Unknown { - typecode: *typecode, - data: data.clone(), - }), - ); - #[cfg(feature = "transparent-inputs")] - let items = items.chain( - self.transparent - .as_ref() - .map(|tfvk| tfvk.serialize().try_into().unwrap()) - .map(unified::Fvk::P2pkh), - ); - - let ufvk = unified::Ufvk::try_from_items(items.collect()) - .expect("UnifiedFullViewingKey should only be constructed safely"); - ufvk.encode(¶ms.address_network().expect("Unrecognized network")) - } - - /// Returns the transparent component of the unified key at the - /// BIP44 path `m/44'/'/'`. - #[cfg(feature = "transparent-inputs")] - pub fn transparent(&self) -> Option<&legacy::AccountPubKey> { - self.transparent.as_ref() - } - - /// Returns the Sapling diversifiable full viewing key component of this unified key. - pub fn sapling(&self) -> Option<&sapling::DiversifiableFullViewingKey> { - self.sapling.as_ref() - } - - /// Returns the Orchard full viewing key component of this unified key. - pub fn orchard(&self) -> Option<&orchard::keys::FullViewingKey> { - self.orchard.as_ref() - } - - /// Attempts to derive the Unified Address for the given diversifier index. - /// - /// Returns `None` if the specified index does not produce a valid diversifier. - // TODO: Allow filtering down by receiver types? - pub fn address(&self, j: DiversifierIndex) -> Option { - let sapling = if let Some(extfvk) = self.sapling.as_ref() { - Some(extfvk.address(j)?) - } else { - None - }; - - #[cfg(feature = "transparent-inputs")] - let transparent = if let Some(tfvk) = self.transparent.as_ref() { - match to_transparent_child_index(j) { - Some(transparent_j) => match tfvk - .derive_external_ivk() - .and_then(|tivk| tivk.derive_address(transparent_j)) - { - Ok(taddr) => Some(taddr), - Err(_) => return None, - }, - // Diversifier doesn't generate a valid transparent child index. - None => return None, - } - } else { - None - }; - #[cfg(not(feature = "transparent-inputs"))] - let transparent = None; - - UnifiedAddress::from_receivers(None, sapling, transparent) - } - - /// 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. - /// - /// Returns `None` if no valid diversifier exists - pub fn find_address( - &self, - mut j: DiversifierIndex, - ) -> Option<(UnifiedAddress, DiversifierIndex)> { - // 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 self.transparent.is_some() && to_transparent_child_index(j).is_none() { - return None; - } - - // Find a working diversifier and construct the associated address. - loop { - let res = self.address(j); - if let Some(ua) = res { - break Some((ua, j)); - } - if j.increment().is_err() { - break None; - } - } - } - - /// Returns the Unified Address corresponding to the smallest valid diversifier index, - /// along with that index. - pub fn default_address(&self) -> (UnifiedAddress, DiversifierIndex) { - self.find_address(DiversifierIndex::new()) - .expect("UFVK should have at least one valid diversifier") - } -} - -#[cfg(any(test, feature = "test-dependencies"))] -pub mod testing { - use proptest::prelude::*; - - use super::UnifiedSpendingKey; - use zcash_primitives::{consensus::Network, 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::from(account)) - }) - .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 super::{sapling, UnifiedFullViewingKey}; - use zcash_primitives::{consensus::MAIN_NETWORK, zip32::AccountId}; - - #[cfg(feature = "transparent-inputs")] - use { - crate::{address::RecipientAddress, encoding::AddressCodec}, - zcash_address::test_vectors, - zcash_primitives::{ - legacy::{ - self, - keys::{AccountPrivKey, IncomingViewingKey}, - }, - zip32::DiversifierIndex, - }, - }; - - #[cfg(feature = "unstable")] - use { - super::{testing::arb_unified_spending_key, Era, UnifiedSpendingKey}, - subtle::ConstantTimeEq, - zcash_primitives::consensus::Network, - }; - - #[cfg(feature = "transparent-inputs")] - fn seed() -> Vec { - let seed_hex = "6ef5f84def6f4b9d38f466586a8380a38593bd47c8cda77f091856176da47f26b5bd1c8d097486e5635df5a66e820d28e1d73346f499801c86228d43f390304f"; - hex::decode(seed_hex).unwrap() - } - - #[test] - #[should_panic] - fn spending_key_panics_on_short_seed() { - let _ = sapling::spending_key(&[0; 31][..], 0, AccountId::from(0)); - } - - #[cfg(feature = "transparent-inputs")] - #[test] - fn pk_to_taddr() { - let taddr = - legacy::keys::AccountPrivKey::from_seed(&MAIN_NETWORK, &seed(), AccountId::from(0)) - .unwrap() - .to_account_pubkey() - .derive_external_ivk() - .unwrap() - .derive_address(0) - .unwrap() - .encode(&MAIN_NETWORK); - assert_eq!(taddr, "t1PKtYdJJHhc3Pxowmznkg7vdTwnhEsCvR4".to_string()); - } - - #[test] - fn ufvk_round_trip() { - let account = 0.into(); - - let orchard = { - let sk = orchard::keys::SpendingKey::from_zip32_seed(&[0; 32], 0, 0).unwrap(); - Some(orchard::keys::FullViewingKey::from(&sk)) - }; - - let sapling = { - let extsk = sapling::spending_key(&[0; 32], 0, account); - Some(extsk.to_diversifiable_full_viewing_key()) - }; - - #[cfg(feature = "transparent-inputs")] - let transparent = { - let privkey = - AccountPrivKey::from_seed(&MAIN_NETWORK, &[0; 32], AccountId::from(0)).unwrap(); - Some(privkey.to_account_pubkey()) - }; - - let ufvk = UnifiedFullViewingKey::new( - #[cfg(feature = "transparent-inputs")] - transparent, - sapling, - orchard, - ) - .unwrap(); - - let encoded = ufvk.encode(&MAIN_NETWORK); - - // test encoded form against known values - let encoded_with_t = "uview1tg6rpjgju2s2j37gkgjq79qrh5lvzr6e0ed3n4sf4hu5qd35vmsh7avl80xa6mx7ryqce9hztwaqwrdthetpy4pc0kce25x453hwcmax02p80pg5savlg865sft9reat07c5vlactr6l2pxtlqtqunt2j9gmvr8spcuzf07af80h5qmut38h0gvcfa9k4rwujacwwca9vu8jev7wq6c725huv8qjmhss3hdj2vh8cfxhpqcm2qzc34msyrfxk5u6dqttt4vv2mr0aajreww5yufpk0gn4xkfm888467k7v6fmw7syqq6cceu078yw8xja502jxr0jgum43lhvpzmf7eu5dmnn6cr6f7p43yw8znzgxg598mllewnx076hljlvynhzwn5es94yrv65tdg3utuz2u3sras0wfcq4adxwdvlk387d22g3q98t5z74quw2fa4wed32escx8dwh4mw35t4jwf35xyfxnu83mk5s4kw2glkgsshmxk"; - let _encoded_no_t = "uview12z384wdq76ceewlsu0esk7d97qnd23v2qnvhujxtcf2lsq8g4hwzpx44fwxssnm5tg8skyh4tnc8gydwxefnnm0hd0a6c6etmj0pp9jqkdsllkr70u8gpf7ndsfqcjlqn6dec3faumzqlqcmtjf8vp92h7kj38ph2786zx30hq2wru8ae3excdwc8w0z3t9fuw7mt7xy5sn6s4e45kwm0cjp70wytnensgdnev286t3vew3yuwt2hcz865y037k30e428dvgne37xvyeal2vu8yjnznphf9t2rw3gdp0hk5zwq00ws8f3l3j5n3qkqgsyzrwx4qzmgq0xwwk4vz2r6vtsykgz089jncvycmem3535zjwvvtvjw8v98y0d5ydwte575gjm7a7k"; - #[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()), - ); - assert_eq!( - decoded.sapling.map(|s| s.to_bytes()), - ufvk.sapling.map(|s| s.to_bytes()), - ); - 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()), - ); - #[cfg(not(feature = "transparent-inputs"))] - assert_eq!(decoded_with_t.unknown.len(), 1); - } - - #[test] - #[cfg(feature = "transparent-inputs")] - fn ufvk_derivation() { - for tv in test_vectors::UNIFIED { - let usk = UnifiedSpendingKey::from_seed( - &MAIN_NETWORK, - &tv.root_seed, - AccountId::from(tv.account), - ) - .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. - if ufvk.sapling().unwrap().address(d_idx).is_none() { - continue; - } - - let ua = ufvk.address(d_idx).unwrap_or_else(|| panic!("diversifier index {} should have produced a valid unified address for account {}", - tv.diversifier_index, tv.account)); - - match RecipientAddress::decode(&MAIN_NETWORK, tv.unified_addr) { - Some(RecipientAddress::Unified(tvua)) => { - // We always derive transparent and Sapling receivers, but not - // every value in the test vectors has these present. - if tvua.transparent().is_some() { - assert_eq!(tvua.transparent(), ua.transparent()); - } - if tvua.sapling().is_some() { - 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(Network::MainNetwork)) { - let encoded = usk.to_bytes(Era::Orchard); - #[cfg(not(feature = "transparent-inputs"))] - assert_eq!(encoded.len(), 4 + 2 + 32 + 2 + 169); - #[cfg(feature = "transparent-inputs")] - assert_eq!(encoded.len(), 4 + 2 + 32 + 2 + 169 + 2 + 64); - let decoded = UnifiedSpendingKey::from_bytes(Era::Orchard, &encoded); - let decoded = decoded.unwrap_or_else(|e| panic!("Error decoding USK: {:?}", e)); - 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_client_backend/src/lib.rs b/zcash_client_backend/src/lib.rs index f737116623..781a54b7fe 100644 --- a/zcash_client_backend/src/lib.rs +++ b/zcash_client_backend/src/lib.rs @@ -2,26 +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(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 fees; -pub mod keys; +pub mod proposal; pub mod proto; pub mod scan; +pub mod scanning; pub mod wallet; -pub mod welding_rig; -pub mod zip321; + +#[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..749da1414e --- /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 761a5affd0..8811e0e372 100644 --- a/zcash_client_backend/src/proto.rs +++ b/zcash_client_backend/src/proto.rs @@ -1,20 +1,51 @@ -//! Generated code for handling light client protobuf structs. +//! This module contains generated code for handling light client protobuf structs. +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::{note::ExtractedNoteCommitment, Nullifier}, - transaction::{components::sapling, TxId}, + memo::{self, MemoBytes}, + value::Zatoshis, + PoolType, ShieldedProtocol, }; +use zip321::{TransactionRequest, Zip321Error}; -use zcash_note_encryption::{EphemeralKeyBytes, COMPACT_NOTE_SIZE}; +use crate::{ + data_api::{chain::ChainState, InputSource}, + fees::{ChangeValue, StandardFeeRule, TransactionBalance}, + proposal::{Proposal, ProposalError, ShieldedInputs, Step, StepOutput, StepOutputIndex}, +}; + +#[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; -#[cfg(feature = "lightwalletd-tonic")] +#[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)] @@ -96,7 +127,7 @@ impl compact_formats::CompactSaplingOutput { /// [`CompactOutput.cmu`]: #structfield.cmu pub fn cmu(&self) -> Result { let mut repr = [0; 32]; - repr.as_mut().copy_from_slice(&self.cmu[..]); + repr.copy_from_slice(&self.cmu[..]); Option::from(ExtractedNoteCommitment::from_bytes(&repr)).ok_or(()) } @@ -113,10 +144,12 @@ impl compact_formats::CompactSaplingOutput { } } -impl From> +impl From<&sapling::bundle::OutputDescription> for compact_formats::CompactSaplingOutput { - fn from(out: sapling::OutputDescription) -> 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(), @@ -125,20 +158,671 @@ impl From> } } -impl TryFrom for sapling::CompactOutputDescription { +impl TryFrom + for sapling::note_encryption::CompactOutputDescription +{ type Error = (); fn try_from(value: compact_formats::CompactSaplingOutput) -> Result { - Ok(sapling::CompactOutputDescription { + (&value).try_into() + } +} + +impl TryFrom<&compact_formats::CompactSaplingOutput> + for sapling::note_encryption::CompactOutputDescription +{ + type Error = (); + + fn try_from(value: &compact_formats::CompactSaplingOutput) -> Result { + Ok(sapling::note_encryption::CompactOutputDescription { cmu: value.cmu()?, ephemeral_key: value.ephemeral_key()?, - enc_ciphertext: value.ciphertext.try_into().map_err(|_| ())?, + enc_ciphertext: value.ciphertext[..].try_into().map_err(|_| ())?, }) } } impl compact_formats::CompactSaplingSpend { - pub fn nf(&self) -> Result { - Nullifier::from_slice(&self.nf).map_err(|_| ()) + 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/compact_formats.rs b/zcash_client_backend/src/proto/compact_formats.rs index 056764b78a..e2931b11b6 100644 --- a/zcash_client_backend/src/proto/compact_formats.rs +++ b/zcash_client_backend/src/proto/compact_formats.rs @@ -1,8 +1,20 @@ +// 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 Sapling address -/// 2. Detect a spend of your shielded Sapling notes -/// 3. Update your witnesses to generate new Sapling spend proofs. -#[allow(clippy::derive_partial_eq_without_eq)] +/// 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 @@ -26,11 +38,15 @@ pub struct CompactBlock { /// 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. -#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct CompactTx { /// Index and hash will allow the receiver to call out to chain @@ -57,36 +73,33 @@ pub struct CompactTx { #[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. -#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct CompactSaplingSpend { - /// nullifier (see the Zcash protocol specification) + /// Nullifier (see the Zcash protocol specification) #[prost(bytes = "vec", tag = "1")] pub nf: ::prost::alloc::vec::Vec, } -/// output encodes the `cmu` field, `ephemeralKey` field, and a 52-byte prefix of the -/// `encCiphertext` field of a Sapling Output Description. These fields are described in -/// section 7.4 of the Zcash protocol spec: -/// -/// Total size is 116 bytes. -#[allow(clippy::derive_partial_eq_without_eq)] +/// 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 + /// Note commitment u-coordinate. #[prost(bytes = "vec", tag = "1")] pub cmu: ::prost::alloc::vec::Vec, - /// ephemeral public key + /// Ephemeral public key. #[prost(bytes = "vec", tag = "2")] pub ephemeral_key: ::prost::alloc::vec::Vec, - /// first 52 bytes of ciphertext + /// First 52 bytes of ciphertext. #[prost(bytes = "vec", tag = "3")] pub ciphertext: ::prost::alloc::vec::Vec, } -/// -/// (but not all fields are needed) -#[allow(clippy::derive_partial_eq_without_eq)] +/// A compact representation of an [Orchard Action](). #[derive(Clone, PartialEq, ::prost::Message)] pub struct CompactOrchardAction { /// \[32\] The nullifier of the input note 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 index 38b15abdbf..9dd1b0a456 100644 --- a/zcash_client_backend/src/proto/service.rs +++ b/zcash_client_backend/src/proto/service.rs @@ -1,6 +1,6 @@ +// 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. -#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct BlockId { #[prost(uint64, tag = "1")] @@ -10,7 +10,6 @@ pub struct BlockId { } /// BlockRange specifies a series of blocks from start to end inclusive. /// Both BlockIDs must be heights; specification by hash is not yet supported. -#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct BlockRange { #[prost(message, optional, tag = "1")] @@ -21,7 +20,6 @@ pub struct BlockRange { /// 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. -#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct TxFilter { /// block identifier, height or hash @@ -37,7 +35,9 @@ pub struct TxFilter { /// 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. -#[allow(clippy::derive_partial_eq_without_eq)] +/// +/// 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' @@ -50,7 +50,6 @@ pub struct RawTransaction { /// 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. -#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct SendResponse { #[prost(int32, tag = "1")] @@ -59,16 +58,13 @@ pub struct SendResponse { pub error_message: ::prost::alloc::string::String, } /// Chainspec is a placeholder to allow specification of a particular chain fork. -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] +#[derive(Clone, Copy, PartialEq, ::prost::Message)] pub struct ChainSpec {} /// Empty is for gRPCs that take no arguments, currently only GetLightdInfo. -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] +#[derive(Clone, Copy, PartialEq, ::prost::Message)] pub struct Empty {} /// LightdInfo returns various information about this lightwalletd instance /// and the state of the blockchain. -#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct LightdInfo { #[prost(string, tag = "1")] @@ -107,10 +103,12 @@ pub struct LightdInfo { /// example: "/MagicBean:4.1.1/" #[prost(string, tag = "14")] pub zcashd_subversion: ::prost::alloc::string::String, + /// Zcash donation UA address + #[prost(string, tag = "15")] + pub donation_address: ::prost::alloc::string::String, } /// TransparentAddressBlockFilter restricts the results to the given address /// or block range. -#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct TransparentAddressBlockFilter { /// t-address @@ -123,8 +121,7 @@ pub struct TransparentAddressBlockFilter { /// Duration is currently used only for testing, so that the Ping rpc /// can simulate a delay, to create many simultaneous connections. Units /// are microseconds. -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] +#[derive(Clone, Copy, PartialEq, ::prost::Message)] pub struct Duration { #[prost(int64, tag = "1")] pub interval_us: i64, @@ -132,40 +129,34 @@ pub struct Duration { /// 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. -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] +#[derive(Clone, Copy, PartialEq, ::prost::Message)] pub struct PingResponse { #[prost(int64, tag = "1")] pub entry: i64, #[prost(int64, tag = "2")] pub exit: i64, } -#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Address { #[prost(string, tag = "1")] pub address: ::prost::alloc::string::String, } -#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct AddressList { #[prost(string, repeated, tag = "1")] pub addresses: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, } -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] +#[derive(Clone, Copy, PartialEq, ::prost::Message)] pub struct Balance { #[prost(int64, tag = "1")] pub value_zat: i64, } -#[allow(clippy::derive_partial_eq_without_eq)] #[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. -#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct TreeState { /// "main" or "test" @@ -187,9 +178,32 @@ pub struct TreeState { #[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. -#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetAddressUtxosArg { #[prost(string, repeated, tag = "1")] @@ -200,7 +214,6 @@ pub struct GetAddressUtxosArg { #[prost(uint32, tag = "3")] pub max_entries: u32, } -#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetAddressUtxosReply { #[prost(string, tag = "6")] @@ -216,38 +229,59 @@ pub struct GetAddressUtxosReply { #[prost(uint64, tag = "5")] pub height: u64, } -#[allow(clippy::derive_partial_eq_without_eq)] #[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::let_unit_value)] + #![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 { - /// 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)) - } - } impl CompactTxStreamerClient where - T: tonic::client::GrpcService, + T: tonic::client::GrpcService, T::Error: Into, - T::ResponseBody: Body + Send + 'static, - ::Error: Into + Send, + 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); @@ -265,14 +299,14 @@ pub mod compact_tx_streamer_client { F: tonic::service::Interceptor, T::ResponseBody: Default, T: tonic::codegen::Service< - http::Request, + http::Request, Response = http::Response< - >::ResponseBody, + >::ResponseBody, >, >, , - >>::Error: Into + Send + Sync, + http::Request, + >>::Error: Into + std::marker::Send + std::marker::Sync, { CompactTxStreamerClient::new(InterceptedService::new(inner, interceptor)) } @@ -307,7 +341,7 @@ pub mod compact_tx_streamer_client { self.inner = self.inner.max_encoding_message_size(limit); self } - /// Return the height of the tip of the best chain + /// 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, @@ -316,8 +350,7 @@ pub mod compact_tx_streamer_client { .ready() .await .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, + tonic::Status::unknown( format!("Service was not ready: {}", e.into()), ) })?; @@ -347,8 +380,7 @@ pub mod compact_tx_streamer_client { .ready() .await .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, + tonic::Status::unknown( format!("Service was not ready: {}", e.into()), ) })?; @@ -366,6 +398,36 @@ pub mod compact_tx_streamer_client { ); 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, @@ -380,8 +442,7 @@ pub mod compact_tx_streamer_client { .ready() .await .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, + tonic::Status::unknown( format!("Service was not ready: {}", e.into()), ) })?; @@ -399,6 +460,38 @@ pub mod compact_tx_streamer_client { ); 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, @@ -408,8 +501,7 @@ pub mod compact_tx_streamer_client { .ready() .await .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, + tonic::Status::unknown( format!("Service was not ready: {}", e.into()), ) })?; @@ -436,8 +528,7 @@ pub mod compact_tx_streamer_client { .ready() .await .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, + tonic::Status::unknown( format!("Service was not ready: {}", e.into()), ) })?; @@ -467,8 +558,7 @@ pub mod compact_tx_streamer_client { .ready() .await .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, + tonic::Status::unknown( format!("Service was not ready: {}", e.into()), ) })?; @@ -494,8 +584,7 @@ pub mod compact_tx_streamer_client { .ready() .await .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, + tonic::Status::unknown( format!("Service was not ready: {}", e.into()), ) })?; @@ -521,8 +610,7 @@ pub mod compact_tx_streamer_client { .ready() .await .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, + tonic::Status::unknown( format!("Service was not ready: {}", e.into()), ) })?; @@ -562,8 +650,7 @@ pub mod compact_tx_streamer_client { .ready() .await .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, + tonic::Status::unknown( format!("Service was not ready: {}", e.into()), ) })?; @@ -594,8 +681,7 @@ pub mod compact_tx_streamer_client { .ready() .await .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, + tonic::Status::unknown( format!("Service was not ready: {}", e.into()), ) })?; @@ -625,8 +711,7 @@ pub mod compact_tx_streamer_client { .ready() .await .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, + tonic::Status::unknown( format!("Service was not ready: {}", e.into()), ) })?; @@ -652,8 +737,7 @@ pub mod compact_tx_streamer_client { .ready() .await .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, + tonic::Status::unknown( format!("Service was not ready: {}", e.into()), ) })?; @@ -671,6 +755,37 @@ pub mod compact_tx_streamer_client { ); 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, @@ -682,8 +797,7 @@ pub mod compact_tx_streamer_client { .ready() .await .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, + tonic::Status::unknown( format!("Service was not ready: {}", e.into()), ) })?; @@ -712,8 +826,7 @@ pub mod compact_tx_streamer_client { .ready() .await .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, + tonic::Status::unknown( format!("Service was not ready: {}", e.into()), ) })?; @@ -740,8 +853,7 @@ pub mod compact_tx_streamer_client { .ready() .await .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, + tonic::Status::unknown( format!("Service was not ready: {}", e.into()), ) })?; @@ -768,8 +880,7 @@ pub mod compact_tx_streamer_client { .ready() .await .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, + tonic::Status::unknown( format!("Service was not ready: {}", e.into()), ) })?; diff --git a/zcash_client_backend/src/scan.rs b/zcash_client_backend/src/scan.rs index a568be4f0a..bb7b85406b 100644 --- a/zcash_client_backend/src/scan.rs +++ b/zcash_client_backend/src/scan.rs @@ -8,36 +8,105 @@ use std::sync::{ }; use memuse::DynamicUsage; -use zcash_note_encryption::{batch, BatchDomain, Domain, ShieldedOutput, COMPACT_NOTE_SIZE}; +use zcash_note_encryption::{ + batch, BatchDomain, Domain, ShieldedOutput, COMPACT_NOTE_SIZE, ENC_CIPHERTEXT_SIZE, +}; use zcash_primitives::{block::BlockHash, transaction::TxId}; -/// A decrypted note. -pub(crate) struct DecryptedNote { +/// 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: A, + 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 DecryptedNote +impl fmt::Debug for DecryptedOutput where - A: fmt::Debug, + IvkTag: fmt::Debug, D::IncomingViewingKey: fmt::Debug, D::Recipient: fmt::Debug, D::Note: fmt::Debug, - D::Memo: fmt::Debug, + M: fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("DecryptedNote") + 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. @@ -46,12 +115,12 @@ struct OutputIndex { value: V, } -type OutputItem = OutputIndex>; +type OutputItem = OutputIndex>; /// The sender for the result of batch scanning a specific transaction output. -struct OutputReplier(OutputIndex>>); +struct OutputReplier(OutputIndex>>); -impl DynamicUsage for OutputReplier { +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. @@ -65,9 +134,9 @@ impl DynamicUsage for OutputReplier { } /// The receiver for the result of batch scanning a specific transaction. -struct BatchReceiver(channel::Receiver>); +struct BatchReceiver(channel::Receiver>); -impl DynamicUsage for BatchReceiver { +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(); @@ -76,7 +145,7 @@ impl DynamicUsage for BatchReceiver { // 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 + ITEMS_PER_BLOCK - 1) / ITEMS_PER_BLOCK; + let num_blocks = num_items.div_ceil(ITEMS_PER_BLOCK); // The structure of a block is: // - A pointer to the next block. @@ -84,7 +153,7 @@ impl DynamicUsage for BatchReceiver { // - 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::>(); + 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); @@ -129,6 +198,7 @@ impl Tasks for () { /// /// 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, @@ -208,8 +278,8 @@ impl Task for WithUsageTask { } /// A batch of outputs to trial decrypt. -pub(crate) struct Batch> { - tags: Vec, +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 @@ -219,15 +289,16 @@ pub(crate) struct Batch, - repliers: Vec>, + repliers: Vec>, } -impl DynamicUsage for Batch +impl DynamicUsage for Batch where - A: DynamicUsage, + IvkTag: DynamicUsage, D: BatchDomain + DynamicUsage, D::IncomingViewingKey: DynamicUsage, - Output: ShieldedOutput + DynamicUsage, + Output: DynamicUsage, + Dec: Decryptor, { fn dynamic_usage(&self) -> usize { self.tags.dynamic_usage() @@ -253,14 +324,14 @@ where } } -impl Batch +impl Batch where - A: Clone, + IvkTag: Clone, D: BatchDomain, - Output: ShieldedOutput, + Dec: Decryptor, { /// Constructs a new batch. - fn new(tags: Vec, ivks: Vec) -> Self { + fn new(tags: Vec, ivks: Vec) -> Self { assert_eq!(tags.len(), ivks.len()); Self { tags, @@ -276,15 +347,17 @@ where } } -impl Task for Batch +impl Task for Batch where - A: Clone + Send + 'static, + IvkTag: Clone + Send + 'static, D: BatchDomain + Send + 'static, D::IncomingViewingKey: Send, D::Memo: Send, D::Note: Send, D::Recipient: Send, - Output: ShieldedOutput + Send + 'static, + Output: Send + 'static, + Dec: Decryptor + 'static, + Dec::Memo: Send, { /// Runs the batch of trial decryptions, and reports the results. fn run(self) { @@ -298,20 +371,14 @@ where assert_eq!(outputs.len(), repliers.len()); - let decryption_results = batch::try_compact_note_decryption(&ivks, &outputs); - for (decryption_result, OutputReplier(replier)) in - decryption_results.into_iter().zip(repliers.into_iter()) - { + 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(((note, recipient), ivk_idx)) = decryption_result { + if let Some(value) = decryption_result { let result = OutputIndex { output_index: replier.output_index, - value: DecryptedNote { - ivk_tag: tags[ivk_idx].clone(), - recipient, - note, - }, + value, }; if replier.value.send(result).is_err() { @@ -323,18 +390,27 @@ where } } -impl + Clone> Batch { +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() -> D, + domain: impl Fn(&Output) -> D, outputs: &[Output], - replier: channel::Sender>, + replier: channel::Sender>, ) { - self.outputs - .extend(outputs.iter().cloned().map(|output| (domain(), output))); + self.outputs.extend( + outputs + .iter() + .cloned() + .map(|output| (domain(&output), output)), + ); self.repliers.extend((0..outputs.len()).map(|output_index| { OutputReplier(OutputIndex { output_index, @@ -361,28 +437,29 @@ impl DynamicUsage for ResultKey { } /// Logic to run batches of trial decryptions on the global threadpool. -pub(crate) struct BatchRunner +pub(crate) struct BatchRunner where D: BatchDomain, - Output: ShieldedOutput, - T: Tasks>, + Dec: Decryptor, + T: Tasks>, { batch_size_threshold: usize, // The batch currently being accumulated. - acc: Batch, + acc: Batch, // The running batches. running_tasks: T, // Receivers for the results of the running batches. - pending_results: HashMap>, + pending_results: HashMap>, } -impl DynamicUsage for BatchRunner +impl DynamicUsage for BatchRunner where - A: DynamicUsage, + IvkTag: DynamicUsage, D: BatchDomain + DynamicUsage, D::IncomingViewingKey: DynamicUsage, - Output: ShieldedOutput + DynamicUsage, - T: Tasks> + DynamicUsage, + Output: DynamicUsage, + Dec: Decryptor, + T: Tasks> + DynamicUsage, { fn dynamic_usage(&self) -> usize { self.acc.dynamic_usage() @@ -408,17 +485,17 @@ where } } -impl BatchRunner +impl BatchRunner where - A: Clone, + IvkTag: Clone, D: BatchDomain, - Output: ShieldedOutput, - T: Tasks>, + 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, + ivks: impl Iterator, ) -> Self { let (tags, ivks) = ivks.unzip(); Self { @@ -430,16 +507,17 @@ where } } -impl BatchRunner +impl BatchRunner where - A: Clone + Send + 'static, + IvkTag: Clone + Send + 'static, D: BatchDomain + Send + 'static, D::IncomingViewingKey: Clone + Send, D::Memo: Send, D::Note: Send, D::Recipient: Send, - Output: ShieldedOutput + Clone + Send + 'static, - T: Tasks>, + Output: Clone + Send + 'static, + Dec: Decryptor, + T: Tasks>, { /// Batches the given outputs for trial decryption. /// @@ -447,14 +525,14 @@ where /// 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 - /// `BATCH_SIZE_THRESHOLD`, `Self::flush` is called. Subsequent calls to - /// `Self::add_outputs` will be accumulated into a new batch. + /// 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() -> D, + domain: impl Fn(&Output) -> D, outputs: &[Output], ) { let (tx, rx) = channel::unbounded(); @@ -487,7 +565,7 @@ where &mut self, block_tag: BlockHash, txid: TxId, - ) -> HashMap<(TxId, usize), DecryptedNote> { + ) -> 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 diff --git a/zcash_client_backend/src/scanning.rs b/zcash_client_backend/src/scanning.rs new file mode 100644 index 0000000000..a78532bab0 --- /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().into_bytes(), + &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..03a3d7ccf6 --- /dev/null +++ b/zcash_client_backend/src/sync.rs @@ -0,0 +1,607 @@ +//! 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::Body as TonicBody, + 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::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, +{ + // 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).await? {} + + Ok(()) +} + +async fn running( + client: &mut CompactTxStreamerClient, + params: &P, + db_cache: &CaT, + db_data: &mut DbT, + batch_size: u32, +) -> 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 + .utxo_query_height(account_id) + .map_err(Error::Wallet)?; + 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, true) + .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..96281591ea --- /dev/null +++ b/zcash_client_backend/src/tor.rs @@ -0,0 +1,184 @@ +//! 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; + +// Re-exported as this is currently the only `arti_client` type users would need to use +// our minimal client API. +pub use arti_client::DormantMode; + +/// 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`. + /// + /// If the `with_permissions` closure does not make any changes (e.g. is + /// passed as `|_| {}`), the default from [`arti_client`] will be used. + /// This default will enable permissions checks unless the + /// `ARTI_FS_DISABLE_PERMISSION_CHECKS` env variable is set. + /// + /// Returns an error if `tor_dir` does not exist, or if bootstrapping fails. + pub async fn create( + tor_dir: &Path, + with_permissions: impl FnOnce(&mut fs_mistrust::MistrustBuilder), + ) -> Result { + let runtime = PreferredRuntime::current()?; + + if !tokio::fs::try_exists(tor_dir).await? { + return Err(Error::MissingTorDirectory); + } + + let mut config_builder = TorClientConfigBuilder::from_directories( + tor_dir.join("arti-data"), + tor_dir.join("arti-cache"), + ); + + with_permissions(config_builder.storage().permissions()); + + let config = config_builder + .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 }) + } + + /// Ensures the Tor client is bootstrapped. + /// + /// This should be called first inside every public method that makes network requests + /// using the Tor client. + /// + /// `Client` ensures it cannot be constructed in an un-bootstrapped state, but Tor + /// clients can become less bootstrapped over time (for example if it loses its + /// internet connectivity, or if its directory information expires before it's able to + /// replace it). + async fn ensure_bootstrapped(&self) -> Result<(), Error> { + if !self.inner.bootstrap_status().ready_for_traffic() { + debug!("Re-bootstrapping Tor"); + self.inner.bootstrap().await?; + debug!("Tor re-bootstrapped"); + } + Ok(()) + } + + /// 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(), + } + } + + /// Changes the client's current dormant mode, putting background tasks to sleep or + /// waking them up as appropriate. + /// + /// This can be used to conserve CPU usage if you aren’t planning on using the client + /// for a while, especially on mobile platforms. + /// + /// See the [`DormantMode`] documentation for more details. + pub fn set_dormant(&self, mode: DormantMode) { + self.inner.set_dormant(mode); + } +} + +/// 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..1d3831d127 --- /dev/null +++ b/zcash_client_backend/src/tor/grpc.rs @@ -0,0 +1,117 @@ +use std::{ + error::Error as _, + 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> { + self.ensure_bootstrapped().await?; + + 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) => { + if let Some(source) = e.source() { + // Tonic doesn't include the source error in its `Display` impl; + // add it manually for the benefit of our downstreams. + write!(f, "Tonic error: {e}: {source}") + } else { + write!(f, "Tonic 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..722160922f --- /dev/null +++ b/zcash_client_backend/src/tor/http.rs @@ -0,0 +1,271 @@ +//! 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::{pki_types::ServerName, ClientConfig, RootCertStore}, + 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 { + /// Makes an HTTP GET request over Tor. + /// + /// On error, retries will be attempted as follows: + /// - A successful request that resulted in a client error (HTTP 400-499) will cause a + /// retry with an isolated client. + /// - All other errors will cause a retry with the same client. + #[tracing::instrument(skip(self, h, f))] + async fn get>>( + &self, + url: Uri, + h: impl Fn(Builder) -> Builder, + f: impl FnOnce(Incoming) -> F, + retry_limit: u8, + ) -> Result, Error> { + let mut retries_remaining = retry_limit; + let mut client = None; + + let (parts, body) = loop { + match client + .as_ref() + .unwrap_or(self) + .get_once(url.clone(), &h) + .await + { + Ok(response) => break Ok(response), + + Err(e) => match retries_remaining.checked_sub(1) { + Some(retries) => { + debug!("Retrying due to error: {e}"); + retries_remaining = retries; + + // A common failure with HTTP requests over Tor is a particular + // exit node being blocked by the server. `Client::get` isn't used + // for anything that requires a persistent Tor client identity + // across queries, so we retry once with an isolated client in + // order to use new circuits that have a decent chance of using a + // different exit node. The isolation is not for privacy; the + // server can trivially link the two requests together via timing. + if let Error::Http(HttpError::Unsuccessful(status)) = e { + if status.is_client_error() { + debug!("Switching to isolated Tor circuit after getting {status}"); + client = Some(self.isolated_client()); + } + } + } + None => break Err(e), + }, + } + }? + .into_parts(); + + Ok(Response::from_parts(parts, f(body).await?)) + } + + async fn get_once( + &self, + url: Uri, + h: impl FnOnce(Builder) -> Builder, + ) -> 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 root_store = RootCertStore { + roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(), + }; + let config = ClientConfig::builder() + .with_root_certificates(root_store) + .with_no_client_auth(); + let connector = TlsConnector::from(Arc::new(config)); + let dnsname = ServerName::try_from(host).expect("Already checked"); + let stream = connector + .connect(dnsname, stream) + .await + .map_err(HttpError::Tls)?; + make_http_request(stream, url, h).await + } else { + make_http_request(stream, url, h).await + } + } + + /// Makes an HTTP GET request over Tor, parsing the response as JSON. + /// + /// On error, retries will be attempted as follows: + /// - A successful request that resulted in a client error (HTTP 400-499) will cause a + /// retry with an isolated client. + /// - All other errors will cause a retry with the same client. + async fn get_json( + &self, + url: Uri, + retry_limit: u8, + ) -> 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)?) + }, + retry_limit, + ) + .await + } +} + +async fn make_http_request( + stream: impl AsyncRead + AsyncWrite + Unpin + Send + 'static, + url: Uri, + h: impl FnOnce(Builder) -> Builder, +) -> 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 response = sender.send_request(req).await.map_err(HttpError::from)?; + debug!("Response status code: {}", response.status()); + + if response.status().is_success() { + Ok(response) + } else { + Err(Error::Http(HttpError::Unsuccessful(response.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..fcd3815b0c --- /dev/null +++ b/zcash_client_backend/src/tor/http/cryptex.rs @@ -0,0 +1,205 @@ +//! 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; + +/// Maximum number of retries for exchange queries implemented in this crate. +const RETRY_LIMIT: u8 = 1; + +/// Exchanges for which we know how to query data over Tor. +/// +/// Queries to these exchanges will be retried a single time on error. +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 { + self.ensure_bootstrapped().await?; + + // 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..7672b062f9 --- /dev/null +++ b/zcash_client_backend/src/tor/http/cryptex/binance.rs @@ -0,0 +1,64 @@ +use rust_decimal::Decimal; +use serde::Deserialize; + +use super::{Exchange, ExchangeData, RETRY_LIMIT}; +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(), + RETRY_LIMIT, + ) + .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..db62b18695 --- /dev/null +++ b/zcash_client_backend/src/tor/http/cryptex/coinbase.rs @@ -0,0 +1,52 @@ +use rust_decimal::Decimal; +use serde::Deserialize; + +use super::{Exchange, ExchangeData, RETRY_LIMIT}; +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(), + RETRY_LIMIT, + ) + .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..3823cfd97c --- /dev/null +++ b/zcash_client_backend/src/tor/http/cryptex/gate_io.rs @@ -0,0 +1,55 @@ +use hyper::StatusCode; +use rust_decimal::Decimal; +use serde::Deserialize; + +use super::{Exchange, ExchangeData, RETRY_LIMIT}; +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(), + RETRY_LIMIT, + ) + .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..a8ff5c88dc --- /dev/null +++ b/zcash_client_backend/src/tor/http/cryptex/gemini.rs @@ -0,0 +1,48 @@ +use rust_decimal::Decimal; +use serde::Deserialize; + +use super::{Exchange, ExchangeData, RETRY_LIMIT}; +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(), + RETRY_LIMIT, + ) + .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..707e3c044b --- /dev/null +++ b/zcash_client_backend/src/tor/http/cryptex/ku_coin.rs @@ -0,0 +1,66 @@ +use rust_decimal::Decimal; +use serde::Deserialize; + +use super::{Exchange, ExchangeData, RETRY_LIMIT}; +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(), + RETRY_LIMIT, + ) + .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..b8e09f4df2 --- /dev/null +++ b/zcash_client_backend/src/tor/http/cryptex/mexc.rs @@ -0,0 +1,59 @@ +use rust_decimal::Decimal; +use serde::Deserialize; + +use super::{Exchange, ExchangeData, RETRY_LIMIT}; +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(), + RETRY_LIMIT, + ) + .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 ba58340b3d..f298b6fdb7 100644 --- a/zcash_client_backend/src/wallet.rs +++ b/zcash_client_backend/src/wallet.rs @@ -1,38 +1,173 @@ //! Structs representing transaction data scanned from the block chain by a wallet or //! light client. +use incrementalmerkletree::Position; + +use ::transparent::{ + address::TransparentAddress, + bundle::{OutPoint, TxOut}, +}; +use zcash_address::ZcashAddress; use zcash_note_encryption::EphemeralKeyBytes; -use zcash_primitives::{ +use zcash_primitives::transaction::{fees::transparent as transparent_fees, TxId}; +use zcash_protocol::{ consensus::BlockHeight, - keys::OutgoingViewingKey, - legacy::TransparentAddress, - sapling, - transaction::{ - components::{ - sapling::fees as sapling_fees, - transparent::{self, OutPoint, TxOut}, - Amount, - }, - TxId, - }, - zip32::AccountId, + value::{BalanceError, Zatoshis}, + PoolType, ShieldedProtocol, }; +use zip32::Scope; + +use crate::fees::sapling as sapling_fees; + +#[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 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 + } -/// A subset of a [`Transaction`] relevant to wallets and light clients. + /// 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 type that represents the recipient of a transaction output: +/// +/// * 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, + }, +} + +/// The shielded subset of a [`Transaction`]'s data that is relevant to a particular wallet. /// /// [`Transaction`]: zcash_primitives::transaction::Transaction -pub struct WalletTx { - pub txid: TxId, - pub index: usize, - pub sapling_spends: Vec, - pub sapling_outputs: Vec>, +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>, } -#[derive(Debug, Clone)] +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, - height: BlockHeight, + mined_height: Option, recipient_address: TransparentAddress, } @@ -40,14 +175,14 @@ impl WalletTransparentOutput { pub fn from_parts( outpoint: OutPoint, txout: TxOut, - height: BlockHeight, + mined_height: Option, ) -> Option { txout .recipient_address() .map(|recipient_address| WalletTransparentOutput { outpoint, txout, - height, + mined_height, recipient_address, }) } @@ -60,20 +195,20 @@ impl WalletTransparentOutput { &self.txout } - pub fn height(&self) -> BlockHeight { - self.height + pub fn mined_height(&self) -> Option { + self.mined_height } pub fn recipient_address(&self) -> &TransparentAddress { &self.recipient_address } - pub fn value(&self) -> Amount { + pub fn value(&self) -> Zatoshis { self.txout.value } } -impl transparent::fees::InputView for WalletTransparentOutput { +impl transparent_fees::InputView for WalletTransparentOutput { fn outpoint(&self) -> &OutPoint { &self.outpoint } @@ -82,116 +217,294 @@ impl transparent::fees::InputView for WalletTransparentOutput { } } -/// A subset of a [`SpendDescription`] relevant to wallets and light clients. -/// -/// [`SpendDescription`]: zcash_primitives::transaction::components::SpendDescription -pub struct WalletSaplingSpend { +/// A reference to a spent note belonging to the wallet within a transaction. +pub struct WalletSpend { index: usize, - nf: sapling::Nullifier, - account: AccountId, + nf: Nf, + account_id: AccountId, } -impl WalletSaplingSpend { - pub fn from_parts(index: usize, nf: sapling::Nullifier, account: AccountId) -> Self { - Self { index, nf, account } +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 } - pub fn nf(&self) -> &sapling::Nullifier { + /// Returns the nullifier of the spent note. + pub fn nf(&self) -> &Nf { &self.nf } - pub fn account(&self) -> AccountId { - self.account + /// Returns the identifier to the account_id to which the note belonged. + pub fn account_id(&self) -> &AccountId { + &self.account_id } } -/// A subset of an [`OutputDescription`] relevant to wallets and light clients. -/// -/// [`OutputDescription`]: zcash_primitives::transaction::components::OutputDescription -pub struct WalletSaplingOutput { +/// 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, - cmu: sapling::note::ExtractedNoteCommitment, ephemeral_key: EphemeralKeyBytes, - account: AccountId, - note: sapling::Note, + note: Note, is_change: bool, - witness: sapling::IncrementalWitness, - nf: N, + note_commitment_tree_position: Position, + nf: Option, + account_id: AccountId, + recipient_key_scope: Option, } -impl WalletSaplingOutput { - /// Constructs a new `WalletSaplingOutput` value from its constituent parts. +impl WalletOutput { + /// Constructs a new `WalletOutput` value from its constituent parts. #[allow(clippy::too_many_arguments)] pub fn from_parts( index: usize, - cmu: sapling::note::ExtractedNoteCommitment, ephemeral_key: EphemeralKeyBytes, - account: AccountId, - note: sapling::Note, + note: Note, is_change: bool, - witness: sapling::IncrementalWitness, - nf: N, + note_commitment_tree_position: Position, + nf: Option, + account_id: AccountId, + recipient_key_scope: Option, ) -> Self { Self { index, - cmu, ephemeral_key, - account, note, is_change, - witness, + 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 } - pub fn cmu(&self) -> &sapling::note::ExtractedNoteCommitment { - &self.cmu - } + /// The [`EphemeralKeyBytes`] used in the decryption of the note. pub fn ephemeral_key(&self) -> &EphemeralKeyBytes { &self.ephemeral_key } - pub fn account(&self) -> AccountId { - self.account - } - pub fn note(&self) -> &sapling::Note { + /// 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 } - pub fn witness(&self) -> &sapling::IncrementalWitness { - &self.witness + /// The position of the note in the global note commitment tree. + pub fn note_commitment_tree_position(&self) -> Position { + self.note_commitment_tree_position } - pub fn witness_mut(&mut self) -> &mut sapling::IncrementalWitness { - &mut self.witness + /// 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() } - pub fn nf(&self) -> &N { - &self.nf + /// 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`]: 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 ReceivedSaplingNote { - pub note_id: NoteRef, - pub diversifier: sapling::Diversifier, - pub note_value: Amount, - pub rseed: sapling::Rseed, - pub witness: sapling::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 sapling_fees::InputView for ReceivedSaplingNote { +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) -> Amount { - self.note_value + 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.") } } @@ -202,23 +515,70 @@ impl sapling_fees::InputView for ReceivedSaplingNote /// 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 1906c133e6..0000000000 --- a/zcash_client_backend/src/welding_rig.rs +++ /dev/null @@ -1,672 +0,0 @@ -//! Tools for scanning a compact representation of the Zcash block chain. - -use std::collections::{HashMap, HashSet}; -use std::convert::TryFrom; - -use subtle::{ConditionallySelectable, ConstantTimeEq, CtOption}; -use zcash_note_encryption::batch; -use zcash_primitives::{ - consensus, - sapling::{ - self, - note_encryption::{PreparedIncomingViewingKey, SaplingDomain}, - Node, Note, Nullifier, NullifierDerivingKey, SaplingIvk, - }, - transaction::components::sapling::CompactOutputDescription, - zip32::{sapling::DiversifiableFullViewingKey, AccountId, Scope}, -}; - -use crate::{ - proto::compact_formats::CompactBlock, - scan::{Batch, BatchRunner, Tasks}, - wallet::{WalletSaplingOutput, WalletSaplingSpend, WalletTx}, -}; - -/// A key that can be used to perform trial decryption and nullifier -/// computation for a Sapling [`CompactSaplingOutput`] -/// -/// 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 -/// [`scan_block`]: crate::welding_rig::scan_block -pub trait ScanningKey { - /// The type representing the scope of the scanning key. - type Scope: Clone + Eq + std::hash::Hash + Send + 'static; - - /// The type of key that is used to decrypt Sapling outputs; - type SaplingNk: Clone; - - type SaplingKeys: IntoIterator; - - /// The type of nullifier extracted when a note is successfully - /// obtained by trial decryption. - type Nf; - - /// Obtain the underlying Sapling incoming viewing key(s) for this scanning key. - fn to_sapling_keys(&self) -> Self::SaplingKeys; - - /// 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 sapling_nf( - key: &Self::SaplingNk, - note: &Note, - witness: &sapling::IncrementalWitness, - ) -> Self::Nf; -} - -impl ScanningKey for DiversifiableFullViewingKey { - type Scope = Scope; - type SaplingNk = NullifierDerivingKey; - type SaplingKeys = [(Self::Scope, SaplingIvk, Self::SaplingNk); 2]; - type Nf = sapling::Nullifier; - - fn to_sapling_keys(&self) -> Self::SaplingKeys { - [ - ( - Scope::External, - self.to_ivk(Scope::External), - self.to_nk(Scope::External), - ), - ( - Scope::Internal, - self.to_ivk(Scope::Internal), - self.to_nk(Scope::Internal), - ), - ] - } - - fn sapling_nf( - key: &Self::SaplingNk, - note: &Note, - witness: &sapling::IncrementalWitness, - ) -> Self::Nf { - note.nf( - key, - u64::try_from(witness.position()) - .expect("Sapling note commitment tree position must fit into a 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 Scope = (); - type SaplingNk = (); - type SaplingKeys = [(Self::Scope, SaplingIvk, Self::SaplingNk); 1]; - type Nf = (); - - fn to_sapling_keys(&self) -> Self::SaplingKeys { - [((), self.clone(), ())] - } - - fn sapling_nf(_key: &Self::SaplingNk, _note: &Note, _witness: &sapling::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 [`WalletSaplingOutput`]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 -/// [`WalletSaplingOutput`]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::sapling::CommitmentTree -/// [`IncrementalWitness`]: zcash_primitives::sapling::IncrementalWitness -/// [`WalletSaplingOutput`]: crate::wallet::WalletSaplingOutput -/// [`WalletTx`]: crate::wallet::WalletTx -pub fn scan_block( - params: &P, - block: CompactBlock, - vks: &[(&AccountId, &K)], - nullifiers: &[(AccountId, Nullifier)], - tree: &mut sapling::CommitmentTree, - existing_witnesses: &mut [&mut sapling::IncrementalWitness], -) -> Vec> { - scan_block_with_runner::<_, _, ()>( - params, - block, - vks, - nullifiers, - tree, - existing_witnesses, - None, - ) -} - -type TaggedBatch = Batch<(AccountId, S), SaplingDomain

, CompactOutputDescription>; -type TaggedBatchRunner = - BatchRunner<(AccountId, S), SaplingDomain

, CompactOutputDescription, T>; - -#[tracing::instrument(skip_all, fields(height = block.height))] -pub(crate) fn add_block_to_runner( - params: &P, - block: CompactBlock, - batch_runner: &mut TaggedBatchRunner, -) where - P: consensus::Parameters + Send + 'static, - S: Clone + Send + 'static, - T: Tasks>, -{ - let block_hash = block.hash(); - let block_height = block.height(); - - for tx in block.vtx.into_iter() { - let txid = tx.txid(); - let outputs = tx - .outputs - .into_iter() - .map(|output| { - CompactOutputDescription::try_from(output) - .expect("Invalid output found in compact block decoding.") - }) - .collect::>(); - - batch_runner.add_outputs( - block_hash, - txid, - || SaplingDomain::for_height(params.clone(), block_height), - &outputs, - ) - } -} - -#[tracing::instrument(skip_all, fields(height = block.height))] -pub(crate) fn scan_block_with_runner< - P: consensus::Parameters + Send + 'static, - K: ScanningKey, - T: Tasks> + Sync, ->( - params: &P, - block: CompactBlock, - vks: &[(&AccountId, &K)], - nullifiers: &[(AccountId, Nullifier)], - tree: &mut sapling::CommitmentTree, - existing_witnesses: &mut [&mut sapling::IncrementalWitness], - mut batch_runner: Option<&mut TaggedBatchRunner>, -) -> Vec> { - let mut wtxs: Vec> = vec![]; - let block_height = block.height(); - let block_hash = block.hash(); - - for tx in block.vtx.into_iter() { - let txid = tx.txid(); - let index = tx.index as usize; - - // 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::from(0), 0.into()), - |first, next| CtOption::conditional_select(&next, &first, first.is_some()), - ) - .map(|account| WalletSaplingSpend::from_parts(index, 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.sapling_outputs - .iter_mut() - .map(|output| output.witness_mut()) - }) - .collect(); - - let decoded = &tx - .outputs - .into_iter() - .map(|output| { - ( - SaplingDomain::for_height(params.clone(), block_height), - CompactOutputDescription::try_from(output) - .expect("Invalid output found in compact block decoding."), - ) - }) - .collect::>(); - - let decrypted: Vec<_> = if let Some(runner) = batch_runner.as_mut() { - let vks = vks - .iter() - .flat_map(|(a, k)| { - k.to_sapling_keys() - .into_iter() - .map(move |(scope, _, nk)| ((**a, scope), nk)) - }) - .collect::>(); - - let mut decrypted = runner.collect_results(block_hash, txid); - (0..decoded.len()) - .map(|i| { - decrypted.remove(&(txid, i)).map(|d_note| { - let a = d_note.ivk_tag.0; - let nk = vks.get(&d_note.ivk_tag).expect( - "The batch runner and scan_block must use the same set of IVKs.", - ); - - ((d_note.note, d_note.recipient), a, (*nk).clone()) - }) - }) - .collect() - } else { - let vks = vks - .iter() - .flat_map(|(a, k)| { - k.to_sapling_keys() - .into_iter() - .map(move |(_, ivk, nk)| (**a, ivk, nk)) - }) - .collect::>(); - - let ivks = vks - .iter() - .map(|(_, ivk, _)| ivk) - .map(PreparedIncomingViewingKey::new) - .collect::>(); - - batch::try_compact_note_decryption(&ivks, decoded) - .into_iter() - .map(|v| { - v.map(|(note_data, ivk_idx)| { - let (account, _, nk) = &vks[ivk_idx]; - (note_data, *account, (*nk).clone()) - }) - }) - .collect() - }; - - for (index, ((_, output), dec_output)) in decoded.iter().zip(decrypted).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 new_witnesses: Vec<_> = shielded_outputs - .iter_mut() - .map(|out| out.witness_mut()) - .collect(); - - // Increment tree and witnesses - let node = Node::from_cmu(&output.cmu); - for witness in &mut *existing_witnesses { - witness.append(node).unwrap(); - } - for witness in &mut block_witnesses { - witness.append(node).unwrap(); - } - for witness in new_witnesses { - witness.append(node).unwrap(); - } - tree.append(node).unwrap(); - - if let Some(((note, _), account, nk)) = dec_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(&account); - let witness = sapling::IncrementalWitness::from_tree(tree.clone()); - let nf = K::sapling_nf(&nk, ¬e, &witness); - - shielded_outputs.push(WalletSaplingOutput::from_parts( - index, - output.cmu, - output.ephemeral_key.clone(), - account, - note, - is_change, - witness, - nf, - )) - } - } - } - - if !(shielded_spends.is_empty() && shielded_outputs.is_empty()) { - wtxs.push(WalletTx { - txid, - index, - sapling_spends: shielded_spends, - sapling_outputs: shielded_outputs, - }); - } - } - - wtxs -} - -#[cfg(test)] -mod tests { - use group::{ - ff::{Field, PrimeField}, - GroupEncoding, - }; - use rand_core::{OsRng, RngCore}; - use zcash_note_encryption::Domain; - use zcash_primitives::{ - consensus::{BlockHeight, Network}, - constants::SPENDING_KEY_GENERATOR, - memo::MemoBytes, - sapling::{ - note_encryption::{sapling_note_encryption, PreparedIncomingViewingKey, SaplingDomain}, - util::generate_random_rseed, - value::NoteValue, - CommitmentTree, Note, Nullifier, SaplingIvk, - }, - transaction::components::Amount, - zip32::{AccountId, DiversifiableFullViewingKey, ExtendedSpendingKey}, - }; - - use crate::{ - proto::compact_formats::{ - CompactBlock, CompactSaplingOutput, CompactSaplingSpend, CompactTx, - }, - scan::BatchRunner, - }; - - use super::{add_block_to_runner, scan_block, scan_block_with_runner, ScanningKey}; - - 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 cspend = CompactSaplingSpend { nf: fake_nf }; - let cout = CompactSaplingOutput { - cmu: fake_cmu, - ephemeral_key: fake_epk, - ciphertext: vec![0; 52], - }; - 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. - fn fake_compact_block( - height: BlockHeight, - nf: Nullifier, - dfvk: &DiversifiableFullViewingKey, - value: Amount, - tx_after: bool, - ) -> CompactBlock { - let to = dfvk.default_address().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::from_parts(to, NoteValue::from_raw(value.into()), rseed); - let encryptor = sapling_note_encryption::<_, Network>( - Some(dfvk.fvk().ovk), - note.clone(), - MemoBytes::empty(), - &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 - }, - 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.as_ref()[..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 - } - - #[test] - fn scan_block_with_my_tx() { - fn go(scan_multithreaded: bool) { - let account = AccountId::from(0); - let extsk = ExtendedSpendingKey::master(&[]); - let dfvk = extsk.to_diversifiable_full_viewing_key(); - - let cb = fake_compact_block( - 1u32.into(), - Nullifier([0; 32]), - &dfvk, - Amount::from_u64(5).unwrap(), - false, - ); - assert_eq!(cb.vtx.len(), 2); - - let mut tree = CommitmentTree::empty(); - let mut batch_runner = if scan_multithreaded { - let mut runner = BatchRunner::<_, _, _, ()>::new( - 10, - dfvk.to_sapling_keys() - .iter() - .map(|(scope, ivk, _)| ((account, *scope), ivk)) - .map(|(tag, ivk)| (tag, PreparedIncomingViewingKey::new(ivk))), - ); - - add_block_to_runner(&Network::TestNetwork, cb.clone(), &mut runner); - runner.flush(); - - Some(runner) - } else { - None - }; - - let txs = scan_block_with_runner( - &Network::TestNetwork, - cb, - &[(&account, &dfvk)], - &[], - &mut tree, - &mut [], - batch_runner.as_mut(), - ); - assert_eq!(txs.len(), 1); - - let tx = &txs[0]; - assert_eq!(tx.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(), account); - assert_eq!(tx.sapling_outputs[0].note().value().inner(), 5); - - // Check that the witness root matches - assert_eq!(tx.sapling_outputs[0].witness().root(), tree.root()); - } - - go(false); - go(true); - } - - #[test] - fn scan_block_with_txs_after_my_tx() { - fn go(scan_multithreaded: bool) { - let account = AccountId::from(0); - let extsk = ExtendedSpendingKey::master(&[]); - let dfvk = extsk.to_diversifiable_full_viewing_key(); - - let cb = fake_compact_block( - 1u32.into(), - Nullifier([0; 32]), - &dfvk, - Amount::from_u64(5).unwrap(), - true, - ); - assert_eq!(cb.vtx.len(), 3); - - let mut tree = CommitmentTree::empty(); - let mut batch_runner = if scan_multithreaded { - let mut runner = BatchRunner::<_, _, _, ()>::new( - 10, - dfvk.to_sapling_keys() - .iter() - .map(|(scope, ivk, _)| ((account, *scope), ivk)) - .map(|(tag, ivk)| (tag, PreparedIncomingViewingKey::new(ivk))), - ); - - add_block_to_runner(&Network::TestNetwork, cb.clone(), &mut runner); - runner.flush(); - - Some(runner) - } else { - None - }; - - let txs = scan_block_with_runner( - &Network::TestNetwork, - cb, - &[(&AccountId::from(0), &dfvk)], - &[], - &mut tree, - &mut [], - batch_runner.as_mut(), - ); - assert_eq!(txs.len(), 1); - - let tx = &txs[0]; - assert_eq!(tx.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(), AccountId::from(0)); - assert_eq!(tx.sapling_outputs[0].note().value().inner(), 5); - - // Check that the witness root matches - assert_eq!(tx.sapling_outputs[0].witness().root(), tree.root()); - } - - go(false); - go(true); - } - - #[test] - fn scan_block_with_my_spend() { - let extsk = ExtendedSpendingKey::master(&[]); - let dfvk = extsk.to_diversifiable_full_viewing_key(); - let nf = Nullifier([7; 32]); - let account = AccountId::from(12); - - let cb = fake_compact_block(1u32.into(), nf, &dfvk, 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.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(), account); - } -} diff --git a/zcash_client_sqlite/CHANGELOG.md b/zcash_client_sqlite/CHANGELOG.md index d72fe90ac0..f46d4de6d2 100644 --- a/zcash_client_sqlite/CHANGELOG.md +++ b/zcash_client_sqlite/CHANGELOG.md @@ -6,13 +6,386 @@ and this library adheres to Rust's notion of [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +### Added +- `zcash_client_sqlite::wallet::init::WalletMigrator` +- `zcash_client_sqlite::wallet::init::migrations` + +### Changed +- `zcash_client_sqlite::wallet::init::WalletMigrationError::` + - Variants `WalletMigrationError::CommitmentTree` and + `WalletMigrationError::Other` now `Box` their contents. + +## [0.16.2] - 2025-04-02 + +### Fixed +- This release fixes a migration error that could cause some wallets + to crash on startup due to an attempt to associate a received transparent + output with an address that does not exist in the wallet's `addresses` + table. + +## [0.16.1] - 2025-03-26 + +### Fixed +- This release fixes a migration error that could cause some wallets + to crash on startup due to an attempt to derive a unified address with + a Sapling receiver at an index for which no Sapling receiver can exist. + +## [0.16.0] - 2025-03-19 + +### Added +- `zcash_client_sqlite::WalletDb::with_gap_limits` +- `zcash_client_sqlite::GapLimits` +- `zcash_client_sqlite::util` +- `zcash_client_sqlite::schedule_ephemeral_address_checks` has been added under + the `transparent-inputs` feature flag. +- `zcash_client_sqlite::wallet::transparent::SchedulingError` + +### Changed +- Updated to `zcash_keys 0.8`, `zcash_client_backend 0.18` +- `zcash_client_sqlite::WalletDb` has added fields and type parameters: + - a `clock` field and corresponding type parameter. Tests that make use of + `WalletDb` now use a `zcash_client_sqlite::util::FixedClock` for this + field value. + - an `rng` field and corresponding type parameter. Tests that make use of + `WalletDb` now use a `ChaChaRng` value initialized with the all-zeros + seed for this field value. + - the following methods have been changed to accept additional parameters + as a result of these changes: + - `WalletDb::for_path` + - `WalletDb::from_connection` + - `wallet::init::init_wallet_db` has additional type constraints +- `zcash_client_sqlite::WalletDb::get_address_for_index` now returns some of + its failure modes via `Err(SqliteClientError::AddressGeneration)` instead of + `Ok(None)`. +- `zcash_client_sqlite::error::SqliteClientError` variants have changed: + - The `EphemeralAddressReuse` variant has been removed and replaced + by a new generalized `AddressReuse` error variant. + - The `ReachedGapLimit` variant no longer includes the account UUID + for the account that reached the limit in its payload. In addition + to the transparent address index, it also contains the key scope + involved when the error was encountered. + - A new `DiversifierIndexReuse` variant has been added. + - A new `Scheduling` variant has been added. +- Each row returned from the `v_received_outputs` view now exposes an + internal identifier for the address that received that output. This should + be ignored by external consumers of this view. + +## [0.15.0] - 2025-02-21 + +### Added +- `zcash_client_sqlite::WalletDb::from_connection` +- `zcash_client_sqlite::WalletDb::check_witnesses` +- `zcash_client_sqlite::WalletDb::queue_rescans` + +### Changed +- MSRV is now 1.81.0. +- Migrated to `bip32 =0.6.0-pre.1`, `nonempty 0.11`.`incrementalmerkletree 0.8`, + `shardtree 0.6`, `orchard 0.11`, `sapling-crypto 0.5`, `zcash_encoding 0.3`, + `zcash_protocol 0.5`, `zcash_address 0.7`, `zcash_transparent 0.2`, + `zcash_primitives 0.22`, `zcash_keys 0.7`, `zcash_client_backend 0.17`. +- `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`. +- The `v_transactions` view has added columns `total_spent` and `total_received`. + +## [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 +- 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.4`, `bs58 0.5`, - `zcash_primitives 0.12` +- 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 diff --git a/zcash_client_sqlite/Cargo.toml b/zcash_client_sqlite/Cargo.toml index c58dba2a7d..c724f8590a 100644 --- a/zcash_client_sqlite/Cargo.toml +++ b/zcash_client_sqlite/Cargo.toml @@ -1,69 +1,155 @@ [package] name = "zcash_client_sqlite" description = "An SQLite-based Zcash light client" -version = "0.7.1" +version = "0.16.2" 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 = "2021" -rust-version = "1.65" +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] -incrementalmerkletree = { version = "0.4", features = ["legacy-api"] } -zcash_client_backend = { version = "0.9", path = "../zcash_client_backend" } -zcash_primitives = { version = "0.12", path = "../zcash_primitives", default-features = false } +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 -bs58 = { version = "0.5", features = ["check"] } -hdwallet = { version = "0.4", optional = true } +bip32 = { workspace = true, optional = true } +bs58.workspace = true # - Logging and metrics -tracing = "0.1" +tracing.workspace = true -# - Protobuf interfaces -prost = "0.11" +# - Serialization +bitflags.workspace = true +byteorder.workspace = true +nonempty.workspace = true +prost.workspace = true +group.workspace = true +jubjub.workspace = true +serde = { workspace = true, optional = true } # - Secret management -secrecy = "0.8" +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 -group = "0.13" -jubjub = "0.10" -rusqlite = { version = "0.25", features = ["bundled", "time", "array"] } -schemer = "0.2" -schemer-rusqlite = "0.2.1" -time = "0.2" -uuid = "1.1" +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 +rand_core.workspace = true +rand_distr.workspace = true +rand.workspace = true [dev-dependencies] -assert_matches = "1.5" -proptest = "1.0.0" -rand_core = "0.6" -regex = "1.4" +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_note_encryption = "0.4" -zcash_proofs = { version = "0.12", path = "../zcash_proofs" } -zcash_primitives = { version = "0.12", path = "../zcash_primitives", features = ["test-dependencies"] } -zcash_address = { version = "0.3", path = "../components/zcash_address", features = ["test-dependencies"] } +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 = [] +default = ["multicore"] +zip-233 = ["zcash_primitives/zip-233"] + +## 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" ] -transparent-inputs = ["hdwallet", "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/README.md b/zcash_client_sqlite/README.md index fb71ab098b..af077e8d59 100644 --- a/zcash_client_sqlite/README.md +++ b/zcash_client_sqlite/README.md @@ -24,16 +24,6 @@ Licensed under either of at your option. -Downstream code forks should note that 'zcash_client_sqlite' depends on the -'orchard' crate, which is licensed under the -[Bootstrap Open Source License](https://github.com/zcash/orchard/blob/main/LICENSE-BOSL). -A license exception is provided allowing some derived works that are linked or -combined with the 'orchard' crate to be copied or distributed under the original -licenses (in this case MIT / Apache 2.0), provided that the included portions of -the 'orchard' code remain subject to BOSL. -See https://github.com/zcash/orchard/blob/main/COPYING for details of which -derived works can make use of this exception. - ### Contribution Unless you explicitly state otherwise, any contribution intentionally diff --git a/zcash_client_sqlite/src/chain.rs b/zcash_client_sqlite/src/chain.rs index fc9e8d09f2..a8835e2324 100644 --- a/zcash_client_sqlite/src/chain.rs +++ b/zcash_client_sqlite/src/chain.rs @@ -3,7 +3,7 @@ use prost::Message; use rusqlite::params; -use zcash_primitives::consensus::BlockHeight; +use zcash_protocol::consensus::BlockHeight; use zcash_client_backend::{data_api::chain::error::Error, proto::compact_formats::CompactBlock}; @@ -23,19 +23,19 @@ pub mod migrations; /// Implements a traversal of `limit` blocks of the block cache database. /// -/// Starting at the next block above `last_scanned_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(crate) fn blockdb_with_blocks( +/// 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(crate) fn blockdb_with_blocks( block_source: &BlockDb, - last_scanned_height: Option, - limit: Option, + from_height: Option, + limit: Option, mut with_row: F, -) -> Result<(), Error> +) -> Result<(), Error> where - F: FnMut(CompactBlock) -> Result<(), Error>, + F: FnMut(CompactBlock) -> Result<(), Error>, { - fn to_chain_error, N>(err: E) -> Error { + fn to_chain_error>(err: E) -> Error { Error::BlockSource(err.into()) } @@ -43,21 +43,35 @@ where let mut stmt_blocks = block_source .0 .prepare( - "SELECT height, data FROM compactblocks - WHERE height > ? + "SELECT height, data FROM compactblocks + WHERE height >= ? ORDER BY height ASC LIMIT ?", ) .map_err(to_chain_error)?; let mut rows = stmt_blocks .query(params![ - last_scanned_height.map_or(0u32, u32::from), - limit.unwrap_or(u32::max_value()), + 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 { @@ -71,6 +85,11 @@ where 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(()) } @@ -101,21 +120,40 @@ 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 (?, ?, ?, ?, ?)" + "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", )?; conn.execute("BEGIN IMMEDIATE", [])?; let result = block_meta .iter() .map(|m| { - stmt_insert.execute(params![ - u32::from(m.height), - &m.block_hash.0[..], - m.block_time, - m.sapling_outputs_count, - m.orchard_actions_count, + 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::, _>>(); @@ -191,20 +229,20 @@ pub(crate) fn blockmetadb_find_block( /// Implements a traversal of `limit` blocks of the filesystem-backed /// block cache. /// -/// Starting at the next block height above `last_scanned_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. +/// 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( +pub(crate) fn fsblockdb_with_blocks( cache: &FsBlockDb, - last_scanned_height: Option, - limit: Option, + from_height: Option, + limit: Option, mut with_block: F, -) -> Result<(), Error> +) -> Result<(), Error> where - F: FnMut(CompactBlock) -> Result<(), Error>, + F: FnMut(CompactBlock) -> Result<(), Error>, { - fn to_chain_error, N>(err: E) -> Error { + fn to_chain_error>(err: E) -> Error { Error::BlockSource(err.into()) } @@ -214,7 +252,7 @@ where .prepare( "SELECT height, blockhash, time, sapling_outputs_count, orchard_actions_count FROM compactblocks_meta - WHERE height > ? + WHERE height >= ? ORDER BY height ASC LIMIT ?", ) .map_err(to_chain_error)?; @@ -222,8 +260,10 @@ where let rows = stmt_blocks .query_map( params![ - last_scanned_height.map_or(0u32, u32::from), - limit.unwrap_or(u32::max_value()), + from_height.map_or(0u32, u32::from), + limit + .and_then(|l| u32::try_from(l).ok()) + .unwrap_or(u32::MAX) ], |row| { Ok(BlockMeta { @@ -237,8 +277,20 @@ where ) .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.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![]; @@ -259,481 +311,109 @@ where 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(()) } #[cfg(test)] -#[allow(deprecated)] mod tests { - use secrecy::Secret; - use tempfile::NamedTempFile; - - use zcash_primitives::{ - block::BlockHash, transaction::components::Amount, zip32::ExtendedSpendingKey, - }; - - use zcash_client_backend::data_api::chain::{ - error::{Cause, Error}, - scan_cached_blocks, validate_chain, - }; - use zcash_client_backend::data_api::WalletRead; - - use crate::{ - chain::init::init_cache_database, - tests::{ - self, fake_compact_block, fake_compact_block_spending, init_test_accounts_table, - insert_into_cache, sapling_activation_height, AddressType, - }, - wallet::{get_balance, init::init_wallet_db, truncate_to_height}, - AccountId, BlockDb, 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 mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap(); - - // Add an account to the wallet - let (dfvk, _taddr) = init_test_accounts_table(&db_data); - - // Empty chain should return None - assert_matches!(db_data.get_max_height_hash(), Ok(None)); - - // Create a fake CompactBlock sending value to the address - let fake_block_hash = BlockHash([0; 32]); - let fake_block_height = sapling_activation_height(); - - let (cb, _) = fake_compact_block( - fake_block_height, - fake_block_hash, - &dfvk, - AddressType::DefaultExternal, - Amount::from_u64(5).unwrap(), - ); - - insert_into_cache(&db_cache, &cb); - - // Cache-only chain should be valid - let validate_chain_result = validate_chain( - &db_cache, - Some((fake_block_height, fake_block_hash)), - Some(1), - ); - - assert_matches!(validate_chain_result, Ok(())); - - // 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(&db_cache, db_data.get_max_height_hash().unwrap(), None).unwrap(); - - // Create a second fake CompactBlock sending more value to the address - let (cb2, _) = fake_compact_block( - sapling_activation_height() + 1, - cb.hash(), - &dfvk, - AddressType::DefaultExternal, - Amount::from_u64(7).unwrap(), - ); - insert_into_cache(&db_cache, &cb2); - - // Data+cache chain should be valid - validate_chain(&db_cache, db_data.get_max_height_hash().unwrap(), None).unwrap(); - - // Scan the cache again - scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); - - // Data-only chain should be valid - validate_chain(&db_cache, db_data.get_max_height_hash().unwrap(), None).unwrap(); + fn valid_chain_states_sapling() { + testing::pool::valid_chain_states::() } #[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 mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap(); - - // Add an account to the wallet - let (dfvk, _taddr) = init_test_accounts_table(&db_data); - - // Create some fake CompactBlocks - let (cb, _) = fake_compact_block( - sapling_activation_height(), - BlockHash([0; 32]), - &dfvk, - AddressType::DefaultExternal, - Amount::from_u64(5).unwrap(), - ); - let (cb2, _) = fake_compact_block( - sapling_activation_height() + 1, - cb.hash(), - &dfvk, - AddressType::DefaultExternal, - 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(&db_cache, db_data.get_max_height_hash().unwrap(), None).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]), - &dfvk, - AddressType::DefaultExternal, - Amount::from_u64(8).unwrap(), - ); - let (cb4, _) = fake_compact_block( - sapling_activation_height() + 3, - cb3.hash(), - &dfvk, - AddressType::DefaultExternal, - 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 - let val_result = validate_chain(&db_cache, db_data.get_max_height_hash().unwrap(), None); - - assert_matches!(val_result, Err(Error::Chain(e)) if e.at_height() == sapling_activation_height() + 2); + #[cfg(feature = "orchard")] + fn valid_chain_states_orchard() { + testing::pool::valid_chain_states::() } #[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 mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap(); - - // Add an account to the wallet - let (dfvk, _taddr) = init_test_accounts_table(&db_data); - - // Create some fake CompactBlocks - let (cb, _) = fake_compact_block( - sapling_activation_height(), - BlockHash([0; 32]), - &dfvk, - AddressType::DefaultExternal, - Amount::from_u64(5).unwrap(), - ); - let (cb2, _) = fake_compact_block( - sapling_activation_height() + 1, - cb.hash(), - &dfvk, - AddressType::DefaultExternal, - 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(&db_cache, db_data.get_max_height_hash().unwrap(), None).unwrap(); - - // Create more fake CompactBlocks that contain a reorg - let (cb3, _) = fake_compact_block( - sapling_activation_height() + 2, - cb2.hash(), - &dfvk, - AddressType::DefaultExternal, - Amount::from_u64(8).unwrap(), - ); - let (cb4, _) = fake_compact_block( - sapling_activation_height() + 3, - BlockHash([1; 32]), - &dfvk, - AddressType::DefaultExternal, - 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 - let val_result = validate_chain(&db_cache, db_data.get_max_height_hash().unwrap(), None); - - assert_matches!(val_result, Err(Error::Chain(e)) if e.at_height() == sapling_activation_height() + 3); + #[cfg(feature = "orchard")] + fn invalid_chain_cache_disconnected_sapling() { + testing::pool::invalid_chain_cache_disconnected::() } #[test] - fn data_db_truncation() { - 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 mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap(); - - // Add an account to the wallet - let (dfvk, _taddr) = init_test_accounts_table(&db_data); - - // Account balance should be zero - assert_eq!( - get_balance(&db_data, AccountId::from(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]), - &dfvk, - AddressType::DefaultExternal, - value, - ); - - let (cb2, _) = fake_compact_block( - sapling_activation_height() + 1, - cb.hash(), - &dfvk, - AddressType::DefaultExternal, - 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::from(0)).unwrap(), - (value + value2).unwrap() - ); - - // "Rewind" to height of last scanned block - truncate_to_height(&db_data, sapling_activation_height() + 1).unwrap(); - - // Account balance should be unaltered - assert_eq!( - get_balance(&db_data, AccountId::from(0)).unwrap(), - (value + value2).unwrap() - ); - - // Rewind so that one block is dropped - truncate_to_height(&db_data, sapling_activation_height()).unwrap(); - - // Account balance should only contain the first received note - assert_eq!(get_balance(&db_data, AccountId::from(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::from(0)).unwrap(), - (value + value2).unwrap() - ); + #[cfg(feature = "orchard")] + fn invalid_chain_cache_disconnected_orchard() { + testing::pool::invalid_chain_cache_disconnected::() } #[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 mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap(); - - // Add an account to the wallet - let (dfvk, _taddr) = init_test_accounts_table(&db_data); - - // 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]), - &dfvk, - AddressType::DefaultExternal, - 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::from(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(), - &dfvk, - AddressType::DefaultExternal, - value, - ); - let (cb3, _) = fake_compact_block( - sapling_activation_height() + 2, - cb2.hash(), - &dfvk, - AddressType::DefaultExternal, - value, - ); - insert_into_cache(&db_cache, &cb3); - match scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None) { - Err(Error::Chain(e)) => { - assert_matches!( - e.cause(), - Cause::BlockHeightDiscontinuity(h) if *h - == sapling_activation_height() + 2 - ); - } - Ok(_) | Err(_) => panic!("Should have failed"), - } + fn data_db_truncation_sapling() { + testing::pool::data_db_truncation::() + } + + #[test] + #[cfg(feature = "orchard")] + fn data_db_truncation_orchard() { + testing::pool::data_db_truncation::() + } - // 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::from(0)).unwrap(), - Amount::from_u64(150_000).unwrap() - ); + #[test] + fn reorg_to_checkpoint_sapling() { + testing::pool::reorg_to_checkpoint::() + } + + #[test] + #[cfg(feature = "orchard")] + fn reorg_to_checkpoint_orchard() { + testing::pool::reorg_to_checkpoint::() + } + + #[test] + fn scan_cached_blocks_allows_blocks_out_of_order_sapling() { + testing::pool::scan_cached_blocks_allows_blocks_out_of_order::() + } + + #[test] + #[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::() + } + + #[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 mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap(); - - // Add an account to the wallet - let (dfvk, _taddr) = init_test_accounts_table(&db_data); - - // Account balance should be zero - assert_eq!( - get_balance(&db_data, AccountId::from(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]), - &dfvk, - AddressType::DefaultExternal, - 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::from(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(), - &dfvk, - AddressType::DefaultExternal, - 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::from(0)).unwrap(), - (value + value2).unwrap() - ); + 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 mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap(); - - // Add an account to the wallet - let (dfvk, _taddr) = init_test_accounts_table(&db_data); - - // Account balance should be zero - assert_eq!( - get_balance(&db_data, AccountId::from(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]), - &dfvk, - AddressType::DefaultExternal, - 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::from(0)).unwrap(), value); - - // Create a second fake CompactBlock spending value from the address - let extsk2 = ExtendedSpendingKey::master(&[0]); - let to2 = extsk2.default_address().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), - &dfvk, - 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::from(0)).unwrap(), - (value - value2).unwrap() - ); + #[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 53f6d50aed..944d724fa0 100644 --- a/zcash_client_sqlite/src/chain/init.rs +++ b/zcash_client_sqlite/src/chain/init.rs @@ -5,8 +5,8 @@ use crate::BlockDb; use { super::migrations, crate::FsBlockDb, - schemer::{Migrator, MigratorError}, - schemer_rusqlite::RusqliteAdapter, + schemerz::{Migrator, MigratorError}, + schemerz_rusqlite::RusqliteAdapter, }; /// Sets up the internal structure of the cache database. @@ -55,13 +55,15 @@ pub fn init_cache_database(db_cache: &BlockDb) -> Result<(), rusqlite::Error> { /// init_blockmeta_db(&mut db).unwrap(); /// ``` #[cfg(feature = "unstable")] -pub fn init_blockmeta_db(db: &mut FsBlockDb) -> Result<(), MigratorError> { +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()) + .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/blockmeta.rs b/zcash_client_sqlite/src/chain/migrations/blockmeta.rs index 854ccd6fcf..6ab2387c6f 100644 --- a/zcash_client_sqlite/src/chain/migrations/blockmeta.rs +++ b/zcash_client_sqlite/src/chain/migrations/blockmeta.rs @@ -1,4 +1,4 @@ -use schemer_rusqlite::RusqliteMigration; +use schemerz_rusqlite::RusqliteMigration; pub fn all_migrations() -> Vec>> { vec![Box::new(init::Migration {})] @@ -6,8 +6,8 @@ pub fn all_migrations() -> Vec, + requested_height: BlockHeight, + }, - /// A requested rewind would violate invariants of the - /// storage layer. The payload returned with this error is - /// (safe rewind height, requested height). - RequestedRewindInvalid(BlockHeight, BlockHeight), + /// An error occurred in generating a Zcash address. + AddressGeneration(AddressGenerationError), - /// The space of allocatable diversifier indices has been exhausted for - /// the given account. - DiversifierIndexOutOfRange, + /// The account for which information was requested does not belong to the wallet. + AccountUnknown, - /// An error occurred deriving a spending key from a seed and an account - /// identifier. - KeyDerivationError(AccountId), + /// 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), - /// A caller attempted to initialize the accounts table with a discontinuous - /// set of account identifiers. - AccountIdDiscontinuity, + /// The account was imported, and ZIP-32 derivation information is not known for it. + UnknownZip32Derivation, - /// A caller attempted to construct a new account with an invalid account identifier. - AccountIdOutOfRange, + /// 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 + /// 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), + + /// 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), + + /// An address cannot be reserved, or a proposal cannot be constructed until a transaction + /// containing outputs belonging to a previously reserved address has been mined. The error + /// contains the index that could not safely be reserved. + #[cfg(feature = "transparent-inputs")] + ReachedGapLimit(TransparentKeyScope, u32), + + /// The backend encountered an attempt to reuse a diversifier index to generate an address + /// having different receivers from an address that had previously been exposed for that + /// diversifier index. Returns the previously exposed address. + DiversifierIndexReuse(DiversifierIndex, Box), + + /// The wallet attempted to create a transaction that would use of one of the wallet's + /// previously-used addresses, potentially creating a problem with on-chain transaction + /// linkability. The returned value contains the string encoding of the address and the txid(s) + /// of the transactions in which it is known to have been used. + AddressReuse(String, NonEmpty), + + /// The wallet encountered an error when attempting to schedule wallet operations. + #[cfg(feature = "transparent-inputs")] + Scheduling(SchedulingError), } impl error::Error for SqliteClientError { fn source(&self) -> Option<&(dyn error::Error + 'static)> { match &self { SqliteClientError::InvalidMemo(e) => Some(e), - SqliteClientError::Bech32DecodeError(Bech32DecodeError::Bech32Error(e)) => Some(e), SqliteClientError::DbError(e) => Some(e), SqliteClientError::Io(e) => Some(e), + SqliteClientError::BalanceError(e) => Some(e), + SqliteClientError::AddressGeneration(e) => Some(e), _ => None, } } @@ -96,24 +166,63 @@ impl fmt::Display for SqliteClientError { } 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::RequestedRewindInvalid(h, r) => - write!(f, "A rewind must be either of less than {} blocks, or at least back to block {} for your wallet; the requested height was {}.", PRUNING_HEIGHT, h, r), - SqliteClientError::Bech32DecodeError(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::HdwalletError(e) => write!(f, "{:?}", e), 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::DiversifierIndexOutOfRange => write!(f, "The space of available diversifier indices is exhausted"), - SqliteClientError::KeyDerivationError(acct_id) => write!(f, "Key derivation failed for account {:?}", acct_id), - SqliteClientError::AccountIdDiscontinuity => write!(f, "Wallet account identifiers must be sequential."), - SqliteClientError::AccountIdOutOfRange => write!(f, "Wallet account identifiers must be less than 0x7FFFFFFF."), + 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(key_scope, bad_index) => write!(f, + "The proposal cannot be constructed until a transaction with outputs to a previously reserved {} address has been mined. \ + The address at index {bad_index} could not be safely reserved.", + match *key_scope { + TransparentKeyScope::EXTERNAL => "external transparent", + TransparentKeyScope::INTERNAL => "transparent change", + TransparentKeyScope::EPHEMERAL => "ephemeral transparent", + _ => panic!("Unsupported transparent key scope.") + } + ), + SqliteClientError::DiversifierIndexReuse(i, _) => { + write!( + f, + "An address has already been exposed for diversifier index {}", + u128::from(*i) + ) + } + SqliteClientError::AddressReuse(address_str, txids) => { + write!(f, "The address {address_str} previously used in txid(s) {:?} would be reused.", txids) + } + #[cfg(feature = "transparent-inputs")] + SqliteClientError::Scheduling(err) => { + write!(f, "The wallet was unable to schedule an event: {}", err) + } } } } @@ -129,10 +238,9 @@ impl From for SqliteClientError { SqliteClientError::Io(e) } } - -impl From for SqliteClientError { - fn from(e: Bech32DecodeError) -> Self { - SqliteClientError::Bech32DecodeError(e) +impl From for SqliteClientError { + fn from(e: ParseError) -> Self { + SqliteClientError::DecodingError(e) } } @@ -143,20 +251,46 @@ impl From for SqliteClientError { } #[cfg(feature = "transparent-inputs")] -impl From for SqliteClientError { - fn from(e: hdwallet::error::Error) -> Self { - SqliteClientError::HdwalletError(e) +impl From for SqliteClientError { + fn from(e: bip32::Error) -> Self { + SqliteClientError::TransparentDerivation(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: 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) + } +} + +#[cfg(feature = "transparent-inputs")] +impl From for SqliteClientError { + fn from(value: SchedulingError) -> Self { + SqliteClientError::Scheduling(value) + } +} diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index 60cc142ae0..f8c5416657 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -18,10 +18,8 @@ //! **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 @@ -29,41 +27,122 @@ //! [`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(rustdoc::broken_intra_doc_links)] -use rusqlite::Connection; +use incrementalmerkletree::{Marking, Position, Retention}; +use nonempty::NonEmpty; +use rusqlite::{self, Connection}; use secrecy::{ExposeSecret, SecretVec}; -use std::collections::HashMap; -use std::fmt; -use std::path::Path; - -use zcash_primitives::{ - block::BlockHash, - consensus::{self, BlockHeight}, - legacy::TransparentAddress, - memo::{Memo, MemoBytes}, - sapling::{self}, - transaction::{ - components::{amount::Amount, OutPoint}, - Transaction, TxId, - }, - zip32::{AccountId, DiversifierIndex, ExtendedFullViewingKey}, +use shardtree::{error::ShardTreeError, store::ShardStore, ShardTree}; +use std::{ + borrow::{Borrow, BorrowMut}, + cmp::{max, min}, + collections::HashMap, + convert::AsRef, + fmt, + num::NonZeroU32, + ops::Range, + path::Path, }; +use subtle::ConditionallySelectable; +use tracing::{debug, trace, warn}; +use util::Clock; +use uuid::Uuid; use zcash_client_backend::{ - address::{AddressMetadata, UnifiedAddress}, data_api::{ - self, chain::BlockSource, DecryptedTransaction, NullifierQuery, PoolType, PrunedBlock, - Recipient, SentTransaction, WalletRead, WalletWrite, + self, + chain::{BlockSource, ChainState, CommitmentTreeRoot}, + scanning::{ScanPriority, ScanRange}, + Account, AccountBirthday, AccountMeta, AccountPurpose, AccountSource, AddressInfo, + BlockMetadata, DecryptedTransaction, InputSource, NoteFilter, NullifierQuery, ScannedBlock, + SeedRelevance, SentTransaction, SpendableNotes, TargetValue, TransactionDataRequest, + WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite, Zip32Derivation, + SAPLING_SHARD_HEIGHT, }, - keys::{UnifiedFullViewingKey, UnifiedSpendingKey}, proto::compact_formats::CompactBlock, - wallet::{ReceivedSaplingNote, WalletTransparentOutput}, - DecryptedOutput, TransferType, + wallet::{Note, NoteId, ReceivedNote, WalletTransparentOutput}, + TransferType, +}; +use zcash_keys::{ + address::UnifiedAddress, + keys::{ReceiverRequirement, UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey}, +}; +use zcash_primitives::{ + block::BlockHash, + transaction::{Transaction, TxId}, +}; +use zcash_protocol::{ + consensus::{self, BlockHeight}, + memo::Memo, + ShieldedProtocol, +}; +use zip32::{fingerprint::SeedFingerprint, DiversifierIndex}; + +use crate::{error::SqliteClientError, wallet::commitment_tree::SqliteShardStore}; +use wallet::{ + commitment_tree::{self, put_shard_roots}, + common::spendable_notes_meta, + scanning::replace_queue_entries, + upsert_address, SubtreeProgressEstimator, }; -use crate::error::SqliteClientError; +#[cfg(feature = "orchard")] +use { + incrementalmerkletree::frontier::Frontier, shardtree::store::Checkpoint, + std::collections::BTreeMap, zcash_client_backend::data_api::ORCHARD_SHARD_HEIGHT, +}; + +#[cfg(feature = "transparent-inputs")] +use { + crate::wallet::transparent::ephemeral::schedule_ephemeral_address_checks, + ::transparent::{address::TransparentAddress, bundle::OutPoint, keys::NonHardenedChildIndex}, + std::collections::BTreeSet, + zcash_client_backend::wallet::TransparentAddressMetadata, + zcash_keys::encoding::AddressCodec, +}; + +#[cfg(feature = "multicore")] +use maybe_rayon::{ + prelude::{IndexedParallelIterator, ParallelIterator}, + slice::ParallelSliceMut, +}; + +#[cfg(any(test, feature = "test-dependencies"))] +use { + rusqlite::named_params, + zcash_client_backend::data_api::{testing::TransactionSummary, OutputOfSentTx, WalletTest}, + zcash_keys::address::Address, +}; + +#[cfg(any(test, feature = "test-dependencies", feature = "transparent-inputs"))] +use crate::wallet::encoding::KeyScope; + +#[cfg(any(test, feature = "test-dependencies", not(feature = "orchard")))] +use zcash_protocol::PoolType; + +#[cfg(any( + test, + feature = "transparent-inputs", + feature = "unstable", + feature = "test-dependencies" +))] +use zcash_protocol::value::Zatoshis; + +/// `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 { @@ -72,652 +151,1911 @@ use { std::{fs, io}, }; -mod prepared; -pub use prepared::DataConnStmtCache; - pub mod chain; pub mod error; +pub mod util; pub mod wallet; +#[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_HEIGHT: u32 = 100; +pub(crate) const PRUNING_DEPTH: u32 = 100; -/// A newtype wrapper for sqlite primary key values for the notes -/// table. -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub enum NoteId { - SentNoteId(i64), - ReceivedNoteId(i64), +/// 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, PartialOrd, Ord)] +#[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 + } } -impl fmt::Display for NoteId { +/// 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, PartialOrd, Ord)] +pub(crate) struct AccountRef(i64); + +/// 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, + )) + } +} + +/// 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. +/// A newtype wrapper for sqlite primary key values for the utxos table. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct UtxoId(pub i64); -/// A wrapper for the SQLite connection to the wallet database. -pub struct WalletDb

{ - conn: Connection, +/// A newtype wrapper for sqlite primary key values for the transactions table. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +struct TxRef(pub i64); + +/// A newtype wrapper for sqlite primary key values for the addresses table. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +struct AddressRef(pub(crate) i64); + +/// A data structure that can be used to configure custom gap limits for use in transparent address +/// rotation. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg(feature = "transparent-inputs")] +pub struct GapLimits { + external: u32, + internal: u32, + ephemeral: u32, +} + +#[cfg(feature = "transparent-inputs")] +impl GapLimits { + /// Constructs a new `GapLimits` value from its constituent parts. + /// + /// The gap limits recommended for use with this crate are supplied by the [`Default`] + /// implementation for this type. + /// + /// This constructor is only available under the `unstable` feature, as it is not recommended + /// for general use. + #[cfg(any(test, feature = "test-dependencies", feature = "unstable"))] + pub fn from_parts(external: u32, internal: u32, ephemeral: u32) -> Self { + Self { + external, + internal, + ephemeral, + } + } + + pub(crate) fn external(&self) -> u32 { + self.external + } + + pub(crate) fn internal(&self) -> u32 { + self.internal + } + + pub(crate) fn ephemeral(&self) -> u32 { + self.ephemeral + } +} + +/// The default gap limits supported by this implementation are: +/// +/// - external addresses: 10 +/// - transparent internal (change) addresses: 5 +/// - ephemeral addresses: 5 +/// +/// These limits are chosen with the following rationale: +/// - At present, many wallets query light wallet servers with a set of addresses, because querying +/// for each address independently and in a fashion that is not susceptible to clustering via +/// timing correlation leads to undesirable delays in discovery of received funds. As such, it is +/// desirable to minimize the number of addresses that can be "linked", i.e. understood by the +/// light wallet server to all belong to the same wallet. +/// - For transparent change addresses and ephemeral addresses, it is always expected that an +/// address will receive funds immediately following its generation except in the case of wallet +/// failure. +/// - For externally-scoped transparent addresses, it is desirable to use a slightly larger gap +/// limit to account for addresses that were shared with counterparties never having been used. +/// However, we don't want to use the full 20-address gap limit space because it's possible that +/// in the future, changes to the light wallet protocol will obviate the need to query for UTXOs +/// in a fashion that links those addresses. In such a circumstance, the gap limit will be +/// adjusted upward and address rotation should then choose an address that is outside the +/// current gap limit; after that change, newly generated addresses will not be exposed as +/// linked in the view of the light wallet server. +#[cfg(feature = "transparent-inputs")] +impl Default for GapLimits { + fn default() -> Self { + Self { + external: 10, + internal: 5, + ephemeral: 5, + } + } +} + +#[cfg(all( + any(test, feature = "test-dependencies"), + feature = "transparent-inputs" +))] +impl From for zcash_client_backend::data_api::testing::transparent::GapLimits { + fn from(value: GapLimits) -> Self { + zcash_client_backend::data_api::testing::transparent::GapLimits::new( + value.external, + value.internal, + value.ephemeral, + ) + } +} + +#[cfg(all( + any(test, feature = "test-dependencies"), + feature = "transparent-inputs" +))] +impl From for GapLimits { + fn from(value: zcash_client_backend::data_api::testing::transparent::GapLimits) -> Self { + GapLimits::from_parts(value.external(), value.internal(), value.ephemeral()) + } +} + +/// A wrapper for the SQLite connection to the wallet database, along with a capability to read the +/// system from the clock. A `WalletDb` encapsulates the full set of capabilities that are required +/// in order to implement the [`WalletRead`], [`WalletWrite`] and [`WalletCommitmentTrees`] traits. +pub struct WalletDb { + conn: C, params: P, + clock: CL, + rng: R, + #[cfg(feature = "transparent-inputs")] + gap_limits: GapLimits, +} + +/// 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 { +impl WalletDb { + /// Construct a [`WalletDb`] instance that connects to the wallet database stored at the + /// specified path. + /// + /// ## Parameters + /// - `path`: The path to the SQLite database used to store wallet data. + /// - `params`: Parameters associated with the Zcash network that the wallet will connect to. + /// - `clock`: The clock to use in the case that the backend needs access to the system time. + /// - `rng`: The random number generation capability to be exposed by the created `WalletDb` + /// instance. + pub fn for_path>( + path: F, + params: P, + clock: CL, + rng: R, + ) -> Result { Connection::open(path).and_then(move |conn| { rusqlite::vtab::array::load_module(&conn)?; - Ok(WalletDb { conn, params }) + Ok(WalletDb { + conn, + params, + clock, + rng, + #[cfg(feature = "transparent-inputs")] + gap_limits: GapLimits::default(), + }) }) } +} - /// 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> { - DataConnStmtCache::new(self) +#[cfg(feature = "transparent-inputs")] +impl WalletDb { + /// Sets the gap limits to be used by the wallet in transparent address generation. + pub fn with_gap_limits(mut self, gap_limits: GapLimits) -> Self { + self.gap_limits = gap_limits; + self } } -impl WalletRead for WalletDb

{ - type Error = SqliteClientError; - type NoteRef = NoteId; - type TxRef = i64; +impl, P, CL, R> 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. + /// + /// ## Parameters + /// - `conn`: A connection to the wallet database. + /// - `params`: Parameters associated with the Zcash network that the wallet will connect to. + /// - `clock`: The clock to use in the case that the backend needs access to the system time. + /// - `rng`: The random number generation capability to be exposed by the created `WalletDb` + /// instance. + pub fn from_connection(conn: C, params: P, clock: CL, rng: R) -> Self { + WalletDb { + conn, + params, + clock, + rng, + #[cfg(feature = "transparent-inputs")] + gap_limits: GapLimits::default(), + } + } +} - fn block_height_extrema(&self) -> Result, Self::Error> { - wallet::block_height_extrema(self).map_err(SqliteClientError::from) +impl, P, CL, R> WalletDb { + pub fn transactionally>(&mut self, f: F) -> Result + where + F: FnOnce(&mut WalletDb, &P, &CL, &mut R>) -> Result, + { + let tx = self.conn.borrow_mut().transaction()?; + let mut wdb = WalletDb { + conn: SqlTransaction(&tx), + params: &self.params, + clock: &self.clock, + rng: &mut self.rng, + #[cfg(feature = "transparent-inputs")] + gap_limits: self.gap_limits, + }; + let result = f(&mut wdb)?; + tx.commit()?; + Ok(result) } - fn get_min_unspent_height(&self) -> Result, Self::Error> { - wallet::get_min_unspent_height(self).map_err(SqliteClientError::from) + /// Attempts to construct a witness for each note belonging to the wallet that is believed by + /// the wallet to currently be spendable, and returns a vector of the ranges that must be + /// rescanned in order to correct missing witness data. + /// + /// This method is intended for repairing wallets that broke due to bugs in `shardtree`. + pub fn check_witnesses(&mut self) -> Result>, SqliteClientError> { + self.transactionally(|wdb| wallet::commitment_tree::check_witnesses(wdb.conn.0)) } - fn get_block_hash(&self, block_height: BlockHeight) -> Result, Self::Error> { - wallet::get_block_hash(self, block_height).map_err(SqliteClientError::from) + /// Updates the scan queue by inserting scan ranges for the given range of block heights, with + /// the specified scanning priority. + pub fn queue_rescans( + &mut self, + rescan_ranges: NonEmpty>, + priority: ScanPriority, + ) -> Result<(), SqliteClientError> { + let query_range = rescan_ranges + .iter() + .fold(None, |acc: Option>, scan_range| { + if let Some(range) = acc { + Some(min(range.start, scan_range.start)..max(range.end, scan_range.end)) + } else { + Some(scan_range.clone()) + } + }) + .expect("rescan_ranges is nonempty"); + + self.transactionally::<_, _, SqliteClientError>(|wdb| { + replace_queue_entries( + wdb.conn.0, + &query_range, + rescan_ranges + .into_iter() + .map(|r| ScanRange::from_parts(r, priority)), + true, + ) + })?; + + Ok(()) } +} - fn get_tx_height(&self, txid: TxId) -> Result, Self::Error> { - wallet::get_tx_height(self, txid).map_err(SqliteClientError::from) +#[cfg(feature = "transparent-inputs")] +impl, P, CL: Clock, R: rand::RngCore> WalletDb { + /// For each ephemeral address in the wallet, ensure that the transaction data request queue + /// contains a request for the wallet to check for UTXOs belonging to that address at some time + /// during the next 24-hour period. + /// + /// We use randomized scheduling of ephemeral address checks to ensure that a + /// lightwalletd-compromising adversary cannot use temporal clustering to determine what + /// ephemeral addresses belong to a given wallet. + pub fn schedule_ephemeral_address_checks(&mut self) -> Result<(), SqliteClientError> { + self.borrow_mut().transactionally(|wdb| { + schedule_ephemeral_address_checks(wdb.conn.0, wdb.clock, &mut wdb.rng) + }) } +} - fn get_unified_full_viewing_keys( +impl, P: consensus::Parameters, CL, R> InputSource + for WalletDb +{ + type Error = SqliteClientError; + type NoteRef = ReceivedNoteId; + type AccountId = AccountUuid; + + fn get_spendable_note( &self, - ) -> Result, Self::Error> { - wallet::get_unified_full_viewing_keys(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_account_for_ufvk( + fn select_spendable_notes( &self, - ufvk: &UnifiedFullViewingKey, - ) -> Result, Self::Error> { - wallet::get_account_for_ufvk(self, ufvk) + account: Self::AccountId, + target_value: TargetValue, + 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_current_address( + #[cfg(feature = "transparent-inputs")] + fn get_unspent_transparent_output( &self, - account: AccountId, - ) -> Result, Self::Error> { - wallet::get_current_address(self, account).map(|res| res.map(|(addr, _)| addr)) + outpoint: &OutPoint, + ) -> Result, Self::Error> { + wallet::transparent::get_wallet_transparent_output(self.conn.borrow(), outpoint, false) } - fn is_valid_account_extfvk( + #[cfg(feature = "transparent-inputs")] + fn get_spendable_transparent_outputs( &self, - account: AccountId, - extfvk: &ExtendedFullViewingKey, - ) -> Result { - wallet::is_valid_account_extfvk(self, account, extfvk) + 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_balance_at( + /// Returns metadata for the spendable notes in the wallet. + fn get_account_metadata( &self, - account: AccountId, - anchor_height: BlockHeight, - ) -> Result { - wallet::get_balance_at(self, account, anchor_height) + 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 get_transaction(&self, id_tx: i64) -> Result { - wallet::get_transaction(self, id_tx) +impl, P: consensus::Parameters, CL, R> 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_memo(&self, id_note: Self::NoteRef) -> Result, Self::Error> { - 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_account( + &self, + account_id: Self::AccountId, + ) -> Result, Self::Error> { + wallet::get_account(self.conn.borrow(), &self.params, account_id) } - fn get_commitment_tree( + fn get_derived_account( &self, - block_height: BlockHeight, - ) -> Result, Self::Error> { - wallet::sapling::get_sapling_commitment_tree(self, block_height) + seed: &SeedFingerprint, + account_id: zip32::AccountId, + ) -> Result, Self::Error> { + wallet::get_derived_account(self.conn.borrow(), &self.params, seed, account_id) } - #[allow(clippy::type_complexity)] - fn get_witnesses( + fn validate_seed( &self, - block_height: BlockHeight, - ) -> Result, Self::Error> { - wallet::sapling::get_sapling_witnesses(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) + } } - fn get_sapling_nullifiers( + fn seed_relevance_to_derived_accounts( &self, - query: data_api::NullifierQuery, - ) -> Result, Self::Error> { - match query { - NullifierQuery::Unspent => wallet::sapling::get_sapling_nullifiers(self), - NullifierQuery::All => wallet::sapling::get_all_sapling_nullifiers(self), + 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); + } + } } + + 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_sapling_notes( + fn get_account_for_ufvk( &self, - account: AccountId, - anchor_height: BlockHeight, - exclude: &[Self::NoteRef], - ) -> Result>, Self::Error> { - wallet::sapling::get_spendable_sapling_notes(self, account, anchor_height, exclude) + ufvk: &UnifiedFullViewingKey, + ) -> Result, Self::Error> { + wallet::get_account_for_ufvk(self.conn.borrow(), &self.params, ufvk) + } + + fn list_addresses(&self, account: Self::AccountId) -> Result, Self::Error> { + wallet::list_addresses(self.conn.borrow(), &self.params, account) } - fn select_spendable_sapling_notes( + fn get_last_generated_address_matching( &self, - account: AccountId, - target_value: Amount, - anchor_height: BlockHeight, - exclude: &[Self::NoteRef], - ) -> Result>, Self::Error> { - wallet::sapling::select_spendable_sapling_notes( - self, + account: Self::AccountId, + request: UnifiedAddressRequest, + ) -> Result, Self::Error> { + wallet::get_last_generated_address_matching( + self.conn.borrow(), + &self.params, account, - target_value, - anchor_height, - exclude, + request, ) + .map(|res| res.map(|(addr, _)| addr)) } - fn get_transparent_receivers( - &self, - _account: AccountId, - ) -> Result, Self::Error> { - #[cfg(feature = "transparent-inputs")] - return wallet::get_transparent_receivers(&self.params, &self.conn, _account); + fn get_account_birthday(&self, account: Self::AccountId) -> Result { + wallet::account_birthday(self.conn.borrow(), account) + } - #[cfg(not(feature = "transparent-inputs"))] - panic!( - "The wallet must be compiled with the transparent-inputs feature to use this method." - ); + fn get_wallet_birthday(&self) -> Result, Self::Error> { + wallet::wallet_birthday(self.conn.borrow()).map_err(SqliteClientError::from) } - fn get_unspent_transparent_outputs( + fn get_wallet_summary( &self, - _address: &TransparentAddress, - _max_height: BlockHeight, - _exclude: &[OutPoint], - ) -> Result, Self::Error> { - #[cfg(feature = "transparent-inputs")] - return wallet::get_unspent_transparent_outputs(self, _address, _max_height, _exclude); + 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, + ) + } - #[cfg(not(feature = "transparent-inputs"))] - panic!( - "The wallet must be compiled with the transparent-inputs feature to use this method." - ); + fn chain_height(&self) -> Result, Self::Error> { + wallet::chain_tip_height(self.conn.borrow()).map_err(SqliteClientError::from) } - fn get_transparent_balances( - &self, - _account: AccountId, - _max_height: BlockHeight, - ) -> Result, Self::Error> { - #[cfg(feature = "transparent-inputs")] - return wallet::get_transparent_balances(self, _account, _max_height); + fn get_block_hash(&self, block_height: BlockHeight) -> Result, Self::Error> { + wallet::get_block_hash(self.conn.borrow(), block_height).map_err(SqliteClientError::from) + } - #[cfg(not(feature = "transparent-inputs"))] - panic!( - "The wallet must be compiled with the transparent-inputs feature to use this method." - ); + fn block_metadata(&self, height: BlockHeight) -> Result, Self::Error> { + wallet::block_metadata(self.conn.borrow(), &self.params, height) } -} -impl<'a, P: consensus::Parameters> WalletRead for DataConnStmtCache<'a, P> { - type Error = SqliteClientError; - type NoteRef = NoteId; - type TxRef = i64; + fn block_fully_scanned(&self) -> Result, Self::Error> { + wallet::block_fully_scanned(self.conn.borrow(), &self.params) + } - fn block_height_extrema(&self) -> Result, Self::Error> { - self.wallet_db.block_height_extrema() + fn get_max_height_hash(&self) -> Result, Self::Error> { + wallet::get_max_height_hash(self.conn.borrow()).map_err(SqliteClientError::from) } - fn get_min_unspent_height(&self) -> Result, Self::Error> { - self.wallet_db.get_min_unspent_height() + 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_unified_full_viewing_keys( &self, - ) -> Result, Self::Error> { - self.wallet_db.get_unified_full_viewing_keys() + ) -> Result, Self::Error> { + wallet::get_unified_full_viewing_keys(self.conn.borrow(), &self.params) } - fn get_account_for_ufvk( + 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 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_sapling_nullifiers( &self, - ufvk: &UnifiedFullViewingKey, - ) -> Result, Self::Error> { - self.wallet_db.get_account_for_ufvk(ufvk) + query: NullifierQuery, + ) -> Result, Self::Error> { + wallet::sapling::get_sapling_nullifiers(self.conn.borrow(), query) } - fn get_current_address( + #[cfg(feature = "orchard")] + fn get_orchard_nullifiers( &self, - account: AccountId, - ) -> Result, Self::Error> { - self.wallet_db.get_current_address(account) + query: NullifierQuery, + ) -> Result, Self::Error> { + wallet::orchard::get_orchard_nullifiers(self.conn.borrow(), query) } - fn is_valid_account_extfvk( + #[cfg(feature = "transparent-inputs")] + fn get_transparent_receivers( &self, - account: AccountId, - extfvk: &ExtendedFullViewingKey, - ) -> Result { - self.wallet_db.is_valid_account_extfvk(account, extfvk) + account: Self::AccountId, + include_change: bool, + ) -> Result>, Self::Error> { + let key_scopes: &[KeyScope] = if include_change { + &[KeyScope::EXTERNAL, KeyScope::INTERNAL] + } else { + &[KeyScope::EXTERNAL] + }; + + wallet::transparent::get_transparent_receivers( + self.conn.borrow(), + &self.params, + account, + key_scopes, + ) } - fn get_balance_at( + #[cfg(feature = "transparent-inputs")] + fn get_transparent_balances( &self, - account: AccountId, - anchor_height: BlockHeight, - ) -> Result { - self.wallet_db.get_balance_at(account, anchor_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_transaction(&self, id_tx: i64) -> Result { - self.wallet_db.get_transaction(id_tx) + #[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_memo(&self, id_note: Self::NoteRef) -> Result, Self::Error> { - self.wallet_db.get_memo(id_note) + #[cfg(feature = "transparent-inputs")] + fn utxo_query_height(&self, account: Self::AccountId) -> Result { + let account_ref = wallet::get_account_ref(self.conn.borrow(), account)?; + wallet::transparent::utxo_query_height(self.conn.borrow(), account_ref, &self.gap_limits) } - fn get_commitment_tree( + #[cfg(feature = "transparent-inputs")] + fn get_known_ephemeral_addresses( &self, - block_height: BlockHeight, - ) -> Result, Self::Error> { - self.wallet_db.get_commitment_tree(block_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, + ) } - #[allow(clippy::type_complexity)] - fn get_witnesses( + #[cfg(feature = "transparent-inputs")] + fn find_account_for_ephemeral_address( &self, - block_height: BlockHeight, - ) -> Result, Self::Error> { - self.wallet_db.get_witnesses(block_height) + address: &TransparentAddress, + ) -> Result, Self::Error> { + wallet::transparent::ephemeral::find_account_for_ephemeral_address_str( + self.conn.borrow(), + &address.encode(&self.params), + ) } - fn get_sapling_nullifiers( - &self, - query: data_api::NullifierQuery, - ) -> Result, Self::Error> { - self.wallet_db.get_sapling_nullifiers(query) + 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()) } +} - fn get_spendable_sapling_notes( +#[cfg(any(test, feature = "test-dependencies"))] +impl, P: consensus::Parameters, CL, R> WalletTest + for WalletDb +{ + fn get_tx_history( &self, - account: AccountId, - anchor_height: BlockHeight, - exclude: &[Self::NoteRef], - ) -> Result>, Self::Error> { - self.wallet_db - .get_spendable_sapling_notes(account, anchor_height, exclude) + ) -> Result::AccountId>>, ::Error> + { + wallet::testing::get_tx_history(self.conn.borrow()) } - fn select_spendable_sapling_notes( + fn get_sent_note_ids( &self, - account: AccountId, - target_value: Amount, - anchor_height: BlockHeight, - exclude: &[Self::NoteRef], - ) -> Result>, Self::Error> { - self.wallet_db - .select_spendable_sapling_notes(account, target_value, anchor_height, exclude) + txid: &TxId, + protocol: ShieldedProtocol, + ) -> Result, ::Error> { + use crate::wallet::encoding::pool_code; + + 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, + a.cached_transparent_receiver_address, a.transparent_child_index + FROM sent_notes + JOIN transactions t ON t.id_tx = sent_notes.tx + LEFT JOIN transparent_received_outputs tro ON tro.transaction_id = t.id_tx + LEFT JOIN addresses a ON a.id = tro.address_id AND a.key_scope = :key_scope + WHERE t.txid = :txid + ORDER BY value", + )?; + + let sends = stmt_sent + .query_map( + named_params![ + ":txid": txid.as_ref(), + ":key_scope": KeyScope::Ephemeral.encode() + ], + |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_transparent_receivers( + fn get_checkpoint_history( &self, - account: AccountId, - ) -> Result, Self::Error> { - self.wallet_db.get_transparent_receivers(account) + protocol: &ShieldedProtocol, + ) -> Result< + Vec<(BlockHeight, Option)>, + ::Error, + > { + wallet::testing::get_checkpoint_history(self.conn.borrow(), protocol) } - fn get_unspent_transparent_outputs( + #[cfg(feature = "transparent-inputs")] + fn get_transparent_output( &self, - address: &TransparentAddress, - max_height: BlockHeight, - exclude: &[OutPoint], - ) -> Result, Self::Error> { - self.wallet_db - .get_unspent_transparent_outputs(address, max_height, exclude) + outpoint: &OutPoint, + allow_unspendable: bool, + ) -> Result, ::Error> { + wallet::transparent::get_wallet_transparent_output( + self.conn.borrow(), + outpoint, + allow_unspendable, + ) } - fn get_transparent_balances( + fn get_notes( &self, - account: AccountId, - max_height: BlockHeight, - ) -> Result, Self::Error> { - self.wallet_db.get_transparent_balances(account, max_height) + 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> DataConnStmtCache<'a, P> { - fn transactionally(&mut self, f: F) -> Result - where - F: FnOnce(&mut Self) -> Result, - { - self.wallet_db.conn.execute("BEGIN IMMEDIATE", [])?; - match f(self) { - Ok(result) => { - self.wallet_db.conn.execute("COMMIT", [])?; - Ok(result) +impl, P: consensus::Parameters, CL: Clock, R> 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, + #[cfg(feature = "transparent-inputs")] + &wdb.gap_limits, + )?; + + Ok((account.id(), usk)) + }) + } + + 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> { + 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, + #[cfg(feature = "transparent-inputs")] + &wdb.gap_limits, + )?; + + 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, + #[cfg(feature = "transparent-inputs")] + &wdb.gap_limits, + ) + }) + } + + fn get_next_available_address( + &mut self, + account_uuid: Self::AccountId, + request: UnifiedAddressRequest, + ) -> Result, Self::Error> { + self.transactionally(|wdb| { + wallet::get_next_available_address( + wdb.conn.0, + &wdb.params, + &wdb.clock, + account_uuid, + request, + #[cfg(feature = "transparent-inputs")] + &wdb.gap_limits, + ) + }) + } + + fn get_address_for_index( + &mut self, + account: Self::AccountId, + diversifier_index: DiversifierIndex, + request: UnifiedAddressRequest, + ) -> Result, Self::Error> { + if let Some(account) = self.get_account(account)? { + use zcash_keys::keys::AddressGenerationError::*; + + match account.uivk().address(diversifier_index, request) { + Ok(address) => { + let chain_tip_height = wallet::chain_tip_height(self.conn.borrow())?; + upsert_address( + self.conn.borrow(), + &self.params, + account.internal_id(), + diversifier_index, + &address, + Some(chain_tip_height.unwrap_or(account.birthday())), + true, + )?; + + Ok(Some(address)) + } + #[cfg(feature = "transparent-inputs")] + Err(InvalidTransparentChildIndex(_)) => Ok(None), + Err(InvalidSaplingDiversifierIndex(_)) => Ok(None), + Err(e) => Err(SqliteClientError::AddressGeneration(e)), } - Err(error) => { - match self.wallet_db.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 + } else { + Err(SqliteClientError::AccountUnknown) + } + } + + 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(()); + } + + 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(), + ), + }; + + let mut sapling_commitments = vec![]; + #[cfg(feature = "orchard")] + let mut orchard_commitments = vec![]; + let mut last_scanned_height = None; + let mut note_positions = vec![]; + + #[cfg(feature = "transparent-inputs")] + let mut tx_refs = BTreeSet::new(); + + 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. + 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_ref = wallet::put_tx_meta(wdb.conn.0, tx, block.height())?; + + #[cfg(feature = "transparent-inputs")] + tx_refs.insert(tx_ref); + + 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_ref, spend.nf())?; + } + #[cfg(feature = "orchard")] + for spend in tx.orchard_spends() { + wallet::orchard::mark_orchard_note_spent(wdb.conn.0, tx_ref, 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, + &wdb.params, + output, + tx_ref, + Some(block.height()), + 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, + &wdb.params, + output, + tx_ref, + Some(block.height()), + 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)); + } + + #[cfg(feature = "transparent-inputs")] + for (account_id, key_scope) in wallet::involved_accounts(wdb.conn.0, tx_refs)? { + use ReceiverRequirement::*; + wallet::transparent::generate_gap_addresses( + wdb.conn.0, + &wdb.params, + account_id, + key_scope, + &wdb.gap_limits, + UnifiedAddressRequest::unsafe_custom(Allow, Allow, Require), + false, + )?; + } + + // 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), + )?; + } + + // 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::>() + } + + #[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(()) + })?; + } + + // 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(()) + }) + } + + fn put_received_transparent_utxo( + &mut self, + _output: &WalletTransparentOutput, + ) -> Result { + #[cfg(feature = "transparent-inputs")] + return self.transactionally(|wdb| { + let (account_id, key_scope, utxo_id) = + wallet::transparent::put_received_transparent_utxo( + wdb.conn.0, + &wdb.params, + _output, + )?; + + use ReceiverRequirement::*; + wallet::transparent::generate_gap_addresses( + wdb.conn.0, + &wdb.params, + account_id, + key_scope, + &wdb.gap_limits, + UnifiedAddressRequest::unsafe_custom(Allow, Allow, Require), + true, + )?; + + Ok(utxo_id) + }); + + #[cfg(not(feature = "transparent-inputs"))] + panic!( + "The wallet must be compiled with the transparent-inputs feature to use this method." + ); } -} -impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> { - type UtxoRef = UtxoId; + 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, + #[cfg(feature = "transparent-inputs")] + &wdb.gap_limits, + ) + }) + } - fn create_account( + fn store_transactions_to_be_sent( &mut self, - seed: &SecretVec, - ) -> Result<(AccountId, UnifiedSpendingKey), Self::Error> { - self.transactionally(|stmts| { - let account = wallet::get_max_account_id(stmts.wallet_db)? - .map(|a| AccountId::from(u32::from(a) + 1)) - .unwrap_or_else(|| AccountId::from(0)); - - if u32::from(account) >= 0x7FFFFFFF { - return Err(SqliteClientError::AccountIdOutOfRange); + transactions: &[SentTransaction], + ) -> Result<(), Self::Error> { + self.transactionally(|wdb| { + for sent_tx in transactions { + wallet::store_transaction_to_be_sent(wdb.conn.0, &wdb.params, sent_tx)?; } + Ok(()) + }) + } - let usk = UnifiedSpendingKey::from_seed( - &stmts.wallet_db.params, - seed.expose_secret(), - account, + fn truncate_to_height(&mut self, max_height: BlockHeight) -> Result { + self.transactionally(|wdb| { + wallet::truncate_to_height( + wdb.conn.0, + &wdb.params, + #[cfg(feature = "transparent-inputs")] + &wdb.gap_limits, + max_height, ) - .map_err(|_| SqliteClientError::KeyDerivationError(account))?; - let ufvk = usk.to_unified_full_viewing_key(); - - wallet::add_account(stmts.wallet_db, account, &ufvk)?; - - Ok((account, usk)) }) } - fn get_next_available_address( + #[cfg(feature = "transparent-inputs")] + fn reserve_next_n_ephemeral_addresses( &mut self, - account: AccountId, - ) -> Result, Self::Error> { - match self.get_unified_full_viewing_keys()?.get(&account) { - Some(ufvk) => { - let search_from = match wallet::get_current_address(self.wallet_db, account)? { - Some((_, mut last_diversifier_index)) => { - last_diversifier_index - .increment() - .map_err(|_| SqliteClientError::DiversifierIndexOutOfRange)?; - last_diversifier_index - } - None => DiversifierIndex::default(), - }; - - let (addr, diversifier_index) = ufvk - .find_address(search_from) - .ok_or(SqliteClientError::DiversifierIndexOutOfRange)?; - - self.stmt_insert_address(account, diversifier_index, &addr)?; + account_id: Self::AccountId, + n: usize, + ) -> Result, Self::Error> { + self.transactionally(|wdb| { + let account_id = wallet::get_account_ref(wdb.conn.0, account_id)?; + let reserved = wallet::transparent::reserve_next_n_addresses( + wdb.conn.0, + &wdb.params, + account_id, + KeyScope::Ephemeral, + wdb.gap_limits.ephemeral(), + n, + )?; - Ok(Some(addr)) - } - None => Ok(None), - } + Ok(reserved.into_iter().map(|(_, a, m)| (a, m)).collect()) + }) } - #[tracing::instrument(skip_all, fields(height = u32::from(block.block_height)))] - #[allow(clippy::type_complexity)] - fn advance_by_block( + fn set_transaction_status( &mut self, - block: &PrunedBlock, - updated_witnesses: &[(Self::NoteRef, sapling::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, - )?; - - let mut new_witnesses = vec![]; - for tx in block.transactions { - let tx_row = wallet::put_tx_meta(up, tx, block.block_height)?; + txid: TxId, + status: data_api::TransactionStatus, + ) -> Result<(), Self::Error> { + self.transactionally(|wdb| wallet::set_transaction_status(wdb.conn.0, txid, status)) + } +} - // Mark notes as spent and remove them from the scanning cache - for spend in &tx.sapling_spends { - wallet::sapling::mark_sapling_note_spent(up, tx_row, spend.nf())?; - } +pub(crate) type SaplingShardStore = SqliteShardStore; +pub(crate) type SaplingCommitmentTree = + ShardTree, { sapling::NOTE_COMMITMENT_TREE_DEPTH }, SAPLING_SHARD_HEIGHT>; + +pub(crate) fn sapling_tree( + conn: C, +) -> Result, ShardTreeError> +where + SaplingShardStore: ShardStore, +{ + Ok(ShardTree::new( + SqliteShardStore::from_connection(conn, SAPLING_TABLES_PREFIX) + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?, + PRUNING_DEPTH.try_into().unwrap(), + )) +} - for output in &tx.sapling_outputs { - let received_note_id = wallet::sapling::put_received_note(up, output, tx_row)?; +#[cfg(feature = "orchard")] +pub(crate) type OrchardShardStore = + SqliteShardStore; + +#[cfg(feature = "orchard")] +pub(crate) type OrchardCommitmentTree = ShardTree< + OrchardShardStore, + { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }, + ORCHARD_SHARD_HEIGHT, +>; + +#[cfg(feature = "orchard")] +pub(crate) fn orchard_tree( + conn: C, +) -> Result, ShardTreeError> +where + OrchardShardStore: + ShardStore, +{ + Ok(ShardTree::new( + SqliteShardStore::from_connection(conn, ORCHARD_TABLES_PREFIX) + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?, + PRUNING_DEPTH.try_into().unwrap(), + )) +} - // Save witness for note. - new_witnesses.push((received_note_id, output.witness().clone())); - } - } +impl, P: consensus::Parameters, CL, R> WalletCommitmentTrees + for WalletDb +{ + type Error = commitment_tree::Error; + type SaplingShardStore<'a> = SaplingShardStore<&'a rusqlite::Transaction<'a>>; - // 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::sapling::insert_witness(up, rnid, witness, block.block_height)?; - } else { - return Err(SqliteClientError::InvalidNoteId); - } - } + fn with_sapling_tree_mut(&mut self, mut callback: F) -> Result + where + for<'a> F: + FnMut(&'a mut SaplingCommitmentTree<&'a rusqlite::Transaction<'a>>) -> Result, + E: From>, + { + let tx = self + .conn + .borrow_mut() + .transaction() + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?; + let result = { + let mut shardtree = sapling_tree(&tx)?; + callback(&mut shardtree)? + }; - // Prune the stored witnesses (we only expect rollbacks of at most PRUNING_HEIGHT blocks). - wallet::prune_witnesses(up, block.block_height - PRUNING_HEIGHT)?; + tx.commit() + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?; + Ok(result) + } - // Update now-expired transactions that didn't get mined. - wallet::update_expired_notes(up, block.block_height)?; + 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 OrchardCommitmentTree<&'a rusqlite::Transaction<'a>>) -> Result, + E: From>, + { + let tx = self + .conn + .borrow_mut() + .transaction() + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?; + let result = { + let mut shardtree = orchard_tree(&tx)?; + callback(&mut shardtree)? + }; - Ok(new_witnesses) - }) + tx.commit() + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?; + Ok(result) } - fn store_decrypted_tx( + #[cfg(feature = "orchard")] + fn put_orchard_subtree_roots( &mut self, - d_tx: DecryptedTransaction, - ) -> Result { - self.transactionally(|up| { - let tx_ref = wallet::put_tx_data(up, d_tx.tx, None, None)?; - - let mut spending_account_id: Option = None; - for output in d_tx.sapling_outputs { - match output.transfer_type { - TransferType::Outgoing | TransferType::WalletInternal => { - let recipient = if output.transfer_type == TransferType::Outgoing { - Recipient::Sapling(output.note.recipient()) - } else { - Recipient::InternalAccount(output.account, PoolType::Sapling) - }; - - wallet::put_sent_output( - up, - output.account, - tx_ref, - output.index, - &recipient, - Amount::from_u64(output.note.value().inner()).map_err(|_| - SqliteClientError::CorruptedData("Note value is not a valid Zcash amount.".to_string()))?, - Some(&output.memo), - )?; - - if matches!(recipient, Recipient::InternalAccount(_, _)) { - wallet::sapling::put_received_note(up, output, tx_ref)?; - } - } - TransferType::Incoming => { - match spending_account_id { - Some(id) => - if id != output.account { - panic!("Unable to determine a unique account identifier for z->t spend."); - } - None => { - spending_account_id = Some(output.account); - } - } + 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(()) + } +} - wallet::sapling::put_received_note(up, output, tx_ref)?; - } - } - } +impl WalletCommitmentTrees + for WalletDb, P, CL, R> +{ + type Error = commitment_tree::Error; + type SaplingShardStore<'a> = crate::SaplingShardStore<&'a rusqlite::Transaction<'a>>; - // 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()) { - wallet::mark_transparent_utxo_spent(up, tx_ref, &txin.prevout)?; - } + fn with_sapling_tree_mut(&mut self, mut callback: F) -> Result + where + for<'a> F: + FnMut(&'a mut SaplingCommitmentTree<&'a rusqlite::Transaction<'a>>) -> Result, + E: From>, + { + let mut shardtree = sapling_tree(self.conn.0)?; + let result = callback(&mut shardtree)?; - // If we have some transparent outputs: - if !d_tx.tx.transparent_bundle().iter().any(|b| b.vout.is_empty()) { - let nullifiers = self.wallet_db.get_sapling_nullifiers(data_api::NullifierQuery::All)?; - // If the transaction contains shielded spends from our wallet, we will store z->t - // transactions we observe in the same way they would be stored by - // create_spend_to_address. - if let Some((account_id, _)) = nullifiers.iter().find( - |(_, nf)| - d_tx.tx.sapling_bundle().iter().flat_map(|b| b.shielded_spends().iter()) - .any(|input| nf == input.nullifier()) - ) { - 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() { - wallet::put_sent_output( - up, - *account_id, - tx_ref, - output_index, - &Recipient::Transparent(address), - txout.value, - None - )?; - } - } - } - } - Ok(tx_ref) - }) + Ok(result) } - 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.fee_amount), - 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. - if let Some(bundle) = sent_tx.tx.sapling_bundle() { - for spend in bundle.shielded_spends() { - wallet::sapling::mark_sapling_note_spent(up, tx_ref, spend.nullifier())?; - } - } - - #[cfg(feature = "transparent-inputs")] - for utxo_outpoint in &sent_tx.utxos_spent { - wallet::mark_transparent_utxo_spent(up, tx_ref, utxo_outpoint)?; - } + 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, + ) + } - for output in &sent_tx.outputs { - wallet::insert_sent_output(up, tx_ref, sent_tx.account, output)?; - - if let Some((account, note)) = output.sapling_change_to() { - wallet::sapling::put_received_note( - up, - &DecryptedOutput { - index: output.output_index(), - note: note.clone(), - account: *account, - memo: output - .memo() - .map_or_else(MemoBytes::empty, |memo| memo.clone()), - transfer_type: TransferType::WalletInternal, - }, - tx_ref, - )?; - } - } + #[cfg(feature = "orchard")] + type OrchardShardStore<'a> = crate::OrchardShardStore<&'a rusqlite::Transaction<'a>>; - // Return the row number of the transaction, so the caller can fetch it for sending. - Ok(tx_ref) - }) - } + #[cfg(feature = "orchard")] + fn with_orchard_tree_mut(&mut self, mut callback: F) -> Result + where + for<'a> F: + FnMut(&'a mut OrchardCommitmentTree<&'a rusqlite::Transaction<'a>>) -> Result, + E: From>, + { + let mut shardtree = orchard_tree(self.conn.0)?; + let result = callback(&mut shardtree)?; - fn truncate_to_height(&mut self, block_height: BlockHeight) -> Result<(), Self::Error> { - wallet::truncate_to_height(self.wallet_db, block_height) + Ok(result) } - fn put_received_transparent_utxo( + #[cfg(feature = "orchard")] + fn put_orchard_subtree_roots( &mut self, - _output: &WalletTransparentOutput, - ) -> Result { - #[cfg(feature = "transparent-inputs")] - return wallet::put_received_transparent_utxo(self, _output); - - #[cfg(not(feature = "transparent-inputs"))] - panic!( - "The wallet must be compiled with the transparent-inputs feature to use this method." - ); + 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, + ) } } @@ -734,17 +2072,14 @@ impl BlockDb { impl BlockSource for BlockDb { type Error = SqliteClientError; - fn with_blocks( + fn with_blocks( &self, from_height: Option, - limit: Option, + limit: Option, with_row: F, - ) -> Result<(), data_api::chain::error::Error> + ) -> Result<(), data_api::chain::error::Error> where - F: FnMut( - CompactBlock, - ) - -> Result<(), data_api::chain::error::Error>, + F: FnMut(CompactBlock) -> Result<(), data_api::chain::error::Error>, { chain::blockdb_with_blocks(self, from_height, limit, with_row) } @@ -765,7 +2100,7 @@ impl BlockSource for BlockDb { /// /// 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 height at which cached data is available +/// * 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- / @@ -806,6 +2141,7 @@ pub enum FsBlockDbError { InvalidBlockstoreRoot(PathBuf), InvalidBlockPath(PathBuf), CorruptedData(String), + CacheMiss(BlockHeight), } #[cfg(feature = "unstable")] @@ -839,7 +2175,7 @@ impl FsBlockDb { /// files as described for [`FsBlockDb`]. /// /// An application using this constructor should ensure that they call - /// [`zcash_client_sqlite::chain::init::init_blockmetadb`] at application startup to ensure + /// [`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 { @@ -864,7 +2200,8 @@ impl FsBlockDb { Ok(chain::blockmetadb_get_max_cached_height(&self.conn)?) } - /// Adds a set of block metadata entries to the metadata database. + /// 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. @@ -915,17 +2252,14 @@ impl FsBlockDb { impl BlockSource for FsBlockDb { type Error = FsBlockDbError; - fn with_blocks( + fn with_blocks( &self, from_height: Option, - limit: Option, + limit: Option, with_row: F, - ) -> Result<(), data_api::chain::error::Error> + ) -> Result<(), data_api::chain::error::Error> where - F: FnMut( - CompactBlock, - ) - -> Result<(), data_api::chain::error::Error>, + F: FnMut(CompactBlock) -> Result<(), data_api::chain::error::Error>, { fsblockdb_with_blocks(self, from_height, limit, with_row) } @@ -972,6 +2306,13 @@ impl std::fmt::Display for FsBlockDbError { e, ) } + FsBlockDbError::CacheMiss(height) => { + write!( + f, + "Requested height {} does not exist in the block cache", + height + ) + } } } } @@ -982,427 +2323,521 @@ extern crate assert_matches; #[cfg(test)] mod tests { - use prost::Message; - use rand_core::{OsRng, RngCore}; - use rusqlite::params; - use std::collections::HashMap; - - #[cfg(feature = "unstable")] - use std::{fs::File, path::Path}; - - #[cfg(feature = "transparent-inputs")] - use zcash_primitives::{legacy, legacy::keys::IncomingViewingKey}; - - use zcash_note_encryption::Domain; - use zcash_primitives::{ - block::BlockHash, - consensus::{BlockHeight, Network, NetworkUpgrade, Parameters}, - legacy::TransparentAddress, - memo::MemoBytes, - sapling::{ - note_encryption::{sapling_note_encryption, SaplingDomain}, - util::generate_random_rseed, - value::NoteValue, - Note, Nullifier, PaymentAddress, - }, - transaction::components::Amount, - zip32::{sapling::DiversifiableFullViewingKey, DiversifierIndex}, - }; - - use zcash_client_backend::{ - data_api::{WalletRead, WalletWrite}, - keys::{sapling, UnifiedFullViewingKey}, - proto::compact_formats::{ - CompactBlock, CompactSaplingOutput, CompactSaplingSpend, CompactTx, - }, + use std::time::{Duration, SystemTime}; + + 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::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey}; + use zcash_primitives::block::BlockHash; + use zcash_protocol::consensus; use crate::{ - wallet::init::{init_accounts_table, init_wallet_db}, - AccountId, WalletDb, + error::SqliteClientError, testing::db::TestDbFactory, util::Clock as _, + wallet::MIN_SHIELDED_DIVERSIFIER_OFFSET, AccountUuid, }; - use super::BlockDb; - #[cfg(feature = "unstable")] - use super::{ - chain::{init::init_blockmeta_db, BlockMeta}, - FsBlockDb, - }; + use zcash_keys::keys::sapling; - #[cfg(feature = "mainnet")] - pub(crate) fn network() -> Network { - Network::MainNetwork - } + #[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() + }); + + // 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(not(feature = "mainnet"))] - pub(crate) fn network() -> Network { - Network::TestNetwork + // check that passing an invalid seed results in a failure + assert!({ + !st.wallet() + .validate_seed(account.id(), &SecretVec::new(vec![1u8; 32])) + .unwrap() + }); } - #[cfg(feature = "mainnet")] - pub(crate) fn sapling_activation_height() -> BlockHeight { - Network::MainNetwork - .activation_height(NetworkUpgrade::Sapling) + #[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(); + + // We have to have the chain tip height in order to allocate new addresses, to record the + // exposed-at height. + st.wallet_mut() + .update_chain_tip(account.birthday().height()) + .unwrap(); + + let current_addr = st + .wallet() + .get_last_generated_address_matching( + account.id(), + UnifiedAddressRequest::AllAvailableKeys, + ) + .unwrap(); + assert!(current_addr.is_some()); + + let addr2 = st + .wallet_mut() + .get_next_available_address(account.id(), UnifiedAddressRequest::AllAvailableKeys) .unwrap() - } + .map(|(a, _)| a); + assert!(addr2.is_some()); + assert_ne!(current_addr, addr2); + + let addr2_cur = st + .wallet() + .get_last_generated_address_matching( + account.id(), + UnifiedAddressRequest::AllAvailableKeys, + ) + .unwrap(); + assert_eq!(addr2, addr2_cur); - #[cfg(not(feature = "mainnet"))] - pub(crate) fn sapling_activation_height() -> BlockHeight { - Network::TestNetwork - .activation_height(NetworkUpgrade::Sapling) + // Perform similar tests for shielded-only addresses. These should be timestamp-based; we + // will tick the clock between each generation. + use zcash_keys::keys::ReceiverRequirement::*; + #[cfg(feature = "orchard")] + let shielded_only_request = UnifiedAddressRequest::unsafe_custom(Require, Require, Omit); + #[cfg(not(feature = "orchard"))] + let shielded_only_request = UnifiedAddressRequest::unsafe_custom(Omit, Require, Omit); + + let cur_shielded_only = st + .wallet() + .get_last_generated_address_matching(account.id(), shielded_only_request) + .unwrap(); + // If transparent support is disabled, then the previous "transparent-including" + // addresses were actually shielded-only, so we do have a current address. + #[cfg(not(feature = "transparent-inputs"))] + assert_eq!(cur_shielded_only, addr2); + // If transparent support is enabled, this works as expected. + #[cfg(feature = "transparent-inputs")] + assert!(cur_shielded_only.is_none()); + + let di_lower = st + .wallet() + .db() + .clock + .now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("current time is valid") + .as_secs() + .saturating_add(MIN_SHIELDED_DIVERSIFIER_OFFSET); + + let (shielded_only, di) = st + .wallet_mut() + .get_next_available_address(account.id(), shielded_only_request) .unwrap() - } + .expect("generated a shielded-only address"); - #[cfg(test)] - pub(crate) fn init_test_accounts_table( - db_data: &WalletDb, - ) -> (DiversifiableFullViewingKey, Option) { - let (ufvk, taddr) = init_test_accounts_table_ufvk(db_data); - (ufvk.sapling().unwrap().clone(), taddr) - } + // since not every Sapling diversifier index is valid, the resulting index will be bounded + // by the current time, but may not be equal to it + assert!(u128::from(di) >= u128::from(di_lower)); - #[cfg(test)] - pub(crate) fn init_test_accounts_table_ufvk( - db_data: &WalletDb, - ) -> (UnifiedFullViewingKey, Option) { - let seed = [0u8; 32]; - let account = AccountId::from(0); - let extsk = sapling::spending_key(&seed, network().coin_type(), account); - let dfvk = extsk.to_diversifiable_full_viewing_key(); + let cur_shielded_only = st + .wallet() + .get_last_generated_address_matching(account.id(), shielded_only_request) + .unwrap() + .expect("retrieved the last-generated shielded-only address"); + assert_eq!(cur_shielded_only, shielded_only); - #[cfg(feature = "transparent-inputs")] - let (tkey, taddr) = { - let tkey = legacy::keys::AccountPrivKey::from_seed(&network(), &seed, account) - .unwrap() - .to_account_pubkey(); - let taddr = tkey.derive_external_ivk().unwrap().default_address().0; - (Some(tkey), Some(taddr)) - }; + // This gives around a 2^{-32} probability of `di` and `di_2` colliding, which is + // low enough for unit tests. + let collision_offset = 32; - #[cfg(not(feature = "transparent-inputs"))] - let taddr = None; + st.wallet_mut() + .db_mut() + .clock + .tick(Duration::from_secs(collision_offset)); - let ufvk = UnifiedFullViewingKey::new( - #[cfg(feature = "transparent-inputs")] - tkey, - Some(dfvk), - None, - ) - .unwrap(); - - let ufvks = HashMap::from([(account, ufvk.clone())]); - init_accounts_table(db_data, &ufvks).unwrap(); - - (ufvk, taddr) - } - - #[allow(dead_code)] - pub(crate) enum AddressType { - DefaultExternal, - DiversifiedExternal(DiversifierIndex), - Internal, - } - - /// Create a fake CompactBlock at the given height, containing a single output paying - /// an address. Returns the CompactBlock and the nullifier for the new note. - pub(crate) fn fake_compact_block( - height: BlockHeight, - prev_hash: BlockHash, - dfvk: &DiversifiableFullViewingKey, - req: AddressType, - value: Amount, - ) -> (CompactBlock, Nullifier) { - let to = match req { - AddressType::DefaultExternal => dfvk.default_address().1, - AddressType::DiversifiedExternal(idx) => dfvk.find_address(idx).unwrap().1, - AddressType::Internal => dfvk.change_address().1, - }; + let (shielded_only_2, di_2) = st + .wallet_mut() + .get_next_available_address(account.id(), shielded_only_request) + .unwrap() + .expect("generated a shielded-only address"); + assert_ne!(shielded_only_2, shielded_only); + assert!(u128::from(di_2) >= u128::from(di_lower) + u128::from(collision_offset)); + } - // Create a fake Note for the account - let mut rng = OsRng; - let rseed = generate_random_rseed(&network(), height, &mut rng); - let note = Note::from_parts(to, NoteValue::from_raw(value.into()), rseed); - let encryptor = sapling_note_encryption::<_, Network>( - Some(dfvk.fvk().ovk), - note.clone(), - MemoBytes::empty(), - &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 cout = CompactSaplingOutput { - cmu, - ephemeral_key, - ciphertext: enc_ciphertext.as_ref()[..52].to_vec(), - }; - let mut ctx = CompactTx::default(); - let mut txid = vec![0; 32]; - rng.fill_bytes(&mut txid); - ctx.hash = txid; - ctx.outputs.push(cout); - 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, note.nf(&dfvk.fvk().vk.nk, 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), - dfvk: &DiversifiableFullViewingKey, - 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 cspend = CompactSaplingSpend { nf: nf.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); - - // Create a fake Note for the payment - ctx.outputs.push({ - let note = Note::from_parts(to, NoteValue::from_raw(value.into()), rseed); - let encryptor = sapling_note_encryption::<_, Network>( - Some(dfvk.fvk().ovk), - note.clone(), - MemoBytes::empty(), - &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(); + #[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); + } - CompactSaplingOutput { - cmu, - ephemeral_key, - ciphertext: enc_ciphertext.as_ref()[..52].to_vec(), - } - }); + #[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 change - ctx.outputs.push({ - let change_addr = dfvk.default_address().1; - let rseed = generate_random_rseed(&network(), height, &mut rng); - let note = Note::from_parts( - change_addr, - NoteValue::from_raw((in_value - value).unwrap().into()), - rseed, - ); - let encryptor = sapling_note_encryption::<_, Network>( - Some(dfvk.fvk().ovk), - note.clone(), - MemoBytes::empty(), - &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(); + let birthday = AccountBirthday::from_parts( + ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])), + None, + ); - CompactSaplingOutput { - cmu, - ephemeral_key, - ciphertext: enc_ciphertext.as_ref()[..52].to_vec(), - } - }); + let seed = Secret::new(vec![0u8; 32]); + let zip32_index_1 = zip32::AccountId::ZERO.next().unwrap(); - 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 + 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); } - /// Insert a fake CompactBlock into the cache DB. - pub(crate) fn insert_into_cache(db_cache: &BlockDb, cb: &CompactBlock) { - let cb_bytes = cb.encode_to_vec(); - db_cache - .0 - .prepare("INSERT INTO compactblocks (height, data) VALUES (?, ?)") - .unwrap() - .execute(params![u32::from(cb.height()), cb_bytes,]) + 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) + ); + } + + // 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) + ); + } } - #[cfg(feature = "unstable")] - pub(crate) fn store_in_fsblockdb>( - fsblockdb_root: P, - cb: &CompactBlock, - ) -> BlockMeta { - 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(), - }; + #[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 blocks_dir = fsblockdb_root.as_ref().join("blocks"); - let block_path = meta.block_file_path(&blocks_dir); + let seed = Secret::new(vec![0u8; 32]); + let zip32_index_1 = zip32::AccountId::ZERO.next().unwrap(); - File::create(block_path) - .unwrap() - .write_all(&cb.encode_to_vec()) + let (first_account, _) = st + .wallet_mut() + .import_account_hd("", &seed, zip32_index_1, &birthday, None) .unwrap(); + let ufvk = first_account.ufvk().unwrap(); - meta + 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 get_next_available_address() { - use tempfile::NamedTempFile; + pub(crate) fn import_account_ufvk_then_conflicts() { + let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory::default()) + .build(); - let data_file = NamedTempFile::new().unwrap(); - let mut db_data = WalletDb::for_path(data_file.path(), network()).unwrap(); + let birthday = AccountBirthday::from_parts( + ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])), + None, + ); - let account = AccountId::from(0); - init_wallet_db(&mut db_data, None).unwrap(); - let _ = init_test_accounts_table_ufvk(&db_data); + 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()) + ); - let current_addr = db_data.get_current_address(account).unwrap(); - assert!(current_addr.is_some()); + assert_matches!( + account.source(), + AccountSource::Imported { + purpose: AccountPurpose::Spending { .. }, + .. + } + ); - let mut update_ops = db_data.get_update_ops().unwrap(); - let addr2 = update_ops.get_next_available_address(account).unwrap(); - assert!(addr2.is_some()); - assert_ne!(current_addr, addr2); + assert_matches!( + st.wallet_mut().import_account_hd("", &seed, zip32_index_0, &birthday, None), + Err(SqliteClientError::AccountCollision(id)) if id == account.id()); - let addr2_cur = db_data.get_current_address(account).unwrap(); - assert_eq!(addr2, addr2_cur); + check_collisions( + &mut st, + &ufvk, + &birthday, + |e| matches!(e, SqliteClientError::AccountCollision(id) if *id == account.id()), + ); } - #[cfg(feature = "transparent-inputs")] #[test] - fn transparent_receivers() { - use secrecy::Secret; - use tempfile::NamedTempFile; - - use crate::{chain::init::init_cache_database, wallet::init::init_wallet_db}; + pub(crate) fn create_account_then_conflicts() { + let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory::default()) + .build(); - let cache_file = NamedTempFile::new().unwrap(); - let db_cache = BlockDb::for_path(cache_file.path()).unwrap(); - init_cache_database(&db_cache).unwrap(); + let birthday = AccountBirthday::from_parts( + ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])), + None, + ); - let data_file = NamedTempFile::new().unwrap(); - let mut db_data = WalletDb::for_path(data_file.path(), network()).unwrap(); - init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap(); + 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), + ); + } - // Add an account to the wallet. - let (ufvk, taddr) = init_test_accounts_table_ufvk(&db_data); - let taddr = taddr.unwrap(); + #[cfg(feature = "transparent-inputs")] + #[test] + fn transparent_receivers() { + use std::collections::BTreeSet; - let receivers = db_data.get_transparent_receivers(0.into()).unwrap(); + use crate::{ + testing::BlockCache, wallet::transparent::transaction_data_requests, GapLimits, + }; + use zcash_client_backend::data_api::TransactionDataRequest; + + 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 account = st.test_account().unwrap(); + let ufvk = account.usk().to_unified_full_viewing_key(); + let (taddr, _) = account.usk().default_transparent_address(); + let birthday = account.birthday().height(); + let account_id = account.id(); + + let receivers = st + .wallet() + .get_transparent_receivers(account.id(), false) + .unwrap(); // The receiver for the default UA should be in the set. - assert!(receivers.contains_key(ufvk.default_address().0.transparent().unwrap())); + assert!(receivers.contains_key( + ufvk.default_address(UnifiedAddressRequest::AllAvailableKeys) + .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)); + + // The chain tip height must be known in order to query for data requests. + st.wallet_mut().update_chain_tip(birthday).unwrap(); + + // Transaction data requests should include a request for each ephemeral address + let ephemeral_addrs = st + .wallet() + .get_known_ephemeral_addresses(account_id, None) + .unwrap(); + + assert_eq!( + ephemeral_addrs.len(), + GapLimits::default().ephemeral() as usize + ); + + st.wallet_mut() + .db_mut() + .schedule_ephemeral_address_checks() + .unwrap(); + let data_requests = + transaction_data_requests(st.wallet().conn(), &st.wallet().db().params).unwrap(); + + let base_time = st.wallet().db().clock.now(); + let day = Duration::from_secs(60 * 60 * 24); + let mut check_times = BTreeSet::new(); + for (addr, _) in ephemeral_addrs { + let has_valid_request = data_requests.iter().any(|req| match req { + TransactionDataRequest::TransactionsInvolvingAddress { + address, + request_at: Some(t), + .. + } => { + *address == addr && *t > base_time && { + let t_delta = t.duration_since(base_time).unwrap(); + // This is an imprecise check; the objective of the randomized time + // selection is that all ephemeral address checks be performed within a + // day, and that their check times be distinct. + let result = t_delta < day && !check_times.contains(t); + check_times.insert(*t); + result + } + } + _ => false, + }); + + assert!(has_valid_request); + } } #[cfg(feature = "unstable")] #[test] pub(crate) fn fsblockdb_api() { - // Initialise a BlockMeta DB in a new directory. - let fsblockdb_root = tempfile::tempdir().unwrap(); - let mut db_meta = FsBlockDb::for_path(&fsblockdb_root).unwrap(); - init_blockmeta_db(&mut db_meta).unwrap(); + 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!(db_meta.get_max_cached_height().unwrap(), None); + assert_eq!(st.cache().get_max_cached_height().unwrap(), None); // Generate some fake CompactBlocks. let seed = [0u8; 32]; - let account = AccountId::from(0); - let extsk = sapling::spending_key(&seed, network().coin_type(), account); + 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 (cb1, _) = fake_compact_block( - BlockHeight::from_u32(1), - BlockHash([1; 32]), + let (h1, meta1, _) = st.generate_next_block( &dfvk, AddressType::DefaultExternal, - Amount::from_u64(5).unwrap(), + Zatoshis::const_from_u64(5), ); - let (cb2, _) = fake_compact_block( - BlockHeight::from_u32(2), - BlockHash([2; 32]), + let (h2, meta2, _) = st.generate_next_block( &dfvk, AddressType::DefaultExternal, - Amount::from_u64(10).unwrap(), + Zatoshis::const_from_u64(10), ); - // Write the CompactBlocks to the BlockMeta DB's corresponding disk storage. - let meta1 = store_in_fsblockdb(&fsblockdb_root, &cb1); - let meta2 = store_in_fsblockdb(&fsblockdb_root, &cb2); - // The BlockMeta DB is not updated until we do so explicitly. - assert_eq!(db_meta.get_max_cached_height().unwrap(), None); + assert_eq!(st.cache().get_max_cached_height().unwrap(), None); // Inform the BlockMeta DB about the newly-persisted CompactBlocks. - db_meta.write_block_metadata(&[meta1, meta2]).unwrap(); + st.cache().write_block_metadata(&[meta1, meta2]).unwrap(); // The BlockMeta DB now sees blocks up to height 2. - assert_eq!( - db_meta.get_max_cached_height().unwrap(), - Some(BlockHeight::from_u32(2)), - ); - assert_eq!( - db_meta.find_block(BlockHeight::from_u32(1)).unwrap(), - Some(meta1), - ); - assert_eq!( - db_meta.find_block(BlockHeight::from_u32(2)).unwrap(), - Some(meta2), - ); - assert_eq!(db_meta.find_block(BlockHeight::from_u32(3)).unwrap(), None); + 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. - db_meta - .truncate_to_height(BlockHeight::from_u32(1)) - .unwrap(); - assert_eq!( - db_meta.get_max_cached_height().unwrap(), - Some(BlockHeight::from_u32(1)), - ); - assert_eq!( - db_meta.find_block(BlockHeight::from_u32(1)).unwrap(), - Some(meta1), - ); - assert_eq!(db_meta.find_block(BlockHeight::from_u32(2)).unwrap(), None); - assert_eq!(db_meta.find_block(BlockHeight::from_u32(3)).unwrap(), None); + 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/prepared.rs b/zcash_client_sqlite/src/prepared.rs deleted file mode 100644 index da97faa6cb..0000000000 --- a/zcash_client_sqlite/src/prepared.rs +++ /dev/null @@ -1,812 +0,0 @@ -//! Prepared SQL statements used by the wallet. -//! -//! Some `rusqlite` crate APIs are only available on prepared statements; these are stored -//! inside the [`DataConnStmtCache`]. When adding a new prepared statement: -//! -//! - Add it as a private field of `DataConnStmtCache`. -//! - Build the statement in [`DataConnStmtCache::new`]. -//! - Add a crate-private helper method to `DataConnStmtCache` for running the statement. - -use rusqlite::{named_params, params, Statement, ToSql}; -use zcash_primitives::{ - block::BlockHash, - consensus::{self, BlockHeight}, - memo::MemoBytes, - merkle_tree::{write_commitment_tree, write_incremental_witness}, - sapling::{self, Diversifier, Nullifier}, - transaction::{components::Amount, TxId}, - zip32::{AccountId, DiversifierIndex}, -}; - -use zcash_client_backend::{ - address::UnifiedAddress, - data_api::{PoolType, Recipient}, - encoding::AddressCodec, -}; - -use crate::{error::SqliteClientError, wallet::pool_code, NoteId, WalletDb}; - -#[cfg(feature = "transparent-inputs")] -use { - crate::UtxoId, rusqlite::OptionalExtension, - zcash_client_backend::wallet::WalletTransparentOutput, - zcash_primitives::transaction::components::transparent::OutPoint, -}; - -pub(crate) struct InsertAddress<'a> { - stmt: Statement<'a>, -} - -impl<'a> InsertAddress<'a> { - pub(crate) fn new(conn: &'a rusqlite::Connection) -> Result { - Ok(InsertAddress { - stmt: conn.prepare( - "INSERT INTO addresses ( - account, - diversifier_index_be, - address, - cached_transparent_receiver_address - ) - VALUES ( - :account, - :diversifier_index_be, - :address, - :cached_transparent_receiver_address - )", - )?, - }) - } - - /// Adds the given address and diversifier index to the addresses table. - /// - /// Returns the database row for the newly-inserted address. - pub(crate) fn execute( - &mut self, - params: &P, - account: AccountId, - mut diversifier_index: DiversifierIndex, - address: &UnifiedAddress, - ) -> Result<(), rusqlite::Error> { - // the diversifier index is stored in big-endian order to allow sorting - diversifier_index.0.reverse(); - self.stmt.execute(named_params![ - ":account": &u32::from(account), - ":diversifier_index_be": &&diversifier_index.0[..], - ":address": &address.encode(params), - ":cached_transparent_receiver_address": &address.transparent().map(|r| r.encode(params)), - ])?; - - Ok(()) - } -} - -/// 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> { - pub(crate) wallet_db: &'a WalletDb

, - stmt_insert_block: Statement<'a>, - - stmt_insert_tx_meta: Statement<'a>, - stmt_update_tx_meta: Statement<'a>, - - stmt_insert_tx_data: Statement<'a>, - stmt_update_tx_data: Statement<'a>, - stmt_select_tx_ref: Statement<'a>, - - stmt_mark_sapling_note_spent: Statement<'a>, - #[cfg(feature = "transparent-inputs")] - stmt_mark_transparent_utxo_spent: Statement<'a>, - - #[cfg(feature = "transparent-inputs")] - stmt_insert_received_transparent_utxo: Statement<'a>, - #[cfg(feature = "transparent-inputs")] - stmt_update_received_transparent_utxo: Statement<'a>, - #[cfg(feature = "transparent-inputs")] - stmt_insert_legacy_transparent_utxo: Statement<'a>, - #[cfg(feature = "transparent-inputs")] - stmt_update_legacy_transparent_utxo: Statement<'a>, - stmt_insert_received_note: Statement<'a>, - stmt_update_received_note: Statement<'a>, - stmt_select_received_note: Statement<'a>, - - stmt_insert_sent_output: Statement<'a>, - stmt_update_sent_output: Statement<'a>, - - stmt_insert_witness: Statement<'a>, - stmt_prune_witnesses: Statement<'a>, - stmt_update_expired: Statement<'a>, - - stmt_insert_address: InsertAddress<'a>, -} - -impl<'a, P> DataConnStmtCache<'a, P> { - pub(crate) fn new(wallet_db: &'a WalletDb

) -> Result { - Ok( - DataConnStmtCache { - wallet_db, - stmt_insert_block: wallet_db.conn.prepare( - "INSERT INTO blocks (height, hash, time, sapling_tree) - VALUES (?, ?, ?, ?)", - )?, - stmt_insert_tx_meta: wallet_db.conn.prepare( - "INSERT INTO transactions (txid, block, tx_index) - VALUES (?, ?, ?)", - )?, - stmt_update_tx_meta: wallet_db.conn.prepare( - "UPDATE transactions - SET block = ?, tx_index = ? WHERE txid = ?", - )?, - stmt_insert_tx_data: wallet_db.conn.prepare( - "INSERT INTO transactions (txid, created, expiry_height, raw, fee) - VALUES (?, ?, ?, ?, ?)", - )?, - stmt_update_tx_data: wallet_db.conn.prepare( - "UPDATE transactions - SET expiry_height = :expiry_height, - raw = :raw, - fee = IFNULL(:fee, fee) - WHERE txid = :txid", - )?, - stmt_select_tx_ref: wallet_db.conn.prepare( - "SELECT id_tx FROM transactions WHERE txid = ?", - )?, - stmt_mark_sapling_note_spent: wallet_db.conn.prepare( - "UPDATE sapling_received_notes SET spent = ? WHERE nf = ?" - )?, - #[cfg(feature = "transparent-inputs")] - stmt_mark_transparent_utxo_spent: wallet_db.conn.prepare( - "UPDATE utxos SET spent_in_tx = :spent_in_tx - WHERE prevout_txid = :prevout_txid - AND prevout_idx = :prevout_idx" - )?, - #[cfg(feature = "transparent-inputs")] - stmt_insert_received_transparent_utxo: wallet_db.conn.prepare( - "INSERT INTO utxos ( - received_by_account, address, - prevout_txid, prevout_idx, script, - value_zat, height) - SELECT - addresses.account, :address, - :prevout_txid, :prevout_idx, :script, - :value_zat, :height - FROM addresses - WHERE addresses.cached_transparent_receiver_address = :address - RETURNING id_utxo" - )?, - #[cfg(feature = "transparent-inputs")] - stmt_update_received_transparent_utxo: wallet_db.conn.prepare( - "UPDATE utxos - SET received_by_account = addresses.account, - height = :height, - address = :address, - script = :script, - value_zat = :value_zat - FROM addresses - WHERE prevout_txid = :prevout_txid - AND prevout_idx = :prevout_idx - AND addresses.cached_transparent_receiver_address = :address - RETURNING id_utxo" - )?, - #[cfg(feature = "transparent-inputs")] - stmt_insert_legacy_transparent_utxo: wallet_db.conn.prepare( - "INSERT INTO utxos ( - received_by_account, address, - prevout_txid, prevout_idx, script, - value_zat, height) - VALUES - (:received_by_account, :address, - :prevout_txid, :prevout_idx, :script, - :value_zat, :height) - RETURNING id_utxo" - )?, - #[cfg(feature = "transparent-inputs")] - stmt_update_legacy_transparent_utxo: wallet_db.conn.prepare( - "UPDATE utxos - SET received_by_account = :received_by_account, - height = :height, - address = :address, - script = :script, - value_zat = :value_zat - WHERE prevout_txid = :prevout_txid - AND prevout_idx = :prevout_idx - RETURNING id_utxo" - )?, - stmt_insert_received_note: wallet_db.conn.prepare( - "INSERT INTO sapling_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: wallet_db.conn.prepare( - "UPDATE sapling_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: wallet_db.conn.prepare( - "SELECT id_note FROM sapling_received_notes WHERE tx = ? AND output_index = ?" - )?, - stmt_update_sent_output: wallet_db.conn.prepare( - "UPDATE sent_notes - SET from_account = :from_account, - to_address = :to_address, - to_account = :to_account, - value = :value, - memo = IFNULL(:memo, memo) - WHERE tx = :tx - AND output_pool = :output_pool - AND output_index = :output_index", - )?, - stmt_insert_sent_output: wallet_db.conn.prepare( - "INSERT INTO sent_notes ( - tx, output_pool, output_index, from_account, - to_address, to_account, value, memo) - VALUES ( - :tx, :output_pool, :output_index, :from_account, - :to_address, :to_account, :value, :memo)" - )?, - stmt_insert_witness: wallet_db.conn.prepare( - "INSERT INTO sapling_witnesses (note, block, witness) - VALUES (?, ?, ?)", - )?, - stmt_prune_witnesses: wallet_db.conn.prepare( - "DELETE FROM sapling_witnesses WHERE block < ?" - )?, - stmt_update_expired: wallet_db.conn.prepare( - "UPDATE sapling_received_notes SET spent = NULL WHERE EXISTS ( - SELECT id_tx FROM transactions - WHERE id_tx = sapling_received_notes.spent AND block IS NULL AND expiry_height < ? - )", - )?, - stmt_insert_address: InsertAddress::new(&wallet_db.conn)? - } - ) - } - - /// Inserts information about a scanned block into the database. - pub fn stmt_insert_block( - &mut self, - block_height: BlockHeight, - block_hash: BlockHash, - block_time: u32, - commitment_tree: &sapling::CommitmentTree, - ) -> Result<(), SqliteClientError> { - let mut encoded_tree = Vec::new(); - write_commitment_tree(commitment_tree, &mut encoded_tree).unwrap(); - - self.stmt_insert_block.execute(params![ - u32::from(block_height), - &block_hash.0[..], - block_time, - encoded_tree - ])?; - - Ok(()) - } - - /// Inserts the given transaction and its block metadata into the wallet. - /// - /// Returns the database row for the newly-inserted transaction, or an error if the - /// transaction exists. - pub(crate) fn stmt_insert_tx_meta( - &mut self, - txid: &TxId, - height: BlockHeight, - tx_index: usize, - ) -> Result { - self.stmt_insert_tx_meta.execute(params![ - &txid.as_ref()[..], - u32::from(height), - (tx_index as i64), - ])?; - - Ok(self.wallet_db.conn.last_insert_rowid()) - } - - /// Updates the block metadata for the given transaction. - /// - /// Returns `false` if the transaction doesn't exist in the wallet. - pub(crate) fn stmt_update_tx_meta( - &mut self, - height: BlockHeight, - tx_index: usize, - txid: &TxId, - ) -> Result { - match self.stmt_update_tx_meta.execute(params![ - u32::from(height), - (tx_index as i64), - &txid.as_ref()[..], - ])? { - 0 => Ok(false), - 1 => Ok(true), - _ => unreachable!("txid column is marked as UNIQUE"), - } - } - - /// Inserts the given transaction and its data into the wallet. - /// - /// Returns the database row for the newly-inserted transaction, or an error if the - /// transaction exists. - pub(crate) fn stmt_insert_tx_data( - &mut self, - txid: &TxId, - created_at: Option, - expiry_height: BlockHeight, - raw_tx: &[u8], - fee: Option, - ) -> Result { - self.stmt_insert_tx_data.execute(params![ - &txid.as_ref()[..], - created_at, - u32::from(expiry_height), - raw_tx, - fee.map(i64::from) - ])?; - - Ok(self.wallet_db.conn.last_insert_rowid()) - } - - /// Updates the data for the given transaction. - /// - /// Returns `false` if the transaction doesn't exist in the wallet. - pub(crate) fn stmt_update_tx_data( - &mut self, - expiry_height: BlockHeight, - raw_tx: &[u8], - fee: Option, - txid: &TxId, - ) -> Result { - let sql_args: &[(&str, &dyn ToSql)] = &[ - (":expiry_height", &u32::from(expiry_height)), - (":raw", &raw_tx), - (":fee", &fee.map(i64::from)), - (":txid", &&txid.as_ref()[..]), - ]; - match self.stmt_update_tx_data.execute(sql_args)? { - 0 => Ok(false), - 1 => Ok(true), - _ => unreachable!("txid column is marked as UNIQUE"), - } - } - - /// Finds the database row for the given `txid`, if the transaction is in the wallet. - pub(crate) fn stmt_select_tx_ref(&mut self, txid: &TxId) -> Result { - self.stmt_select_tx_ref - .query_row([&txid.as_ref()[..]], |row| row.get(0)) - .map_err(SqliteClientError::from) - } - - /// 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. - /// - /// Returns `false` if the nullifier does not correspond to any received note. - pub(crate) fn stmt_mark_sapling_note_spent( - &mut self, - tx_ref: i64, - nf: &Nullifier, - ) -> Result { - match self - .stmt_mark_sapling_note_spent - .execute(params![tx_ref, &nf.0[..]])? - { - 0 => Ok(false), - 1 => Ok(true), - _ => unreachable!("nf column is marked as UNIQUE"), - } - } - - /// Marks the given UTXO as having been spent. - /// - /// Returns `false` if `outpoint` does not correspond to any tracked UTXO. - #[cfg(feature = "transparent-inputs")] - pub(crate) fn stmt_mark_transparent_utxo_spent( - &mut self, - tx_ref: i64, - outpoint: &OutPoint, - ) -> Result { - let sql_args: &[(&str, &dyn ToSql)] = &[ - (":spent_in_tx", &tx_ref), - (":prevout_txid", &outpoint.hash().to_vec()), - (":prevout_idx", &outpoint.n()), - ]; - - match self.stmt_mark_transparent_utxo_spent.execute(sql_args)? { - 0 => Ok(false), - 1 => Ok(true), - _ => unreachable!("tx_outpoint constraint is marked as UNIQUE"), - } - } -} - -impl<'a, P: consensus::Parameters> DataConnStmtCache<'a, P> { - /// Inserts a sent note into the wallet database. - /// - /// `output_index` is the index within the transaction that contains the recipient output: - /// - /// - If `to` is a Unified address, this is an index into the outputs of the transaction - /// within the bundle associated with the recipient's output pool. - /// - 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 - /// the transaction. - /// - If `to` is an internal account, this is an index into the Sapling outputs of the - /// transaction. - #[allow(clippy::too_many_arguments)] - pub(crate) fn stmt_insert_sent_output( - &mut self, - tx_ref: i64, - output_index: usize, - from_account: AccountId, - to: &Recipient, - value: Amount, - memo: Option<&MemoBytes>, - ) -> Result<(), SqliteClientError> { - let (to_address, to_account, pool_type) = match to { - Recipient::Transparent(addr) => ( - Some(addr.encode(&self.wallet_db.params)), - None, - PoolType::Transparent, - ), - Recipient::Sapling(addr) => ( - Some(addr.encode(&self.wallet_db.params)), - None, - PoolType::Sapling, - ), - Recipient::Unified(addr, pool) => { - (Some(addr.encode(&self.wallet_db.params)), None, *pool) - } - Recipient::InternalAccount(id, pool) => (None, Some(u32::from(*id)), *pool), - }; - - self.stmt_insert_sent_output.execute(named_params![ - ":tx": &tx_ref, - ":output_pool": &pool_code(pool_type), - ":output_index": &i64::try_from(output_index).unwrap(), - ":from_account": &u32::from(from_account), - ":to_address": &to_address, - ":to_account": &to_account, - ":value": &i64::from(value), - ":memo": &memo.filter(|m| *m != &MemoBytes::empty()).map(|m| m.as_slice()), - ])?; - - Ok(()) - } - - /// Updates the data for the given sent note. - /// - /// Returns `false` if the transaction doesn't exist in the wallet. - #[allow(clippy::too_many_arguments)] - pub(crate) fn stmt_update_sent_output( - &mut self, - from_account: AccountId, - to: &Recipient, - value: Amount, - memo: Option<&MemoBytes>, - tx_ref: i64, - output_index: usize, - ) -> Result { - let (to_address, to_account, pool_type) = match to { - Recipient::Transparent(addr) => ( - Some(addr.encode(&self.wallet_db.params)), - None, - PoolType::Transparent, - ), - Recipient::Sapling(addr) => ( - Some(addr.encode(&self.wallet_db.params)), - None, - PoolType::Sapling, - ), - Recipient::Unified(addr, pool) => { - (Some(addr.encode(&self.wallet_db.params)), None, *pool) - } - Recipient::InternalAccount(id, pool) => (None, Some(u32::from(*id)), *pool), - }; - - match self.stmt_update_sent_output.execute(named_params![ - ":from_account": &u32::from(from_account), - ":to_address": &to_address, - ":to_account": &to_account, - ":value": &i64::from(value), - ":memo": &memo.filter(|m| *m != &MemoBytes::empty()).map(|m| m.as_slice()), - ":tx": &tx_ref, - ":output_pool": &pool_code(pool_type), - ":output_index": &i64::try_from(output_index).unwrap(), - ])? { - 0 => Ok(false), - 1 => Ok(true), - _ => unreachable!("tx_output constraint is marked as UNIQUE"), - } - } - - /// Adds the given received UTXO to the datastore. - /// - /// Returns the database identifier for the newly-inserted UTXO if the address to which the - /// UTXO was sent corresponds to a cached transparent receiver in the addresses table, or - /// Ok(None) if the address is unknown. Returns an error if the UTXO exists. - #[cfg(feature = "transparent-inputs")] - pub(crate) fn stmt_insert_received_transparent_utxo( - &mut self, - output: &WalletTransparentOutput, - ) -> Result, SqliteClientError> { - self.stmt_insert_received_transparent_utxo - .query_row( - named_params![ - ":address": &output.recipient_address().encode(&self.wallet_db.params), - ":prevout_txid": &output.outpoint().hash().to_vec(), - ":prevout_idx": &output.outpoint().n(), - ":script": &output.txout().script_pubkey.0, - ":value_zat": &i64::from(output.txout().value), - ":height": &u32::from(output.height()), - ], - |row| { - let id = row.get(0)?; - Ok(UtxoId(id)) - }, - ) - .optional() - .map_err(SqliteClientError::from) - } - - /// Adds the given received UTXO to the datastore. - /// - /// Returns the database identifier for the updated UTXO if the address to which the UTXO was - /// sent corresponds to a cached transparent receiver in the addresses table, or Ok(None) if - /// the address is unknown. - #[cfg(feature = "transparent-inputs")] - pub(crate) fn stmt_update_received_transparent_utxo( - &mut self, - output: &WalletTransparentOutput, - ) -> Result, SqliteClientError> { - self.stmt_update_received_transparent_utxo - .query_row( - named_params![ - ":prevout_txid": &output.outpoint().hash().to_vec(), - ":prevout_idx": &output.outpoint().n(), - ":address": &output.recipient_address().encode(&self.wallet_db.params), - ":script": &output.txout().script_pubkey.0, - ":value_zat": &i64::from(output.txout().value), - ":height": &u32::from(output.height()), - ], - |row| { - let id = row.get(0)?; - Ok(UtxoId(id)) - }, - ) - .optional() - .map_err(SqliteClientError::from) - } - - /// Adds the given legacy UTXO to the datastore. - /// - /// Returns the database row for the newly-inserted UTXO, or an error if the UTXO - /// exists. - #[cfg(feature = "transparent-inputs")] - pub(crate) fn stmt_insert_legacy_transparent_utxo( - &mut self, - output: &WalletTransparentOutput, - received_by_account: AccountId, - ) -> Result { - self.stmt_insert_legacy_transparent_utxo - .query_row( - named_params![ - ":received_by_account": &u32::from(received_by_account), - ":address": &output.recipient_address().encode(&self.wallet_db.params), - ":prevout_txid": &output.outpoint().hash().to_vec(), - ":prevout_idx": &output.outpoint().n(), - ":script": &output.txout().script_pubkey.0, - ":value_zat": &i64::from(output.txout().value), - ":height": &u32::from(output.height()), - ], - |row| { - let id = row.get(0)?; - Ok(UtxoId(id)) - }, - ) - .map_err(SqliteClientError::from) - } - - /// Adds the given legacy UTXO to the datastore. - /// - /// Returns the database row for the newly-inserted UTXO, or an error if the UTXO - /// exists. - #[cfg(feature = "transparent-inputs")] - pub(crate) fn stmt_update_legacy_transparent_utxo( - &mut self, - output: &WalletTransparentOutput, - received_by_account: AccountId, - ) -> Result, SqliteClientError> { - self.stmt_update_legacy_transparent_utxo - .query_row( - named_params![ - ":received_by_account": &u32::from(received_by_account), - ":prevout_txid": &output.outpoint().hash().to_vec(), - ":prevout_idx": &output.outpoint().n(), - ":address": &output.recipient_address().encode(&self.wallet_db.params), - ":script": &output.txout().script_pubkey.0, - ":value_zat": &i64::from(output.txout().value), - ":height": &u32::from(output.height()), - ], - |row| { - let id = row.get(0)?; - Ok(UtxoId(id)) - }, - ) - .optional() - .map_err(SqliteClientError::from) - } - - /// Adds the given address and diversifier index to the addresses table. - /// - /// Returns the database row for the newly-inserted address. - pub(crate) fn stmt_insert_address( - &mut self, - account: AccountId, - diversifier_index: DiversifierIndex, - address: &UnifiedAddress, - ) -> Result<(), SqliteClientError> { - self.stmt_insert_address.execute( - &self.wallet_db.params, - account, - diversifier_index, - address, - )?; - - Ok(()) - } -} - -impl<'a, P> DataConnStmtCache<'a, P> { - /// Inserts the given received note into the wallet. - /// - /// 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. - /// - /// Returns the database row for the newly-inserted note, or an error if the note - /// exists. - #[allow(clippy::too_many_arguments)] - pub(crate) fn stmt_insert_received_note( - &mut self, - tx_ref: i64, - output_index: usize, - account: AccountId, - diversifier: &Diversifier, - value: u64, - rcm: [u8; 32], - nf: Option<&Nullifier>, - memo: Option<&MemoBytes>, - is_change: bool, - ) -> Result { - let sql_args: &[(&str, &dyn ToSql)] = &[ - (":tx", &tx_ref), - (":output_index", &(output_index as i64)), - (":account", &u32::from(account)), - (":diversifier", &diversifier.0.as_ref()), - (":value", &(value as i64)), - (":rcm", &rcm.as_ref()), - (":nf", &nf.map(|nf| nf.0.as_ref())), - ( - ":memo", - &memo - .filter(|m| *m != &MemoBytes::empty()) - .map(|m| m.as_slice()), - ), - (":is_change", &is_change), - ]; - - self.stmt_insert_received_note.execute(sql_args)?; - - Ok(NoteId::ReceivedNoteId( - self.wallet_db.conn.last_insert_rowid(), - )) - } - - /// Updates the data for the given transaction. - /// - /// 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. - /// - /// Returns `false` if the transaction doesn't exist in the wallet. - #[allow(clippy::too_many_arguments)] - pub(crate) fn stmt_update_received_note( - &mut self, - account: AccountId, - diversifier: &Diversifier, - value: u64, - rcm: [u8; 32], - nf: Option<&Nullifier>, - memo: Option<&MemoBytes>, - is_change: bool, - tx_ref: i64, - output_index: usize, - ) -> Result { - let sql_args: &[(&str, &dyn ToSql)] = &[ - (":account", &u32::from(account)), - (":diversifier", &diversifier.0.as_ref()), - (":value", &(value as i64)), - (":rcm", &rcm.as_ref()), - (":nf", &nf.map(|nf| nf.0.as_ref())), - ( - ":memo", - &memo - .filter(|m| *m != &MemoBytes::empty()) - .map(|m| m.as_slice()), - ), - (":is_change", &is_change), - (":tx", &tx_ref), - (":output_index", &(output_index as i64)), - ]; - - match self.stmt_update_received_note.execute(sql_args)? { - 0 => Ok(false), - 1 => Ok(true), - _ => unreachable!("tx_output constraint is marked as UNIQUE"), - } - } - - /// Finds the database row for the given `txid`, if the transaction is in the wallet. - pub(crate) fn stmt_select_received_note( - &mut self, - tx_ref: i64, - output_index: usize, - ) -> Result { - self.stmt_select_received_note - .query_row(params![tx_ref, (output_index as i64)], |row| { - row.get(0).map(NoteId::ReceivedNoteId) - }) - .map_err(SqliteClientError::from) - } - - /// Records the incremental witness for the specified note, as of the given block - /// height. - /// - /// Returns `SqliteClientError::InvalidNoteId` if the note ID is for a sent note. - pub(crate) fn stmt_insert_witness( - &mut self, - note_id: NoteId, - height: BlockHeight, - witness: &sapling::IncrementalWitness, - ) -> Result<(), SqliteClientError> { - let note_id = match note_id { - NoteId::ReceivedNoteId(note_id) => Ok(note_id), - NoteId::SentNoteId(_) => Err(SqliteClientError::InvalidNoteId), - }?; - - let mut encoded = Vec::new(); - write_incremental_witness(witness, &mut encoded).unwrap(); - - self.stmt_insert_witness - .execute(params![note_id, u32::from(height), encoded])?; - - Ok(()) - } - - /// Removes old incremental witnesses up to the given block height. - pub(crate) fn stmt_prune_witnesses( - &mut self, - below_height: BlockHeight, - ) -> Result<(), SqliteClientError> { - self.stmt_prune_witnesses - .execute([u32::from(below_height)])?; - Ok(()) - } - - /// Marks notes that have not been mined in transactions as expired, up to the given - /// block height. - pub fn stmt_update_expired(&mut self, height: BlockHeight) -> Result<(), SqliteClientError> { - self.stmt_update_expired.execute([u32::from(height)])?; - Ok(()) - } -} 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..9fdf336cce --- /dev/null +++ b/zcash_client_sqlite/src/testing/db.rs @@ -0,0 +1,228 @@ +use ambassador::Delegate; +use rand::SeedableRng; +use rand_chacha::ChaChaRng; +use rusqlite::Connection; +use std::num::NonZeroU32; +use std::time::Duration; +use std::{collections::HashMap, time::SystemTime}; +use uuid::Uuid; + +use tempfile::NamedTempFile; + +use rusqlite::{self}; +use secrecy::SecretVec; +use shardtree::{error::ShardTreeError, ShardTree}; +use zcash_client_backend::{ + data_api::{ + chain::{ChainState, CommitmentTreeRoot}, + scanning::ScanRange, + testing::{DataStoreFactory, Reset, TestState}, + TargetValue, *, + }, + 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, ShieldedProtocol, +}; +use zip32::{fingerprint::SeedFingerprint, DiversifierIndex}; + +use crate::{ + error::SqliteClientError, util::testing::FixedClock, wallet::init::WalletMigrator, AccountUuid, + WalletDb, +}; + +#[cfg(feature = "transparent-inputs")] +use { + crate::TransparentAddressMetadata, + ::transparent::{address::TransparentAddress, bundle::OutPoint, keys::NonHardenedChildIndex}, + core::ops::Range, + testing::transparent::GapLimits, +}; + +#[cfg(feature = "test-dependencies")] +use crate::Zatoshis; + +/// Tuesday, 25 February 2025 00:00:00Z (the day the clock code was added). +const TEST_EPOCH_SECONDS_OFFSET: Duration = Duration::from_secs(1740441600); + +pub(crate) fn test_clock() -> FixedClock { + FixedClock::new(SystemTime::UNIX_EPOCH + TEST_EPOCH_SECONDS_OFFSET) +} + +pub(crate) fn test_rng() -> ChaChaRng { + ChaChaRng::from_seed([0u8; 32]) +} + +#[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 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, + #[cfg(feature = "transparent-inputs")] gap_limits: GapLimits, + ) -> Result { + let data_file = NamedTempFile::new().unwrap(); + let mut db_data = + WalletDb::for_path(data_file.path(), network, test_clock(), test_rng()).unwrap(); + #[cfg(feature = "transparent-inputs")] + { + db_data = db_data.with_gap_limits(gap_limits.into()); + } + + let migrator = WalletMigrator::new(); + if let Some(migrations) = &self.target_migrations { + migrator + .init_or_migrate_to(&mut db_data, migrations) + .unwrap(); + } else { + migrator.init_or_migrate(&mut db_data).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(); + #[cfg(feature = "transparent-inputs")] + let gap_limits = st.wallet().db().gap_limits; + let old_db = std::mem::replace( + st.wallet_mut(), + TestDbFactory::default() + .new_data_store( + network, + #[cfg(feature = "transparent-inputs")] + gap_limits.into(), + ) + .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..9e48944901 --- /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, _, expected_bad_index| { + matches!( + e, + crate::error::SqliteClientError::ReachedGapLimit(_, bad_index) + if 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/util.rs b/zcash_client_sqlite/src/util.rs new file mode 100644 index 0000000000..260e2eb266 --- /dev/null +++ b/zcash_client_sqlite/src/util.rs @@ -0,0 +1,75 @@ +//! Types that should be part of the standard library, but aren't. + +use std::time::SystemTime; + +/// A trait that represents the capability to read the system time. +/// +/// Using implementations of this trait instead of accessing the system clock directly allows +/// mocking with a controlled clock for testing purposes. +pub trait Clock { + /// Returns the current system time, according to this clock. + fn now(&self) -> SystemTime; +} + +/// A [`Clock`] impl that returns the current time according to the system clock. +/// +/// This clock may be freely copied, as it is a zero-allocation type that simply delegates to +/// [`SystemTime::now`] to return the current time. +#[derive(Clone, Copy)] +pub struct SystemClock; + +impl Clock for SystemClock { + fn now(&self) -> SystemTime { + SystemTime::now() + } +} + +impl Clock for &C { + fn now(&self) -> SystemTime { + (*self).now() + } +} + +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing { + use std::sync::{Arc, RwLock}; + use std::time::SystemTime; + + use std::time::Duration; + + use super::Clock; + + /// A [`Clock`] impl that always returns a constant value for calls to [`now`]. + /// + /// Calling `.clone()` on this clock will return a clock that shares the underlying storage and + /// uses a read-write lock to ensure serialized access to its [`tick`] method. + /// + /// [`now`]: Clock::now + /// [`tick`]: Self::tick + #[derive(Clone)] + pub struct FixedClock { + now: Arc>, + } + + impl FixedClock { + /// Constructs a new [`FixedClock`] with the given time as the current instant. + pub fn new(now: SystemTime) -> Self { + Self { + now: Arc::new(RwLock::new(now)), + } + } + + /// Updates the current time held by this [`FixedClock`] by adding the specified duration to + /// that instant. + pub fn tick(&self, delta: Duration) { + let mut w = self.now.write().unwrap(); + *w += delta; + } + } + + impl Clock for FixedClock { + fn now(&self) -> SystemTime { + *self.now.read().unwrap() + } + } +} diff --git a/zcash_client_sqlite/src/wallet.rs b/zcash_client_sqlite/src/wallet.rs index dbffcaadd2..3be78eb921 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 @@ -53,8 +53,8 @@ //! 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` for sent outputs, the account from which the value was sent. -//! - `to_account` in the case that the output was received by an account in the wallet, the +//! - `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. @@ -64,977 +64,4379 @@ //! wallet. //! - `memo` the shielded memo associated with the output, if any. -use rusqlite::{named_params, OptionalExtension, ToSql}; -use std::collections::HashMap; -use std::convert::TryFrom; +use std::{ + collections::{HashMap, HashSet}, + convert::TryFrom, + io::{self, Cursor}, + num::NonZeroU32, + ops::{Range, RangeInclusive}, + time::SystemTime, +}; + +use encoding::{ + account_kind_code, decode_diversifier_index_be, encode_diversifier_index_be, memo_repr, + pool_code, KeyScope, ReceiverFlags, +}; +use incrementalmerkletree::{Marking, Retention}; +use rusqlite::{self, named_params, params, Connection, OptionalExtension}; +use secrecy::{ExposeSecret, SecretVec}; +use shardtree::{error::ShardTreeError, store::ShardStore, ShardTree}; +use tracing::{debug, warn}; +use uuid::Uuid; +use zcash_address::ZcashAddress; +use zcash_client_backend::{ + data_api::{ + scanning::{ScanPriority, ScanRange}, + Account as _, AccountBalance, AccountBirthday, AccountPurpose, AccountSource, AddressInfo, + BlockMetadata, DecryptedTransaction, Progress, Ratio, SentTransaction, + SentTransactionOutput, TransactionDataRequest, TransactionStatus, WalletSummary, + Zip32Derivation, SAPLING_SHARD_HEIGHT, + }, + wallet::{Note, NoteId, Recipient, WalletTx}, + DecryptedOutput, +}; +use zcash_keys::{ + address::{Address, Receiver, UnifiedAddress}, + encoding::AddressCodec, + keys::{ + AddressGenerationError, ReceiverRequirement, UnifiedAddressRequest, UnifiedFullViewingKey, + UnifiedIncomingViewingKey, UnifiedSpendingKey, + }, +}; use zcash_primitives::{ block::BlockHash, + merkle_tree::read_commitment_tree, + transaction::{Transaction, TransactionData}, +}; +use zcash_protocol::{ consensus::{self, BlockHeight, BranchId, NetworkUpgrade, Parameters}, memo::{Memo, MemoBytes}, - sapling::CommitmentTree, - transaction::{components::Amount, Transaction, TxId}, - zip32::{ - sapling::{DiversifiableFullViewingKey, ExtendedFullViewingKey}, - AccountId, DiversifierIndex, - }, -}; - -use zcash_client_backend::{ - address::{RecipientAddress, UnifiedAddress}, - data_api::{PoolType, Recipient, SentTransactionOutput}, - keys::UnifiedFullViewingKey, - wallet::WalletTx, + value::{ZatBalance, Zatoshis}, + PoolType, ShieldedProtocol, TxId, }; +use zip32::{fingerprint::SeedFingerprint, DiversifierIndex}; +use self::scanning::{parse_priority_code, priority_code, replace_queue_entries}; use crate::{ - error::SqliteClientError, prepared::InsertAddress, DataConnStmtCache, WalletDb, PRUNING_HEIGHT, + error::SqliteClientError, + util::Clock, + wallet::commitment_tree::{get_max_checkpointed_height, SqliteShardStore}, + AccountRef, AccountUuid, AddressRef, SqlTransaction, TransferType, TxRef, + WalletCommitmentTrees, WalletDb, PRUNING_DEPTH, SAPLING_TABLES_PREFIX, VERIFY_LOOKAHEAD, }; #[cfg(feature = "transparent-inputs")] use { - crate::UtxoId, - rusqlite::{params, Connection}, - std::collections::BTreeSet, - zcash_client_backend::{ - address::AddressMetadata, encoding::AddressCodec, wallet::WalletTransparentOutput, - }, - zcash_primitives::{ - legacy::{keys::IncomingViewingKey, Script, TransparentAddress}, - transaction::components::{OutPoint, TxOut}, + crate::GapLimits, + ::transparent::{ + bundle::{OutPoint, TxOut}, + keys::{NonHardenedChildIndex, TransparentKeyScope}, }, + std::collections::BTreeMap, }; +#[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(crate) mod encoding; pub mod init; +#[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) 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::Sapling => 2i64, +pub(crate) const BLOCK_SAPLING_FRONTIER_ABSENT: &[u8] = &[0x0]; + +/// A constant for use in converting Unix timestamps to shielded-only diversifier indices. The +/// value here is intended to be added to the current time, in seconds since the epoch, to obtain +/// an index that is greater than or equal to 2^32. While it would be possible to use indices in +/// the range 2^31..2^32, we wish to avoid any confusion with indices in the BIP 32 child +/// index derivation space. +/// +/// 2^32 - (date --date "Oct 28, 2016 07:56 UTC" +%s) +pub(crate) const MIN_SHIELDED_DIVERSIFIER_OFFSET: u64 = 2817325936; + +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(), + )), } } -pub(crate) fn get_max_account_id

( - wdb: &WalletDb

, -) -> Result, SqliteClientError> { - // This returns the most recently generated address. - wdb.conn - .query_row("SELECT MAX(account) FROM accounts", [], |row| { - let account_id: Option = row.get(0)?; - Ok(account_id.map(AccountId::from)) - }) - .map_err(SqliteClientError::from) +/// 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), +} + +/// An account stored in a `zcash_client_sqlite` database. +#[derive(Debug, Clone)] +pub struct Account { + id: AccountRef, + uuid: AccountUuid, + name: Option, + kind: AccountSource, + viewing_key: ViewingKey, + birthday: BlockHeight, +} + +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: UnifiedAddressRequest, + ) -> Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError> { + self.uivk().default_address(request) + } + + pub(crate) fn internal_id(&self) -> AccountRef { + self.id + } + + pub(crate) fn birthday(&self) -> BlockHeight { + self.birthday + } +} + +impl zcash_client_backend::data_api::Account for Account { + type AccountId = AccountUuid; + + fn id(&self) -> AccountUuid { + self.uuid + } + + fn name(&self) -> Option<&str> { + self.name.as_deref() + } + + fn source(&self) -> &AccountSource { + &self.kind + } + + fn ufvk(&self) -> Option<&UnifiedFullViewingKey> { + self.viewing_key.ufvk() + } + + fn uivk(&self) -> UnifiedIncomingViewingKey { + self.viewing_key.uivk() + } +} + +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(), + } + } +} + +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(UnifiedAddressRequest::AllAvailableKeys)?; + let (uivk_addr, _) = uivk.default_address(UnifiedAddressRequest::AllAvailableKeys)?; + + #[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) + } +} + +// 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( - wdb: &WalletDb

, - account: AccountId, - key: &UnifiedFullViewingKey, -) -> Result<(), SqliteClientError> { - add_account_internal(&wdb.conn, &wdb.params, "accounts", account, key) + conn: &rusqlite::Transaction, + params: &P, + account_name: &str, + kind: &AccountSource, + viewing_key: ViewingKey, + birthday: &AccountBirthday, + #[cfg(feature = "transparent-inputs")] gap_limits: &GapLimits, +) -> 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())); + } + } + // 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 { + id: account_id, + name: Some(account_name.to_owned()), + uuid: account_uuid, + kind: kind.clone(), + viewing_key, + birthday: birthday.height(), + }; + + // 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, + }, + )?; + } + + #[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(UnifiedAddressRequest::AllAvailableKeys)?; + upsert_address( + conn, + params, + account_id, + d_idx, + &address, + Some(birthday.height()), + false, + )?; + + // Pre-generate external transparent addresses prior to the index of the default address. + #[cfg(feature = "transparent-inputs")] + if let Ok(default_addr_idx) = NonHardenedChildIndex::try_from(d_idx) { + transparent::generate_address_range( + conn, + params, + account_id, + KeyScope::EXTERNAL, + UnifiedAddressRequest::ALLOW_ALL, + NonHardenedChildIndex::const_from_index(0)..default_addr_idx, + false, + )? + } + + // Pre-generate transparent addresses up to the gap limits for the external, internal, + // and ephemeral key scopes. + #[cfg(feature = "transparent-inputs")] + for key_scope in [KeyScope::EXTERNAL, KeyScope::INTERNAL, KeyScope::Ephemeral] { + use ReceiverRequirement::*; + transparent::generate_gap_addresses( + conn, + params, + account_id, + key_scope, + gap_limits, + UnifiedAddressRequest::unsafe_custom(Allow, Allow, Require), + false, + )?; + } + + Ok(account) +} + +pub(crate) fn get_next_available_address( + conn: &rusqlite::Transaction, + params: &P, + clock: &C, + account_uuid: AccountUuid, + request: UnifiedAddressRequest, + #[cfg(feature = "transparent-inputs")] gap_limits: &GapLimits, +) -> Result, SqliteClientError> { + let account: Account = match get_account(conn, params, account_uuid)? { + Some(account) => account, + None => { + return Ok(None); + } + }; + + // This will also ensure that the provided request can be satisfied by the account's UIVK + let requirements = account.uivk().receiver_requirements(request)?; + + let (addr, diversifier_index) = if requirements.p2pkh() == ReceiverRequirement::Require { + #[cfg(not(feature = "transparent-inputs"))] + { + return Err(SqliteClientError::AddressGeneration( + AddressGenerationError::ReceiverTypeNotSupported( + zcash_address::unified::Typecode::P2pkh, + ), + )); + } + + // If a p2pkh receiver is required, return the first un-exposed address from within the + // transparent gap limit. + #[cfg(feature = "transparent-inputs")] + { + use ReceiverRequirement::*; + // First, ensure that we have pre-generated as many addresses as we can. + transparent::generate_gap_addresses( + conn, + params, + account.internal_id(), + KeyScope::EXTERNAL, + gap_limits, + UnifiedAddressRequest::unsafe_custom(Allow, Allow, Require), + true, + )?; + + // Select indices from the transparent gap limit that are available for use as + // diversifier indices. + let (gap_start, addrs) = transparent::select_addrs_to_reserve( + conn, + params, + account.internal_id(), + KeyScope::EXTERNAL, + gap_limits.external(), + gap_limits + .external() + .try_into() + .expect("gap limit fits in usize"), + )?; + + // Find the first index that generates an address conforming to the request. + addrs + .iter() + .find_map(|(_, _, meta)| { + let j = DiversifierIndex::from(meta.address_index()); + account.uivk().address(j, request).ok().map(|ua| (ua, j)) + }) + .ok_or(SqliteClientError::ReachedGapLimit( + TransparentKeyScope::EXTERNAL, + gap_start.index() + gap_limits.external(), + ))? + } + } else { + // compute a base diversifier index from the timestamp + let mut j = DiversifierIndex::from( + clock + .now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("system time is valid") + .as_secs() + .saturating_add(MIN_SHIELDED_DIVERSIFIER_OFFSET), + ); + + let mut find_collision = conn.prepare( + "SELECT exposed_at_height + FROM addresses + WHERE account_id = :account_id + AND key_scope = :key_scope + AND diversifier_index_be = :diversifier_index_be", + )?; + + // search the diversifier space for a diversifier index that creates a valid address + // satisfying the request and is currently not used in an exposed address + loop { + let found_addr = account.uivk().find_address(j, request)?; + let collision = find_collision + .query_row( + named_params! { + ":account_id": account.internal_id().0, + ":key_scope": KeyScope::EXTERNAL.encode(), + ":diversifier_index_be": &encode_diversifier_index_be(found_addr.1) + }, + |row| row.get::<_, Option>(0), + ) + .optional()? + .flatten(); + + if collision.is_none() { + break found_addr; + } else { + j.increment().map_err(|_| { + SqliteClientError::AddressGeneration( + AddressGenerationError::DiversifierSpaceExhausted, + ) + })?; + } + } + }; + + let chain_tip_height = chain_tip_height(conn)?.ok_or(SqliteClientError::ChainHeightUnknown)?; + upsert_address( + conn, + params, + account.internal_id(), + diversifier_index, + &addr, + Some(chain_tip_height), + true, + )?; + + Ok(Some((addr, diversifier_index))) } -pub(crate) fn add_account_internal>( +pub(crate) fn list_addresses( conn: &rusqlite::Connection, - network: &P, - accounts_table: &'static str, - account: AccountId, - key: &UnifiedFullViewingKey, -) -> Result<(), E> { - let ufvk_str: String = key.encode(network); - conn.execute( - &format!( - "INSERT INTO {} (account, ufvk) VALUES (:account, :ufvk)", - accounts_table - ), - named_params![":account": &::from(account), ":ufvk": &ufvk_str], + params: &P, + account_uuid: AccountUuid, +) -> Result, SqliteClientError> { + let mut addrs = vec![]; + + let mut stmt_addrs = conn.prepare( + "SELECT address, diversifier_index_be, key_scope + FROM addresses + JOIN accounts ON accounts.id = addresses.account_id + WHERE accounts.uuid = :account_uuid + AND exposed_at_height IS NOT NULL + ORDER BY exposed_at_height ASC, diversifier_index_be ASC", )?; - // Always derive the default Unified Address for the account. - let (address, d_idx) = key.default_address(); - InsertAddress::new(conn)?.execute(network, account, d_idx, &address)?; + let mut rows = stmt_addrs.query(named_params![ + ":account_uuid": account_uuid.0, + ])?; - Ok(()) + while let Some(row) = rows.next()? { + let addr_str: String = row.get(0)?; + let di_vec: Vec = row.get(1)?; + let _scope = KeyScope::decode(row.get(2)?)?; + + let addr = Address::decode(params, &addr_str).ok_or_else(|| { + SqliteClientError::CorruptedData("Not a valid Zcash recipient address".to_owned()) + })?; + let diversifier_index = decode_diversifier_index_be(&di_vec)?; + // Sapling and Unified addresses always have external scope. + #[cfg(feature = "transparent-inputs")] + let transparent_scope = + matches!(addr, Address::Transparent(_) | Address::Tex(_)).then(|| _scope.into()); + + addrs.push( + AddressInfo::from_parts( + addr, + diversifier_index, + #[cfg(feature = "transparent-inputs")] + transparent_scope, + ) + .expect("valid"), + ); + } + + Ok(addrs) } -pub(crate) fn get_current_address( - wdb: &WalletDb

, - account: AccountId, +pub(crate) fn get_last_generated_address_matching( + conn: &rusqlite::Connection, + params: &P, + account_uuid: AccountUuid, + address_filter: UnifiedAddressRequest, ) -> Result, SqliteClientError> { - // This returns the most recently generated address. - let addr: Option<(String, Vec)> = wdb - .conn + let account: Account = + get_account(conn, params, account_uuid)?.ok_or(SqliteClientError::AccountUnknown)?; + + let requirements = account + .uivk() + .receiver_requirements(address_filter) + .map_err(|_| { + SqliteClientError::BadAccountData( + "Could not generate UnifiedAddressRequest for UIVK".to_string(), + ) + })?; + let require_flags = ReceiverFlags::required(requirements); + let omit_flags = ReceiverFlags::omitted(requirements); + // This returns the most recently exposed external-scope address (the address that was exposed + // at the greatest block height, using the largest diversifier index to break ties) + // that conforms to the specified requirements. + let addr: Option<(String, Vec)> = conn .query_row( "SELECT address, diversifier_index_be - FROM addresses WHERE account = :account - ORDER BY diversifier_index_be DESC - LIMIT 1", - named_params![":account": &u32::from(account)], + FROM addresses + WHERE account_id = :account_id + AND key_scope = :key_scope + AND (receiver_flags & :require_flags) = :require_flags + AND (receiver_flags & :omit_flags) = 0 + AND exposed_at_height IS NOT NULL + ORDER BY exposed_at_height DESC, diversifier_index_be DESC + LIMIT 1", + named_params![ + ":account_id": account.internal_id().0, + ":key_scope": KeyScope::EXTERNAL.encode(), + ":require_flags": require_flags.bits(), + ":omit_flags": omit_flags.bits(), + ], |row| Ok((row.get(0)?, row.get(1)?)), ) .optional()?; 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(); - - RecipientAddress::decode(&wdb.params, &addr_str) + let diversifier_index = decode_diversifier_index_be(&di_vec)?; + Address::decode(params, &addr_str) .ok_or_else(|| { SqliteClientError::CorruptedData("Not a valid Zcash recipient address".to_owned()) }) .and_then(|addr| match addr { - RecipientAddress::Unified(ua) => Ok(ua), + Address::Unified(ua) => Ok(ua), _ => Err(SqliteClientError::CorruptedData(format!( "Addresses table contains {} which is not a unified address", addr_str, ))), }) - .map(|addr| (addr, DiversifierIndex(di_be))) + .map(|addr| (addr, diversifier_index)) }) .transpose() } -#[cfg(feature = "transparent-inputs")] -pub(crate) fn get_transparent_receivers( +/// Adds the given external address and diversifier index to the addresses table. +/// +/// Returns the primary key identifier for the newly-inserted address. +/// +/// ## Parameters +/// - `account_id`: The account that the address was generated for. +/// - `diversifier_index`: The diversifier index used to generate the address. +/// - `address`: The unified address itself. +/// - `exposed_at_height`: The block height at the earliest time that the address may have been +/// exposed to a user, assuming a single generator of addresses. +/// - `force_update_address`: If this argument is set to `true`, an address has already been +/// inserted for the given account and diversifier index, and the `exposed_at_height` column +/// is currently `NULL` (i.e. the address at this diversifier index has not yet been exposed) +/// then the value of the `address` column will be replaced with the provided address. +pub(crate) fn upsert_address( + conn: &rusqlite::Connection, params: &P, - conn: &Connection, - account: AccountId, -) -> Result, SqliteClientError> { - let mut ret = HashMap::new(); + account_id: AccountRef, + diversifier_index: DiversifierIndex, + address: &UnifiedAddress, + exposed_at_height: Option, + force_update_address: bool, +) -> Result { + // the diversifier index is stored in big-endian order to allow sorting + let di_be = encode_diversifier_index_be(diversifier_index); + + // If a force-update was requested, check whether an address has previously been exposed for + // this diversifier index. If so, and if that address differs from the given address, return an + // error. + if force_update_address { + let previously_exposed_as = conn + .query_row( + "SELECT address, exposed_at_height + FROM addresses + WHERE account_id = :account_id + AND diversifier_index_be = :diversifier_index_be + AND key_scope = :key_scope", + named_params![ + ":account_id": account_id.0, + ":diversifier_index_be": di_be, + ":key_scope": KeyScope::EXTERNAL.encode(), + ], + |row| { + let address = row.get::<_, String>("address")?; + let exposed_at = row.get::<_, Option>("exposed_at_height")?; + Ok(exposed_at.map(|_| address)) + }, + ) + .optional()? + .flatten() + .map(|addr_str| UnifiedAddress::decode(params, &addr_str)) + .transpose() + .map_err(SqliteClientError::CorruptedData)?; - // 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)])?; + match previously_exposed_as { + Some(addr) if &addr != address => { + return Err(SqliteClientError::DiversifierIndexReuse( + diversifier_index, + Box::new(addr), + )); + } + _ => (), + } + } - while let Some(row) = rows.next()? { - let ua_str: String = row.get(0)?; - let di_vec: Vec = row.get(1)?; - let mut di_be: [u8; 11] = di_vec.try_into().map_err(|_| { - SqliteClientError::CorruptedData( - "Diverisifier index is not an 11-byte value".to_owned(), + let mut stmt = conn.prepare_cached( + "INSERT INTO addresses ( + account_id, + diversifier_index_be, + key_scope, + address, + transparent_child_index, + cached_transparent_receiver_address, + exposed_at_height, + receiver_flags + ) + VALUES ( + :account_id, + :diversifier_index_be, + :key_scope, + :address, + :transparent_child_index, + :cached_transparent_receiver_address, + :exposed_at_height, + :receiver_flags + ) + ON CONFLICT (account_id, diversifier_index_be, key_scope) DO UPDATE + SET exposed_at_height = COALESCE( + MIN(exposed_at_height, :exposed_at_height), + exposed_at_height, + :exposed_at_height + ), + address = IIF( + exposed_at_height IS NULL AND :force_update_address, + :address, + address + ), + receiver_flags = IIF( + exposed_at_height IS NULL AND :force_update_address, + :receiver_flags, + receiver_flags ) - })?; - di_be.reverse(); + RETURNING id", + )?; - let ua = RecipientAddress::decode(params, &ua_str) - .ok_or_else(|| { - SqliteClientError::CorruptedData("Not a valid Zcash recipient address".to_owned()) - }) - .and_then(|addr| match addr { - RecipientAddress::Unified(ua) => Ok(ua), - _ => Err(SqliteClientError::CorruptedData(format!( - "Addresses table contains {} which is not a unified address", - ua_str, - ))), - })?; + #[cfg(feature = "transparent-inputs")] + let (transparent_child_index, cached_taddr) = { + let idx = NonHardenedChildIndex::try_from(diversifier_index) + .ok() + .map(|i| i.index()); - if let Some(taddr) = ua.transparent() { - ret.insert( - *taddr, - AddressMetadata::new(account, DiversifierIndex(di_be)), - ); + // This upholds the `transparent_index_consistency` check on the `addresses` table. + match (idx, address.transparent()) { + (Some(idx), Some(r)) => Ok((Some(idx), Some(r.encode(params)))), + (_, None) => Ok((None, None)), + (None, Some(addr)) => Err(SqliteClientError::AddressNotRecognized(*addr)), } - } + }?; - if let Some((taddr, diversifier_index)) = get_legacy_transparent_address(params, conn, account)? - { - ret.insert(taddr, AddressMetadata::new(account, diversifier_index)); - } + #[cfg(not(feature = "transparent-inputs"))] + let (transparent_child_index, cached_taddr): (Option, Option) = (None, None); - Ok(ret) + stmt.query_row( + named_params![ + ":account_id": account_id.0, + // the diversifier index is stored in big-endian order to allow sorting + ":diversifier_index_be": &di_be, + ":key_scope": KeyScope::EXTERNAL.encode(), + ":address": &address.encode(params), + ":transparent_child_index": transparent_child_index, + ":cached_transparent_receiver_address": &cached_taddr, + ":exposed_at_height": exposed_at_height.map(u32::from), + ":force_update_address": force_update_address, + ":receiver_flags": ReceiverFlags::from(address).bits() + ], + |row| row.get(0).map(AddressRef), + ) + .map_err(SqliteClientError::from) } #[cfg(feature = "transparent-inputs")] -pub(crate) fn get_legacy_transparent_address( - params: &P, - conn: &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()?; +pub(crate) fn involved_accounts( + conn: &rusqlite::Connection, + tx_refs: impl IntoIterator, +) -> Result, SqliteClientError> { + use rusqlite::types::Value; + use std::rc::Rc; - if let Some(ufvk_str) = ufvk_str { - let ufvk = UnifiedFullViewingKey::decode(params, &ufvk_str) - .map_err(SqliteClientError::CorruptedData)?; + let mut stmt = conn.prepare_cached( + "SELECT account_id, key_scope + FROM v_address_uses + WHERE transaction_id IN rarray(:tx_refs_ptr)", + )?; - // 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| { - let (taddr, child_index) = tivk.default_address(); - (taddr, DiversifierIndex::from(child_index)) - }) - .map_err(SqliteClientError::HdwalletError) - }) - .transpose() - } else { - Ok(None) - } + let tx_refs_values: Vec = tx_refs.into_iter().map(|r| Value::Integer(r.0)).collect(); + let tx_refs_ptr = Rc::new(tx_refs_values); + let result = stmt + .query_and_then( + named_params! { + ":tx_refs_ptr": &tx_refs_ptr + }, + |row| { + Ok::<_, SqliteClientError>(( + row.get(0).map(AccountRef)?, + KeyScope::decode(row.get(1)?)?, + )) + }, + )? + .collect::, _>>()?; + + Ok(result) } /// Returns the [`UnifiedFullViewingKey`]s for the wallet. pub(crate) fn get_unified_full_viewing_keys( - wdb: &WalletDb

, -) -> Result, SqliteClientError> { + conn: &rusqlite::Connection, + params: &P, +) -> Result, SqliteClientError> { // Fetch the UnifiedFullViewingKeys we are tracking - let mut stmt_fetch_accounts = wdb - .conn - .prepare("SELECT account, ufvk FROM accounts ORDER BY account ASC")?; + let mut stmt_fetch_accounts = conn.prepare("SELECT uuid, ufvk FROM accounts")?; let rows = stmt_fetch_accounts.query_map([], |row| { - let acct: u32 = row.get(0)?; - let account = AccountId::from(acct); - let ufvk_str: String = row.get(1)?; - let ufvk = UnifiedFullViewingKey::decode(&wdb.params, &ufvk_str) - .map_err(SqliteClientError::CorruptedData); - - Ok((account, ufvk)) + 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(); + let mut res: HashMap = HashMap::new(); for row in rows { - let (account_id, ufvkr) = row?; - res.insert(account_id, ufvkr?); + if let Some((account_id, ufvkr)) = row? { + res.insert(account_id, ufvkr?); + } } Ok(res) } -/// Returns the account id corresponding to a given [`UnifiedFullViewingKey`], -/// if any. -pub(crate) fn get_account_for_ufvk( - wdb: &WalletDb

, - ufvk: &UnifiedFullViewingKey, -) -> Result, SqliteClientError> { - wdb.conn - .query_row( - "SELECT account FROM accounts WHERE ufvk = ?", - [&ufvk.encode(&wdb.params)], - |row| { - let acct: u32 = row.get(0)?; - Ok(AccountId::from(acct)) - }, - ) - .optional() - .map_err(SqliteClientError::from) +fn parse_account_row( + row: &rusqlite::Row<'_>, + params: &P, +) -> Result { + let account_id = AccountRef(row.get("id")?); + 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 + )) + })?, + )) + }; + + let birthday = BlockHeight::from(row.get::<_, u32>("birthday_height")?); + + Ok(Account { + id: account_id, + name: account_name, + uuid: account_uuid, + kind, + viewing_key, + birthday, + }) } -/// Checks whether the specified [`ExtendedFullViewingKey`] is valid and corresponds to the -/// specified account. -/// -/// [`ExtendedFullViewingKey`]: zcash_primitives::zip32::ExtendedFullViewingKey -pub(crate) fn is_valid_account_extfvk( - wdb: &WalletDb

, - account: AccountId, - extfvk: &ExtendedFullViewingKey, -) -> Result { - wdb.conn - .prepare("SELECT ufvk FROM accounts WHERE account = ?")? - .query_row([u32::from(account).to_sql()?], |row| { - row.get(0).map(|ufvk_str: String| { - UnifiedFullViewingKey::decode(&wdb.params, &ufvk_str) - .map_err(SqliteClientError::CorruptedData) - }) +pub(crate) fn get_account( + conn: &rusqlite::Connection, + params: &P, + account_uuid: AccountUuid, +) -> Result, SqliteClientError> { + let mut stmt = conn.prepare_cached( + r#" + SELECT id, name, uuid, account_kind, + hd_seed_fingerprint, hd_account_index, key_source, + ufvk, uivk, has_spend_key, birthday_height + FROM accounts + WHERE uuid = :account_uuid + "#, + )?; + + let mut rows = stmt.query_and_then::<_, SqliteClientError, _, _>( + named_params![":account_uuid": account_uuid.0], + |row| parse_account_row(row, params), + )?; + + rows.next().transpose() +} + +#[cfg(feature = "transparent-inputs")] +pub(crate) fn get_account_internal( + conn: &rusqlite::Connection, + params: &P, + account_id: AccountRef, +) -> Result, SqliteClientError> { + let mut stmt = conn.prepare_cached( + r#" + SELECT id, name, uuid, account_kind, + hd_seed_fingerprint, hd_account_index, key_source, + ufvk, uivk, has_spend_key, birthday_height + FROM accounts + WHERE id = :account_id + "#, + )?; + + let mut rows = stmt.query_and_then::<_, SqliteClientError, _, _>( + named_params![":account_id": account_id.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 id, name, uuid, account_kind, + hd_seed_fingerprint, hd_account_index, key_source, + ufvk, uivk, has_spend_key, birthday_height + 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 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 id, name, key_source, uuid, ufvk, birthday_height + 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_id = AccountRef(row.get("id")?); + 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 + )) + }), + }?; + let birthday = BlockHeight::from(row.get::<_, u32>("birthday_height")?); + + Ok(Account { + id: account_id, + 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)), + birthday, + }) + }, + )?; + + 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)), + } +} + +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()?; + + 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 + }) }) - .optional() - .map_err(SqliteClientError::from) + }; + + Ok(result) +} + +#[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_start_tree_size_at = conn.prepare_cached(&format!( + "SELECT {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 birthday height, + // get the tree size from a nearby subtree. It's fine for this to be approximate; + // it just alters the magnitude of recovery progress a bit. + 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)?, + }; + + // 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, + )?, + }; + + // 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 + .map(|h| { + let size_from_blocks = stmt_start_tree_size_at + .query_row(named_params![":start_height": u32::from(h)], |row| { + row.get::<_, Option>(0) + }) + .optional()? + .flatten(); + + match size_from_blocks { + // We know the tree size as of the start of the recover-until height. + Some(size) => Ok::<_, SqliteClientError>(Some(size)), + + // If the recover-until height is equal to the chain tip height, + // then this is almost certainly a newly-recovered wallet, and all + // progress can count as recovery progress. Approximate the size + // of the tree at the start of the block as equal to the size of + // the tree at the end of the block; the scan progress will show + // as 0/0 which is fine. + None if h == chain_tip_height => Ok(tip_tree_size), + + // Linearly extrapolate a tree size between the nearest two bounds + // we have. + // TODO: Use a closer lower bound if available. + None => Ok(birthday_size.zip(tip_tree_size).and_then( + |(lower_size, upper_size)| { + let total_notes = upper_size - lower_size; + let total_range = u64::from(chain_tip_height - birthday_height); + let recovery_range = u64::from(h - birthday_height); + + (total_notes * recovery_range).checked_div(total_range).map( + |extrapolated_recovery_notes| { + lower_size + extrapolated_recovery_notes + }, + ) + }, + )), + } + }) + .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()?; + + 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. +/// +/// 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. +/// +/// `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, + )?; + + #[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| { - if let Some(ufvk) = row { - ufvk.map(|ufvk| { - ufvk.sapling().map(|dfvk| dfvk.to_bytes()) - == Some(DiversifiableFullViewingKey::from(extfvk.clone()).to_bytes()) + 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 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`]. +/// +/// 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, + #[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))] + tx_data.zip233_amount(), + 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 { - Ok(false) + 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 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()? + .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)) +} + +#[cfg(feature = "transparent-inputs")] +pub(crate) fn account_birthday_internal( + conn: &rusqlite::Connection, + account_ref: AccountRef, +) -> Result { + conn.query_row( + "SELECT birthday_height + FROM accounts + WHERE id = :account_ref", + named_params![":account_ref": account_ref.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) +} + +/// 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)))) + }) } -/// 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. -#[cfg(test)] -pub(crate) fn get_balance

( - wdb: &WalletDb

, - account: AccountId, -) -> Result { - let balance = wdb.conn.query_row( - "SELECT SUM(value) FROM sapling_received_notes - INNER JOIN transactions ON transactions.id_tx = sapling_received_notes.tx - WHERE account = ? AND spent IS NULL AND transactions.block IS NOT NULL", - [u32::from(account)], - |row| row.get(0).or(Ok(0)), - )?; +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, + )?; - match Amount::from_i64(balance) { - Ok(amount) if !amount.is_negative() => Ok(amount), - _ => Err(SqliteClientError::CorruptedData( - "Sum of values in sapling_received_notes is out of range".to_string(), - )), - } -} + #[cfg(feature = "orchard")] + let orchard_anchor_height = get_max_checkpointed_height( + conn, + ORCHARD_TABLES_PREFIX, + chain_tip_height, + min_confirmations, + )?; -/// 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. -pub(crate) fn get_balance_at

( - wdb: &WalletDb

, - account: AccountId, - anchor_height: BlockHeight, -) -> Result { - let balance = wdb.conn.query_row( - "SELECT SUM(value) FROM sapling_received_notes - INNER JOIN transactions ON transactions.id_tx = sapling_received_notes.tx - WHERE account = ? AND spent IS NULL AND transactions.block <= ?", - [u32::from(account), u32::from(anchor_height)], - |row| row.get(0).or(Ok(0)), - )?; + #[cfg(not(feature = "orchard"))] + let orchard_anchor_height: Option = None; - match Amount::from_i64(balance) { - Ok(amount) if !amount.is_negative() => Ok(amount), - _ => Err(SqliteClientError::CorruptedData( - "Sum of values in sapling_received_notes is out of range".to_string(), - )), + 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), } } -/// Returns the memo for a received note. -/// -/// The note is identified by its row index in the `sapling_received_notes` table within the wdb -/// database. -pub(crate) fn get_received_memo

( - wdb: &WalletDb

, - id_note: i64, -) -> Result, SqliteClientError> { - let memo_bytes: Option> = wdb.conn.query_row( - "SELECT memo FROM sapling_received_notes - WHERE id_note = ?", - [id_note], - |row| row.get(0), - )?; +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)?; - memo_bytes - .map(|b| { - MemoBytes::from_bytes(&b) - .and_then(Memo::try_from) - .map_err(SqliteClientError::from) - }) - .transpose() + 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) + }, + )) } -/// Looks up a transaction by its internal database identifier. -pub(crate) fn get_transaction( - wdb: &WalletDb

, - id_tx: i64, -) -> Result { - let (tx_bytes, block_height): (Vec<_>, BlockHeight) = wdb.conn.query_row( - "SELECT raw, block FROM transactions - WHERE id_tx = ?", - [id_tx], +#[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 h: u32 = row.get(1)?; - Ok((row.get(0)?, BlockHeight::from(h))) + 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, + )) }, - )?; - - Transaction::read( - &tx_bytes[..], - BranchId::for_height(&wdb.params, block_height), ) + .optional() .map_err(SqliteClientError::from) + .and_then(|meta_row| meta_row.map(|r| parse_block_metadata(params, r)).transpose()) } -/// Returns the memo for a sent note. -/// -/// The note is identified by its row index in the `sent_notes` table within the wdb -/// database. -pub(crate) fn get_sent_memo

( - wdb: &WalletDb

, - id_note: i64, -) -> Result, SqliteClientError> { - let memo_bytes: Option> = wdb.conn.query_row( - "SELECT memo FROM sent_notes - WHERE id_note = ?", - [id_note], - |row| row.get(0), - )?; +#[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)?); - memo_bytes - .map(|b| { - MemoBytes::from_bytes(&b) - .and_then(Memo::try_from) - .map_err(SqliteClientError::from) - }) - .transpose() + // 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) + } } -/// Returns the minimum and maximum heights for blocks stored in the wallet database. -pub(crate) fn block_height_extrema

( - wdb: &WalletDb

, -) -> Result, rusqlite::Error> { - wdb.conn - .query_row("SELECT MIN(height), MAX(height) FROM blocks", [], |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), - ))) - }) - //.optional() doesn't work here because a failed aggregate function - //produces a runtime error, not an empty set of rows. - .or(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. -pub(crate) 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.as_ref().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. -pub(crate) 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() } -/// Gets the height to which the database must be truncated if any truncation that would remove a -/// number of blocks greater than the pruning height is attempted. -pub(crate) fn get_min_unspent_height

( - wdb: &WalletDb

, -) -> Result, SqliteClientError> { - wdb.conn - .query_row( - "SELECT MIN(tx.block) - FROM sapling_received_notes n - JOIN transactions tx ON tx.id_tx = n.tx - WHERE n.spent IS NULL", - [], - |row| { - row.get(0) - .map(|maybe_height: Option| maybe_height.map(|height| height.into())) +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( + conn: &rusqlite::Transaction, + params: &P, + sent_tx: &SentTransaction, +) -> Result<(), SqliteClientError> { + let tx_ref = put_tx_data( + conn, + sent_tx.tx(), + Some(sent_tx.fee_amount()), + Some(sent_tx.created()), + Some(sent_tx.target_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(conn, 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(conn, tx_ref, action.nullifier())?; + } + } + + #[cfg(not(feature = "orchard"))] + panic!("Sent a transaction with Orchard Actions without `orchard` enabled?"); + } + + #[cfg(feature = "transparent-inputs")] + for utxo_outpoint in sent_tx.utxos_spent() { + transparent::mark_transparent_utxo_spent(conn, tx_ref, utxo_outpoint)?; + } + + for output in sent_tx.outputs() { + insert_sent_output(conn, params, tx_ref, *sent_tx.account_id(), output)?; + + match output.recipient() { + Recipient::External { + recipient_address: _addr, + output_pool: _pool, + .. + } => { + // Nothing to do for external recipients. + } + Recipient::InternalAccount { + receiving_account, + note, + .. + } => match note.as_ref() { + Note::Sapling(note) => { + sapling::put_received_note( + conn, + params, + &DecryptedOutput::new( + output.output_index(), + note.clone(), + *receiving_account, + output + .memo() + .map_or_else(MemoBytes::empty, |memo| memo.clone()), + TransferType::WalletInternal, + ), + tx_ref, + Some(sent_tx.target_height()), + None, + )?; + } + #[cfg(feature = "orchard")] + Note::Orchard(note) => { + orchard::put_received_note( + conn, + params, + &DecryptedOutput::new( + output.output_index(), + *note, + *receiving_account, + output + .memo() + .map_or_else(MemoBytes::empty, |memo| memo.clone()), + TransferType::WalletInternal, + ), + tx_ref, + Some(sent_tx.target_height()), + None, + )?; + } }, - ) - .map_err(SqliteClientError::from) + #[cfg(feature = "transparent-inputs")] + Recipient::EphemeralTransparent { + ephemeral_address, + outpoint, + .. + } => { + // First check to verify that creation of this output does not result in reuse of + // an ephemeral address. + transparent::check_ephemeral_address_reuse(conn, params, ephemeral_address)?; + + transparent::put_transparent_output( + conn, + ¶ms, + outpoint, + &TxOut { + value: output.value(), + script_pubkey: ephemeral_address.script(), + }, + None, + ephemeral_address, + true, + )?; + } + } + } + + // 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(conn, std::iter::once(sent_tx.tx().txid()), None)?; + } + + Ok(()) +} + +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 the given height. +/// Truncates the database to at most 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. +/// +/// Returns the block height to which the database was truncated. pub(crate) fn truncate_to_height( - wdb: &WalletDb

, + conn: &rusqlite::Transaction, + params: &P, + #[cfg(feature = "transparent-inputs")] gap_limits: &GapLimits, + 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, + }) + }, + |h| Ok(BlockHeight::from(h)), + )?; + + let last_scanned_height = conn.query_row("SELECT MAX(height) FROM blocks", [], |row| { + let h = row.get::<_, Option>(0)?; + + Ok(h.map_or_else( + || { + params + .activation_height(NetworkUpgrade::Sapling) + .expect("Sapling activation height must be available.") + - 1 + }, + BlockHeight::from, + )) + })?; + + // 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)], + )?; + + // 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(), + clock: (), + rng: (), + #[cfg(feature = "transparent-inputs")] + gap_limits: *gap_limits, + }; + 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. +#[allow(clippy::too_many_arguments)] +pub(crate) 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 sapling_activation_height = wdb - .params - .activation_height(NetworkUpgrade::Sapling) - .expect("Sapling activation height mutst be available."); - - // Recall where we synced up to previously. - let last_scanned_height = wdb - .conn - .query_row("SELECT MAX(height) FROM blocks", [], |row| { - row.get(0) - .map(|h: u32| h.into()) - .or_else(|_| Ok(sapling_activation_height - 1)) + 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, + #[cfg(feature = "transparent-inputs")] gap_limits: &GapLimits, +) -> 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; + + #[cfg(feature = "transparent-inputs")] + let mut receiving_accounts = BTreeMap::new(); + + 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, + params, + output, + tx_ref, + d_tx.mined_height(), + 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 => { + let _account_id = sapling::put_received_note( + conn, + params, + output, + tx_ref, + d_tx.mined_height(), + None, + )?; + + #[cfg(feature = "transparent-inputs")] + receiving_accounts.insert(_account_id, KeyScope::EXTERNAL); + + 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, + params, + output, + tx_ref, + d_tx.mined_height(), + 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 => { + let _account_id = orchard::put_received_note( + conn, + params, + output, + tx_ref, + d_tx.mined_height(), + None, + )?; + + #[cfg(feature = "transparent-inputs")] + receiving_accounts.insert(_account_id, KeyScope::EXTERNAL); + + 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) + ); + + // If the output belongs to the wallet, add it to `transparent_received_outputs`. + #[cfg(feature = "transparent-inputs")] + if let Some((account_uuid, key_scope)) = + transparent::find_account_uuid_for_transparent_address(conn, params, &address)? + { + debug!( + "{:?} output {} belongs to account {:?}", + d_tx.tx().txid(), + output_index, + account_uuid + ); + let (account_id, _, _) = 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, + false, + )?; + + receiving_accounts.insert(account_id, key_scope); + + // 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, + }; - if block_height < last_scanned_height - PRUNING_HEIGHT { - if let Some(h) = get_min_unspent_height(wdb)? { - if block_height > h { - return Err(SqliteClientError::RequestedRewindInvalid(h, block_height)); + 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 + ); } } } - // nothing to do if we're deleting back down to the max height - if block_height < last_scanned_height { - // Decrement witnesses. - wdb.conn.execute( - "DELETE FROM sapling_witnesses WHERE block > ?", - [u32::from(block_height)], - )?; - - // Rewind received notes - wdb.conn.execute( - "DELETE FROM sapling_received_notes - WHERE id_note IN ( - SELECT rn.id_note - FROM sapling_received_notes rn - LEFT OUTER JOIN transactions tx - ON tx.id_tx = rn.tx - WHERE tx.block IS NOT NULL AND tx.block > ? - );", - [u32::from(block_height)], + #[cfg(feature = "transparent-inputs")] + for (account_id, key_scope) in receiving_accounts { + use ReceiverRequirement::*; + transparent::generate_gap_addresses( + conn, + params, + account_id, + key_scope, + gap_limits, + UnifiedAddressRequest::unsafe_custom(Allow, Allow, Require), + false, )?; + } - // 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. - - // Rewind utxos - wdb.conn.execute( - "DELETE FROM utxos WHERE height > ?", - [u32::from(block_height)], - )?; + // 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)? + } - // Un-mine transactions. - wdb.conn.execute( - "UPDATE transactions SET block = NULL, tx_index = NULL WHERE block IS NOT NULL AND block > ?", - [u32::from(block_height)], - )?; + notify_tx_retrieved(conn, d_tx.tx().txid())?; - // Now that they aren't depended on, delete scanned blocks. - wdb.conn.execute( - "DELETE FROM blocks WHERE height > ?", - [u32::from(block_height)], - )?; - } + // 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(()) } -/// Returns unspent transparent outputs that have been received by this wallet at the given -/// transparent address, such that the block that included the transaction was mined at a -/// height less than or equal to the provided `max_height`. -#[cfg(feature = "transparent-inputs")] -pub(crate) fn get_unspent_transparent_outputs( - wdb: &WalletDb

, - address: &TransparentAddress, - max_height: BlockHeight, - exclude: &[OutPoint], -) -> Result, SqliteClientError> { - let mut stmt_blocks = wdb.conn.prepare( - "SELECT u.prevout_txid, u.prevout_idx, u.script, - u.value_zat, u.height, tx.block as block - FROM utxos u - LEFT OUTER JOIN transactions tx - ON tx.id_tx = u.spent_in_tx - WHERE u.address = ? - AND u.height <= ? - AND tx.block IS NULL", +/// Inserts information about a mined transaction that was observed to +/// contain a note related to this wallet into the database. +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, 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", )?; - let addr_str = address.encode(&wdb.params); + 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"), + ]; - let mut utxos = Vec::::new(); - let mut rows = stmt_blocks.query(params![addr_str, u32::from(max_height)])?; - let excluded: BTreeSet = exclude.iter().cloned().collect(); - while let Some(row) = rows.next()? { - let txid: Vec = row.get(0)?; - let mut txid_bytes = [0u8; 32]; - txid_bytes.copy_from_slice(&txid); - - let index: u32 = row.get(1)?; - let script_pubkey = Script(row.get(2)?); - let value = Amount::from_i64(row.get(3)?).unwrap(); - let height: u32 = row.get(4)?; - - let outpoint = OutPoint::new(txid_bytes, index); - if excluded.contains(&outpoint) { - continue; - } + stmt_upsert_tx_meta + .query_row(tx_params, |row| row.get::<_, i64>(0).map(TxRef)) + .map_err(SqliteClientError::from) +} - let output = WalletTransparentOutput::from_parts( - outpoint, - TxOut { - value, - script_pubkey, - }, - BlockHeight::from(height), - ) - .ok_or_else(|| { - SqliteClientError::CorruptedData( - "Txout script_pubkey value did not correspond to a P2PKH or P2SH address" - .to_string(), +/// 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 + AND key_scope = :key_scope", + )?; - utxos.push(output); - } + let mut result = stmt.query(named_params! { + ":account_uuid": account.0, + ":key_scope": KeyScope::EXTERNAL.encode(), + })?; + 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(utxos) + Ok(None) + } + } } -/// Returns the unspent balance for each transparent address associated with the specified account, -/// such that the block that included the transaction was mined at a height less than or equal to -/// the provided `max_height`. -#[cfg(feature = "transparent-inputs")] -pub(crate) fn get_transparent_balances( - wdb: &WalletDb

, - account: AccountId, - max_height: BlockHeight, -) -> Result, SqliteClientError> { - let mut stmt_blocks = wdb.conn.prepare( - "SELECT u.address, SUM(u.value_zat) - FROM utxos u - LEFT OUTER JOIN transactions tx - ON tx.id_tx = u.spent_in_tx - WHERE u.received_by_account = ? - AND u.height <= ? - AND tx.block IS NULL - GROUP BY u.address", +/// Inserts full transaction data into the database. +pub(crate) fn put_tx_data( + conn: &rusqlite::Connection, + tx: &Transaction, + fee: Option, + created_at: Option, + 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 mut res = HashMap::new(); - let mut rows = stmt_blocks.query(params![u32::from(account), u32::from(max_height)])?; - while let Some(row) = rows.next()? { - let taddr_str: String = row.get(0)?; - let taddr = TransparentAddress::decode(&wdb.params, &taddr_str)?; - let value = Amount::from_i64(row.get(1)?).unwrap(); + let txid = tx.txid(); + let mut raw_tx = vec![]; + tx.write(&mut raw_tx)?; - res.insert(taddr, value); - } + 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(res) + stmt_upsert_tx_data + .query_row(tx_params, |row| row.get::<_, i64>(0).map(TxRef)) + .map_err(SqliteClientError::from) } -/// Inserts information about a scanned block into the database. -pub(crate) fn insert_block<'a, P>( - stmts: &mut DataConnStmtCache<'a, P>, - block_height: BlockHeight, - block_hash: BlockHash, - block_time: u32, - commitment_tree: &CommitmentTree, -) -> Result<(), SqliteClientError> { - stmts.stmt_insert_block(block_height, block_hash, block_time, commitment_tree) +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum TxQueryType { + Status, + Enhancement, } -/// Inserts information about a mined transaction that was observed to -/// contain a note related to this wallet into the database. -pub(crate) fn put_tx_meta<'a, P, N>( - stmts: &mut DataConnStmtCache<'a, P>, - tx: &WalletTx, - height: BlockHeight, -) -> Result { - if !stmts.stmt_update_tx_meta(height, tx.index, &tx.txid)? { - // It isn't there, so insert our transaction into the database. - stmts.stmt_insert_tx_meta(&tx.txid, height, tx.index) - } else { - // It was there, so grab its row number. - stmts.stmt_select_tx_ref(&tx.txid) +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, + } } } -/// Inserts full transaction data into the database. -pub(crate) fn put_tx_data<'a, P>( - stmts: &mut DataConnStmtCache<'a, P>, - tx: &Transaction, - fee: Option, - created_at: Option, -) -> Result { - let txid = tx.txid(); +#[cfg(feature = "transparent-inputs")] +pub(crate) fn queue_transparent_input_retrieval( + conn: &rusqlite::Transaction<'_>, + tx_ref: TxRef, + d_tx: &DecryptedTransaction<'_, AccountId>, +) -> Result<(), SqliteClientError> { + 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), + )?; + } - let mut raw_tx = vec![]; - tx.write(&mut raw_tx)?; + Ok(()) +} - if !stmts.stmt_update_tx_data(tx.expiry_height(), &raw_tx, fee, &txid)? { - // It isn't there, so insert our transaction into the database. - stmts.stmt_insert_tx_data(&txid, created_at, tx.expiry_height(), &raw_tx, fee) - } else { - // It was there, so grab its row number. - stmts.stmt_select_tx_ref(&txid) +#[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(); + + if d_tx.mined_height().is_none() && !detectable_via_scanning { + queue_tx_retrieval(conn, std::iter::once(d_tx.tx().txid()), None)? } + + Ok(()) } -/// Marks the given UTXO as having been spent. -#[cfg(feature = "transparent-inputs")] -pub(crate) fn mark_transparent_utxo_spent<'a, P>( - stmts: &mut DataConnStmtCache<'a, P>, - tx_ref: i64, - outpoint: &OutPoint, +pub(crate) fn queue_tx_retrieval( + conn: &rusqlite::Transaction<'_>, + txids: impl Iterator, + dependent_tx_ref: Option, ) -> Result<(), SqliteClientError> { - stmts.stmt_mark_transparent_utxo_spent(tx_ref, outpoint)?; + // 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)", + )?; + + 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(()) } -/// Adds the given received UTXO to the datastore. -#[cfg(feature = "transparent-inputs")] -pub(crate) fn put_received_transparent_utxo<'a, P: consensus::Parameters>( - stmts: &mut DataConnStmtCache<'a, P>, - output: &WalletTransparentOutput, -) -> Result { - stmts - .stmt_update_received_transparent_utxo(output) - .transpose() - .or_else(|| { - stmts - .stmt_insert_received_transparent_utxo(output) - .transpose() - }) - .unwrap_or_else(|| { - // This could occur if the UTXO is received at the legacy transparent - // address, in which case the join to the `addresses` table will fail. - // In this case, we should look up the legacy address for account 0 and - // check whether it matches the address for the received UTXO, and if - // so then insert/update it directly. - let account = AccountId::from(0u32); - get_legacy_transparent_address(&stmts.wallet_db.params, &stmts.wallet_db.conn, account) - .and_then(|legacy_taddr| { - if legacy_taddr - .iter() - .any(|(taddr, _)| taddr == output.recipient_address()) - { - stmts - .stmt_update_legacy_transparent_utxo(output, account) - .transpose() - .unwrap_or_else(|| { - stmts.stmt_insert_legacy_transparent_utxo(output, account) - }) - } else { - Err(SqliteClientError::AddressNotRecognized( - *output.recipient_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, +) -> 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) } -/// Removes old incremental witnesses up to the given block height. -pub(crate) fn prune_witnesses

( - stmts: &mut DataConnStmtCache<'_, P>, - below_height: BlockHeight, +pub(crate) fn notify_tx_retrieved( + conn: &rusqlite::Transaction<'_>, + txid: TxId, ) -> Result<(), SqliteClientError> { - stmts.stmt_prune_witnesses(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(crate) 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(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": KeyScope::INTERNAL.encode() + }, + ) + }; + + flag_received_change(SAPLING_TABLES_PREFIX)?; + #[cfg(feature = "orchard")] + flag_received_change(ORCHARD_TABLES_PREFIX)?; + + Ok(()) } /// Records information about a transaction output that your wallet created. -/// -/// This is a crate-internal convenience method. -pub(crate) fn insert_sent_output<'a, P: consensus::Parameters>( - stmts: &mut DataConnStmtCache<'a, P>, - tx_ref: i64, - from_account: AccountId, - output: &SentTransactionOutput, +pub(crate) fn insert_sent_output( + conn: &rusqlite::Transaction, + params: &P, + tx_ref: TxRef, + from_account_uuid: AccountUuid, + output: &SentTransactionOutput, ) -> Result<(), SqliteClientError> { - stmts.stmt_insert_sent_output( - tx_ref, - output.output_index(), - from_account, - output.recipient(), - output.value(), - output.memo(), - ) + 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)", + )?; + + 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(()) } -/// Records information about a transaction output that your wallet created. +/// Records information about a transaction output that your wallet created, from the constituent +/// properties of that output. /// -/// This is a crate-internal convenience method. +/// - 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. +/// - 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<'a, P: consensus::Parameters>( - stmts: &mut DataConnStmtCache<'a, P>, - from_account: AccountId, - tx_ref: i64, +pub(crate) fn put_sent_output( + conn: &rusqlite::Transaction, + params: &P, + from_account_uuid: AccountUuid, + tx_ref: TxRef, output_index: usize, - recipient: &Recipient, - value: Amount, + recipient: &Recipient, + value: Zatoshis, memo: Option<&MemoBytes>, ) -> Result<(), SqliteClientError> { - if !stmts.stmt_update_sent_output(from_account, recipient, value, memo, tx_ref, output_index)? { - stmts.stmt_insert_sent_output( - tx_ref, - output_index, - from_account, - recipient, - value, - memo, - )?; - } + 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 secrecy::Secret; - 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", + )?; - use zcash_primitives::transaction::components::Amount; + 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(), + ]; - use zcash_client_backend::data_api::WalletRead; + // 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)), + } + })?; - use crate::{ - tests, - wallet::{get_current_address, init::init_wallet_db}, - AccountId, WalletDb, + 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), }; - use super::get_balance; + // 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) +} - #[cfg(feature = "transparent-inputs")] - use { - zcash_client_backend::{ - data_api::WalletWrite, encoding::AddressCodec, wallet::WalletTransparentOutput, +/// 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(()) +} + +pub(crate) fn get_block_range( + conn: &rusqlite::Connection, + protocol: ShieldedProtocol, + commitment_tree_address: incrementalmerkletree::Address, +) -> Result>, SqliteClientError> { + let prefix = match protocol { + ShieldedProtocol::Sapling => "sapling", + ShieldedProtocol::Orchard => "orchard", + }; + let mut stmt = conn.prepare_cached(&format!( + "SELECT MIN(height), MAX(height), MAX({prefix}_commitment_tree_size) + FROM blocks + WHERE {prefix}_commitment_tree_size BETWEEN :min_tree_size AND :max_tree_size" + ))?; + + stmt.query_row( + // BETWEEN is inclusive on both ends. However, we are comparing commitment tree sizes + // to commitment tree positions, so we must add one to the start, and we do not subtract + // one from the end. + named_params! { + ":min_tree_size": u64::from(commitment_tree_address.position_range_start()) + 1, + ":max_tree_size": u64::from(commitment_tree_address.position_range_end()), }, - zcash_primitives::{ - consensus::BlockHeight, - transaction::components::{OutPoint, TxOut}, + |row| { + // The first block to be scanned is known to contain the start of the address range in + // question because the tree size we compared against is measured as of the end of the + // block. + let min_height = row.get::<_, Option>(0)?.map(BlockHeight::from_u32); + let max_height_inclusive = row.get::<_, Option>(1)?.map(BlockHeight::from_u32); + let end_offset = row.get::<_, Option>(2)?.map(|max_height_tree_size| { + // If the tree size at the end of the max-height block is less than the + // end-exclusive maximum position of the address range, this means that the end of + // the subtree referred to by that address is somewhere in the next block, so we + // need to rescan an extra block to ensure that we have observed all of the note + // commitments that aggregate up to that address. + if max_height_tree_size < u64::from(commitment_tree_address.position_range_end()) { + 1 + } else { + 0 + } + }); + + Ok(min_height + .zip(max_height_inclusive) + .zip(end_offset) + .map(|((min, max_inclusive), offset)| min..(max_inclusive + offset + 1))) }, + ) + .map_err(SqliteClientError::from) +} + +#[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, }; - #[test] - fn empty_database_has_no_balance() { - let data_file = NamedTempFile::new().unwrap(); - let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap(); + use crate::{error::SqliteClientError, AccountUuid, SAPLING_TABLES_PREFIX}; - // Add an account to the wallet - tests::init_test_accounts_table(&db_data); + #[cfg(feature = "orchard")] + use crate::ORCHARD_TABLES_PREFIX; - // The account should be empty - assert_eq!( - get_balance(&db_data, AccountId::from(0)).unwrap(), - Amount::zero() - ); + 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", + )?; - // We can't get an anchor height, as we have not scanned any blocks. - assert_eq!(db_data.get_target_and_anchor_heights(10).unwrap(), None); + 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")?)?, + Zatoshis::from_nonnegative_i64(row.get("total_spent")?)?, + Zatoshis::from_nonnegative_i64(row.get("total_received")?)?, + 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::, _>>()?; - // An invalid account has zero balance - assert_matches!(get_current_address(&db_data, AccountId::from(1)), Ok(None)); - assert_eq!( - get_balance(&db_data, AccountId::from(0)).unwrap(), - Amount::zero() - ); + 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_keys::keys::UnifiedAddressRequest; + use zcash_primitives::block::BlockHash; + use zcash_protocol::value::Zatoshis; + + use crate::{ + error::SqliteClientError, + testing::{db::TestDbFactory, BlockCache}, + AccountUuid, + }; + + use super::account_birthday; #[test] - #[cfg(feature = "transparent-inputs")] - fn put_received_transparent_utxo() { - let data_file = NamedTempFile::new().unwrap(); - let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&mut db_data, None).unwrap(); - - // Add an account to the wallet - let mut ops = db_data.get_update_ops().unwrap(); - let seed = Secret::new([0u8; 32].to_vec()); - let (account_id, _usk) = ops.create_account(&seed).unwrap(); - let uaddr = db_data.get_current_address(account_id).unwrap().unwrap(); - let taddr = uaddr.transparent().unwrap(); - - let bal_absent = db_data - .get_transparent_balances(account_id, BlockHeight::from_u32(12345)) - .unwrap(); - assert!(bal_absent.is_empty()); + fn empty_database_has_no_balance() { + 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(); - let utxo = WalletTransparentOutput::from_parts( - OutPoint::new([1u8; 32], 1), - TxOut { - value: Amount::from_u64(100000).unwrap(), - script_pubkey: taddr.script(), - }, - BlockHeight::from_u32(12345), - ) - .unwrap(); - - let res0 = super::put_received_transparent_utxo(&mut ops, &utxo); - assert_matches!(res0, Ok(_)); - - // Change the mined height of the UTXO and upsert; we should get back - // the same utxoid - let utxo2 = WalletTransparentOutput::from_parts( - OutPoint::new([1u8; 32], 1), - TxOut { - value: Amount::from_u64(100000).unwrap(), - script_pubkey: taddr.script(), - }, - BlockHeight::from_u32(34567), - ) - .unwrap(); - let res1 = super::put_received_transparent_utxo(&mut ops, &utxo2); - assert_matches!(res1, Ok(id) if id == res0.unwrap()); + // 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!( + st.wallet() + .get_target_and_anchor_heights(NonZeroU32::new(10).unwrap()) + .unwrap(), + None + ); + // The default address is set for the test account assert_matches!( - super::get_unspent_transparent_outputs( - &db_data, - taddr, - BlockHeight::from_u32(12345), - &[] + st.wallet().get_last_generated_address_matching( + account.id(), + UnifiedAddressRequest::AllAvailableKeys ), - Ok(utxos) if utxos.is_empty() + Ok(Some(_)) ); + // No default address is set for an un-initialized account assert_matches!( - super::get_unspent_transparent_outputs( - &db_data, - taddr, - BlockHeight::from_u32(34567), - &[] + st.wallet().get_last_generated_address_matching( + AccountUuid(Uuid::nil()), + UnifiedAddressRequest::AllAvailableKeys ), - Ok(utxos) if { - utxos.len() == 1 && - utxos.iter().any(|rutxo| rutxo.height() == utxo2.height()) - } + Err(SqliteClientError::AccountUnknown) ); + } + + #[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!( - db_data.get_transparent_balances(account_id, BlockHeight::from_u32(34567)), - Ok(h) if h.get(taddr) == Amount::from_u64(100000).ok().as_ref() + account_parameters.kind, + AccountSource::Derived{derivation, ..} if derivation.account_index() == expected_account_index ); + } - // Artificially delete the address from the addresses table so that - // we can ensure the update fails if the join doesn't work. - db_data - .conn - .execute( - "DELETE FROM addresses WHERE cached_transparent_receiver_address = ?", - [Some(taddr.encode(&db_data.params))], - ) + #[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(); - let res2 = super::put_received_transparent_utxo(&mut ops, &utxo2); - assert_matches!(res2, Err(_)); + 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()) + } + + 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..5b4fd0bbc8 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/commitment_tree.rs @@ -0,0 +1,1378 @@ +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::{QueryError, 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, ShieldedProtocol}; + +use crate::{error::SqliteClientError, sapling_tree}; + +#[cfg(feature = "orchard")] +use crate::orchard_tree; + +/// 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(()) +} + +pub(crate) fn check_witnesses( + conn: &rusqlite::Transaction<'_>, +) -> Result>, SqliteClientError> { + let chain_tip_height = + super::chain_tip_height(conn)?.ok_or(SqliteClientError::ChainHeightUnknown)?; + let wallet_birthday = super::wallet_birthday(conn)?.ok_or(SqliteClientError::AccountUnknown)?; + let unspent_sapling_note_meta = + super::sapling::select_unspent_note_meta(conn, chain_tip_height, wallet_birthday)?; + + let mut scan_ranges = vec![]; + let mut sapling_incomplete = vec![]; + let sapling_tree = sapling_tree(conn)?; + for m in unspent_sapling_note_meta.iter() { + match sapling_tree.witness_at_checkpoint_depth(m.commitment_tree_position(), 0) { + Ok(_) => {} + Err(ShardTreeError::Query(QueryError::TreeIncomplete(mut addrs))) => { + sapling_incomplete.append(&mut addrs); + } + Err(other) => { + return Err(SqliteClientError::CommitmentTree(other)); + } + } + } + + for addr in sapling_incomplete { + let range = super::get_block_range(conn, ShieldedProtocol::Sapling, addr)?; + scan_ranges.extend(range.into_iter()); + } + + #[cfg(feature = "orchard")] + { + let unspent_orchard_note_meta = + super::orchard::select_unspent_note_meta(conn, chain_tip_height, wallet_birthday)?; + let mut orchard_incomplete = vec![]; + let orchard_tree = orchard_tree(conn)?; + for m in unspent_orchard_note_meta.iter() { + match orchard_tree.witness_at_checkpoint_depth(m.commitment_tree_position(), 0) { + Ok(_) => {} + Err(ShardTreeError::Query(QueryError::TreeIncomplete(mut addrs))) => { + orchard_incomplete.append(&mut addrs); + } + Err(other) => { + return Err(SqliteClientError::CommitmentTree(other)); + } + } + } + + for addr in orchard_incomplete { + let range = super::get_block_range(conn, ShieldedProtocol::Orchard, addr)?; + scan_ranges.extend(range.into_iter()); + } + } + + Ok(scan_ranges) +} + +#[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::{ + db::{test_clock, test_rng}, + pool::ShieldedPoolPersistence, + }, + wallet::init::WalletMigrator, + 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, + test_clock(), + test_rng(), + ) + .unwrap(); + data_file.keep().unwrap(); + + WalletMigrator::new().init_or_migrate(&mut db_data).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, + test_clock(), + test_rng(), + ) + .unwrap(); + data_file.keep().unwrap(); + + WalletMigrator::new().init_or_migrate(&mut db_data).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..cb74f7e77e --- /dev/null +++ b/zcash_client_sqlite/src/wallet/common.rs @@ -0,0 +1,548 @@ +//! Functions common to Sapling and Orchard support in the wallet. + +use incrementalmerkletree::Position; +use rusqlite::{named_params, types::Value, Connection, Row}; +use std::{num::NonZeroU64, rc::Rc}; + +use zcash_client_backend::{ + data_api::{NoteFilter, PoolMeta, TargetValue}, + 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: TargetValue, + anchor_height: BlockHeight, + exclude: &[ReceivedNoteId], + protocol: ShieldedProtocol, + to_spendable_note: F, +) -> Result>, SqliteClientError> +where + F: Fn(&P, &Row) -> Result>, SqliteClientError>, +{ + match target_value { + TargetValue::AtLeast(zats) => select_minimum_spendable_notes( + conn, + params, + account, + zats, + anchor_height, + exclude, + protocol, + to_spendable_note, + ), + } +} + +#[allow(clippy::too_many_arguments)] +fn select_minimum_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::>() +} + +#[allow(dead_code)] +pub(crate) struct UnspentNoteMeta { + note_id: ReceivedNoteId, + txid: TxId, + output_index: u32, + commitment_tree_position: Position, + value: Zatoshis, +} + +#[allow(dead_code)] +impl UnspentNoteMeta { + pub(crate) fn note_id(&self) -> ReceivedNoteId { + self.note_id + } + + pub(crate) fn txid(&self) -> TxId { + self.txid + } + + pub(crate) fn output_index(&self) -> u32 { + self.output_index + } + + pub(crate) fn commitment_tree_position(&self) -> Position { + self.commitment_tree_position + } + + pub(crate) fn value(&self) -> Zatoshis { + self.value + } +} + +pub(crate) fn select_unspent_note_meta( + conn: &rusqlite::Connection, + protocol: ShieldedProtocol, + chain_tip_height: BlockHeight, + wallet_birthday: BlockHeight, +) -> Result, SqliteClientError> { + let (table_prefix, index_col, _) = per_protocol_names(protocol); + // This query is effectively the same as the internal `eligible` subquery + // used in `select_spendable_notes`. + // + // TODO: Deduplicate this in the future by introducing a view? + let mut stmt = conn.prepare_cached(&format!(" + SELECT {table_prefix}_received_notes.id AS id, txid, {index_col}, + commitment_tree_position, value + FROM {table_prefix}_received_notes + INNER JOIN transactions + ON transactions.id_tx = {table_prefix}_received_notes.tx + WHERE value > 5000 -- FIXME #1316, allow selection of dust inputs + AND recipient_key_scope IS NOT NULL + AND nf IS NOT NULL + AND commitment_tree_position IS NOT NULL + 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 + ) + "))?; + + let res = stmt + .query_and_then::<_, SqliteClientError, _, _>( + named_params![ + ":anchor_height": u32::from(chain_tip_height), + ":wallet_birthday": u32::from(wallet_birthday), + ], + |row| { + Ok(UnspentNoteMeta { + note_id: row.get("id").map(|id| ReceivedNoteId(protocol, id))?, + txid: row.get("txid").map(TxId::from_bytes)?, + output_index: row.get(index_col)?, + commitment_tree_position: row + .get::<_, u64>("commitment_tree_position") + .map(Position::from)?, + value: Zatoshis::from_nonnegative_i64(row.get("value")?)?, + }) + }, + )? + .collect::, _>>()?; + + Ok(res) +} + +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..3685951d8a --- /dev/null +++ b/zcash_client_sqlite/src/wallet/db.rs @@ -0,0 +1,1156 @@ +//! Documentation about the wallet database structure. +//! +//! The database structure is managed by [`crate::wallet::init::WalletMigrator`], 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 zcash_client_backend::data_api::scanning::ScanPriority; +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 addresses that have been generated from accounts in the wallet. +/// +/// ### Columns +/// +/// - `account_id`: the account whose IVK was used to derive this address. +/// - `diversifier_index_be`: the diversifier index at which this address was derived. +/// - `key_scope`: the key scope for which this address was derived. +/// - `address`: The Unified, Sapling, or transparent address. For Unified and Sapling addresses, +/// only external-key scoped addresses should be stored in this table; for purely transparent +/// addresses, this may be an internal-scope (change) address, so that we can provide +/// compatibility with HD-derived change addresses produced by transparent-only wallets. +/// - `transparent_child_index`: the diversifier index in integer form, if it is in the range of a `u31` +/// (i.e. a non-hardened transparent address index). It is used for gap limit handling, and is set +/// whenever a transparent address at a given index should be scanned at receive time. This +/// includes: +/// - Unified Addresses with transparent receivers (at any valid index). +/// - Unified Addresses without transparent receivers, but within the gap limit of potential +/// sequential transparent addresses. +/// - Transparent change addresses. +/// - ZIP 320 ephemeral addresses. +/// +/// This column exists because the diversifier index is stored as a byte array, meaning that we +/// cannot use SQL integer operations on it for gap limit calculations, and thus need it as an +/// integer as well. +/// - `cached_transparent_receiver_address`: the transparent address derived from the same +/// viewing key and at the same diversifier index as `address`. This may be the same as `address` +/// in the case of an internal-scope transparent change address or a ZIP 320 interstitial +/// address, and it may be a receiver within `address` in the case of a Unified Address with +/// transparent receiver. It is cached directly in the table to make account lookups for +/// transparent outputs more efficient, enabling joins to [`TABLE_TRANSPARENT_RECEIVED_OUTPUTS`]. +/// - `exposed_at_height`: Our best knowledge as to when this address was first exposed to the +/// wider ecosystem. +/// - For user-generated addresses, this is the chain tip height at the time that the address was +/// generated by an explicit request by the user or reserved for use in a ZIP 320 transaction. +/// These heights are not recoverable from chain. +/// - In the case of an address with its first use discovered in a transaction obtained by scanning +/// the chain, this will be set to the mined height of that transaction. In recover from seed +/// cases, this is what user-generated addresses will be assigned. +/// - `receiver_flags`: A set of bitflags that describes which receiver types are included in +/// `address`. See the documentation of [`ReceiverFlags`] for details. +/// - `transparent_receiver_next_check_time`: The Unix epoch time at which a client should next +/// check to determine whether any new UTXOs have been received by the cached transparent receiver +/// address. At present, this will ordinarily be populated only for ZIP 320 ephemeral addresses. +/// +/// [`ReceiverFlags`]: crate::wallet::encoding::ReceiverFlags +pub(super) const TABLE_ADDRESSES: &str = r#" +CREATE TABLE "addresses" ( + id INTEGER NOT NULL PRIMARY KEY, + account_id INTEGER NOT NULL, + key_scope INTEGER NOT NULL, + diversifier_index_be BLOB NOT NULL, + address TEXT NOT NULL, + transparent_child_index INTEGER, + cached_transparent_receiver_address TEXT, + exposed_at_height INTEGER, + receiver_flags INTEGER NOT NULL, + transparent_receiver_next_check_time INTEGER, + FOREIGN KEY (account_id) REFERENCES accounts(id), + CONSTRAINT diversification UNIQUE (account_id, key_scope, diversifier_index_be), + CONSTRAINT transparent_index_consistency CHECK ( + (transparent_child_index IS NOT NULL) == (cached_transparent_receiver_address IS NOT NULL) + ) +)"#; +pub(super) const INDEX_ADDRESSES_ACCOUNTS: &str = r#" +CREATE INDEX idx_addresses_accounts ON addresses ( + account_id ASC +)"#; +pub(super) const INDEX_ADDRESSES_INDICES: &str = r#" +CREATE INDEX idx_addresses_indices ON addresses ( + diversifier_index_be ASC +)"#; +pub(super) const INDEX_ADDRESSES_T_INDICES: &str = r#" +CREATE INDEX idx_addresses_t_indices ON addresses ( + transparent_child_index ASC +)"#; + +/// 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`]. +/// +/// ### Columns +/// - `tx`: a foreign key reference to the transaction that contained this output +/// - `output_index`: the index of this Sapling output in the transaction +/// - `account_id`: a foreign key reference to the account whose ivk decrypted this output +/// - `diversifier`: the diversifier used to construct the note +/// - `value`: the value of the note +/// - `rcm`: the random commitment trapdoor for the note +/// - `nf`: the nullifier that will be exposed when the note is spent +/// - `is_change`: a flag indicating whether the note was received in a transaction where +/// the receiving account also spent notes. +/// - `memo`: the memo output associated with the note, if known +/// - `commitment_tree_position`: the 0-based index of the note in the leaves of the note +/// commitment tree. +/// - `recipient_key_scope`: the ZIP 32 key scope of the key that decrypted this output, +/// encoded as `0` for external scope and `1` for internal scope. +/// - `address_id`: a foreign key to the address that this note was sent to; null in the +/// case that the note was sent to an internally-scoped address (we never store addresses +/// containing internal Sapling receivers in the `addresses` table). +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, + address_id INTEGER REFERENCES addresses(id), + 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`]. +/// +/// ### Columns +/// - `tx`: a foreign key reference to the transaction that contained this output +/// - `action_index`: the index of the Orchard action that produced this note in the transaction +/// - `account_id`: a foreign key reference to the account whose ivk decrypted this output +/// - `diversifier`: the diversifier used to construct the note +/// - `value`: the value of the note +/// - `rho`: the rho value used to derive the nullifier of the note +/// - `rseed`: the rseed value used to generate the note +/// - `nf`: the nullifier that will be exposed when the note is spent +/// - `is_change`: a flag indicating whether the note was received in a transaction where +/// the receiving account also spent notes. +/// - `memo`: the memo output associated with the note, if known +/// - `commitment_tree_position`: the 0-based index of the note in the leaves of the note +/// commitment tree. +/// - `recipient_key_scope`: the ZIP 32 key scope of the key that decrypted this output, +/// encoded as `0` for external scope and `1` for internal scope. +/// - `address_id`: a foreign key to the address that this note was sent to; null in the +/// case that the note was sent to an internally-scoped address (we never store addresses +/// containing internal Orchard receivers in the `addresses` table). +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, + address_id INTEGER REFERENCES addresses(id), + 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. +/// - `address_id`: a foreign key to the address that this note was sent to; non-null because +/// we can only find transparent outputs for known addresses (and therefore we must record +/// both internal and external addresses in the `addresses` table). +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, + address_id INTEGER NOT NULL REFERENCES addresses(id), + 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, + sapling_received_notes.address_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, + orchard_received_notes.address_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, + u.address_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, + ro.value AS received_value, + 0 AS spent_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, + 0 AS received_value, + ro.value AS spent_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, + SUM(notes.spent_value) AS total_spent, + SUM(notes.received_value) AS total_received, + 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"; + +pub(super) const VIEW_ADDRESS_USES: &str = " +CREATE VIEW v_address_uses AS + SELECT orn.address_id, orn.account_id, orn.tx AS transaction_id, t.mined_height, + a.key_scope, a.diversifier_index_be, a.transparent_child_index + FROM orchard_received_notes orn + JOIN addresses a ON a.id = orn.address_id + JOIN transactions t ON t.id_tx = orn.tx +UNION + SELECT srn.address_id, srn.account_id, srn.tx AS transaction_id, t.mined_height, + a.key_scope, a.diversifier_index_be, a.transparent_child_index + FROM sapling_received_notes srn + JOIN addresses a ON a.id = srn.address_id + JOIN transactions t ON t.id_tx = srn.tx +UNION + SELECT tro.address_id, tro.account_id, tro.transaction_id, t.mined_height, + a.key_scope, a.diversifier_index_be, a.transparent_child_index + FROM transparent_received_outputs tro + JOIN addresses a ON a.id = tro.address_id + JOIN transactions t ON t.id_tx = tro.transaction_id"; + +pub(super) const VIEW_ADDRESS_FIRST_USE: &str = " + CREATE VIEW v_address_first_use AS + SELECT + address_id, + account_id, + key_scope, + diversifier_index_be, + transparent_child_index, + MIN(mined_height) AS first_use_height + FROM v_address_uses + GROUP BY + address_id, account_id, key_scope, + diversifier_index_be, transparent_child_index"; diff --git a/zcash_client_sqlite/src/wallet/encoding.rs b/zcash_client_sqlite/src/wallet/encoding.rs new file mode 100644 index 0000000000..c576a90931 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/encoding.rs @@ -0,0 +1,290 @@ +//! Functions and types related to encoding and decoding wallet data for storage in the wallet +//! database. + +use bitflags::bitflags; +use transparent::address::TransparentAddress::*; +use zcash_address::{ + unified::{Container, Receiver}, + ConversionError, TryFromAddress, +}; +use zcash_client_backend::data_api::AccountSource; +use zcash_keys::{ + address::{Address, UnifiedAddress}, + keys::{ReceiverRequirement, ReceiverRequirements}, +}; +use zcash_protocol::{consensus::NetworkType, memo::MemoBytes, PoolType, ShieldedProtocol}; +use zip32::DiversifierIndex; + +use crate::error::SqliteClientError; + +#[cfg(feature = "transparent-inputs")] +use { + super::transparent::SchedulingError, std::time::SystemTime, + transparent::keys::TransparentKeyScope, +}; + +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, + } +} + +pub(crate) fn account_kind_code(value: &AccountSource) -> u32 { + match value { + AccountSource::Derived { .. } => 0, + AccountSource::Imported { .. } => 1, + } +} + +pub(crate) fn encode_diversifier_index_be(idx: DiversifierIndex) -> [u8; 11] { + let mut di_be = *idx.as_bytes(); + di_be.reverse(); + di_be +} + +pub(crate) fn decode_diversifier_index_be( + di_be: &[u8], +) -> Result { + let mut di_be: [u8; 11] = di_be.try_into().map_err(|_| { + SqliteClientError::CorruptedData("Diversifier index is not an 11-byte value".to_owned()) + })?; + di_be.reverse(); + + Ok(DiversifierIndex::from(di_be)) +} + +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() + } + }) +} + +#[cfg(feature = "transparent-inputs")] +pub(crate) fn epoch_seconds(t: SystemTime) -> Result { + let integer_seconds_since_epoch = + i64::try_from(t.duration_since(SystemTime::UNIX_EPOCH)?.as_secs())?; + + Ok(integer_seconds_since_epoch) +} + +#[cfg(feature = "transparent-inputs")] +pub(crate) fn decode_epoch_seconds(i: i64) -> Result { + use std::time::Duration; + + Ok(SystemTime::UNIX_EPOCH + Duration::from_secs(u64::try_from(i)?)) +} + +/// An enumeration of the scopes of keys that are generated by the `zcash_client_sqlite` +/// implementation of the `WalletWrite` trait. +/// +/// This extends the [`zip32::Scope`] type to include the custom scope used to generate keys for +/// ephemeral transparent addresses. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum KeyScope { + /// A key scope corresponding to a [`zip32::Scope`]. + Zip32(zip32::Scope), + /// An ephemeral transparent address, which is derived from an account's transparent + /// [`AccountPubKey`] with the BIP 44 path `change` level index set to the value `2`. + /// + /// [`AccountPubKey`]: zcash_primitives::legacy::keys::AccountPubKey + Ephemeral, +} + +impl KeyScope { + pub(crate) const EXTERNAL: KeyScope = KeyScope::Zip32(zip32::Scope::External); + pub(crate) const INTERNAL: KeyScope = KeyScope::Zip32(zip32::Scope::Internal); + + pub(crate) fn encode(&self) -> i64 { + match self { + KeyScope::Zip32(zip32::Scope::External) => 0i64, + KeyScope::Zip32(zip32::Scope::Internal) => 1i64, + KeyScope::Ephemeral => 2i64, + } + } + + pub(crate) fn decode(code: i64) -> Result { + match code { + 0i64 => Ok(KeyScope::EXTERNAL), + 1i64 => Ok(KeyScope::INTERNAL), + 2i64 => Ok(KeyScope::Ephemeral), + other => Err(SqliteClientError::CorruptedData(format!( + "Invalid key scope code: {}", + other + ))), + } + } +} + +impl From for KeyScope { + fn from(value: zip32::Scope) -> Self { + KeyScope::Zip32(value) + } +} + +#[cfg(feature = "transparent-inputs")] +impl From for TransparentKeyScope { + fn from(value: KeyScope) -> Self { + match value { + KeyScope::Zip32(scope) => scope.into(), + KeyScope::Ephemeral => TransparentKeyScope::custom(2).expect("valid scope"), + } + } +} + +impl TryFrom for zip32::Scope { + type Error = (); + + fn try_from(value: KeyScope) -> Result { + match value { + KeyScope::Zip32(scope) => Ok(scope), + KeyScope::Ephemeral => Err(()), + } + } +} + +bitflags! { + /// A set of flags describing the type(s) of outputs that a Zcash address can receive. + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub(crate) struct ReceiverFlags: i64 { + /// The address did not contain any recognized receiver types. + const UNKNOWN = 0b00000000; + /// The associated address can receive transparent p2pkh outputs. + const P2PKH = 0b00000001; + /// The associated address can receive transparent p2sh outputs. + const P2SH = 0b00000010; + /// The associated address can receive Sapling outputs. + const SAPLING = 0b00000100; + /// The associated address can receive Orchard outputs. + const ORCHARD = 0b00001000; + } +} + +impl ReceiverFlags { + pub(crate) fn required(request: ReceiverRequirements) -> Self { + let mut flags = ReceiverFlags::UNKNOWN; + if matches!(request.orchard(), ReceiverRequirement::Require) { + flags |= ReceiverFlags::ORCHARD; + } + if matches!(request.sapling(), ReceiverRequirement::Require) { + flags |= ReceiverFlags::SAPLING; + } + if matches!(request.p2pkh(), ReceiverRequirement::Require) { + flags |= ReceiverFlags::P2PKH; + } + flags + } + + pub(crate) fn omitted(request: ReceiverRequirements) -> Self { + let mut flags = ReceiverFlags::UNKNOWN; + if matches!(request.orchard(), ReceiverRequirement::Omit) { + flags |= ReceiverFlags::ORCHARD; + } + if matches!(request.sapling(), ReceiverRequirement::Omit) { + flags |= ReceiverFlags::SAPLING; + } + if matches!(request.p2pkh(), ReceiverRequirement::Omit) { + flags |= ReceiverFlags::P2PKH; + } + flags + } +} + +/// Computes the [`ReceiverFlags`] describing the types of outputs that the provided +/// [`UnifiedAddress`] can receive. +impl From<&UnifiedAddress> for ReceiverFlags { + fn from(value: &UnifiedAddress) -> Self { + let mut flags = ReceiverFlags::UNKNOWN; + match value.transparent() { + Some(PublicKeyHash(_)) => { + flags |= ReceiverFlags::P2PKH; + } + Some(ScriptHash(_)) => { + flags |= ReceiverFlags::P2SH; + } + _ => {} + } + if value.has_sapling() { + flags |= ReceiverFlags::SAPLING; + } + if value.has_orchard() { + flags |= ReceiverFlags::ORCHARD; + } + flags + } +} + +/// Computes the [`ReceiverFlags`] describing the types of outputs that the provided +/// [`Address`] can receive. +impl From<&Address> for ReceiverFlags { + fn from(address: &Address) -> Self { + match address { + Address::Sapling(_) => ReceiverFlags::SAPLING, + Address::Transparent(addr) => match addr { + PublicKeyHash(_) => ReceiverFlags::P2PKH, + ScriptHash(_) => ReceiverFlags::P2SH, + }, + Address::Unified(ua) => ReceiverFlags::from(ua), + Address::Tex(_) => ReceiverFlags::P2PKH, + } + } +} + +impl TryFromAddress for ReceiverFlags { + type Error = (); + + fn try_from_sapling( + _net: NetworkType, + _data: [u8; 43], + ) -> Result> { + Ok(ReceiverFlags::SAPLING) + } + + fn try_from_unified( + _net: NetworkType, + data: zcash_address::unified::Address, + ) -> Result> { + let mut result = ReceiverFlags::UNKNOWN; + for i in data.items() { + match i { + Receiver::Orchard(_) => result |= ReceiverFlags::ORCHARD, + Receiver::Sapling(_) => result |= ReceiverFlags::SAPLING, + Receiver::P2pkh(_) => result |= ReceiverFlags::P2PKH, + Receiver::P2sh(_) => result |= ReceiverFlags::P2SH, + Receiver::Unknown { .. } => {} + } + } + + Ok(result) + } + + fn try_from_transparent_p2pkh( + _net: NetworkType, + _data: [u8; 20], + ) -> Result> { + Ok(ReceiverFlags::P2PKH) + } + + fn try_from_transparent_p2sh( + _net: NetworkType, + _data: [u8; 20], + ) -> Result> { + Ok(ReceiverFlags::P2SH) + } + + fn try_from_tex( + _net: NetworkType, + _data: [u8; 20], + ) -> Result> { + Ok(ReceiverFlags::P2PKH) + } +} diff --git a/zcash_client_sqlite/src/wallet/init.rs b/zcash_client_sqlite/src/wallet/init.rs index 6650322915..afdbfb3999 100644 --- a/zcash_client_sqlite/src/wallet/init.rs +++ b/zcash_client_sqlite/src/wallet/init.rs @@ -1,39 +1,73 @@ //! Functions for initializing the various databases. -use std::collections::HashMap; + +use std::borrow::BorrowMut; use std::fmt; +use std::rc::Rc; -use rusqlite::{self, types::ToSql}; -use schemer::{Migrator, MigratorError}; -use schemer_rusqlite::RusqliteAdapter; +use rand_core::RngCore; +use regex::Regex; +use schemerz::{Migrator, MigratorError}; +use schemerz_rusqlite::{RusqliteAdapter, RusqliteMigration}; use secrecy::SecretVec; +use shardtree::error::ShardTreeError; use uuid::Uuid; -use zcash_primitives::{ - block::BlockHash, - consensus::{self, BlockHeight}, - transaction::components::amount::BalanceError, - zip32::AccountId, -}; +use zcash_client_backend::data_api::{SeedRelevance, WalletRead}; +use zcash_keys::keys::AddressGenerationError; +use zcash_protocol::{consensus, value::BalanceError}; + +use self::migrations::verify_network_compatibility; + +use super::commitment_tree; +use crate::{error::SqliteClientError, util::Clock, WalletDb}; -use zcash_client_backend::keys::UnifiedFullViewingKey; +pub mod migrations; -use crate::{error::SqliteClientError, wallet, WalletDb}; +const SQLITE_MAJOR_VERSION: u32 = 3; +const MIN_SQLITE_MINOR_VERSION: u32 = 35; -mod migrations; +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(Box>), + + /// Reverting the specified migration is not supported. + CannotRevert(Uuid), + + /// Some other unexpected violation of database business rules occurred + Other(Box), } impl From for WalletMigrationError { @@ -48,20 +82,76 @@ impl From for WalletMigrationError { } } +impl From> for WalletMigrationError { + fn from(e: ShardTreeError) -> Self { + WalletMigrationError::CommitmentTree(Box::new(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(Box::new(err)) + } + SqliteClientError::BalanceError(err) => WalletMigrationError::BalanceError(err), + SqliteClientError::AddressGeneration(err) => { + WalletMigrationError::AddressGeneration(err) + } + other => WalletMigrationError::Other(Box::new(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 + ) + } } } } @@ -70,11 +160,92 @@ 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(Box::new(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") + } + SqliteClientError::DiversifierIndexReuse(i, _) => { + WalletMigrationError::CorruptedData(format!( + "invalid attempt to overwrite address at diversifier index {}", + u128::from(i) + )) + } + SqliteClientError::AddressReuse(_, _) => { + unreachable!("we don't create transactions in migrations") + } + SqliteClientError::NoteFilterInvalid(_) => { + unreachable!("we don't do note selection in migrations") + } + #[cfg(feature = "transparent-inputs")] + SqliteClientError::Scheduling(e) => { + WalletMigrationError::Other(Box::new(SqliteClientError::Scheduling(e))) + } + } +} + /// Sets up the internal structure of the data database. /// /// This procedure will automatically perform migration operations to update the wallet database to @@ -83,26 +254,69 @@ impl std::error::Error for WalletMigrationError { /// operation of this procedure is idempotent, so it is safe (though not required) to invoke this /// operation every time the wallet is opened. /// +/// 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. +/// +/// > 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]. +/// +/// 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))`. +/// +/// 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, seed-requiring +/// migrations cannot be applied to imported accounts. +/// /// 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. /// +/// [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 secrecy::Secret; -/// use tempfile::NamedTempFile; -/// use zcash_primitives::consensus::Network; +/// # use std::error::Error; +/// # use secrecy::SecretVec; +/// # use tempfile::NamedTempFile; +/// use rand_core::OsRng; +/// use zcash_protocol::consensus::Network; /// use zcash_client_sqlite::{ /// WalletDb, -/// wallet::init::init_wallet_db, +/// util::SystemClock, +/// wallet::init::{WalletMigrationError, init_wallet_db}, /// }; /// -/// let data_file = NamedTempFile::new().unwrap(); -/// let mut db = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap(); -/// init_wallet_db(&mut db, Some(Secret::new(vec![]))).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, SystemClock, OsRng)?; +/// 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(()) +/// # } /// ``` // 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 @@ -110,484 +324,536 @@ impl std::error::Error for WalletMigrationError { // 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( - wdb: &mut WalletDb

, - seed: Option>, -) -> Result<(), MigratorError> { - init_wallet_db_internal(wdb, seed, &[]) -} - -fn init_wallet_db_internal( - wdb: &mut WalletDb

, +pub fn init_wallet_db< + C: BorrowMut, + P: consensus::Parameters + 'static, + CL: Clock + Clone + 'static, + R: RngCore + Clone + 'static, +>( + wdb: &mut WalletDb, seed: Option>, - target_migrations: &[Uuid], -) -> Result<(), MigratorError> { - // Turn off foreign keys, and ensure that table replacement/modification - // does not break views - wdb.conn - .execute_batch( - "PRAGMA foreign_keys = OFF; - PRAGMA legacy_alter_table = TRUE;", - ) - .map_err(|e| MigratorError::Adapter(WalletMigrationError::from(e)))?; - let adapter = RusqliteAdapter::new(&mut wdb.conn, Some("schemer_migrations".to_string())); - adapter.init().expect("Migrations table setup succeeds."); - - let mut migrator = Migrator::new(adapter); - migrator - .register_multiple(migrations::all_migrations(&wdb.params, seed)) - .expect("Wallet migration registration should have been successful."); - if target_migrations.is_empty() { - migrator.up(None)?; +) -> Result<(), MigratorError> { + if let Some(seed) = seed { + WalletMigrator::new().with_seed(seed) } else { - for target_migration in target_migrations { - migrator.up(Some(*target_migration))?; - } + WalletMigrator::new() } - wdb.conn - .execute("PRAGMA foreign_keys = ON", []) - .map_err(|e| MigratorError::Adapter(WalletMigrationError::from(e)))?; - Ok(()) + .init_or_migrate(wdb) } -/// Initialises the data database with the given set of account [`UnifiedFullViewingKey`]s. +/// A migrator that sets up the internal structure of the wallet database. /// -/// **WARNING** This method should be used with care, and should ordinarily be unnecessary. -/// Prefer to use [`WalletWrite::create_account`] instead. +/// 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. /// -/// [`WalletWrite::create_account`]: zcash_client_backend::data_api::WalletWrite::create_account +/// In order to correctly apply migrations to accounts derived from a seed, sometimes the +/// seed is required. The migrator should first be used without calling [`Self::with_seed`]; +/// if a pending migration requires the seed, [`Self::init_or_migrate`] returns +/// `Err(schemerz::MigratorError::Migration { error: WalletMigrationError::SeedRequired, .. })`. +/// The caller can then call [`Self::with_seed`] and then re-call [`Self::init_or_migrate`] +/// with the necessary seed. /// -/// The [`UnifiedFullViewingKey`]s are stored internally and used by other APIs such as -/// [`scan_cached_blocks`], and [`create_spend_to_address`]. Account identifiers in `keys` **MUST** -/// form a consecutive sequence beginning at account 0, and the [`UnifiedFullViewingKey`] -/// corresponding to a given account identifier **MUST** be derived from the wallet's mnemonic seed -/// at the BIP-44 `account` path level as described by [ZIP -/// 316](https://zips.z.cash/zip-0316) +/// > 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]. /// -/// # Examples +/// When a seed is provided, it 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, [`Self::init_or_migrate`] returns +/// `Err(schemerz::MigratorError::Migration { error: WalletMigrationError::SeedNotRelevant, .. })` +/// or `Err(schemerz::MigratorError::Adapter(WalletMigrationError::SeedNotRelevant))`. /// -/// ``` -/// # #[cfg(feature = "transparent-inputs")] -/// # { -/// use tempfile::NamedTempFile; -/// use secrecy::Secret; -/// use std::collections::HashMap; +/// 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, seed-requiring +/// migrations cannot be applied to imported accounts. /// -/// use zcash_primitives::{ -/// consensus::{Network, Parameters}, -/// zip32::{AccountId, ExtendedSpendingKey} -/// }; +/// 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_client_backend::{ -/// keys::{ -/// sapling, -/// UnifiedFullViewingKey -/// }, -/// }; +/// [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 rand_core::OsRng; +/// use zcash_protocol::consensus::Network; /// use zcash_client_sqlite::{ /// WalletDb, -/// wallet::init::{init_accounts_table, init_wallet_db} +/// util::SystemClock, +/// wallet::init::{WalletMigrationError, WalletMigrator}, /// }; /// -/// let data_file = NamedTempFile::new().unwrap(); -/// let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap(); -/// init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap(); -/// -/// let seed = [0u8; 32]; // insecure; replace with a strong random seed -/// let account = AccountId::from(0); -/// let extsk = sapling::spending_key(&seed, Network::TestNetwork.coin_type(), account); -/// let dfvk = extsk.to_diversifiable_full_viewing_key(); -/// let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk), None).unwrap(); -/// let ufvks = HashMap::from([(account, ufvk)]); -/// init_accounts_table(&db_data, &ufvks).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, SystemClock, OsRng)?; +/// match WalletMigrator::new().init_or_migrate(&mut db) { +/// Err(e) +/// if matches!( +/// e.source().and_then(|e| e.downcast_ref()), +/// Some(&WalletMigrationError::SeedRequired) +/// ) => +/// { +/// let seed = load_seed()?; +/// WalletMigrator::new() +/// .with_seed(seed) +/// .init_or_migrate(&mut db) +/// } +/// 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

, - keys: &HashMap, -) -> Result<(), SqliteClientError> { - let mut empty_check = wdb.conn.prepare("SELECT * FROM accounts LIMIT 1")?; - if empty_check.exists([])? { - return Err(SqliteClientError::TableNotEmpty); +pub struct WalletMigrator { + seed: Option>, + verify_seed_relevance: bool, + external_migrations: Option>>>, +} + +impl Default for WalletMigrator { + fn default() -> Self { + Self::new() } +} - // Ensure that the account identifiers are sequential and begin at zero. - if let Some(account_id) = keys.keys().max() { - if usize::try_from(u32::from(*account_id)).unwrap() >= keys.len() { - return Err(SqliteClientError::AccountIdDiscontinuity); +impl WalletMigrator { + /// Constructs a new wallet migrator. + pub fn new() -> Self { + Self { + seed: None, + verify_seed_relevance: true, + external_migrations: None, } } - // Insert accounts atomically - wdb.conn.execute("BEGIN IMMEDIATE", [])?; - for (account, key) in keys.iter() { - wallet::add_account(wdb, *account, key)?; + /// Sets the seed for the migrator to use. + pub fn with_seed(mut self, seed: SecretVec) -> Self { + self.seed = Some(seed); + self } - wdb.conn.execute("COMMIT", [])?; - Ok(()) + /// API for internal test usage only. + #[cfg(test)] + pub(crate) fn ignore_seed_relevance(mut self) -> Self { + self.verify_seed_relevance = false; + self + } + + /// Sets the external migration graph to apply alongside the internal migrations. + /// + /// From a data management perspective, it can be useful to store additional data + /// alongside the `zcash_client_sqlite` wallet database. This method enables you to + /// provide an external [`schemerz`] migration graph that the migrator will apply to + /// the wallet database. + /// + /// # WARNING + /// + /// **DO NOT** depend on or modify internal details of the `zcash_client_sqlite` + /// schema! + /// + /// The internal migrations are written to take into account internal relationships + /// between the `zcash_client_sqlite` tables, but they will never take into account + /// external tables. In particular, this means that you **MUST NOT**: + /// - Modify the structure or contents of any internal table. + /// - Assume that internal IDs will exist indefinitely (instead have a backup plan for + /// recovering your data relationships if a new internal migration affects your + /// foreign keys). + /// + /// The `zcash_client_sqlite` schema does not have any common prefix it uses for + /// tables, indexes, or views. However, we promise to not use the prefix `ext_` for + /// any internal names. Schema created by external migrations **MUST** use name + /// prefixing with a prefix that is unlikely to collide with either the internal names + /// or other potential external schemas (e.g. `ext_myappname_*`). + /// + /// # Integration + /// + /// In order to enable anchoring your external migrations correctly with respect to + /// this library's internal migrations, we provide constants in the [`migrations`] + /// module (for each release that adds a migration) which you can include within your + /// [`schemerz::Migration::dependencies`] set. + /// + /// Each migration runs inside a database transaction, which has the following + /// implications: + /// - `PRAGMA foreign_keys` has no effect inside a transaction, so the migrator + /// handles foreign key enforcement itself: + /// - `PRAGMA foreign_keys = OFF` is set before running any migrations. + /// - `PRAGMA foreign_keys = ON` is set after all migrations are successful. + /// - `PRAGMA legacy_alter_table` should only be used in cases where its effect is + /// explicitly intended, so the migrator does not use it globally. If you want to + /// rename tables without breaking foreign key relationships, you need to do so + /// yourself inside individual migrations: + /// ```sql + /// PRAGMA legacy_alter_table = ON; + /// DROP TABLE table_name; + /// ALTER TABLE table_name_new RENAME TO table_name; + /// PRAGMA legacy_alter_table = OFF; + /// ``` + pub fn with_external_migrations( + mut self, + migrations: Vec>>, + ) -> Self { + self.external_migrations = Some(migrations); + self + } + + /// Sets up the internal structure of the given wallet database to be compatible with + /// this library version. + pub fn init_or_migrate< + C: BorrowMut, + P: consensus::Parameters + 'static, + CL: Clock + Clone + 'static, + R: RngCore + Clone + 'static, + >( + self, + wdb: &mut WalletDb, + ) -> Result<(), MigratorError> { + self.init_or_migrate_to(wdb, &[]) + } + + /// Sets up the internal structure of the given wallet database to be compatible with + /// this library version. + pub(crate) fn init_or_migrate_to< + C: BorrowMut, + P: consensus::Parameters + 'static, + CL: Clock + Clone + 'static, + R: RngCore + Clone + 'static, + >( + self, + wdb: &mut WalletDb, + target_migrations: &[Uuid], + ) -> Result<(), MigratorError> { + init_wallet_db_internal( + wdb, + self.seed, + self.external_migrations, + target_migrations, + self.verify_seed_relevance, + ) + } } -/// 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([])? { - return Err(SqliteClientError::TableNotEmpty); +fn init_wallet_db_internal< + C: BorrowMut, + P: consensus::Parameters + 'static, + CL: Clock + Clone + 'static, + R: RngCore + Clone + 'static, +>( + wdb: &mut WalletDb, + seed: Option>, + external_migrations: Option>>>, + target_migrations: &[Uuid], + verify_seed_relevance: bool, +) -> Result<(), MigratorError> { + let seed = seed.map(Rc::new); + + 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)?; + + // 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, + wdb.clock.clone(), + wdb.rng.clone(), + seed.clone(), + ) + .into_iter(), + ) + .expect("Wallet migration registration should have been successful."); + if let Some(migrations) = external_migrations { + migrator.register_multiple(migrations.into_iter())?; } + 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)))?; - 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()?, - ], - )?; + // 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()) + } + } + } + } Ok(()) } +/// 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))?; + + 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")?; + + if major != SQLITE_MAJOR_VERSION || minor < MIN_SQLITE_MINOR_VERSION { + Err(WalletMigrationError::DatabaseNotSupported(sqlite_version)) + } else { + Ok(()) + } +} + +#[cfg(test)] +pub(crate) mod testing { + use rand::RngCore; + use schemerz::MigratorError; + use secrecy::SecretVec; + use uuid::Uuid; + use zcash_protocol::consensus; + + use crate::{util::Clock, WalletDb}; + + use super::WalletMigrationError; + + pub(crate) fn init_wallet_db< + P: consensus::Parameters + 'static, + CL: Clock + Clone + 'static, + R: RngCore + Clone + 'static, + >( + wdb: &mut WalletDb, + seed: Option>, + ) -> Result<(), MigratorError> { + super::init_wallet_db_internal(wdb, seed, None, &[], true) + } +} + #[cfg(test)] -#[allow(deprecated)] mod tests { - use rusqlite::{self, ToSql}; + use rand::RngCore; + use rusqlite::{self, named_params, Connection, ToSql}; use secrecy::Secret; - use std::collections::HashMap; + use tempfile::NamedTempFile; - use zcash_client_backend::{ - address::RecipientAddress, - data_api::WalletRead, + 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, UnifiedFullViewingKey, UnifiedSpendingKey}, - }; - - use zcash_primitives::{ - block::BlockHash, - consensus::{BlockHeight, BranchId, Parameters}, - transaction::{TransactionData, TxVersion}, - zip32::sapling::ExtendedFullViewingKey, + 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 super::testing::init_wallet_db; use crate::{ - error::SqliteClientError, - tests::{self, network}, - AccountId, WalletDb, + testing::db::{test_clock, test_rng, TestDbFactory}, + util::Clock, + wallet::db, + WalletDb, UA_TRANSPARENT, }; - use super::{init_accounts_table, init_blocks_table, init_wallet_db}; - #[cfg(feature = "transparent-inputs")] use { - crate::{ - wallet::{self, pool_code, PoolType}, - WalletWrite, - }, + super::WalletMigrationError, + crate::wallet::{self, pool_code, PoolType}, zcash_address::test_vectors, - zcash_primitives::{ - consensus::Network, legacy::keys as transparent, zip32::DiversifierIndex, - }, + zcash_client_backend::data_api::WalletWrite, + zip32::DiversifierIndex, }; + #[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))] + use zcash_protocol::value::Zatoshis; + + 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::, _>>()?; + + Ok(result) + } + #[test] fn verify_schema() { - let data_file = NamedTempFile::new().unwrap(); - let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&mut db_data, None).unwrap(); + 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![ - "CREATE TABLE \"accounts\" ( - account INTEGER PRIMARY KEY, - ufvk TEXT NOT NULL - )", - "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 blocks ( - height INTEGER PRIMARY KEY, - hash BLOB NOT NULL, - time INTEGER NOT NULL, - sapling_tree BLOB NOT NULL - )", - "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) - )", - "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) - )", - "CREATE TABLE schemer_migrations ( - id blob PRIMARY KEY - )", - "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, - 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) - ) - )", - "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, - fee INTEGER, - FOREIGN KEY (block) REFERENCES blocks(height) - )", - "CREATE TABLE \"utxos\" ( - 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) - )", + db::TABLE_ACCOUNTS, + db::TABLE_ADDRESSES, + db::TABLE_BLOCKS, + 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 mut tables_query = db_data + 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_ADDRESSES_INDICES, + db::INDEX_ADDRESSES_T_INDICES, + 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_schema WHERE type = 'table' ORDER BY tbl_name") + .prepare("SELECT sql FROM sqlite_master WHERE type = 'index' AND sql != '' ORDER BY tbl_name, name") .unwrap(); - let mut rows = tables_query.query([]).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_tables[expected_idx], " ") + re.replace_all(expected_indices[expected_idx], " ").trim(), ); expected_idx += 1; } let expected_views = vec![ - // 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", - // 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" + db::VIEW_ADDRESS_FIRST_USE.to_owned(), + db::VIEW_ADDRESS_USES.to_owned(), + 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 = db_data + let mut views_query = st + .wallet() + .db() .conn .prepare("SELECT sql FROM sqlite_schema WHERE type = 'view' ORDER BY tbl_name") .unwrap(); @@ -597,16 +863,35 @@ mod tests { let sql: String = row.get(0).unwrap(); assert_eq!( re.replace_all(&sql, " "), - re.replace_all(expected_views[expected_idx], " ") + re.replace_all(&expected_views[expected_idx], " ").trim(), ); expected_idx += 1; } } + #[test] + fn external_schema_prefix_unused() { + let st = TestBuilder::new() + .with_data_store_factory(TestDbFactory::default()) + .build(); + + let mut names_query = st + .wallet() + .db() + .conn + .prepare("SELECT tbl_name FROM sqlite_schema") + .unwrap(); + let mut rows = names_query.query([]).unwrap(); + while let Some(row) = rows.next().unwrap() { + let name: String = row.get(0).unwrap(); + assert!(!name.starts_with("ext_")); + } + } + #[test] fn init_migrate_from_0_3_0() { - fn init_0_3_0

( - wdb: &mut WalletDb

, + fn init_0_3_0( + wdb: &mut WalletDb, extfvk: &ExtendedFullViewingKey, account: AccountId, ) -> Result<(), rusqlite::Error> { @@ -689,11 +974,11 @@ mod tests { )?; let address = encode_payment_address( - tests::network().hrp_sapling_payment_address(), + wdb.params.hrp_sapling_payment_address(), &extfvk.default_address().1, ); let extfvk = encode_extended_full_viewing_key( - tests::network().hrp_sapling_extended_full_viewing_key(), + wdb.params.hrp_sapling_extended_full_viewing_key(), extfvk, ); wdb.conn.execute( @@ -709,20 +994,32 @@ mod tests { Ok(()) } + let data_file = NamedTempFile::new().unwrap(); + let mut db_data = WalletDb::for_path( + data_file.path(), + Network::TestNetwork, + test_clock(), + test_rng(), + ) + .unwrap(); + let seed = [0xab; 32]; - let account = AccountId::from(0); - let secret_key = sapling::spending_key(&seed, tests::network().coin_type(), account); + 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(); - let data_file = NamedTempFile::new().unwrap(); - let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); + init_0_3_0(&mut db_data, &extfvk, account).unwrap(); - init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))).unwrap(); + assert_matches!( + init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))), + Ok(_) + ); } #[test] fn init_migrate_from_autoshielding_poc() { - fn init_autoshielding

( - wdb: &WalletDb

, + fn init_autoshielding( + wdb: &mut WalletDb, extfvk: &ExtendedFullViewingKey, account: AccountId, ) -> Result<(), rusqlite::Error> { @@ -821,11 +1118,11 @@ mod tests { )?; let address = encode_payment_address( - tests::network().hrp_sapling_payment_address(), + wdb.params.hrp_sapling_payment_address(), &extfvk.default_address().1, ); let extfvk = encode_extended_full_viewing_key( - tests::network().hrp_sapling_extended_full_viewing_key(), + wdb.params.hrp_sapling_extended_full_viewing_key(), extfvk, ); wdb.conn.execute( @@ -840,15 +1137,17 @@ mod tests { // add a sapling sent note wdb.conn.execute( - "INSERT INTO blocks (height, hash, time, sapling_tree) VALUES (0, 0, 0, '')", + "INSERT INTO blocks (height, hash, time, sapling_tree) VALUES (0, 0, 0, x'000000')", [], )?; let tx = TransactionData::from_parts( - TxVersion::Sapling, + TxVersion::V4, BranchId::Canopy, 0, BlockHeight::from(0), + #[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))] + Zatoshis::ZERO, None, None, None, @@ -860,8 +1159,11 @@ mod tests { 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, '', ?)", - [&tx_bytes[..]], + "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) @@ -872,20 +1174,32 @@ mod tests { Ok(()) } + let data_file = NamedTempFile::new().unwrap(); + let mut db_data = WalletDb::for_path( + data_file.path(), + Network::TestNetwork, + test_clock(), + test_rng(), + ) + .unwrap(); + let seed = [0xab; 32]; - let account = AccountId::from(0); - let secret_key = sapling::spending_key(&seed, tests::network().coin_type(), account); + 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(); - let data_file = NamedTempFile::new().unwrap(); - let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_autoshielding(&db_data, &extfvk, account).unwrap(); - init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))).unwrap(); + + 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: &WalletDb

, + fn init_main( + wdb: &mut WalletDb, ufvk: &UnifiedFullViewingKey, account: AccountId, ) -> Result<(), rusqlite::Error> { @@ -984,9 +1298,17 @@ mod tests { [], )?; - let ufvk_str = ufvk.encode(&tests::network()); - let address_str = - RecipientAddress::Unified(ufvk.default_address().0).encode(&tests::network()); + 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_custom(Omit, Require, UA_TRANSPARENT); + let address_str = Address::Unified( + ufvk.default_address(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 (?, ?, ?, '')", @@ -1000,11 +1322,17 @@ mod tests { // add a transparent "sent note" #[cfg(feature = "transparent-inputs")] { - let taddr = - RecipientAddress::Transparent(*ufvk.default_address().0.transparent().unwrap()) - .encode(&tests::network()); + let taddr = Address::Transparent( + *ufvk + .default_address(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, '')", + "INSERT INTO blocks (height, hash, time, sapling_tree) VALUES (0, 0, 0, x'000000')", [], )?; wdb.conn.execute( @@ -1014,163 +1342,117 @@ mod tests { 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()?])?; + [pool_code(PoolType::TRANSPARENT).to_sql()?, u32::from(account).to_sql()?, taddr.to_sql()?])?; } Ok(()) } - let seed = [0xab; 32]; - let account = AccountId::from(0); - let secret_key = UnifiedSpendingKey::from_seed(&tests::network(), &seed, account).unwrap(); - let data_file = NamedTempFile::new().unwrap(); - let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_main(&db_data, &secret_key.to_unified_full_viewing_key(), account).unwrap(); - init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))).unwrap(); - } - - #[test] - fn init_accounts_table_only_works_once() { let data_file = NamedTempFile::new().unwrap(); - let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap(); - - // We can call the function as many times as we want with no data - init_accounts_table(&db_data, &HashMap::new()).unwrap(); - init_accounts_table(&db_data, &HashMap::new()).unwrap(); - - let seed = [0u8; 32]; - let account = AccountId::from(0); - - // First call with data should initialise the accounts table - let extsk = sapling::spending_key(&seed, network().coin_type(), account); - let dfvk = extsk.to_diversifiable_full_viewing_key(); - - #[cfg(feature = "transparent-inputs")] - let ufvk = UnifiedFullViewingKey::new( - Some( - transparent::AccountPrivKey::from_seed(&network(), &seed, account) - .unwrap() - .to_account_pubkey(), - ), - Some(dfvk), - None, + let mut db_data = WalletDb::for_path( + data_file.path(), + Network::TestNetwork, + test_clock(), + test_rng(), ) .unwrap(); - #[cfg(not(feature = "transparent-inputs"))] - let ufvk = UnifiedFullViewingKey::new(Some(dfvk), None).unwrap(); - let ufvks = HashMap::from([(account, ufvk)]); - - init_accounts_table(&db_data, &ufvks).unwrap(); - - // Subsequent calls should return an error - init_accounts_table(&db_data, &HashMap::new()).unwrap_err(); - init_accounts_table(&db_data, &ufvks).unwrap_err(); - } - - #[test] - fn init_accounts_table_allows_no_gaps() { - let data_file = NamedTempFile::new().unwrap(); - let mut db_data = WalletDb::for_path(data_file.path(), network()).unwrap(); - init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap(); - - // allow sequential initialization - let seed = [0u8; 32]; - let ufvks = |ids: &[u32]| { - ids.iter() - .map(|a| { - let account = AccountId::from(*a); - UnifiedSpendingKey::from_seed(&network(), &seed, account) - .map(|k| (account, k.to_unified_full_viewing_key())) - .unwrap() - }) - .collect::>() - }; - - // should fail if we have a gap - assert_matches!( - init_accounts_table(&db_data, &ufvks(&[0, 2])), - Err(SqliteClientError::AccountIdDiscontinuity) - ); - - // should succeed if there are no gaps - assert!(init_accounts_table(&db_data, &ufvks(&[0, 1, 2])).is_ok()); - } + let seed = [0xab; 32]; + let account = AccountId::ZERO; + let secret_key = UnifiedSpendingKey::from_seed(&db_data.params, &seed, account).unwrap(); - #[test] - fn init_blocks_table_only_works_once() { - let data_file = NamedTempFile::new().unwrap(); - let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap(); - - // First call with data should initialise the blocks table - init_blocks_table( - &db_data, - BlockHeight::from(1u32), - BlockHash([1; 32]), - 1, - &[], + init_main( + &mut db_data, + &secret_key.to_unified_full_viewing_key(), + account, ) .unwrap(); - - // Subsequent calls should return an error - init_blocks_table( - &db_data, - BlockHeight::from(2u32), - BlockHash([2; 32]), - 2, - &[], - ) - .unwrap_err(); - } - - #[test] - fn init_accounts_table_stores_correct_address() { - let data_file = NamedTempFile::new().unwrap(); - let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&mut db_data, None).unwrap(); - - let seed = [0u8; 32]; - - // Add an account to the wallet - let account_id = AccountId::from(0); - let usk = UnifiedSpendingKey::from_seed(&tests::network(), &seed, account_id).unwrap(); - let ufvk = usk.to_unified_full_viewing_key(); - let expected_address = ufvk.sapling().unwrap().default_address().1; - let ufvks = HashMap::from([(account_id, ufvk)]); - init_accounts_table(&db_data, &ufvks).unwrap(); - - // The account's address should be in the data DB - let ua = db_data.get_current_address(AccountId::from(0)).unwrap(); - assert_eq!(ua.unwrap().sapling().unwrap(), &expected_address); + assert_matches!( + init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))), + Ok(_) + ); } #[test] #[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 mut db_data = WalletDb::for_path(data_file.path(), Network::MainNetwork).unwrap(); - init_wallet_db(&mut db_data, None).unwrap(); + let mut db_data = + WalletDb::for_path(data_file.path(), network, test_clock(), test_rng()).unwrap(); + assert_matches!(init_wallet_db(&mut db_data, None), Ok(_)); - let mut ops = db_data.get_update_ops().unwrap(); + // Prior to adding any accounts, every seed phrase is relevant to the wallet. let seed = test_vectors::UNIFIED[0].root_seed; - let (account, _usk) = ops.create_account(&Secret::new(seed.to_vec())).unwrap(); - assert_eq!(account, AccountId::from(0u32)); + 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(); + + // We have to have the chain tip height in order to allocate new addresses, to record the + // exposed-at height. + db_data.update_chain_tip(birthday.height()).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 + )) + ); for tv in &test_vectors::UNIFIED[..3] { - if let Some(RecipientAddress::Unified(tvua)) = - RecipientAddress::decode(&Network::MainNetwork, tv.unified_addr) + if let Some(Address::Unified(tvua)) = + Address::decode(&Network::MainNetwork, tv.unified_addr) { - let (ua, di) = wallet::get_current_address(&db_data, account) - .unwrap() - .expect("create_account generated the first address"); + // hardcoded with knowledge of test vectors + let ua_request = UnifiedAddressRequest::unsafe_custom(Omit, Require, Require); + + let (ua, di) = wallet::get_last_generated_address_matching( + &db_data.conn, + &db_data.params, + account_id, + if tv.diversifier_index == 0 { + UnifiedAddressRequest::AllAvailableKeys + } else { + ua_request + }, + ) + .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)); - ops.get_next_available_address(account) + db_data + .get_next_available_address(account_id, ua_request) .unwrap() .expect("get_next_available_address generated an address"); } else { diff --git a/zcash_client_sqlite/src/wallet/init/migrations.rs b/zcash_client_sqlite/src/wallet/init/migrations.rs index 1cc9bcfc5f..28eb276cb0 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations.rs @@ -1,40 +1,124 @@ +//! Migrations for the `zcash_client_sqlite` wallet database. +//! +//! The constants in this module cover 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. + +mod add_account_birthdays; +mod add_account_uuids; mod add_transaction_views; mod add_utxo_account; mod addresses_table; +mod ensure_default_transparent_address; +mod ensure_orchard_ua_receiver; +mod ephemeral_addresses; +mod fix_bad_change_flagging; +mod fix_broken_commitment_trees; +mod fix_transparent_received_outputs; +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 transparent_gap_limit_handling; +mod tx_retrieval_queue; mod ufvk_support; mod utxos_table; +mod utxos_to_txos; +mod v_sapling_shard_unscanned_ranges; +mod v_transactions_additional_totals; 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, sync::Mutex}; -use schemer_rusqlite::RusqliteMigration; +use rand_core::RngCore; +use rusqlite::{named_params, OptionalExtension}; +use schemerz_rusqlite::RusqliteMigration; use secrecy::SecretVec; -use zcash_primitives::consensus; +use uuid::Uuid; +use zcash_address::unified::{Encoding as _, Ufvk}; +use zcash_protocol::consensus; + +use crate::util::Clock; use super::WalletMigrationError; -pub(super) fn all_migrations( +pub(super) fn all_migrations< + P: consensus::Parameters + 'static, + C: Clock + Clone + 'static, + R: RngCore + Clone + 'static, +>( params: &P, - seed: Option>, + clock: C, + rng: R, + seed: Option>>, ) -> Vec>> { - // initial_setup - // / \ - // utxos_table ufvk_support ---------- - // \ \ \ - // \ addresses_table sent_notes_to_internal - // \ / / - // add_utxo_account / - // \ / - // add_transaction_views - // / - // v_transactions_net + // 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 transparent_gap_limit_handling v_transactions_additional_totals + // \ | / + // \ ensure_default_transparent_address / + // \ | / + // `---- fix_transparent_received_outputs --' + let rng = Rc::new(Mutex::new(rng)); vec![ Box::new(initial_setup::Migration {}), Box::new(utxos_table::Migration {}), Box::new(ufvk_support::Migration { params: params.clone(), - seed, + seed: seed.clone(), }), Box::new(addresses_table::Migration { params: params.clone(), @@ -46,5 +130,298 @@ pub(super) fn all_migrations( 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), + Box::new(v_transactions_additional_totals::Migration), + Box::new(transparent_gap_limit_handling::Migration { + params: params.clone(), + _clock: clock.clone(), + _rng: rng.clone(), + }), + Box::new(ensure_default_transparent_address::Migration { + _params: params.clone(), + }), + Box::new(fix_transparent_received_outputs::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, V_0_14_0, V_0_15_0, V_0_16_0, V_0_16_2, +]; + +/// Leaf migrations in the 0.4.0 release. +pub const V_0_4_0: &[Uuid] = &[add_transaction_views::MIGRATION_ID]; + +/// Leaf migrations in the 0.6.0 release. +pub const V_0_6_0: &[Uuid] = &[v_transactions_net::MIGRATION_ID]; + +/// Leaf migrations in the 0.8.0 release. +pub 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. +pub 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. +pub 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. +pub 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. +pub 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. +pub const V_0_11_1: &[Uuid] = &[tx_retrieval_queue::MIGRATION_ID]; + +/// Leaf migrations in the 0.11.2 release. +pub const V_0_11_2: &[Uuid] = &[support_legacy_sqlite::MIGRATION_ID]; + +/// Leaf migrations in the 0.12.0 release. +pub const V_0_12_0: &[Uuid] = &[fix_broken_commitment_trees::MIGRATION_ID]; + +/// Leaf migrations in the 0.13.0 release. +pub const V_0_13_0: &[Uuid] = &[fix_bad_change_flagging::MIGRATION_ID]; + +/// Leaf migrations in the 0.14.0 release. +pub const V_0_14_0: &[Uuid] = &[ + fix_bad_change_flagging::MIGRATION_ID, + add_account_uuids::MIGRATION_ID, +]; + +/// Leaf migrations in the 0.15.0 release. +pub const V_0_15_0: &[Uuid] = &[ + fix_bad_change_flagging::MIGRATION_ID, + v_transactions_additional_totals::MIGRATION_ID, +]; + +/// Leaf migrations in the 0.16.0 release. +const V_0_16_0: &[Uuid] = &[ + fix_bad_change_flagging::MIGRATION_ID, + v_transactions_additional_totals::MIGRATION_ID, + transparent_gap_limit_handling::MIGRATION_ID, +]; + +/// Leaf migrations in the 0.16.2 release. +const V_0_16_2: &[Uuid] = &[ + fix_bad_change_flagging::MIGRATION_ID, + v_transactions_additional_totals::MIGRATION_ID, + ensure_default_transparent_address::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::{ + testing::db::{test_clock, test_rng}, + wallet::init::WalletMigrator, + 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, + test_clock(), + test_rng(), + ) + .unwrap(); + + let seed = [0xab; 32]; + assert_matches!( + WalletMigrator::new() + .with_seed(Secret::new(seed.to_vec())) + .ignore_seed_relevance() + .init_or_migrate_to(&mut db_data, migrations), + 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, + test_clock(), + test_rng(), + ) + .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!( + WalletMigrator::new() + .with_seed(Secret::new(seed.clone())) + .ignore_seed_relevance() + .init_or_migrate_to(&mut db_data, migrations), + 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!( + WalletMigrator::new() + .with_seed(Secret::new(seed)) + .ignore_seed_relevance() + .init_or_migrate(&mut db_data), + 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..680d201250 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/add_account_birthdays.rs @@ -0,0 +1,131 @@ +//! 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::{ + testing::db::{test_clock, test_rng}, + wallet::init::WalletMigrator, + 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, test_clock(), test_rng()).unwrap(); + + let seed_bytes = vec![0xab; 32]; + WalletMigrator::new() + .with_seed(Secret::new(seed_bytes.clone())) + .ignore_seed_relevance() + .init_or_migrate_to(&mut db_data, DEPENDENCIES) + .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(); + + WalletMigrator::new() + .with_seed(Secret::new(seed_bytes)) + .ignore_seed_relevance() + .init_or_migrate_to(&mut db_data, &[MIGRATION_ID]) + .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 index 70694f8425..3fd581d440 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/add_transaction_views.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/add_transaction_views.rs @@ -2,42 +2,34 @@ use std::collections::HashSet; use rusqlite::{self, types::ToSql, OptionalExtension}; -use schemer::{self}; -use schemer_rusqlite::RusqliteMigration; +use schemerz_rusqlite::RusqliteMigration; use uuid::Uuid; -use zcash_primitives::{ +use zcash_primitives::transaction::Transaction; +use zcash_protocol::{ consensus::BranchId, - transaction::{ - components::amount::{Amount, BalanceError}, - Transaction, - }, + 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_fields( - 0x282fad2e, - 0x8372, - 0x4ca0, - b"\x8b\xed\x71\x82\x13\x20\x90\x9f", -); +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 schemer::Migration for Migration { +impl schemerz::Migration for Migration { fn id(&self) -> Uuid { MIGRATION_ID } fn dependencies(&self) -> HashSet { - [ - add_utxo_account::MIGRATION_ID, - sent_notes_to_internal::MIGRATION_ID, - ] - .into_iter() - .collect() + DEPENDENCIES.iter().copied().collect() } fn description(&self) -> &'static str { @@ -111,7 +103,7 @@ impl RusqliteMigration for Migration { op_amount.map_or_else( || Err(FeeError::UtxoNotFound), |i| { - Amount::from_i64(i).map_err(|_| { + ZatBalance::from_i64(i).map_err(|_| { FeeError::CorruptedData(format!( "UTXO amount out of range in outpoint {:?}", op @@ -272,8 +264,7 @@ impl RusqliteMigration for Migration { } fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { - // TODO: something better than just panic? - panic!("Cannot revert this migration."); + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) } } @@ -282,52 +273,55 @@ mod tests { use rusqlite::{self, params}; use tempfile::NamedTempFile; - use zcash_client_backend::keys::UnifiedSpendingKey; - use zcash_primitives::zip32::AccountId; + use zcash_keys::keys::UnifiedSpendingKey; + use zcash_protocol::consensus::Network; + use zip32::AccountId; use crate::{ - tests, - wallet::init::{init_wallet_db_internal, migrations::addresses_table}, + testing::db::{test_clock, test_rng}, + wallet::init::{migrations::addresses_table, WalletMigrator}, WalletDb, }; #[cfg(feature = "transparent-inputs")] use { crate::wallet::init::migrations::{ufvk_support, utxos_table}, - zcash_client_backend::encoding::AddressCodec, - zcash_primitives::{ + ::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}, - legacy::{keys::IncomingViewingKey, Script}, - transaction::{ - components::{ - transparent::{self, Authorized, OutPoint}, - Amount, TxIn, TxOut, - }, - TransactionData, TxVersion, - }, + 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(), tests::network()).unwrap(); - init_wallet_db_internal(&mut db_data, None, &[addresses_table::MIGRATION_ID]).unwrap(); - let usk = - UnifiedSpendingKey::from_seed(&tests::network(), &[0u8; 32][..], AccountId::from(0)) - .unwrap(); + let mut db_data = + WalletDb::for_path(data_file.path(), network, test_clock(), test_rng()).unwrap(); + WalletMigrator::new() + .ignore_seed_relevance() + .init_or_migrate_to(&mut db_data, &[addresses_table::MIGRATION_ID]) + .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(&tests::network())], + params![ufvk.encode(&network)], ) .unwrap(); db_data.conn.execute_batch( - "INSERT INTO blocks (height, hash, time, sapling_tree) VALUES (0, 0, 0, ''); + "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) @@ -345,7 +339,10 @@ mod tests { VALUES (0, 4, 0, '', 7, '', 'c', true, X'63');", ).unwrap(); - init_wallet_db_internal(&mut db_data, None, &[super::MIGRATION_ID]).unwrap(); + WalletMigrator::new() + .ignore_seed_relevance() + .init_or_migrate_to(&mut db_data, &[super::MIGRATION_ID]) + .unwrap(); let mut q = db_data .conn @@ -402,29 +399,41 @@ mod tests { #[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(), tests::network()).unwrap(); - init_wallet_db_internal( - &mut db_data, - None, - &[utxos_table::MIGRATION_ID, ufvk_support::MIGRATION_ID], - ) - .unwrap(); + let mut db_data = + WalletDb::for_path(data_file.path(), network, test_clock(), test_rng()).unwrap(); + WalletMigrator::new() + .ignore_seed_relevance() + .init_or_migrate_to( + &mut db_data, + &[utxos_table::MIGRATION_ID, ufvk_support::MIGRATION_ID], + ) + .unwrap(); // create a UTXO to spend let tx = TransactionData::from_parts( - TxVersion::Sapling, + TxVersion::V4, BranchId::Canopy, 0, BlockHeight::from(3), + #[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))] + Zatoshis::ZERO, Some(transparent::Bundle { vin: vec![TxIn { - prevout: OutPoint::new([1u8; 32], 1), + prevout: OutPoint::fake(), script_sig: Script(vec![]), sequence: 0, }], vout: vec![TxOut { - value: Amount::from_i64(1100000000).unwrap(), + value: Zatoshis::const_from_u64(1100000000), script_pubkey: Script(vec![]), }], authorization: Authorized, @@ -439,28 +448,32 @@ mod tests { let mut tx_bytes = vec![]; tx.write(&mut tx_bytes).unwrap(); - let usk = - UnifiedSpendingKey::from_seed(&tests::network(), &[0u8; 32][..], AccountId::from(0)) - .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(); + let (ua, _) = ufvk + .default_address(UnifiedAddressRequest::unsafe_custom( + 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(0).unwrap()) + .map(|k| k.derive_address(NonHardenedChildIndex::ZERO).unwrap()) }) - .map(|a| a.encode(&tests::network())); + .map(|a| a.encode(&network)); db_data.conn.execute( "INSERT INTO accounts (account, ufvk, address, transparent_address) VALUES (0, ?, ?, ?)", - params![ufvk.encode(&tests::network()), ua.encode(&tests::network()), &taddr] + 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, '');", + "INSERT INTO blocks (height, hash, time, sapling_tree) VALUES (0, 0, 0, x'00');", ) .unwrap(); db_data.conn.execute( @@ -476,15 +489,18 @@ mod tests { ) .unwrap(); - init_wallet_db_internal(&mut db_data, None, &[super::MIGRATION_ID]).unwrap(); + WalletMigrator::new() + .ignore_seed_relevance() + .init_or_migrate_to(&mut db_data, &[super::MIGRATION_ID]) + .unwrap(); let fee = db_data .conn .query_row("SELECT fee FROM transactions WHERE id_tx = 0", [], |row| { - Ok(Amount::from_i64(row.get(0)?).unwrap()) + Ok(ZatBalance::from_i64(row.get(0)?).unwrap()) }) .unwrap(); - assert_eq!(fee, Amount::from_i64(300000000).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 index f658ae1030..3fe32cb5ee 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/add_utxo_account.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/add_utxo_account.rs @@ -1,47 +1,43 @@ //! A migration that adds an identifier for the account that received a UTXO to the utxos table -use std::collections::HashSet; -use rusqlite; -use schemer; -use schemer_rusqlite::RusqliteMigration; +use schemerz_rusqlite::RusqliteMigration; +use std::collections::HashSet; use uuid::Uuid; - -use zcash_primitives::consensus; +use zcash_protocol::consensus; use super::{addresses_table, utxos_table}; use crate::wallet::init::WalletMigrationError; #[cfg(feature = "transparent-inputs")] use { - crate::{error::SqliteClientError, wallet::get_transparent_receivers}, - rusqlite::named_params, - zcash_client_backend::encoding::AddressCodec, - zcash_primitives::zip32::AccountId, + 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, Scope}, }; /// This migration adds an account identifier column to the UTXOs table. -/// -/// 761884d6-30d8-44ef-b204-0b82551c4ca1 -pub(super) const MIGRATION_ID: Uuid = Uuid::from_fields( - 0x761884d6, - 0x30d8, - 0x44ef, - b"\xb2\x04\x0b\x82\x55\x1c\x4c\xa1", -); +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

schemer::Migration for Migration

{ +impl

schemerz::Migration for Migration

{ fn id(&self) -> Uuid { MIGRATION_ID } fn dependencies(&self) -> HashSet { - [utxos_table::MIGRATION_ID, addresses_table::MIGRATION_ID] - .into_iter() - .collect() + DEPENDENCIES.iter().copied().collect() } fn description(&self) -> &'static str { @@ -65,23 +61,26 @@ impl RusqliteMigration for Migration

{ let mut rows = stmt_fetch_accounts.query([])?; while let Some(row) = rows.next()? { - let account: u32 = row.get(0)?; - let taddrs = - get_transparent_receivers(&self._params, transaction, AccountId::from(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 - )), - })?; + 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": &account, + ":account": u32::from(account), ":address": &taddr.encode(&self._params), ])?; } @@ -123,7 +122,114 @@ impl RusqliteMigration for Migration

{ } fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { - // TODO: something better than just panic? - panic!("Cannot revert this migration."); + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) + } +} + +#[cfg(feature = "transparent-inputs")] +fn get_transparent_receivers( + conn: &rusqlite::Connection, + params: &P, + account: AccountId, +) -> Result>, SqliteClientError> { + use crate::wallet::encoding::decode_diversifier_index_be; + + 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 = decode_diversifier_index_be(&row.get::<_, Vec>(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 index = NonHardenedChildIndex::from_index(u32::try_from(di).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 index 3d3a7fa95d..1572e1a0f3 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/addresses_table.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/addresses_table.rs @@ -1,41 +1,41 @@ use std::collections::HashSet; -use rusqlite::Transaction; -use schemer; -use schemer_rusqlite::RusqliteMigration; +use rusqlite::{named_params, Transaction}; +use schemerz_rusqlite::RusqliteMigration; use uuid::Uuid; -use zcash_client_backend::{address::RecipientAddress, keys::UnifiedFullViewingKey}; -use zcash_primitives::{consensus, zip32::AccountId}; -use crate::wallet::{add_account_internal, init::WalletMigrationError}; +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 zcash_primitives::legacy::keys::IncomingViewingKey; +use ::transparent::keys::IncomingViewingKey; use super::ufvk_support; /// The migration that removed the address columns from the `accounts` table, and created /// the `accounts` table. -/// -/// d956978c-9c87-4d6e-815d-fb8f088d094c -pub(super) const MIGRATION_ID: Uuid = Uuid::from_fields( - 0xd956978c, - 0x9c87, - 0x4d6e, - b"\x81\x5d\xfb\x8f\x08\x8d\x09\x4c", -); +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 schemer::Migration for Migration

{ +impl schemerz::Migration for Migration

{ fn id(&self) -> Uuid { MIGRATION_ID } fn dependencies(&self) -> HashSet { - [ufvk_support::MIGRATION_ID].into_iter().collect() + DEPENDENCIES.iter().copied().collect() } fn description(&self) -> &'static str { @@ -67,8 +67,9 @@ impl RusqliteMigration for Migration

{ let mut rows = stmt_fetch_accounts.query([])?; while let Some(row) = rows.next()? { - let account: u32 = row.get(0)?; - let account = AccountId::from(account); + 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) @@ -76,25 +77,27 @@ impl RusqliteMigration for Migration

{ // Verify that the address column contains the expected value. let address: String = row.get(2)?; - let decoded = RecipientAddress::decode(&self.params, &address).ok_or_else(|| { + 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 RecipientAddress::Unified(ua) = decoded { + 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(); + let (expected_address, idx) = ufvk.default_address( + UnifiedAddressRequest::unsafe_custom(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, - RecipientAddress::Unified(expected_address).encode(&self.params), + Address::Unified(expected_address).encode(&self.params), idx, ))); } @@ -102,16 +105,14 @@ impl RusqliteMigration for Migration

{ // 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 = - RecipientAddress::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 RecipientAddress::Transparent(addr) = + 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 @@ -152,13 +153,21 @@ impl RusqliteMigration for Migration

{ } } - add_account_internal::( - transaction, - &self.params, - "accounts_new", - account, - &ufvk, + 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(UnifiedAddressRequest::unsafe_custom( + Omit, + Require, + UA_TRANSPARENT, + ))?; + insert_address(transaction, &self.params, account, d_idx, &address)?; } transaction.execute_batch( @@ -170,7 +179,52 @@ impl RusqliteMigration for Migration

{ } fn down(&self, _transaction: &Transaction) -> Result<(), WalletMigrationError> { - // TODO: something better than just panic? - panic!("Cannot revert this migration."); + 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_default_transparent_address.rs b/zcash_client_sqlite/src/wallet/init/migrations/ensure_default_transparent_address.rs new file mode 100644 index 0000000000..37b6de0f4f --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/ensure_default_transparent_address.rs @@ -0,0 +1,50 @@ +//! Ensures that an external transparent address exists in the `addresses` table for each +//! non-hardened child index starting at index 0 and ending at the index corresponding to default +//! address for the account. + +use std::collections::HashSet; +use uuid::Uuid; + +use rusqlite::Transaction; +use schemerz_rusqlite::RusqliteMigration; +use zcash_protocol::consensus; + +use super::transparent_gap_limit_handling; +use crate::wallet::init::WalletMigrationError; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0x702cf97b_8395_4edc_b584_5c9f87f0ef35); + +const DEPENDENCIES: &[Uuid] = &[transparent_gap_limit_handling::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 the existence of transparent addresses in the range 0.." + } +} + +impl RusqliteMigration for Migration

{ + type Error = WalletMigrationError; + + fn up(&self, _conn: &Transaction) -> Result<(), WalletMigrationError> { + #[cfg(feature = "transparent-inputs")] + transparent_gap_limit_handling::insert_initial_transparent_addrs(_conn, &self._params)?; + + Ok(()) + } + + fn down(&self, _: &Transaction) -> Result<(), WalletMigrationError> { + Err(WalletMigrationError::CannotRevert(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..3d67c761c0 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/ensure_orchard_ua_receiver.rs @@ -0,0 +1,205 @@ +//! 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( + UnifiedAddressRequest::unsafe_custom(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::{ + testing::db::{test_clock, test_rng}, + wallet::init::{migrations::addresses_table, WalletMigrator}, + 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, + test_clock(), + test_rng(), + ) + .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!( + WalletMigrator::new() + .with_seed(SecretVec::new(seed.clone())) + .ignore_seed_relevance() + .init_or_migrate_to(&mut db_data, &[addresses_table::MIGRATION_ID]), + 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(UnifiedAddressRequest::unsafe_custom( + 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!( + WalletMigrator::new() + .with_seed(SecretVec::new(seed)) + .init_or_migrate(&mut db_data), + 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..70c6def147 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/ephemeral_addresses.rs @@ -0,0 +1,153 @@ +//! 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; + +use super::utxos_to_txos; + +#[cfg(feature = "transparent-inputs")] +use { + crate::{error::SqliteClientError, AccountRef, GapLimits}, + rusqlite::named_params, + transparent::keys::NonHardenedChildIndex, + zcash_keys::{ + encoding::AddressCodec, + keys::{AddressGenerationError, UnifiedFullViewingKey}, + }, + zip32::DiversifierIndex, +}; + +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." + } +} + +#[cfg(feature = "transparent-inputs")] +fn init_accounts( + transaction: &rusqlite::Transaction, + params: &P, +) -> Result<(), SqliteClientError> { + let ephemeral_gap_limit = GapLimits::default().ephemeral(); + + let mut stmt = transaction.prepare("SELECT id, ufvk FROM accounts")?; + let mut rows = stmt.query([])?; + while let Some(row) = rows.next()? { + let account_id = AccountRef(row.get(0)?); + let ufvk_str: Option = row.get(1)?; + if let Some(ufvk_str) = ufvk_str { + if let Some(tfvk) = UnifiedFullViewingKey::decode(params, &ufvk_str) + .map_err(SqliteClientError::CorruptedData)? + .transparent() + { + let ephemeral_ivk = tfvk.derive_ephemeral_ivk().map_err(|_| { + SqliteClientError::CorruptedData( + "Unexpected failure to derive ephemeral transparent IVK".to_owned(), + ) + })?; + + let mut ea_insert = transaction.prepare( + "INSERT INTO ephemeral_addresses (account_id, address_index, address) + VALUES (:account_id, :address_index, :address)", + )?; + + // NB: we have reduced the initial space of generated ephemeral addresses + // from 20 addresses to 5, as ephemeral addresses should always be used in + // a transaction immediately after being reserved, and as a consequence + // there is no significant benefit in having a larger gap limit. + for i in 0..ephemeral_gap_limit { + let address = ephemeral_ivk + .derive_ephemeral_address( + NonHardenedChildIndex::from_index(i).expect("index is valid"), + ) + .map_err(|_| { + AddressGenerationError::InvalidTransparentChildIndex( + DiversifierIndex::from(i), + ) + })?; + + ea_insert.execute(named_params! { + ":account_id": account_id.0, + ":address_index": i, + ":address": address.encode(params) + })?; + } + } + } + } + + Ok(()) +} + +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 `GapLimits::default().ephemeral()` ephemeral transparent addresses are + // stored in each account. + #[cfg(feature = "transparent-inputs")] + { + init_accounts(transaction, &self.params)?; + } + + 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..e25e0711e5 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/fix_bad_change_flagging.rs @@ -0,0 +1,78 @@ +//! 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 crate::{ + wallet::{ + init::{migrations::fix_broken_commitment_trees, WalletMigrationError}, + KeyScope, + }, + 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": KeyScope::INTERNAL.encode()}, + ) + }; + + 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..9d8f841177 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/fix_broken_commitment_trees.rs @@ -0,0 +1,90 @@ +//! 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}, +}; + +#[cfg(feature = "transparent-inputs")] +use crate::GapLimits; + +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, + #[cfg(feature = "transparent-inputs")] + &GapLimits::default(), + 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/fix_transparent_received_outputs.rs b/zcash_client_sqlite/src/wallet/init/migrations/fix_transparent_received_outputs.rs new file mode 100644 index 0000000000..3f5b43f3ea --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/fix_transparent_received_outputs.rs @@ -0,0 +1,87 @@ +//! Fixes the `transparent_received_outputs` table schema to not depend on feature flags. +use std::collections::HashSet; + +use schemerz_rusqlite::RusqliteMigration; +use uuid::Uuid; + +use crate::wallet::init::WalletMigrationError; + +use super::{ + ensure_default_transparent_address, fix_bad_change_flagging, v_transactions_additional_totals, +}; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0xb951587c_34fd_4f02_a313_05ff7adb6268); + +const DEPENDENCIES: &[Uuid] = &[ + fix_bad_change_flagging::MIGRATION_ID, + v_transactions_additional_totals::MIGRATION_ID, + ensure_default_transparent_address::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 the `transparent_received_outputs` table schema to not depend on feature flags" + } +} + +impl RusqliteMigration for Migration { + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + // This is the same table rewrite done in `transparent_gap_limit_handling`, but + // unconditionally. If the wallet ran `transparent_gap_limit_handling` with the + // `transparent-inputs` feature flag enabled, this will be a no-op. + transaction.execute_batch( + r#" + PRAGMA legacy_alter_table = ON; + + CREATE TABLE transparent_received_outputs_new ( + 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, + address_id INTEGER NOT NULL REFERENCES addresses(id), + FOREIGN KEY (transaction_id) REFERENCES transactions(id_tx), + FOREIGN KEY (account_id) REFERENCES accounts(id), + CONSTRAINT transparent_output_unique UNIQUE (transaction_id, output_index) + ); + INSERT INTO transparent_received_outputs_new SELECT * FROM transparent_received_outputs; + + DROP TABLE transparent_received_outputs; + ALTER TABLE transparent_received_outputs_new RENAME TO transparent_received_outputs; + + PRAGMA legacy_alter_table = OFF; + "#, + )?; + + Ok(()) + } + + fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + 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/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 index 8501368564..5433fcf676 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/initial_setup.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/initial_setup.rs @@ -1,26 +1,17 @@ //! The migration that performs the initial setup of the wallet database. use std::collections::HashSet; -use rusqlite; -use schemer; -use schemer_rusqlite::RusqliteMigration; +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. -/// -/// bc4f5e57-d600-4b6c-990f-b3538f0bfce1, -pub(super) const MIGRATION_ID: Uuid = Uuid::from_fields( - 0xbc4f5e57, - 0xd600, - 0x4b6c, - b"\x99\x0f\xb3\x53\x8f\x0b\xfc\xe1", -); +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0xbc4f5e57_d600_4b6c_990f_b3538f0bfce1); pub(super) struct Migration; -impl schemer::Migration for Migration { +impl schemerz::Migration for Migration { fn id(&self) -> Uuid { MIGRATION_ID } @@ -111,6 +102,16 @@ impl RusqliteMigration for Migration { fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { // We should never down-migrate the first migration, as that can irreversibly // destroy data. - panic!("Cannot revert the initial migration."); + 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 index 811a1a0e5e..de16facdaf 100644 --- 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 @@ -3,30 +3,25 @@ //! table prior to being mined. use std::collections::HashSet; -use rusqlite; -use schemer; -use schemer_rusqlite::RusqliteMigration; +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_fields( - 0xbdcdcedc, - 0x7b29, - 0x4f1c, - b"\x83\x07\x35\xf9\x37\xf0\xd3\x2a", -); +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 schemer::Migration for Migration { +impl schemerz::Migration for Migration { fn id(&self) -> Uuid { MIGRATION_ID } fn dependencies(&self) -> HashSet { - [v_transactions_net::MIGRATION_ID].into_iter().collect() + DEPENDENCIES.iter().copied().collect() } fn description(&self) -> &'static str { @@ -222,8 +217,7 @@ impl RusqliteMigration for Migration { } fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { - // TODO: something better than just panic? - panic!("Cannot revert this migration."); + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) } } @@ -232,37 +226,46 @@ mod tests { use rusqlite::{self, params}; use tempfile::NamedTempFile; - use zcash_client_backend::keys::UnifiedSpendingKey; - use zcash_primitives::zip32::AccountId; + use zcash_keys::keys::UnifiedSpendingKey; + use zcash_protocol::consensus::Network; + use zip32::AccountId; use crate::{ - tests, - wallet::init::{init_wallet_db_internal, migrations::v_transactions_net}, + testing::db::{test_clock, test_rng}, + wallet::init::{migrations::v_transactions_net, WalletMigrator}, WalletDb, }; #[test] fn received_notes_nullable_migration() { let data_file = NamedTempFile::new().unwrap(); - let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db_internal(&mut db_data, None, &[v_transactions_net::MIGRATION_ID]).unwrap(); + let mut db_data = WalletDb::for_path( + data_file.path(), + Network::TestNetwork, + test_clock(), + test_rng(), + ) + .unwrap(); + WalletMigrator::new() + .ignore_seed_relevance() + .init_or_migrate_to(&mut db_data, &[v_transactions_net::MIGRATION_ID]) + .unwrap(); // Create an account in the wallet - let usk0 = - UnifiedSpendingKey::from_seed(&tests::network(), &[0u8; 32][..], AccountId::from(0)) - .unwrap(); + 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(&tests::network())], + 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, ''); + "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) @@ -271,7 +274,10 @@ mod tests { VALUES (0, 3, 0, '', 5, '', 'nf_b', false);").unwrap(); // Apply the current migration - init_wallet_db_internal(&mut db_data, None, &[super::MIGRATION_ID]).unwrap(); + WalletMigrator::new() + .ignore_seed_relevance() + .init_or_migrate_to(&mut db_data, &[super::MIGRATION_ID]) + .unwrap(); { let mut q = db_data 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..75faeb2898 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/receiving_key_scopes.rs @@ -0,0 +1,924 @@ +//! 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}, + KeyScope, + }, + 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 {};", + KeyScope::EXTERNAL.encode() + ) + )?; + + // 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": KeyScope::INTERNAL.encode(), ":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": KeyScope::INTERNAL.encode(), ":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, + testing::db::{test_clock, test_rng}, + wallet::{ + init::{ + migrations::{add_account_birthdays, shardtree_support, wallet_summaries}, + WalletMigrator, + }, + memo_repr, + sapling::ReceivedSaplingOutput, + KeyScope, + }, + 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, test_clock(), test_rng()).unwrap(); + WalletMigrator::new() + .ignore_seed_relevance() + .init_or_migrate_to( + &mut db_data, + &[ + add_account_birthdays::MIGRATION_ID, + shardtree_support::MIGRATION_ID, + ], + ) + .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, + Some(height), + None, + 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 + WalletMigrator::new() + .ignore_seed_relevance() + .init_or_migrate_to(&mut db_data, &[super::MIGRATION_ID]) + .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 = KeyScope::decode(row.get(1).unwrap()).unwrap(); + match value { + EXTERNAL_VALUE => assert_eq!(scope, KeyScope::EXTERNAL), + INTERNAL_VALUE => assert_eq!(scope, KeyScope::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, test_clock(), test_rng()).unwrap(); + WalletMigrator::new() + .ignore_seed_relevance() + .init_or_migrate_to( + &mut db_data, + &[ + wallet_summaries::MIGRATION_ID, + shardtree_support::MIGRATION_ID, + ], + ) + .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 + WalletMigrator::new() + .ignore_seed_relevance() + .init_or_migrate_to(&mut db_data, &[super::MIGRATION_ID]) + .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 = KeyScope::decode(row.get(1).unwrap()).unwrap(); + match value { + EXTERNAL_VALUE => assert_eq!(scope, KeyScope::EXTERNAL), + INTERNAL_VALUE => assert_eq!(scope, KeyScope::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..b20468c6e2 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/sapling_memo_consistency.rs @@ -0,0 +1,237 @@ +//! 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, Some(block_height), None, &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 index ee191b9570..a549993eb7 100644 --- 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 @@ -2,33 +2,26 @@ //! on an internal address to the sent_notes table. use std::collections::HashSet; -use rusqlite; -use schemer; -use schemer_rusqlite::RusqliteMigration; +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. -/// -/// 0ddbe561-8259-4212-9ab7-66fdc4a74e1d -pub(super) const MIGRATION_ID: Uuid = Uuid::from_fields( - 0x0ddbe561, - 0x8259, - 0x4212, - b"\x9a\xb7\x66\xfd\xc4\xa7\x4e\x1d", -); +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 schemer::Migration for Migration { +impl schemerz::Migration for Migration { fn id(&self) -> Uuid { MIGRATION_ID } fn dependencies(&self) -> HashSet { - [ufvk_support::MIGRATION_ID].into_iter().collect() + DEPENDENCIES.iter().copied().collect() } fn description(&self) -> &'static str { @@ -81,7 +74,16 @@ impl RusqliteMigration for Migration { } fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { - // TODO: something better than just panic? - panic!("Cannot revert this migration."); + 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/transparent_gap_limit_handling.rs b/zcash_client_sqlite/src/wallet/init/migrations/transparent_gap_limit_handling.rs new file mode 100644 index 0000000000..affbe19db1 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/transparent_gap_limit_handling.rs @@ -0,0 +1,734 @@ +//! Add support for general transparent gap limit handling, and unify the `addresses` and +//! `ephemeral_addresses` tables. + +use rand_core::RngCore; +use std::collections::HashSet; +use std::rc::Rc; +use std::sync::Mutex; +use uuid::Uuid; + +use rusqlite::{named_params, Transaction}; +use schemerz_rusqlite::RusqliteMigration; + +use zcash_address::ZcashAddress; +use zcash_keys::keys::{UnifiedAddressRequest, UnifiedIncomingViewingKey}; +use zcash_protocol::consensus::{self, BlockHeight}; + +use super::add_account_uuids; +use crate::{ + util::Clock, + wallet::{self, encoding::ReceiverFlags, init::WalletMigrationError, KeyScope}, + AccountRef, +}; + +#[cfg(feature = "transparent-inputs")] +use { + crate::{ + wallet::{ + encoding::{decode_diversifier_index_be, encode_diversifier_index_be, epoch_seconds}, + transparent::{generate_address_range, generate_gap_addresses, next_check_time}, + }, + GapLimits, + }, + ::transparent::keys::{IncomingViewingKey as _, NonHardenedChildIndex}, + zcash_keys::{encoding::AddressCodec as _, keys::ReceiverRequirement}, + zcash_primitives::transaction::builder::DEFAULT_TX_EXPIRY_DELTA, + zip32::DiversifierIndex, +}; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0xc41dfc0e_e870_4859_be47_d2f572f5ca73); + +const DEPENDENCIES: &[Uuid] = &[add_account_uuids::MIGRATION_ID]; + +pub(super) struct Migration { + pub(super) params: P, + pub(super) _clock: C, + pub(super) _rng: Rc>, +} + +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 general transparent gap limit handling, unifying the `addresses` and `ephemeral_addresses` tables." + } +} + +// For each account, ensure that all diversifier indexes prior to that for the default +// address have corresponding cached transparent addresses. +#[cfg(feature = "transparent-inputs")] +pub(super) fn insert_initial_transparent_addrs( + conn: &rusqlite::Transaction, + params: &P, +) -> Result<(), WalletMigrationError> { + let mut min_addr_diversifiers = conn.prepare( + r#" + SELECT accounts.id AS account_id, + MIN(addresses.transparent_child_index) AS transparent_child_index, + MIN(addresses.diversifier_index_be) AS diversifier_index_be + FROM accounts + LEFT OUTER JOIN addresses + ON accounts.id = addresses.account_id + AND addresses.key_scope = :key_scope_external + GROUP BY accounts.id + "#, + )?; + + let mut min_addr_rows = min_addr_diversifiers.query(named_params![ + ":key_scope_external": KeyScope::EXTERNAL.encode() + ])?; + while let Some(row) = min_addr_rows.next()? { + let account_id = AccountRef(row.get("account_id")?); + + let min_transparent_idx = row + .get::<_, Option>("transparent_child_index")? + .map(|i| { + NonHardenedChildIndex::from_index(i).ok_or(WalletMigrationError::CorruptedData( + format!("{} is not a valid transparent child index.", i), + )) + }) + .transpose()?; + + let min_diversifier_idx = row + .get::<_, Option>>("diversifier_index_be")? + .map(|b| decode_diversifier_index_be(&b[..])) + .transpose()? + .and_then(|di| NonHardenedChildIndex::try_from(di).ok()); + + // Ensure that there is an address for each possible external address index prior to the + // default UA for the account. If the default address has a diversifier index greater than + // the gap limit, we generate transparent addresses up to that index but not beyond. + let start = NonHardenedChildIndex::const_from_index(0); + let end = std::cmp::min( + min_transparent_idx + .or(min_diversifier_idx) + // guarantee that we have an entry at index 0; this address will have previously + // been provided explicitly as one of the wallet's addresses in response to a call + // to `get_transparent_receivers, even if we have no other addresses generated + // (which shouldn't ordinarily be the case anyway) + .unwrap_or(NonHardenedChildIndex::const_from_index(1)), + NonHardenedChildIndex::from_index(GapLimits::default().external()) + .expect("default external gap limit fits in non-hardened child index space."), + ); + + generate_address_range( + conn, + params, + account_id, + KeyScope::EXTERNAL, + UnifiedAddressRequest::ALLOW_ALL, + start..end, + false, + )?; + } + + Ok(()) +} + +impl RusqliteMigration for Migration { + type Error = WalletMigrationError; + + fn up(&self, conn: &Transaction) -> Result<(), WalletMigrationError> { + let decode_uivk = |uivk_str: String| { + UnifiedIncomingViewingKey::decode(&self.params, &uivk_str).map_err(|e| { + WalletMigrationError::CorruptedData(format!( + "Invalid UIVK encoding {}: {}", + uivk_str, e + )) + }) + }; + + let external_scope_code = KeyScope::EXTERNAL.encode(); + + conn.execute_batch(&format!( + r#" + ALTER TABLE addresses ADD COLUMN key_scope INTEGER NOT NULL DEFAULT {external_scope_code}; + ALTER TABLE addresses ADD COLUMN transparent_child_index INTEGER; + ALTER TABLE addresses ADD COLUMN exposed_at_height INTEGER; + ALTER TABLE addresses ADD COLUMN receiver_flags INTEGER; + "# + ))?; + + let mut account_ids = HashSet::new(); + + { + // If the diversifier index is in the valid range of non-hardened child indices, set + // `transparent_child_index` so that we can use it for gap limit handling. + // No `DISTINCT` is necessary here due to the preexisting UNIQUE(account_id, + // diversifier_index_be) constraint. + let mut di_query = conn.prepare( + r#" + SELECT + account_id, + address, + accounts.uivk AS uivk, + diversifier_index_be, + accounts.birthday_height + FROM addresses + JOIN accounts ON accounts.id = account_id + "#, + )?; + let mut rows = di_query.query([])?; + while let Some(row) = rows.next()? { + let account_id: i64 = row.get("account_id")?; + account_ids.insert(account_id); + + let addr_str: String = row.get("address")?; + let address = ZcashAddress::try_from_encoded(&addr_str).map_err(|e| { + WalletMigrationError::CorruptedData(format!( + "Encoded address {} is not a valid zcash address: {}", + addr_str, e + )) + })?; + let receiver_flags = address.convert::().map_err(|_| { + WalletMigrationError::CorruptedData("Unexpected address type".to_string()) + })?; + let di_be: Vec = row.get("diversifier_index_be")?; + let account_birthday: i64 = row.get("birthday_height")?; + + let update_without_taddr = || { + conn.execute( + r#" + UPDATE addresses + SET exposed_at_height = :account_birthday, + receiver_flags = :receiver_flags + WHERE account_id = :account_id + AND diversifier_index_be = :diversifier_index_be + "#, + named_params! { + ":account_id": account_id, + ":diversifier_index_be": &di_be[..], + ":account_birthday": account_birthday, + ":receiver_flags": receiver_flags.bits(), + }, + ) + }; + + #[cfg(feature = "transparent-inputs")] + { + let uivk = decode_uivk(row.get("uivk")?)?; + let diversifier_index = decode_diversifier_index_be(&di_be)?; + let transparent_external = NonHardenedChildIndex::try_from(diversifier_index) + .ok() + .and_then(|idx| { + uivk.transparent() + .as_ref() + .and_then(|external_ivk| external_ivk.derive_address(idx).ok()) + .map(|t_addr| (idx, t_addr.encode(&self.params))) + }); + + // Add transparent address index metadata and the transparent address corresponding + // to the index to the addresses table. We unconditionally set the cached + // transparent receiver address in order to simplify gap limit handling; even if a + // unified address is generated without a transparent receiver, we still assume + // that a transparent-only wallet for which we have imported the seed may have + // generated an address at that index. + if let Some((idx, t_addr)) = transparent_external { + conn.execute( + r#" + UPDATE addresses + SET transparent_child_index = :transparent_child_index, + cached_transparent_receiver_address = :t_addr, + exposed_at_height = :account_birthday, + receiver_flags = :receiver_flags + WHERE account_id = :account_id + AND diversifier_index_be = :diversifier_index_be + "#, + named_params! { + ":account_id": account_id, + ":diversifier_index_be": &di_be[..], + ":transparent_child_index": idx.index(), + ":t_addr": t_addr, + ":account_birthday": account_birthday, + ":receiver_flags": receiver_flags.bits(), + }, + )?; + } else { + update_without_taddr()?; + } + } + + #[cfg(not(feature = "transparent-inputs"))] + { + update_without_taddr()?; + } + } + }; + + // We now have to re-create the `addresses` table in order to fix the constraints. Note + // that we do not include the `seen_in_tx` column as this is duplicative of information + // that can be discovered via joins with the various `*_received_{notes|outputs}` tables, + // which we will create a view to perform below. The `used_in_tx` column data is used only + // to determine the height at which the address was exposed (for which we use the target + // height for the transaction.) + conn.execute_batch(r#" + CREATE TABLE addresses_new ( + id INTEGER NOT NULL PRIMARY KEY, + account_id INTEGER NOT NULL, + key_scope INTEGER NOT NULL, + diversifier_index_be BLOB NOT NULL, + address TEXT NOT NULL, + transparent_child_index INTEGER, + cached_transparent_receiver_address TEXT, + exposed_at_height INTEGER, + receiver_flags INTEGER NOT NULL, + transparent_receiver_next_check_time INTEGER, + FOREIGN KEY (account_id) REFERENCES accounts(id), + CONSTRAINT diversification UNIQUE (account_id, key_scope, diversifier_index_be), + CONSTRAINT transparent_index_consistency CHECK ( + (transparent_child_index IS NOT NULL) == (cached_transparent_receiver_address IS NOT NULL) + ) + ); + + -- we will only set `transparent_receiver_next_check_time` for ephemeral addresses + INSERT INTO addresses_new ( + account_id, key_scope, diversifier_index_be, address, + transparent_child_index, cached_transparent_receiver_address, + exposed_at_height, receiver_flags + ) + SELECT + account_id, key_scope, diversifier_index_be, address, + transparent_child_index, cached_transparent_receiver_address, + exposed_at_height, receiver_flags + FROM addresses; + "#)?; + + // Now, we add the ephemeral addresses to the newly unified `addresses` table. + #[cfg(feature = "transparent-inputs")] + { + let mut ea_insert = conn.prepare( + r#" + INSERT INTO addresses_new ( + account_id, key_scope, diversifier_index_be, address, + transparent_child_index, cached_transparent_receiver_address, + exposed_at_height, receiver_flags, + transparent_receiver_next_check_time + ) VALUES ( + :account_id, :key_scope, :diversifier_index_be, :address, + :transparent_child_index, :cached_transparent_receiver_address, + :exposed_at_height, :receiver_flags, + :transparent_receiver_next_check_time + ) + "#, + )?; + + let mut ea_query = conn.prepare( + r#" + SELECT + account_id, address_index, address, + t.expiry_height - :expiry_delta AS exposed_at_height + FROM ephemeral_addresses ea + LEFT OUTER JOIN transactions t ON t.id_tx = ea.used_in_tx + "#, + )?; + let rows = ea_query + .query_and_then( + named_params! {":expiry_delta": DEFAULT_TX_EXPIRY_DELTA }, + |row| { + let account_id: i64 = row.get("account_id")?; + let transparent_child_index = row.get::<_, i64>("address_index")?; + let diversifier_index = DiversifierIndex::from( + u32::try_from(transparent_child_index) + .ok() + .and_then(NonHardenedChildIndex::from_index) + .ok_or(WalletMigrationError::CorruptedData( + "ephemeral address indices must be in the range of `u31`" + .to_owned(), + ))? + .index(), + ); + let address: String = row.get("address")?; + let exposed_at_height: Option = row.get("exposed_at_height")?; + Ok(( + account_id, + diversifier_index, + transparent_child_index, + address, + exposed_at_height, + )) + }, + )? + .collect::, WalletMigrationError>>()?; + + let ephemeral_address_count = + u32::try_from(rows.len()).expect("number of ephemeral addrs fits into u32"); + let mut check_time = self._clock.now(); + for ( + account_id, + diversifier_index, + transparent_child_index, + address, + exposed_at_height, + ) in rows + { + // Compute a next check time for the address such that, when considered in the + // context of all other allocated ephemeral addresses, it will be checked once per + // day. + let next_check_time = { + let rng = self + ._rng + .lock() + .expect("can obtain write lock to shared rng"); + + next_check_time(rng, check_time, (24 * 60 * 60) / ephemeral_address_count) + .expect("computed next check time is valid") + }; + let next_check_epoch_seconds = epoch_seconds(next_check_time).unwrap(); + + // We set both the `address` column and the `cached_transparent_receiver_address` + // column to the same value here; there is no Unified address that corresponds to + // this transparent address. + ea_insert.execute(named_params! { + ":account_id": account_id, + ":key_scope": KeyScope::Ephemeral.encode(), + ":diversifier_index_be": encode_diversifier_index_be(diversifier_index), + ":address": address, + ":transparent_child_index": transparent_child_index, + ":cached_transparent_receiver_address": address, + ":exposed_at_height": exposed_at_height, + ":receiver_flags": ReceiverFlags::P2PKH.bits(), + ":transparent_receiver_next_check_time": next_check_epoch_seconds + })?; + + account_ids.insert(account_id); + check_time = next_check_time; + } + } + + conn.execute_batch( + r#" + PRAGMA legacy_alter_table = ON; + + DROP TABLE addresses; + ALTER TABLE addresses_new RENAME TO addresses; + CREATE INDEX idx_addresses_accounts ON addresses ( + account_id ASC + ); + CREATE INDEX idx_addresses_indices ON addresses ( + diversifier_index_be ASC + ); + CREATE INDEX idx_addresses_t_indices ON addresses ( + transparent_child_index ASC + ); + + DROP TABLE ephemeral_addresses; + + PRAGMA legacy_alter_table = OFF; + "#, + )?; + + #[cfg(feature = "transparent-inputs")] + insert_initial_transparent_addrs(conn, &self.params)?; + + // Add foreign key references from the *_received_{notes|outputs} tables to the addresses + // table to make it possible to identify which address was involved. These foreign key + // columns must be nullable as for shielded account-internal. Ideally the foreign key + // relationship between `transparent_received_outputs` and `addresses` would not be + // nullable, but we allow it to be so here in order to avoid having to re-create that + // table. + // + // While it would be possible to only add the address reference to + // `transparent_received_outputs`, that would mean that a note received at a shielded + // component of a diversified Unified Address would not update the position of the + // transparent "address gap". Since we will include shielded address indices in the gap + // computation, transparent-only wallets may not be able to discover all transparent funds, + // but users of shielded wallets will be guaranteed to be able to recover all of their + // funds. + conn.execute_batch( + r#" + ALTER TABLE orchard_received_notes + ADD COLUMN address_id INTEGER REFERENCES addresses(id); + ALTER TABLE sapling_received_notes + ADD COLUMN address_id INTEGER REFERENCES addresses(id); + ALTER TABLE transparent_received_outputs + ADD COLUMN address_id INTEGER REFERENCES addresses(id); + "#, + )?; + + // Ensure that an address exists for each received Orchard note, and populate the + // `address_id` column. + #[cfg(feature = "orchard")] + { + let mut stmt_rn_diversifiers = conn.prepare( + r#" + SELECT orn.id, orn.account_id, accounts.uivk, + orn.recipient_key_scope, orn.diversifier, t.mined_height + FROM orchard_received_notes orn + JOIN accounts ON accounts.id = account_id + JOIN transactions t on t.id_tx = orn.tx + "#, + )?; + + let mut rows = stmt_rn_diversifiers.query([])?; + while let Some(row) = rows.next()? { + let scope = KeyScope::decode(row.get("recipient_key_scope")?)?; + // for Orchard and Sapling, we only store addresses for externally-scoped keys. + if scope == KeyScope::EXTERNAL { + let row_id: i64 = row.get("id")?; + let account_id = AccountRef(row.get("account_id")?); + let mined_height = row + .get::<_, Option>("mined_height")? + .map(BlockHeight::from); + + let uivk = decode_uivk(row.get("uivk")?)?; + let diversifier = + orchard::keys::Diversifier::from_bytes(row.get("diversifier")?); + + // TODO: It's annoying that `IncomingViewingKey` doesn't expose the ability to + // decrypt the diversifier to find the index directly, and doesn't provide an + // accessor for `dk`. We already know we have the right IVK. + let ivk = uivk + .orchard() + .as_ref() + .expect("previously received an Orchard output"); + let di = ivk + .diversifier_index(&ivk.address(diversifier)) + .expect("roundtrip"); + let ua = uivk.address(di, UnifiedAddressRequest::ALLOW_ALL)?; + let address_id = wallet::upsert_address( + conn, + &self.params, + account_id, + di, + &ua, + mined_height, + false, + )?; + + conn.execute( + "UPDATE orchard_received_notes + SET address_id = :address_id + WHERE id = :row_id", + named_params! { + ":address_id": address_id.0, + ":row_id": row_id + }, + )?; + } + } + } + + // Ensure that an address exists for each received Sapling note, and populate the + // `address_id` column. + { + let mut stmt_rn_diversifiers = conn.prepare( + r#" + SELECT srn.id, srn.account_id, accounts.uivk, + srn.recipient_key_scope, srn.diversifier, t.mined_height + FROM sapling_received_notes srn + JOIN accounts ON accounts.id = account_id + JOIN transactions t ON t.id_tx = srn.tx + "#, + )?; + + let mut rows = stmt_rn_diversifiers.query([])?; + while let Some(row) = rows.next()? { + let scope = KeyScope::decode(row.get("recipient_key_scope")?)?; + // for Orchard and Sapling, we only store addresses for externally-scoped keys. + if scope == KeyScope::EXTERNAL { + let row_id: i64 = row.get("id")?; + let account_id = AccountRef(row.get("account_id")?); + let mined_height = row + .get::<_, Option>("mined_height")? + .map(BlockHeight::from); + + let uivk = decode_uivk(row.get("uivk")?)?; + let diversifier = sapling::Diversifier(row.get("diversifier")?); + + // TODO: It's annoying that `IncomingViewingKey` doesn't expose the ability to + // decrypt the diversifier to find the index directly, and doesn't provide an + // accessor for `dk`. We already know we have the right IVK. + let ivk = uivk + .sapling() + .as_ref() + .expect("previously received a Sapling output"); + let di = ivk + .decrypt_diversifier( + &ivk.address(diversifier) + .expect("previously generated an address"), + ) + .expect("roundtrip"); + let ua = uivk.address(di, UnifiedAddressRequest::ALLOW_ALL)?; + let address_id = wallet::upsert_address( + conn, + &self.params, + account_id, + di, + &ua, + mined_height, + false, + )?; + + conn.execute( + "UPDATE sapling_received_notes + SET address_id = :address_id + WHERE id = :row_id", + named_params! { + ":address_id": address_id.0, + ":row_id": row_id + }, + )?; + } + } + } + + // At this point, every address on which we've received a transparent output should have a + // corresponding row in the `addresses` table with a valid + // `cached_transparent_receiver_address` entry, because we will only have queried the light + // wallet server for outputs from exactly these addresses. So for transparent outputs, we + // join to the addresses table using the address itself in order to obtain the address index. + #[cfg(feature = "transparent-inputs")] + { + conn.execute_batch( + r#" + PRAGMA legacy_alter_table = ON; + + UPDATE transparent_received_outputs + SET address_id = addresses.id + FROM addresses + WHERE addresses.cached_transparent_receiver_address = transparent_received_outputs.address; + + CREATE TABLE transparent_received_outputs_new ( + 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, + address_id INTEGER NOT NULL REFERENCES addresses(id), + FOREIGN KEY (transaction_id) REFERENCES transactions(id_tx), + FOREIGN KEY (account_id) REFERENCES accounts(id), + CONSTRAINT transparent_output_unique UNIQUE (transaction_id, output_index) + ); + INSERT INTO transparent_received_outputs_new SELECT * FROM transparent_received_outputs; + + DROP TABLE transparent_received_outputs; + ALTER TABLE transparent_received_outputs_new RENAME TO transparent_received_outputs; + + PRAGMA legacy_alter_table = OFF; + "#, + )?; + } + + // Construct a view that identifies the minimum block height at which each address was + // first used + conn.execute_batch( + r#" + CREATE VIEW v_address_uses AS + SELECT orn.address_id, orn.account_id, orn.tx AS transaction_id, t.mined_height, + a.key_scope, a.diversifier_index_be, a.transparent_child_index + FROM orchard_received_notes orn + JOIN addresses a ON a.id = orn.address_id + JOIN transactions t ON t.id_tx = orn.tx + UNION + SELECT srn.address_id, srn.account_id, srn.tx AS transaction_id, t.mined_height, + a.key_scope, a.diversifier_index_be, a.transparent_child_index + FROM sapling_received_notes srn + JOIN addresses a ON a.id = srn.address_id + JOIN transactions t ON t.id_tx = srn.tx + UNION + SELECT tro.address_id, tro.account_id, tro.transaction_id, t.mined_height, + a.key_scope, a.diversifier_index_be, a.transparent_child_index + FROM transparent_received_outputs tro + JOIN addresses a ON a.id = tro.address_id + JOIN transactions t ON t.id_tx = tro.transaction_id; + + CREATE VIEW v_address_first_use AS + SELECT + address_id, + account_id, + key_scope, + diversifier_index_be, + transparent_child_index, + MIN(mined_height) AS first_use_height + FROM v_address_uses + GROUP BY + address_id, account_id, key_scope, + diversifier_index_be, transparent_child_index; + + DROP VIEW v_received_outputs; + 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, + sapling_received_notes.address_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, + orchard_received_notes.address_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, + u.address_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); + "#, + )?; + + // At this point, we have completed updating the infrastructure for gap limit handling, + // so we can regenerate the gap limit worth of addresses for each account that we + // recorded. + #[cfg(feature = "transparent-inputs")] + for account_id in account_ids { + for key_scope in [KeyScope::EXTERNAL, KeyScope::INTERNAL] { + use ReceiverRequirement::*; + generate_gap_addresses( + conn, + &self.params, + AccountRef(account_id), + key_scope, + &GapLimits::default(), + UnifiedAddressRequest::unsafe_custom(Allow, Allow, Require), + false, + )?; + } + } + + Ok(()) + } + + fn down(&self, _: &Transaction) -> Result<(), WalletMigrationError> { + Err(WalletMigrationError::CannotRevert(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..9778b48971 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/tx_retrieval_queue.rs @@ -0,0 +1,314 @@ +//! 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::{ + testing::db::{test_clock, test_rng}, + wallet::init::{migrations::tests::test_migrate, WalletMigrator}, + 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, + test_clock(), + test_rng(), + ) + .unwrap(); + + let seed_bytes = vec![0xab; 32]; + + // Migrate to database state just prior to this migration. + WalletMigrator::new() + .with_seed(Secret::new(seed_bytes.clone())) + .ignore_seed_relevance() + .init_or_migrate_to(&mut db_data, DEPENDENCIES) + .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::V5, + BranchId::Nu5, + 0, + 12345678.into(), + #[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))] + Zatoshis::ZERO, + 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. + WalletMigrator::new() + .with_seed(Secret::new(seed_bytes)) + .ignore_seed_relevance() + .init_or_migrate_to(&mut db_data, &[MIGRATION_ID]) + .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 index def63a91d4..e2839ff983 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs @@ -1,47 +1,48 @@ //! Migration that adds support for unified full viewing keys. -use std::collections::HashSet; +use std::{collections::HashSet, rc::Rc}; -use rusqlite::{self, named_params, params}; -use schemer; -use schemer_rusqlite::RusqliteMigration; +use rusqlite::{named_params, params}; +use schemerz_rusqlite::RusqliteMigration; use secrecy::{ExposeSecret, SecretVec}; use uuid::Uuid; -use zcash_client_backend::{ - address::RecipientAddress, data_api::PoolType, keys::UnifiedSpendingKey, +use zcash_keys::{ + address::Address, + keys::{ReceiverRequirement::*, UnifiedAddressRequest, UnifiedSpendingKey}, }; -use zcash_primitives::{consensus, zip32::AccountId}; +use zcash_protocol::{consensus, PoolType}; +use zip32::AccountId; #[cfg(feature = "transparent-inputs")] -use zcash_primitives::legacy::keys::IncomingViewingKey; +use ::transparent::keys::IncomingViewingKey; #[cfg(feature = "transparent-inputs")] -use zcash_client_backend::encoding::AddressCodec; - -use crate::wallet::{ - init::{migrations::initial_setup, WalletMigrationError}, - pool_code, +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_fields( - 0xbe57ef3b, - 0x388e, - 0x42ea, - b"\x97\xe2\x67\x8d\xaf\xcf\x97\x54", -); +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>, + pub(super) seed: Option>>, } -impl

schemer::Migration for Migration

{ +impl

schemerz::Migration for Migration

{ fn id(&self) -> Uuid { MIGRATION_ID } fn dependencies(&self) -> HashSet { - [initial_setup::MIGRATION_ID].into_iter().collect() + DEPENDENCIES.iter().copied().collect() } fn description(&self) -> &'static str { @@ -69,6 +70,22 @@ impl RusqliteMigration for Migration

{ 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_custom(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 @@ -76,52 +93,71 @@ impl RusqliteMigration for Migration

{ // migration is being used to initialize an empty database. if let Some(seed) = &self.seed { let account: u32 = row.get(0)?; - let account = AccountId::from(account); + 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) - .unwrap(); + .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 = - RecipientAddress::decode(&self.params, &address).ok_or_else(|| { - WalletMigrationError::CorruptedData(format!( - "Could not decode {} as a valid Zcash address.", - address - )) - })?; + let decoded = Address::decode(&self.params, &address).ok_or_else(|| { + WalletMigrationError::CorruptedData(format!( + "Could not decode {} as a valid Zcash address.", + address + )) + })?; match decoded { - RecipientAddress::Shielded(decoded_address) => { - let dfvk = ufvk.sapling().expect( - "Derivation should have produced a UFVK containing a Sapling component.", - ); + 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(WalletMigrationError::CorruptedData( + return Err(if seed_is_relevant { + WalletMigrationError::CorruptedData( format!("Decoded Sapling address {} does not match the ufvk's Sapling address {} at {:?}.", address, - RecipientAddress::Shielded(expected_address).encode(&self.params), - idx))); + Address::Sapling(expected_address).encode(&self.params), + idx)) + } else { + WalletMigrationError::SeedNotRelevant + }); } } - RecipientAddress::Transparent(_) => { + Address::Transparent(_) | Address::Tex(_) => { return Err(WalletMigrationError::CorruptedData( "Address field value decoded to a transparent address; should have been Sapling or unified.".to_string())); } - RecipientAddress::Unified(decoded_address) => { - let (expected_address, idx) = ufvk.default_address(); + Address::Unified(decoded_address) => { + let (expected_address, idx) = ufvk.default_address(ua_request)?; if decoded_address != expected_address { - return Err(WalletMigrationError::CorruptedData( + return Err(if seed_is_relevant { + WalletMigrationError::CorruptedData( format!("Decoded unified address {} does not match the ufvk's default address {} at {:?}.", address, - RecipientAddress::Unified(expected_address).encode(&self.params), - idx))); + 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().0.encode(&self.params); + let address_str: String = ufvk.default_address(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 @@ -221,17 +257,18 @@ impl RusqliteMigration for Migration

{ let value: i64 = row.get(5)?; let memo: Option> = row.get(6)?; - let decoded_address = - RecipientAddress::decode(&self.params, &address).ok_or_else(|| { - WalletMigrationError::CorruptedData(format!( - "Could not decode {} as a valid Zcash address.", - address - )) - })?; + 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 { - RecipientAddress::Shielded(_) => Ok(pool_code(PoolType::Sapling)), - RecipientAddress::Transparent(_) => Ok(pool_code(PoolType::Transparent)), - RecipientAddress::Unified(_) => Err(WalletMigrationError::CorruptedData( + 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(), )), @@ -259,7 +296,16 @@ impl RusqliteMigration for Migration

{ } fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { - // TODO: something better than just panic? - panic!("Cannot revert this migration."); + 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 index 7180394292..e206a26dd4 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/utxos_table.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/utxos_table.rs @@ -1,29 +1,24 @@ //! The migration that adds initial support for transparent UTXOs to the wallet. use std::collections::HashSet; -use rusqlite; -use schemer; -use schemer_rusqlite::RusqliteMigration; +use schemerz_rusqlite::RusqliteMigration; use uuid::Uuid; use crate::wallet::init::{migrations::initial_setup, WalletMigrationError}; -pub(super) const MIGRATION_ID: Uuid = Uuid::from_fields( - 0xa2e0ed2e, - 0x8852, - 0x475e, - b"\xb0\xa4\xf1\x54\xb1\x5b\x9d\xbe", -); +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 schemer::Migration for Migration { +impl schemerz::Migration for Migration { fn id(&self) -> Uuid { MIGRATION_ID } fn dependencies(&self) -> HashSet { - [initial_setup::MIGRATION_ID].into_iter().collect() + DEPENDENCIES.iter().copied().collect() } fn description(&self) -> &'static str { @@ -57,3 +52,13 @@ impl RusqliteMigration for Migration { 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_additional_totals.rs b/zcash_client_sqlite/src/wallet/init/migrations/v_transactions_additional_totals.rs new file mode 100644 index 0000000000..fa1c53c693 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/v_transactions_additional_totals.rs @@ -0,0 +1,240 @@ +//! This migration adds `total_spent` and `total_received` columns to the `v_transactions` view to +//! aid wallets in distinguishing shielding transactions. + +use std::collections::HashSet; + +use schemerz_rusqlite::RusqliteMigration; +use uuid::Uuid; + +use crate::wallet::init::WalletMigrationError; + +use super::add_account_uuids; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0x7f2fd1b3_1872_4b90_88ba_7d02b470090f); + +const DEPENDENCIES: &[Uuid] = &[add_account_uuids::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 total_spent and total_received columns to the v_transactions view." + } +} + +impl RusqliteMigration for Migration { + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), Self::Error> { + transaction.execute_batch( + r#" + 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, + ro.value AS received_value, + 0 AS spent_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, + 0 AS received_value, + ro.value AS spent_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, + SUM(notes.spent_value) AS total_spent, + SUM(notes.received_value) AS total_received, + 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/v_transactions_net.rs b/zcash_client_sqlite/src/wallet/init/migrations/v_transactions_net.rs index 10c3a26a9b..dcdee99815 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/v_transactions_net.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/v_transactions_net.rs @@ -2,30 +2,28 @@ //! as received value. use std::collections::HashSet; -use rusqlite::{self, named_params}; -use schemer; -use schemer_rusqlite::RusqliteMigration; +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, PoolType}; +use crate::wallet::{init::WalletMigrationError, pool_code}; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0x2aa4d24f_51aa_4a4c_8d9b_e5b8a762865f); -pub(super) const MIGRATION_ID: Uuid = Uuid::from_fields( - 0x2aa4d24f, - 0x51aa, - 0x4a4c, - b"\x8d\x9b\xe5\xb8\xa7\x62\x86\x5f", -); +const DEPENDENCIES: &[Uuid] = &[add_transaction_views::MIGRATION_ID]; pub(crate) struct Migration; -impl schemer::Migration for Migration { +impl schemerz::Migration for Migration { fn id(&self) -> Uuid { MIGRATION_ID } fn dependencies(&self) -> HashSet { - [add_transaction_views::MIGRATION_ID].into_iter().collect() + DEPENDENCIES.iter().copied().collect() } fn description(&self) -> &'static str { @@ -48,7 +46,7 @@ impl RusqliteMigration for Migration { SELECT tx, :output_pool, output_index, from_account, from_account, value FROM sent_notes", named_params![ - ":output_pool": &pool_code(PoolType::Sapling) + ":output_pool": &pool_code(PoolType::SAPLING) ] )?; @@ -200,8 +198,7 @@ impl RusqliteMigration for Migration { } fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { - // TODO: something better than just panic? - panic!("Cannot revert this migration."); + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) } } @@ -210,50 +207,61 @@ mod tests { use rusqlite::{self, params}; use tempfile::NamedTempFile; - use zcash_client_backend::keys::UnifiedSpendingKey; - use zcash_primitives::zip32::AccountId; + use zcash_keys::keys::UnifiedSpendingKey; + use zcash_protocol::consensus::Network; + use zip32::AccountId; use crate::{ - tests, - wallet::init::{init_wallet_db_internal, migrations::add_transaction_views}, + testing::db::{test_clock, test_rng}, + wallet::init::{migrations::add_transaction_views, WalletMigrator}, WalletDb, }; #[test] fn v_transactions_net() { let data_file = NamedTempFile::new().unwrap(); - let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db_internal(&mut db_data, None, &[add_transaction_views::MIGRATION_ID]) + let mut db_data = WalletDb::for_path( + data_file.path(), + Network::TestNetwork, + test_clock(), + test_rng(), + ) + .unwrap(); + WalletMigrator::new() + .ignore_seed_relevance() + .init_or_migrate_to(&mut db_data, &[add_transaction_views::MIGRATION_ID]) .unwrap(); // Create two accounts in the wallet. - let usk0 = - UnifiedSpendingKey::from_seed(&tests::network(), &[0u8; 32][..], AccountId::from(0)) - .unwrap(); + 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(&tests::network())], + params![ufvk0.encode(&db_data.params)], ) .unwrap(); - let usk1 = - UnifiedSpendingKey::from_seed(&tests::network(), &[1u8; 32][..], AccountId::from(1)) - .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(&tests::network())], + 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, ''); + "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) @@ -265,7 +273,7 @@ mod tests { // 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, ''); + "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) @@ -279,7 +287,7 @@ mod tests { // 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, ''); + "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) @@ -297,7 +305,7 @@ mod tests { // - 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, ''); + "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) @@ -387,7 +395,10 @@ mod tests { } // Run this migration - init_wallet_db_internal(&mut db_data, None, &[super::MIGRATION_ID]).unwrap(); + WalletMigrator::new() + .ignore_seed_relevance() + .init_or_migrate_to(&mut db_data, &[super::MIGRATION_ID]) + .unwrap(); // Corrected behavior after v_transactions has been updated { 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..a797f0ad7a --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/v_transactions_note_uniqueness.rs @@ -0,0 +1,270 @@ +//! 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 rand_chacha::ChaChaRng; + use rusqlite::{self, params}; + use tempfile::NamedTempFile; + + use zcash_keys::keys::UnifiedSpendingKey; + use zcash_protocol::consensus::Network; + use zip32::AccountId; + + use crate::{ + testing::db::{test_clock, test_rng}, + util::testing::FixedClock, + wallet::init::{migrations::v_transactions_net, WalletMigrator}, + 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, + test_clock(), + test_rng(), + ) + .unwrap(); + WalletMigrator::new() + .ignore_seed_relevance() + .init_or_migrate_to(&mut db_data, &[v_transactions_net::MIGRATION_ID]) + .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< + rusqlite::Connection, + Network, + FixedClock, + ChaChaRng, + >, + 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. + WalletMigrator::new() + .ignore_seed_relevance() + .init_or_migrate_to(&mut db_data, &[super::MIGRATION_ID]) + .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..f1771ae218 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/orchard.rs @@ -0,0 +1,611 @@ +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}; + +use zcash_client_backend::{ + data_api::{Account as _, NullifierQuery, TargetValue}, + wallet::{ReceivedNote, WalletOrchardOutput}, + DecryptedOutput, TransferType, +}; +use zcash_keys::keys::{UnifiedAddressRequest, UnifiedFullViewingKey}; +use zcash_primitives::transaction::TxId; +use zcash_protocol::{ + consensus::{self, BlockHeight}, + memo::MemoBytes, + ShieldedProtocol, +}; +use zip32::Scope; + +use crate::{error::SqliteClientError, AccountRef, AccountUuid, AddressRef, ReceivedNoteId, TxRef}; + +use super::{ + common::UnspentNoteMeta, get_account, get_account_ref, memo_repr, upsert_address, KeyScope, +}; + +/// 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 = zip32::Scope::try_from(KeyScope::decode(scope_code)?) + .map_err(|_| { + 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: TargetValue, + 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, + ) +} + +pub(crate) fn ensure_address< + T: ReceivedOrchardOutput, + P: consensus::Parameters, +>( + conn: &rusqlite::Transaction, + params: &P, + output: &T, + exposure_height: Option, +) -> Result, SqliteClientError> { + if output.recipient_key_scope() != Some(Scope::Internal) { + let account = get_account(conn, params, output.account_id())? + .ok_or(SqliteClientError::AccountUnknown)?; + + let uivk = account.uivk(); + let ivk = uivk + .orchard() + .as_ref() + .expect("uivk decrypted this output."); + let to = output.note().recipient(); + let diversifier_index = ivk + .diversifier_index(&to) + .expect("address corresponds to account"); + + let ua = account + .uivk() + .address(diversifier_index, UnifiedAddressRequest::ALLOW_ALL)?; + upsert_address( + conn, + params, + account.internal_id(), + diversifier_index, + &ua, + exposure_height, + false, + ) + .map(Some) + } else { + Ok(None) + } +} + +pub(crate) fn select_unspent_note_meta( + conn: &Connection, + chain_tip_height: BlockHeight, + wallet_birthday: BlockHeight, +) -> Result, SqliteClientError> { + super::common::select_unspent_note_meta( + conn, + ShieldedProtocol::Orchard, + chain_tip_height, + wallet_birthday, + ) +} + +/// 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. +/// +/// Returns the internal account identifier of the account that received the output. +pub(crate) fn put_received_note< + T: ReceivedOrchardOutput, + P: consensus::Parameters, +>( + conn: &rusqlite::Transaction, + params: &P, + output: &T, + tx_ref: TxRef, + target_or_mined_height: Option, + spent_in: Option, +) -> Result { + let account_id = get_account_ref(conn, output.account_id())?; + let address_id = ensure_address(conn, params, output, target_or_mined_height)?; + let mut stmt_upsert_received_note = conn.prepare_cached( + "INSERT INTO orchard_received_notes ( + tx, action_index, account_id, address_id, + diversifier, value, rho, rseed, memo, nf, + is_change, commitment_tree_position, + recipient_key_scope + ) + VALUES ( + :tx, :action_index, :account_id, :address_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, + address_id = :address_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, + ":address_id": address_id.map(|a| a.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(|s| KeyScope::from(s).encode()), + ]; + + 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(account_id) +} + +/// 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 index a18281dc87..4c71104a9c 100644 --- a/zcash_client_sqlite/src/wallet/sapling.rs +++ b/zcash_client_sqlite/src/wallet/sapling.rs @@ -1,42 +1,55 @@ //! Functions for Sapling support in the wallet. -use group::ff::PrimeField; -use rusqlite::{named_params, types::Value, OptionalExtension, Row}; -use std::rc::Rc; -use zcash_primitives::{ - consensus::BlockHeight, - memo::MemoBytes, - merkle_tree::{read_commitment_tree, read_incremental_witness}, - sapling::{self, Diversifier, Note, Nullifier, Rseed}, - transaction::components::Amount, - zip32::AccountId, -}; +use std::{collections::HashSet, rc::Rc}; + +use group::ff::PrimeField; +use incrementalmerkletree::Position; +use rusqlite::{named_params, types::Value, Connection, Row}; +use sapling::{self, Diversifier, Nullifier, Rseed}; use zcash_client_backend::{ - wallet::{ReceivedSaplingNote, WalletSaplingOutput}, + data_api::{Account, NullifierQuery, TargetValue}, + wallet::{ReceivedNote, WalletSaplingOutput}, DecryptedOutput, TransferType, }; +use zcash_keys::keys::{UnifiedAddressRequest, UnifiedFullViewingKey}; +use zcash_protocol::{ + consensus::{self, BlockHeight}, + memo::MemoBytes, + ShieldedProtocol, TxId, +}; +use zip32::Scope; -use crate::{error::SqliteClientError, DataConnStmtCache, NoteId, WalletDb}; +use crate::{error::SqliteClientError, AccountRef, AccountUuid, AddressRef, ReceivedNoteId, TxRef}; + +use super::{ + common::UnspentNoteMeta, get_account, get_account_ref, memo_repr, upsert_address, KeyScope, +}; /// This trait provides a generalization over shielded output representations. pub(crate) trait ReceivedSaplingOutput { + type AccountId; + fn index(&self) -> usize; - fn account(&self) -> AccountId; - fn note(&self) -> &Note; + 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<&Nullifier>; + fn nullifier(&self) -> Option<&sapling::Nullifier>; + fn note_commitment_tree_position(&self) -> Option; + fn recipient_key_scope(&self) -> Option; } -impl ReceivedSaplingOutput for WalletSaplingOutput { +impl ReceivedSaplingOutput for WalletSaplingOutput { + type AccountId = AccountId; + fn index(&self) -> usize { self.index() } - fn account(&self) -> AccountId { - WalletSaplingOutput::account(self) + fn account_id(&self) -> Self::AccountId { + *WalletSaplingOutput::account_id(self) } - fn note(&self) -> &Note { + fn note(&self) -> &sapling::Note { WalletSaplingOutput::note(self) } fn memo(&self) -> Option<&MemoBytes> { @@ -45,37 +58,59 @@ impl ReceivedSaplingOutput for WalletSaplingOutput { fn is_change(&self) -> bool { WalletSaplingOutput::is_change(self) } - - fn nullifier(&self) -> Option<&Nullifier> { - Some(self.nf()) + 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 { +impl ReceivedSaplingOutput for DecryptedOutput { + type AccountId = AccountId; + fn index(&self) -> usize { - self.index + self.index() } - fn account(&self) -> AccountId { - self.account + fn account_id(&self) -> Self::AccountId { + *self.account() } - fn note(&self) -> &Note { - &self.note + fn note(&self) -> &sapling::Note { + self.note() } fn memo(&self) -> Option<&MemoBytes> { - Some(&self.memo) + Some(self.memo()) } fn is_change(&self) -> bool { - self.transfer_type == TransferType::WalletInternal + self.transfer_type() == TransferType::WalletInternal + } + fn nullifier(&self) -> Option<&sapling::Nullifier> { + None } - fn nullifier(&self) -> Option<&Nullifier> { + 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(row: &Row) -> Result, SqliteClientError> { - let note_id = NoteId::ReceivedNoteId(row.get(0)?); +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(1)?; + let d: Vec<_> = row.get("diversifier")?; if d.len() != 11 { return Err(SqliteClientError::CorruptedData( "Invalid diversifier length".to_string(), @@ -86,10 +121,12 @@ fn to_spendable_note(row: &Row) -> Result, SqliteCli Diversifier(tmp) }; - let note_value = Amount::from_i64(row.get(2)?).unwrap(); + 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(3)?; + 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 @@ -103,187 +140,118 @@ fn to_spendable_note(row: &Row) -> Result, SqliteCli Rseed::BeforeZip212(rcm) }; - let witness = { - let d: Vec<_> = row.get(4)?; - read_incremental_witness(&d[..])? - }; - - Ok(ReceivedSaplingNote { - note_id, - diversifier, - note_value, - rseed, - witness, - }) + 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 = zip32::Scope::try_from(KeyScope::decode(scope_code)?) + .map_err(|_| { + 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() } -pub(crate) fn get_spendable_sapling_notes

( - wdb: &WalletDb

, - account: AccountId, - anchor_height: BlockHeight, - exclude: &[NoteId], -) -> Result>, SqliteClientError> { - let mut stmt_select_notes = wdb.conn.prepare( - "SELECT id_note, diversifier, value, rcm, witness - FROM sapling_received_notes - INNER JOIN transactions ON transactions.id_tx = sapling_received_notes.tx - INNER JOIN sapling_witnesses ON sapling_witnesses.note = sapling_received_notes.id_note - WHERE account = :account - AND spent IS NULL - AND transactions.block <= :anchor_height - AND sapling_witnesses.block = :anchor_height - AND id_note NOT IN rarray(:exclude)", - )?; - - let excluded: Vec = exclude - .iter() - .filter_map(|n| match n { - NoteId::ReceivedNoteId(i) => Some(Value::from(*i)), - NoteId::SentNoteId(_) => None, - }) - .collect(); - let excluded_ptr = Rc::new(excluded); - - let notes = stmt_select_notes.query_and_then( - named_params![ - ":account": &u32::from(account), - ":anchor_height": &u32::from(anchor_height), - ":exclude": &excluded_ptr, - ], +// 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, - )?; - - notes.collect::>() + ) } -pub(crate) fn select_spendable_sapling_notes

( - wdb: &WalletDb

, - account: AccountId, - target_value: Amount, +/// 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: TargetValue, anchor_height: BlockHeight, - exclude: &[NoteId], -) -> 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 sapling_received_notes - INNER JOIN transactions ON transactions.id_tx = sapling_received_notes.tx - WHERE account = :account - AND spent IS NULL - AND transactions.block <= :anchor_height - AND id_note NOT IN rarray(:exclude) - ) - 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.id_note, selected.diversifier, selected.value, selected.rcm, witnesses.witness - FROM selected - INNER JOIN witnesses ON selected.id_note = witnesses.note", - )?; - - let excluded: Vec = exclude - .iter() - .filter_map(|n| match n { - NoteId::ReceivedNoteId(i) => Some(Value::from(*i)), - NoteId::SentNoteId(_) => None, - }) - .collect(); - let excluded_ptr = Rc::new(excluded); - - let notes = stmt_select_notes.query_and_then( - named_params![ - ":account": &u32::from(account), - ":anchor_height": &u32::from(anchor_height), - ":target_value": &i64::from(target_value), - ":exclude": &excluded_ptr - ], + exclude: &[ReceivedNoteId], +) -> Result>, SqliteClientError> { + super::common::select_spendable_notes( + conn, + params, + account, + target_value, + anchor_height, + exclude, + ShieldedProtocol::Sapling, to_spendable_note, - )?; - - notes.collect::>() + ) } -/// Returns the commitment tree for the block at the specified height, -/// if any. -pub(crate) fn get_sapling_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)?; - read_commitment_tree(&row_data[..]).map_err(|e| { - rusqlite::Error::FromSqlConversionFailure( - row_data.len(), - rusqlite::types::Type::Blob, - Box::new(e), - ) - }) - }, - ) - .optional() - .map_err(SqliteClientError::from) -} - -/// Returns the incremental witnesses for the block at the specified height, -/// if any. -pub(crate) fn get_sapling_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(read_incremental_witness(&wdb[..]).map(|witness| (id_note, witness))) - }) - .map_err(SqliteClientError::from)?; - - // unwrap database error & IO error from IncrementalWitness::read - let res: Vec<_> = witnesses.collect::, _>>()??; - Ok(res) -} - -/// Records the incremental witness for the specified note, -/// as of the given block height. -pub(crate) fn insert_witness<'a, P>( - stmts: &mut DataConnStmtCache<'a, P>, - note_id: i64, - witness: &sapling::IncrementalWitness, - height: BlockHeight, -) -> Result<(), SqliteClientError> { - stmts.stmt_insert_witness(NoteId::ReceivedNoteId(note_id), height, witness) +pub(crate) fn select_unspent_note_meta( + conn: &Connection, + chain_tip_height: BlockHeight, + wallet_birthday: BlockHeight, +) -> Result, SqliteClientError> { + super::common::select_unspent_note_meta( + conn, + ShieldedProtocol::Sapling, + chain_tip_height, + wallet_birthday, + ) } /// Retrieves the set of nullifiers for "potentially spendable" Sapling notes that the @@ -292,51 +260,64 @@ pub(crate) fn insert_witness<'a, P>( /// "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

( - wdb: &WalletDb

, -) -> Result, SqliteClientError> { +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 = wdb.conn.prepare( - "SELECT rn.id_note, rn.account, rn.nf, tx.block as block - FROM sapling_received_notes rn - LEFT OUTER JOIN transactions tx - ON tx.id_tx = rn.spent - WHERE block IS NULL - AND nf IS NOT NULL", - )?; - let nullifiers = stmt_fetch_nullifiers.query_map([], |row| { - let account: u32 = row.get(1)?; - let nf_bytes: Vec = row.get(2)?; - Ok(( - AccountId::from(account), - Nullifier::from_slice(&nf_bytes).unwrap(), - )) + 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) } -/// Returns the nullifiers for the notes that this wallet is tracking. -pub(crate) fn get_all_sapling_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 - FROM sapling_received_notes rn - WHERE nf IS NOT NULL", +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 nullifiers = stmt_fetch_nullifiers.query_map([], |row| { - let account: u32 = row.get(1)?; - let nf_bytes: Vec = row.get(2)?; - Ok(( - AccountId::from(account), - Nullifier::from_slice(&nf_bytes).unwrap(), - )) - })?; - let res: Vec<_> = nullifiers.collect::>()?; + 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) } @@ -345,13 +326,67 @@ pub(crate) fn get_all_sapling_nullifiers

( /// /// Marking a note spent in this fashion does NOT imply that the /// spending transaction has been mined. -pub(crate) fn mark_sapling_note_spent<'a, P>( - stmts: &mut DataConnStmtCache<'a, P>, - tx_ref: i64, - nf: &Nullifier, -) -> Result<(), SqliteClientError> { - stmts.stmt_mark_sapling_note_spent(tx_ref, nf)?; - Ok(()) +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"), + } +} + +pub(crate) fn ensure_address< + T: ReceivedSaplingOutput, + P: consensus::Parameters, +>( + conn: &rusqlite::Transaction, + params: &P, + output: &T, + exposure_height: Option, +) -> Result, SqliteClientError> { + if output.recipient_key_scope() != Some(Scope::Internal) { + let account = get_account(conn, params, output.account_id())? + .ok_or(SqliteClientError::AccountUnknown)?; + + let uivk = account.uivk(); + let ivk = uivk + .sapling() + .as_ref() + .expect("uivk decrypted this output."); + let to = output.note().recipient(); + let diversifier_index = ivk + .decrypt_diversifier(&to) + .expect("address corresponds to account"); + + let ua = account + .uivk() + .address(diversifier_index, UnifiedAddressRequest::ALLOW_ALL)?; + + upsert_address( + conn, + params, + account.internal_id(), + diversifier_index, + &ua, + exposure_height, + false, + ) + .map(Some) + } else { + Ok(None) + } } /// Records the specified shielded output as having been received. @@ -359,891 +394,239 @@ pub(crate) fn mark_sapling_note_spent<'a, P>( /// 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<'a, P, T: ReceivedSaplingOutput>( - stmts: &mut DataConnStmtCache<'a, P>, +/// +/// Returns the internal account identifier of the account that received the output. +pub(crate) fn put_received_note< + T: ReceivedSaplingOutput, + P: consensus::Parameters, +>( + conn: &rusqlite::Transaction, + params: &P, output: &T, - tx_ref: i64, -) -> Result { + tx_ref: TxRef, + target_or_mined_height: Option, + spent_in: Option, +) -> Result { + let account_id = get_account_ref(conn, output.account_id())?; + let address_id = ensure_address(conn, params, output, target_or_mined_height)?; + let mut stmt_upsert_received_note = conn.prepare_cached( + "INSERT INTO sapling_received_notes ( + tx, output_index, account_id, address_id, + diversifier, value, rcm, memo, nf, + is_change, commitment_tree_position, + recipient_key_scope + ) + VALUES ( + :tx, + :output_index, + :account_id, + :address_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, + address_id = :address_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 account = output.account(); let to = output.note().recipient(); let diversifier = to.diversifier(); - let value = output.note().value(); - let memo = output.memo(); - let is_change = output.is_change(); - let output_index = output.index(); - let nf = output.nullifier(); - - // First try updating an existing received note into the database. - if !stmts.stmt_update_received_note( - account, - diversifier, - value.inner(), - rcm, - nf, - memo, - is_change, - tx_ref, - output_index, - )? { - // It isn't there, so insert our note into the database. - stmts.stmt_insert_received_note( - tx_ref, - output_index, - account, - diversifier, - value.inner(), - rcm, - nf, - memo, - is_change, - ) - } else { - // It was there, so grab its row number. - stmts.stmt_select_received_note(tx_ref, output.index()) + + 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, + ":address_id": address_id.map(|a| a.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(|s| KeyScope::from(s).encode()), + ]; + + 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(account_id) } #[cfg(test)] -#[allow(deprecated)] -mod tests { - use rusqlite::Connection; - use secrecy::Secret; - use tempfile::NamedTempFile; - - use zcash_proofs::prover::LocalTxProver; - - use zcash_primitives::{ - block::BlockHash, - consensus::{BlockHeight, BranchId}, - legacy::TransparentAddress, - sapling::{note_encryption::try_sapling_output_recovery, prover::TxProver}, - transaction::{components::Amount, fees::zip317::FeeRule as Zip317FeeRule, Transaction}, - zip32::{sapling::ExtendedSpendingKey, Scope}, - }; +pub(crate) mod tests { + use zcash_client_backend::data_api::testing::sapling::SaplingPoolTester; - use zcash_client_backend::{ - address::RecipientAddress, - data_api::{ - self, - chain::scan_cached_blocks, - error::Error, - wallet::{create_spend_to_address, input_selection::GreedyInputSelector, spend}, - WalletRead, WalletWrite, - }, - fees::{zip317, DustOutputPolicy}, - keys::UnifiedSpendingKey, - wallet::OvkPolicy, - zip321::{Payment, TransactionRequest}, - }; + use crate::testing; - use crate::{ - chain::init::init_cache_database, - tests::{ - self, fake_compact_block, insert_into_cache, network, sapling_activation_height, - AddressType, - }, - wallet::{ - get_balance, get_balance_at, - init::{init_blocks_table, init_wallet_db}, - }, - AccountId, BlockDb, DataConnStmtCache, WalletDb, - }; + #[cfg(feature = "orchard")] + use zcash_client_backend::data_api::testing::orchard::OrchardPoolTester; - #[cfg(feature = "transparent-inputs")] - use { - zcash_client_backend::{ - data_api::wallet::shield_transparent_funds, fees::fixed, - wallet::WalletTransparentOutput, - }, - zcash_primitives::{ - memo::MemoBytes, - transaction::{ - components::{amount::NonNegativeAmount, OutPoint, TxOut}, - fees::fixed::FeeRule as FixedFeeRule, - }, - }, - }; + #[test] + fn send_single_step_proposed_transfer() { + testing::pool::send_single_step_proposed_transfer::() + } - 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 send_with_multiple_change_outputs() { + testing::pool::send_with_multiple_change_outputs::() } #[test] - fn create_to_address_fails_on_incorrect_usk() { - let data_file = NamedTempFile::new().unwrap(); - let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap(); - - // Add an account to the wallet - let mut ops = db_data.get_update_ops().unwrap(); - let seed = Secret::new([0u8; 32].to_vec()); - let (_, usk) = ops.create_account(&seed).unwrap(); - let dfvk = usk.sapling().to_diversifiable_full_viewing_key(); - let to = dfvk.default_address().1.into(); - - // Create a USK that doesn't exist in the wallet - let acct1 = AccountId::from(1); - let usk1 = UnifiedSpendingKey::from_seed(&network(), &[1u8; 32], acct1).unwrap(); - - // Attempting to spend with a USK that is not in the wallet results in an error - let mut db_write = db_data.get_update_ops().unwrap(); - assert_matches!( - create_spend_to_address( - &mut db_write, - &tests::network(), - test_prover(), - &usk1, - &to, - Amount::from_u64(1).unwrap(), - None, - OvkPolicy::Sender, - 10, - ), - Err(data_api::error::Error::KeyNotRecognized) - ); + #[cfg(feature = "transparent-inputs")] + fn send_multi_step_proposed_transfer() { + testing::pool::send_multi_step_proposed_transfer::() } #[test] - fn create_to_address_fails_with_no_blocks() { - let data_file = NamedTempFile::new().unwrap(); - let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&mut db_data, None).unwrap(); - - // Add an account to the wallet - let mut ops = db_data.get_update_ops().unwrap(); - let seed = Secret::new([0u8; 32].to_vec()); - let (_, usk) = ops.create_account(&seed).unwrap(); - let dfvk = usk.sapling().to_diversifiable_full_viewing_key(); - let to = dfvk.default_address().1.into(); - - // We cannot do anything if we aren't synchronised - let mut db_write = db_data.get_update_ops().unwrap(); - assert_matches!( - create_spend_to_address( - &mut db_write, - &tests::network(), - test_prover(), - &usk, - &to, - Amount::from_u64(1).unwrap(), - None, - OvkPolicy::Sender, - 10, - ), - Err(data_api::error::Error::ScanRequired) - ); + #[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_insufficient_balance() { - let data_file = NamedTempFile::new().unwrap(); - let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&mut db_data, None).unwrap(); - init_blocks_table( - &db_data, - BlockHeight::from(1u32), - BlockHash([1; 32]), - 1, - &[], - ) - .unwrap(); - - // Add an account to the wallet - let mut ops = db_data.get_update_ops().unwrap(); - let seed = Secret::new([0u8; 32].to_vec()); - let (_, usk) = ops.create_account(&seed).unwrap(); - let dfvk = usk.sapling().to_diversifiable_full_viewing_key(); - let to = dfvk.default_address().1.into(); - - // Account balance should be zero - assert_eq!( - get_balance(&db_data, AccountId::from(0)).unwrap(), - Amount::zero() - ); - - // We cannot spend anything - let mut db_write = db_data.get_update_ops().unwrap(); - assert_matches!( - create_spend_to_address( - &mut db_write, - &tests::network(), - test_prover(), - &usk, - &to, - Amount::from_u64(1).unwrap(), - None, - OvkPolicy::Sender, - 10, - ), - Err(data_api::error::Error::InsufficientFunds { - available, - required - }) - if available == Amount::zero() && required == Amount::from_u64(10001).unwrap() - ); + fn create_to_address_fails_on_incorrect_usk() { + testing::pool::create_to_address_fails_on_incorrect_usk::() } #[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 mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&mut db_data, None).unwrap(); - - // Add an account to the wallet - let mut ops = db_data.get_update_ops().unwrap(); - let seed = Secret::new([0u8; 32].to_vec()); - let (_, usk) = ops.create_account(&seed).unwrap(); - let dfvk = usk.sapling().to_diversifiable_full_viewing_key(); - - // 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]), - &dfvk, - AddressType::DefaultExternal, - 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(10).unwrap().unwrap(); - assert_eq!(get_balance(&db_data, AccountId::from(0)).unwrap(), value); - assert_eq!( - get_balance_at(&db_data, AccountId::from(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(), - &dfvk, - AddressType::DefaultExternal, - 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(10).unwrap().unwrap(); - assert_eq!( - get_balance(&db_data, AccountId::from(0)).unwrap(), - (value + value).unwrap() - ); - assert_eq!( - get_balance_at(&db_data, AccountId::from(0), anchor_height2).unwrap(), - value - ); - - // Spend fails because there are insufficient verified notes - let extsk2 = ExtendedSpendingKey::master(&[]); - let to = extsk2.default_address().1.into(); - assert_matches!( - create_spend_to_address( - &mut db_write, - &tests::network(), - test_prover(), - &usk, - &to, - Amount::from_u64(70000).unwrap(), - None, - OvkPolicy::Sender, - 10, - ), - Err(data_api::error::Error::InsufficientFunds { - available, - required - }) - if available == Amount::from_u64(50000).unwrap() - && required == Amount::from_u64(80000).unwrap() - ); - - // 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(), - &dfvk, - AddressType::DefaultExternal, - value, - ); - insert_into_cache(&db_cache, &cb); - } - scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); - - // Second spend still fails - assert_matches!( - create_spend_to_address( - &mut db_write, - &tests::network(), - test_prover(), - &usk, - &to, - Amount::from_u64(70000).unwrap(), - None, - OvkPolicy::Sender, - 10, - ), - Err(data_api::error::Error::InsufficientFunds { - available, - required - }) - if available == Amount::from_u64(50000).unwrap() - && required == Amount::from_u64(80000).unwrap() - ); - - // Mine block 11 so that the second note becomes verified - let (cb, _) = fake_compact_block( - sapling_activation_height() + 10, - cb.hash(), - &dfvk, - AddressType::DefaultExternal, - value, - ); - insert_into_cache(&db_cache, &cb); - scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); - - // Second spend should now succeed - assert_matches!( - create_spend_to_address( - &mut db_write, - &tests::network(), - test_prover(), - &usk, - &to, - Amount::from_u64(70000).unwrap(), - None, - OvkPolicy::Sender, - 10, - ), - Ok(_) - ); + fn proposal_fails_with_no_blocks() { + testing::pool::proposal_fails_with_no_blocks::() } #[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 mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap(); - - // Add an account to the wallet - let mut ops = db_data.get_update_ops().unwrap(); - let seed = Secret::new([0u8; 32].to_vec()); - let (_, usk) = ops.create_account(&seed).unwrap(); - let dfvk = usk.sapling().to_diversifiable_full_viewing_key(); - - // 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]), - &dfvk, - AddressType::DefaultExternal, - 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::from(0)).unwrap(), value); - - // Send some of the funds to another address - let extsk2 = ExtendedSpendingKey::master(&[]); - let to = extsk2.default_address().1.into(); - assert_matches!( - create_spend_to_address( - &mut db_write, - &tests::network(), - test_prover(), - &usk, - &to, - Amount::from_u64(15000).unwrap(), - None, - OvkPolicy::Sender, - 10, - ), - Ok(_) - ); - - // A second spend fails because there are no usable notes - assert_matches!( - create_spend_to_address( - &mut db_write, - &tests::network(), - test_prover(), - &usk, - &to, - Amount::from_u64(2000).unwrap(), - None, - OvkPolicy::Sender, - 10, - ), - Err(data_api::error::Error::InsufficientFunds { - available, - required - }) - if available == Amount::zero() && required == Amount::from_u64(12000).unwrap() - ); - - // 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 { - let (cb, _) = fake_compact_block( - sapling_activation_height() + i, - cb.hash(), - &ExtendedSpendingKey::master(&[i as u8]).to_diversifiable_full_viewing_key(), - AddressType::DefaultExternal, - value, - ); - insert_into_cache(&db_cache, &cb); - } - scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); - - // Second spend still fails - assert_matches!( - create_spend_to_address( - &mut db_write, - &tests::network(), - test_prover(), - &usk, - &to, - Amount::from_u64(2000).unwrap(), - None, - OvkPolicy::Sender, - 10, - ), - Err(data_api::error::Error::InsufficientFunds { - available, - required - }) - if available == Amount::zero() && required == Amount::from_u64(12000).unwrap() - ); - - // Mine block SAPLING_ACTIVATION_HEIGHT + 42 so that the first transaction expires - let (cb, _) = fake_compact_block( - sapling_activation_height() + 42, - cb.hash(), - &ExtendedSpendingKey::master(&[42]).to_diversifiable_full_viewing_key(), - AddressType::DefaultExternal, - 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(), - &usk, - &to, - Amount::from_u64(2000).unwrap(), - None, - OvkPolicy::Sender, - 10, - ) - .unwrap(); + fn spend_fails_on_unverified_notes() { + testing::pool::spend_fails_on_unverified_notes::() } #[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 mut db_data = WalletDb::for_path(data_file.path(), network).unwrap(); - init_wallet_db(&mut db_data, None).unwrap(); - - // Add an account to the wallet - let mut ops = db_data.get_update_ops().unwrap(); - let seed = Secret::new([0u8; 32].to_vec()); - let (_, usk) = ops.create_account(&seed).unwrap(); - let dfvk = usk.sapling().to_diversifiable_full_viewing_key(); - - // 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]), - &dfvk, - AddressType::DefaultExternal, - 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::from(0)).unwrap(), value); - - let extsk2 = ExtendedSpendingKey::master(&[]); - let addr2 = extsk2.default_address().1; - let to = addr2.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(), - &usk, - &to, - Amount::from_u64(15000).unwrap(), - None, - ovk_policy, - 10, - ) - .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[..], BranchId::Canopy).unwrap(); - - for output in tx.sapling_bundle().unwrap().shielded_outputs() { - // Find the output that decrypts with the external OVK - let result = try_sapling_output_recovery( - &network, - sapling_activation_height(), - &dfvk.to_ovk(Scope::External), - output, - ); - - if result.is_some() { - return result; - } - } + fn spend_fails_on_locked_notes() { + testing::pool::spend_fails_on_locked_notes::() + } - None - }; - - // 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 42 (that don't send us funds) - // so that the first transaction expires - for i in 1..=42 { - let (cb, _) = fake_compact_block( - sapling_activation_height() + i, - cb.hash(), - &ExtendedSpendingKey::master(&[i as u8]).to_diversifiable_full_viewing_key(), - AddressType::DefaultExternal, - value, - ); - insert_into_cache(&db_cache, &cb); - } - scan_cached_blocks(&network, &db_cache, &mut db_write, None).unwrap(); + #[test] + fn ovk_policy_prevents_recovery_from_chain() { + testing::pool::ovk_policy_prevents_recovery_from_chain::() + } - // 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 spend_succeeds_to_t_addr_zero_change() { + testing::pool::spend_succeeds_to_t_addr_zero_change::() } #[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 mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&mut db_data, None).unwrap(); - - // Add an account to the wallet - let mut ops = db_data.get_update_ops().unwrap(); - let seed = Secret::new([0u8; 32].to_vec()); - let (_, usk) = ops.create_account(&seed).unwrap(); - let dfvk = usk.sapling().to_diversifiable_full_viewing_key(); - - // Add funds to the wallet in a single note - let value = Amount::from_u64(60000).unwrap(); - let (cb, _) = fake_compact_block( - sapling_activation_height(), - BlockHash([0; 32]), - &dfvk, - AddressType::DefaultExternal, - 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(10).unwrap().unwrap(); - assert_eq!(get_balance(&db_data, AccountId::from(0)).unwrap(), value); - assert_eq!( - get_balance_at(&db_data, AccountId::from(0), anchor_height).unwrap(), - value - ); - - let to = TransparentAddress::PublicKey([7; 20]).into(); - assert_matches!( - create_spend_to_address( - &mut db_write, - &tests::network(), - test_prover(), - &usk, - &to, - Amount::from_u64(50000).unwrap(), - None, - OvkPolicy::Sender, - 10, - ), - Ok(_) - ); + fn change_note_spends_succeed() { + testing::pool::change_note_spends_succeed::() } #[test] - fn create_to_address_spends_a_change_note() { - 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 mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&mut db_data, None).unwrap(); - - // Add an account to the wallet - let mut ops = db_data.get_update_ops().unwrap(); - let seed = Secret::new([0u8; 32].to_vec()); - let (_, usk) = ops.create_account(&seed).unwrap(); - let dfvk = usk.sapling().to_diversifiable_full_viewing_key(); - - // Add funds to the wallet in a single note - let value = Amount::from_u64(60000).unwrap(); - let (cb, _) = fake_compact_block( - sapling_activation_height(), - BlockHash([0; 32]), - &dfvk, - AddressType::Internal, - 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(10).unwrap().unwrap(); - assert_eq!(get_balance(&db_data, AccountId::from(0)).unwrap(), value); - assert_eq!( - get_balance_at(&db_data, AccountId::from(0), anchor_height).unwrap(), - value - ); - - let to = TransparentAddress::PublicKey([7; 20]).into(); - assert_matches!( - create_spend_to_address( - &mut db_write, - &tests::network(), - test_prover(), - &usk, - &to, - Amount::from_u64(50000).unwrap(), - None, - OvkPolicy::Sender, - 10, - ), - Ok(_) - ); + 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() { - 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 mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&mut db_data, None).unwrap(); - - // Add an account to the wallet - let mut ops = db_data.get_update_ops().unwrap(); - let seed = Secret::new([0u8; 32].to_vec()); - let (_, usk) = ops.create_account(&seed).unwrap(); - let dfvk = usk.sapling().to_diversifiable_full_viewing_key(); - - // Add funds to the wallet - let (cb, _) = fake_compact_block( - sapling_activation_height(), - BlockHash([0; 32]), - &dfvk, - AddressType::Internal, - Amount::from_u64(50000).unwrap(), - ); - insert_into_cache(&db_cache, &cb); - - // Add 10 dust notes to the wallet - for i in 1..=10 { - let (cb, _) = fake_compact_block( - sapling_activation_height() + i, - cb.hash(), - &dfvk, - AddressType::DefaultExternal, - Amount::from_u64(1000).unwrap(), - ); - 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 total = Amount::from_u64(60000).unwrap(); - let (_, anchor_height) = db_data.get_target_and_anchor_heights(1).unwrap().unwrap(); - assert_eq!(get_balance(&db_data, AccountId::from(0)).unwrap(), total); - assert_eq!( - get_balance_at(&db_data, AccountId::from(0), anchor_height).unwrap(), - total - ); - - let input_selector = GreedyInputSelector::new( - zip317::SingleOutputChangeStrategy::new(Zip317FeeRule::standard()), - DustOutputPolicy::default(), - ); - - // This first request will fail due to insufficient non-dust funds - let req = TransactionRequest::new(vec![Payment { - recipient_address: RecipientAddress::Shielded(dfvk.default_address().1), - amount: Amount::from_u64(50000).unwrap(), - memo: None, - label: None, - message: None, - other_params: vec![], - }]) - .unwrap(); - - assert_matches!( - spend( - &mut db_write, - &tests::network(), - test_prover(), - &input_selector, - &usk, - req, - OvkPolicy::Sender, - 1, - ), - Err(Error::InsufficientFunds { available, required }) - if available == Amount::from_u64(51000).unwrap() - && required == Amount::from_u64(60000).unwrap() - ); - - // 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 { - recipient_address: RecipientAddress::Shielded(dfvk.default_address().1), - amount: Amount::from_u64(41000).unwrap(), - memo: None, - label: None, - message: None, - other_params: vec![], - }]) - .unwrap(); - - assert_matches!( - spend( - &mut db_write, - &tests::network(), - test_prover(), - &input_selector, - &usk, - req, - OvkPolicy::Sender, - 1, - ), - Ok(_) - ); + testing::pool::zip317_spend::() } #[test] #[cfg(feature = "transparent-inputs")] fn shield_transparent() { - 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 mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); - init_wallet_db(&mut db_data, None).unwrap(); - - // Add an account to the wallet - let mut db_write = db_data.get_update_ops().unwrap(); - let seed = Secret::new([0u8; 32].to_vec()); - let (account_id, usk) = db_write.create_account(&seed).unwrap(); - let dfvk = usk.sapling().to_diversifiable_full_viewing_key(); - let uaddr = db_data.get_current_address(account_id).unwrap().unwrap(); - let taddr = uaddr.transparent().unwrap(); - - let utxo = WalletTransparentOutput::from_parts( - OutPoint::new([1u8; 32], 1), - TxOut { - value: Amount::from_u64(10000).unwrap(), - script_pubkey: taddr.script(), - }, - sapling_activation_height(), - ) - .unwrap(); - - let res0 = db_write.put_received_transparent_utxo(&utxo); - assert!(matches!(res0, Ok(_))); - - let input_selector = GreedyInputSelector::new( - fixed::SingleOutputChangeStrategy::new(FixedFeeRule::standard()), - DustOutputPolicy::default(), - ); - - // Add funds to the wallet - let (cb, _) = fake_compact_block( - sapling_activation_height(), - BlockHash([0; 32]), - &dfvk, - AddressType::Internal, - Amount::from_u64(50000).unwrap(), - ); - insert_into_cache(&db_cache, &cb); - scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); - - assert_matches!( - shield_transparent_funds( - &mut db_write, - &tests::network(), - test_prover(), - &input_selector, - NonNegativeAmount::from_u64(10000).unwrap(), - &usk, - &[*taddr], - &MemoBytes::empty(), - 0 - ), - Ok(_) - ); + 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/transparent.rs b/zcash_client_sqlite/src/wallet/transparent.rs new file mode 100644 index 0000000000..fc7f9dfce4 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/transparent.rs @@ -0,0 +1,1642 @@ +//! Functions for transparent input support in the wallet. +use core::ops::Range; +use std::collections::{HashMap, HashSet}; +use std::num::TryFromIntError; +use std::ops::DerefMut; +use std::rc::Rc; +use std::time::{Duration, SystemTime, SystemTimeError}; + +use nonempty::NonEmpty; +use rand::RngCore; +use rand_distr::Distribution; +use rusqlite::types::Value; +use rusqlite::OptionalExtension; +use rusqlite::{named_params, Connection, Row}; + +use transparent::keys::NonHardenedChildRange; +use transparent::{ + address::{Script, TransparentAddress}, + bundle::{OutPoint, TxOut}, + keys::{IncomingViewingKey, NonHardenedChildIndex}, +}; +use zcash_address::unified::{Ivk, Typecode, Uivk}; +use zcash_client_backend::{ + data_api::{ + Account, AccountBalance, OutputStatusFilter, TransactionDataRequest, + TransactionStatusFilter, + }, + wallet::{TransparentAddressMetadata, WalletTransparentOutput}, +}; +use zcash_keys::keys::UnifiedIncomingViewingKey; +use zcash_keys::{ + address::Address, + encoding::AddressCodec, + keys::{AddressGenerationError, UnifiedAddressRequest}, +}; +use zcash_primitives::transaction::builder::DEFAULT_TX_EXPIRY_DELTA; +use zcash_protocol::{ + consensus::{self, BlockHeight}, + value::{ZatBalance, Zatoshis}, + TxId, +}; +use zip32::{DiversifierIndex, Scope}; + +use super::encoding::{decode_epoch_seconds, ReceiverFlags}; +use super::{ + account_birthday_internal, chain_tip_height, + encoding::{decode_diversifier_index_be, encode_diversifier_index_be}, + get_account_ids, get_account_internal, KeyScope, +}; +use crate::{error::SqliteClientError, AccountUuid, TxRef, UtxoId}; +use crate::{AccountRef, AddressRef, GapLimits}; + +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 di = decode_diversifier_index_be(diversifier_index_be)?; + + NonHardenedChildIndex::try_from(di).map_err(|_| { + SqliteClientError::CorruptedData( + "Unexpected hardened index for transparent address.".to_string(), + ) + }) +} + +pub(crate) fn get_transparent_receivers( + conn: &rusqlite::Connection, + params: &P, + account_uuid: AccountUuid, + scopes: &[KeyScope], +) -> Result>, SqliteClientError> { + let mut ret: HashMap> = HashMap::new(); + + // Get all addresses with the provided scopes. + let mut addr_query = conn.prepare( + "SELECT cached_transparent_receiver_address, transparent_child_index, key_scope + FROM addresses + JOIN accounts ON accounts.id = addresses.account_id + WHERE accounts.uuid = :account_uuid + AND cached_transparent_receiver_address IS NOT NULL + AND key_scope IN rarray(:scopes_ptr)", + )?; + + let scope_values: Vec = scopes.iter().map(|s| Value::Integer(s.encode())).collect(); + let scopes_ptr = Rc::new(scope_values); + let mut rows = addr_query.query(named_params![ + ":account_uuid": account_uuid.0, + ":scopes_ptr": &scopes_ptr + ])?; + + while let Some(row) = rows.next()? { + let addr_str: String = row.get(0)?; + let address_index: u32 = row.get(1)?; + let address_index = NonHardenedChildIndex::from_index(address_index).ok_or( + SqliteClientError::CorruptedData(format!( + "{} is not a valid transparent child index", + address_index + )), + )?; + let scope = KeyScope::decode(row.get(2)?)?; + + let taddr = Address::decode(params, &addr_str) + .ok_or_else(|| { + SqliteClientError::CorruptedData("Not a valid Zcash recipient address".to_owned()) + })? + .to_transparent_address(); + + if let Some(taddr) = taddr { + let metadata = TransparentAddressMetadata::new(scope.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) +} + +/// Returns the transparent address index at the start of the first gap of at least `gap_limit` +/// indices in the given account, considering only addresses derived for the specified key scope. +/// +/// Returns `Ok(None)` if the gap would start at an index greater than the maximum valid +/// non-hardened transparent child index. +pub(crate) fn find_gap_start( + conn: &rusqlite::Connection, + account_id: AccountRef, + key_scope: KeyScope, + gap_limit: u32, +) -> Result, SqliteClientError> { + match conn + .query_row( + r#" + WITH offsets AS ( + SELECT + a.transparent_child_index, + LEAD(a.transparent_child_index) + OVER (ORDER BY a.transparent_child_index) + AS next_child_index + FROM v_address_first_use a + WHERE a.account_id = :account_id + AND a.key_scope = :key_scope + AND a.transparent_child_index IS NOT NULL + AND a.first_use_height IS NOT NULL + ) + SELECT + transparent_child_index + 1, + -- both next_child_index and transparent_child_index are used indices, + -- so the gap between them is one less than their difference + next_child_index - transparent_child_index - 1 AS gap_len + FROM offsets + -- if gap_len is at least the gap limit, then we have found a gap. + -- if next_child_index is NULL, then we have reached the end of + -- the allocated indices (the remainder of the index space is a gap). + WHERE gap_len >= :gap_limit OR next_child_index IS NULL + ORDER BY transparent_child_index + LIMIT 1 + "#, + named_params![ + ":account_id": account_id.0, + ":key_scope": key_scope.encode(), + ":gap_limit": gap_limit + ], + |row| row.get::<_, u32>(0), + ) + .optional()? + { + Some(i) => Ok(NonHardenedChildIndex::from_index(i)), + None => Ok(Some(NonHardenedChildIndex::ZERO)), + } +} + +pub(crate) fn decode_transparent_child_index( + value: i64, +) -> Result { + u32::try_from(value) + .ok() + .and_then(NonHardenedChildIndex::from_index) + .ok_or_else(|| { + SqliteClientError::CorruptedData(format!("Illegal transparent child index {value}")) + }) +} + +/// Returns the current gap start, along with a vector with at most the next `n` previously +/// unreserved transparent addresses for the given account. These addresses must have been +/// previously generated using `generate_gap_addresses`. +/// +/// WARNING: the addresses returned by this method have not been marked as exposed; it is the +/// responsibility of the caller to correctly update the `exposed_at_height` value for each +/// returned address before such an address is exposed to a user. +/// +/// # Errors +/// +/// * `SqliteClientError::AccountUnknown`, if there is no account with the given id. +/// * `SqliteClientError::AddressGeneration(AddressGenerationError::DiversifierSpaceExhausted)`, +/// if the limit on transparent address indices has been reached. +#[allow(clippy::type_complexity)] +pub(crate) fn select_addrs_to_reserve( + conn: &rusqlite::Transaction, + params: &P, + account_id: AccountRef, + key_scope: KeyScope, + gap_limit: u32, + n: usize, +) -> Result< + ( + NonHardenedChildIndex, + Vec<(AddressRef, TransparentAddress, TransparentAddressMetadata)>, + ), + SqliteClientError, +> { + let gap_start = find_gap_start(conn, account_id, key_scope, gap_limit)?.ok_or( + SqliteClientError::AddressGeneration(AddressGenerationError::DiversifierSpaceExhausted), + )?; + + let mut stmt_addrs_to_reserve = conn.prepare( + "SELECT id, transparent_child_index, cached_transparent_receiver_address + FROM addresses + WHERE account_id = :account_id + AND key_scope = :key_scope + AND transparent_child_index >= :gap_start + AND transparent_child_index < :gap_end + AND exposed_at_height IS NULL + ORDER BY transparent_child_index + LIMIT :n", + )?; + + let addresses_to_reserve = stmt_addrs_to_reserve + .query_and_then( + named_params! { + ":account_id": account_id.0, + ":key_scope": key_scope.encode(), + ":gap_start": gap_start.index(), + // NOTE: this approach means that the address at index 2^31 - 1 will never be + // allocated. I think that's fine. + ":gap_end": gap_start.saturating_add(gap_limit).index(), + ":n": n + }, + |row| { + let address_id = row.get("id").map(AddressRef)?; + let transparent_child_index = row + .get::<_, Option>("transparent_child_index")? + .map(decode_transparent_child_index) + .transpose()?; + let address = row + .get::<_, Option>("cached_transparent_receiver_address")? + .map(|addr_str| TransparentAddress::decode(params, &addr_str)) + .transpose()?; + + Ok::<_, SqliteClientError>(transparent_child_index.zip(address).map(|(i, a)| { + ( + address_id, + a, + TransparentAddressMetadata::new(key_scope.into(), i), + ) + })) + }, + )? + .filter_map(|r| r.transpose()) + .collect::, _>>()?; + + Ok((gap_start, addresses_to_reserve)) +} + +/// Returns a vector with the next `n` previously unreserved transparent addresses for the given +/// account, having marked each address as having been exposed at the current chain-tip height. +/// These addresses must have been previously generated using `generate_gap_addresses`. +/// +/// # Errors +/// +/// * [`SqliteClientError::AccountUnknown`], if there is no account with the given id. +/// * [`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. +/// +/// [`SqliteClientError::AddressGeneration(AddressGenerationError::DiversifierSpaceExhausted)`]: +/// SqliteClientError::AddressGeneration +pub(crate) fn reserve_next_n_addresses( + conn: &rusqlite::Transaction, + params: &P, + account_id: AccountRef, + key_scope: KeyScope, + gap_limit: u32, + n: usize, +) -> Result, SqliteClientError> { + if n == 0 { + return Ok(vec![]); + } + + let (gap_start, addresses_to_reserve) = + select_addrs_to_reserve(conn, params, account_id, key_scope, gap_limit, n)?; + + if addresses_to_reserve.len() < n { + return Err(SqliteClientError::ReachedGapLimit( + key_scope.into(), + gap_start.index() + gap_limit, + )); + } + + let current_chain_tip = chain_tip_height(conn)?.ok_or(SqliteClientError::ChainHeightUnknown)?; + + let reserve_id_values: Vec = addresses_to_reserve + .iter() + .map(|(id, _, _)| Value::Integer(id.0)) + .collect(); + let reserved_ptr = Rc::new(reserve_id_values); + conn.execute( + "UPDATE addresses + SET exposed_at_height = :chain_tip_height + WHERE id IN rarray(:reserved_ptr)", + named_params! { + ":chain_tip_height": u32::from(current_chain_tip), + ":reserved_ptr": &reserved_ptr + }, + )?; + + Ok(addresses_to_reserve) +} + +pub(crate) fn generate_external_address( + uivk: &UnifiedIncomingViewingKey, + ua_request: UnifiedAddressRequest, + index: NonHardenedChildIndex, +) -> Result<(Address, TransparentAddress), AddressGenerationError> { + let ua = uivk.address(index.into(), ua_request); + let transparent_address = uivk + .transparent() + .as_ref() + .ok_or(AddressGenerationError::KeyNotAvailable(Typecode::P2pkh))? + .derive_address(index) + .map_err(|_| { + AddressGenerationError::InvalidTransparentChildIndex(DiversifierIndex::from(index)) + })?; + Ok(( + ua.map_or_else( + |e| { + if matches!(e, AddressGenerationError::ShieldedReceiverRequired) { + // fall back to the transparent-only address + Ok(Address::from(transparent_address)) + } else { + // other address generation errors are allowed to propagate + Err(e) + } + }, + |addr| Ok(Address::from(addr)), + )?, + transparent_address, + )) +} + +/// Generates addresses to fill the specified non-hardened child index range. +/// +/// The provided [`UnifiedAddressRequest`] is used to pre-generate unified addresses that correspond +/// to each transparent address index in question; such unified addresses need not internally +/// contain a transparent receiver, and may be overwritten when these addresses are exposed via the +/// [`WalletWrite::get_next_available_address`] or [`WalletWrite::get_address_for_index`] methods. +/// If no request is provided, each address so generated will contain a receiver for each possible +/// pool: i.e., a recevier for each data item in the account's UFVK or UIVK where the transparent +/// child index is valid. +/// +/// [`WalletWrite::get_next_available_address`]: zcash_client_backend::data_api::WalletWrite::get_next_available_address +/// [`WalletWrite::get_address_for_index`]: zcash_client_backend::data_api::WalletWrite::get_address_for_index +pub(crate) fn generate_address_range( + conn: &rusqlite::Transaction, + params: &P, + account_id: AccountRef, + key_scope: KeyScope, + request: UnifiedAddressRequest, + range_to_store: Range, + require_key: bool, +) -> Result<(), SqliteClientError> { + let account = get_account_internal(conn, params, account_id)? + .ok_or_else(|| SqliteClientError::AccountUnknown)?; + + if !account.uivk().has_transparent() { + if require_key { + return Err(SqliteClientError::AddressGeneration( + AddressGenerationError::KeyNotAvailable(Typecode::P2pkh), + )); + } else { + return Ok(()); + } + } + + let gen_addrs = |key_scope: KeyScope, index: NonHardenedChildIndex| { + Ok::<_, SqliteClientError>(match key_scope { + KeyScope::Zip32(zip32::Scope::External) => { + generate_external_address(&account.uivk(), request, index)? + } + KeyScope::Zip32(zip32::Scope::Internal) => { + let internal_address = account + .ufvk() + .and_then(|k| k.transparent()) + .expect("presence of transparent key was checked above.") + .derive_internal_ivk()? + .derive_address(index)?; + (Address::from(internal_address), internal_address) + } + KeyScope::Ephemeral => { + let ephemeral_address = account + .ufvk() + .and_then(|k| k.transparent()) + .expect("presence of transparent key was checked above.") + .derive_ephemeral_ivk()? + .derive_ephemeral_address(index)?; + (Address::from(ephemeral_address), ephemeral_address) + } + }) + }; + + // exposed_at_height is initially NULL + let mut stmt_insert_address = conn.prepare_cached( + "INSERT INTO addresses ( + account_id, diversifier_index_be, key_scope, address, + transparent_child_index, cached_transparent_receiver_address, + receiver_flags + ) + VALUES ( + :account_id, :diversifier_index_be, :key_scope, :address, + :transparent_child_index, :transparent_address, + :receiver_flags + ) + ON CONFLICT (account_id, diversifier_index_be, key_scope) DO NOTHING", + )?; + + for transparent_child_index in NonHardenedChildRange::from(range_to_store) { + let (address, transparent_address) = gen_addrs(key_scope, transparent_child_index)?; + let zcash_address = address.to_zcash_address(params); + let receiver_flags: ReceiverFlags = zcash_address + .clone() + .convert::() + .expect("address is valid"); + + stmt_insert_address.execute(named_params![ + ":account_id": account_id.0, + ":diversifier_index_be": encode_diversifier_index_be(transparent_child_index.into()), + ":key_scope": key_scope.encode(), + ":address": zcash_address.encode(), + ":transparent_child_index": transparent_child_index.index(), + ":transparent_address": transparent_address.encode(params), + ":receiver_flags": receiver_flags.bits() + ])?; + } + Ok(()) +} + +/// Extend the range of preallocated addresses in an account to ensure that a full `gap_limit` of +/// transparent addresses is available from the first gap in existing indices of addresses at which +/// a received transaction has been observed on the chain, for each key scope. +/// +/// The provided [`UnifiedAddressRequest`] is used to pre-generate unified addresses that correspond +/// to the transparent address index in question; such unified addresses need not internally +/// contain a transparent receiver, and may be overwritten when these addresses are exposed via the +/// [`WalletWrite::get_next_available_address`] or [`WalletWrite::get_address_for_index`] methods. +/// If no request is provided, each address so generated will contain a receiver for each possible +/// pool: i.e., a recevier for each data item in the account's UFVK or UIVK where the transparent +/// child index is valid. +/// +/// [`WalletWrite::get_next_available_address`]: zcash_client_backend::data_api::WalletWrite::get_next_available_address +/// [`WalletWrite::get_address_for_index`]: zcash_client_backend::data_api::WalletWrite::get_address_for_index +pub(crate) fn generate_gap_addresses( + conn: &rusqlite::Transaction, + params: &P, + account_id: AccountRef, + key_scope: KeyScope, + gap_limits: &GapLimits, + request: UnifiedAddressRequest, + require_key: bool, +) -> Result<(), SqliteClientError> { + let gap_limit = match key_scope { + KeyScope::Zip32(zip32::Scope::External) => gap_limits.external(), + KeyScope::Zip32(zip32::Scope::Internal) => gap_limits.internal(), + KeyScope::Ephemeral => gap_limits.ephemeral(), + }; + + if let Some(gap_start) = find_gap_start(conn, account_id, key_scope, gap_limit)? { + generate_address_range( + conn, + params, + account_id, + key_scope, + request, + gap_start..gap_start.saturating_add(gap_limit), + require_key, + )?; + } + + Ok(()) +} + +/// Check whether `address` has previously been used as the recipient address for any previously +/// received output. This is intended primarily for use in ensuring that the wallet does not create +/// ZIP 320 transactions that reuse the same ephemeral address, although it is written in such a +/// way that it may be used for detection of transparent address reuse more generally. +/// +/// If the address was already used in an output we received, this method will return +/// [`SqliteClientError::AddressReuse`]. +pub(crate) fn check_ephemeral_address_reuse( + conn: &rusqlite::Transaction, + params: &P, + address: &TransparentAddress, +) -> Result<(), SqliteClientError> { + let taddr_str = address.encode(params); + let mut stmt = conn.prepare_cached( + "SELECT t.txid + FROM transactions t + JOIN v_received_outputs vro ON vro.transaction_id = t.id_tx + JOIN addresses a ON a.id = vro.address_id + WHERE a.cached_transparent_receiver_address = :transparent_address", + )?; + + let txids = stmt + .query_and_then( + named_params![ + ":transparent_address": taddr_str, + ], + |row| Ok(TxId::from_bytes(row.get::<_, [u8; 32]>(0)?)), + )? + .collect::, SqliteClientError>>()?; + + if let Some(txids) = NonEmpty::from_vec(txids) { + return Err(SqliteClientError::AddressReuse(taddr_str, txids)); + } + + Ok(()) +} + +/// Returns the block height at which we should start scanning for UTXOs. +/// +/// We must start looking for UTXOs for addresses within the current gap limit as of the block +/// height at which they might have first been revealed. This would have occurred when the gap +/// advanced as a consequence of a transaction being mined. The address at the start of the current +/// gap was potentially first revealed after the address at index `gap_start - (gap_limit + 1)` +/// received an output in a mined transaction; therefore, we take that height to be where we should +/// start searching for UTXOs. +pub(crate) fn utxo_query_height( + conn: &rusqlite::Connection, + account_ref: AccountRef, + gap_limits: &GapLimits, +) -> Result { + let mut stmt = conn.prepare_cached( + "SELECT MIN(au.mined_height) + FROM v_address_uses au + JOIN addresses a ON a.id = au.address_id + WHERE a.account_id = :account_id + AND au.key_scope = :key_scope + AND au.transparent_child_index >= :transparent_child_index", + )?; + + let mut get_height = |key_scope: KeyScope, gap_limit: u32| { + if let Some(gap_start) = find_gap_start(conn, account_ref, key_scope, gap_limit)? { + stmt.query_row( + named_params! { + ":account_id": account_ref.0, + ":key_scope": key_scope.encode(), + ":transparent_child_index": gap_start.index().saturating_sub(gap_limit + 1) + }, + |row| { + row.get::<_, Option>(0) + .map(|opt| opt.map(BlockHeight::from)) + }, + ) + .optional() + .map(|opt| opt.flatten()) + .map_err(SqliteClientError::from) + } else { + Ok(None) + } + }; + + let h_external = get_height(KeyScope::EXTERNAL, gap_limits.external())?; + let h_internal = get_height(KeyScope::INTERNAL, gap_limits.internal())?; + + match (h_external, h_internal) { + (Some(ext), Some(int)) => Ok(std::cmp::min(ext, int)), + (Some(h), None) | (None, Some(h)) => Ok(h), + (None, None) => account_birthday_internal(conn, account_ref), + } +} + +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::Transaction, + params: &P, + output: &WalletTransparentOutput, +) -> Result<(AccountRef, KeyScope, UtxoId), SqliteClientError> { + put_transparent_output( + conn, + params, + output.outpoint(), + output.txout(), + output.mined_height(), + output.recipient_address(), + true, + ) +} + +/// An enumeration of the types of errors that can occur when scheduling an event to happen at a +/// specific time. +#[derive(Debug, Clone)] +pub enum SchedulingError { + /// An error occurred in sampling a time offset using an exponential distribution. + Distribution(rand_distr::ExpError), + /// The system attempted to generate an invalid timestamp. + Time(SystemTimeError), + /// A generated duration was out of the range of valid integer values for durations. + OutOfRange(TryFromIntError), +} + +impl std::fmt::Display for SchedulingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self { + SchedulingError::Distribution(e) => { + write!(f, "Failure in sampling scheduling time: {}", e) + } + SchedulingError::Time(t) => write!(f, "Invalid system time: {}", t), + SchedulingError::OutOfRange(t) => write!(f, "Not a valid timestamp or duration: {}", t), + } + } +} + +impl std::error::Error for SchedulingError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match &self { + SchedulingError::Distribution(_) => None, + SchedulingError::Time(t) => Some(t), + SchedulingError::OutOfRange(i) => Some(i), + } + } +} + +impl From for SchedulingError { + fn from(value: rand_distr::ExpError) -> Self { + SchedulingError::Distribution(value) + } +} + +impl From for SchedulingError { + fn from(value: SystemTimeError) -> Self { + SchedulingError::Time(value) + } +} + +impl From for SchedulingError { + fn from(value: TryFromIntError) -> Self { + SchedulingError::OutOfRange(value) + } +} + +/// Sample a random timestamp from an exponential distribution such that the expected value of the +/// generated timestamp is `check_interval_seconds` after the provided `from_event` time. +pub(crate) fn next_check_time>( + mut rng: D, + from_event: SystemTime, + check_interval_seconds: u32, +) -> Result { + // A λ parameter of 1/check_interval_seconds will result in a distribution with an expected + // value of `check_interval_seconds`. + let dist = rand_distr::Exp::new(1.0 / f64::from(check_interval_seconds))?; + let event_delay = dist.sample(rng.deref_mut()).round() as u64; + + Ok(from_event + Duration::new(event_delay, 0)) +} + +/// 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)?.ok_or(SqliteClientError::ChainHeightUnknown)?; + + // 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 spend_requests_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 spend_search_rows = spend_requests_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::TransactionsInvolvingAddress { + address, + block_range_start, + block_range_end: Some(std::cmp::min(chain_tip_height + 1, max_end_height)), + request_at: None, + tx_status_filter: TransactionStatusFilter::Mined, + output_status_filter: OutputStatusFilter::All, + }, + ) + })?; + + // Since we don't want to interpret funds that are temporarily held by an ephemeral address in + // the course of creating ZIP 320 transaction pair as belonging to the wallet, we will perform + // ephemeral address checks only for addresses that do not have an unexpired transaction + // associated with them in the database. If, for some reason, the second transaction in a ZIP + // 320 pair fails to be mined after the first transaction in the pair succeeded, we will begin + // including the associated ephemeral address in the set to be checked for funds only after + // the transaction that spends from it has expired. + let mut ephemeral_check_stmt = conn.prepare_cached( + "SELECT + cached_transparent_receiver_address, + transparent_receiver_next_check_time + FROM addresses + WHERE key_scope = :ephemeral_key_scope + AND NOT EXISTS ( + SELECT 'x' + FROM transparent_received_outputs tro + JOIN transactions t ON t.id_tx = tro.transaction_id + WHERE tro.address_id = addresses.id + AND t.expiry_height > :chain_tip_height + )", + )?; + + let ephemeral_check_rows = ephemeral_check_stmt.query_and_then( + named_params! { + ":ephemeral_key_scope": KeyScope::Ephemeral.encode(), + ":chain_tip_height": u32::from(chain_tip_height) + }, + |row| { + let address = TransparentAddress::decode(params, &row.get::<_, String>(0)?)?; + let request_at = row + .get::<_, Option>(1)? + .map(decode_epoch_seconds) + .transpose()?; + + Ok::( + TransactionDataRequest::TransactionsInvolvingAddress { + address, + // We don't want these queries to leak anything about when the wallet created + // or exposed the address, so we just query for all UTXOs for the address. + block_range_start: BlockHeight::from(0), + block_range_end: None, + request_at, + tx_status_filter: TransactionStatusFilter::All, + output_status_filter: OutputStatusFilter::Unspent, + }, + ) + }, + )?; + + spend_search_rows + .chain(ephemeral_check_rows) + .collect::, _>>() +} + +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); + let addr_meta = conn + .query_row( + "SELECT diversifier_index_be, key_scope + 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| { + let di_be: Vec = row.get(0)?; + let scope_code = row.get(1)?; + Ok(KeyScope::decode(scope_code).and_then(|key_scope| { + address_index_from_diversifier_index_be(&di_be).map(|address_index| { + TransparentAddressMetadata::new(key_scope.into(), address_index) + }) + })) + }, + ) + .optional()? + .transpose()?; + + if addr_meta.is_some() { + return Ok(addr_meta); + } + + 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)); + } + } + + 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, key_scope_code)) = conn + .query_row( + "SELECT accounts.uuid, addresses.key_scope + 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)?), row.get(1)?)), + ) + .optional()? + { + return Ok(Some((account_id, KeyScope::decode(key_scope_code)?))); + } + + 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, KeyScope::EXTERNAL))); + } + } + } + + 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, + known_unspent: bool, +) -> Result<(AccountRef, KeyScope, UtxoId), SqliteClientError> { + let addr_str = address.encode(params); + + // Unlike the shielded pools, we only can receive transparent outputs on addresses for which we + // have an `addresses` table entry, so we can just query for that here. + let (address_id, account_id, key_scope_code) = conn + .query_row( + "SELECT id, account_id, key_scope + FROM addresses + WHERE cached_transparent_receiver_address = :transparent_address", + named_params! {":transparent_address": addr_str}, + |row| { + Ok(( + row.get("id").map(AddressRef)?, + row.get("account_id").map(AccountRef)?, + row.get("key_scope")?, + )) + }, + ) + .optional()? + .ok_or(SqliteClientError::AddressNotRecognized(*address))?; + + let key_scope = KeyScope::decode(key_scope_code)?; + + let output_height = output_height.map(u32::from); + + // 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_id, address, script, + value_zat, max_observed_unspent_height + ) + VALUES ( + :transaction_id, :output_index, + :account_id, :address_id, :address, :script, + :value_zat, :max_observed_unspent_height + ) + ON CONFLICT (transaction_id, output_index) DO UPDATE + SET account_id = :account_id, + address_id = :address_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": account_id.0, + ":address_id": address_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((account_id, key_scope, 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}, + wallet::TransparentAddressMetadata, + }; + use zcash_primitives::block::BlockHash; + + use crate::{ + error::SqliteClientError, + testing::{db::TestDbFactory, BlockCache}, + wallet::{ + get_account_ref, + transparent::{ephemeral, find_gap_start, reserve_next_n_addresses}, + KeyScope, + }, + GapLimits, 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 gap_limits() { + zcash_client_backend::data_api::testing::transparent::gap_limits( + TestDbFactory::default(), + BlockCache::new(), + GapLimits::default().into(), + ); + } + + #[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(); + + // The chain height must be known in order to reserve addresses, as we store the height at + // which the address was considered to be exposed. + st.wallet_mut() + .db_mut() + .update_chain_tip(birthday.height()) + .unwrap(); + + let check = |db: &WalletDb<_, _, _, _>, account_id| { + eprintln!("checking {account_id:?}"); + assert_matches!( + find_gap_start(&db.conn, account_id, KeyScope::Ephemeral, db.gap_limits.ephemeral()), Ok(addr_index) + if addr_index == Some(NonHardenedChildIndex::ZERO) + ); + //assert_matches!(ephemeral::first_unstored_index(&db.conn, account_id), Ok(addr_index) if addr_index == GAP_LIMIT); + + let known_addrs = + ephemeral::get_known_ephemeral_addresses(&db.conn, &db.params, account_id, None) + .unwrap(); + + let expected_metadata: Vec = (0..db.gap_limits.ephemeral()) + .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); + + let transaction = &db.conn.unchecked_transaction().unwrap(); + // reserve half the addresses (rounding down) + let reserved = reserve_next_n_addresses( + transaction, + &db.params, + account_id, + KeyScope::Ephemeral, + db.gap_limits.ephemeral(), + (db.gap_limits.ephemeral() / 2) as usize, + ) + .unwrap(); + assert_eq!(reserved.len(), (db.gap_limits.ephemeral() / 2) as usize); + + // we have not yet used any of the addresses, so the maximum available address index + // should not have increased, and therefore attempting to reserve a full gap limit + // worth of addresses should fail. + assert_matches!( + reserve_next_n_addresses( + transaction, + &db.params, + account_id, + KeyScope::Ephemeral, + db.gap_limits.ephemeral(), + db.gap_limits.ephemeral() as usize + ), + Err(SqliteClientError::ReachedGapLimit(..)) + ); + }; + + 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..cd819927f4 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/transparent/ephemeral.rs @@ -0,0 +1,156 @@ +//! Functions for wallet support of ephemeral transparent addresses. +use std::ops::Range; + +use rand::{seq::SliceRandom, RngCore}; +use rusqlite::{named_params, OptionalExtension}; + +use ::transparent::{ + address::TransparentAddress, + keys::{NonHardenedChildIndex, TransparentKeyScope}, +}; +use zcash_client_backend::wallet::TransparentAddressMetadata; +use zcash_keys::encoding::AddressCodec; +use zcash_protocol::consensus; + +use crate::{ + error::SqliteClientError, + util::Clock, + wallet::{ + encoding::{decode_epoch_seconds, epoch_seconds}, + KeyScope, + }, + AccountRef, AccountUuid, +}; + +use super::next_check_time; + +// 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 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 the wallet's configured ephemeral address gap limit of 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 mut stmt = conn.prepare( + "SELECT cached_transparent_receiver_address, transparent_child_index + FROM addresses + WHERE account_id = :account_id + AND transparent_child_index >= :start + AND transparent_child_index < :end + AND key_scope = :key_scope + ORDER BY transparent_child_index", + )?; + + let results = stmt + .query_and_then( + named_params![ + ":account_id": account_id.0, + ":start": index_range.as_ref().map_or(NonHardenedChildIndex::ZERO, |i| i.start).index(), + ":end": index_range.as_ref().map_or(NonHardenedChildIndex::MAX, |i| i.end).index(), + ":key_scope": KeyScope::Ephemeral.encode() + ], + |row| { + 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"); + Ok::<_, SqliteClientError>(( + TransparentAddress::decode(params, &addr_str)?, + metadata(address_index) + )) + }, + )? + .collect::, _>>()?; + + Ok(results) +} + +/// 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 addresses + JOIN accounts ON accounts.id = account_id + WHERE cached_transparent_receiver_address = :address + AND key_scope = :key_scope", + named_params![ + ":address": &address_str, + ":key_scope": KeyScope::Ephemeral.encode() + ], + |row| Ok(AccountUuid(row.get(0)?)), + ) + .optional()?) +} + +pub(crate) fn schedule_ephemeral_address_checks( + conn: &rusqlite::Transaction, + clock: C, + mut rng: R, +) -> Result<(), SqliteClientError> { + let mut addr_check_times = conn.prepare( + "SELECT id, transparent_receiver_next_check_time + FROM addresses + WHERE key_scope = :ephemeral_key_scope + ORDER BY transparent_receiver_next_check_time NULLS FIRST", + )?; + let mut rows = addr_check_times + .query_and_then( + named_params! { + ":ephemeral_key_scope": KeyScope::Ephemeral.encode() + }, + |row| { + let id: i64 = row.get("id")?; + let next_check = row + .get::<_, Option>("transparent_receiver_next_check_time")? + .map(decode_epoch_seconds) + .transpose()?; + Ok::<_, SqliteClientError>((id, next_check)) + }, + )? + .collect::, _>>()?; + + if let Some((_, max_check_time)) = rows.last().as_ref() { + let mut set_check_time = conn.prepare( + "UPDATE addresses + SET transparent_receiver_next_check_time = :next_check + WHERE id = :address_id", + )?; + + // Set the expected value of the check time such that each ephemeral address will be + // checked once per day. + let check_interval = + (24 * 60 * 60) / u32::try_from(rows.len()).expect("number of addresses fits in a u32"); + let start_time = clock.now(); + let mut check_time = max_check_time.map_or(start_time, |t| std::cmp::max(t, start_time)); + + // Shuffle the addresses so that we don't always check them in the same order. + rows.shuffle(&mut rng); + for (address_id, addr_check_time) in rows { + // if the check time for this address is absent or in the past, schedule a check. + if addr_check_time.iter().all(|t| *t < start_time) { + check_time = next_check_time(&mut rng, check_time, check_interval)?; + set_check_time.execute(named_params! { + ":next_check": epoch_seconds(check_time)?, + ":address_id": address_id + })?; + } + } + } + + Ok(()) +} diff --git a/zcash_extensions/CHANGELOG.md b/zcash_extensions/CHANGELOG.md index 27ea222fe6..533e54eae4 100644 --- a/zcash_extensions/CHANGELOG.md +++ b/zcash_extensions/CHANGELOG.md @@ -6,5 +6,11 @@ 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 `zcash_protocol 0.5`, `zcash_primitives 0.22`. + +## [0.1.0] - 2024-07-15 Initial release. -MSRV is 1.65.0. +MSRV is 1.70.0. diff --git a/zcash_extensions/Cargo.toml b/zcash_extensions/Cargo.toml index 912d78c7fa..c176861b59 100644 --- a/zcash_extensions/Cargo.toml +++ b/zcash_extensions/Cargo.toml @@ -1,27 +1,43 @@ [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 = "2021" -rust-version = "1.65" +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"] + +[package.metadata.release] +release = false [dependencies] -blake2b_simd = "1" -zcash_primitives = { version = "0.12", path = "../zcash_primitives", default-features = false, features = ["zfuture" ] } +blake2b_simd.workspace = true +zcash_primitives = { workspace = true, features = ["non-standard-fees"] } +zcash_protocol.workspace = true [dev-dependencies] -ff = "0.13" -jubjub = "0.10" -rand_core = "0.6" -zcash_address = { version = "0.3", path = "../components/zcash_address" } -zcash_proofs = { version = "0.12", 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 = [] +zip-233 = ["zcash_primitives/zip-233"] [lib] bench = false + +[lints] +workspace = true diff --git a/zcash_extensions/README.md b/zcash_extensions/README.md index 0b38603caf..b0af0d6cec 100644 --- a/zcash_extensions/README.md +++ b/zcash_extensions/README.md @@ -12,16 +12,6 @@ Licensed under either of at your option. -Downstream code forks should note that 'zcash_extensions' depends on the -'orchard' crate, which is licensed under the -[Bootstrap Open Source License](https://github.com/zcash/orchard/blob/main/LICENSE-BOSL). -A license exception is provided allowing some derived works that are linked or -combined with the 'orchard' crate to be copied or distributed under the original -licenses (in this case MIT / Apache 2.0), provided that the included portions of -the 'orchard' code remain subject to BOSL. -See https://github.com/zcash/orchard/blob/main/COPYING for details of which -derived works can make use of this exception. - ### Contribution Unless you explicitly state otherwise, any contribution intentionally diff --git a/zcash_extensions/src/lib.rs b/zcash_extensions/src/lib.rs index 4ccb49efb6..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(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 b949817327..bd67ef0cc2 100644 --- a/zcash_extensions/src/transparent/demo.rs +++ b/zcash_extensions/src/transparent/demo.rs @@ -25,11 +25,9 @@ use blake2b_simd::Params; use zcash_primitives::{ extensions::transparent::{Extension, ExtensionTxBuilder, FromPayload, ToPayload}, - transaction::components::{ - amount::Amount, - tze::{OutPoint, TzeOut}, - }, + transaction::components::tze::{OutPoint, TzeOut}, }; +use zcash_protocol::value::Zatoshis; /// Types and constants used for Mode 0 (open a channel) mod open { @@ -377,7 +375,7 @@ impl<'a, B: ExtensionTxBuilder<'a>> DemoBuilder { /// construction. pub fn demo_open( &mut self, - value: Amount, + value: Zatoshis, hash_1: [u8; 32], ) -> Result<(), DemoBuildError> { // Call through to the generic builder. @@ -391,7 +389,7 @@ impl<'a, B: ExtensionTxBuilder<'a>> DemoBuilder { pub fn demo_transfer_to_close( &mut self, prevout: (OutPoint, TzeOut), - transfer_amount: Amount, + transfer_amount: Zatoshis, preimage_1: [u8; 32], hash_2: [u8; 32], ) -> Result<(), DemoBuildError> { @@ -476,27 +474,28 @@ impl<'a, B: ExtensionTxBuilder<'a>> DemoBuilder { #[cfg(test)] mod tests { + use std::convert::Infallible; + use blake2b_simd::Params; use ff::Field; use rand_core::OsRng; + use sapling::{zip32::ExtendedSpendingKey, Node, Rseed}; + use transparent::{address::TransparentAddress, builder::TransparentSigningSet}; use zcash_primitives::{ - consensus::{BlockHeight, BranchId, NetworkUpgrade, Parameters}, - constants, extensions::transparent::{self as tze, Extension, FromPayload, ToPayload}, - legacy::TransparentAddress, - sapling::{self, Node, Rseed}, transaction::{ - builder::Builder, - components::{ - amount::Amount, - tze::{Authorized, Bundle, OutPoint, TzeIn, TzeOut}, - }, - fees::fixed, + 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}; @@ -513,38 +512,16 @@ mod tests { 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 address_network(&self) -> Option { - 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 + 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]; @@ -636,9 +613,19 @@ mod tests { } } - fn demo_builder<'a>(height: BlockHeight) -> DemoBuilder> { + fn demo_builder<'a>( + height: BlockHeight, + sapling_anchor: sapling::Anchor, + ) -> DemoBuilder> { DemoBuilder { - txn_builder: Builder::new(FutureNetwork, height), + txn_builder: Builder::new( + FutureNetwork, + height, + BuildConfig::Standard { + sapling_anchor: Some(sapling_anchor), + orchard_anchor: Some(orchard::Anchor::empty_tree()), + }, + ), extension_id: 0, } } @@ -680,7 +667,7 @@ 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)), }; @@ -693,6 +680,8 @@ mod tests { None, None, None, + #[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))] + None, Some(Bundle { vin: vec![], vout: vec![out_a], @@ -711,7 +700,7 @@ mod tests { 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)), }; @@ -724,6 +713,8 @@ mod tests { None, None, None, + #[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))] + None, Some(Bundle { vin: vec![in_b], vout: vec![out_b], @@ -751,6 +742,8 @@ mod tests { None, None, None, + #[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))] + None, Some(Bundle { vin: vec![in_c], vout: vec![], @@ -796,11 +789,7 @@ mod tests { .activation_height(NetworkUpgrade::ZFuture) .unwrap(); - // Only run the test if we have the prover parameters. - let prover = match LocalTxProver::with_default_location() { - Some(prover) => prover, - None => return, - }; + let prover = LocalTxProver::bundled(); // // Opening transaction @@ -810,61 +799,88 @@ mod tests { // FIXME: implement zcash_primitives::transaction::fees::FutureFeeRule for zip317::FeeRule. #[allow(deprecated)] - let fee_rule = fixed::FeeRule::standard(); + let fee_rule = fixed::FeeRule::non_standard(MINIMUM_FEE); // create some inputs to spend let extsk = ExtendedSpendingKey::master(&[]); + let dfvk = extsk.to_diversifiable_full_viewing_key(); let to = extsk.default_address().1; - let note1 = to.create_note(110000, Rseed::BeforeZip212(jubjub::Fr::random(&mut rng))); + 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 = sapling::IncrementalWitness::from_tree(tree); + let witness1 = sapling::IncrementalWitness::from_tree(tree).unwrap(); - let mut builder_a = demo_builder(tx_height); + 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 value = Amount::from_u64(100000).unwrap(); + let value = Zatoshis::const_from_u64(100000); let (h1, h2) = demo_hashes(&preimage_1, &preimage_2); builder_a - .demo_open(value, h1) + .demo_open(value.into(), h1) .map_err(|e| format!("open failure: {:?}", e)) .unwrap(); - let (tx_a, _) = builder_a + let res_a = builder_a .txn_builder - .build_zfuture(&prover, &fee_rule) + .build_zfuture( + &TransparentSigningSet::new(), + sapling_extsks, + &[], + OsRng, + &prover, + &prover, + &fee_rule, + ) .map_err(|e| format!("build failure: {:?}", e)) .unwrap(); - let tze_a = tx_a.tze_bundle().unwrap(); + let tze_a = res_a.transaction().tze_bundle().unwrap(); // // Transfer // - let mut builder_b = demo_builder(tx_height + 1); - let prevout_a = (OutPoint::new(tx_a.txid(), 0), tze_a.vout[0].clone()); + let mut builder_b = demo_builder(tx_height + 1, sapling::Anchor::empty_tree()); + let prevout_a = ( + OutPoint::new(res_a.transaction().txid(), 0), + tze_a.vout[0].clone(), + ); let value_xfr = (value - fee_rule.fixed_fee()).unwrap(); builder_b - .demo_transfer_to_close(prevout_a, value_xfr, preimage_1, h2) + .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 + let res_b = builder_b .txn_builder - .build_zfuture(&prover, &fee_rule) + .build_zfuture( + &TransparentSigningSet::new(), + sapling_extsks, + &[], + OsRng, + &prover, + &prover, + &fee_rule, + ) .map_err(|e| format!("build failure: {:?}", e)) .unwrap(); - let tze_b = tx_b.tze_bundle().unwrap(); + let tze_b = res_b.transaction().tze_bundle().unwrap(); // // Closing transaction // - let mut builder_c = demo_builder(tx_height + 2); - let prevout_b = (OutPoint::new(tx_a.txid(), 0), tze_b.vout[0].clone()); + let mut builder_c = demo_builder(tx_height + 2, sapling::Anchor::empty_tree()); + let prevout_b = ( + OutPoint::new(res_a.transaction().txid(), 0), + tze_b.vout[0].clone(), + ); builder_c .demo_close(prevout_b, preimage_2) .map_err(|e| format!("close failure: {:?}", e)) @@ -872,27 +888,39 @@ mod tests { builder_c .add_transparent_output( - &TransparentAddress::PublicKey([0; 20]), + &TransparentAddress::PublicKeyHash([0; 20]), (value_xfr - fee_rule.fixed_fee()).unwrap(), ) .unwrap(); - let (tx_c, _) = builder_c + let res_c = builder_c .txn_builder - .build_zfuture(&prover, &fee_rule) + .build_zfuture( + &TransparentSigningSet::new(), + sapling_extsks, + &[], + OsRng, + &prover, + &prover, + &fee_rule, + ) .map_err(|e| format!("build failure: {:?}", e)) .unwrap(); - let tze_c = tx_c.tze_bundle().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(&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(&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 df9f83a6b1..3e010d8ad0 100644 --- a/zcash_history/CHANGELOG.md +++ b/zcash_history/CHANGELOG.md @@ -7,6 +7,10 @@ and this library adheres to Rust's notion of ## [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`. diff --git a/zcash_history/Cargo.toml b/zcash_history/Cargo.toml index c9de761fb9..5f9fc93225 100644 --- a/zcash_history/Cargo.toml +++ b/zcash_history/Cargo.toml @@ -1,26 +1,29 @@ [package] name = "zcash_history" -version = "0.3.0" +version = "0.4.0" authors = ["NikVolf "] -edition = "2021" -rust-version = "1.65" -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 = ["cryptography::cryptocurrencies"] +categories.workspace = true [dev-dependencies] -assert_matches = "1.3.0" -proptest = "1.0.0" +assert_matches.workspace = true +proptest.workspace = true [dependencies] primitive-types = { version = "0.12", default-features = false } -byteorder = "1" -blake2 = { package = "blake2b_simd", version = "1" } -proptest = { version = "1.0.0", optional = true } +byteorder.workspace = true +blake2b_simd.workspace = true +proptest = { workspace = true, optional = true } [features] -test-dependencies = ["proptest"] +test-dependencies = ["dep:proptest"] [lib] bench = false + +[lints] +workspace = true diff --git a/zcash_history/src/entry.rs b/zcash_history/src/entry.rs index 81c6b46ff7..b097db5d08 100644 --- a/zcash_history/src/entry.rs +++ b/zcash_history/src/entry.rs @@ -31,8 +31,7 @@ impl Entry { /// Returns if is this node complete (has total of 2^N leaves) pub fn complete(&self) -> bool { - let leaves = self.leaf_count(); - leaves & (leaves - 1) == 0 + self.leaf_count().is_power_of_two() } /// Number of leaves under this node. diff --git a/zcash_history/src/lib.rs b/zcash_history/src/lib.rs index 60e347803e..deb4867fc0 100644 --- a/zcash_history/src/lib.rs +++ b/zcash_history/src/lib.rs @@ -35,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/tree.rs b/zcash_history/src/tree.rs index 5d543c7387..e5a207ec58 100644 --- a/zcash_history/src/tree.rs +++ b/zcash_history/src/tree.rs @@ -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) @@ -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( @@ -291,7 +291,7 @@ pub struct IndexedNode<'a, V: Version> { link: EntryLink, } -impl<'a, V: Version> IndexedNode<'a, V> { +impl IndexedNode<'_, V> { fn left(&self) -> Result { self.node.left().map_err(|e| e.augment(self.link)) } diff --git a/zcash_history/src/version.rs b/zcash_history/src/version.rs index c9e53157d8..bfc18fa6f0 100644 --- a/zcash_history/src/version.rs +++ b/zcash_history/src/version.rs @@ -1,7 +1,7 @@ use std::fmt; use std::io; -use blake2::Params as Blake2Params; +use blake2b_simd::Params as Blake2Params; use byteorder::{ByteOrder, LittleEndian}; use crate::{node_data, NodeData, MAX_NODE_DATA_SIZE}; diff --git a/zcash_keys/CHANGELOG.md b/zcash_keys/CHANGELOG.md new file mode 100644 index 0000000000..303855f516 --- /dev/null +++ b/zcash_keys/CHANGELOG.md @@ -0,0 +1,208 @@ +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.8.0] - 2025-03-19 + +### Added +- `zcash_keys::keys::UnifiedIncomingViewingKey::{has_sapling, has_orchard, + has_transparent, receiver_requirements, to_receiver_requirements}` +- `zcash_keys::keys::ReceiverRequirements` + +### Changed +- `zcash_keys::keys::UnifiedAddressRequest` is now an enum instead of a struct. + The `new` and `unsafe_new` methods have been replaced by `custom` and + `unsafe_custom` respectively. +- Arguments to `zcash_keys::keys::UnifiedIncomingViewingKey::address` have been + modified; the `request` argument to this method now has type + `UnifiedAddressRequest` instead of `Option`. Use + `UnifiedAddressRequest::AllAvailableKeys` where `None` was previously + used to obtain the same semantics. + +### Removed +- `UnifiedAddressRequest::{new, unsafe_new}`: use `{custom, unsafe_custom}` + respectively instead. +- `UnifiedAddressRequest::intersect`: a replacement for this method is now + provided with the newly-added `ReceiverRequirements` type. + +## [0.7.0] - 2025-02-21 + +### 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`, `orchard 0.11`, + `sapling-crypto 0.5`, `zcash_encoding 0.3`, `zcash_protocol 0.5`, + `zcash_address 0.7`, `zcash_transparent 0.2`. +- `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..b341f4588f --- /dev/null +++ b/zcash_keys/Cargo.toml @@ -0,0 +1,114 @@ +[package] +name = "zcash_keys" +description = "Zcash key and address management" +version = "0.8.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..92a0fba6f6 --- /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(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_client_backend/src/encoding.rs b/zcash_keys/src/encoding.rs similarity index 70% rename from zcash_client_backend/src/encoding.rs rename to zcash_keys/src/encoding.rs index 9484122c7e..7be07a3d4b 100644 --- a/zcash_client_backend/src/encoding.rs +++ b/zcash_keys/src/encoding.rs @@ -1,55 +1,69 @@ //! 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 +//! [zcash_protocol::constants] module. use crate::address::UnifiedAddress; -use bech32::{self, Error, FromBase32, ToBase32, Variant}; +use alloc::borrow::ToOwned; +use alloc::string::{String, ToString}; use bs58::{self, decode::Error as Bs58Error}; -use std::fmt; -use std::io::{self, Write}; +use core::fmt; + +use transparent::address::TransparentAddress; use zcash_address::unified::{self, Encoding}; -use zcash_primitives::{ - consensus, - legacy::TransparentAddress, - sapling, - zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, +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, data.to_base32(), Variant::Bech32).expect("hrp is invalid") + bech32::encode::(Hrp::parse_unchecked(hrp), &data).expect("encoding is short enough") } #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg(feature = "sapling")] pub enum Bech32DecodeError { - Bech32Error(Error), - IncorrectVariant(Variant), + Bech32Error(bech32::DecodeError), + Hrp(CheckedHrpstringError), ReadError, HrpMismatch { expected: String, actual: String }, } -impl From for Bech32DecodeError { - fn from(err: Error) -> Self { +#[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::IncorrectVariant(variant) => write!( - f, - "Incorrect bech32 encoding (wrong variant: {:?})", - variant - ), + Bech32DecodeError::Hrp(e) => write!(f, "Incorrect HRP encoding: {e}"), Bech32DecodeError::ReadError => { write!(f, "Failed to decode key from its binary representation.") } @@ -62,30 +76,43 @@ impl fmt::Display for Bech32DecodeError { } } +#[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 (decoded_hrp, data, variant) = bech32::decode(s)?; - if variant != Variant::Bech32 { - Err(Bech32DecodeError::IncorrectVariant(variant)) - } else if decoded_hrp != hrp { + let parsed = CheckedHrpstring::new::(s)?; + if parsed.hrp().as_str() != hrp { Err(Bech32DecodeError::HrpMismatch { expected: hrp.to_string(), - actual: decoded_hrp, + actual: parsed.hrp().as_str().to_owned(), }) } else { - read(Vec::::from_base32(&data)?).ok_or(Bech32DecodeError::ReadError) + read(parsed.byte_iter().collect::>()).ok_or(Bech32DecodeError::ReadError) } } +/// A trait for encoding and decoding Zcash addresses. pub trait AddressCodec

where - Self: std::marker::Sized, + 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; } @@ -108,6 +135,7 @@ impl fmt::Display for TransparentCodecError { } } +#[cfg(feature = "std")] impl std::error::Error for TransparentCodecError {} impl AddressCodec

for TransparentAddress { @@ -134,6 +162,7 @@ impl AddressCodec

for TransparentAddress { } } +#[cfg(feature = "sapling")] impl AddressCodec

for sapling::PaymentAddress { type Error = Bech32DecodeError; @@ -157,7 +186,7 @@ impl AddressCodec

for UnifiedAddress { unified::Address::decode(address) .map_err(|e| format!("{}", e)) .and_then(|(network, addr)| { - if params.address_network() == Some(network) { + if params.network_type() == network { UnifiedAddress::try_from(addr).map_err(|e| e.to_owned()) } else { Err(format!( @@ -174,26 +203,27 @@ impl AddressCodec

for UnifiedAddress { /// # Examples /// /// ``` -/// use zcash_primitives::{ -/// constants::testnet::{COIN_TYPE, HRP_SAPLING_EXTENDED_SPENDING_KEY}, -/// zip32::AccountId, -/// }; -/// use zcash_client_backend::{ +/// 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::from(0)); +/// let extsk = sapling::spending_key(&[0; 32][..], COIN_TYPE, AccountId::ZERO); /// let encoded = encode_extended_spending_key(HRP_SAPLING_EXTENDED_SPENDING_KEY, &extsk); /// ``` -/// [`ExtendedSpendingKey`]: zcash_primitives::zip32::ExtendedSpendingKey +/// [`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`]: zcash_primitives::zip32::ExtendedSpendingKey +/// [`ExtendedSpendingKey`]: sapling::zip32::ExtendedSpendingKey +#[cfg(feature = "sapling")] pub fn decode_extended_spending_key( hrp: &str, s: &str, @@ -206,28 +236,27 @@ pub fn decode_extended_spending_key( /// # Examples /// /// ``` -/// use zcash_primitives::{ -/// constants::testnet::{COIN_TYPE, HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY}, -/// zip32::AccountId, -/// }; -/// use zcash_client_backend::{ +/// 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, /// }; -/// use zcash_primitives::zip32::ExtendedFullViewingKey; /// -/// let extsk = sapling::spending_key(&[0; 32][..], COIN_TYPE, AccountId::from(0)); +/// 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`]: zcash_primitives::zip32::ExtendedFullViewingKey +/// [`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. -/// -/// [`ExtendedFullViewingKey`]: zcash_primitives::zip32::ExtendedFullViewingKey +/// 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, @@ -235,19 +264,46 @@ pub fn decode_extended_full_viewing_key( 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 zcash_client_backend::{ +/// use sapling::{Diversifier, PaymentAddress}; +/// use zcash_keys::{ /// encoding::encode_payment_address, /// }; -/// use zcash_primitives::{ -/// constants::testnet::HRP_SAPLING_PAYMENT_ADDRESS, -/// sapling::{Diversifier, PaymentAddress}, -/// }; +/// 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, @@ -262,7 +318,8 @@ pub fn decode_extended_full_viewing_key( /// "ztestsapling1qqqqqqqqqqqqqqqqqqcguyvaw2vjk4sdyeg0lc970u659lvhqq7t0np6hlup5lusxle75ss7jnk", /// ); /// ``` -/// [`PaymentAddress`]: zcash_primitives::sapling::PaymentAddress +/// [`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())) } @@ -271,7 +328,8 @@ pub fn encode_payment_address(hrp: &str, addr: &sapling::PaymentAddress) -> Stri /// using the human-readable prefix values defined in the specified /// network parameters. /// -/// [`PaymentAddress`]: zcash_primitives::sapling::PaymentAddress +/// [`PaymentAddress`]: sapling::PaymentAddress +#[cfg(feature = "sapling")] pub fn encode_payment_address_p( params: &P, addr: &sapling::PaymentAddress, @@ -285,13 +343,11 @@ pub fn encode_payment_address_p( /// /// ``` /// use group::Group; -/// use zcash_client_backend::{ +/// use sapling::{Diversifier, PaymentAddress}; +/// use zcash_keys::{ /// encoding::decode_payment_address, /// }; -/// use zcash_primitives::{ -/// consensus::{TEST_NETWORK, Parameters}, -/// sapling::{Diversifier, PaymentAddress}, -/// }; +/// 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, @@ -309,7 +365,8 @@ pub fn encode_payment_address_p( /// Ok(pa), /// ); /// ``` -/// [`PaymentAddress`]: zcash_primitives::sapling::PaymentAddress +/// [`PaymentAddress`]: sapling::PaymentAddress +#[cfg(feature = "sapling")] pub fn decode_payment_address( hrp: &str, s: &str, @@ -330,19 +387,15 @@ pub fn decode_payment_address( /// # Examples /// /// ``` -/// use zcash_client_backend::{ -/// encoding::encode_transparent_address, -/// }; -/// use zcash_primitives::{ -/// consensus::{TEST_NETWORK, Parameters}, -/// legacy::TransparentAddress, -/// }; +/// 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::PublicKey([0; 20]), +/// &TransparentAddress::PublicKeyHash([0; 20]), /// ), /// "tm9iMLAuYMzJ6jtFLcA7rzUmfreGuKvr7Ma", /// ); @@ -351,25 +404,23 @@ pub fn decode_payment_address( /// encode_transparent_address( /// &TEST_NETWORK.b58_pubkey_address_prefix(), /// &TEST_NETWORK.b58_script_address_prefix(), -/// &TransparentAddress::Script([0; 20]), +/// &TransparentAddress::ScriptHash([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) => { + 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::Script(script_id) => { + 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); @@ -398,13 +449,11 @@ pub fn encode_transparent_address_p( /// # Examples /// /// ``` -/// use zcash_primitives::{ -/// consensus::{TEST_NETWORK, Parameters}, -/// }; -/// use zcash_client_backend::{ +/// use zcash_protocol::consensus::{TEST_NETWORK, NetworkConstants, Parameters}; +/// use transparent::address::TransparentAddress; +/// use zcash_keys::{ /// encoding::decode_transparent_address, /// }; -/// use zcash_primitives::legacy::TransparentAddress; /// /// assert_eq!( /// decode_transparent_address( @@ -412,7 +461,7 @@ pub fn encode_transparent_address_p( /// &TEST_NETWORK.b58_script_address_prefix(), /// "tm9iMLAuYMzJ6jtFLcA7rzUmfreGuKvr7Ma", /// ), -/// Ok(Some(TransparentAddress::PublicKey([0; 20]))), +/// Ok(Some(TransparentAddress::PublicKeyHash([0; 20]))), /// ); /// /// assert_eq!( @@ -421,10 +470,8 @@ pub fn encode_transparent_address_p( /// &TEST_NETWORK.b58_script_address_prefix(), /// "t26YoyZ1iPgiMEWL4zGUm74eVWfhyDMXzY2", /// ), -/// Ok(Some(TransparentAddress::Script([0; 20]))), +/// Ok(Some(TransparentAddress::ScriptHash([0; 20]))), /// ); -/// ``` -/// [`TransparentAddress`]: zcash_primitives::legacy::TransparentAddress pub fn decode_transparent_address( pubkey_version: &[u8], script_version: &[u8], @@ -435,12 +482,12 @@ pub fn decode_transparent_address( decoded[pubkey_version.len()..] .try_into() .ok() - .map(TransparentAddress::PublicKey) + .map(TransparentAddress::PublicKeyHash) } else if decoded.starts_with(script_version) { decoded[script_version.len()..] .try_into() .ok() - .map(TransparentAddress::Script) + .map(TransparentAddress::ScriptHash) } else { None } @@ -448,14 +495,15 @@ pub fn decode_transparent_address( } #[cfg(test)] -mod tests { - use zcash_primitives::{constants, sapling::PaymentAddress, zip32::ExtendedSpendingKey}; - +#[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() { @@ -504,7 +552,7 @@ mod tests { 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, @@ -528,10 +576,19 @@ mod tests { ), 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::testnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, - encoded_test + constants::regtest::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, + encoded_regtest ) .unwrap(), extfvk @@ -552,11 +609,14 @@ mod tests { "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, @@ -570,6 +630,12 @@ mod tests { 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, @@ -578,6 +644,15 @@ mod tests { .unwrap(), addr ); + + assert_eq!( + decode_payment_address( + constants::regtest::HRP_SAPLING_PAYMENT_ADDRESS, + encoded_regtest + ) + .unwrap(), + addr + ); } #[test] diff --git a/zcash_keys/src/keys.rs b/zcash_keys/src/keys.rs new file mode 100644 index 0000000000..22fa841c7f --- /dev/null +++ b/zcash_keys/src/keys.rs @@ -0,0 +1,1952 @@ +//! 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: UnifiedAddressRequest, + ) -> (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 enum UnifiedAddressRequest { + AllAvailableKeys, + Custom(ReceiverRequirements), +} + +impl UnifiedAddressRequest { + /// Constructs a new unified address request that allows a receiver of each type. + pub const ALLOW_ALL: Self = Self::Custom(ReceiverRequirements::ALLOW_ALL); + + pub fn custom( + orchard: ReceiverRequirement, + sapling: ReceiverRequirement, + p2pkh: ReceiverRequirement, + ) -> Result { + ReceiverRequirements::new(orchard, sapling, p2pkh).map(UnifiedAddressRequest::Custom) + } + + pub const fn unsafe_custom( + orchard: ReceiverRequirement, + sapling: ReceiverRequirement, + p2pkh: ReceiverRequirement, + ) -> Self { + UnifiedAddressRequest::Custom(ReceiverRequirements::unsafe_new(orchard, sapling, p2pkh)) + } +} + +/// Specification for how a unified address should be generated from a unified viewing key. +#[derive(Clone, Copy, Debug)] +pub struct ReceiverRequirements { + orchard: ReceiverRequirement, + sapling: ReceiverRequirement, + p2pkh: ReceiverRequirement, +} + +impl ReceiverRequirements { + /// 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: ReceiverRequirements = { + 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: &ReceiverRequirements) -> 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, + } + } + + /// Returns the [`ReceiverRequirement`] for inclusion of an Orchard receiver. + pub fn orchard(&self) -> ReceiverRequirement { + self.orchard + } + + /// Returns the [`ReceiverRequirement`] for inclusion of a Sapling receiver. + pub fn sapling(&self) -> ReceiverRequirement { + self.sapling + } + + /// Returns the [`ReceiverRequirement`] for inclusion of a P2PKH receiver. + pub fn p2pkh(&self) -> ReceiverRequirement { + self.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: UnifiedAddressRequest, + ) -> 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: UnifiedAddressRequest, + ) -> 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: UnifiedAddressRequest, + ) -> 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 whether this uivk has a transparent key item. + /// + /// This method is available irrespective of whether the `transparent-inputs` feature flag is enabled. + pub fn has_transparent(&self) -> bool { + #[cfg(not(feature = "transparent-inputs"))] + return false; + #[cfg(feature = "transparent-inputs")] + return self.transparent.is_some(); + } + + /// Returns the Transparent external IVK, if present. + #[cfg(feature = "transparent-inputs")] + pub fn transparent(&self) -> &Option { + &self.transparent + } + + /// Returns whether this uivk has a Sapling key item. + /// + /// This method is available irrespective of whether the `sapling` feature flag is enabled. + pub fn has_sapling(&self) -> bool { + #[cfg(not(feature = "sapling"))] + return false; + #[cfg(feature = "sapling")] + return self.sapling.is_some(); + } + + /// Returns the Sapling IVK, if present. + #[cfg(feature = "sapling")] + pub fn sapling(&self) -> &Option<::sapling::zip32::IncomingViewingKey> { + &self.sapling + } + + /// Returns whether this uivk has an Orchard key item. + /// + /// 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 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: UnifiedAddressRequest, + ) -> Result { + use ReceiverRequirement::*; + + let request = self + .receiver_requirements(request) + .map_err(|_| 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)); + } + + #[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: UnifiedAddressRequest, + ) -> Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError> { + // Find a working diversifier and construct the associated address. + loop { + let res = self.address(j, 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: UnifiedAddressRequest, + ) -> Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError> { + self.find_address(DiversifierIndex::new(), request) + } + + /// Convenience method for choosing a set of receiver requirements based upon the given unified + /// address request and the available items of this key. + /// + /// Returns an error if the provided request cannot be satisfied in address generation using + /// this key. + pub fn receiver_requirements( + &self, + request: UnifiedAddressRequest, + ) -> Result { + use ReceiverRequirement::*; + match request { + UnifiedAddressRequest::AllAvailableKeys => self + .to_receiver_requirements() + .map_err(|_| AddressGenerationError::ShieldedReceiverRequired), + UnifiedAddressRequest::Custom(req) => { + if req.orchard() == Require && !self.has_orchard() { + return Err(AddressGenerationError::ReceiverTypeNotSupported( + Typecode::Orchard, + )); + } + + if req.sapling() == Require && !self.has_sapling() { + return Err(AddressGenerationError::ReceiverTypeNotSupported( + Typecode::Sapling, + )); + } + + if req.p2pkh() == Require && !self.has_transparent() { + return Err(AddressGenerationError::ReceiverTypeNotSupported( + Typecode::P2pkh, + )); + } + + Ok(req) + } + } + } + + /// Constructs the [`ReceiverRequirements`] 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_receiver_requirements(&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; + } + + ReceiverRequirements::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, + UnifiedAddressRequest::unsafe_custom(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, + UnifiedAddressRequest::unsafe_custom(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..292846f99e --- /dev/null +++ b/zcash_keys/src/lib.rs @@ -0,0 +1,32 @@ +//! *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. +//! +#![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 4f3d765178..3eac27d3b1 100644 --- a/zcash_primitives/CHANGELOG.md +++ b/zcash_primitives/CHANGELOG.md @@ -7,6 +7,460 @@ and this library adheres to Rust's notion of ## [Unreleased] +### Changed +- Variants of `zcash_primitives::transaction::TxVersion` have changed. They + now represent explicit transaction versions, in order to avoid accidental + confusion with the names of the network upgrades that they were introduced + in. + +## [0.22.0] - 2025-02-21 + +### Changed +- MSRV is now 1.81.0. +- Migrated to `bip32 =0.6.0-pre.1`, `nonempty 0.11`, `secp256k1 0.29`, + `incrementalmerkletree 0.8`, `redjubjub 0.8`, `orchard 0.11`, + `sapling-crypto 0.5`, `zcash_encoding 0.3`, `zcash_protocol 0.5`, + `zcash_address 0.7`, `zcash_transparent 0.2`. + +### 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.19.1, 0.20.1] - 2025-05-09 + +### Fixed +- Migrated to `orchard 0.10.2` to fix a missing feature dependency. + +## [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`: @@ -27,7 +481,7 @@ and this library adheres to Rust's notion of `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` +- `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 @@ -89,7 +543,7 @@ and this library adheres to Rust's notion of - 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}` +- 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 @@ -539,6 +993,9 @@ and `zcash_encoding`. 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 diff --git a/zcash_primitives/Cargo.toml b/zcash_primitives/Cargo.toml index cec7ecc2c8..fdb8ac3923 100644 --- a/zcash_primitives/Cargo.toml +++ b/zcash_primitives/Cargo.toml @@ -1,109 +1,154 @@ [package] name = "zcash_primitives" description = "Rust implementations of the Zcash primitives" -version = "0.12.0" +version = "0.22.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 = "2021" -rust-version = "1.65" -categories = ["cryptography::cryptocurrencies"] +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] -equihash = { version = "0.2", path = "../components/equihash" } -zcash_address = { version = "0.3", path = "../components/zcash_address" } -zcash_encoding = { version = "0.2", path = "../components/zcash_encoding" } +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 = "0.8" -rand_core = "0.6" +rand.workspace = true +rand_core.workspace = true # - Digests (output types exposed) -blake2b_simd = "1" -sha2 = "0.10" +blake2b_simd.workspace = true +sha2.workspace = true -# - Metrics -memuse = "0.2.1" +# - Logging and metrics +memuse.workspace = true +tracing = { workspace = true, default-features = false } # - Secret management -subtle = "2.2.3" +subtle.workspace = true # - Shielded protocols -bls12_381 = "0.8" -ff = "0.13" -group = { version = "0.13", features = ["wnaf-memuse"] } -jubjub = "0.10" -nonempty = "0.7" -orchard = { version = "0.5", default-features = false } +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 = { version = "0.4", features = ["legacy-api"] } - -# - Static constants -lazy_static = "1" +incrementalmerkletree = { workspace = true, features = ["legacy-api"] } # - Test dependencies -proptest = { version = "1.0.0", optional = true } +proptest = { workspace = true, optional = true } + +# - Transparent protocol +transparent.workspace = true # - Transparent inputs # - `Error` type exposed -hdwallet = { version = "0.4", optional = true } +bip32.workspace = true +block-buffer.workspace = true # remove after edition2024 upgrade +crypto-common.workspace = true # remove after edition2024 upgrade # - `SecretKey` and `PublicKey` types exposed -secp256k1 = { version = "0.26", optional = true } - -# - ZIP 339 -bip0039 = { version = "0.10", features = ["std", "all-languages"] } +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 -byteorder = "1" -hex = "0.4" +bs58.workspace = true +hex.workspace = true # - Shielded protocols -bitvec = "1" -blake2s_simd = "1" +redjubjub.workspace = true -# - Transparent inputs -ripemd = { version = "0.1", optional = true } +# - Transparent protocol +ripemd.workspace = true # - ZIP 32 -aes = "0.8" -fpe = "0.6" +fpe.workspace = true + +# No-std support +core2.workspace = true [dependencies.zcash_note_encryption] -version = "0.4" +workspace = true features = ["pre-zip-212"] [dev-dependencies] chacha20poly1305 = "0.10" -criterion = "0.4" -incrementalmerkletree = { version = "0.4", features = ["legacy-api", "test-dependencies"] } -proptest = "1.0.0" -assert_matches = "1.3.0" -rand_xorshift = "0.3" -orchard = { version = "0.5", default-features = false, features = ["test-dependencies"] } +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.11", features = ["criterion", "flamegraph"] } # MSRV 1.56 +pprof = { version = "0.14", features = ["criterion", "flamegraph"] } [features] -default = ["multicore"] -multicore = ["orchard/multicore"] -transparent-inputs = ["hdwallet", "ripemd", "secp256k1"] +default = ["multicore", "std", "circuits"] +std = ["document-features", "redjubjub/std"] +zip-233 = [] + +## 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 = [] -test-dependencies = ["proptest", "orchard/test-dependencies"] -zfuture = [] + +## 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 @@ -112,9 +157,8 @@ bench = false name = "note_decryption" harness = false -[[bench]] -name = "pedersen_hash" -harness = false - [badges] maintenance = { status = "actively-developed" } + +[lints] +workspace = true diff --git a/zcash_primitives/README.md b/zcash_primitives/README.md index efa356ba3e..02a0c33a10 100644 --- a/zcash_primitives/README.md +++ b/zcash_primitives/README.md @@ -12,16 +12,6 @@ Licensed under either of at your option. -Downstream code forks should note that 'zcash_primitives' depends on the -'orchard' crate, which is licensed under the -[Bootstrap Open Source License](https://github.com/zcash/orchard/blob/main/LICENSE-BOSL). -A license exception is provided allowing some derived works that are linked or -combined with the 'orchard' crate to be copied or distributed under the original -licenses (in this case MIT / Apache 2.0), provided that the included portions of -the 'orchard' code remain subject to BOSL. -See https://github.com/zcash/orchard/blob/main/COPYING for details of which -derived works can make use of this exception. - ### Contribution Unless you explicitly state otherwise, any contribution intentionally diff --git a/zcash_primitives/benches/note_decryption.rs b/zcash_primitives/benches/note_decryption.rs index 4827badce7..619a40f3a7 100644 --- a/zcash_primitives/benches/note_decryption.rs +++ b/zcash_primitives/benches/note_decryption.rs @@ -1,24 +1,24 @@ -use std::iter; +use core::iter; use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; use ff::Field; use rand_core::OsRng; +use sapling::{ + self, + note_encryption::{ + try_sapling_compact_note_decryption, try_sapling_note_decryption, CompactOutputDescription, + PreparedIncomingViewingKey, SaplingDomain, + }, + prover::mock::{MockOutputProver, MockSpendProver}, + value::NoteValue, + Diversifier, SaplingIvk, +}; use zcash_note_encryption::batch; -use zcash_primitives::{ +use zcash_primitives::transaction::components::sapling::zip212_enforcement; +use zcash_protocol::{ consensus::{NetworkUpgrade::Canopy, Parameters, TEST_NETWORK}, - memo::MemoBytes, - sapling::{ - note_encryption::{ - try_sapling_compact_note_decryption, try_sapling_note_decryption, - PreparedIncomingViewingKey, SaplingDomain, - }, - prover::mock::MockTxProver, - value::NoteValue, - Diversifier, SaplingIvk, - }, - transaction::components::sapling::{ - builder::SaplingBuilder, CompactOutputDescription, GrothProofBytes, OutputDescription, - }, + memo::Memo, + value::ZatBalance, }; #[cfg(unix)] @@ -27,27 +27,33 @@ 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 Sapling output. - let output: OutputDescription = { + let output = { let diversifier = Diversifier([0; 11]); let pa = valid_ivk.to_payment_address(diversifier).unwrap(); - let mut builder = SaplingBuilder::new(TEST_NETWORK, height); + 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( - &mut rng, None, pa, NoteValue::from_raw(100), - MemoBytes::empty(), + Memo::Empty.encode().into_bytes(), ) .unwrap(); - let bundle = builder - .build(&MockTxProver, &mut (), &mut rng, height, None) + let (bundle, _) = builder + .build::(&[], &mut rng) .unwrap() .unwrap(); bundle.shielded_outputs()[0].clone() @@ -61,27 +67,25 @@ fn bench_note_decryption(c: &mut Criterion) { group.throughput(Throughput::Elements(1)); group.bench_function("valid", |b| { - b.iter(|| { - try_sapling_note_decryption(&TEST_NETWORK, height, &valid_ivk, &output).unwrap() - }) + b.iter(|| try_sapling_note_decryption(&valid_ivk, &output, zip212_enforcement).unwrap()) }); group.bench_function("invalid", |b| { - b.iter(|| try_sapling_note_decryption(&TEST_NETWORK, height, &invalid_ivk, &output)) + 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(&TEST_NETWORK, height, &valid_ivk, &compact) + try_sapling_compact_note_decryption(&valid_ivk, &compact, zip212_enforcement) .unwrap() }) }); group.bench_function("compact-invalid", |b| { b.iter(|| { - try_sapling_compact_note_decryption(&TEST_NETWORK, height, &invalid_ivk, &compact) + try_sapling_compact_note_decryption(&invalid_ivk, &compact, zip212_enforcement) }) }); } @@ -95,7 +99,7 @@ fn bench_note_decryption(c: &mut Criterion) { let outputs: Vec<_> = iter::repeat(output.clone()) .take(noutputs) - .map(|output| (SaplingDomain::for_height(TEST_NETWORK, height), output)) + .map(|output| (SaplingDomain::new(zip212_enforcement), output)) .collect(); group.bench_function( diff --git a/zcash_primitives/benches/pedersen_hash.rs b/zcash_primitives/benches/pedersen_hash.rs deleted file mode 100644 index 847e68b751..0000000000 --- a/zcash_primitives/benches/pedersen_hash.rs +++ /dev/null @@ -1,28 +0,0 @@ -use criterion::{criterion_group, criterion_main, Criterion}; -use rand_core::{OsRng, RngCore}; -use zcash_primitives::sapling::pedersen_hash::{pedersen_hash, Personalization}; - -#[cfg(unix)] -use pprof::criterion::{Output, PProfProfiler}; - -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())) - }); -} - -#[cfg(unix)] -criterion_group! { - name = benches; - config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flamegraph(None))); - targets = bench_pedersen_hash -} -#[cfg(not(unix))] -criterion_group!(benches, bench_pedersen_hash); -criterion_main!(benches); diff --git a/zcash_primitives/src/block.rs b/zcash_primitives/src/block.rs index 6271e05356..9748941f00 100644 --- a/zcash_primitives/src/block.rs +++ b/zcash_primitives/src/block.rs @@ -1,15 +1,22 @@ //! 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 zcash_encoding::Vector; pub use equihash; +/// 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]); @@ -39,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 + } } } @@ -60,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, @@ -98,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)?; @@ -109,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)?; @@ -130,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))?; @@ -146,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/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 9f53bd3008..513eadaf9f 100644 --- a/zcash_primitives/src/extensions/transparent.rs +++ b/zcash_primitives/src/extensions/transparent.rs @@ -1,11 +1,10 @@ //! Core traits and structs for Transparent Zcash Extensions. -use std::fmt; +use alloc::vec::Vec; +use core::fmt; -use crate::transaction::components::{ - tze::{self, TzeOut}, - Amount, -}; +use crate::transaction::components::tze::{self, TzeOut}; +use zcash_protocol::value::Zatoshis; /// A typesafe wrapper for witness payloads #[derive(Debug, Clone, PartialEq, Eq)] @@ -14,7 +13,7 @@ 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; @@ -25,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); @@ -203,11 +202,11 @@ pub trait ExtensionTxBuilder<'a> { 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/keys.rs b/zcash_primitives/src/keys.rs deleted file mode 100644 index ea085b2a1e..0000000000 --- a/zcash_primitives/src/keys.rs +++ /dev/null @@ -1,22 +0,0 @@ -use blake2b_simd::{Hash as Blake2bHash, Params as Blake2bParams}; - -pub use crate::sapling::keys::OutgoingViewingKey; - -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() -} diff --git a/zcash_primitives/src/legacy.rs b/zcash_primitives/src/legacy.rs deleted file mode 100644 index e1d12a766f..0000000000 --- a/zcash_primitives/src/legacy.rs +++ /dev/null @@ -1,207 +0,0 @@ -//! Support for legacy transparent addresses and scripts. - -use byteorder::{ReadBytesExt, WriteBytesExt}; -use std::io::{self, Read, Write}; -use std::ops::Shl; - -use zcash_encoding::Vector; - -#[cfg(feature = "transparent-inputs")] -pub mod keys; - -/// 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, Eq)] -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(crate) 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(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum TransparentAddress { - PublicKey([u8; 20]), // TODO: Rename to PublicKeyHash - Script([u8; 20]), // TODO: Rename to ScriptHash -} - -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/legacy/keys.rs b/zcash_primitives/src/legacy/keys.rs deleted file mode 100644 index fead35a812..0000000000 --- a/zcash_primitives/src/legacy/keys.rs +++ /dev/null @@ -1,520 +0,0 @@ -use hdwallet::{ - traits::{Deserialize, Serialize}, - ExtendedPrivKey, ExtendedPubKey, KeyIndex, -}; -use secp256k1::PublicKey; -use sha2::{Digest, Sha256}; - -use crate::{consensus, keys::prf_expand_vec, zip32::AccountId}; - -use super::TransparentAddress; - -const MAX_TRANSPARENT_CHILD_INDEX: u32 = 0x7FFFFFFF; - -/// A type representing a BIP-44 private key at the account path level -/// `m/44'/'/' -#[derive(Clone, Debug)] -pub struct AccountPrivKey(ExtendedPrivKey); - -impl AccountPrivKey { - /// Performs derivation of the extended private key for the BIP-44 path: - /// `m/44'/'/'`. - /// - /// This produces the root of the derivation tree for transparent - /// viewing keys and addresses for the for the provided account. - pub fn from_seed( - params: &P, - seed: &[u8], - account: AccountId, - ) -> Result { - ExtendedPrivKey::with_seed(seed)? - .derive_private_key(KeyIndex::hardened_from_normalize_index(44)?)? - .derive_private_key(KeyIndex::hardened_from_normalize_index(params.coin_type())?)? - .derive_private_key(KeyIndex::hardened_from_normalize_index(account.into())?) - .map(AccountPrivKey) - } - - pub fn from_extended_privkey(extprivkey: ExtendedPrivKey) -> Self { - AccountPrivKey(extprivkey) - } - - pub fn to_account_pubkey(&self) -> AccountPubKey { - AccountPubKey(ExtendedPubKey::from_private_key(&self.0)) - } - - /// Derives the BIP-44 private spending key for the external (incoming payment) child path - /// `m/44'/'/'/0/`. - pub fn derive_external_secret_key( - &self, - child_index: u32, - ) -> Result { - self.0 - .derive_private_key(KeyIndex::Normal(0))? - .derive_private_key(KeyIndex::Normal(child_index)) - .map(|k| k.private_key) - } - - /// Derives the BIP-44 private spending key for the internal (change) child path - /// `m/44'/'/'/1/`. - pub fn derive_internal_secret_key( - &self, - child_index: u32, - ) -> Result { - self.0 - .derive_private_key(KeyIndex::Normal(1))? - .derive_private_key(KeyIndex::Normal(child_index)) - .map(|k| k.private_key) - } - - /// Returns the `AccountPrivKey` serialized using the encoding for a - /// [BIP 32](https://en.bitcoin.it/wiki/BIP_0032) ExtendedPrivKey - pub fn to_bytes(&self) -> Vec { - self.0.serialize() - } - - /// Decodes the `AccountPrivKey` from the encoding specified for a - /// [BIP 32](https://en.bitcoin.it/wiki/BIP_0032) ExtendedPrivKey - pub fn from_bytes(b: &[u8]) -> Option { - ExtendedPrivKey::deserialize(b) - .map(AccountPrivKey::from_extended_privkey) - .ok() - } -} - -/// A type representing a BIP-44 public key at the account path level -/// `m/44'/'/'`. -/// -/// This provides the necessary derivation capability for the transparent component of a unified -/// full viewing key. -#[derive(Clone, Debug)] -pub struct AccountPubKey(ExtendedPubKey); - -impl AccountPubKey { - /// Derives the BIP-44 public key at the external "change level" path - /// `m/44'/'/'/0`. - pub fn derive_external_ivk(&self) -> Result { - self.0 - .derive_public_key(KeyIndex::Normal(0)) - .map(ExternalIvk) - } - - /// Derives the BIP-44 public key at the internal "change level" path - /// `m/44'/'/'/1`. - pub fn derive_internal_ivk(&self) -> Result { - self.0 - .derive_public_key(KeyIndex::Normal(1)) - .map(InternalIvk) - } - - /// Derives the internal ovk and external ovk corresponding to this - /// transparent fvk. As specified in [ZIP 316][transparent-ovk]. - /// - /// [transparent-ovk]: https://zips.z.cash/zip-0316#deriving-internal-keys - pub fn ovks_for_shielding(&self) -> (InternalOvk, ExternalOvk) { - let i_ovk = prf_expand_vec( - &self.0.chain_code, - &[&[0xd0], &self.0.public_key.serialize()], - ); - let i_ovk = i_ovk.as_bytes(); - let ovk_external = ExternalOvk(i_ovk[..32].try_into().unwrap()); - let ovk_internal = InternalOvk(i_ovk[32..].try_into().unwrap()); - - (ovk_internal, ovk_external) - } - - /// Derives the internal ovk corresponding to this transparent fvk. - pub fn internal_ovk(&self) -> InternalOvk { - self.ovks_for_shielding().0 - } - - /// Derives the external ovk corresponding to this transparent fvk. - pub fn external_ovk(&self) -> ExternalOvk { - self.ovks_for_shielding().1 - } - - pub fn serialize(&self) -> Vec { - let mut buf = self.0.chain_code.clone(); - buf.extend(self.0.public_key.serialize().to_vec()); - buf - } - - pub fn deserialize(data: &[u8; 65]) -> Result { - let chain_code = data[..32].to_vec(); - let public_key = PublicKey::from_slice(&data[32..])?; - Ok(AccountPubKey(ExtendedPubKey { - public_key, - chain_code, - })) - } -} - -/// Derives the P2PKH transparent address corresponding to the given pubkey. -#[deprecated(note = "This function will be removed from the public API in an upcoming refactor.")] -pub fn pubkey_to_address(pubkey: &secp256k1::PublicKey) -> TransparentAddress { - TransparentAddress::PublicKey( - *ripemd::Ripemd160::digest(Sha256::digest(pubkey.serialize())).as_ref(), - ) -} - -pub(crate) mod private { - use hdwallet::ExtendedPubKey; - pub trait SealedChangeLevelKey { - fn extended_pubkey(&self) -> &ExtendedPubKey; - fn from_extended_pubkey(key: ExtendedPubKey) -> Self; - } -} - -pub trait IncomingViewingKey: private::SealedChangeLevelKey + std::marker::Sized { - /// Derives a transparent address at the provided child index. - #[allow(deprecated)] - fn derive_address( - &self, - child_index: u32, - ) -> Result { - let child_key = self - .extended_pubkey() - .derive_public_key(KeyIndex::Normal(child_index))?; - Ok(pubkey_to_address(&child_key.public_key)) - } - - /// Searches the space of child indexes for an index that will - /// generate a valid transparent address, and returns the resulting - /// address and the index at which it was generated. - fn default_address(&self) -> (TransparentAddress, u32) { - let mut child_index = 0; - while child_index <= MAX_TRANSPARENT_CHILD_INDEX { - match self.derive_address(child_index) { - Ok(addr) => { - return (addr, child_index); - } - Err(_) => { - child_index += 1; - } - } - } - panic!("Exhausted child index space attempting to find a default address."); - } - - fn serialize(&self) -> Vec { - let extpubkey = self.extended_pubkey(); - let mut buf = extpubkey.chain_code.clone(); - buf.extend(extpubkey.public_key.serialize().to_vec()); - buf - } - - fn deserialize(data: &[u8; 65]) -> Result { - let chain_code = data[..32].to_vec(); - let public_key = PublicKey::from_slice(&data[32..])?; - Ok(Self::from_extended_pubkey(ExtendedPubKey { - public_key, - chain_code, - })) - } -} - -/// A type representing an incoming viewing key at the BIP-44 "external" -/// path `m/44'/'/'/0`. This allows derivation -/// of child addresses that may be provided to external parties. -#[derive(Clone, Debug)] -pub struct ExternalIvk(ExtendedPubKey); - -impl private::SealedChangeLevelKey for ExternalIvk { - fn extended_pubkey(&self) -> &ExtendedPubKey { - &self.0 - } - - fn from_extended_pubkey(key: ExtendedPubKey) -> Self { - ExternalIvk(key) - } -} - -impl IncomingViewingKey for ExternalIvk {} - -/// A type representing an incoming viewing key at the BIP-44 "internal" -/// path `m/44'/'/'/1`. This allows derivation -/// of change addresses for use within the wallet, but which should -/// not be shared with external parties. -#[derive(Clone, Debug)] -pub struct InternalIvk(ExtendedPubKey); - -impl private::SealedChangeLevelKey for InternalIvk { - fn extended_pubkey(&self) -> &ExtendedPubKey { - &self.0 - } - - fn from_extended_pubkey(key: ExtendedPubKey) -> Self { - InternalIvk(key) - } -} - -impl IncomingViewingKey for InternalIvk {} - -/// Internal ovk used for autoshielding. -pub struct InternalOvk([u8; 32]); - -impl InternalOvk { - pub fn as_bytes(&self) -> [u8; 32] { - self.0 - } -} - -/// External ovk used by zcashd for transparent -> shielded spends to -/// external receivers. -pub struct ExternalOvk([u8; 32]); - -impl ExternalOvk { - pub fn as_bytes(&self) -> [u8; 32] { - self.0 - } -} - -#[cfg(test)] -mod tests { - use super::AccountPubKey; - - #[test] - fn check_ovk_test_vectors() { - struct TestVector { - c: [u8; 32], - pk: [u8; 33], - external_ovk: [u8; 32], - internal_ovk: [u8; 32], - } - - // From https://github.com/zcash-hackworks/zcash-test-vectors/blob/master/zip_0316.py - let test_vectors = vec![ - TestVector { - c: [ - 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, - ], - pk: [ - 0x02, 0x16, 0x88, 0x4f, 0x1d, 0xbc, 0x92, 0x90, 0x89, 0xa4, 0x17, 0x6e, 0x84, - 0x0b, 0xb5, 0x81, 0xc8, 0x0e, 0x16, 0xe9, 0xb1, 0xab, 0xd6, 0x54, 0xe6, 0x2c, - 0x8b, 0x0b, 0x95, 0x70, 0x20, 0xb7, 0x48, - ], - external_ovk: [ - 0xdc, 0xe7, 0xfb, 0x7f, 0x20, 0xeb, 0x77, 0x64, 0xd5, 0x12, 0x4f, 0xbd, 0x23, - 0xc4, 0xd7, 0xca, 0x8c, 0x32, 0x19, 0xec, 0x1d, 0xb3, 0xff, 0x1e, 0x08, 0x13, - 0x50, 0xad, 0x03, 0x9b, 0x40, 0x79, - ], - internal_ovk: [ - 0x4d, 0x46, 0xc7, 0x14, 0xed, 0xda, 0xd9, 0x4a, 0x40, 0xac, 0x21, 0x28, 0x6a, - 0xff, 0x32, 0x7d, 0x7e, 0xbf, 0x11, 0x9e, 0x86, 0x85, 0x10, 0x9b, 0x44, 0xe8, - 0x02, 0x83, 0xd8, 0xc8, 0xa4, 0x00, - ], - }, - TestVector { - c: [ - 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, - ], - pk: [ - 0x03, 0x72, 0x73, 0xb6, 0x57, 0xd9, 0x71, 0xa4, 0x5e, 0x72, 0x24, 0x0c, 0x7a, - 0xaa, 0xa7, 0xd0, 0x68, 0x5d, 0x06, 0xd7, 0x99, 0x9b, 0x0a, 0x19, 0xc4, 0xce, - 0xa3, 0x27, 0x88, 0xa6, 0xab, 0x51, 0x3d, - ], - external_ovk: [ - 0x8d, 0x31, 0x53, 0x7b, 0x38, 0x8f, 0x40, 0x23, 0xe6, 0x48, 0x70, 0x8b, 0xfb, - 0xde, 0x2b, 0xa1, 0xff, 0x1a, 0x4e, 0xe1, 0x12, 0xea, 0x67, 0x0a, 0xd1, 0x67, - 0x44, 0xf4, 0x58, 0x3e, 0x95, 0x52, - ], - internal_ovk: [ - 0x16, 0x77, 0x49, 0x00, 0x76, 0x9d, 0x9c, 0x03, 0xbe, 0x06, 0x32, 0x45, 0xcf, - 0x1c, 0x22, 0x44, 0xa9, 0x2e, 0x48, 0x51, 0x01, 0x54, 0x73, 0x61, 0x3f, 0xbf, - 0x38, 0xd2, 0x42, 0xd7, 0x54, 0xf6, - ], - }, - TestVector { - c: [ - 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, - ], - pk: [ - 0x03, 0xec, 0x05, 0xbb, 0x7f, 0x06, 0x5e, 0x25, 0x6f, 0xf4, 0x54, 0xf8, 0xa8, - 0xdf, 0x6f, 0x2f, 0x9b, 0x8a, 0x8c, 0x95, 0x08, 0xca, 0xac, 0xfe, 0xe9, 0x52, - 0x1c, 0xbe, 0x68, 0x9d, 0xd1, 0x12, 0x0f, - ], - external_ovk: [ - 0xdb, 0x97, 0x52, 0x0e, 0x2f, 0xe3, 0x68, 0xad, 0x50, 0x2d, 0xef, 0xf8, 0x42, - 0xf0, 0xc0, 0xee, 0x5d, 0x20, 0x3b, 0x48, 0x33, 0x7a, 0x0f, 0xff, 0x75, 0xbe, - 0x24, 0x52, 0x59, 0x77, 0xf3, 0x7e, - ], - internal_ovk: [ - 0xbc, 0x4a, 0xcb, 0x5f, 0x52, 0xb8, 0xae, 0x21, 0xe3, 0x32, 0xb1, 0x7c, 0x29, - 0x63, 0x1f, 0x68, 0xe9, 0x68, 0x2a, 0x46, 0xc4, 0xa7, 0xab, 0xc8, 0xed, 0xf9, - 0x0d, 0x37, 0xae, 0xea, 0xd3, 0x6c, - ], - }, - TestVector { - c: [ - 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, - ], - pk: [ - 0x02, 0x81, 0x8f, 0x50, 0xce, 0x47, 0x10, 0xf4, 0xeb, 0x11, 0xe7, 0x43, 0xe6, - 0x40, 0x85, 0x44, 0xaa, 0x3c, 0x12, 0x3c, 0x7f, 0x07, 0xe2, 0xaa, 0xbb, 0x91, - 0xaf, 0xc4, 0xec, 0x48, 0x78, 0x8d, 0xe9, - ], - external_ovk: [ - 0xb8, 0xa3, 0x6d, 0x62, 0xa6, 0x3f, 0x69, 0x36, 0x7b, 0xe3, 0xf4, 0xbe, 0xd4, - 0x20, 0x26, 0x4a, 0xdb, 0x63, 0x7b, 0xbb, 0x47, 0x0e, 0x1f, 0x56, 0xe0, 0x33, - 0x8b, 0x38, 0xe2, 0xa6, 0x90, 0x97, - ], - internal_ovk: [ - 0x4f, 0xf6, 0xfa, 0xf2, 0x06, 0x63, 0x1e, 0xcb, 0x01, 0xf9, 0x57, 0x30, 0xf7, - 0xe5, 0x5b, 0xfc, 0xff, 0x8b, 0x02, 0xa3, 0x14, 0x88, 0x5a, 0x6d, 0x24, 0x8e, - 0x6e, 0xbe, 0xb7, 0x4d, 0x3e, 0x50, - ], - }, - TestVector { - c: [ - 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, - ], - pk: [ - 0x02, 0xae, 0x36, 0xb6, 0x1a, 0x3d, 0x10, 0xf1, 0xaa, 0x75, 0x2a, 0xb1, 0xdc, - 0x16, 0xe3, 0xe4, 0x9b, 0x6a, 0xc0, 0xd2, 0xae, 0x19, 0x07, 0xd2, 0xe6, 0x94, - 0x25, 0xec, 0x12, 0xc9, 0x3a, 0xae, 0xbc, - ], - external_ovk: [ - 0xda, 0x6f, 0x47, 0x0f, 0x42, 0x5b, 0x3d, 0x27, 0xf4, 0x28, 0x6e, 0xf0, 0x3b, - 0x7e, 0x87, 0x01, 0x7c, 0x20, 0xa7, 0x10, 0xb3, 0xff, 0xb9, 0xc1, 0xb6, 0x6c, - 0x71, 0x60, 0x92, 0xe3, 0xd9, 0xbc, - ], - internal_ovk: [ - 0x09, 0xb5, 0x4f, 0x75, 0xcb, 0x70, 0x32, 0x67, 0x1d, 0xc6, 0x8a, 0xaa, 0x07, - 0x30, 0x5f, 0x38, 0xcd, 0xbc, 0x87, 0x9e, 0xe1, 0x5b, 0xec, 0x04, 0x71, 0x3c, - 0x24, 0xdc, 0xe3, 0xca, 0x70, 0x26, - ], - }, - TestVector { - c: [ - 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, - ], - pk: [ - 0x02, 0x49, 0x26, 0x53, 0x80, 0xd2, 0xb0, 0x2e, 0x0a, 0x1d, 0x98, 0x8f, 0x3d, - 0xe3, 0x45, 0x8b, 0x6e, 0x00, 0x29, 0x1d, 0xb0, 0xe6, 0x2e, 0x17, 0x47, 0x91, - 0xd0, 0x09, 0x29, 0x9f, 0x61, 0xfe, 0xc4, - ], - external_ovk: [ - 0x60, 0xa7, 0xa0, 0x8e, 0xef, 0xa2, 0x4e, 0x75, 0xcc, 0xbb, 0x29, 0xdc, 0x84, - 0x94, 0x67, 0x2d, 0x73, 0x0f, 0xb3, 0x88, 0x7c, 0xb2, 0x6e, 0xf5, 0x1c, 0x6a, - 0x1a, 0x78, 0xe8, 0x8a, 0x78, 0x39, - ], - internal_ovk: [ - 0x3b, 0xab, 0x40, 0x98, 0x08, 0x10, 0x8b, 0xa9, 0xe5, 0xa1, 0xbb, 0x6a, 0x42, - 0x24, 0x59, 0x9d, 0x62, 0xcc, 0xee, 0x63, 0xff, 0x2f, 0x38, 0x15, 0x4c, 0x7f, - 0xb0, 0xc9, 0xa9, 0xa5, 0x79, 0x0f, - ], - }, - TestVector { - c: [ - 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, - ], - pk: [ - 0x03, 0x9a, 0x0e, 0x46, 0x39, 0xb4, 0x69, 0x1f, 0x02, 0x7c, 0x0d, 0xb7, 0xfe, - 0xf1, 0xbb, 0x5e, 0xf9, 0x0a, 0xcd, 0xb7, 0x08, 0x62, 0x6d, 0x2e, 0x1f, 0x3e, - 0x38, 0x3e, 0xe7, 0x5b, 0x31, 0xcf, 0x57, - ], - external_ovk: [ - 0xbb, 0x47, 0x87, 0x2c, 0x25, 0x09, 0xbf, 0x3c, 0x72, 0xde, 0xdf, 0x4f, 0xc1, - 0x77, 0x0f, 0x91, 0x93, 0xe2, 0xc1, 0x90, 0xd7, 0xaa, 0x8e, 0x9e, 0x88, 0x1a, - 0xd2, 0xf1, 0x73, 0x48, 0x4e, 0xf2, - ], - internal_ovk: [ - 0x5f, 0x36, 0xdf, 0xa3, 0x6c, 0xa7, 0x65, 0x74, 0x50, 0x29, 0x4e, 0xaa, 0xdd, - 0xad, 0x78, 0xaf, 0xf2, 0xb3, 0xdc, 0x38, 0x5a, 0x57, 0x73, 0x5a, 0xc0, 0x0d, - 0x3d, 0x9a, 0x29, 0x2b, 0x8c, 0x77, - ], - }, - TestVector { - c: [ - 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, - ], - pk: [ - 0x03, 0xbb, 0xf4, 0x49, 0x82, 0xf1, 0xba, 0x3a, 0x2b, 0x9d, 0xd3, 0xc1, 0x77, - 0x4d, 0x71, 0xce, 0x33, 0x60, 0x59, 0x9b, 0x07, 0xf2, 0x11, 0xc8, 0x16, 0xb8, - 0xc4, 0x3b, 0x98, 0x42, 0x23, 0x09, 0x24, - ], - external_ovk: [ - 0xed, 0xe8, 0xfb, 0x11, 0x37, 0x9b, 0x15, 0xae, 0xc4, 0xfa, 0x4e, 0xc5, 0x12, - 0x4c, 0x95, 0x00, 0xad, 0xf4, 0x0e, 0xb6, 0xf7, 0xca, 0xa5, 0xe9, 0xce, 0x80, - 0xf6, 0xbd, 0x9e, 0x73, 0xd0, 0xe7, - ], - internal_ovk: [ - 0x25, 0x0b, 0x4d, 0xfc, 0x34, 0xdd, 0x57, 0x76, 0x74, 0x51, 0x57, 0xf3, 0x82, - 0xce, 0x6d, 0xe4, 0xf6, 0xfe, 0x22, 0xd7, 0x98, 0x02, 0xf3, 0x9f, 0xe1, 0x34, - 0x77, 0x8b, 0x79, 0x40, 0x42, 0xd3, - ], - }, - TestVector { - c: [ - 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, - ], - pk: [ - 0x03, 0xff, 0x63, 0xc7, 0x89, 0x25, 0x1c, 0x10, 0x43, 0xc6, 0xf9, 0x6c, 0x66, - 0xbf, 0x5b, 0x0f, 0x61, 0xc9, 0xd6, 0x5f, 0xef, 0x5a, 0xaf, 0x42, 0x84, 0xa6, - 0xa5, 0x69, 0x94, 0x94, 0x1c, 0x05, 0xfa, - ], - external_ovk: [ - 0xb3, 0x11, 0x52, 0x06, 0x42, 0x71, 0x01, 0x01, 0xbb, 0xc8, 0x1b, 0xbe, 0x92, - 0x85, 0x1f, 0x9e, 0x65, 0x36, 0x22, 0x3e, 0xd6, 0xe6, 0xa1, 0x28, 0x59, 0x06, - 0x62, 0x1e, 0xfa, 0xe6, 0x41, 0x10, - ], - internal_ovk: [ - 0xf4, 0x46, 0xc0, 0xc1, 0x74, 0x1c, 0x94, 0x42, 0x56, 0x8e, 0x12, 0xf0, 0x55, - 0xef, 0xd5, 0x0c, 0x1e, 0xfe, 0x4d, 0x71, 0x53, 0x3d, 0x97, 0x6b, 0x08, 0xe9, - 0x94, 0x41, 0x44, 0x49, 0xc4, 0xac, - ], - }, - TestVector { - c: [ - 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, - ], - pk: [ - 0x02, 0xbf, 0x39, 0x20, 0xce, 0x2e, 0x9e, 0x95, 0xb0, 0xee, 0xce, 0x13, 0x0a, - 0x50, 0xba, 0x7d, 0xcc, 0x6f, 0x26, 0x51, 0x2a, 0x9f, 0xc7, 0xb8, 0x04, 0xaf, - 0xf0, 0x89, 0xf5, 0x0c, 0xbc, 0xff, 0xf7, - ], - external_ovk: [ - 0xae, 0x63, 0x84, 0xf8, 0x07, 0x72, 0x1c, 0x5f, 0x46, 0xc8, 0xaa, 0x83, 0x3b, - 0x66, 0x9b, 0x01, 0xc4, 0x22, 0x7c, 0x00, 0x18, 0xcb, 0x27, 0x29, 0xa9, 0x79, - 0x91, 0x01, 0xea, 0xb8, 0x5a, 0xb9, - ], - internal_ovk: [ - 0xef, 0x70, 0x8e, 0xb8, 0x26, 0xd8, 0xbf, 0xcd, 0x7f, 0xaa, 0x4f, 0x90, 0xdf, - 0x46, 0x1d, 0xed, 0x08, 0xd1, 0x6e, 0x19, 0x1b, 0x4e, 0x51, 0xb8, 0xa3, 0xa9, - 0x1c, 0x02, 0x0b, 0x32, 0xcc, 0x07, - ], - }, - ]; - - for tv in test_vectors { - let mut key_bytes = [0u8; 65]; - key_bytes[..32].copy_from_slice(&tv.c); - key_bytes[32..].copy_from_slice(&tv.pk); - let account_key = AccountPubKey::deserialize(&key_bytes).unwrap(); - - let (internal, external) = account_key.ovks_for_shielding(); - - assert_eq!(tv.internal_ovk, internal.as_bytes()); - assert_eq!(tv.external_ovk, external.as_bytes()); - } - } -} diff --git a/zcash_primitives/src/lib.rs b/zcash_primitives/src/lib.rs index 87ea2baaf0..7aac5940ae 100644 --- a/zcash_primitives/src/lib.rs +++ b/zcash_primitives/src/lib.rs @@ -2,27 +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(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 keys; -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 transaction; -pub mod zip32; -pub mod zip339; - -#[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 176d3b4375..2a57409c6d 100644 --- a/zcash_primitives/src/merkle_tree.rs +++ b/zcash_primitives/src/merkle_tree.rs @@ -1,17 +1,17 @@ -//! 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, WriteBytesExt}; +use alloc::vec::Vec; +use core2::io::{self, Read, Write}; + +use crate::encoding::{ReadBytesExt, WriteBytesExt}; use incrementalmerkletree::{ frontier::{CommitmentTree, Frontier, NonEmptyFrontier}, witness::IncrementalWitness, Address, Hashable, Level, MerklePath, Position, }; use orchard::tree::MerkleHashOrchard; -use std::io::{self, Read, Write}; use zcash_encoding::{Optional, Vector}; -use crate::sapling; - /// A hashable node within a Merkle tree. pub trait HashSer { /// Parses a node from the given byte source. @@ -23,6 +23,23 @@ pub trait HashSer { fn write(&self, writer: W) -> io::Result<()>; } +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 write(&self, mut writer: W) -> io::Result<()> { + writer.write_all(&self.to_bytes()) + } +} + impl HashSer for MerkleHashOrchard { fn read(mut reader: R) -> io::Result where @@ -47,41 +64,44 @@ impl HashSer for MerkleHashOrchard { /// is platform-dependent, we consistently represent it as u64 in serialized /// formats. pub fn write_usize_leu64(mut writer: W, value: usize) -> io::Result<()> { - // Panic if we get a usize value that can't fit into a u64. - writer.write_u64::(value.try_into().unwrap()) + 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", + ) + })?) } /// 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 { - reader.read_u64::()?.try_into().map_err(|e| { + 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, - format!( - "usize could not be decoded from a 64-bit value on this platform: {:?}", - e - ), + "usize could not be decoded from a 64-bit value", ) }) } pub fn write_position(mut writer: W, position: Position) -> io::Result<()> { - writer.write_u64::(position.into()) + writer.write_u64_le(u64::from(position)) } pub fn read_position(mut reader: R) -> io::Result { - reader.read_u64::().map(Position::from) + reader.read_u64_le().map(Position::from) } pub fn write_address(mut writer: W, addr: Address) -> io::Result<()> { writer.write_u8(addr.level().into())?; - writer.write_u64::(addr.index()) + writer.write_u64_le(addr.index()) } pub fn read_address(mut reader: R) -> io::Result

{ let level = reader.read_u8().map(Level::from)?; - let index = reader.read_u64::()?; + let index = reader.read_u64_le()?; Ok(Address::from_parts(level, index)) } @@ -98,12 +118,12 @@ pub fn write_nonempty_frontier_v1( frontier: &NonEmptyFrontier, ) -> io::Result<()> { write_position(&mut writer, frontier.position())?; - if frontier.position().is_odd() { + 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() - .get(0) + .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))?; @@ -117,7 +137,6 @@ pub fn write_nonempty_frontier_v1( Ok(()) } -#[allow(clippy::redundant_closure)] pub fn read_nonempty_frontier_v1( mut reader: R, ) -> io::Result> { @@ -134,10 +153,10 @@ pub fn read_nonempty_frontier_v1( left }; - NonEmptyFrontier::from_parts(position, leaf, ommers).map_err(|err| { + NonEmptyFrontier::from_parts(position, leaf, ommers).map_err(|_err| { io::Error::new( io::ErrorKind::InvalidData, - format!("Parsing resulted in an invalid Merkle frontier: {:?}", err), + "Parsing resulted in an invalid Merkle frontier", ) }) } @@ -149,14 +168,13 @@ pub fn write_frontier_v1( Optional::write(writer, frontier.value(), write_nonempty_frontier_v1) } -#[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| { + Some(f) => Frontier::try_from(f).map_err(|_err| { io::Error::new( io::ErrorKind::InvalidData, - format!("Parsing resulted in an invalid Merkle frontier: {:?}", err), + "Parsing resulted in an invalid Merkle frontier", ) }), } @@ -191,7 +209,6 @@ pub fn write_commitment_tree( } /// Reads an `IncrementalWitness` from its serialized form. -#[allow(clippy::redundant_closure)] pub fn read_incremental_witness( mut reader: R, ) -> io::Result> { @@ -199,7 +216,10 @@ pub fn read_incremental_witness( let filled = Vector::read(&mut reader, |r| Node::read(r))?; let cursor = Optional::read(&mut reader, read_commitment_tree)?; - Ok(IncrementalWitness::from_parts(tree, filled, cursor)) + IncrementalWitness::from_parts(tree, filled, cursor).ok_or(io::Error::new( + io::ErrorKind::InvalidData, + "invalid witness: inconsistency detected between witness parts", + )) } /// Serializes an `IncrementalWitness` as an array of bytes. @@ -251,32 +271,21 @@ pub fn merkle_path_from_slice( if auth_path.len() != usize::from(DEPTH) { return Err(io::Error::new( io::ErrorKind::InvalidData, - format!("length of auth path is not the expected {} elements", DEPTH), + "auth path has unexpected length", )); } // Read the position from the witness - let position = witness.read_u64::().and_then(|p| { - Position::try_from(p).map_err(|_| { - io::Error::new( - io::ErrorKind::InvalidData, - format!("decoded position {} exceeded the range of a `usize`", p), - ) - }) - })?; + 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() { - let path_len = auth_path.len(); MerklePath::from_parts(auth_path, position).map_err(|_| { io::Error::new( io::ErrorKind::InvalidData, - format!( - "auth path expected to contain {} elements, got {}", - DEPTH, path_len - ), + "auth path contained incorrect number of elements", ) }) } else { @@ -289,25 +298,43 @@ pub fn merkle_path_from_slice( #[cfg(any(test, feature = "test-dependencies"))] pub mod testing { - use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; + use crate::encoding::{ReadBytesExt, WriteBytesExt}; + use alloc::string::String; + use core2::io::{self, Read, Write}; use incrementalmerkletree::frontier::testing::TestNode; - use std::io::{self, Read, Write}; + use zcash_encoding::Vector; use super::HashSer; impl HashSer for TestNode { fn read(mut reader: R) -> io::Result { - reader.read_u64::().map(TestNode) + reader.read_u64_le().map(TestNode) } fn write(&self, mut writer: W) -> io::Result<()> { - writer.write_u64::(self.0) + writer.write_u64_le(self.0) + } + } + + 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") + }) + }) + } + + fn write(&self, writer: W) -> io::Result<()> { + Vector::write(writer, self.as_bytes(), |w, b| w.write_all(&[*b])) } } } #[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}, @@ -322,7 +349,7 @@ mod tests { read_incremental_witness, write_commitment_tree, write_frontier_v1, write_incremental_witness, CommitmentTree, HashSer, }; - use crate::sapling::{self, Node}; + use ::sapling::{self, Node}; proptest! { #[test] @@ -797,10 +824,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((IncrementalWitness::from_tree(tree.clone()), 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()); diff --git a/zcash_primitives/src/sapling.rs b/zcash_primitives/src/sapling.rs deleted file mode 100644 index 31899c72be..0000000000 --- a/zcash_primitives/src/sapling.rs +++ /dev/null @@ -1,67 +0,0 @@ -//! Structs and constants specific to the Sapling shielded pool. - -mod address; -pub mod group_hash; -pub mod keys; -pub mod note; -pub mod note_encryption; -pub mod pedersen_hash; -pub mod prover; -pub mod redjubjub; -mod spec; -mod tree; -pub mod util; -pub mod value; - -use group::GroupEncoding; -use rand_core::{CryptoRng, RngCore}; - -use crate::constants::SPENDING_KEY_GENERATOR; - -use self::redjubjub::{PrivateKey, PublicKey, Signature}; - -pub use address::PaymentAddress; -pub use keys::{Diversifier, NullifierDerivingKey, ProofGenerationKey, SaplingIvk, ViewingKey}; -pub use note::{nullifier::Nullifier, Note, Rseed}; -pub use tree::{ - merkle_hash, CommitmentTree, IncrementalWitness, MerklePath, Node, NOTE_COMMITMENT_TREE_DEPTH, -}; - -/// 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()); - data_to_be_signed[32..64].copy_from_slice(&sighash[..]); - - // Do the signing - rsk.sign(&data_to_be_signed, rng, SPENDING_KEY_GENERATOR) -} - -#[cfg(any(test, feature = "test-dependencies"))] -pub mod testing { - pub use super::{ - address::testing::arb_payment_address, keys::testing::arb_incoming_viewing_key, - note::testing::arb_note, tree::testing::arb_node, - }; -} diff --git a/zcash_primitives/src/sapling/address.rs b/zcash_primitives/src/sapling/address.rs deleted file mode 100644 index 4ee547e53e..0000000000 --- a/zcash_primitives/src/sapling/address.rs +++ /dev/null @@ -1,118 +0,0 @@ -use super::{ - keys::{DiversifiedTransmissionKey, Diversifier}, - note::{Note, Rseed}, - value::NoteValue, -}; - -/// A Sapling payment address. -/// -/// # Invariants -/// -/// - `diversifier` is guaranteed to be valid for Sapling (only 50% of diversifiers are). -/// - `pk_d` is guaranteed to be prime-order (i.e. in the prime-order subgroup of Jubjub, -/// and not the identity). -#[derive(Clone, Copy, Debug)] -pub struct PaymentAddress { - pk_d: DiversifiedTransmissionKey, - diversifier: Diversifier, -} - -impl PartialEq for PaymentAddress { - fn eq(&self, other: &Self) -> bool { - self.pk_d == other.pk_d && self.diversifier == other.diversifier - } -} - -impl Eq for PaymentAddress {} - -impl PaymentAddress { - /// Constructs a PaymentAddress from a diversifier and a Jubjub point. - /// - /// Returns None if `diversifier` is not valid for Sapling, or `pk_d` is the identity. - /// Note that we cannot verify in this constructor that `pk_d` is derived from - /// `diversifier`, so addresses for which these values have no known relationship - /// (and therefore no-one can receive funds at them) can still be constructed. - pub fn from_parts(diversifier: Diversifier, pk_d: DiversifiedTransmissionKey) -> Option { - // Check that the diversifier is valid - diversifier.g_d()?; - - Self::from_parts_unchecked(diversifier, pk_d) - } - - /// Constructs a PaymentAddress from a diversifier and a Jubjub point. - /// - /// Returns None if `pk_d` is the identity. The caller must check that `diversifier` - /// is valid for Sapling. - pub(crate) fn from_parts_unchecked( - diversifier: Diversifier, - pk_d: DiversifiedTransmissionKey, - ) -> Option { - if pk_d.is_identity() { - None - } else { - Some(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) - }; - - let pk_d = DiversifiedTransmissionKey::from_bytes(bytes[11..43].try_into().unwrap()); - if pk_d.is_some().into() { - // The remaining invariants are checked here. - 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) -> &DiversifiedTransmissionKey { - &self.pk_d - } - - pub(crate) fn g_d(&self) -> jubjub::SubgroupPoint { - self.diversifier.g_d().expect("checked at construction") - } - - pub fn create_note(&self, value: u64, rseed: Rseed) -> Note { - Note::from_parts(*self, NoteValue::from_raw(value), rseed) - } -} - -#[cfg(any(test, feature = "test-dependencies"))] -pub(super) mod testing { - use proptest::prelude::*; - - use super::{ - super::keys::{testing::arb_incoming_viewing_key, Diversifier, SaplingIvk}, - PaymentAddress, - }; - - pub fn arb_payment_address() -> impl Strategy { - arb_incoming_viewing_key().prop_flat_map(|ivk: SaplingIvk| { - any::<[u8; 11]>().prop_filter_map( - "Sampled diversifier must generate a valid Sapling payment address.", - move |d| ivk.to_payment_address(Diversifier(d)), - ) - }) - } -} 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 ad42694927..0000000000 --- a/zcash_primitives/src/sapling/keys.rs +++ /dev/null @@ -1,536 +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 std::io::{self, Read, Write}; - -use super::{ - address::PaymentAddress, - note_encryption::KDF_SAPLING_PERSONALIZATION, - spec::{ - crh_ivk, diversify_hash, ka_sapling_agree, ka_sapling_agree_prepared, - ka_sapling_derive_public, ka_sapling_derive_public_subgroup_prepared, PreparedBase, - PreparedBaseSubgroup, PreparedScalar, - }, -}; -use crate::{ - constants::{self, PROOF_GENERATION_KEY_GENERATOR, SPENDING_KEY_GENERATOR}, - keys::prf_expand, -}; - -use blake2b_simd::{Hash as Blake2bHash, Params as Blake2bParams}; -use ff::PrimeField; -use group::{Curve, Group, GroupEncoding}; -use subtle::{Choice, ConditionallySelectable, ConstantTimeEq, CtOption}; -use zcash_note_encryption::EphemeralKeyBytes; - -/// Errors that can occur in the decoding of Sapling spending keys. -pub enum DecodingError { - /// The length of the byte slice provided for decoding was incorrect. - LengthInvalid { expected: usize, actual: usize }, - /// Could not decode the `ask` bytes to a jubjub field element. - InvalidAsk, - /// Could not decode the `nsk` bytes to a jubjub field element. - InvalidNsk, -} - -/// An outgoing viewing key -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -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, -} - -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, - } - } - - /// Decodes the expanded spending key from its serialized representation - /// as part of the encoding of the extended spending key as defined in - /// [ZIP 32](https://zips.z.cash/zip-0032) - pub fn from_bytes(b: &[u8]) -> Result { - if b.len() != 96 { - return Err(DecodingError::LengthInvalid { - expected: 96, - actual: b.len(), - }); - } - - let ask = Option::from(jubjub::Fr::from_repr(b[0..32].try_into().unwrap())) - .ok_or(DecodingError::InvalidAsk)?; - let nsk = Option::from(jubjub::Fr::from_repr(b[32..64].try_into().unwrap())) - .ok_or(DecodingError::InvalidNsk)?; - let ovk = OutgoingViewingKey(b[64..96].try_into().unwrap()); - - Ok(ExpandedSpendingKey { ask, nsk, ovk }) - } - - pub fn read(mut reader: R) -> io::Result { - let mut repr = [0u8; 96]; - reader.read_exact(repr.as_mut())?; - Self::from_bytes(&repr).map_err(|e| match e { - DecodingError::InvalidAsk => { - io::Error::new(io::ErrorKind::InvalidData, "ask not in field") - } - DecodingError::InvalidNsk => { - io::Error::new(io::ErrorKind::InvalidData, "nsk not in field") - } - DecodingError::LengthInvalid { .. } => unreachable!(), - }) - } - - pub fn write(&self, mut writer: W) -> io::Result<()> { - writer.write_all(&self.to_bytes()) - } - - /// Encodes the expanded spending key to the its seralized representation - /// as part of the encoding of the extended spending key as defined in - /// [ZIP 32](https://zips.z.cash/zip-0032) - pub fn to_bytes(&self) -> [u8; 96] { - let mut result = [0u8; 96]; - result[0..32].copy_from_slice(&self.ask.to_repr()); - result[32..64].copy_from_slice(&self.nsk.to_repr()); - result[64..96].copy_from_slice(&self.ovk.0); - result - } -} - -#[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: NullifierDerivingKey(constants::PROOF_GENERATION_KEY_GENERATOR * self.nsk), - } - } -} - -/// A key used to derive the nullifier for a Sapling note. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct NullifierDerivingKey(pub jubjub::SubgroupPoint); - -#[derive(Debug, Clone)] -pub struct ViewingKey { - pub ak: jubjub::SubgroupPoint, - pub nk: NullifierDerivingKey, -} - -impl ViewingKey { - pub fn rk(&self, ar: jubjub::Fr) -> jubjub::SubgroupPoint { - self.ak + constants::SPENDING_KEY_GENERATOR * ar - } - - pub fn ivk(&self) -> SaplingIvk { - SaplingIvk(crh_ivk(self.ak.to_bytes(), self.nk.0.to_bytes())) - } - - pub fn to_payment_address(&self, diversifier: Diversifier) -> Option { - self.ivk().to_payment_address(diversifier) - } -} - -/// A Sapling key that provides the capability to view incoming and outgoing transactions. -#[derive(Debug)] -pub struct FullViewingKey { - pub vk: ViewingKey, - pub ovk: OutgoingViewingKey, -} - -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: NullifierDerivingKey(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 = NullifierDerivingKey(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.0.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 - } -} - -#[derive(Debug, Clone)] -pub struct SaplingIvk(pub jubjub::Fr); - -impl SaplingIvk { - pub fn to_payment_address(&self, diversifier: Diversifier) -> Option { - let prepared_ivk = PreparedIncomingViewingKey::new(self); - DiversifiedTransmissionKey::derive(&prepared_ivk, &diversifier) - .and_then(|pk_d| PaymentAddress::from_parts(diversifier, pk_d)) - } - - pub fn to_repr(&self) -> [u8; 32] { - self.0.to_repr() - } -} - -/// A Sapling incoming viewing key that has been precomputed for trial decryption. -#[derive(Clone, Debug)] -pub struct PreparedIncomingViewingKey(PreparedScalar); - -impl memuse::DynamicUsage for PreparedIncomingViewingKey { - fn dynamic_usage(&self) -> usize { - self.0.dynamic_usage() - } - - fn dynamic_usage_bounds(&self) -> (usize, Option) { - self.0.dynamic_usage_bounds() - } -} - -impl PreparedIncomingViewingKey { - /// Performs the necessary precomputations to use a `SaplingIvk` for note decryption. - pub fn new(ivk: &SaplingIvk) -> Self { - Self(PreparedScalar::new(&ivk.0)) - } -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct Diversifier(pub [u8; 11]); - -impl Diversifier { - pub fn g_d(&self) -> Option { - diversify_hash(&self.0) - } -} - -/// The diversified transmission key for a given payment address. -/// -/// Defined in [Zcash Protocol Spec § 4.2.2: Sapling Key Components][saplingkeycomponents]. -/// -/// Note that this type is allowed to be the identity in the protocol, but we reject this -/// in [`PaymentAddress::from_parts`]. -/// -/// [saplingkeycomponents]: https://zips.z.cash/protocol/protocol.pdf#saplingkeycomponents -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub struct DiversifiedTransmissionKey(jubjub::SubgroupPoint); - -impl DiversifiedTransmissionKey { - /// Defined in [Zcash Protocol Spec § 4.2.2: Sapling Key Components][saplingkeycomponents]. - /// - /// Returns `None` if `d` is an invalid diversifier. - /// - /// [saplingkeycomponents]: https://zips.z.cash/protocol/protocol.pdf#saplingkeycomponents - pub(crate) fn derive(ivk: &PreparedIncomingViewingKey, d: &Diversifier) -> Option { - d.g_d() - .map(PreparedBaseSubgroup::new) - .map(|g_d| ka_sapling_derive_public_subgroup_prepared(&ivk.0, &g_d)) - .map(DiversifiedTransmissionKey) - } - - /// $abst_J(bytes)$ - pub(crate) fn from_bytes(bytes: &[u8; 32]) -> CtOption { - jubjub::SubgroupPoint::from_bytes(bytes).map(DiversifiedTransmissionKey) - } - - /// $repr_J(self)$ - pub(crate) fn to_bytes(self) -> [u8; 32] { - self.0.to_bytes() - } - - /// Returns true if this is the identity. - pub(crate) fn is_identity(&self) -> bool { - self.0.is_identity().into() - } - - /// Exposes the inner Jubjub point. - /// - /// This API is exposed for `zcash_proof` usage, and will be removed when this type is - /// refactored into the `sapling-crypto` crate. - pub fn inner(&self) -> jubjub::SubgroupPoint { - self.0 - } -} - -impl ConditionallySelectable for DiversifiedTransmissionKey { - fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { - DiversifiedTransmissionKey(jubjub::SubgroupPoint::conditional_select( - &a.0, &b.0, choice, - )) - } -} - -/// An ephemeral secret key used to encrypt an output note on-chain. -/// -/// `esk` is "ephemeral" in the sense that each secret key is only used once. In -/// practice, `esk` is derived deterministically from the note that it is encrypting. -/// -/// $\mathsf{KA}^\mathsf{Sapling}.\mathsf{Private} := \mathbb{F}_{r_J}$ -/// -/// Defined in [section 5.4.5.3: Sapling Key Agreement][concretesaplingkeyagreement]. -/// -/// [concretesaplingkeyagreement]: https://zips.z.cash/protocol/protocol.pdf#concretesaplingkeyagreement -#[derive(Debug)] -pub struct EphemeralSecretKey(pub(crate) jubjub::Scalar); - -impl ConstantTimeEq for EphemeralSecretKey { - fn ct_eq(&self, other: &Self) -> subtle::Choice { - self.0.ct_eq(&other.0) - } -} - -impl EphemeralSecretKey { - pub(crate) fn from_bytes(bytes: &[u8; 32]) -> CtOption { - jubjub::Scalar::from_bytes(bytes).map(EphemeralSecretKey) - } - - pub(crate) fn derive_public(&self, g_d: jubjub::ExtendedPoint) -> EphemeralPublicKey { - EphemeralPublicKey(ka_sapling_derive_public(&self.0, &g_d)) - } - - pub(crate) fn agree(&self, pk_d: &DiversifiedTransmissionKey) -> SharedSecret { - SharedSecret(ka_sapling_agree(&self.0, &pk_d.0.into())) - } -} - -/// An ephemeral public key used to encrypt an output note on-chain. -/// -/// `epk` is "ephemeral" in the sense that each public key is only used once. In practice, -/// `epk` is derived deterministically from the note that it is encrypting. -/// -/// $\mathsf{KA}^\mathsf{Sapling}.\mathsf{Public} := \mathbb{J}$ -/// -/// Defined in [section 5.4.5.3: Sapling Key Agreement][concretesaplingkeyagreement]. -/// -/// [concretesaplingkeyagreement]: https://zips.z.cash/protocol/protocol.pdf#concretesaplingkeyagreement -#[derive(Debug)] -pub struct EphemeralPublicKey(jubjub::ExtendedPoint); - -impl EphemeralPublicKey { - pub(crate) fn from_affine(epk: jubjub::AffinePoint) -> Self { - EphemeralPublicKey(epk.into()) - } - - pub(crate) fn from_bytes(bytes: &[u8; 32]) -> CtOption { - jubjub::ExtendedPoint::from_bytes(bytes).map(EphemeralPublicKey) - } - - pub(crate) fn to_bytes(&self) -> EphemeralKeyBytes { - EphemeralKeyBytes(self.0.to_bytes()) - } -} - -/// A Sapling ephemeral public key that has been precomputed for trial decryption. -#[derive(Clone, Debug)] -pub struct PreparedEphemeralPublicKey(PreparedBase); - -impl PreparedEphemeralPublicKey { - pub(crate) fn new(epk: EphemeralPublicKey) -> Self { - PreparedEphemeralPublicKey(PreparedBase::new(epk.0)) - } - - pub(crate) fn agree(&self, ivk: &PreparedIncomingViewingKey) -> SharedSecret { - SharedSecret(ka_sapling_agree_prepared(&ivk.0, &self.0)) - } -} - -/// $\mathsf{KA}^\mathsf{Sapling}.\mathsf{SharedSecret} := \mathbb{J}^{(r)}$ -/// -/// Defined in [section 5.4.5.3: Sapling Key Agreement][concretesaplingkeyagreement]. -/// -/// [concretesaplingkeyagreement]: https://zips.z.cash/protocol/protocol.pdf#concretesaplingkeyagreement -#[derive(Debug)] -pub struct SharedSecret(jubjub::SubgroupPoint); - -impl SharedSecret { - /// For checking test vectors only. - #[cfg(test)] - pub(crate) fn to_bytes(&self) -> [u8; 32] { - self.0.to_bytes() - } - - /// Only for use in batched note encryption. - pub(crate) fn batch_to_affine( - shared_secrets: Vec>, - ) -> impl Iterator> { - // Filter out the positions for which ephemeral_key was not a valid encoding. - let secrets: Vec<_> = shared_secrets - .iter() - .filter_map(|s| s.as_ref().map(|s| jubjub::ExtendedPoint::from(s.0))) - .collect(); - - // Batch-normalize the shared secrets. - let mut secrets_affine = vec![jubjub::AffinePoint::identity(); secrets.len()]; - group::Curve::batch_normalize(&secrets, &mut secrets_affine); - - // Re-insert the invalid ephemeral_key positions. - let mut secrets_affine = secrets_affine.into_iter(); - shared_secrets - .into_iter() - .map(move |s| s.and_then(|_| secrets_affine.next())) - } - - /// Defined in [Zcash Protocol Spec § 5.4.5.4: Sapling Key Agreement][concretesaplingkdf]. - /// - /// [concretesaplingkdf]: https://zips.z.cash/protocol/protocol.pdf#concretesaplingkdf - pub(crate) fn kdf_sapling(self, ephemeral_key: &EphemeralKeyBytes) -> Blake2bHash { - Self::kdf_sapling_inner( - jubjub::ExtendedPoint::from(self.0).to_affine(), - ephemeral_key, - ) - } - - /// Only for direct use in batched note encryption. - pub(crate) fn kdf_sapling_inner( - secret: jubjub::AffinePoint, - ephemeral_key: &EphemeralKeyBytes, - ) -> Blake2bHash { - Blake2bParams::new() - .hash_length(32) - .personal(KDF_SAPLING_PERSONALIZATION) - .to_state() - .update(&secret.to_bytes()) - .update(ephemeral_key.as_ref()) - .finalize() - } -} - -#[cfg(any(test, feature = "test-dependencies"))] -pub mod testing { - use proptest::collection::vec; - use proptest::prelude::*; - use std::fmt::{self, Debug, Formatter}; - - use super::{ExpandedSpendingKey, FullViewingKey, SaplingIvk}; - - impl Debug for ExpandedSpendingKey { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "Spending keys cannot be Debug-formatted.") - } - } - - prop_compose! { - pub fn arb_expanded_spending_key()(v in vec(any::(), 32..252)) -> ExpandedSpendingKey { - ExpandedSpendingKey::from_spending_key(&v) - } - } - - prop_compose! { - pub fn arb_full_viewing_key()(sk in arb_expanded_spending_key()) -> FullViewingKey { - FullViewingKey::from_expanded_spending_key(&sk) - } - } - - prop_compose! { - pub fn arb_incoming_viewing_key()(fvk in arb_full_viewing_key()) -> SaplingIvk { - fvk.vk.ivk() - } - } -} - -#[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.rs b/zcash_primitives/src/sapling/note.rs deleted file mode 100644 index 2e5b21bc6b..0000000000 --- a/zcash_primitives/src/sapling/note.rs +++ /dev/null @@ -1,174 +0,0 @@ -use group::{ff::Field, GroupEncoding}; -use rand_core::{CryptoRng, RngCore}; - -use super::{ - keys::EphemeralSecretKey, value::NoteValue, Nullifier, NullifierDerivingKey, PaymentAddress, -}; -use crate::keys::prf_expand; - -mod commitment; -pub use self::commitment::{ExtractedNoteCommitment, NoteCommitment}; - -pub(super) mod nullifier; - -/// 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]), -} - -impl Rseed { - /// Defined in [Zcash Protocol Spec § 4.7.2: Sending Notes (Sapling)][saplingsend]. - /// - /// [saplingsend]: https://zips.z.cash/protocol/protocol.pdf#saplingsend - pub(crate) fn rcm(&self) -> commitment::NoteCommitTrapdoor { - commitment::NoteCommitTrapdoor(match self { - Rseed::BeforeZip212(rcm) => *rcm, - Rseed::AfterZip212(rseed) => { - jubjub::Fr::from_bytes_wide(prf_expand(rseed, &[0x04]).as_array()) - } - }) - } -} - -/// A discrete amount of funds received by an address. -#[derive(Clone, Debug)] -pub struct Note { - /// The recipient of the funds. - recipient: PaymentAddress, - /// The value of this note. - value: NoteValue, - /// The seed randomness for various note components. - rseed: Rseed, -} - -impl PartialEq for Note { - fn eq(&self, other: &Self) -> bool { - // Notes are canonically defined by their commitments. - self.cmu().eq(&other.cmu()) - } -} - -impl Eq for Note {} - -impl Note { - /// Creates a note from its component parts. - /// - /// # Caveats - /// - /// This low-level constructor enforces that the provided arguments produce an - /// internally valid `Note`. However, it allows notes to be constructed in a way that - /// violates required security checks for note decryption, as specified in - /// [Section 4.19] of the Zcash Protocol Specification. Users of this constructor - /// should only call it with note components that have been fully validated by - /// decrypting a received note according to [Section 4.19]. - /// - /// [Section 4.19]: https://zips.z.cash/protocol/protocol.pdf#saplingandorchardinband - pub fn from_parts(recipient: PaymentAddress, value: NoteValue, rseed: Rseed) -> Self { - Note { - recipient, - value, - rseed, - } - } - - /// Returns the recipient of this note. - pub fn recipient(&self) -> PaymentAddress { - self.recipient - } - - /// Returns the value of this note. - pub fn value(&self) -> NoteValue { - self.value - } - - /// Returns the rseed value of this note. - pub fn rseed(&self) -> &Rseed { - &self.rseed - } - - /// Computes the note commitment, returning the full point. - fn cm_full_point(&self) -> NoteCommitment { - NoteCommitment::derive( - self.recipient.g_d().to_bytes(), - self.recipient.pk_d().to_bytes(), - self.value, - self.rseed.rcm(), - ) - } - - /// Computes the nullifier given the nullifier deriving key and - /// note position - pub fn nf(&self, nk: &NullifierDerivingKey, position: u64) -> Nullifier { - Nullifier::derive(nk, self.cm_full_point(), position) - } - - /// Computes the note commitment - pub fn cmu(&self) -> ExtractedNoteCommitment { - self.cm_full_point().into() - } - - /// Defined in [Zcash Protocol Spec § 4.7.2: Sending Notes (Sapling)][saplingsend]. - /// - /// [saplingsend]: https://zips.z.cash/protocol/protocol.pdf#saplingsend - pub fn rcm(&self) -> jubjub::Fr { - self.rseed.rcm().0 - } - - /// Derives `esk` from the internal `Rseed` value, or generates a random value if this - /// note was created with a v1 (i.e. pre-ZIP 212) note plaintext. - pub fn generate_or_derive_esk( - &self, - rng: &mut R, - ) -> EphemeralSecretKey { - self.generate_or_derive_esk_internal(rng) - } - - pub(crate) fn generate_or_derive_esk_internal( - &self, - rng: &mut R, - ) -> EphemeralSecretKey { - match self.derive_esk() { - None => EphemeralSecretKey(jubjub::Fr::random(rng)), - Some(esk) => esk, - } - } - - /// Returns the derived `esk` if this note was created after ZIP 212 activated. - pub(crate) fn derive_esk(&self) -> Option { - match self.rseed { - Rseed::BeforeZip212(_) => None, - Rseed::AfterZip212(rseed) => Some(EphemeralSecretKey(jubjub::Fr::from_bytes_wide( - prf_expand(&rseed, &[0x05]).as_array(), - ))), - } - } -} - -#[cfg(any(test, feature = "test-dependencies"))] -pub(super) mod testing { - use proptest::prelude::*; - - use super::{ - super::{testing::arb_payment_address, value::NoteValue}, - Note, Rseed, - }; - - prop_compose! { - pub fn arb_note(value: NoteValue)( - recipient in arb_payment_address(), - rseed in prop::array::uniform32(prop::num::u8::ANY).prop_map(Rseed::AfterZip212) - ) -> Note { - Note { - recipient, - value, - rseed - } - } - } -} diff --git a/zcash_primitives/src/sapling/note/commitment.rs b/zcash_primitives/src/sapling/note/commitment.rs deleted file mode 100644 index 613537ec97..0000000000 --- a/zcash_primitives/src/sapling/note/commitment.rs +++ /dev/null @@ -1,106 +0,0 @@ -use core::iter; - -use bitvec::{array::BitArray, order::Lsb0}; -use group::ff::PrimeField; -use subtle::{ConstantTimeEq, CtOption}; - -use crate::sapling::{ - pedersen_hash::Personalization, - spec::{extract_p, windowed_pedersen_commit}, - value::NoteValue, -}; - -/// The trapdoor for a Sapling note commitment. -#[derive(Clone, Debug)] -pub(crate) struct NoteCommitTrapdoor(pub(super) jubjub::Fr); - -/// A commitment to a note. -#[derive(Clone, Debug)] -pub struct NoteCommitment(jubjub::SubgroupPoint); - -impl NoteCommitment { - pub(crate) fn inner(&self) -> jubjub::SubgroupPoint { - self.0 - } -} - -impl NoteCommitment { - /// Derives a Sapling note commitment. - #[cfg(feature = "temporary-zcashd")] - pub fn temporary_zcashd_derive( - g_d: [u8; 32], - pk_d: [u8; 32], - v: NoteValue, - rcm: jubjub::Fr, - ) -> Self { - Self::derive(g_d, pk_d, v, NoteCommitTrapdoor(rcm)) - } - - /// $NoteCommit^Sapling$. - /// - /// Defined in [Zcash Protocol Spec § 5.4.8.2: Windowed Pedersen commitments][concretewindowedcommit]. - /// - /// [concretewindowedcommit]: https://zips.z.cash/protocol/protocol.pdf#concretewindowedcommit - pub(super) fn derive( - g_d: [u8; 32], - pk_d: [u8; 32], - v: NoteValue, - rcm: NoteCommitTrapdoor, - ) -> Self { - NoteCommitment(windowed_pedersen_commit( - Personalization::NoteCommitment, - iter::empty() - .chain(v.to_le_bits().iter().by_vals()) - .chain(BitArray::<_, Lsb0>::new(g_d).iter().by_vals()) - .chain(BitArray::<_, Lsb0>::new(pk_d).iter().by_vals()), - rcm.0, - )) - } -} - -/// The u-coordinate of the commitment to a note. -#[derive(Copy, Clone, Debug)] -pub struct ExtractedNoteCommitment(pub(super) bls12_381::Scalar); - -impl ExtractedNoteCommitment { - /// Deserialize the extracted note commitment from a byte array. - /// - /// This method enforces the [consensus rule][cmucanon] that the byte representation - /// of cmu MUST be canonical. - /// - /// [cmucanon]: https://zips.z.cash/protocol/protocol.pdf#outputencodingandconsensus - pub fn from_bytes(bytes: &[u8; 32]) -> CtOption { - bls12_381::Scalar::from_repr(*bytes).map(ExtractedNoteCommitment) - } - - /// Serialize the value commitment to its canonical byte representation. - pub fn to_bytes(self) -> [u8; 32] { - self.0.to_repr() - } -} - -impl From for ExtractedNoteCommitment { - fn from(cm: NoteCommitment) -> Self { - ExtractedNoteCommitment(extract_p(&cm.0)) - } -} - -impl From<&ExtractedNoteCommitment> for [u8; 32] { - fn from(cmu: &ExtractedNoteCommitment) -> Self { - cmu.to_bytes() - } -} - -impl ConstantTimeEq for ExtractedNoteCommitment { - fn ct_eq(&self, other: &Self) -> subtle::Choice { - self.0.ct_eq(&other.0) - } -} - -impl PartialEq for ExtractedNoteCommitment { - fn eq(&self, other: &Self) -> bool { - self.ct_eq(other).into() - } -} - -impl Eq for ExtractedNoteCommitment {} diff --git a/zcash_primitives/src/sapling/note/nullifier.rs b/zcash_primitives/src/sapling/note/nullifier.rs deleted file mode 100644 index 86f7bf946d..0000000000 --- a/zcash_primitives/src/sapling/note/nullifier.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::array::TryFromSliceError; - -use subtle::{Choice, ConstantTimeEq}; - -use super::NoteCommitment; -use crate::sapling::{ - keys::NullifierDerivingKey, - spec::{mixing_pedersen_hash, prf_nf}, -}; - -/// 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() - } - - /// $DeriveNullifier$. - /// - /// Defined in [Zcash Protocol Spec § 4.16: Note Commitments and Nullifiers][commitmentsandnullifiers]. - /// - /// [commitmentsandnullifiers]: https://zips.z.cash/protocol/protocol.pdf#commitmentsandnullifiers - pub(super) fn derive(nk: &NullifierDerivingKey, cm: NoteCommitment, position: u64) -> Self { - let rho = mixing_pedersen_hash(cm.inner(), position); - Nullifier(prf_nf(&nk.0, &rho)) - } -} - -impl AsRef<[u8]> for Nullifier { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} - -impl ConstantTimeEq for Nullifier { - fn ct_eq(&self, other: &Self) -> Choice { - self.0.ct_eq(&other.0) - } -} diff --git a/zcash_primitives/src/sapling/note_encryption.rs b/zcash_primitives/src/sapling/note_encryption.rs deleted file mode 100644 index a12dd4404f..0000000000 --- a/zcash_primitives/src/sapling/note_encryption.rs +++ /dev/null @@ -1,1550 +0,0 @@ -//! Implementation of in-band secret distribution for Zcash transactions. -//! -//! NB: the example code is only covering the post-Canopy case. - -use blake2b_simd::{Hash as Blake2bHash, Params as Blake2bParams}; -use byteorder::{LittleEndian, WriteBytesExt}; -use ff::PrimeField; -use memuse::DynamicUsage; -use rand_core::RngCore; - -use zcash_note_encryption::{ - try_compact_note_decryption, try_note_decryption, try_output_recovery_with_ock, - try_output_recovery_with_ovk, BatchDomain, Domain, EphemeralKeyBytes, NoteEncryption, - NotePlaintextBytes, OutPlaintextBytes, OutgoingCipherKey, ShieldedOutput, COMPACT_NOTE_SIZE, - ENC_CIPHERTEXT_SIZE, NOTE_PLAINTEXT_SIZE, OUT_PLAINTEXT_SIZE, -}; - -use crate::{ - consensus::{self, BlockHeight, NetworkUpgrade::Canopy, ZIP212_GRACE_PERIOD}, - memo::MemoBytes, - sapling::{ - keys::{ - DiversifiedTransmissionKey, EphemeralPublicKey, EphemeralSecretKey, OutgoingViewingKey, - SharedSecret, - }, - value::ValueCommitment, - Diversifier, Note, PaymentAddress, Rseed, - }, - transaction::components::{ - amount::Amount, - sapling::{self, OutputDescription}, - }, -}; - -use super::note::ExtractedNoteCommitment; - -pub use crate::sapling::keys::{PreparedEphemeralPublicKey, PreparedIncomingViewingKey}; - -pub const KDF_SAPLING_PERSONALIZATION: &[u8; 16] = b"Zcash_SaplingKDF"; -pub const PRF_OCK_PERSONALIZATION: &[u8; 16] = b"Zcash_Derive_ock"; - -/// Sapling PRF^ock. -/// -/// Implemented per section 5.4.2 of the Zcash Protocol Specification. -pub fn prf_ock( - ovk: &OutgoingViewingKey, - cv: &ValueCommitment, - cmu_bytes: &[u8; 32], - 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_bytes) - .update(ephemeral_key.as_ref()) - .finalize() - .as_bytes() - .try_into() - .unwrap(), - ) -} - -/// `get_pk_d` must check that the diversifier contained within the note plaintext is a -/// valid Sapling diversifier. -fn sapling_parse_note_plaintext_without_memo( - domain: &SaplingDomain

, - plaintext: &[u8], - get_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 = Option::from(jubjub::Fr::from_repr(r))?; - Rseed::BeforeZip212(rcm) - } else { - Rseed::AfterZip212(r) - }; - - let pk_d = get_pk_d(&diversifier)?; - - // `diversifier` was checked by `get_pk_d`. - let to = PaymentAddress::from_parts_unchecked(diversifier, pk_d)?; - let note = to.create_note(value.into(), rseed); - Some((note, to)) -} - -pub struct SaplingDomain { - params: P, - height: BlockHeight, -} - -impl DynamicUsage for SaplingDomain

{ - fn dynamic_usage(&self) -> usize { - self.params.dynamic_usage() + self.height.dynamic_usage() - } - - fn dynamic_usage_bounds(&self) -> (usize, Option) { - let (params_lower, params_upper) = self.params.dynamic_usage_bounds(); - let (height_lower, height_upper) = self.height.dynamic_usage_bounds(); - ( - params_lower + height_lower, - params_upper.zip(height_upper).map(|(a, b)| a + b), - ) - } -} - -impl SaplingDomain

{ - pub fn for_height(params: P, height: BlockHeight) -> Self { - Self { params, height } - } -} - -impl Domain for SaplingDomain

{ - type EphemeralSecretKey = EphemeralSecretKey; - // It is acceptable for this to be a point rather than a byte array, because we - // enforce by consensus that points must not be small-order, and all points with - // non-canonical serialization are small-order. - type EphemeralPublicKey = EphemeralPublicKey; - type PreparedEphemeralPublicKey = PreparedEphemeralPublicKey; - type SharedSecret = SharedSecret; - type SymmetricKey = Blake2bHash; - type Note = Note; - type Recipient = PaymentAddress; - type DiversifiedTransmissionKey = DiversifiedTransmissionKey; - type IncomingViewingKey = PreparedIncomingViewingKey; - type OutgoingViewingKey = OutgoingViewingKey; - type ValueCommitment = ValueCommitment; - type ExtractedCommitment = ExtractedNoteCommitment; - 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.recipient().pk_d() - } - - fn prepare_epk(epk: Self::EphemeralPublicKey) -> Self::PreparedEphemeralPublicKey { - PreparedEphemeralPublicKey::new(epk) - } - - fn ka_derive_public( - note: &Self::Note, - esk: &Self::EphemeralSecretKey, - ) -> Self::EphemeralPublicKey { - esk.derive_public(note.recipient().g_d().into()) - } - - fn ka_agree_enc( - esk: &Self::EphemeralSecretKey, - pk_d: &Self::DiversifiedTransmissionKey, - ) -> Self::SharedSecret { - esk.agree(pk_d) - } - - fn ka_agree_dec( - ivk: &Self::IncomingViewingKey, - epk: &Self::PreparedEphemeralPublicKey, - ) -> Self::SharedSecret { - epk.agree(ivk) - } - - /// Sapling KDF for note encryption. - /// - /// Implements section 5.4.4.4 of the Zcash Protocol Specification. - fn kdf(dhsecret: SharedSecret, epk: &EphemeralKeyBytes) -> Blake2bHash { - dhsecret.kdf_sapling(epk) - } - - fn note_plaintext_bytes(note: &Self::Note, 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(¬e.recipient().diversifier().0); - (&mut input[12..20]) - .write_u64::(note.value().inner()) - .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_bytes: &Self::ExtractedCommitmentBytes, - epk: &EphemeralKeyBytes, - ) -> OutgoingCipherKey { - prf_ock(ovk, cv, cmu_bytes, 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.recipient().pk_d().to_bytes()); - input[32..OUT_PLAINTEXT_SIZE].copy_from_slice(esk.0.to_repr().as_ref()); - - OutPlaintextBytes(input) - } - - fn epk_bytes(epk: &Self::EphemeralPublicKey) -> EphemeralKeyBytes { - epk.to_bytes() - } - - fn epk(ephemeral_key: &EphemeralKeyBytes) -> Option { - // ZIP 216: We unconditionally reject non-canonical encodings, because these have - // always been rejected by consensus (due to small-order checks). - // https://zips.z.cash/zip-0216#specification - EphemeralPublicKey::from_bytes(&ephemeral_key.0).into() - } - - 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| { - DiversifiedTransmissionKey::derive(ivk, diversifier) - }) - } - - fn parse_note_plaintext_without_memo_ovk( - &self, - pk_d: &Self::DiversifiedTransmissionKey, - plaintext: &NotePlaintextBytes, - ) -> Option<(Self::Note, Self::Recipient)> { - sapling_parse_note_plaintext_without_memo(self, &plaintext.0, |diversifier| { - diversifier.g_d().map(|_| *pk_d) - }) - } - - fn cmstar(note: &Self::Note) -> Self::ExtractedCommitment { - note.cmu() - } - - fn extract_pk_d(op: &OutPlaintextBytes) -> Option { - DiversifiedTransmissionKey::from_bytes( - op.0[0..32].try_into().expect("slice is the correct length"), - ) - .into() - } - - fn extract_esk(op: &OutPlaintextBytes) -> Option { - EphemeralSecretKey::from_bytes( - op.0[32..OUT_PLAINTEXT_SIZE] - .try_into() - .expect("slice is the correct length"), - ) - .into() - } - - fn extract_memo(&self, plaintext: &NotePlaintextBytes) -> Self::Memo { - MemoBytes::from_bytes(&plaintext.0[COMPACT_NOTE_SIZE..NOTE_PLAINTEXT_SIZE]).unwrap() - } -} - -impl BatchDomain for SaplingDomain

{ - fn batch_kdf<'a>( - items: impl Iterator, &'a EphemeralKeyBytes)>, - ) -> Vec> { - let (shared_secrets, ephemeral_keys): (Vec<_>, Vec<_>) = items.unzip(); - - SharedSecret::batch_to_affine(shared_secrets) - .zip(ephemeral_keys.into_iter()) - .map(|(secret, ephemeral_key)| { - secret.map(|dhsecret| SharedSecret::kdf_sapling_inner(dhsecret, ephemeral_key)) - }) - .collect() - } - - fn batch_epk( - ephemeral_keys: impl Iterator, - ) -> Vec<(Option, EphemeralKeyBytes)> { - let ephemeral_keys: Vec<_> = ephemeral_keys.collect(); - let epks = jubjub::AffinePoint::batch_from_bytes(ephemeral_keys.iter().map(|b| b.0)); - epks.into_iter() - .zip(ephemeral_keys.into_iter()) - .map(|(epk, ephemeral_key)| { - ( - Option::from(epk) - .map(EphemeralPublicKey::from_affine) - .map(Self::prepare_epk), - ephemeral_key, - ) - }) - .collect() - } -} - -/// 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. -/// -/// NB: the example code here only covers the post-Canopy case. -/// -/// # Examples -/// -/// ``` -/// use ff::Field; -/// use rand_core::OsRng; -/// use zcash_primitives::{ -/// keys::{OutgoingViewingKey, prf_expand}, -/// consensus::{TEST_NETWORK, TestNetwork, NetworkUpgrade, Parameters}, -/// memo::MemoBytes, -/// sapling::{ -/// note_encryption::sapling_note_encryption, -/// util::generate_random_rseed, -/// value::{NoteValue, ValueCommitTrapdoor, ValueCommitment}, -/// Diversifier, PaymentAddress, Rseed, SaplingIvk, -/// }, -/// }; -/// -/// let mut rng = OsRng; -/// -/// let ivk = SaplingIvk(jubjub::Scalar::random(&mut rng)); -/// let diversifier = Diversifier([0; 11]); -/// let to = ivk.to_payment_address(diversifier).unwrap(); -/// let ovk = Some(OutgoingViewingKey([0; 32])); -/// -/// let value = NoteValue::from_raw(1000); -/// let rcv = ValueCommitTrapdoor::random(&mut rng); -/// let cv = ValueCommitment::derive(value, rcv); -/// 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.inner(), rseed); -/// let cmu = note.cmu(); -/// -/// let mut enc = sapling_note_encryption::<_, TestNetwork>(ovk, note, MemoBytes::empty(), &mut rng); -/// let encCiphertext = enc.encrypt_note_plaintext(); -/// let outCiphertext = enc.encrypt_outgoing_plaintext(&cv, &cmu, &mut rng); -/// ``` -pub fn sapling_note_encryption( - ovk: Option, - note: Note, - memo: MemoBytes, - rng: &mut R, -) -> NoteEncryption> { - let esk = note.generate_or_derive_esk_internal(rng); - NoteEncryption::new_with_esk(esk, ovk, note, 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, ENC_CIPHERTEXT_SIZE>, ->( - params: &P, - height: BlockHeight, - ivk: &PreparedIncomingViewingKey, - 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, COMPACT_NOTE_SIZE>, ->( - params: &P, - height: BlockHeight, - ivk: &PreparedIncomingViewingKey, - 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)> { - let domain = SaplingDomain { - params: params.clone(), - height, - }; - - try_output_recovery_with_ovk(&domain, ovk, output, output.cv(), output.out_ciphertext()) -} - -#[cfg(test)] -mod tests { - use chacha20poly1305::{ - aead::{AeadInPlace, KeyInit}, - ChaCha20Poly1305, - }; - use ff::{Field, PrimeField}; - use group::Group; - use group::GroupEncoding; - use rand_core::OsRng; - use rand_core::{CryptoRng, RngCore}; - - use zcash_note_encryption::{ - batch, EphemeralKeyBytes, NoteEncryption, OutgoingCipherKey, ENC_CIPHERTEXT_SIZE, - NOTE_PLAINTEXT_SIZE, OUT_CIPHERTEXT_SIZE, OUT_PLAINTEXT_SIZE, - }; - - use super::{ - prf_ock, 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, - }, - keys::OutgoingViewingKey, - memo::MemoBytes, - sapling::{ - keys::{DiversifiedTransmissionKey, EphemeralSecretKey}, - note::ExtractedNoteCommitment, - note_encryption::PreparedIncomingViewingKey, - util::generate_random_rseed, - value::{NoteValue, ValueCommitTrapdoor, ValueCommitment}, - Diversifier, PaymentAddress, Rseed, SaplingIvk, - }, - transaction::components::{ - sapling::{self, CompactOutputDescription, OutputDescription}, - GROTH_PROOF_SIZE, - }, - }; - - fn random_enc_ciphertext( - height: BlockHeight, - mut rng: &mut R, - ) -> ( - OutgoingViewingKey, - OutgoingCipherKey, - PreparedIncomingViewingKey, - OutputDescription, - ) { - let ivk = SaplingIvk(jubjub::Fr::random(&mut rng)); - let prepared_ivk = PreparedIncomingViewingKey::new(&ivk); - - let (ovk, ock, output) = random_enc_ciphertext_with(height, &ivk, rng); - - assert!( - try_sapling_note_decryption(&TEST_NETWORK, height, &prepared_ivk, &output).is_some() - ); - assert!(try_sapling_compact_note_decryption( - &TEST_NETWORK, - height, - &prepared_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, prepared_ivk, output) - } - - fn random_enc_ciphertext_with( - height: BlockHeight, - ivk: &SaplingIvk, - mut rng: &mut R, - ) -> ( - OutgoingViewingKey, - OutgoingCipherKey, - OutputDescription, - ) { - let diversifier = Diversifier([0; 11]); - let pa = ivk.to_payment_address(diversifier).unwrap(); - - // Construct the value commitment for the proof instance - let value = NoteValue::from_raw(100); - let rcv = ValueCommitTrapdoor::random(&mut rng); - let cv = ValueCommitment::derive(value, rcv); - - let rseed = generate_random_rseed(&TEST_NETWORK, height, &mut rng); - - let note = pa.create_note(value.inner(), rseed); - let cmu = note.cmu(); - - let ovk = OutgoingViewingKey([0; 32]); - let ne = sapling_note_encryption::<_, TestNetwork>( - Some(ovk), - note, - MemoBytes::empty(), - &mut rng, - ); - let epk = ne.epk(); - let ock = prf_ock(&ovk, &cv, &cmu.to_bytes(), &epk.to_bytes()); - - let out_ciphertext = ne.encrypt_outgoing_plaintext(&cv, &cmu, &mut rng); - let output = OutputDescription::from_parts( - cv, - cmu, - epk.to_bytes(), - ne.encrypt_note_plaintext(), - out_ciphertext, - [0u8; GROTH_PROOF_SIZE], - ); - - (ovk, ock, output) - } - - fn reencrypt_out_ciphertext( - ovk: &OutgoingViewingKey, - cv: &ValueCommitment, - cmu: &ExtractedNoteCommitment, - ephemeral_key: &EphemeralKeyBytes, - out_ciphertext: &[u8; OUT_CIPHERTEXT_SIZE], - modify_plaintext: impl Fn(&mut [u8; OUT_PLAINTEXT_SIZE]), - ) -> [u8; OUT_CIPHERTEXT_SIZE] { - let ock = prf_ock(ovk, cv, &cmu.to_bytes(), ephemeral_key); - - let mut op = [0; OUT_PLAINTEXT_SIZE]; - op.copy_from_slice(&out_ciphertext[..OUT_PLAINTEXT_SIZE]); - - ChaCha20Poly1305::new(ock.as_ref().into()) - .decrypt_in_place_detached( - [0u8; 12][..].into(), - &[], - &mut op, - out_ciphertext[OUT_PLAINTEXT_SIZE..].into(), - ) - .unwrap(); - - modify_plaintext(&mut op); - - let tag = ChaCha20Poly1305::new(ock.as_ref().into()) - .encrypt_in_place_detached([0u8; 12][..].into(), &[], &mut op) - .unwrap(); - - let mut out_ciphertext = [0u8; OUT_CIPHERTEXT_SIZE]; - out_ciphertext[..OUT_PLAINTEXT_SIZE].copy_from_slice(&op); - out_ciphertext[OUT_PLAINTEXT_SIZE..].copy_from_slice(&tag); - out_ciphertext - } - - fn reencrypt_enc_ciphertext( - ovk: &OutgoingViewingKey, - cv: &ValueCommitment, - cmu: &ExtractedNoteCommitment, - ephemeral_key: &EphemeralKeyBytes, - enc_ciphertext: &[u8; ENC_CIPHERTEXT_SIZE], - out_ciphertext: &[u8; OUT_CIPHERTEXT_SIZE], - modify_plaintext: impl Fn(&mut [u8; NOTE_PLAINTEXT_SIZE]), - ) -> [u8; ENC_CIPHERTEXT_SIZE] { - let ock = prf_ock(ovk, cv, &cmu.to_bytes(), ephemeral_key); - - let mut op = [0; OUT_PLAINTEXT_SIZE]; - op.copy_from_slice(&out_ciphertext[..OUT_PLAINTEXT_SIZE]); - - ChaCha20Poly1305::new(ock.as_ref().into()) - .decrypt_in_place_detached( - [0u8; 12][..].into(), - &[], - &mut op, - out_ciphertext[OUT_PLAINTEXT_SIZE..].into(), - ) - .unwrap(); - - let pk_d = DiversifiedTransmissionKey::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 = EphemeralSecretKey(esk).agree(&pk_d); - let key = shared_secret.kdf_sapling(ephemeral_key); - - let mut plaintext = [0; NOTE_PLAINTEXT_SIZE]; - plaintext.copy_from_slice(&enc_ciphertext[..NOTE_PLAINTEXT_SIZE]); - - ChaCha20Poly1305::new(key.as_bytes().into()) - .decrypt_in_place_detached( - [0u8; 12][..].into(), - &[], - &mut plaintext, - enc_ciphertext[NOTE_PLAINTEXT_SIZE..].into(), - ) - .unwrap(); - - modify_plaintext(&mut plaintext); - - let tag = ChaCha20Poly1305::new(key.as_ref().into()) - .encrypt_in_place_detached([0u8; 12][..].into(), &[], &mut plaintext) - .unwrap(); - - let mut enc_ciphertext = [0u8; ENC_CIPHERTEXT_SIZE]; - enc_ciphertext[..NOTE_PLAINTEXT_SIZE].copy_from_slice(&plaintext); - enc_ciphertext[NOTE_PLAINTEXT_SIZE..].copy_from_slice(&tag); - enc_ciphertext - } - - 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, - &PreparedIncomingViewingKey::new(&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_mut() = jubjub::ExtendedPoint::random(&mut rng).to_bytes().into(); - - 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_mut() = - ExtractedNoteCommitment::from_bytes(&bls12_381::Scalar::random(&mut rng).to_repr()) - .unwrap(); - - 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_mut()[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); - - *output.enc_ciphertext_mut() = reencrypt_enc_ciphertext( - &ovk, - output.cv(), - output.cmu(), - output.ephemeral_key(), - 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); - - *output.enc_ciphertext_mut() = reencrypt_enc_ciphertext( - &ovk, - output.cv(), - output.cmu(), - output.ephemeral_key(), - 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); - - *output.enc_ciphertext_mut() = reencrypt_enc_ciphertext( - &ovk, - output.cv(), - output.cmu(), - output.ephemeral_key(), - 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, - &PreparedIncomingViewingKey::new(&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_mut() = jubjub::ExtendedPoint::random(&mut rng).to_bytes().into(); - - 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_mut() = - ExtractedNoteCommitment::from_bytes(&bls12_381::Scalar::random(&mut rng).to_repr()) - .unwrap(); - - 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); - - *output.enc_ciphertext_mut() = reencrypt_enc_ciphertext( - &ovk, - output.cv(), - output.cmu(), - output.ephemeral_key(), - 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); - - *output.enc_ciphertext_mut() = reencrypt_enc_ciphertext( - &ovk, - output.cv(), - output.cmu(), - output.ephemeral_key(), - 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); - - *output.enc_ciphertext_mut() = reencrypt_enc_ciphertext( - &ovk, - output.cv(), - output.cmu(), - output.ephemeral_key(), - 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_mut() = ValueCommitment::derive( - NoteValue::from_raw(7), - ValueCommitTrapdoor::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_mut() = - ExtractedNoteCommitment::from_bytes(&bls12_381::Scalar::random(&mut rng).to_repr()) - .unwrap(); - - 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_mut() = jubjub::ExtendedPoint::random(&mut rng).to_bytes().into(); - - 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_mut()[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_mut()[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); - - *output.enc_ciphertext_mut() = reencrypt_enc_ciphertext( - &ovk, - output.cv(), - output.cmu(), - output.ephemeral_key(), - 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); - - *output.enc_ciphertext_mut() = reencrypt_enc_ciphertext( - &ovk, - output.cv(), - output.cmu(), - output.ephemeral_key(), - 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); - - *output.enc_ciphertext_mut() = reencrypt_enc_ciphertext( - &ovk, - output.cv(), - output.cmu(), - output.ephemeral_key(), - 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 (ovk, ock, _, mut output) = random_enc_ciphertext(height, &mut rng); - - *output.out_ciphertext_mut() = reencrypt_out_ciphertext( - &ovk, - output.cv(), - output.cmu(), - output.ephemeral_key(), - output.out_ciphertext(), - |pt| pt[0..32].copy_from_slice(&jubjub::ExtendedPoint::random(rng).to_bytes()), - ); - 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_cmu { - ($field:expr) => {{ - ExtractedNoteCommitment::from_bytes($field[..].try_into().unwrap()).unwrap() - }}; - } - - macro_rules! read_jubjub_scalar { - ($field:expr) => {{ - jubjub::Fr::from_repr($field[..].try_into().unwrap()).unwrap() - }}; - } - - macro_rules! read_pk_d { - ($field:expr) => { - DiversifiedTransmissionKey::from_bytes(&$field).unwrap() - }; - } - - macro_rules! read_cv { - ($field:expr) => { - ValueCommitment::from_bytes_not_small_order(&$field).unwrap() - }; - } - - let height = TEST_NETWORK.activation_height(Sapling).unwrap(); - - for tv in test_vectors { - // - // Load the test vector components - // - - let ivk = PreparedIncomingViewingKey::new(&SaplingIvk(read_jubjub_scalar!(tv.ivk))); - let pk_d = read_pk_d!(tv.default_pk_d); - let rcm = read_jubjub_scalar!(tv.rcm); - let cv = read_cv!(tv.cv); - let cmu = read_cmu!(tv.cmu); - let esk = EphemeralSecretKey(read_jubjub_scalar!(tv.esk)); - let ephemeral_key = EphemeralKeyBytes(tv.epk); - - // - // Test the individual components - // - - let shared_secret = esk.agree(&pk_d); - assert_eq!(shared_secret.to_bytes(), tv.shared_secret); - - let k_enc = shared_secret.kdf_sapling(&ephemeral_key); - assert_eq!(k_enc.as_bytes(), tv.k_enc); - - let ovk = OutgoingViewingKey(tv.ovk); - let ock = prf_ock(&ovk, &cv, &cmu.to_bytes(), &ephemeral_key); - 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)); - assert_eq!(note.cmu(), cmu); - - let output = OutputDescription::from_parts( - cv.clone(), - cmu, - ephemeral_key, - tv.c_enc, - tv.c_out, - [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"), - } - - match &batch::try_note_decryption( - &[ivk.clone()], - &[( - SaplingDomain::for_height(TEST_NETWORK, height), - output.clone(), - )], - )[..] - { - [Some(((decrypted_note, decrypted_to, decrypted_memo), i))] => { - assert_eq!(decrypted_note, ¬e); - assert_eq!(decrypted_to, &to); - assert_eq!(&decrypted_memo.as_array()[..], &tv.memo[..]); - assert_eq!(*i, 0); - } - _ => panic!("Note decryption failed"), - } - - match &batch::try_compact_note_decryption( - &[ivk.clone()], - &[( - SaplingDomain::for_height(TEST_NETWORK, height), - CompactOutputDescription::from(output.clone()), - )], - )[..] - { - [Some(((decrypted_note, decrypted_to), i))] => { - assert_eq!(decrypted_note, ¬e); - assert_eq!(decrypted_to, &to); - assert_eq!(*i, 0); - } - _ => panic!("Note decryption failed"), - } - - // - // Test encryption - // - - let ne = NoteEncryption::>::new_with_esk( - esk, - Some(ovk), - note, - 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[..] - ); - } - } - - #[test] - fn batching() { - let mut rng = OsRng; - let height = TEST_NETWORK.activation_height(Canopy).unwrap(); - - // Test batch trial-decryption with multiple IVKs and outputs. - let invalid_ivk = PreparedIncomingViewingKey::new(&SaplingIvk(jubjub::Fr::random(rng))); - let valid_ivk = SaplingIvk(jubjub::Fr::random(rng)); - let outputs: Vec<_> = (0..10) - .map(|_| { - ( - SaplingDomain::for_height(TEST_NETWORK, height), - random_enc_ciphertext_with(height, &valid_ivk, &mut rng).2, - ) - }) - .collect(); - let valid_ivk = PreparedIncomingViewingKey::new(&valid_ivk); - - // Check that batched trial decryptions with invalid_ivk fails. - let res = batch::try_note_decryption(&[invalid_ivk.clone()], &outputs); - assert_eq!(res.len(), 10); - assert_eq!(&res[..], &vec![None; 10][..]); - - // Check that batched trial decryptions with valid_ivk succeeds. - let res = batch::try_note_decryption(&[invalid_ivk, valid_ivk.clone()], &outputs); - assert_eq!(res.len(), 10); - for (result, (_, output)) in res.iter().zip(outputs.iter()) { - // Confirm the successful batched trial decryptions gave the same result. - // In all cases, the index of the valid ivk is returned. - assert!(result.is_some()); - assert_eq!( - result, - &try_sapling_note_decryption(&TEST_NETWORK, height, &valid_ivk, output) - .map(|r| (r, 1)) - ); - } - } -} diff --git a/zcash_primitives/src/sapling/pedersen_hash.rs b/zcash_primitives/src/sapling/pedersen_hash.rs deleted file mode 100644 index 0e5ed26c53..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 4d051afcad..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> { - 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 0dc8c86816..0000000000 --- a/zcash_primitives/src/sapling/prover.rs +++ /dev/null @@ -1,137 +0,0 @@ -//! Abstractions over the proving system and parameters. - -use crate::{ - sapling::{ - self, - redjubjub::{PublicKey, Signature}, - value::ValueCommitment, - }, - 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: sapling::MerklePath, - ) -> Result<([u8; GROTH_PROOF_SIZE], ValueCommitment, 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], ValueCommitment); - - /// 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 rand_core::OsRng; - - use super::TxProver; - use crate::{ - constants::SPENDING_KEY_GENERATOR, - sapling::{ - self, - redjubjub::{PublicKey, Signature}, - value::{NoteValue, ValueCommitTrapdoor, ValueCommitment}, - Diversifier, PaymentAddress, ProofGenerationKey, Rseed, - }, - transaction::components::{Amount, GROTH_PROOF_SIZE}, - }; - - 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: sapling::MerklePath, - ) -> Result<([u8; GROTH_PROOF_SIZE], ValueCommitment, PublicKey), ()> { - let mut rng = OsRng; - - let value = NoteValue::from_raw(value); - let rcv = ValueCommitTrapdoor::random(&mut rng); - let cv = ValueCommitment::derive(value, rcv); - - let rk = - PublicKey(proof_generation_key.ak.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], ValueCommitment) { - let mut rng = OsRng; - - let value = NoteValue::from_raw(value); - let rcv = ValueCommitTrapdoor::random(&mut rng); - let cv = ValueCommitment::derive(value, rcv); - - ([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 74b7b6e0d5..0000000000 --- a/zcash_primitives/src/sapling/redjubjub.rs +++ /dev/null @@ -1,375 +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::{AffinePoint, 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())?; - - Option::from(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 { - #[must_use] - 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()) - } - - #[must_use] - 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 { - self.verify_with_zip216(msg, sig, p_g, true) - } - - pub fn verify_with_zip216( - &self, - msg: &[u8], - sig: &Signature, - p_g: SubgroupPoint, - zip216_enabled: bool, - ) -> bool { - // c = H*(Rbar || M) - let c = h_star(&sig.rbar[..], msg); - - // Signature checks: - // R != invalid - let r = { - let r = if zip216_enabled { - ExtendedPoint::from_bytes(&sig.rbar) - } else { - AffinePoint::from_bytes_pre_zip216_compatibility(sig.rbar).map(|p| p.to_extended()) - }; - 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/spec.rs b/zcash_primitives/src/sapling/spec.rs deleted file mode 100644 index 0e78073265..0000000000 --- a/zcash_primitives/src/sapling/spec.rs +++ /dev/null @@ -1,165 +0,0 @@ -//! Helper functions defined in the Zcash Protocol Specification. - -use blake2s_simd::Params as Blake2sParams; -use group::{cofactor::CofactorGroup, ff::PrimeField, Curve, GroupEncoding, WnafBase, WnafScalar}; - -use super::{ - group_hash::group_hash, - pedersen_hash::{pedersen_hash, Personalization}, -}; -use crate::constants::{ - CRH_IVK_PERSONALIZATION, KEY_DIVERSIFICATION_PERSONALIZATION, - NOTE_COMMITMENT_RANDOMNESS_GENERATOR, NULLIFIER_POSITION_GENERATOR, PRF_NF_PERSONALIZATION, -}; - -const PREPARED_WINDOW_SIZE: usize = 4; -pub(crate) type PreparedBase = WnafBase; -pub(crate) type PreparedBaseSubgroup = WnafBase; -pub(crate) type PreparedScalar = WnafScalar; - -/// $CRH^\mathsf{ivk}(ak, nk)$ -/// -/// Defined in [Zcash Protocol Spec § 5.4.1.5: CRH^ivk Hash Function][concretecrhivk]. -/// -/// [concretecrhivk]: https://zips.z.cash/protocol/protocol.pdf#concretecrhivk -pub(crate) fn crh_ivk(ak: [u8; 32], nk: [u8; 32]) -> jubjub::Scalar { - let mut h: [u8; 32] = Blake2sParams::new() - .hash_length(32) - .personal(CRH_IVK_PERSONALIZATION) - .to_state() - .update(&ak) - .update(&nk) - .finalize() - .as_bytes() - .try_into() - .expect("output length is correct"); - - // Drop the most significant five bits, so it can be interpreted as a scalar. - h[31] &= 0b0000_0111; - - jubjub::Fr::from_repr(h).unwrap() -} - -/// Defined in [Zcash Protocol Spec § 5.4.1.6: DiversifyHash^Sapling and DiversifyHash^Orchard Hash Functions][concretediversifyhash]. -/// -/// [concretediversifyhash]: https://zips.z.cash/protocol/protocol.pdf#concretediversifyhash -pub(crate) fn diversify_hash(d: &[u8; 11]) -> Option { - group_hash(d, KEY_DIVERSIFICATION_PERSONALIZATION) -} - -/// $MixingPedersenHash$. -/// -/// Defined in [Zcash Protocol Spec § 5.4.1.8: Mixing Pedersen Hash Function][concretemixinghash]. -/// -/// [concretemixinghash]: https://zips.z.cash/protocol/protocol.pdf#concretemixinghash -pub(crate) fn mixing_pedersen_hash( - cm: jubjub::SubgroupPoint, - position: u64, -) -> jubjub::SubgroupPoint { - cm + (NULLIFIER_POSITION_GENERATOR * jubjub::Fr::from(position)) -} - -/// $PRF^\mathsf{nfSapling}_{nk}(\rho)$ -/// -/// Defined in [Zcash Protocol Spec § 5.4.2: Pseudo Random Functions][concreteprfs]. -/// -/// [concreteprfs]: https://zips.z.cash/protocol/protocol.pdf#concreteprfs -pub(crate) fn prf_nf(nk: &jubjub::SubgroupPoint, rho: &jubjub::SubgroupPoint) -> [u8; 32] { - Blake2sParams::new() - .hash_length(32) - .personal(PRF_NF_PERSONALIZATION) - .to_state() - .update(&nk.to_bytes()) - .update(&rho.to_bytes()) - .finalize() - .as_bytes() - .try_into() - .expect("output length is correct") -} - -/// Defined in [Zcash Protocol Spec § 5.4.5.3: Sapling Key Agreement][concretesaplingkeyagreement]. -/// -/// [concretesaplingkeyagreement]: https://zips.z.cash/protocol/protocol.pdf#concretesaplingkeyagreement -pub(crate) fn ka_sapling_derive_public( - sk: &jubjub::Scalar, - b: &jubjub::ExtendedPoint, -) -> jubjub::ExtendedPoint { - ka_sapling_derive_public_prepared(&PreparedScalar::new(sk), &PreparedBase::new(*b)) -} - -/// Defined in [Zcash Protocol Spec § 5.4.5.3: Sapling Key Agreement][concretesaplingkeyagreement]. -/// -/// [concretesaplingkeyagreement]: https://zips.z.cash/protocol/protocol.pdf#concretesaplingkeyagreement -pub(crate) fn ka_sapling_derive_public_prepared( - sk: &PreparedScalar, - b: &PreparedBase, -) -> jubjub::ExtendedPoint { - // [sk] b - b * sk -} - -/// This is defined implicitly by [Zcash Protocol Spec § 4.2.2: Sapling Key Components][saplingkeycomponents] -/// which uses $KA^\mathsf{Sapling}.\mathsf{DerivePublic}$ to produce a diversified -/// transmission key with type $KA^\mathsf{Sapling}.\mathsf{PublicPrimeSubgroup}$. -/// -/// [saplingkeycomponents]: https://zips.z.cash/protocol/protocol.pdf#saplingkeycomponents -pub(crate) fn ka_sapling_derive_public_subgroup_prepared( - sk: &PreparedScalar, - b: &PreparedBaseSubgroup, -) -> jubjub::SubgroupPoint { - // [sk] b - b * sk -} - -/// Defined in [Zcash Protocol Spec § 5.4.5.3: Sapling Key Agreement][concretesaplingkeyagreement]. -/// -/// [concretesaplingkeyagreement]: https://zips.z.cash/protocol/protocol.pdf#concretesaplingkeyagreement -pub(crate) fn ka_sapling_agree( - sk: &jubjub::Scalar, - b: &jubjub::ExtendedPoint, -) -> jubjub::SubgroupPoint { - ka_sapling_agree_prepared(&PreparedScalar::new(sk), &PreparedBase::new(*b)) -} - -/// Defined in [Zcash Protocol Spec § 5.4.5.3: Sapling Key Agreement][concretesaplingkeyagreement]. -/// -/// [concretesaplingkeyagreement]: https://zips.z.cash/protocol/protocol.pdf#concretesaplingkeyagreement -pub(crate) fn ka_sapling_agree_prepared( - sk: &PreparedScalar, - b: &PreparedBase, -) -> jubjub::SubgroupPoint { - // [8 sk] b - // ::clear_cofactor is implemented using - // ExtendedPoint::mul_by_cofactor in the jubjub crate. - - (b * sk).clear_cofactor() -} - -/// $WindowedPedersenCommit_r(s)$ -/// -/// Defined in [Zcash Protocol Spec § 5.4.8.2: Windowed Pedersen commitments][concretewindowedcommit]. -/// -/// [concretewindowedcommit]: https://zips.z.cash/protocol/protocol.pdf#concretewindowedcommit -pub(crate) fn windowed_pedersen_commit( - personalization: Personalization, - s: I, - r: jubjub::Scalar, -) -> jubjub::SubgroupPoint -where - I: IntoIterator, -{ - pedersen_hash(personalization, s) + (NOTE_COMMITMENT_RANDOMNESS_GENERATOR * r) -} - -/// Coordinate extractor for Jubjub. -/// -/// Defined in [Zcash Protocol Spec § 5.4.9.4: Coordinate Extractor for Jubjub][concreteextractorjubjub]. -/// -/// [concreteextractorjubjub]: https://zips.z.cash/protocol/protocol.pdf#concreteextractorjubjub -pub(crate) fn extract_p(point: &jubjub::SubgroupPoint) -> bls12_381::Scalar { - // The commitment is in the prime order subgroup, so mapping the - // commitment to the u-coordinate is an injective encoding. - Into::<&jubjub::ExtendedPoint>::into(point) - .to_affine() - .get_u() -} diff --git a/zcash_primitives/src/sapling/tree.rs b/zcash_primitives/src/sapling/tree.rs deleted file mode 100644 index 5bacb51e23..0000000000 --- a/zcash_primitives/src/sapling/tree.rs +++ /dev/null @@ -1,143 +0,0 @@ -use bitvec::{order::Lsb0, view::AsBits}; -use group::{ff::PrimeField, Curve}; -use incrementalmerkletree::{Hashable, Level}; -use lazy_static::lazy_static; -use std::io::{self, Read, Write}; - -use super::{ - note::ExtractedNoteCommitment, - pedersen_hash::{pedersen_hash, Personalization}, -}; -use crate::merkle_tree::HashSer; - -pub const NOTE_COMMITMENT_TREE_DEPTH: u8 = 32; -pub type CommitmentTree = - incrementalmerkletree::frontier::CommitmentTree; -pub type IncrementalWitness = - incrementalmerkletree::witness::IncrementalWitness; -pub type MerklePath = incrementalmerkletree::MerklePath; - -lazy_static! { - static ref UNCOMMITTED_SAPLING: bls12_381::Scalar = bls12_381::Scalar::one(); - static ref EMPTY_ROOTS: Vec = { - let mut v = vec![Node::empty_leaf()]; - for d in 0..NOTE_COMMITMENT_TREE_DEPTH { - let next = Node::combine(d.into(), &v[usize::from(d)], &v[usize::from(d)]); - v.push(next); - } - v - }; -} - -/// 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, Eq)] -pub struct Node { - pub(super) repr: [u8; 32], -} - -impl Node { - #[cfg(test)] - pub(crate) fn new(repr: [u8; 32]) -> Self { - Node { repr } - } - - /// Creates a tree leaf from the given Sapling note commitment. - pub fn from_cmu(value: &ExtractedNoteCommitment) -> Self { - Node { - repr: value.to_bytes(), - } - } - - /// Constructs a new note commitment tree node from a [`bls12_381::Scalar`] - pub fn from_scalar(cmu: bls12_381::Scalar) -> Self { - Self { - repr: cmu.to_repr(), - } - } -} - -impl Hashable for Node { - fn empty_leaf() -> Self { - Node { - repr: UNCOMMITTED_SAPLING.to_repr(), - } - } - - fn combine(level: Level, lhs: &Self, rhs: &Self) -> Self { - Node { - repr: merkle_hash(level.into(), &lhs.repr, &rhs.repr), - } - } - - fn empty_root(level: Level) -> Self { - EMPTY_ROOTS[::from(level)] - } -} - -impl HashSer for Node { - fn read(mut reader: R) -> io::Result { - let mut repr = [0u8; 32]; - reader.read_exact(&mut repr)?; - Ok(Node { repr }) - } - - fn write(&self, mut writer: W) -> io::Result<()> { - writer.write_all(self.repr.as_ref()) - } -} - -impl From for bls12_381::Scalar { - fn from(node: Node) -> Self { - // Tree nodes should be in the prime field. - bls12_381::Scalar::from_repr(node.repr).unwrap() - } -} - -#[cfg(any(test, feature = "test-dependencies"))] -pub(super) mod testing { - use proptest::prelude::*; - - use super::Node; - - prop_compose! { - pub fn arb_node()(value in prop::array::uniform32(prop::num::u8::ANY)) -> Node { - Node { - repr: value - } - } - } -} 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/sapling/value.rs b/zcash_primitives/src/sapling/value.rs deleted file mode 100644 index b504fb0b72..0000000000 --- a/zcash_primitives/src/sapling/value.rs +++ /dev/null @@ -1,248 +0,0 @@ -//! Monetary values within the Sapling shielded pool. -//! -//! Values are represented in three places within the Sapling protocol: -//! - [`NoteValue`], the value of an individual note. It is an unsigned 64-bit integer -//! (with maximum value [`MAX_NOTE_VALUE`]), and is serialized in a note plaintext. -//! - [`ValueSum`], the sum of note values within a Sapling [`Bundle`]. It is represented -//! as an `i128` and places an upper bound on the maximum number of notes within a -//! single [`Bundle`]. -//! - `valueBalanceSapling`, which is a signed 63-bit integer. This is represented -//! by a user-defined type parameter on [`Bundle`], returned by -//! [`Bundle::value_balance`] and [`SaplingBuilder::value_balance`]. -//! -//! If your specific instantiation of the Sapling protocol requires a smaller bound on -//! valid note values (for example, Zcash's `MAX_MONEY` fits into a 51-bit integer), you -//! should enforce this in two ways: -//! -//! - Define your `valueBalanceSapling` type to enforce your valid value range. This can -//! be checked in its `TryFrom` implementation. -//! - Define your own "amount" type for note values, and convert it to `NoteValue` prior -//! to calling [`SaplingBuilder::add_output`]. -//! -//! Inside the circuit, note values are constrained to be unsigned 64-bit integers. -//! -//! # Caution! -//! -//! An `i64` is _not_ a signed 64-bit integer! The [Rust documentation] calls `i64` the -//! 64-bit signed integer type, which is true in the sense that its encoding in memory -//! takes up 64 bits. Numerically, however, `i64` is a signed 63-bit integer. -//! -//! Fortunately, users of this crate should never need to construct [`ValueSum`] directly; -//! you should only need to interact with [`NoteValue`] (which can be safely constructed -//! from a `u64`) and `valueBalanceSapling` (which can be represented as an `i64`). -//! -//! [`Bundle`]: crate::transaction::components::sapling::Bundle -//! [`Bundle::value_balance`]: crate::transaction::components::sapling::Bundle::value_balance -//! [`SaplingBuilder::value_balance`]: crate::transaction::components::sapling::builder::SaplingBuilder::value_balance -//! [`SaplingBuilder::add_output`]: crate::transaction::components::sapling::builder::SaplingBuilder::add_output -//! [Rust documentation]: https://doc.rust-lang.org/stable/std/primitive.i64.html - -use bitvec::{array::BitArray, order::Lsb0}; -use ff::Field; -use group::GroupEncoding; -use rand::RngCore; -use subtle::CtOption; - -use crate::constants::{VALUE_COMMITMENT_RANDOMNESS_GENERATOR, VALUE_COMMITMENT_VALUE_GENERATOR}; - -mod sums; -pub use sums::{CommitmentSum, OverflowError, TrapdoorSum, ValueSum}; - -/// Maximum note value. -pub const MAX_NOTE_VALUE: u64 = u64::MAX; - -/// The non-negative value of an individual Sapling note. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub struct NoteValue(u64); - -impl NoteValue { - /// Returns the raw underlying value. - pub fn inner(&self) -> u64 { - self.0 - } - - /// Creates a note value from its raw numeric value. - /// - /// This only enforces that the value is an unsigned 64-bit integer. Callers should - /// enforce any additional constraints on the value's valid range themselves. - pub fn from_raw(value: u64) -> Self { - NoteValue(value) - } - - pub(crate) fn to_le_bits(self) -> BitArray<[u8; 8], Lsb0> { - BitArray::<_, Lsb0>::new(self.0.to_le_bytes()) - } -} - -/// The blinding factor for a [`ValueCommitment`]. -#[derive(Clone, Debug)] -pub struct ValueCommitTrapdoor(jubjub::Scalar); - -impl ValueCommitTrapdoor { - /// Generates a new value commitment trapdoor. - /// - /// This is public for access by `zcash_proofs`. - pub fn random(rng: impl RngCore) -> Self { - ValueCommitTrapdoor(jubjub::Scalar::random(rng)) - } - - /// Returns the inner Jubjub scalar representing this trapdoor. - /// - /// This is public for access by `zcash_proofs`. - pub fn inner(&self) -> jubjub::Scalar { - self.0 - } -} - -/// A commitment to a [`ValueSum`]. -/// -/// # Consensus rules -/// -/// The Zcash Protocol Spec requires Sapling Spend Descriptions and Output Descriptions to -/// not contain a small order `ValueCommitment`. However, the `ValueCommitment` type as -/// specified (and implemented here) may contain a small order point. In practice, it will -/// not occur: -/// - [`ValueCommitment::derive`] will only produce a small order point if both the given -/// [`NoteValue`] and [`ValueCommitTrapdoor`] are zero. However, the only constructor -/// available for `ValueCommitTrapdoor` is [`ValueCommitTrapdoor::random`], which will -/// produce zero with negligible probability (assuming a non-broken PRNG). -/// - [`ValueCommitment::from_bytes_not_small_order`] enforces this by definition, and is -/// the only constructor that can be used with data received over the network. -#[derive(Clone, Debug)] -pub struct ValueCommitment(jubjub::ExtendedPoint); - -impl ValueCommitment { - /// Derives a `ValueCommitment` by $\mathsf{ValueCommit^{Sapling}}$. - /// - /// Defined in [Zcash Protocol Spec § 5.4.8.3: Homomorphic Pedersen commitments (Sapling and Orchard)][concretehomomorphiccommit]. - /// - /// [concretehomomorphiccommit]: https://zips.z.cash/protocol/protocol.pdf#concretehomomorphiccommit - pub fn derive(value: NoteValue, rcv: ValueCommitTrapdoor) -> Self { - let cv = (VALUE_COMMITMENT_VALUE_GENERATOR * jubjub::Scalar::from(value.0)) - + (VALUE_COMMITMENT_RANDOMNESS_GENERATOR * rcv.0); - - ValueCommitment(cv.into()) - } - - /// Returns the inner Jubjub point representing this value commitment. - /// - /// This is public for access by `zcash_proofs`. - pub fn as_inner(&self) -> &jubjub::ExtendedPoint { - &self.0 - } - - /// Deserializes a value commitment from its byte representation. - /// - /// Returns `None` if `bytes` is an invalid representation of a Jubjub point, or the - /// resulting point is of small order. - /// - /// This method can be used to enforce the "not small order" consensus rules defined - /// in [Zcash Protocol Spec § 4.4: Spend Descriptions][spenddesc] and - /// [§ 4.5: Output Descriptions][outputdesc]. - /// - /// [spenddesc]: https://zips.z.cash/protocol/protocol.pdf#spenddesc - /// [outputdesc]: https://zips.z.cash/protocol/protocol.pdf#outputdesc - pub fn from_bytes_not_small_order(bytes: &[u8; 32]) -> CtOption { - jubjub::ExtendedPoint::from_bytes(bytes) - .and_then(|cv| CtOption::new(ValueCommitment(cv), !cv.is_small_order())) - } - - /// Serializes this value commitment to its canonical byte representation. - pub fn to_bytes(&self) -> [u8; 32] { - self.0.to_bytes() - } -} - -/// Generators for property testing. -#[cfg(any(test, feature = "test-dependencies"))] -#[cfg_attr(docsrs, doc(cfg(feature = "test-dependencies")))] -pub mod testing { - use proptest::prelude::*; - - use super::{NoteValue, ValueCommitTrapdoor, MAX_NOTE_VALUE}; - - prop_compose! { - /// Generate an arbitrary value in the range of valid nonnegative amounts. - pub fn arb_note_value()(value in 0u64..MAX_NOTE_VALUE) -> NoteValue { - NoteValue(value) - } - } - - prop_compose! { - /// Generate an arbitrary value in the range of valid positive amounts less than a - /// specified value. - pub fn arb_note_value_bounded(max: u64)(value in 0u64..max) -> NoteValue { - NoteValue(value) - } - } - - prop_compose! { - /// Generate an arbitrary value in the range of valid positive amounts less than a - /// specified value. - pub fn arb_positive_note_value(max: u64)(value in 1u64..max) -> NoteValue { - NoteValue(value) - } - } - - prop_compose! { - /// Generate an arbitrary Jubjub scalar. - fn arb_scalar()(bytes in prop::array::uniform32(0u8..)) -> jubjub::Scalar { - // Instead of rejecting out-of-range bytes, let's reduce them. - let mut buf = [0; 64]; - buf[..32].copy_from_slice(&bytes); - jubjub::Scalar::from_bytes_wide(&buf) - } - } - - prop_compose! { - /// Generate an arbitrary ValueCommitTrapdoor - pub fn arb_trapdoor()(rcv in arb_scalar()) -> ValueCommitTrapdoor { - ValueCommitTrapdoor(rcv) - } - } -} - -#[cfg(test)] -mod tests { - use proptest::prelude::*; - - use super::{ - testing::{arb_note_value_bounded, arb_trapdoor}, - CommitmentSum, OverflowError, TrapdoorSum, ValueCommitment, ValueSum, - VALUE_COMMITMENT_RANDOMNESS_GENERATOR, - }; - use crate::sapling::redjubjub; - - proptest! { - #[test] - fn bsk_consistent_with_bvk( - values in (1usize..10).prop_flat_map(|n_values| prop::collection::vec( - (arb_note_value_bounded((i64::MAX as u64) / (n_values as u64)), arb_trapdoor()), - n_values, - )) - ) { - let value_balance: i64 = values - .iter() - .map(|(value, _)| value) - .sum::>() - .expect("we generate values that won't overflow") - .try_into() - .unwrap(); - - let bsk = values - .iter() - .map(|(_, rcv)| rcv) - .sum::() - .into_bsk(); - - let bvk = values - .into_iter() - .map(|(value, rcv)| ValueCommitment::derive(value, rcv)) - .sum::() - .into_bvk(value_balance); - - assert_eq!(redjubjub::PublicKey::from_private( - &bsk, VALUE_COMMITMENT_RANDOMNESS_GENERATOR).0, bvk.0); - } - } -} diff --git a/zcash_primitives/src/sapling/value/sums.rs b/zcash_primitives/src/sapling/value/sums.rs deleted file mode 100644 index 7a5420df25..0000000000 --- a/zcash_primitives/src/sapling/value/sums.rs +++ /dev/null @@ -1,221 +0,0 @@ -use core::fmt::{self, Debug}; -use core::iter::Sum; -use core::ops::{Add, AddAssign, Sub, SubAssign}; - -use super::{NoteValue, ValueCommitTrapdoor, ValueCommitment}; -use crate::constants::VALUE_COMMITMENT_VALUE_GENERATOR; -use crate::sapling::redjubjub; - -/// A value operation overflowed. -#[derive(Debug)] -pub struct OverflowError; - -impl fmt::Display for OverflowError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Sapling value operation overflowed") - } -} - -impl std::error::Error for OverflowError {} - -/// A sum of Sapling note values. -/// -/// [Zcash Protocol Spec § 4.13: Balance and Binding Signature (Sapling)][saplingbalance] -/// constrains the range of this type to between `[-(r_J - 1)/2..(r_J - 1)/2]` in the -/// abstract protocol, and `[−38913406623490299131842..104805176454780817500623]` in the -/// concrete Zcash protocol. We represent it as an `i128`, which has a range large enough -/// to handle Zcash transactions while small enough to ensure the abstract protocol bounds -/// are not breached. -/// -/// [saplingbalance]: https://zips.z.cash/protocol/protocol.pdf#saplingbalance -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub struct ValueSum(i128); - -impl ValueSum { - /// Initializes a sum of `NoteValue`s to zero. - pub fn zero() -> Self { - ValueSum(0) - } -} - -impl Add for ValueSum { - type Output = Option; - - #[allow(clippy::suspicious_arithmetic_impl)] - fn add(self, rhs: NoteValue) -> Self::Output { - self.0.checked_add(rhs.0.into()).map(ValueSum) - } -} - -impl Sub for ValueSum { - type Output = Option; - - #[allow(clippy::suspicious_arithmetic_impl)] - fn sub(self, rhs: NoteValue) -> Self::Output { - self.0.checked_sub(rhs.0.into()).map(ValueSum) - } -} - -impl<'a> Sum<&'a NoteValue> for Result { - fn sum>(iter: I) -> Self { - iter.fold(Ok(ValueSum(0)), |acc, v| (acc? + *v).ok_or(OverflowError)) - } -} - -impl Sum for Result { - fn sum>(iter: I) -> Self { - iter.fold(Ok(ValueSum(0)), |acc, v| (acc? + v).ok_or(OverflowError)) - } -} - -impl TryFrom for i64 { - type Error = OverflowError; - - fn try_from(v: ValueSum) -> Result { - i64::try_from(v.0).map_err(|_| OverflowError) - } -} - -/// A sum of Sapling value commitment blinding factors. -#[derive(Clone, Copy, Debug)] -pub struct TrapdoorSum(jubjub::Scalar); - -impl TrapdoorSum { - /// Initializes a sum of `ValueCommitTrapdoor`s to zero. - pub fn zero() -> Self { - TrapdoorSum(jubjub::Scalar::zero()) - } - - /// Transform this trapdoor sum into the corresponding RedJubjub private key. - /// - /// This is public for access by `zcash_proofs`. - pub fn into_bsk(self) -> redjubjub::PrivateKey { - redjubjub::PrivateKey(self.0) - } -} - -impl Add<&ValueCommitTrapdoor> for ValueCommitTrapdoor { - type Output = TrapdoorSum; - - fn add(self, rhs: &Self) -> Self::Output { - TrapdoorSum(self.0 + rhs.0) - } -} - -impl Add<&ValueCommitTrapdoor> for TrapdoorSum { - type Output = TrapdoorSum; - - fn add(self, rhs: &ValueCommitTrapdoor) -> Self::Output { - TrapdoorSum(self.0 + rhs.0) - } -} - -impl AddAssign<&ValueCommitTrapdoor> for TrapdoorSum { - fn add_assign(&mut self, rhs: &ValueCommitTrapdoor) { - self.0 += rhs.0; - } -} - -impl Sub<&ValueCommitTrapdoor> for ValueCommitTrapdoor { - type Output = TrapdoorSum; - - fn sub(self, rhs: &Self) -> Self::Output { - TrapdoorSum(self.0 - rhs.0) - } -} - -impl SubAssign<&ValueCommitTrapdoor> for TrapdoorSum { - fn sub_assign(&mut self, rhs: &ValueCommitTrapdoor) { - self.0 -= rhs.0; - } -} - -impl<'a> Sum<&'a ValueCommitTrapdoor> for TrapdoorSum { - fn sum>(iter: I) -> Self { - iter.fold(TrapdoorSum::zero(), |acc, cv| acc + cv) - } -} - -/// A sum of Sapling value commitments. -#[derive(Clone, Copy, Debug)] -pub struct CommitmentSum(jubjub::ExtendedPoint); - -impl CommitmentSum { - /// Initializes a sum of `ValueCommitment`s to zero. - pub fn zero() -> Self { - CommitmentSum(jubjub::ExtendedPoint::identity()) - } - - /// Transform this value commitment sum into the corresponding RedJubjub public key. - /// - /// This is public for access by `zcash_proofs`. - pub fn into_bvk>(self, value_balance: V) -> redjubjub::PublicKey { - let value: i64 = value_balance.into(); - - // Compute the absolute value. - let abs_value = match value.checked_abs() { - Some(v) => u64::try_from(v).expect("v is non-negative"), - None => 1u64 << 63, - }; - - // Construct the field representation of the signed value. - let value_balance = if value.is_negative() { - -jubjub::Scalar::from(abs_value) - } else { - jubjub::Scalar::from(abs_value) - }; - - // Subtract `value_balance` from the sum to get the final bvk. - let bvk = self.0 - VALUE_COMMITMENT_VALUE_GENERATOR * value_balance; - - redjubjub::PublicKey(bvk) - } -} - -impl Add<&ValueCommitment> for ValueCommitment { - type Output = CommitmentSum; - - fn add(self, rhs: &Self) -> Self::Output { - CommitmentSum(self.0 + rhs.0) - } -} - -impl Add<&ValueCommitment> for CommitmentSum { - type Output = CommitmentSum; - - fn add(self, rhs: &ValueCommitment) -> Self::Output { - CommitmentSum(self.0 + rhs.0) - } -} - -impl AddAssign<&ValueCommitment> for CommitmentSum { - fn add_assign(&mut self, rhs: &ValueCommitment) { - self.0 += rhs.0; - } -} - -impl Sub<&ValueCommitment> for ValueCommitment { - type Output = CommitmentSum; - - fn sub(self, rhs: &Self) -> Self::Output { - CommitmentSum(self.0 - rhs.0) - } -} - -impl SubAssign<&ValueCommitment> for CommitmentSum { - fn sub_assign(&mut self, rhs: &ValueCommitment) { - self.0 -= rhs.0; - } -} - -impl Sum for CommitmentSum { - fn sum>(iter: I) -> Self { - iter.fold(CommitmentSum::zero(), |acc, cv| acc + &cv) - } -} - -impl<'a> Sum<&'a ValueCommitment> for CommitmentSum { - fn sum>(iter: I) -> Self { - iter.fold(CommitmentSum::zero(), |acc, cv| acc + cv) - } -} 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 09209f29a9..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 958a64454e..209c2bd104 100644 --- a/zcash_primitives/src/transaction/builder.rs +++ b/zcash_primitives/src/transaction/builder.rs @@ -1,39 +1,47 @@ //! Structs for building transactions. -use std::cmp::Ordering; -use std::error; -use std::fmt; -use std::sync::mpsc::Sender; +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 rand::{rngs::OsRng, CryptoRng, RngCore}; +use crate::transaction::{ + fees::{ + transparent::{InputView, OutputView}, + FeeRule, + }, + Transaction, TxVersion, +}; -use crate::{ - consensus::{self, BlockHeight, BranchId}, - keys::OutgoingViewingKey, - legacy::TransparentAddress, - memo::MemoBytes, - sapling::{self, prover::TxProver, value::NoteValue, Diversifier, Note, PaymentAddress}, - transaction::{ - components::{ - amount::{Amount, BalanceError}, - sapling::{ - builder::{self as sapling_builder, SaplingBuilder, SaplingMetadata}, - fees as sapling_fees, - }, - transparent::{self, builder::TransparentBuilder}, - }, - fees::FeeRule, +#[cfg(feature = "std")] +use std::sync::mpsc::Sender; + +#[cfg(feature = "circuits")] +use { + crate::transaction::{ sighash::{signature_hash, SignableInput}, txid::TxIdDigester, - Transaction, TransactionData, TxVersion, Unauthorized, + TransactionData, Unauthorized, }, - zip32::ExtendedSpendingKey, + ::sapling::prover::{OutputProver, SpendProver}, + ::transparent::builder::TransparentSigningSet, + alloc::vec::Vec, }; #[cfg(feature = "transparent-inputs")] -use crate::transaction::components::transparent::TxOut; +use ::transparent::builder::TransparentInputInfo; + +#[cfg(not(feature = "transparent-inputs"))] +use core::convert::Infallible; -#[cfg(feature = "zfuture")] +#[cfg(zcash_unstable = "zfuture")] use crate::{ extensions::transparent::{ExtensionTxBuilder, ToPayload}, transaction::{ @@ -45,29 +53,59 @@ use crate::{ }, }; +use super::components::sapling::zip212_enforcement; + /// Since Blossom activation, the default transaction expiry delta should be 40 blocks. /// -const DEFAULT_TX_EXPIRY_DELTA: u32 = 40; +pub const DEFAULT_TX_EXPIRY_DELTA: u32 = 40; + +/// Errors that can occur during fee calculation. +#[derive(Debug)] +pub enum FeeError { + FeeRule(FE), + Bundle(&'static str), +} + +impl fmt::Display for FeeError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + FeeError::FeeRule(e) => write!(f, "An error occurred in fee calculation: {}", e), + FeeError::Bundle(b) => write!(f, "Bundle structure invalid in fee calculation: {}", b), + } + } +} /// Errors that can occur during transaction construction. -#[derive(Debug, PartialEq, Eq)] -pub enum Error { +#[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(Amount), + InsufficientFunds(ZatBalance), /// The transaction has inputs in excess of outputs and fees; the user must /// add a change output. - ChangeRequired(Amount), + ChangeRequired(ZatBalance), /// An error occurred in computing the fees for a transaction. - Fee(FeeError), + 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), + 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(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] TzeBuild(tze::builder::Error), } @@ -88,13 +126,25 @@ impl fmt::Display for Error { Error::Fee(e) => write!(f, "An error occurred in fee calculation: {}", e), Error::TransparentBuild(err) => err.fmt(f), Error::SaplingBuild(err) => err.fmt(f), - #[cfg(feature = "zfuture")] + 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), } } } -impl error::Error for Error {} +#[cfg(feature = "std")] +impl std::error::Error for Error {} impl From for Error { fn from(e: BalanceError) -> Self { @@ -102,6 +152,24 @@ impl From for Error { } } +impl From> for Error { + fn from(e: FeeError) -> Self { + Error::Fee(e) + } +} + +impl From for Error { + fn from(e: sapling::builder::Error) -> Self { + Error::SaplingBuild(e) + } +} + +impl From for Error { + fn from(e: orchard::builder::SpendError) -> Self { + Error::OrchardSpend(e) + } +} + /// Reports on the progress made by the builder towards building a transaction. pub struct Progress { /// The number of steps completed. @@ -110,11 +178,16 @@ pub struct Progress { end: Option, } -impl Progress { - pub fn new(cur: u32, end: Option) -> Self { - Self { cur, end } +impl From<(u32, u32)> for Progress { + fn from((cur, end): (u32, u32)) -> Self { + Self { + cur, + end: Some(end), + } } +} +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. @@ -131,22 +204,120 @@ impl Progress { } } +/// 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, +} + +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(), + )), + } + } + + /// Returns the Orchard bundle type and anchor for this configuration. + pub fn orchard_builder_config( + &self, + ) -> 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(), + )), + } + } +} + +/// 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, +} + +impl BuildResult { + /// Returns the transaction that was constructed by the builder. + pub fn transaction(&self) -> &Transaction { + &self.transaction + } + + /// 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 + } + + /// 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 + } +} + +/// 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, +} + +/// 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, R> { +pub struct Builder<'a, P, U: sapling::builder::ProverProgress> { params: P, - rng: R, + build_config: BuildConfig, target_height: BlockHeight, expiry_height: BlockHeight, + #[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))] + zip233_amount: Zatoshis, transparent_builder: TransparentBuilder, - sapling_builder: SaplingBuilder

{ - params: P, - anchor: Option, - target_height: BlockHeight, - value_balance: ValueSum, - spends: Vec, - outputs: Vec, -} - -#[derive(Clone)] -pub struct Unauthorized { - tx_metadata: SaplingMetadata, -} - -impl std::fmt::Debug for Unauthorized { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - write!(f, "Unauthorized") - } -} - -impl Authorization for Unauthorized { - type SpendProof = GrothProofBytes; - type OutputProof = GrothProofBytes; - type AuthSig = SpendDescriptionInfo; -} - -impl

SaplingBuilder

{ - pub fn new(params: P, target_height: BlockHeight) -> Self { - SaplingBuilder { - params, - anchor: None, - target_height, - value_balance: ValueSum::zero(), - spends: vec![], - outputs: vec![], - } - } - - /// Returns the list of Sapling inputs that will be consumed by the transaction being - /// constructed. - pub fn inputs(&self) -> &[impl fees::InputView<()>] { - &self.spends - } - - /// Returns the Sapling outputs that will be produced by the transaction being constructed - pub fn outputs(&self) -> &[impl fees::OutputView] { - &self.outputs - } - - /// Returns the number of outputs that will be present in the Sapling bundle built by - /// this builder. - /// - /// This may be larger than the number of outputs that have been added to the builder, - /// depending on whether padding is going to be applied. - pub(in crate::transaction) fn bundle_output_count(&self) -> usize { - // This matches the padding behaviour in `Self::build`. - match self.spends.len() { - 0 => self.outputs.len(), - _ => std::cmp::max(MIN_SHIELDED_OUTPUTS, self.outputs.len()), - } - } - - /// Returns the net value represented by the spends and outputs added to this builder, - /// or an error if the values added to this builder overflow the range of a Zcash - /// monetary amount. - fn try_value_balance(&self) -> Result { - self.value_balance - .try_into() - .map_err(|_| ()) - .and_then(Amount::from_i64) - .map_err(|()| Error::InvalidAmount) - } - - /// Returns the net value represented by the spends and outputs added to this builder. - pub fn value_balance(&self) -> Amount { - self.try_value_balance() - .expect("we check this when mutating self.value_balance") - } -} - -impl SaplingBuilder

{ - /// 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_spend( - &mut self, - mut rng: R, - extsk: ExtendedSpendingKey, - diversifier: Diversifier, - note: Note, - merkle_path: MerklePath, - ) -> Result<(), Error> { - // Consistency check: all anchors must equal the first one - let node = Node::from_cmu(¬e.cmu()); - if let Some(anchor) = self.anchor { - let path_root: bls12_381::Scalar = merkle_path.root(node).into(); - if path_root != anchor { - return Err(Error::AnchorMismatch); - } - } else { - self.anchor = Some(merkle_path.root(node).into()) - } - - let alpha = jubjub::Fr::random(&mut rng); - - self.value_balance = (self.value_balance + note.value()).ok_or(Error::InvalidAmount)?; - self.try_value_balance()?; - - self.spends.push(SpendDescriptionInfo { - extsk, - diversifier, - note, - alpha, - merkle_path, - }); - - Ok(()) - } - - /// Adds a Sapling address to send funds to. - #[allow(clippy::too_many_arguments)] - pub fn add_output( - &mut self, - mut rng: R, - ovk: Option, - to: PaymentAddress, - value: NoteValue, - memo: MemoBytes, - ) -> Result<(), Error> { - let output = SaplingOutputInfo::new_internal( - &self.params, - &mut rng, - self.target_height, - ovk, - to, - value, - memo, - ); - - self.value_balance = (self.value_balance - value).ok_or(Error::InvalidAddress)?; - self.try_value_balance()?; - - self.outputs.push(output); - - Ok(()) - } - - pub fn build( - self, - prover: &Pr, - ctx: &mut Pr::SaplingProvingContext, - mut rng: R, - target_height: BlockHeight, - progress_notifier: Option<&Sender>, - ) -> Result>, Error> { - let value_balance = self.try_value_balance()?; - - // Record initial positions of spends and outputs - let params = self.params; - let mut indexed_spends: Vec<_> = self.spends.into_iter().enumerate().collect(); - let mut indexed_outputs: Vec<_> = self - .outputs - .iter() - .enumerate() - .map(|(i, o)| Some((i, o))) - .collect(); - - // Set up the transaction metadata that will be used to record how - // inputs and outputs are shuffled. - let mut tx_metadata = SaplingMetadata::empty(); - tx_metadata.spend_indices.resize(indexed_spends.len(), 0); - tx_metadata.output_indices.resize(indexed_outputs.len(), 0); - - // Pad Sapling outputs - if !indexed_spends.is_empty() { - while indexed_outputs.len() < MIN_SHIELDED_OUTPUTS { - indexed_outputs.push(None); - } - } - - // Randomize order of inputs and outputs - indexed_spends.shuffle(&mut rng); - indexed_outputs.shuffle(&mut rng); - - // Keep track of the total number of steps computed - let total_progress = indexed_spends.len() as u32 + indexed_outputs.len() as u32; - let mut progress = 0u32; - - // Create Sapling SpendDescriptions - let shielded_spends: Vec> = if !indexed_spends.is_empty() { - let anchor = self - .anchor - .expect("Sapling anchor must be set if Sapling spends are present."); - - indexed_spends - .into_iter() - .enumerate() - .map(|(i, (pos, spend))| { - let proof_generation_key = spend.extsk.expsk.proof_generation_key(); - - let nullifier = spend.note.nf( - &proof_generation_key.to_viewing_key().nk, - u64::try_from(spend.merkle_path.position()) - .expect("Sapling note commitment tree position must fit into a u64"), - ); - - let (zkproof, cv, rk) = prover - .spend_proof( - ctx, - proof_generation_key, - spend.diversifier, - *spend.note.rseed(), - spend.alpha, - spend.note.value().inner(), - anchor, - spend.merkle_path.clone(), - ) - .map_err(|_| Error::SpendProof)?; - - // Record the post-randomized spend location - tx_metadata.spend_indices[pos] = i; - - // Update progress and send a notification on the channel - progress += 1; - if let Some(sender) = progress_notifier { - // If the send fails, we should ignore the error, not crash. - sender - .send(Progress::new(progress, Some(total_progress))) - .unwrap_or(()); - } - - Ok(SpendDescription { - cv, - anchor, - nullifier, - rk, - zkproof, - spend_auth_sig: spend, - }) - }) - .collect::, Error>>()? - } else { - vec![] - }; - - // Create Sapling OutputDescriptions - let shielded_outputs: Vec> = indexed_outputs - .into_iter() - .enumerate() - .map(|(i, output)| { - let result = if let Some((pos, output)) = output { - // Record the post-randomized output location - tx_metadata.output_indices[pos] = i; - - output.clone().build::(prover, ctx, &mut rng) - } else { - // This is a dummy output - let dummy_note = { - let payment_address = { - let mut diversifier = Diversifier([0; 11]); - loop { - rng.fill_bytes(&mut diversifier.0); - let dummy_ivk = SaplingIvk(jubjub::Fr::random(&mut rng)); - if let Some(addr) = dummy_ivk.to_payment_address(diversifier) { - break addr; - } - } - }; - - let rseed = - generate_random_rseed_internal(¶ms, target_height, &mut rng); - - Note::from_parts(payment_address, NoteValue::from_raw(0), rseed) - }; - - let esk = dummy_note.generate_or_derive_esk_internal(&mut rng); - let epk = esk.derive_public( - dummy_note - .recipient() - .diversifier() - .g_d() - .expect("checked at construction") - .into(), - ); - - let (zkproof, cv) = prover.output_proof( - ctx, - esk.0, - dummy_note.recipient(), - dummy_note.rcm(), - dummy_note.value().inner(), - ); - - let cmu = dummy_note.cmu(); - - let mut enc_ciphertext = [0u8; 580]; - let mut out_ciphertext = [0u8; 80]; - rng.fill_bytes(&mut enc_ciphertext[..]); - rng.fill_bytes(&mut out_ciphertext[..]); - - OutputDescription { - cv, - cmu, - ephemeral_key: epk.to_bytes(), - enc_ciphertext, - out_ciphertext, - zkproof, - } - }; - - // Update progress and send a notification on the channel - progress += 1; - if let Some(sender) = progress_notifier { - // If the send fails, we should ignore the error, not crash. - sender - .send(Progress::new(progress, Some(total_progress))) - .unwrap_or(()); - } - - result - }) - .collect(); - - let bundle = if shielded_spends.is_empty() && shielded_outputs.is_empty() { - None - } else { - Some(Bundle { - shielded_spends, - shielded_outputs, - value_balance, - authorization: Unauthorized { tx_metadata }, - }) - }; - - Ok(bundle) - } -} - -impl SpendDescription { - pub fn apply_signature(&self, spend_auth_sig: Signature) -> SpendDescription { - SpendDescription { - cv: self.cv.clone(), - anchor: self.anchor, - nullifier: self.nullifier, - rk: self.rk.clone(), - zkproof: self.zkproof, - spend_auth_sig, - } - } -} - -impl Bundle { - pub fn apply_signatures( - self, - prover: &Pr, - ctx: &mut Pr::SaplingProvingContext, - rng: &mut R, - sighash_bytes: &[u8; 32], - ) -> Result<(Bundle, SaplingMetadata), Error> { - let binding_sig = prover - .binding_sig(ctx, self.value_balance, sighash_bytes) - .map_err(|_| Error::BindingSig)?; - - Ok(( - Bundle { - shielded_spends: self - .shielded_spends - .iter() - .map(|spend| { - spend.apply_signature(spend_sig_internal( - PrivateKey(spend.spend_auth_sig.extsk.expsk.ask), - spend.spend_auth_sig.alpha, - sighash_bytes, - rng, - )) - }) - .collect(), - shielded_outputs: self.shielded_outputs, - value_balance: self.value_balance, - authorization: Authorized { binding_sig }, - }, - self.authorization.tx_metadata, - )) - } -} - -#[cfg(any(test, feature = "test-dependencies"))] -pub mod testing { - use proptest::collection::vec; - use proptest::prelude::*; - use rand::{rngs::StdRng, SeedableRng}; - - use crate::{ - consensus::{ - testing::{arb_branch_id, arb_height}, - TEST_NETWORK, - }, - sapling::{ - prover::mock::MockTxProver, - testing::{arb_node, arb_note}, - value::testing::arb_positive_note_value, - Diversifier, - }, - transaction::components::{ - amount::MAX_MONEY, - sapling::{Authorized, Bundle}, - }, - zip32::sapling::testing::arb_extended_spending_key, - }; - use incrementalmerkletree::{ - frontier::testing::arb_commitment_tree, witness::IncrementalWitness, - }; - - use super::SaplingBuilder; - - prop_compose! { - fn arb_bundle()(n_notes in 1..30usize)( - extsk in arb_extended_spending_key(), - spendable_notes in vec( - arb_positive_note_value(MAX_MONEY as u64 / 10000).prop_flat_map(arb_note), - n_notes - ), - commitment_trees in vec( - arb_commitment_tree::<_, _, 32>(n_notes, arb_node()).prop_map( - |t| IncrementalWitness::from_tree(t).path().unwrap() - ), - n_notes - ), - diversifiers in vec(prop::array::uniform11(any::()).prop_map(Diversifier), n_notes), - target_height in arb_branch_id().prop_flat_map(|b| arb_height(b, &TEST_NETWORK)), - rng_seed in prop::array::uniform32(any::()), - fake_sighash_bytes in prop::array::uniform32(any::()), - ) -> Bundle { - let mut builder = SaplingBuilder::new(TEST_NETWORK, target_height.unwrap()); - let mut rng = StdRng::from_seed(rng_seed); - - for ((note, path), diversifier) in spendable_notes.into_iter().zip(commitment_trees.into_iter()).zip(diversifiers.into_iter()) { - builder.add_spend( - &mut rng, - extsk.clone(), - diversifier, - note, - path - ).unwrap(); - } - - let prover = MockTxProver; - - let bundle = builder.build( - &prover, - &mut (), - &mut rng, - target_height.unwrap(), - None - ).unwrap().unwrap(); - - let (bundle, _) = bundle.apply_signatures( - &prover, - &mut (), - &mut rng, - &fake_sighash_bytes, - ).unwrap(); - - bundle - } - } -} diff --git a/zcash_primitives/src/transaction/components/sapling/fees.rs b/zcash_primitives/src/transaction/components/sapling/fees.rs deleted file mode 100644 index 10d72adb60..0000000000 --- a/zcash_primitives/src/transaction/components/sapling/fees.rs +++ /dev/null @@ -1,20 +0,0 @@ -//! Types related to computation of fees and change related to the Sapling components -//! of a transaction. - -use crate::transaction::components::amount::Amount; - -/// 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) -> Amount; -} - -/// 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) -> Amount; -} diff --git a/zcash_primitives/src/transaction/components/sprout.rs b/zcash_primitives/src/transaction/components/sprout.rs index b3e2370f71..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; @@ -22,10 +24,10 @@ impl Bundle { /// 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 { + pub fn value_balance(&self) -> Option { self.joinsplits .iter() - .try_fold(Amount::zero(), |total, js| total + js.net_value()) + .try_fold(ZatBalance::zero(), |total, js| total + js.net_value()) } } @@ -36,8 +38,8 @@ pub(crate) enum SproutProof { 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"), @@ -47,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], @@ -59,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( @@ -89,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"))?; @@ -97,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"))?; @@ -189,7 +191,7 @@ impl JsDescription { /// 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) -> Amount { + 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/builder.rs b/zcash_primitives/src/transaction/components/transparent/builder.rs deleted file mode 100644 index a65645fc93..0000000000 --- a/zcash_primitives/src/transaction/components/transparent/builder.rs +++ /dev/null @@ -1,321 +0,0 @@ -//! Types and functions for building transparent transaction components. - -use std::fmt; - -use crate::{ - legacy::{Script, TransparentAddress}, - transaction::{ - components::{ - amount::{Amount, BalanceError}, - transparent::{self, fees, Authorization, Authorized, Bundle, TxIn, TxOut}, - }, - sighash::TransparentAuthorizingContext, - OutPoint, - }, -}; - -#[cfg(feature = "transparent-inputs")] -use { - crate::transaction::{ - self as tx, - sighash::{signature_hash, SignableInput, SIGHASH_ALL}, - TransactionData, TxDigests, - }, - blake2b_simd::Hash as Blake2bHash, - sha2::Digest, -}; - -#[derive(Debug, PartialEq, Eq)] -pub enum Error { - InvalidAddress, - InvalidAmount, -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Error::InvalidAddress => write!(f, "Invalid address"), - Error::InvalidAmount => write!(f, "Invalid amount"), - } - } -} - -/// An uninhabited type that allows the type of [`TransparentBuilder::inputs`] -/// to resolve when the transparent-inputs feature is not turned on. -#[cfg(not(feature = "transparent-inputs"))] -enum InvalidTransparentInput {} - -#[cfg(not(feature = "transparent-inputs"))] -impl fees::InputView for InvalidTransparentInput { - fn outpoint(&self) -> &OutPoint { - panic!("transparent-inputs feature flag is not enabled."); - } - fn coin(&self) -> &TxOut { - panic!("transparent-inputs feature flag is not enabled."); - } -} - -#[cfg(feature = "transparent-inputs")] -#[derive(Debug, Clone)] -struct TransparentInputInfo { - sk: secp256k1::SecretKey, - pubkey: [u8; secp256k1::constants::PUBLIC_KEY_SIZE], - utxo: OutPoint, - coin: TxOut, -} - -#[cfg(feature = "transparent-inputs")] -impl fees::InputView for TransparentInputInfo { - fn outpoint(&self) -> &OutPoint { - &self.utxo - } - - fn coin(&self) -> &TxOut { - &self.coin - } -} - -pub struct TransparentBuilder { - #[cfg(feature = "transparent-inputs")] - secp: secp256k1::Secp256k1, - #[cfg(feature = "transparent-inputs")] - inputs: Vec, - vout: Vec, -} - -#[derive(Debug, Clone)] -pub struct Unauthorized { - #[cfg(feature = "transparent-inputs")] - secp: secp256k1::Secp256k1, - #[cfg(feature = "transparent-inputs")] - inputs: Vec, -} - -impl Authorization for Unauthorized { - type ScriptSig = (); -} - -impl TransparentBuilder { - /// Constructs a new TransparentBuilder - pub fn empty() -> Self { - TransparentBuilder { - #[cfg(feature = "transparent-inputs")] - secp: secp256k1::Secp256k1::gen_new(), - #[cfg(feature = "transparent-inputs")] - inputs: vec![], - vout: vec![], - } - } - - /// Returns the list of transparent inputs that will be consumed by the transaction being - /// constructed. - pub fn inputs(&self) -> &[impl fees::InputView] { - #[cfg(feature = "transparent-inputs")] - return &self.inputs; - - #[cfg(not(feature = "transparent-inputs"))] - { - let invalid: &[InvalidTransparentInput] = &[]; - invalid - } - } - - /// Returns the transparent outputs that will be produced by the transaction being constructed. - pub fn outputs(&self) -> &[impl fees::OutputView] { - &self.vout - } - - /// Adds a coin (the output of a previous transaction) to be spent to the transaction. - #[cfg(feature = "transparent-inputs")] - pub fn add_input( - &mut self, - sk: secp256k1::SecretKey, - utxo: OutPoint, - 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 ripemd::Ripemd160; - use sha2::Sha256; - - if hash[..] != Ripemd160::digest(Sha256::digest(pubkey))[..] { - return Err(Error::InvalidAddress); - } - } - _ => return Err(Error::InvalidAddress), - } - - self.inputs.push(TransparentInputInfo { - sk, - pubkey, - utxo, - coin, - }); - - Ok(()) - } - - pub fn add_output(&mut self, to: &TransparentAddress, value: Amount) -> Result<(), Error> { - if value.is_negative() { - return Err(Error::InvalidAmount); - } - - self.vout.push(TxOut { - value, - script_pubkey: to.script(), - }); - - Ok(()) - } - - pub fn value_balance(&self) -> Result { - #[cfg(feature = "transparent-inputs")] - let input_sum = self - .inputs - .iter() - .map(|input| input.coin.value) - .sum::>() - .ok_or(BalanceError::Overflow)?; - - #[cfg(not(feature = "transparent-inputs"))] - let input_sum = Amount::zero(); - - let output_sum = self - .vout - .iter() - .map(|vo| vo.value) - .sum::>() - .ok_or(BalanceError::Overflow)?; - - (input_sum - output_sum).ok_or(BalanceError::Underflow) - } - - pub fn build(self) -> Option> { - #[cfg(feature = "transparent-inputs")] - let vin: Vec> = self - .inputs - .iter() - .map(|i| TxIn::new(i.utxo.clone())) - .collect(); - - #[cfg(not(feature = "transparent-inputs"))] - let vin: Vec> = vec![]; - - if vin.is_empty() && self.vout.is_empty() { - None - } else { - Some(transparent::Bundle { - vin, - vout: self.vout, - authorization: Unauthorized { - #[cfg(feature = "transparent-inputs")] - secp: self.secp, - #[cfg(feature = "transparent-inputs")] - inputs: self.inputs, - }, - }) - } - } -} - -impl TxIn { - #[cfg(feature = "transparent-inputs")] - #[cfg_attr(docsrs, doc(cfg(feature = "transparent-inputs")))] - pub fn new(prevout: OutPoint) -> Self { - TxIn { - prevout, - script_sig: (), - sequence: std::u32::MAX, - } - } -} - -#[cfg(not(feature = "transparent-inputs"))] -impl TransparentAuthorizingContext for Unauthorized { - fn input_amounts(&self) -> Vec { - vec![] - } - - fn input_scriptpubkeys(&self) -> Vec

, - #[cfg(feature = "zfuture")] + sapling_builder: Option, + orchard_builder: Option, + #[cfg(zcash_unstable = "zfuture")] tze_builder: TzeBuilder<'a, TransactionData>, - #[cfg(not(feature = "zfuture"))] - tze_builder: std::marker::PhantomData<&'a ()>, - progress_notifier: Option>, + #[cfg(not(zcash_unstable = "zfuture"))] + tze_builder: core::marker::PhantomData<&'a ()>, + _progress_notifier: U, } -impl<'a, P, R> Builder<'a, P, R> { +impl Builder<'_, P, U> { /// Returns the network parameters that the builder has been configured for. pub fn params(&self) -> &P { &self.params @@ -159,148 +330,223 @@ impl<'a, P, R> Builder<'a, P, R> { /// Returns the set of transparent inputs currently committed to be consumed /// by the transaction. - pub fn transparent_inputs(&self) -> &[impl transparent::fees::InputView] { + #[cfg(feature = "transparent-inputs")] + pub fn transparent_inputs(&self) -> &[TransparentInputInfo] { self.transparent_builder.inputs() } /// Returns the set of transparent outputs currently set to be produced by /// the transaction. - pub fn transparent_outputs(&self) -> &[impl transparent::fees::OutputView] { + pub fn transparent_outputs(&self) -> &[TxOut] { self.transparent_builder.outputs() } /// Returns the set of Sapling inputs currently committed to be consumed /// by the transaction. - pub fn sapling_inputs(&self) -> &[impl sapling_fees::InputView<()>] { - self.sapling_builder.inputs() + pub fn sapling_inputs(&self) -> &[sapling::builder::SpendInfo] { + self.sapling_builder + .as_ref() + .map_or_else(|| &[][..], |b| b.inputs()) } /// Returns the set of Sapling outputs currently set to be produced by /// the transaction. - pub fn sapling_outputs(&self) -> &[impl sapling_fees::OutputView] { - self.sapling_builder.outputs() + pub fn sapling_outputs(&self) -> &[sapling::builder::OutputInfo] { + self.sapling_builder + .as_ref() + .map_or_else(|| &[][..], |b| b.outputs()) } } -impl<'a, P: consensus::Parameters> Builder<'a, P, OsRng> { +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 and the default OS random. + /// 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). - pub fn new(params: P, target_height: BlockHeight) -> Self { - Builder::new_with_rng(params, target_height, OsRng) - } -} + 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 + }; -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. - /// - /// # Default values - /// - /// The expiry height will be set to the given height plus the default transaction - /// expiry delta. - pub fn new_with_rng(params: P, target_height: BlockHeight, rng: R) -> Builder<'a, P, R> { - Self::new_internal(params, rng, target_height) - } -} + let sapling_builder = build_config + .sapling_builder_config() + .map(|(bundle_type, anchor)| { + sapling::builder::Builder::new( + zip212_enforcement(¶ms, target_height), + bundle_type, + anchor, + ) + }); -impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { - /// Common utility function for builder construction. - /// - /// WARNING: THIS MUST REMAIN PRIVATE AS IT ALLOWS CONSTRUCTION - /// OF BUILDERS WITH NON-CryptoRng RNGs - fn new_internal(params: P, rng: R, target_height: BlockHeight) -> Builder<'a, P, R> { Builder { - params: params.clone(), - rng, + params, + build_config, target_height, expiry_height: target_height + DEFAULT_TX_EXPIRY_DELTA, + #[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))] + zip233_amount: Zatoshis::ZERO, transparent_builder: TransparentBuilder::empty(), - sapling_builder: SaplingBuilder::new(params, target_height), - #[cfg(feature = "zfuture")] + sapling_builder, + orchard_builder, + #[cfg(zcash_unstable = "zfuture")] tze_builder: TzeBuilder::empty(), - #[cfg(not(feature = "zfuture"))] - tze_builder: std::marker::PhantomData, - progress_notifier: None, + #[cfg(not(zcash_unstable = "zfuture"))] + tze_builder: core::marker::PhantomData, + _progress_notifier: (), } } + /// Sets the notifier channel, where progress of building the transaction is sent. + /// + /// 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, + #[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))] + zip233_amount: self.zip233_amount, + transparent_builder: self.transparent_builder, + sapling_builder: self.sapling_builder, + orchard_builder: self.orchard_builder, + tze_builder: self.tze_builder, + _progress_notifier, + } + } +} + +impl Builder<'_, P, U> { + /// Adds an Orchard note to be spent in this bundle. + /// + /// 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), + memo.into_bytes(), + ) + .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: sapling::MerklePath, - ) -> Result<(), sapling_builder::Error> { - self.sapling_builder - .add_spend(&mut self.rng, extsk, diversifier, note, merkle_path) + ) -> Result<(), Error> { + if let Some(builder) = self.sapling_builder.as_mut() { + builder.add_spend(fvk, note, merkle_path)?; + Ok(()) + } else { + Err(Error::SaplingBuilderNotAvailable) + } } /// 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, + value: Zatoshis, memo: MemoBytes, - ) -> Result<(), sapling_builder::Error> { - if value.is_negative() { - return Err(sapling_builder::Error::InvalidAmount); - } - self.sapling_builder.add_output( - &mut self.rng, - ovk, - to, - NoteValue::from_raw(value.into()), - memo, - ) + ) -> Result<(), Error> { + self.sapling_builder + .as_mut() + .ok_or(Error::SaplingBuilderNotAvailable)? + .add_output( + ovk, + to, + sapling::value::NoteValue::from_raw(value.into()), + memo.into_bytes(), + ) + .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: transparent::OutPoint, + pubkey: secp256k1::PublicKey, + utxo: transparent::bundle::OutPoint, coin: TxOut, ) -> Result<(), transparent::builder::Error> { - self.transparent_builder.add_input(sk, utxo, coin) + 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, + value: Zatoshis, ) -> Result<(), transparent::builder::Error> { self.transparent_builder.add_output(to, value) } - /// Sets the notifier channel, where progress of building the transaction is sent. - /// - /// An update is sent after every 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. - pub fn with_progress_notifier(&mut self, progress_notifier: Sender) { - self.progress_notifier = Some(progress_notifier); - } - - /// Returns the sum of the transparent, Sapling, and TZE value balances. - fn value_balance(&self) -> Result { + /// Returns the sum of the transparent, Sapling, Orchard, zip233_amount and TZE value balances. + fn value_balance(&self) -> Result { let value_balances = [ self.transparent_builder.value_balance()?, - self.sapling_builder.value_balance(), - #[cfg(feature = "zfuture")] + 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(all(zcash_unstable = "nu7", feature = "zip-233"))] + ->::into(self.zip233_amount), + #[cfg(zcash_unstable = "zfuture")] self.tze_builder.value_balance()?, ]; @@ -310,59 +556,177 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { .ok_or(BalanceError::Overflow) } - /// Builds a transaction from the configured spends and outputs. + /// Reports the calculated fee given the specified fee rule. /// - /// Upon success, returns a tuple containing the final transaction, and the - /// [`SaplingMetadata`] generated during the build process. - pub fn build( - self, - prover: &impl TxProver, - fee_rule: &FR, - ) -> Result<(Transaction, SaplingMetadata), Error> { - let fee = fee_rule + /// 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, - self.transparent_builder.inputs(), - self.transparent_builder.outputs(), - self.sapling_builder.inputs().len(), - self.sapling_builder.bundle_output_count(), + 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(Error::Fee)?; - self.build_internal(prover, fee) + .map_err(FeeError::FeeRule) } - /// Builds a transaction from the configured spends and outputs. - /// - /// Upon success, returns a tuple containing the final transaction, and the - /// [`SaplingMetadata`] generated during the build process. - #[cfg(feature = "zfuture")] - pub fn build_zfuture( - self, - prover: &impl TxProver, + #[cfg(zcash_unstable = "zfuture")] + pub fn get_fee_zfuture( + &self, fee_rule: &FR, - ) -> Result<(Transaction, SaplingMetadata), Error> { - let fee = fee_rule + ) -> 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, - self.transparent_builder.inputs(), - self.transparent_builder.outputs(), - self.sapling_builder.inputs().len(), - self.sapling_builder.bundle_output_count(), + 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(Error::Fee)?; + .map_err(FeeError::FeeRule) + } + + #[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))] + pub fn set_zip233_amount(&mut self, zip233_amount: Zatoshis) { + self.zip233_amount = zip233_amount; + } + + /// Builds a transaction from the configured spends and outputs. + /// + /// Upon success, returns a tuple containing the final transaction, and the + /// [`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, + ) + } - self.build_internal(prover, fee) + /// Builds a transaction from the configured spends and outputs. + /// + /// 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, + ) } - fn build_internal( + #[allow(clippy::too_many_arguments)] + #[cfg(feature = "circuits")] + fn build_internal( self, - prover: &impl TxProver, - fee: Amount, - ) -> Result<(Transaction, SaplingMetadata), Error> { + 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 @@ -373,9 +737,10 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { // // After fees are accounted for, the value balance of the transaction must be zero. - let balance_after_fees = (self.value_balance()? - fee).ok_or(BalanceError::Underflow)?; + let balance_after_fees = + (self.value_balance()? - fee.into()).ok_or(BalanceError::Underflow)?; - match balance_after_fees.cmp(&Amount::zero()) { + match balance_after_fees.cmp(&ZatBalance::zero()) { Ordering::Less => { return Err(Error::InsufficientFunds(-balance_after_fees)); } @@ -387,20 +752,51 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { let transparent_bundle = self.transparent_builder.build(); - let mut rng = self.rng; - let mut ctx = prover.new_sapling_proving_context(); - let sapling_bundle = self + let (sapling_bundle, sapling_meta) = match self .sapling_builder - .build( - prover, - &mut ctx, - &mut rng, - self.target_height, - self.progress_notifier.as_ref(), - ) - .map_err(Error::SaplingBuild)?; + .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()), + }; + + 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()), + }; - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] let (tze_bundle, tze_signers) = self.tze_builder.build(); let unauthed_tx: TransactionData = TransactionData { @@ -408,11 +804,13 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { consensus_branch_id: BranchId::for_height(&self.params, self.target_height), lock_time: 0, expiry_height: self.expiry_height, + #[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))] + zip233_amount: self.zip233_amount, transparent_bundle, sprout_bundle: None, sapling_bundle, - orchard_bundle: None, - #[cfg(feature = "zfuture")] + orchard_bundle, + #[cfg(zcash_unstable = "zfuture")] tze_bundle, }; @@ -421,16 +819,26 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { // let txid_parts = unauthed_tx.digest(TxIdDigester); - let transparent_bundle = unauthed_tx.transparent_bundle.clone().map(|b| { - b.apply_signatures( - #[cfg(feature = "transparent-inputs")] - &unauthed_tx, - #[cfg(feature = "transparent-inputs")] - &txid_parts, - ) - }); + 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(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] let tze_bundle = unauthed_tx .tze_bundle .clone() @@ -444,40 +852,139 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { let shielded_sig_commitment = signature_hash(&unauthed_tx, &SignableInput::Shielded, &txid_parts); - let (sapling_bundle, tx_metadata) = match unauthed_tx + 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.apply_signatures(prover, &mut ctx, &mut rng, shielded_sig_commitment.as_ref()) + 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::SaplingBuild)? - { - Some((bundle, meta)) => (Some(bundle), meta), - None => (None, SaplingMetadata::empty()), - }; + .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, + #[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))] + zip233_amount: self.zip233_amount, transparent_bundle, sprout_bundle: unauthed_tx.sprout_bundle, sapling_bundle, - orchard_bundle: None, - #[cfg(feature = "zfuture")] + orchard_bundle, + #[cfg(zcash_unstable = "zfuture")] tze_bundle, }; // The unwrap() here is safe because the txid hashing // of freeze() should be infalliable. - Ok((authorized_tx.freeze().unwrap(), tx_metadata)) + Ok(BuildResult { + transaction: authorized_tx.freeze().unwrap(), + sapling_meta, + orchard_meta, + }) + } + + /// 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); + + // determine transaction version + let version = TxVersion::suggested_for_branch(consensus_branch_id); + + let consensus_branch_id = BranchId::for_height(&self.params, self.target_height); + + // + // Consistency checks + // + + // 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)?; + + 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 => (), + }; + + let transparent_bundle = self.transparent_builder.build_for_pczt(); + + 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()), + }; + + 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(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 = tze::builder::Error; @@ -501,111 +1008,109 @@ impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> ExtensionTxBuilder<'a fn add_tze_output( &mut self, extension_id: u32, - value: Amount, + value: Zatoshis, guarded_by: &G, ) -> Result<(), Self::BuildError> { - self.tze_builder.add_output(extension_id, value, guarded_by) + self.tze_builder.add_output(extension_id, value, guarded_by); + Ok(()) } } #[cfg(any(test, feature = "test-dependencies"))] mod testing { use rand::RngCore; - use std::convert::Infallible; - - use super::{Builder, Error, SaplingMetadata}; - use crate::{ - consensus::{self, BlockHeight}, - sapling::prover::mock::MockTxProver, - transaction::{fees::fixed, Transaction}, - }; - - 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. - /// - /// 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_internal(params, rng, height) - } + 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() + } + + fn next_u64(&mut self) -> u64 { + self.0.next_u64() + } + + 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) + } + } - pub fn mock_build(self) -> Result<(Transaction, SaplingMetadata), Error> { - #[allow(deprecated)] - self.build(&MockTxProver, &fixed::FeeRule::standard()) + self.build( + transparent_signing_set, + sapling_extsks, + orchard_saks, + FakeCryptoRng(rng), + &MockSpendProver, + &MockOutputProver, + #[allow(deprecated)] + &zip317::FeeRule::standard(), + ) } } } #[cfg(test)] mod tests { + use core::convert::Infallible; + + use assert_matches::assert_matches; use ff::Field; use incrementalmerkletree::{frontier::CommitmentTree, witness::IncrementalWitness}; use rand_core::OsRng; - use crate::{ + 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}, - legacy::TransparentAddress, memo::MemoBytes, - sapling::{Node, Rseed}, - transaction::components::{ - amount::Amount, - sapling::builder::{self as sapling_builder}, - transparent::builder::{self as transparent_builder}, - }, - zip32::ExtendedSpendingKey, + value::{BalanceError, ZatBalance, Zatoshis}, }; - use super::{Builder, Error}; - - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] #[cfg(feature = "transparent-inputs")] use super::TzeBuilder; #[cfg(feature = "transparent-inputs")] - use crate::{ - legacy::keys::{AccountPrivKey, IncomingViewingKey}, - transaction::{ - builder::{SaplingBuilder, DEFAULT_TX_EXPIRY_DELTA}, - OutPoint, TxOut, - }, + use { + crate::transaction::{builder::DEFAULT_TX_EXPIRY_DELTA, OutPoint, TxOut}, + ::transparent::keys::{AccountPrivKey, IncomingViewingKey}, zip32::AccountId, }; - #[test] - fn fails_on_negative_output() { - let extsk = ExtendedSpendingKey::master(&[]); - let dfvk = extsk.to_diversifiable_full_viewing_key(); - let ovk = dfvk.fvk().ovk; - let to = dfvk.default_address().1; - - let sapling_activation_height = TEST_NETWORK - .activation_height(NetworkUpgrade::Sapling) - .unwrap(); - - let mut builder = Builder::new(TEST_NETWORK, sapling_activation_height); - assert_eq!( - builder.add_sapling_output( - Some(ovk), - to, - Amount::from_i64(-1).unwrap(), - MemoBytes::empty() - ), - Err(sapling_builder::Error::InvalidAmount) - ); - } - // 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, TransparentBuilder}; + use ::transparent::{builder::TransparentSigningSet, keys::NonHardenedChildIndex}; + use zcash_protocol::consensus::NetworkUpgrade; let sapling_activation_height = TEST_NETWORK .activation_height(NetworkUpgrade::Sapling) @@ -614,48 +1119,57 @@ mod tests { // Create a builder with 0 fee, so we can construct t outputs let mut builder = builder::Builder { params: TEST_NETWORK, - rng: OsRng, + 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, + #[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))] + zip233_amount: Zatoshis::ZERO, transparent_builder: TransparentBuilder::empty(), - sapling_builder: SaplingBuilder::new(TEST_NETWORK, sapling_activation_height), - #[cfg(feature = "zfuture")] + sapling_builder: None, + #[cfg(zcash_unstable = "zfuture")] tze_builder: TzeBuilder::empty(), - #[cfg(not(feature = "zfuture"))] - tze_builder: std::marker::PhantomData, - progress_notifier: None, + #[cfg(not(zcash_unstable = "zfuture"))] + tze_builder: core::marker::PhantomData, + _progress_notifier: (), + orchard_builder: None, }; - let tsk = AccountPrivKey::from_seed(&TEST_NETWORK, &[0u8; 32], AccountId::from(0)).unwrap(); + 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: Amount::from_u64(50000).unwrap(), + value: Zatoshis::const_from_u64(50000), script_pubkey: tsk .to_account_pubkey() .derive_external_ivk() .unwrap() - .derive_address(0) + .derive_address(NonHardenedChildIndex::ZERO) .unwrap() .script(), }; builder - .add_transparent_input( - tsk.derive_external_secret_key(0).unwrap(), - OutPoint::new([0u8; 32], 1), - prev_coin, - ) + .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::from_u64(40000).unwrap(), + &TransparentAddress::PublicKeyHash([0; 20]), + Zatoshis::const_from_u64(40000), ) .unwrap(); - let (tx, _) = builder.mock_build().unwrap(); + let res = builder + .mock_build(&transparent_signing_set, &[], &[], OsRng) + .unwrap(); // No binding signature, because only t input and outputs - assert!(tx.sapling_bundle.is_none()); + assert!(res.transaction().sapling_bundle.is_none()); } #[test] @@ -666,50 +1180,42 @@ mod tests { let mut rng = OsRng; - let note1 = to.create_note(50000, Rseed::BeforeZip212(jubjub::Fr::random(&mut rng))); + 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 tx_height = TEST_NETWORK .activation_height(NetworkUpgrade::Sapling) .unwrap(); - let mut builder = Builder::new(TEST_NETWORK, tx_height); + + 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::from_u64(40000).unwrap(), + &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.mock_build(), - Err(Error::SaplingBuild(sapling_builder::Error::BindingSig)) - ); - } - - #[test] - fn fails_on_negative_transparent_output() { - let tx_height = TEST_NETWORK - .activation_height(NetworkUpgrade::Sapling) + // A binding signature (and bundle) is present because there is a Sapling spend. + let res = builder + .mock_build(&TransparentSigningSet::new(), &[extsk], &[], OsRng) .unwrap(); - let mut builder = Builder::new(TEST_NETWORK, tx_height); - assert_eq!( - builder.add_transparent_output( - &TransparentAddress::PublicKey([0; 20]), - Amount::from_i64(-1).unwrap(), - ), - Err(transparent_builder::Error::InvalidAmount) - ); + assert!(res.transaction().sapling_bundle().is_some()); } #[test] @@ -727,10 +1233,14 @@ mod tests { // Fails with no inputs or outputs // 0.0001 t-ZEC fee { - let builder = Builder::new(TEST_NETWORK, tx_height); - assert_eq!( - builder.mock_build(), - Err(Error::InsufficientFunds(MINIMUM_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() ); } @@ -738,124 +1248,254 @@ mod tests { 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.0001 t-ZEC fee { - let mut builder = Builder::new(TEST_NETWORK, tx_height); + 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( + .add_sapling_output::( ovk, to, - Amount::from_u64(50000).unwrap(), + Zatoshis::const_from_u64(50000), MemoBytes::empty(), ) .unwrap(); - assert_eq!( - builder.mock_build(), - Err(Error::InsufficientFunds( - (Amount::from_i64(50000).unwrap() + MINIMUM_FEE).unwrap() - )) + 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.0001 t-ZEC fee { - let mut builder = Builder::new(TEST_NETWORK, tx_height); + 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.mock_build(), - Err(Error::InsufficientFunds( - (Amount::from_i64(50000).unwrap() + MINIMUM_FEE).unwrap() - )) + 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 burn + // 0.0005 burned, 0.0001 t-ZEC fee + #[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))] + { + 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.set_zip233_amount(Zatoshis::const_from_u64(50000)); + + 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(59999, Rseed::BeforeZip212(jubjub::Fr::random(&mut rng))); + 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.clone()); + 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.0001 t-ZEC fee, 0.00059999 z-ZEC in + // 0.0003 z-ZEC out, 0.00015 t-ZEC out, 0.0001 t-ZEC fee, 0.00059999 z-ZEC in { - let mut builder = Builder::new(TEST_NETWORK, tx_height); + 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( + .add_sapling_output::( ovk, to, - Amount::from_u64(30000).unwrap(), + 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.mock_build(), - Err(Error::InsufficientFunds(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) + ); + } + + // Fail if there is insufficient input + // 0.0003 z-ZEC out, 0.00005 t-ZEC out, 0.0001 burned, 0.0001 t-ZEC fee, 0.00059999 z-ZEC in + #[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))] + { + 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::( + dfvk.fvk().clone(), + note1.clone(), + witness1.path().unwrap(), + ) + .unwrap(); + builder + .add_sapling_output::( + ovk, + to, + Zatoshis::const_from_u64(30000), + MemoBytes::empty(), + ) + .unwrap(); + builder + .add_transparent_output( + &TransparentAddress::PublicKeyHash([0; 20]), + Zatoshis::const_from_u64(5000), + ) + .unwrap(); + builder.set_zip233_amount(Zatoshis::const_from_u64(10000)); + 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))); + 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 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::( + dfvk.fvk().clone(), + note1.clone(), + witness1.path().unwrap(), + ) + .unwrap(); + builder + .add_sapling_spend::( + dfvk.fvk().clone(), + note2.clone(), + witness2.path().unwrap(), + ) + .unwrap(); + builder + .add_sapling_output::( + ovk, + to, + Zatoshis::const_from_u64(30000), + MemoBytes::empty(), + ) + .unwrap(); + builder + .add_transparent_output( + &TransparentAddress::PublicKeyHash([0; 20]), + Zatoshis::const_from_u64(15000), + ) + .unwrap(); + let res = builder + .mock_build(&TransparentSigningSet::new(), extsks, &[], OsRng) + .unwrap(); + assert_eq!( + res.transaction() + .fee_paid(|_| Err(BalanceError::Overflow)) + .unwrap(), + ZatBalance::const_from_i64(15_000) + ); + } + + // Succeeds if there is sufficient input + // 0.0003 z-ZEC out, 0.00005 t-ZEC out, 0.0001 burned, 0.00015 t-ZEC fee, 0.0006 z-ZEC in + #[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))] { - let mut builder = Builder::new(TEST_NETWORK, tx_height); + 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( + .add_sapling_output::( ovk, to, - Amount::from_u64(30000).unwrap(), + 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(5000), ) .unwrap(); + builder.set_zip233_amount(Zatoshis::const_from_u64(10000)); + let res = builder + .mock_build(&TransparentSigningSet::new(), extsks, &[], OsRng) + .unwrap(); assert_eq!( - builder.mock_build(), - Err(Error::SaplingBuild(sapling_builder::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 759d85c73b..944f45e9d9 100644 --- a/zcash_primitives/src/transaction/components.rs +++ b/zcash_primitives/src/transaction/components.rs @@ -1,19 +1,60 @@ //! 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::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 diff --git a/zcash_primitives/src/transaction/components/amount.rs b/zcash_primitives/src/transaction/components/amount.rs deleted file mode 100644 index 83b57c38d9..0000000000 --- a/zcash_primitives/src/transaction/components/amount.rs +++ /dev/null @@ -1,397 +0,0 @@ -use std::convert::TryFrom; -use std::iter::Sum; -use std::ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign}; - -use memuse::DynamicUsage; -use orchard::value as orchard; - -pub const COIN: i64 = 1_0000_0000; -pub const MAX_MONEY: i64 = 21_000_000 * COIN; - -#[deprecated( - since = "0.12.0", - note = "To calculate the ZIP 317 fee, use `transaction::fees::zip317::FeeRule`. -For a constant representing the minimum ZIP 317 fee, use `transaction::fees::zip317::MINIMUM_FEE`. -For the constant amount 1000 zatoshis, use `Amount::const_from_i64(1000)`." -)] -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); - -memuse::impl_no_dynamic_usage!(Amount); - -impl Amount { - /// Returns a zero-valued Amount. - pub const fn zero() -> Self { - Amount(0) - } - - /// Creates a constant Amount from an i64. - /// - /// Panics: if the amount is outside the range `{-MAX_MONEY..MAX_MONEY}`. - pub const fn const_from_i64(amount: i64) -> Self { - assert!(-MAX_MONEY <= amount && amount <= MAX_MONEY); // contains is not const - Amount(amount) - } - - /// 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..=MAX_MONEY).contains(&amount) { - 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() - } - - pub fn sum>(values: I) -> Option { - let mut result = Amount::zero(); - for value in values { - result = (result + value)?; - } - Some(result) - } -} - -impl TryFrom for Amount { - type Error = (); - - fn try_from(value: i64) -> Result { - Amount::from_i64(value) - } -} - -impl From for i64 { - fn from(amount: Amount) -> i64 { - amount.0 - } -} - -impl From<&Amount> 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 = Option; - - fn add(self, rhs: Amount) -> Option { - Amount::from_i64(self.0 + rhs.0).ok() - } -} - -impl Add for Option { - type Output = Self; - - fn add(self, rhs: Amount) -> Option { - self.and_then(|lhs| lhs + rhs) - } -} - -impl AddAssign for Amount { - fn add_assign(&mut self, rhs: Amount) { - *self = (*self + rhs).expect("Addition must produce a valid amount value.") - } -} - -impl Sub for Amount { - type Output = Option; - - fn sub(self, rhs: Amount) -> Option { - Amount::from_i64(self.0 - rhs.0).ok() - } -} - -impl Sub for Option { - type Output = Self; - - fn sub(self, rhs: Amount) -> Option { - self.and_then(|lhs| lhs - rhs) - } -} - -impl SubAssign for Amount { - fn sub_assign(&mut self, rhs: Amount) { - *self = (*self - rhs).expect("Subtraction must produce a valid amount value.") - } -} - -impl Sum for Option { - fn sum>(iter: I) -> Self { - iter.fold(Some(Amount::zero()), |acc, a| acc? + a) - } -} - -impl<'a> Sum<&'a Amount> for Option { - fn sum>(iter: I) -> Self { - iter.fold(Some(Amount::zero()), |acc, a| acc? + *a) - } -} - -impl Neg for Amount { - type Output = Self; - - fn neg(self) -> Self { - Amount(-self.0) - } -} - -impl Mul for Amount { - type Output = Option; - - fn mul(self, rhs: usize) -> Option { - let rhs: i64 = rhs.try_into().ok()?; - self.0 - .checked_mul(rhs) - .and_then(|i| Amount::try_from(i).ok()) - } -} - -impl TryFrom for Amount { - type Error = (); - - fn try_from(v: orchard::ValueSum) -> Result { - i64::try_from(v).map_err(|_| ()).and_then(Amount::try_from) - } -} - -/// A type-safe representation of some nonnegative amount of Zcash. -/// -/// A NonNegativeAmount 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 NonNegativeAmount(Amount); - -impl NonNegativeAmount { - /// Creates a NonNegativeAmount from a u64. - /// - /// Returns an error if the amount is outside the range `{0..MAX_MONEY}`. - pub fn from_u64(amount: u64) -> Result { - Amount::from_u64(amount).map(NonNegativeAmount) - } - - /// Creates a NonNegativeAmount from an i64. - /// - /// Returns an error if the amount is outside the range `{0..MAX_MONEY}`. - pub fn from_nonnegative_i64(amount: i64) -> Result { - Amount::from_nonnegative_i64(amount).map(NonNegativeAmount) - } -} - -impl From for Amount { - fn from(n: NonNegativeAmount) -> Self { - n.0 - } -} - -/// 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, -} - -impl std::fmt::Display for BalanceError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match &self { - BalanceError::Overflow => { - write!( - f, - "Amount addition resulted in a value outside the valid range." - ) - } - BalanceError::Underflow => write!( - f, - "Amount subtraction resulted in a value outside the valid range." - ), - } - } -} - -#[cfg(any(test, feature = "test-dependencies"))] -pub mod testing { - use proptest::prelude::prop_compose; - - use super::{Amount, MAX_MONEY}; - - prop_compose! { - pub fn arb_amount()(amt in -MAX_MONEY..MAX_MONEY) -> Amount { - Amount::from_i64(amt).unwrap() - } - } - - prop_compose! { - pub fn arb_nonnegative_amount()(amt in 0i64..MAX_MONEY) -> Amount { - Amount::from_i64(amt).unwrap() - } - } - - prop_compose! { - pub fn arb_positive_amount()(amt in 1i64..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] - fn add_overflow() { - let v = Amount(MAX_MONEY); - assert_eq!(v + Amount(1), None) - } - - #[test] - #[should_panic] - fn add_assign_panics_on_overflow() { - let mut a = Amount(MAX_MONEY); - a += Amount(1); - } - - #[test] - fn sub_underflow() { - let v = Amount(-MAX_MONEY); - assert_eq!(v - Amount(1), None) - } - - #[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 index 440e8ab8b4..a8f2361d9e 100644 --- a/zcash_primitives/src/transaction/components/orchard.rs +++ b/zcash_primitives/src/transaction/components/orchard.rs @@ -1,9 +1,12 @@ -/// Functions for parsing & serialization of Orchard transaction components. -use std::convert::TryFrom; -use std::io::{self, Read, Write}; +//! 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 byteorder::{ReadBytesExt, WriteBytesExt}; use nonempty::NonEmpty; + use orchard::{ bundle::{Authorization, Authorized, Flags}, note::{ExtractedNoteCommitment, Nullifier, TransmittedNoteCiphertext}, @@ -12,22 +15,14 @@ use orchard::{ Action, Anchor, }; use zcash_encoding::{Array, CompactSize, Vector}; +use zcash_protocol::value::ZatBalance; -use super::Amount; 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); -/// Marker for a bundle with no proofs or signatures. -#[derive(Debug)] -pub struct Unauthorized; - -impl Authorization for Unauthorized { - type SpendAuth = (); -} - pub trait MapAuth { fn map_spend_auth(&self, s: A::SpendAuth) -> B::SpendAuth; fn map_authorization(&self, a: A) -> B; @@ -55,7 +50,7 @@ impl MapAuth for () { /// Reads an [`orchard::Bundle`] from a v5 transaction format. pub fn read_v5_bundle( mut reader: R, -) -> io::Result>> { +) -> 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() { @@ -89,6 +84,13 @@ pub fn read_v5_bundle( } } +#[cfg(any(zcash_unstable = "zfuture", zcash_unstable = "nu7"))] +pub fn read_v6_bundle( + reader: R, +) -> io::Result>> { + read_v5_bundle(reader) +} + pub fn read_value_commitment(mut reader: R) -> io::Result { let mut bytes = [0u8; 32]; reader.read_exact(&mut bytes)?; @@ -97,7 +99,7 @@ pub fn read_value_commitment(mut reader: R) -> io::Result(mut reader: R) -> io::Result { if nullifier_ctopt.is_none().into() { Err(io::Error::new( io::ErrorKind::InvalidInput, - "invalid Pallas point for nullifier".to_owned(), + "invalid Pallas point for nullifier", )) } else { Ok(nullifier_ctopt.unwrap()) @@ -121,12 +123,8 @@ pub fn read_nullifier(mut reader: R) -> io::Result { 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".to_owned(), - ) - }) + VerificationKey::try_from(bytes) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid verification key")) } pub fn read_cmx(mut reader: R) -> io::Result { @@ -136,7 +134,7 @@ pub fn read_cmx(mut reader: R) -> io::Result { Option::from(cmx).ok_or_else(|| { io::Error::new( io::ErrorKind::InvalidInput, - "invalid Pallas base for field cmx".to_owned(), + "invalid Pallas base for field cmx", ) }) } @@ -175,23 +173,15 @@ pub fn read_action_without_auth(mut reader: R) -> io::Result 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".to_owned(), - ) - }) + 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".to_owned(), - ) - }) + 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> { @@ -202,7 +192,7 @@ pub fn read_signature(mut reader: R) -> io::Result( - bundle: Option<&orchard::Bundle>, + bundle: Option<&orchard::Bundle>, mut writer: W, ) -> io::Result<()> { if let Some(bundle) = &bundle { @@ -216,7 +206,7 @@ pub fn write_v5_bundle( Vector::write( &mut writer, bundle.authorization().proof().as_ref(), - |w, b| w.write_u8(*b), + |w, b| w.write_all(&[*b]), )?; Array::write( &mut writer, @@ -233,6 +223,14 @@ pub fn write_v5_bundle( Ok(()) } +#[cfg(any(zcash_unstable = "zfuture", zcash_unstable = "nu7"))] +pub fn write_v6_bundle( + bundle: Option<&orchard::Bundle>, + writer: W, +) -> io::Result<()> { + write_v5_bundle(bundle, writer) +} + pub fn write_value_commitment(mut writer: W, cv: &ValueCommitment) -> io::Result<()> { writer.write_all(&cv.to_bytes()) } @@ -281,17 +279,15 @@ pub mod testing { testing::{self as t_orch}, Authorized, Bundle, }; + use zcash_protocol::value::{testing::arb_zat_balance, ZatBalance}; - use crate::transaction::{ - components::amount::{testing::arb_amount, Amount}, - TxVersion, - }; + use crate::transaction::TxVersion; prop_compose! { pub fn arb_bundle(n_actions: usize)( - orchard_value_balance in arb_amount(), + orchard_value_balance in arb_zat_balance(), bundle in t_orch::arb_bundle(n_actions) - ) -> Bundle { + ) -> 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() @@ -300,7 +296,7 @@ pub mod testing { pub fn arb_bundle_for_version( v: TxVersion, - ) -> impl Strategy>> { + ) -> impl Strategy>> { if v.has_orchard() { Strategy::boxed((1usize..100).prop_flat_map(|n| prop::option::of(arb_bundle(n)))) } else { diff --git a/zcash_primitives/src/transaction/components/sapling.rs b/zcash_primitives/src/transaction/components/sapling.rs index 149bfdbd4c..943b11a387 100644 --- a/zcash_primitives/src/transaction/components/sapling.rs +++ b/zcash_primitives/src/transaction/components/sapling.rs @@ -1,296 +1,89 @@ -use core::fmt::Debug; - +use alloc::vec::Vec; +use core2::io::{self, Read, Write}; use ff::PrimeField; -use memuse::DynamicUsage; - -use std::io::{self, Read, Write}; - -use zcash_note_encryption::{ - EphemeralKeyBytes, ShieldedOutput, COMPACT_NOTE_SIZE, ENC_CIPHERTEXT_SIZE, -}; -use crate::{ - consensus, - sapling::{ - note::ExtractedNoteCommitment, - note_encryption::SaplingDomain, - redjubjub::{self, PublicKey, Signature}, - value::ValueCommitment, - 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 super::{amount::Amount, GROTH_PROOF_SIZE}; - -pub type GrothProofBytes = [u8; GROTH_PROOF_SIZE]; - -pub mod builder; -pub mod fees; - -/// Defines the authorization type of a Sapling bundle. -pub trait Authorization: Debug { - type SpendProof: Clone + Debug; - type OutputProof: Clone + Debug; - type AuthSig: Clone + Debug; -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct Unproven; +use super::GROTH_PROOF_SIZE; +use crate::transaction::Transaction; -impl Authorization for Unproven { - type SpendProof = (); - type OutputProof = (); - type AuthSig = (); -} +/// 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; -/// Authorizing data for a bundle of Sapling spends and outputs, ready to be committed to -/// the ledger. -#[derive(Debug, Copy, Clone)] -pub struct Authorized { - pub binding_sig: redjubjub::Signature, -} - -impl Authorization for Authorized { - type SpendProof = GrothProofBytes; - type OutputProof = GrothProofBytes; - type AuthSig = redjubjub::Signature; + if height < grace_period_end_height { + Zip212Enforcement::GracePeriod + } else { + Zip212Enforcement::On + } + } else { + Zip212Enforcement::Off + } } +/// 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(&self, p: A::SpendProof) -> B::SpendProof; - fn map_output_proof(&self, p: A::OutputProof) -> B::OutputProof; - fn map_auth_sig(&self, s: A::AuthSig) -> B::AuthSig; - fn map_authorization(&self, a: A) -> B; + 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; } /// 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. +/// authorization of a subset of a transaction's bundles. /// /// [`TransactionData::map_authorization`]: crate::transaction::TransactionData::map_authorization impl MapAuth for () { fn map_spend_proof( - &self, + &mut self, p: ::SpendProof, ) -> ::SpendProof { p } fn map_output_proof( - &self, + &mut self, p: ::OutputProof, ) -> ::OutputProof { p } fn map_auth_sig( - &self, + &mut self, s: ::AuthSig, ) -> ::AuthSig { s } - fn map_authorization(&self, a: Authorized) -> Authorized { + fn map_authorization(&mut self, a: Authorized) -> Authorized { a } } -#[derive(Debug, Clone)] -pub struct Bundle { - shielded_spends: Vec>, - shielded_outputs: Vec>, - value_balance: Amount, - authorization: A, -} - -impl Bundle { - /// Constructs a `Bundle` from its constituent parts. - #[cfg(feature = "temporary-zcashd")] - pub fn temporary_zcashd_from_parts( - shielded_spends: Vec>, - shielded_outputs: Vec>, - value_balance: Amount, - authorization: A, - ) -> Self { - Self::from_parts( - shielded_spends, - shielded_outputs, - value_balance, - authorization, - ) - } - - /// Constructs a `Bundle` from its constituent parts. - pub(crate) fn from_parts( - shielded_spends: Vec>, - shielded_outputs: Vec>, - value_balance: Amount, - authorization: A, - ) -> Self { - Bundle { - shielded_spends, - shielded_outputs, - value_balance, - authorization, - } - } - - /// Returns the list of spends in this bundle. - pub fn shielded_spends(&self) -> &[SpendDescription] { - &self.shielded_spends - } - - /// Returns the list of outputs in this bundle. - pub fn shielded_outputs(&self) -> &[OutputDescription] { - &self.shielded_outputs - } - - /// Returns the net value moved into or out of the Sapling shielded pool. - /// - /// This is the sum of Sapling spends minus the sum of Sapling outputs. - pub fn value_balance(&self) -> &Amount { - &self.value_balance - } - - /// Returns the authorization for this bundle. - /// - /// In the case of a `Bundle`, this is the binding signature. - pub fn authorization(&self) -> &A { - &self.authorization - } - - pub fn map_authorization>(self, f: F) -> Bundle { - Bundle { - shielded_spends: self - .shielded_spends - .into_iter() - .map(|d| SpendDescription { - cv: d.cv, - anchor: d.anchor, - nullifier: d.nullifier, - rk: d.rk, - zkproof: f.map_spend_proof(d.zkproof), - spend_auth_sig: f.map_auth_sig(d.spend_auth_sig), - }) - .collect(), - shielded_outputs: self - .shielded_outputs - .into_iter() - .map(|o| OutputDescription { - cv: o.cv, - cmu: o.cmu, - ephemeral_key: o.ephemeral_key, - enc_ciphertext: o.enc_ciphertext, - out_ciphertext: o.out_ciphertext, - zkproof: f.map_output_proof(o.zkproof), - }) - .collect(), - value_balance: self.value_balance, - authorization: f.map_authorization(self.authorization), - } - } -} - -impl DynamicUsage for Bundle { - fn dynamic_usage(&self) -> usize { - self.shielded_spends.dynamic_usage() + self.shielded_outputs.dynamic_usage() - } - - fn dynamic_usage_bounds(&self) -> (usize, Option) { - let bounds = ( - self.shielded_spends.dynamic_usage_bounds(), - self.shielded_outputs.dynamic_usage_bounds(), - ); - - ( - bounds.0 .0 + bounds.1 .0, - bounds.0 .1.zip(bounds.1 .1).map(|(a, b)| a + b), - ) - } -} - -#[derive(Clone)] -pub struct SpendDescription { - cv: ValueCommitment, - anchor: bls12_381::Scalar, - nullifier: Nullifier, - rk: PublicKey, - zkproof: A::SpendProof, - spend_auth_sig: A::AuthSig, -} - -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 - ) - } -} - -impl SpendDescription { - #[cfg(feature = "temporary-zcashd")] - pub fn temporary_zcashd_from_parts( - cv: ValueCommitment, - anchor: bls12_381::Scalar, - nullifier: Nullifier, - rk: PublicKey, - zkproof: A::SpendProof, - spend_auth_sig: A::AuthSig, - ) -> Self { - Self { - cv, - anchor, - nullifier, - rk, - zkproof, - spend_auth_sig, - } - } - - /// Returns the commitment to the value consumed by this spend. - pub fn cv(&self) -> &ValueCommitment { - &self.cv - } - - /// Returns the root of the Sapling commitment tree that this spend commits to. - pub fn anchor(&self) -> &bls12_381::Scalar { - &self.anchor - } - - /// Returns the nullifier of the note being spent. - pub fn nullifier(&self) -> &Nullifier { - &self.nullifier - } - - /// Returns the randomized verification key for the note being spent. - pub fn rk(&self) -> &PublicKey { - &self.rk - } - - /// Returns the proof for this spend. - pub fn zkproof(&self) -> &A::SpendProof { - &self.zkproof - } - - /// Returns the authorization signature for this spend. - pub fn spend_auth_sig(&self) -> &A::AuthSig { - &self.spend_auth_sig - } -} - -impl DynamicUsage for SpendDescription { - fn dynamic_usage(&self) -> usize { - self.zkproof.dynamic_usage() - } - - fn dynamic_usage_bounds(&self) -> (usize, Option) { - self.zkproof.dynamic_usage_bounds() - } -} - /// Consensus rules (§4.4) & (§4.5): /// - Canonical encoding is enforced here. /// - "Not small order" is enforced here. @@ -317,13 +110,13 @@ fn read_cmu(mut reader: R) -> io::Result { /// Consensus rules (§7.3) & (§7.4): /// - Canonical encoding is enforced here -pub fn read_base(mut reader: R, field: &str) -> io::Result { +pub fn read_base(mut reader: R, _field: &str) -> io::Result { let mut f = [0u8; 32]; reader.read_exact(&mut f)?; - Option::from(bls12_381::Scalar::from_repr(f)).ok_or_else(|| { + Option::from(jubjub::Base::from_repr(f)).ok_or_else(|| { io::Error::new( io::ErrorKind::InvalidInput, - format!("{} not in field", field), + "base value not a valid field element", ) }) } @@ -340,500 +133,391 @@ pub fn read_zkproof(mut reader: R) -> io::Result { Ok(zkproof) } -impl SpendDescription { - pub 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() - pub fn read_rk(mut reader: R) -> io::Result { - PublicKey::read(&mut reader) - } - - /// Consensus rules (§4.4): - /// - Canonical encoding is enforced here. - /// - Signature validity is enforced in SaplingVerificationContext::check_spend() - pub fn read_spend_auth_sig(mut reader: R) -> io::Result { - Signature::read(&mut reader) - } - - pub fn read(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 = Self::read_nullifier(&mut reader)?; - let rk = Self::read_rk(&mut reader)?; - let zkproof = read_zkproof(&mut reader)?; - let spend_auth_sig = Self::read_spend_auth_sig(&mut reader)?; - - Ok(SpendDescription { - cv, - anchor, - nullifier, - rk, - zkproof, - spend_auth_sig, - }) - } - - pub fn write_v4(&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)?; - self.spend_auth_sig.write(&mut writer) - } - - pub fn write_v5_without_witness_data(&self, mut writer: W) -> io::Result<()> { - writer.write_all(&self.cv.to_bytes())?; - writer.write_all(&self.nullifier.0)?; - self.rk.write(&mut writer) - } -} - -#[derive(Clone)] -pub struct SpendDescriptionV5 { - cv: ValueCommitment, - nullifier: Nullifier, - rk: PublicKey, -} - -impl SpendDescriptionV5 { - pub fn read(mut reader: &mut R) -> io::Result { - let cv = read_value_commitment(&mut reader)?; - let nullifier = SpendDescription::read_nullifier(&mut reader)?; - let rk = SpendDescription::read_rk(&mut reader)?; - - Ok(SpendDescriptionV5 { cv, nullifier, rk }) - } - - pub fn into_spend_description( - self, - anchor: bls12_381::Scalar, - zkproof: GrothProofBytes, - spend_auth_sig: Signature, - ) -> SpendDescription { - SpendDescription { - cv: self.cv, - anchor, - nullifier: self.nullifier, - rk: self.rk, - zkproof, - spend_auth_sig, - } - } -} - -#[derive(Clone)] -pub struct OutputDescription { - cv: ValueCommitment, - cmu: ExtractedNoteCommitment, - ephemeral_key: EphemeralKeyBytes, - enc_ciphertext: [u8; 580], - out_ciphertext: [u8; 80], - zkproof: Proof, -} - -impl OutputDescription { - /// Returns the commitment to the value consumed by this output. - pub fn cv(&self) -> &ValueCommitment { - &self.cv - } - - /// Returns the commitment to the new note being created. - pub fn cmu(&self) -> &ExtractedNoteCommitment { - &self.cmu - } - - pub fn ephemeral_key(&self) -> &EphemeralKeyBytes { - &self.ephemeral_key - } - - /// Returns the encrypted note ciphertext. - pub fn enc_ciphertext(&self) -> &[u8; 580] { - &self.enc_ciphertext - } - - /// Returns the output recovery ciphertext. - pub fn out_ciphertext(&self) -> &[u8; 80] { - &self.out_ciphertext - } - - /// Returns the proof for this output. - pub fn zkproof(&self) -> &Proof { - &self.zkproof - } - - #[cfg(feature = "temporary-zcashd")] - pub fn temporary_zcashd_from_parts( - cv: ValueCommitment, - cmu: ExtractedNoteCommitment, - ephemeral_key: EphemeralKeyBytes, - enc_ciphertext: [u8; 580], - out_ciphertext: [u8; 80], - zkproof: Proof, - ) -> Self { - Self::from_parts( - cv, - cmu, - ephemeral_key, - enc_ciphertext, - out_ciphertext, - zkproof, - ) - } - - #[cfg(any(test, feature = "temporary-zcashd"))] - pub(crate) fn from_parts( - cv: ValueCommitment, - cmu: ExtractedNoteCommitment, - ephemeral_key: EphemeralKeyBytes, - enc_ciphertext: [u8; 580], - out_ciphertext: [u8; 80], - zkproof: Proof, - ) -> Self { - OutputDescription { - cv, - cmu, - ephemeral_key, - enc_ciphertext, - out_ciphertext, - zkproof, - } - } +fn read_nullifier(mut reader: R) -> io::Result { + let mut nullifier = Nullifier([0u8; 32]); + reader.read_exact(&mut nullifier.0)?; + Ok(nullifier) } -#[cfg(test)] -impl OutputDescription { - pub(crate) fn cv_mut(&mut self) -> &mut ValueCommitment { - &mut self.cv - } - pub(crate) fn cmu_mut(&mut self) -> &mut ExtractedNoteCommitment { - &mut self.cmu - } - pub(crate) fn ephemeral_key_mut(&mut self) -> &mut EphemeralKeyBytes { - &mut self.ephemeral_key - } - pub(crate) fn enc_ciphertext_mut(&mut self) -> &mut [u8; 580] { - &mut self.enc_ciphertext - } - pub(crate) fn out_ciphertext_mut(&mut self) -> &mut [u8; 80] { - &mut self.out_ciphertext - } +/// 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")) } -impl DynamicUsage for OutputDescription { - fn dynamic_usage(&self) -> usize { - self.zkproof.dynamic_usage() +/// 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())) +} + +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![])) + } +} + +/// 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) +} + +/// 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.", + )); } - fn dynamic_usage_bounds(&self) -> (usize, Option) { - self.zkproof.dynamic_usage_bounds() - } + Ok(()) } -impl ShieldedOutput, ENC_CIPHERTEXT_SIZE> - for OutputDescription -{ - fn ephemeral_key(&self) -> EphemeralKeyBytes { - self.ephemeral_key.clone() - } +/// 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() + }; - fn cmstar_bytes(&self) -> [u8; 32] { - self.cmu.to_bytes() - } + let anchor = if n_spends > 0 { + Some(read_base(&mut reader, "anchor")?) + } else { + None + }; - fn enc_ciphertext(&self) -> &[u8; ENC_CIPHERTEXT_SIZE] { - &self.enc_ciphertext - } -} + 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))?; -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 - ) - } -} + 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 + }; -impl OutputDescription { - pub fn read(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; 580]; - let mut out_ciphertext = [0u8; 80]; - reader.read_exact(&mut enc_ciphertext)?; - reader.read_exact(&mut out_ciphertext)?; - - let zkproof = read_zkproof(&mut reader)?; - - Ok(OutputDescription { - cv, - cmu, - ephemeral_key, - enc_ciphertext, - out_ciphertext, - zkproof, + 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(); - pub fn write_v4(&self, mut writer: W) -> io::Result<()> { - writer.write_all(&self.cv.to_bytes())?; - writer.write_all(self.cmu.to_bytes().as_ref())?; - writer.write_all(self.ephemeral_key.as_ref())?; - writer.write_all(&self.enc_ciphertext)?; - writer.write_all(&self.out_ciphertext)?; - writer.write_all(&self.zkproof) - } + let shielded_outputs = od_v5s + .into_iter() + .zip(v_output_proofs) + .map(|(od_5, zkproof)| od_5.into_output_description(zkproof)) + .collect(); - pub fn write_v5_without_proof(&self, mut writer: W) -> io::Result<()> { - writer.write_all(&self.cv.to_bytes())?; - writer.write_all(self.cmu.to_bytes().as_ref())?; - writer.write_all(self.ephemeral_key.as_ref())?; - writer.write_all(&self.enc_ciphertext)?; - writer.write_all(&self.out_ciphertext) - } + Ok(binding_sig.and_then(|binding_sig| { + Bundle::from_parts( + shielded_spends, + shielded_outputs, + value_balance, + Authorized { binding_sig }, + ) + })) } -#[derive(Clone)] -pub struct OutputDescriptionV5 { - cv: ValueCommitment, - cmu: ExtractedNoteCommitment, - ephemeral_key: EphemeralKeyBytes, - enc_ciphertext: [u8; 580], - out_ciphertext: [u8; 80], -} +/// 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) + })?; -memuse::impl_no_dynamic_usage!(OutputDescriptionV5); - -impl OutputDescriptionV5 { - pub fn read(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 { - cv, - cmu, - ephemeral_key, - enc_ciphertext, - out_ciphertext, - }) - } + Vector::write(&mut writer, bundle.shielded_outputs(), |w, e| { + write_output_v5_without_proof(w, e) + })?; - pub fn into_output_description( - self, - zkproof: GrothProofBytes, - ) -> OutputDescription { - OutputDescription { - cv: self.cv, - cmu: self.cmu, - ephemeral_key: self.ephemeral_key, - enc_ciphertext: self.enc_ciphertext, - out_ciphertext: self.out_ciphertext, - zkproof, + if !(bundle.shielded_spends().is_empty() && bundle.shielded_outputs().is_empty()) { + writer.write_all(&bundle.value_balance().to_i64_le_bytes())?; } - } -} - -#[derive(Clone)] -pub struct CompactOutputDescription { - pub ephemeral_key: EphemeralKeyBytes, - pub cmu: ExtractedNoteCommitment, - pub enc_ciphertext: [u8; COMPACT_NOTE_SIZE], -} - -memuse::impl_no_dynamic_usage!(CompactOutputDescription); - -impl From> for CompactOutputDescription { - fn from(out: OutputDescription) -> CompactOutputDescription { - CompactOutputDescription { - ephemeral_key: out.ephemeral_key, - cmu: out.cmu, - enc_ciphertext: out.enc_ciphertext[..COMPACT_NOTE_SIZE].try_into().unwrap(), + if !bundle.shielded_spends().is_empty() { + writer.write_all(bundle.shielded_spends()[0].anchor().to_repr().as_ref())?; } - } -} - -impl ShieldedOutput, COMPACT_NOTE_SIZE> - for CompactOutputDescription -{ - fn ephemeral_key(&self) -> EphemeralKeyBytes { - self.ephemeral_key.clone() - } - fn cmstar_bytes(&self) -> [u8; 32] { - self.cmu.to_bytes() + 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 enc_ciphertext(&self) -> &[u8; COMPACT_NOTE_SIZE] { - &self.enc_ciphertext - } + Ok(()) } #[cfg(any(test, feature = "test-dependencies"))] pub mod testing { - use ff::Field; - use group::{Group, GroupEncoding}; - use proptest::collection::vec; use proptest::prelude::*; - use rand::{rngs::StdRng, SeedableRng}; - - use crate::{ - constants::{SPENDING_KEY_GENERATOR, VALUE_COMMITMENT_RANDOMNESS_GENERATOR}, - sapling::{ - note::ExtractedNoteCommitment, - redjubjub::{PrivateKey, PublicKey}, - value::{ - testing::{arb_note_value_bounded, arb_trapdoor}, - ValueCommitment, MAX_NOTE_VALUE, - }, - Nullifier, - }, - transaction::{ - components::{amount::testing::arb_amount, GROTH_PROOF_SIZE}, - TxVersion, - }, - }; - - use super::{Authorized, Bundle, GrothProofBytes, OutputDescription, SpendDescription}; - prop_compose! { - fn arb_extended_point()(rng_seed in prop::array::uniform32(any::())) -> jubjub::ExtendedPoint { - let mut rng = StdRng::from_seed(rng_seed); - let scalar = jubjub::Scalar::random(&mut rng); - jubjub::ExtendedPoint::generator() * scalar - } - } - - prop_compose! { - /// produce a spend description with invalid data (useful only for serialization - /// roundtrip testing). - fn arb_spend_description(n_spends: usize)( - value in arb_note_value_bounded(MAX_NOTE_VALUE.checked_div(n_spends as u64).unwrap_or(0)), - rcv in arb_trapdoor(), - anchor in vec(any::(), 64) - .prop_map(|v| <[u8;64]>::try_from(v.as_slice()).unwrap()) - .prop_map(|v| bls12_381::Scalar::from_bytes_wide(&v)), - nullifier in prop::array::uniform32(any::()) - .prop_map(|v| Nullifier::from_slice(&v).unwrap()), - zkproof in vec(any::(), GROTH_PROOF_SIZE) - .prop_map(|v| <[u8;GROTH_PROOF_SIZE]>::try_from(v.as_slice()).unwrap()), - rng_seed in prop::array::uniform32(prop::num::u8::ANY), - fake_sighash_bytes in prop::array::uniform32(prop::num::u8::ANY), - ) -> SpendDescription { - let mut rng = StdRng::from_seed(rng_seed); - let sk1 = PrivateKey(jubjub::Fr::random(&mut rng)); - let rk = PublicKey::from_private(&sk1, SPENDING_KEY_GENERATOR); - let cv = ValueCommitment::derive(value, rcv); - SpendDescription { - cv, - anchor, - nullifier, - rk, - zkproof, - spend_auth_sig: sk1.sign(&fake_sighash_bytes, &mut rng, SPENDING_KEY_GENERATOR), - } - } - } - - prop_compose! { - /// produce an output description with invalid data (useful only for serialization - /// roundtrip testing). - pub fn arb_output_description(n_outputs: usize)( - value in arb_note_value_bounded(MAX_NOTE_VALUE.checked_div(n_outputs as u64).unwrap_or(0)), - rcv in arb_trapdoor(), - cmu in vec(any::(), 64) - .prop_map(|v| <[u8;64]>::try_from(v.as_slice()).unwrap()) - .prop_map(|v| bls12_381::Scalar::from_bytes_wide(&v)), - enc_ciphertext in vec(any::(), 580) - .prop_map(|v| <[u8;580]>::try_from(v.as_slice()).unwrap()), - epk in arb_extended_point(), - out_ciphertext in vec(any::(), 80) - .prop_map(|v| <[u8;80]>::try_from(v.as_slice()).unwrap()), - zkproof in vec(any::(), GROTH_PROOF_SIZE) - .prop_map(|v| <[u8;GROTH_PROOF_SIZE]>::try_from(v.as_slice()).unwrap()), - ) -> OutputDescription { - let cv = ValueCommitment::derive(value, rcv); - let cmu = ExtractedNoteCommitment::from_bytes(&cmu.to_bytes()).unwrap(); - OutputDescription { - cv, - cmu, - ephemeral_key: epk.to_bytes().into(), - enc_ciphertext, - out_ciphertext, - zkproof, - } - } - } + use crate::transaction::TxVersion; + use ::sapling::bundle::{testing as t_sap, Authorized, Bundle}; + use zcash_protocol::value::{testing::arb_zat_balance, ZatBalance}; prop_compose! { - pub fn arb_bundle()( - n_spends in 0usize..30, - n_outputs in 0usize..30, + fn arb_bundle()( + value_balance in arb_zat_balance() )( - shielded_spends in vec(arb_spend_description(n_spends), n_spends), - shielded_outputs in vec(arb_output_description(n_outputs), n_outputs), - value_balance in arb_amount(), - rng_seed in prop::array::uniform32(prop::num::u8::ANY), - fake_bvk_bytes in prop::array::uniform32(prop::num::u8::ANY), - ) -> Option> { - if shielded_spends.is_empty() && shielded_outputs.is_empty() { - None - } else { - let mut rng = StdRng::from_seed(rng_seed); - let bsk = PrivateKey(jubjub::Fr::random(&mut rng)); - - Some( - Bundle { - shielded_spends, - shielded_outputs, - value_balance, - authorization: Authorized { binding_sig: bsk.sign(&fake_bvk_bytes, &mut rng, VALUE_COMMITMENT_RANDOMNESS_GENERATOR) }, - } - ) - } + bundle in t_sap::arb_bundle(value_balance) + ) -> Option> { + bundle } } pub fn arb_bundle_for_version( v: TxVersion, - ) -> impl Strategy>> { + ) -> impl Strategy>> { if v.has_sapling() { Strategy::boxed(arb_bundle()) } else { diff --git a/zcash_primitives/src/transaction/components/sapling/builder.rs b/zcash_primitives/src/transaction/components/sapling/builder.rs deleted file mode 100644 index 7890d5143c..0000000000 --- a/zcash_primitives/src/transaction/components/sapling/builder.rs +++ /dev/null @@ -1,658 +0,0 @@ -//! Types and functions for building Sapling transaction components. - -use core::fmt; -use std::sync::mpsc::Sender; - -use ff::Field; -use rand::{seq::SliceRandom, RngCore}; - -use crate::{ - consensus::{self, BlockHeight}, - keys::OutgoingViewingKey, - memo::MemoBytes, - sapling::{ - keys::SaplingIvk, - note_encryption::sapling_note_encryption, - prover::TxProver, - redjubjub::{PrivateKey, Signature}, - spend_sig_internal, - util::generate_random_rseed_internal, - value::{NoteValue, ValueSum}, - Diversifier, MerklePath, Node, Note, PaymentAddress, - }, - transaction::{ - builder::Progress, - components::{ - amount::Amount, - sapling::{ - fees, Authorization, Authorized, Bundle, GrothProofBytes, OutputDescription, - SpendDescription, - }, - }, - }, - zip32::ExtendedSpendingKey, -}; - -/// 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, Eq)] -pub enum Error { - AnchorMismatch, - BindingSig, - InvalidAddress, - InvalidAmount, - SpendProof, -} - -impl fmt::Display for Error { - 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::InvalidAddress => write!(f, "Invalid address"), - Error::InvalidAmount => write!(f, "Invalid amount"), - Error::SpendProof => write!(f, "Failed to create Sapling spend proof"), - } - } -} - -#[derive(Debug, Clone)] -pub struct SpendDescriptionInfo { - extsk: ExtendedSpendingKey, - diversifier: Diversifier, - note: Note, - alpha: jubjub::Fr, - merkle_path: MerklePath, -} - -impl fees::InputView<()> for SpendDescriptionInfo { - fn note_id(&self) -> &() { - // The builder does not make use of note identifiers, so we can just return the unit value. - &() - } - - fn value(&self) -> Amount { - // An existing note to be spent must have a valid amount value. - Amount::from_u64(self.note.value().inner()).unwrap() - } -} - -/// A struct containing the information required in order to construct a -/// Sapling output to a transaction. -#[derive(Clone)] -struct SaplingOutputInfo { - /// `None` represents the `ovk = ⊥` case. - ovk: Option, - note: Note, - memo: MemoBytes, -} - -impl SaplingOutputInfo { - fn new_internal( - params: &P, - rng: &mut R, - target_height: BlockHeight, - ovk: Option, - to: PaymentAddress, - value: NoteValue, - memo: MemoBytes, - ) -> Self { - let rseed = generate_random_rseed_internal(params, target_height, rng); - - let note = Note::from_parts(to, value, rseed); - - SaplingOutputInfo { ovk, note, memo } - } - - fn build( - self, - prover: &Pr, - ctx: &mut Pr::SaplingProvingContext, - rng: &mut R, - ) -> OutputDescription { - let encryptor = - sapling_note_encryption::(self.ovk, self.note.clone(), self.memo, rng); - - let (zkproof, cv) = prover.output_proof( - ctx, - encryptor.esk().0, - self.note.recipient(), - self.note.rcm(), - self.note.value().inner(), - ); - - let cmu = self.note.cmu(); - - let enc_ciphertext = encryptor.encrypt_note_plaintext(); - let out_ciphertext = encryptor.encrypt_outgoing_plaintext(&cv, &cmu, rng); - - let epk = encryptor.epk(); - - OutputDescription { - cv, - cmu, - ephemeral_key: epk.to_bytes(), - enc_ciphertext, - out_ciphertext, - zkproof, - } - } -} - -impl fees::OutputView for SaplingOutputInfo { - fn value(&self) -> Amount { - Amount::from_u64(self.note.value().inner()) - .expect("Note values should be checked at construction.") - } -} - -/// Metadata about a transaction created by a [`SaplingBuilder`]. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SaplingMetadata { - spend_indices: Vec, - output_indices: Vec, -} - -impl SaplingMetadata { - pub fn empty() -> Self { - SaplingMetadata { - spend_indices: vec![], - output_indices: vec![], - } - } - - /// Returns the index within the transaction of the [`SpendDescription`] corresponding - /// to the `n`-th call to [`SaplingBuilder::add_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 [`SaplingBuilder::add_spend`]) is the first - /// [`SpendDescription`] in the transaction. - pub fn spend_index(&self, n: usize) -> Option { - self.spend_indices.get(n).copied() - } - - /// Returns the index within the transaction of the [`OutputDescription`] corresponding - /// to the `n`-th call to [`SaplingBuilder::add_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 [`SaplingBuilder::add_output`]) is the first - /// [`OutputDescription`] in the transaction. - pub fn output_index(&self, n: usize) -> Option { - self.output_indices.get(n).copied() - } -} - -pub struct SaplingBuilder