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..7a9871203e --- /dev/null +++ b/.github/actions/prepare/action.yml @@ -0,0 +1,36 @@ +name: 'Prepare CI' +description: 'Prepares feature flags' +inputs: + extra-features: + description: 'Extra feature flags to enable' + required: false + default: '' + test-dependencies: + description: 'Include test dependencies' + required: false + default: true +outputs: + feature-flags: + description: 'Feature flags' + value: ${{ steps.prepare.outputs.flags }} +runs: + using: 'composite' + steps: + - id: test + shell: bash + run: echo "feature=test-dependencies" >> $GITHUB_OUTPUT + if: inputs.test-dependencies == 'true' + - name: Prepare feature flags + id: prepare + shell: bash + run: > + echo "flags=--features ' + bundled-prover + download-params + temporary-zcashd + transparent-inputs + sapling + unstable + ${{ inputs.extra-features }} + ${{ steps.test.outputs.feature }} + '" >> $GITHUB_OUTPUT 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..7e58402dec --- /dev/null +++ b/.github/workflows/audits.yml @@ -0,0 +1,30 @@ +name: Audits + +on: + pull_request: + push: + branches: main + +permissions: + contents: read + +jobs: + cargo-vet: + name: Vet Rust dependencies + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + id: toolchain + - run: rustup override set ${{steps.toolchain.outputs.name}} + - run: cargo install cargo-vet --version ~0.9 + - run: cargo vet --locked + + cargo-deny: + name: Check licenses + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: EmbarkStudios/cargo-deny-action@v1 + with: + command: check licenses diff --git a/.github/workflows/book.yml b/.github/workflows/book.yml index f62273e552..03157783c0 100644 --- a/.github/workflows/book.yml +++ b/.github/workflows/book.yml @@ -9,13 +9,19 @@ jobs: deploy: runs-on: ubuntu-18.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - id: prepare + uses: ./.github/actions/prepare - uses: dtolnay/rust-toolchain@nightly id: toolchain - run: rustup override set ${{steps.toolchain.outputs.name}} - name: Build latest rustdocs - run: cargo doc --no-deps --workspace --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 +31,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..ccb6bd343a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,40 +1,174 @@ name: CI checks -on: [push, pull_request] +on: + pull_request: + push: + branches: main jobs: test: - name: Test on ${{ matrix.os }} + name: > + Test${{ + matrix.state != 'NOT_A_PUZZLE' && format(' {0}', matrix.state) || '' + }} on ${{ matrix.target }} runs-on: ${{ matrix.os }} + continue-on-error: ${{ matrix.state != 'NOT_A_PUZZLE' }} strategy: matrix: - os: [ubuntu-latest, windows-latest, macOS-latest] + target: + - Linux + - macOS + - Windows + state: + - NOT_A_PUZZLE + - Orchard + - NU6 + + include: + - target: Linux + os: ubuntu-latest + - target: macOS + os: macOS-latest + - target: Windows + os: windows-latest + + - state: Orchard + extra_flags: orchard + - state: NU6 + rustflags: '--cfg zcash_unstable="nu6"' + + exclude: + - target: macOS + state: NU6 + + 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 + - 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 - + extra-features: ${{ matrix.state != 'NOT_A_PUZZLE' && 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 + --release + --workspace + ${{ steps.prepare.outputs.feature-flags }} - name: Run slow tests - run: cargo test --all-features --verbose --release --all -- --ignored + run: > + cargo test + --release + --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 != 'NOT_A_PUZZLE' && format(' {0}', matrix.state) || '' + }} build on ${{ matrix.target }} + runs-on: ${{ matrix.os }} + continue-on-error: ${{ matrix.state != 'NOT_A_PUZZLE' }} + strategy: + matrix: + target: + - Linux + - macOS + - Windows + state: + - ZFuture + + include: + - target: Linux + os: ubuntu-latest + - target: macOS + os: macOS-latest + - target: Windows + os: windows-latest + + - state: ZFuture + rustflags: '--cfg zcash_unstable="zfuture"' + + env: + RUSTFLAGS: ${{ matrix.rustflags }} + RUSTDOCFLAGS: ${{ matrix.rustflags }} + + steps: + - uses: actions/checkout@v4 + - id: prepare + uses: ./.github/actions/prepare + with: + extra-features: ${{ matrix.state != 'NOT_A_PUZZLE' && matrix.extra_flags || '' }} + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-msrv-${{ hashFiles('**/Cargo.lock') }} + - name: Run check + run: > + cargo check + --release + --workspace + --tests + ${{ steps.prepare.outputs.feature-flags }} - name: Verify working directory is clean run: git diff --exit-code - build: + build-latest: + name: Latest build on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest] + steps: + - uses: actions/checkout@v4 + - id: prepare + uses: ./.github/actions/prepare + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-latest + - uses: dtolnay/rust-toolchain@stable + id: toolchain + - run: rustup override set ${{steps.toolchain.outputs.name}} + - name: Remove lockfile to build with latest dependencies + run: rm Cargo.lock + - name: Build crates + run: > + cargo build + --workspace + --all-targets + ${{ steps.prepare.outputs.feature-flags }} + --verbose + - name: Verify working directory is clean (excluding lockfile) + run: git diff --exit-code ':!Cargo.lock' + + build-nodefault: name: Build target ${{ matrix.target }} runs-on: ubuntu-latest strategy: @@ -43,46 +177,69 @@ jobs: - wasm32-wasi steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + with: + path: crates + # We use a synthetic crate to ensure no dev-dependencies are enabled, which can + # be incompatible with some of these targets. + - name: Create synthetic crate for testing + run: cargo init --lib ci-build + - name: Copy Rust version into synthetic crate + run: cp crates/rust-toolchain.toml ci-build/ + - name: Copy patch directives into synthetic crate + run: | + echo "[patch.crates-io]" >> ./ci-build/Cargo.toml + cat ./crates/Cargo.toml | sed "0,/.\+\(patch.crates.\+\)/d" >> ./ci-build/Cargo.toml + - name: Add zcash_proofs as a dependency of the synthetic crate + working-directory: ./ci-build + run: cargo add --no-default-features --path ../crates/zcash_proofs + - name: Add zcash_client_backend as a dependency of the synthetic crate + working-directory: ./ci-build + run: cargo add --features lightwalletd-tonic --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 }} - - 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 # 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 + - 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 + - id: prepare + uses: ./.github/actions/prepare - uses: dtolnay/rust-toolchain@beta id: toolchain - run: rustup override set ${{steps.toolchain.outputs.name}} @@ -92,7 +249,11 @@ jobs: 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 +263,105 @@ 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 + - id: prepare + uses: ./.github/actions/prepare + - uses: actions/cache@v4 with: - path: ${{ env.ZCASH_PARAMS }} - key: ${{ runner.os }}-params - - name: Fetch Zcash parameters - if: steps.cache-params.outputs.cache-hit != 'true' - working-directory: ./zcash_proofs - run: cargo run --release --example download-params --features download-params - + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: codecov-cargo-${{ hashFiles('**/Cargo.lock') }} - name: Generate coverage report - 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@v4.3.0 + with: + token: ${{ secrets.CODECOV_TOKEN }} doc-links: name: Intra-doc links runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - id: prepare + uses: ./.github/actions/prepare - run: cargo fetch # Requires #![deny(rustdoc::broken_intra_doc_links)] in crates. - name: Check intra-doc links - 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 - name: Check formatting run: cargo fmt --all -- --check + + protobuf: + name: protobuf consistency + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - id: prepare + uses: ./.github/actions/prepare + - name: Install protoc + uses: supplypike/setup-bin@v4 + with: + 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 + - 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 + ) diff --git a/.gitignore b/.gitignore index fa8d85ac52..cd8e4b1f6f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -Cargo.lock target +.cargo diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..7b7fe97d46 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "rust-analyzer.cargo.extraEnv": { + "RUSTFLAGS": "--cfg zcash_unstable=\"orchard\"" + }, + "rust-analyzer.cargo.features": "all" +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000000..4781d8af86 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2650 @@ +# 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", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "ahash" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[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 = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +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.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" + +[[package]] +name = "base64ct" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a32fd6af2b5827bce66c29053ba0e7c42b9dcab01835835058558c10851a46b" + +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + +[[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 = "bip0039" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef0f0152ec5cf17f49a5866afaa3439816207fd4f0a224c0211ffaf5e278426" +dependencies = [ + "hmac", + "pbkdf2", + "rand", + "sha2", + "unicode-normalization", + "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.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c2f0dc9a68c6317d884f97cc36cf5a3d20ba14ce404227df55e1af708ab04bc" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "blake2s_simd" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637f448b9e61dfadbdcbae9a885fadee1f3eaffb1f8d3c1965d3ade8bdfd44f" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[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 = "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 = "bs58" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5353f36341f7451062466f0b755b96ac3a9547e4d7f6b70d603fc721a7d7896" +dependencies = [ + "sha2", + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "bytemuck" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[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.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[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 = "ciborium" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" + +[[package]] +name = "ciborium-ll" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +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", + "inout", + "zeroize", +] + +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "bitflags 1.3.2", + "clap_lex", + "indexmap 1.9.3", + "textwrap", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[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.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6" + +[[package]] +name = "cpp_demangle" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8227005286ec39567949b33df9896bcadfa6051bccca2488129f108ca23119" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "cpufeatures" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +dependencies = [ + "libc", +] + +[[package]] +name = "criterion" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb" +dependencies = [ + "anes", + "atty", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools", + "lazy_static", + "num-traits", + "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", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[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 = "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", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "document-features" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef5282ad69563b5fc40319526ba27e0e7363d552a896f0297d54f767717f9b95" +dependencies = [ + "litrs", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "elliptic-curve" +version = "0.13.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9775b22bc152ad86a0cf23f0f348b884b26add12bf741e7ffc4d4ab2ab4d205" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "equihash" +version = "0.2.0" +dependencies = [ + "blake2b_simd", + "byteorder", +] + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "f4jumble" +version = "0.1.0" +dependencies = [ + "blake2b_simd", + "hex", + "proptest", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[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 = "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 = "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 = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[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.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + +[[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 = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + +[[package]] +name = "halo2_gadgets" +version = "0.3.0" +source = "git+https://github.com/QED-it/halo2?branch=zsa1#5f436dc3387665fe3201d381791a62a8233b2171" +dependencies = [ + "arrayvec", + "bitvec", + "ff", + "group", + "halo2_proofs", + "lazy_static", + "pasta_curves", + "rand", + "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_proofs" +version = "0.3.0" +source = "git+https://github.com/QED-it/halo2?branch=zsa1#5f436dc3387665fe3201d381791a62a8233b2171" +dependencies = [ + "blake2b_simd", + "ff", + "group", + "halo2_legacy_pdqsort", + "maybe-rayon", + "pasta_curves", + "rand_core", + "tracing", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" + +[[package]] +name = "hdwallet" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a03ba7d4c9ea41552cd4351965ff96883e629693ae85005c501bb4b9e1c48a7" +dependencies = [ + "lazy_static", + "rand_core", + "ring 0.16.20", + "secp256k1", + "thiserror", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "incrementalmerkletree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb1872810fb725b06b8c153dde9e86f3ec26747b9b60096da7a869883b549cbe" +dependencies = [ + "either", + "proptest", + "rand", + "rand_core", +] + +[[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", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown 0.14.2", +] + +[[package]] +name = "inferno" +version = "0.11.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c50453ec3a6555fad17b1cd1a80d16af5bc7cb35094f64e429fd46549018c6a3" +dependencies = [ + "ahash", + "indexmap 2.1.0", + "is-terminal", + "itoa", + "log", + "num-format", + "once_cell", + "quick-xml", + "rgb", + "str_stack", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi 0.3.3", + "rustix", + "windows-sys", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "js-sys" +version = "0.3.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" +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 = "k256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f01b677d82ef7a676aa37e099defd83a28e15687112cafdd112d60236b6115b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", + "signature", +] + +[[package]] +name = "known-folders" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6f1427d9c43b1cce87434c4d9eca33f43bdbb6246a762aa823a582f74c1684" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] + +[[package]] +name = "libc" +version = "0.2.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "linux-raw-sys" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" + +[[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.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[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.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "memmap2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memuse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2145869435ace5ea6ea3d35f59be559317ec9a0d04e1812d5f185a87b6d36f1a" +dependencies = [ + "nonempty", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "minreq" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3371dfc7b772c540da1380123674a8e20583aca99907087d990ca58cf44203" +dependencies = [ + "log", + "once_cell", + "rustls", + "rustls-webpki", + "webpki-roots", +] + +[[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.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" + +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[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.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +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 0.3.3", + "libc", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "oorandom" +version = "11.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "orchard" +version = "0.8.0" +source = "git+https://github.com/QED-it/orchard?branch=zsa1#78c8efc23a37dc1d64ded53fab231098681966c0" +dependencies = [ + "aes", + "bitvec", + "blake2b_simd", + "ff", + "fpe", + "group", + "halo2_gadgets", + "halo2_proofs", + "hex", + "incrementalmerkletree", + "k256", + "lazy_static", + "memuse", + "nonempty", + "pasta_curves", + "proptest", + "rand", + "reddsa", + "serde", + "subtle", + "tracing", + "zcash_note_encryption 0.4.0 (git+https://github.com/QED-it/zcash_note_encryption?branch=zsa1)", + "zcash_spec", + "zip32", +] + +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" + +[[package]] +name = "pairing" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" +dependencies = [ + "group", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "password-hash" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d791538a6dcc1e7cb7fe6f6b58aca40e7f79403c45b2bc274008b5e647af1d8" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[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 = "pbkdf2" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271779f35b581956db91a3e55737327a03aa051e90b1c47aeb189508533adfd7" +dependencies = [ + "digest", + "password-hash", +] + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[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 = "pprof" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "196ded5d4be535690899a4631cc9f18cdc41b7ebf24a79400f46f48e49a11059" +dependencies = [ + "backtrace", + "cfg-if", + "criterion", + "findshlibs", + "inferno", + "libc", + "log", + "nix", + "once_cell", + "parking_lot", + "smallvec", + "symbolic-demangle", + "tempfile", + "thiserror", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "primitive-types" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" +dependencies = [ + "fixed-hash", + "uint", +] + +[[package]] +name = "proc-macro2" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c003ac8c77cb07bb74f5f198bce836a689bcd5a42574612bf14d17bfd08c20e" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.4.2", + "lazy_static", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax 0.7.5", + "rusty-fork", + "tempfile", + "unarray", +] + +[[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.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rayon" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "reddsa" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78a5191930e84973293aa5f532b513404460cd2216c1cfb76d08748c15b40b02" +dependencies = [ + "blake2b_simd", + "byteorder", + "group", + "hex", + "jubjub", + "pasta_curves", + "rand_core", + "serde", + "thiserror", + "zeroize", +] + +[[package]] +name = "redjubjub" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a60db2c3bc9c6fd1e8631fee75abc008841d27144be744951d6b9b75f9b569c" +dependencies = [ + "rand_core", + "reddsa", + "serde", + "thiserror", + "zeroize", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rgb" +version = "0.8.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8" +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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys", +] + +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.38.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +dependencies = [ + "bitflags 2.4.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustls" +version = "0.21.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c" +dependencies = [ + "log", + "ring 0.17.5", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring 0.17.5", + "untrusted 0.9.0", +] + +[[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.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[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 = "sapling-crypto" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02f4270033afcb0c74c5c7d59c73cfd1040367f67f224fe7ed9a919ae618f1b7" +dependencies = [ + "aes", + "bellman", + "bitvec", + "blake2b_simd", + "blake2s_simd", + "bls12_381", + "byteorder", + "document-features", + "ff", + "fpe", + "group", + "hex", + "incrementalmerkletree", + "jubjub", + "lazy_static", + "memuse", + "proptest", + "rand", + "rand_core", + "redjubjub", + "subtle", + "tracing", + "zcash_note_encryption 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zcash_spec", + "zip32", +] + +[[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.5", + "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.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4124a35fe33ae14259c490fd70fa199a32b9ce9502f2ee6bc4f81ec06fa65894" +dependencies = [ + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a129b9e9efbfb223753b9163c4ab3b13cff7fd9c7f010fbac25ab4099fa07e" +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 = "serde" +version = "1.0.192" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.192" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "smallvec" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" + +[[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" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[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 = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "symbolic-common" +version = "10.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b55cdc318ede251d0957f07afe5fed912119b8c1bc5a7804151826db999e737" +dependencies = [ + "debugid", + "memmap2", + "stable_deref_trait", + "uuid", +] + +[[package]] +name = "symbolic-demangle" +version = "10.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79be897be8a483a81fff6a3a4e195b4ac838ef73ca42d348b3f722da9902e489" +dependencies = [ + "cpp_demangle", + "rustc-demangle", + "symbolic-common", +] + +[[package]] +name = "syn" +version = "2.0.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys", +] + +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" + +[[package]] +name = "thiserror" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +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 = "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", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[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 = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "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.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[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.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" + +[[package]] +name = "web-sys" +version = "0.3.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[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-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[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_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[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_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[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_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "xdg" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" + +[[package]] +name = "zcash_address" +version = "0.3.2" +dependencies = [ + "assert_matches", + "bech32", + "bs58", + "f4jumble", + "proptest", + "zcash_encoding", + "zcash_protocol", +] + +[[package]] +name = "zcash_encoding" +version = "0.2.0" +dependencies = [ + "byteorder", + "nonempty", +] + +[[package]] +name = "zcash_extensions" +version = "0.0.0" +dependencies = [ + "blake2b_simd", + "ff", + "jubjub", + "orchard", + "rand_core", + "sapling-crypto", + "zcash_address", + "zcash_primitives", + "zcash_proofs", +] + +[[package]] +name = "zcash_history" +version = "0.4.0" +dependencies = [ + "assert_matches", + "blake2b_simd", + "byteorder", + "primitive-types", + "proptest", +] + +[[package]] +name = "zcash_keys" +version = "0.2.0" +dependencies = [ + "bech32", + "blake2b_simd", + "bls12_381", + "bs58", + "byteorder", + "document-features", + "group", + "hdwallet", + "hex", + "jubjub", + "memuse", + "nonempty", + "orchard", + "proptest", + "rand_core", + "sapling-crypto", + "secrecy", + "subtle", + "tracing", + "zcash_address", + "zcash_encoding", + "zcash_primitives", + "zcash_protocol", + "zip32", +] + +[[package]] +name = "zcash_note_encryption" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b4580cd6cee12e44421dac43169be8d23791650816bdb34e6ddfa70ac89c1c5" +dependencies = [ + "chacha20", + "chacha20poly1305", + "cipher", + "rand_core", + "subtle", +] + +[[package]] +name = "zcash_note_encryption" +version = "0.4.0" +source = "git+https://github.com/QED-it/zcash_note_encryption?branch=zsa1#8b6b31dcea4a883a606425c4b644fca213e78b3b" +dependencies = [ + "chacha20", + "chacha20poly1305", + "cipher", + "rand_core", + "subtle", +] + +[[package]] +name = "zcash_primitives" +version = "0.15.0" +dependencies = [ + "aes", + "assert_matches", + "bip0039", + "blake2b_simd", + "byteorder", + "chacha20poly1305", + "criterion", + "document-features", + "equihash", + "ff", + "fpe", + "group", + "hdwallet", + "hex", + "incrementalmerkletree", + "jubjub", + "memuse", + "nonempty", + "orchard", + "pprof", + "proptest", + "rand", + "rand_core", + "rand_xorshift", + "redjubjub", + "ripemd", + "sapling-crypto", + "secp256k1", + "sha2", + "subtle", + "tracing", + "zcash_address", + "zcash_encoding", + "zcash_note_encryption 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zcash_protocol", + "zcash_spec", + "zip32", +] + +[[package]] +name = "zcash_proofs" +version = "0.15.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.1.1" +dependencies = [ + "document-features", + "incrementalmerkletree", + "memuse", + "proptest", +] + +[[package]] +name = "zcash_spec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7a3bf58b673cb3dacd8ae09ba345998923a197ab0da70d6239d8e8838949e9b" +dependencies = [ + "blake2b_simd", +] + +[[package]] +name = "zerocopy" +version = "0.7.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cd369a67c0edfef15010f980c3cbe45d7f651deac2cd67ce097cd801de16557" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2f140bda219a26ccc0cdb03dba58af72590c53b22642577d88a927bc5c87d6b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +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", +] + +[[package]] +name = "zip32" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4226d0aee9c9407c27064dfeec9d7b281c917de3374e1e5a2e2cfad9e09de19e" +dependencies = [ + "blake2b_simd", + "memuse", + "subtle", +] + +[[package]] +name = "zip321" +version = "0.0.0" +dependencies = [ + "base64", + "nom", + "percent-encoding", + "proptest", + "zcash_address", + "zcash_protocol", +] diff --git a/Cargo.toml b/Cargo.toml index 6c961e4134..cb1347f1ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,21 +4,124 @@ members = [ "components/f4jumble", "components/zcash_address", "components/zcash_encoding", - "components/zcash_note_encryption", - "zcash_client_backend", - "zcash_client_sqlite", + "components/zcash_protocol", + "components/zip321", +# "zcash_client_backend", +# "zcash_client_sqlite", "zcash_extensions", "zcash_history", + "zcash_keys", "zcash_primitives", "zcash_proofs", ] +[workspace.package] +edition = "2021" +rust-version = "1.65" +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" } +zcash_address = { version = "0.3", path = "components/zcash_address" } +#zcash_client_backend = { version = "0.12", path = "zcash_client_backend" } +zcash_encoding = { version = "0.2", path = "components/zcash_encoding" } +zcash_keys = { version = "0.2", path = "zcash_keys" } +zcash_protocol = { version = "0.1", path = "components/zcash_protocol" } +zip321 = { version = "0.0", path = "components/zip321" } + +zcash_note_encryption = "0.4" +zcash_primitives = { version = "0.15", path = "zcash_primitives", default-features = false } +zcash_proofs = { version = "0.15", path = "zcash_proofs", default-features = false } + +# Shielded protocols +ff = "0.13" +group = "0.13" +incrementalmerkletree = "0.5.1" +shardtree = "0.3" +zcash_spec = "0.1" + +# Payment protocols +# - Sapling +bitvec = "1" +blake2s_simd = "1" +bls12_381 = "0.8" +jubjub = "0.10" +sapling = { package = "sapling-crypto", version = "0.1.3" } + +# - Orchard +nonempty = "0.7" +orchard = { version = "0.8.0", default-features = false } +pasta_curves = "0.5" + +# - Transparent +hdwallet = "0.4" +ripemd = "0.1" +secp256k1 = "0.26" + +# CSPRNG +rand = "0.8" +rand_core = "0.6" + +# Digests +blake2b_simd = "1" +sha2 = "0.10" + +# Documentation +document-features = "0.2" + +# Encodings +base64 = "0.21" +bech32 = "0.9" +bs58 = { version = "0.5", features = ["check"] } +byteorder = "1" +hex = "0.4" +percent-encoding = "2.1.0" + +# Logging and metrics +memuse = "0.2.1" +tracing = "0.1" + +# Parallel processing +crossbeam-channel = "0.5" +maybe-rayon = { version = "0.1.0", default-features = false } +rayon = "1.5" + +# Protobuf and gRPC +prost = "=0.12.3" +tonic = { version = "0.10", default-features = false } +tonic-build = { version = "0.10", default-features = false } + +# Secret management +secrecy = "0.8" +subtle = "2.2.3" + +# Static constants +lazy_static = "1" + +# Tests and benchmarks +assert_matches = "1.5" +criterion = "0.4" +proptest = "1" +rand_chacha = "0.3" +rand_xorshift = "0.3" + +# ZIP 32 +aes = "0.8" +fpe = "0.6" +zip32 = "0.1.1" + [profile.release] lto = true panic = 'abort' codegen-units = 1 [patch.crates-io] -zcash_encoding = { path = "components/zcash_encoding" } -zcash_note_encryption = { path = "components/zcash_note_encryption" } -orchard = { version = "0.5", git = "https://github.com/QED-it/orchard", tag = "zsa_0.5.0" } +orchard = { version = "0.8.0", git = "https://github.com/QED-it/orchard", branch = "zsa1" } diff --git a/README.md b/README.md index c10053bd85..3df799fe0e 100644 --- a/README.md +++ b/README.md @@ -16,17 +16,6 @@ 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 diff --git a/components/equihash/src/lib.rs b/components/equihash/src/lib.rs index fc23642063..cb6131ca3b 100644 --- a/components/equihash/src/lib.rs +++ b/components/equihash/src/lib.rs @@ -20,6 +20,8 @@ // Catch documentation errors caused by code changes. #![deny(rustdoc::broken_intra_doc_links)] +mod minimal; +mod params; mod verify; #[cfg(test)] diff --git a/components/equihash/src/minimal.rs b/components/equihash/src/minimal.rs new file mode 100644 index 0000000000..81da63e657 --- /dev/null +++ b/components/equihash/src/minimal.rs @@ -0,0 +1,190 @@ +use std::io::Cursor; +use std::mem::size_of; + +use byteorder::{BigEndian, ReadBytesExt}; + +use crate::params::Params; + +pub(crate) fn expand_array(vin: &[u8], bit_len: usize, byte_pad: usize) -> Vec { + assert!(bit_len >= 8); + assert!(u32::BITS as usize >= 7 + bit_len); + + let out_width = (bit_len + 7) / 8 + byte_pad; + let out_len = 8 * out_width * vin.len() / bit_len; + + // Shortcut for parameters where expansion is a no-op + if out_len == vin.len() { + return vin.to_vec(); + } + + let mut vout: Vec = vec![0; out_len]; + let bit_len_mask: u32 = (1 << bit_len) - 1; + + // The acc_bits least-significant bits of acc_value represent a bit sequence + // in big-endian order. + let mut acc_bits = 0; + let mut acc_value: u32 = 0; + + let mut j = 0; + for b in vin { + acc_value = (acc_value << 8) | u32::from(*b); + acc_bits += 8; + + // When we have bit_len or more bits in the accumulator, write the next + // output element. + if acc_bits >= bit_len { + acc_bits -= bit_len; + for x in byte_pad..out_width { + vout[j + x] = (( + // Big-endian + acc_value >> (acc_bits + (8 * (out_width - x - 1))) + ) & ( + // Apply bit_len_mask across byte boundaries + (bit_len_mask >> (8 * (out_width - x - 1))) & 0xFF + )) as u8; + } + j += out_width; + } + } + + vout +} + +/// 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) = csr.read_u32::() { + ret.push(i); + } + + Some(ret) +} + +#[cfg(test)] +mod tests { + use super::{expand_array, indices_from_minimal, Params}; + + #[test] + fn array_expansion() { + let check_array = |(bit_len, byte_pad), compact, expanded| { + assert_eq!(expand_array(compact, bit_len, byte_pad), expanded); + }; + + // 8 11-bit chunks, all-ones + check_array( + (11, 0), + &[ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + ], + &[ + 0x07, 0xff, 0x07, 0xff, 0x07, 0xff, 0x07, 0xff, 0x07, 0xff, 0x07, 0xff, 0x07, 0xff, + 0x07, 0xff, + ][..], + ); + // 8 21-bit chunks, alternating 1s and 0s + check_array( + (21, 0), + &[ + 0xaa, 0xaa, 0xad, 0x55, 0x55, 0x6a, 0xaa, 0xab, 0x55, 0x55, 0x5a, 0xaa, 0xaa, 0xd5, + 0x55, 0x56, 0xaa, 0xaa, 0xb5, 0x55, 0x55, + ], + &[ + 0x15, 0x55, 0x55, 0x15, 0x55, 0x55, 0x15, 0x55, 0x55, 0x15, 0x55, 0x55, 0x15, 0x55, + 0x55, 0x15, 0x55, 0x55, 0x15, 0x55, 0x55, 0x15, 0x55, 0x55, + ][..], + ); + // 8 21-bit chunks, based on example in the spec + check_array( + (21, 0), + &[ + 0x00, 0x02, 0x20, 0x00, 0x0a, 0x7f, 0xff, 0xfe, 0x00, 0x12, 0x30, 0x22, 0xb3, 0x82, + 0x26, 0xac, 0x19, 0xbd, 0xf2, 0x34, 0x56, + ], + &[ + 0x00, 0x00, 0x44, 0x00, 0x00, 0x29, 0x1f, 0xff, 0xff, 0x00, 0x01, 0x23, 0x00, 0x45, + 0x67, 0x00, 0x89, 0xab, 0x00, 0xcd, 0xef, 0x12, 0x34, 0x56, + ][..], + ); + // 16 14-bit chunks, alternating 11s and 00s + check_array( + (14, 0), + &[ + 0xcc, 0xcf, 0x33, 0x3c, 0xcc, 0xf3, 0x33, 0xcc, 0xcf, 0x33, 0x3c, 0xcc, 0xf3, 0x33, + 0xcc, 0xcf, 0x33, 0x3c, 0xcc, 0xf3, 0x33, 0xcc, 0xcf, 0x33, 0x3c, 0xcc, 0xf3, 0x33, + ], + &[ + 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, + 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, + 0x33, 0x33, 0x33, 0x33, + ][..], + ); + // 8 11-bit chunks, all-ones, 2-byte padding + check_array( + (11, 2), + &[ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + ], + &[ + 0x00, 0x00, 0x07, 0xff, 0x00, 0x00, 0x07, 0xff, 0x00, 0x00, 0x07, 0xff, 0x00, 0x00, + 0x07, 0xff, 0x00, 0x00, 0x07, 0xff, 0x00, 0x00, 0x07, 0xff, 0x00, 0x00, 0x07, 0xff, + 0x00, 0x00, 0x07, 0xff, + ][..], + ); + } + + #[test] + fn minimal_solution_repr() { + let check_repr = |minimal, indices| { + assert_eq!( + indices_from_minimal(Params { n: 80, k: 3 }, minimal).unwrap(), + indices, + ); + }; + + // The solutions here are not intended to be valid. + check_repr( + &[ + 0x00, 0x00, 0x08, 0x00, 0x00, 0x40, 0x00, 0x02, 0x00, 0x00, 0x10, 0x00, 0x00, 0x80, + 0x00, 0x04, 0x00, 0x00, 0x20, 0x00, 0x01, + ], + &[1, 1, 1, 1, 1, 1, 1, 1], + ); + check_repr( + &[ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + ], + &[ + 2097151, 2097151, 2097151, 2097151, 2097151, 2097151, 2097151, 2097151, + ], + ); + check_repr( + &[ + 0x0f, 0xff, 0xf8, 0x00, 0x20, 0x03, 0xff, 0xfe, 0x00, 0x08, 0x00, 0xff, 0xff, 0x80, + 0x02, 0x00, 0x3f, 0xff, 0xe0, 0x00, 0x80, + ], + &[131071, 128, 131071, 128, 131071, 128, 131071, 128], + ); + check_repr( + &[ + 0x00, 0x02, 0x20, 0x00, 0x0a, 0x7f, 0xff, 0xfe, 0x00, 0x4d, 0x10, 0x01, 0x4c, 0x80, + 0x0f, 0xfc, 0x00, 0x00, 0x2f, 0xff, 0xff, + ], + &[68, 41, 2097151, 1233, 665, 1023, 1, 1048575], + ); + } +} diff --git a/components/equihash/src/params.rs b/components/equihash/src/params.rs new file mode 100644 index 0000000000..2e17700065 --- /dev/null +++ b/components/equihash/src/params.rs @@ -0,0 +1,37 @@ +#[derive(Clone, Copy)] +pub(crate) struct Params { + pub(crate) n: u32, + pub(crate) k: u32, +} + +impl Params { + /// Returns `None` if the parameters are invalid. + pub(crate) fn new(n: u32, k: u32) -> Option { + // We place the following requirements on the parameters: + // - n is a multiple of 8, so the hash output has an exact byte length. + // - k >= 3 so the encoded solutions have an exact byte length. + // - k < n, so the collision bit length is at least 1. + // - n is a multiple of k + 1, so we have an integer collision bit length. + if (n % 8 == 0) && (k >= 3) && (k < n) && (n % (k + 1) == 0) { + Some(Params { n, k }) + } else { + None + } + } + pub(crate) fn indices_per_hash_output(&self) -> u32 { + 512 / self.n + } + pub(crate) fn hash_output(&self) -> u8 { + (self.indices_per_hash_output() * self.n / 8) as u8 + } + pub(crate) fn collision_bit_length(&self) -> usize { + (self.n / (self.k + 1)) as usize + } + pub(crate) fn collision_byte_length(&self) -> usize { + (self.collision_bit_length() + 7) / 8 + } + #[cfg(test)] + pub(crate) fn hash_length(&self) -> usize { + ((self.k as usize) + 1) * self.collision_byte_length() + } +} diff --git a/components/equihash/src/test_vectors/invalid.rs b/components/equihash/src/test_vectors/invalid.rs index 11da849e0d..5dbec5a33d 100644 --- a/components/equihash/src/test_vectors/invalid.rs +++ b/components/equihash/src/test_vectors/invalid.rs @@ -1,4 +1,4 @@ -use crate::verify::{Kind, Params}; +use crate::{params::Params, verify::Kind}; pub(crate) struct TestVector { pub(crate) params: Params, diff --git a/components/equihash/src/test_vectors/valid.rs b/components/equihash/src/test_vectors/valid.rs index a55de1b96a..4df20c642a 100644 --- a/components/equihash/src/test_vectors/valid.rs +++ b/components/equihash/src/test_vectors/valid.rs @@ -1,4 +1,4 @@ -use crate::verify::Params; +use crate::params::Params; pub(crate) struct TestVector { pub(crate) params: Params, diff --git a/components/equihash/src/verify.rs b/components/equihash/src/verify.rs index 2015008838..53071ddc01 100644 --- a/components/equihash/src/verify.rs +++ b/components/equihash/src/verify.rs @@ -3,16 +3,13 @@ //! [Equihash]: https://zips.z.cash/protocol/protocol.pdf#equihash use blake2b_simd::{Hash as Blake2bHash, Params as Blake2bParams, State as Blake2bState}; -use byteorder::{BigEndian, LittleEndian, ReadBytesExt, WriteBytesExt}; +use byteorder::{LittleEndian, 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 crate::{ + minimal::{expand_array, indices_from_minimal}, + params::Params, +}; #[derive(Clone)] struct Node { @@ -20,37 +17,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()); @@ -168,74 +134,6 @@ fn generate_hash(base_state: &Blake2bState, i: u32) -> Blake2bHash { 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 +245,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 +254,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/zcash_address/CHANGELOG.md b/components/zcash_address/CHANGELOG.md index a7bf6f6385..daa4506114 100644 --- a/components/zcash_address/CHANGELOG.md +++ b/components/zcash_address/CHANGELOG.md @@ -7,6 +7,25 @@ and this library adheres to Rust's notion of ## [Unreleased] +### 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. + +## [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..c51b9f7646 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.3.2" authors = [ "Jack Grigg ", ] @@ -14,18 +14,23 @@ 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"] } +bech32.workspace = true +bs58.workspace = true f4jumble = { version = "0.1", path = "../f4jumble" } -zcash_encoding = { version = "0.2", path = "../zcash_encoding" } +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 [features] -test-dependencies = [] +test-dependencies = ["dep:proptest"] [lib] bench = false diff --git a/components/zcash_address/src/convert.rs b/components/zcash_address/src/convert.rs index 38b04f374a..225b8775bb 100644 --- a/components/zcash_address/src/convert.rs +++ b/components/zcash_address/src/convert.rs @@ -2,7 +2,7 @@ use std::{error::Error, fmt}; use crate::{kind::*, AddressKind, Network, ZcashAddress}; -/// An address type is not supported for conversion. +/// An error indicating that an address type is not supported for conversion. #[derive(Debug)] pub struct UnsupportedAddress(&'static str); @@ -107,12 +107,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 +123,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 +131,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. @@ -187,17 +192,14 @@ pub trait TryFromAddress: Sized { /// [`Self::try_from_sapling`] as a valid Sapling address). type Error; - fn try_from_sprout( - net: Network, - data: sprout::Data, - ) -> Result> { + fn try_from_sprout(net: Network, data: [u8; 64]) -> Result> { let _ = (net, data); Err(ConversionError::Unsupported(UnsupportedAddress("Sprout"))) } fn try_from_sapling( net: Network, - data: sapling::Data, + data: [u8; 43], ) -> Result> { let _ = (net, data); Err(ConversionError::Unsupported(UnsupportedAddress("Sapling"))) @@ -213,7 +215,7 @@ pub trait TryFromAddress: Sized { fn try_from_transparent_p2pkh( net: Network, - data: p2pkh::Data, + data: [u8; 20], ) -> Result> { let _ = (net, data); Err(ConversionError::Unsupported(UnsupportedAddress( @@ -223,28 +225,32 @@ pub trait TryFromAddress: Sized { fn try_from_transparent_p2sh( net: Network, - data: p2sh::Data, + data: [u8; 20], ) -> Result> { let _ = (net, data); Err(ConversionError::Unsupported(UnsupportedAddress( "transparent P2SH", ))) } + + fn try_from_tex(net: Network, data: [u8; 20]) -> Result> { + let _ = (net, data); + Err(ConversionError::Unsupported(UnsupportedAddress( + "transparent-source restricted P2PKH", + ))) + } } impl TryFromAddress for (Network, T) { type Error = T::Error; - fn try_from_sprout( - net: Network, - data: sprout::Data, - ) -> Result> { + fn try_from_sprout(net: Network, data: [u8; 64]) -> Result> { T::try_from_raw_sprout(data).map(|addr| (net, addr)) } fn try_from_sapling( net: Network, - data: sapling::Data, + data: [u8; 43], ) -> Result> { T::try_from_raw_sapling(data).map(|addr| (net, addr)) } @@ -258,17 +264,21 @@ impl TryFromAddress for (Network, T) { fn try_from_transparent_p2pkh( net: Network, - data: p2pkh::Data, + 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, + data: [u8; 20], ) -> Result> { T::try_from_raw_transparent_p2sh(data).map(|addr| (net, addr)) } + + fn try_from_tex(net: Network, data: [u8; 20]) -> Result> { + T::try_from_raw_tex(data).map(|addr| (net, addr)) + } } /// A helper trait for converting another type into a [`ZcashAddress`]. @@ -303,19 +313,21 @@ impl TryFromAddress for (Network, T) { /// ); /// ``` pub trait ToAddress: private::Sealed { - fn from_sprout(net: Network, data: sprout::Data) -> Self; + fn from_sprout(net: Network, data: [u8; 64]) -> Self; - fn from_sapling(net: Network, data: sapling::Data) -> Self; + fn from_sapling(net: Network, data: [u8; 43]) -> Self; fn from_unified(net: Network, data: unified::Address) -> Self; - fn from_transparent_p2pkh(net: Network, data: p2pkh::Data) -> Self; + fn from_transparent_p2pkh(net: Network, data: [u8; 20]) -> Self; + + fn from_transparent_p2sh(net: Network, data: [u8; 20]) -> Self; - fn from_transparent_p2sh(net: Network, data: p2sh::Data) -> Self; + fn from_tex(net: Network, data: [u8; 20]) -> Self; } impl ToAddress for ZcashAddress { - fn from_sprout(net: Network, data: sprout::Data) -> Self { + fn from_sprout(net: Network, data: [u8; 64]) -> Self { ZcashAddress { net: if let Network::Regtest = net { Network::Test @@ -326,7 +338,7 @@ impl ToAddress for ZcashAddress { } } - fn from_sapling(net: Network, data: sapling::Data) -> Self { + fn from_sapling(net: Network, data: [u8; 43]) -> Self { ZcashAddress { net, kind: AddressKind::Sapling(data), @@ -340,7 +352,7 @@ impl ToAddress for ZcashAddress { } } - fn from_transparent_p2pkh(net: Network, data: p2pkh::Data) -> Self { + fn from_transparent_p2pkh(net: Network, data: [u8; 20]) -> Self { ZcashAddress { net: if let Network::Regtest = net { Network::Test @@ -351,7 +363,7 @@ impl ToAddress for ZcashAddress { } } - fn from_transparent_p2sh(net: Network, data: p2sh::Data) -> Self { + fn from_transparent_p2sh(net: Network, data: [u8; 20]) -> Self { ZcashAddress { net: if let Network::Regtest = net { Network::Test @@ -361,6 +373,13 @@ impl ToAddress for ZcashAddress { kind: AddressKind::P2sh(data), } } + + fn from_tex(net: Network, 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..a0d3c117d6 100644 --- a/components/zcash_address/src/encoding.rs +++ b/components/zcash_address/src/encoding.rs @@ -1,9 +1,11 @@ use std::{convert::TryInto, error::Error, fmt, str::FromStr}; use bech32::{self, FromBase32, ToBase32, Variant}; +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)] @@ -54,55 +56,87 @@ 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)); } } - // 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. + // Try decoding as a Sapling or TEX address (Bech32/Bech32m) + if let Ok((hrp, data, variant)) = bech32::decode(s) { + // If we reached this point, the encoding is found to be valid Bech32 or Bech32m. 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, - // We will not define new Bech32 address encodings. - _ => { - return Err(ParseError::NotZcash); + match variant { + Variant::Bech32 => { + let net = match 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[..] + .try_into() + .map(AddressKind::Sapling) + .map_err(|_| ParseError::InvalidEncoding) + .map(|kind| ZcashAddress { net, kind }); } - }; + Variant::Bech32m => { + // Try decoding as a TEX address (Bech32m) + let net = match 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); + } + }; - return data[..] - .try_into() - .map(AddressKind::Sapling) - .map_err(|_| ParseError::InvalidEncoding) - .map(|kind| ZcashAddress { net, kind }); + 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 +144,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], variant: Variant) -> String { + bech32::encode(hrp, data.to_base32(), variant).expect("hrp is invalid") } fn encode_b58(prefix: [u8; 2], data: &[u8]) -> String { @@ -124,36 +158,18 @@ 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::Sprout(data) => encode_b58(self.net.b58_sprout_address_prefix(), data), AddressKind::Sapling(data) => encode_bech32( - match self.net { - Network::Main => sapling::MAINNET, - Network::Test => sapling::TESTNET, - Network::Regtest => sapling::REGTEST, - }, + self.net.hrp_sapling_payment_address(), data, + Variant::Bech32, ), 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, Variant::Bech32m) + } }; write!(f, "{}", encoded) } @@ -161,8 +177,10 @@ impl fmt::Display for ZcashAddress { #[cfg(test)] mod tests { + use assert_matches::assert_matches; + use super::*; - use crate::kind::unified; + use crate::{kind::unified, Network}; fn encoding(encoded: &str, decoded: ZcashAddress) { assert_eq!(decoded.to_string(), encoded); @@ -269,6 +287,74 @@ mod tests { ); } + #[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, Network::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, Network::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, Network::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, Network::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!( 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..8f5ea1903c 100644 --- a/components/zcash_address/src/kind/unified.rs +++ b/components/zcash_address/src/kind/unified.rs @@ -1,3 +1,5 @@ +//! Implementation of [ZIP 316](https://zips.z.cash/zip-0316) Unified Addresses and Viewing Keys. + use bech32::{self, FromBase32, ToBase32, Variant}; use std::cmp; use std::convert::{TryFrom, TryInto}; @@ -17,12 +19,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), } diff --git a/components/zcash_address/src/kind/unified/address.rs b/components/zcash_address/src/kind/unified/address.rs index addba7d186..00d3c5c540 100644 --- a/components/zcash_address/src/kind/unified/address.rs +++ b/components/zcash_address/src/kind/unified/address.rs @@ -1,5 +1,6 @@ +use zcash_protocol::{PoolType, ShieldedProtocol}; + use super::{private::SealedItem, ParseError, Typecode}; -use crate::kind; use std::convert::{TryFrom, TryInto}; @@ -7,9 +8,9 @@ use std::convert::{TryFrom, TryInto}; #[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,9 +57,76 @@ impl SealedItem for Receiver { } /// A Unified Address. +/// +/// # Examples +/// +/// ``` +/// # use std::convert::Infallible; +/// # use std::error::Error; +/// use zcash_address::{ +/// unified::{self, Container, Encoding}, +/// ConversionError, TryFromRawAddress, ZcashAddress, +/// }; +/// +/// # 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::Shielded(ShieldedProtocol::Orchard), + Receiver::Sapling(_) => pool_type == PoolType::Shielded(ShieldedProtocol::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. /// @@ -91,27 +159,19 @@ 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, - }; - +#[cfg(feature = "test-dependencies")] +pub mod testing { 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 +182,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 +199,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 +209,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,11 +230,33 @@ 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 assert_matches::assert_matches; + + use crate::{ + kind::unified::{private::SealedContainer, Container, Encoding}, + unified::address::testing::arb_unified_address, + Network, + }; + + use proptest::{prelude::*, sample::select}; + + use super::{Address, ParseError, Receiver, Typecode}; proptest! { #[test] diff --git a/components/zcash_address/src/kind/unified/fvk.rs b/components/zcash_address/src/kind/unified/fvk.rs index 2afc80de66..cafff00891 100644 --- a/components/zcash_address/src/kind/unified/fvk.rs +++ b/components/zcash_address/src/kind/unified/fvk.rs @@ -78,6 +78,30 @@ impl SealedItem for Fvk { } /// A Unified Full Viewing Key. +/// +/// # Examples +/// +/// ``` +/// # use std::error::Error; +/// use zcash_address::unified::{self, Container, Encoding}; +/// +/// # 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); diff --git a/components/zcash_address/src/kind/unified/ivk.rs b/components/zcash_address/src/kind/unified/ivk.rs index 31c2ad56f0..5bf34e2274 100644 --- a/components/zcash_address/src/kind/unified/ivk.rs +++ b/components/zcash_address/src/kind/unified/ivk.rs @@ -83,6 +83,30 @@ impl SealedItem for Ivk { } /// A Unified Incoming Viewing Key. +/// +/// # Examples +/// +/// ``` +/// # use std::error::Error; +/// use zcash_address::unified::{self, Container, Encoding}; +/// +/// # 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); diff --git a/components/zcash_address/src/lib.rs b/components/zcash_address/src/lib.rs index c0f7fbc62c..e5db457290 100644 --- a/components/zcash_address/src/lib.rs +++ b/components/zcash_address/src/lib.rs @@ -1,3 +1,134 @@ +//! *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, Network, TryFromRawAddress, ZcashAddress}; +//! +//! // 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::(Network::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() +//! } +//! } +//! } +//! ``` + +#![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)] + mod convert; mod encoding; mod kind; @@ -10,6 +141,9 @@ pub use convert::{ }; pub use encoding::ParseError; pub use kind::unified; +use kind::unified::Receiver; +pub use zcash_protocol::consensus::NetworkType as Network; +use zcash_protocol::{PoolType, ShieldedProtocol}; /// A Zcash address. #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -18,28 +152,15 @@ pub struct ZcashAddress { 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 { @@ -104,6 +225,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), } } @@ -139,10 +261,123 @@ 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, }), } } + + /// 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::Shielded(ShieldedProtocol::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 std::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_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 0ae49d7278..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.61.0" -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 e06f35ebb6..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, -}; - -/// 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>( - 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 d32b5d2212..0000000000 --- a/components/zcash_note_encryption/src/lib.rs +++ /dev/null @@ -1,663 +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::{borrow::ToOwned, 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 the memo. -pub const MEMO_SIZE: usize = 512; -/// The size of the authentication tag used for note encryption. -pub const AEAD_TAG_SIZE: usize = 16; - -/// The size of [`OutPlaintextBytes`]. -pub const OUT_PLAINTEXT_SIZE: usize = 32 + // pk_d - 32; // esk -/// 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 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; - - type NotePlaintextBytes: AsMut<[u8]> + for<'a> From<&'a [u8]>; - type NoteCiphertextBytes: AsRef<[u8]> + for<'a> From<&'a [u8]>; - type CompactNotePlaintextBytes: AsMut<[u8]> + for<'a> From<&'a [u8]>; - type CompactNoteCiphertextBytes: AsRef<[u8]>; - - /// 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) -> Self::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 - fn parse_note_plaintext_without_memo_ivk( - &self, - ivk: &Self::IncomingViewingKey, - plaintext: &Self::CompactNotePlaintextBytes, - ) -> 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: &Self::CompactNotePlaintextBytes, - ) -> Option<(Self::Note, Self::Recipient)>; - - /// Splits 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: &Self::NotePlaintextBytes, - ) -> (Self::CompactNotePlaintextBytes, 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. -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. Returns `None` if the output is compact. - fn enc_ciphertext(&self) -> Option; - - /// Exposes the compact note ciphertext of the output. - fn enc_ciphertext_compact(&self) -> D::CompactNoteCiphertextBytes; -} - -/// 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) -> D::NoteCiphertextBytes { - 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 mut input = D::note_plaintext_bytes(&self.note, &self.memo); - - let output = input.as_mut(); - - let tag = ChaCha20Poly1305::new(key.as_ref().into()) - .encrypt_in_place_detached([0u8; 12][..].into(), &[], output) - .unwrap(); - D::NoteCiphertextBytes::from(&[output, tag.as_ref()].concat()) - } - - /// 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 mut enc_ciphertext = output.enc_ciphertext()?.as_ref().to_owned(); - - let (plaintext, tag) = extract_tag(&mut enc_ciphertext); - - ChaCha20Poly1305::new(key.as_ref().into()) - .decrypt_in_place_detached([0u8; 12][..].into(), &[], plaintext, &tag.into()) - .ok()?; - - let (compact, memo) = domain.extract_memo(&D::NotePlaintextBytes::from(plaintext)); - let (note, to) = parse_note_plaintext_without_memo_ivk( - domain, - ivk, - ephemeral_key, - &output.cmstar_bytes(), - &compact, - )?; - - Some((note, to, memo)) -} - -fn parse_note_plaintext_without_memo_ivk( - domain: &D, - ivk: &D::IncomingViewingKey, - ephemeral_key: &EphemeralKeyBytes, - cmstar_bytes: &D::ExtractedCommitmentBytes, - plaintext: &D::CompactNotePlaintextBytes, -) -> 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: D::CompactNotePlaintextBytes = - output.enc_ciphertext_compact().as_ref().into(); - - let mut keystream = ChaCha20::new(key.as_ref().into(), [0u8; 12][..].into()); - keystream.seek(64); - keystream.apply_keystream(plaintext.as_mut()); - - 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 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 enc_ciphertext = output.enc_ciphertext()?.as_ref().to_owned(); - - let (plaintext, tag) = extract_tag(&mut enc_ciphertext); - - ChaCha20Poly1305::new(key.as_ref().into()) - .decrypt_in_place_detached([0u8; 12][..].into(), &[], plaintext, &tag.into()) - .ok()?; - - let (compact, memo) = domain.extract_memo(&plaintext.as_ref().into()); - - let (note, to) = domain.parse_note_plaintext_without_memo_ovk(&pk_d, &compact)?; - - // 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 - } -} - -// Splits the AEAD tag from the ciphertext. -fn extract_tag(enc_ciphertext: &mut Vec) -> (&mut [u8], [u8; AEAD_TAG_SIZE]) { - let tag_loc = enc_ciphertext.len() - AEAD_TAG_SIZE; - - let (plaintext, tail) = enc_ciphertext.split_at_mut(tag_loc); - - let tag: [u8; AEAD_TAG_SIZE] = tail.try_into().unwrap(); - (plaintext, tag) -} diff --git a/components/zcash_protocol/CHANGELOG.md b/components/zcash_protocol/CHANGELOG.md new file mode 100644 index 0000000000..930c92f526 --- /dev/null +++ b/components/zcash_protocol/CHANGELOG.md @@ -0,0 +1,70 @@ +# 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::PoolType::{TRANSPARENT, SAPLING, ORCHARD}` + +## [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..8b086e6ce2 --- /dev/null +++ b/components/zcash_protocol/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "zcash_protocol" +description = "Zcash protocol network constants and value types." +version = "0.1.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.workspace = true +categories = ["cryptography::cryptocurrencies"] +keywords = ["zcash"] + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +# - Logging and metrics +memuse.workspace = true + +# Dependencies used internally: +# (Breaking upgrades to these are usually backwards-compatible, but check MSRVs.) +# - Documentation +document-features.workspace = true + +# - Test dependencies +proptest = { workspace = true, optional = true } +incrementalmerkletree = { workspace = true, optional = true } + +[dev-dependencies] +proptest.workspace = true + +[features] +## Exposes APIs that are useful for testing, such as `proptest` strategies. +test-dependencies = [ + "dep:incrementalmerkletree", + "dep:proptest", + "incrementalmerkletree?/test-dependencies", +] + +## Exposes support for working with a local consensus (e.g. regtest). +local-consensus = [] 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 71% rename from zcash_primitives/src/consensus.rs rename to components/zcash_protocol/src/consensus.rs index 02ceffa165..32af0be70b 100644 --- a/zcash_primitives/src/consensus.rs +++ b/components/zcash_protocol/src/consensus.rs @@ -5,24 +5,32 @@ 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); 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 { @@ -127,232 +135,286 @@ impl Sub for BlockHeight { } } -/// 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; + 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; + fn hrp_sapling_payment_address(&self) -> &'static str; + + /// Returns the human-readable prefix for Base58Check-encoded Sprout + /// payment addresses for the network to which this NetworkConstants value + /// applies. + /// + /// Defined in the [Zcash Protocol Specification section 5.6.3][sproutpaymentaddrencoding]. + /// + /// [sproutpaymentaddrencoding]: https://zips.z.cash/protocol/protocol.pdf#sproutpaymentaddrencoding + fn b58_sprout_address_prefix(&self) -> [u8; 2]; /// Returns the human-readable prefix for Base58Check-encoded transparent - /// pay-to-public-key-hash payment addresses for the network to which this Parameters value + /// pay-to-public-key-hash payment addresses for the network to which this NetworkConstants value /// applies. /// /// [`TransparentAddress::PublicKey`]: zcash_primitives::legacy::TransparentAddress::PublicKey fn b58_pubkey_address_prefix(&self) -> [u8; 2]; /// Returns the human-readable prefix for Base58Check-encoded transparent pay-to-script-hash - /// payment addresses for the network to which this Parameters value applies. + /// payment addresses for the network to which this NetworkConstants value applies. /// /// [`TransparentAddress::Script`]: zcash_primitives::legacy::TransparentAddress::Script fn b58_script_address_prefix(&self) -> [u8; 2]; -} -/// 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 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, +} -pub const MAIN_NETWORK: MainNetwork = MainNetwork; - -impl Parameters for MainNetwork { - fn activation_height(&self, nu: NetworkUpgrade) -> Option { - match nu { - NetworkUpgrade::Overwinter => Some(BlockHeight(347_500)), - NetworkUpgrade::Sapling => Some(BlockHeight(419_200)), - NetworkUpgrade::Blossom => Some(BlockHeight(653_600)), - NetworkUpgrade::Heartwood => Some(BlockHeight(903_000)), - NetworkUpgrade::Canopy => Some(BlockHeight(1_046_400)), - NetworkUpgrade::Nu5 => Some(BlockHeight(1_687_104)), - #[cfg(feature = "zfuture")] - NetworkUpgrade::ZFuture => None, - } - } +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, + } } -} -/// Marker struct for the test network. -#[derive(PartialEq, Eq, Copy, Clone, Debug)] -pub struct TestNetwork; + 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, + } + } +} -memuse::impl_no_dynamic_usage!(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; -pub const TEST_NETWORK: TestNetwork = TestNetwork; + /// Returns the activation height for a particular network upgrade, + /// if an activation height has been set. + fn activation_height(&self, nu: NetworkUpgrade) -> Option; -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, - } + /// 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) } +} +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() + } + + fn hrp_tex_address(&self) -> &'static str { + self.network_type().hrp_tex_address() } } +/// Marker struct for the production network. #[derive(PartialEq, Eq, Copy, Clone, Debug)] -pub enum Network { - MainNetwork, - TestNetwork, -} +pub struct MainNetwork; -memuse::impl_no_dynamic_usage!(Network); +memuse::impl_no_dynamic_usage!(MainNetwork); -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), - } - } +/// The production network. +pub const MAIN_NETWORK: MainNetwork = MainNetwork; - fn coin_type(&self) -> u32 { - match self { - Network::MainNetwork => MAIN_NETWORK.coin_type(), - Network::TestNetwork => TEST_NETWORK.coin_type(), - } +impl Parameters for MainNetwork { + fn network_type(&self) -> NetworkType { + NetworkType::Main } - fn address_network(&self) -> Option { - match self { - Network::MainNetwork => Some(zcash_address::Network::Main), - Network::TestNetwork => Some(zcash_address::Network::Test), + 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(zcash_unstable = "nu6")] + NetworkUpgrade::Nu6 => None, + #[cfg(zcash_unstable = "zfuture")] + NetworkUpgrade::ZFuture => None, } } +} - 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(), - } - } +/// Marker struct for the test network. +#[derive(PartialEq, Eq, Copy, Clone, Debug)] +pub struct TestNetwork; - 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(), - } +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)), + #[cfg(zcash_unstable = "nu6")] + NetworkUpgrade::Nu6 => 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, +} + +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 +423,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,12 +449,17 @@ pub enum NetworkUpgrade { /// /// [Nu5]: https://z.cash/upgrade/nu5/ Nu5, + /// The [Nu6] network upgrade. + /// + /// [Nu6]: https://z.cash/upgrade/nu6/ + #[cfg(zcash_unstable = "nu6")] + Nu6, /// The ZFUTURE network upgrade. /// /// This upgrade is expected never to activate on mainnet; /// it is intended for use in integration testing of functionality /// that is a candidate for integration in a future network upgrade. - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] ZFuture, } @@ -407,7 +474,9 @@ impl fmt::Display for NetworkUpgrade { NetworkUpgrade::Heartwood => write!(f, "Heartwood"), NetworkUpgrade::Canopy => write!(f, "Canopy"), NetworkUpgrade::Nu5 => write!(f, "Nu5"), - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "nu6")] + NetworkUpgrade::Nu6 => write!(f, "Nu6"), + #[cfg(zcash_unstable = "zfuture")] NetworkUpgrade::ZFuture => write!(f, "ZFUTURE"), } } @@ -422,7 +491,9 @@ impl NetworkUpgrade { NetworkUpgrade::Heartwood => BranchId::Heartwood, NetworkUpgrade::Canopy => BranchId::Canopy, NetworkUpgrade::Nu5 => BranchId::Nu5, - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "nu6")] + NetworkUpgrade::Nu6 => BranchId::Nu6, + #[cfg(zcash_unstable = "zfuture")] NetworkUpgrade::ZFuture => BranchId::ZFuture, } } @@ -439,8 +510,13 @@ const UPGRADES_IN_ORDER: &[NetworkUpgrade] = &[ NetworkUpgrade::Heartwood, NetworkUpgrade::Canopy, NetworkUpgrade::Nu5, + #[cfg(zcash_unstable = "nu6")] + NetworkUpgrade::Nu6, ]; +/// The "grace period" defined in [ZIP 212]. +/// +/// [ZIP 212]: https://zips.z.cash/zip-0212#changes-to-the-process-of-receiving-sapling-or-orchard-notes pub const ZIP212_GRACE_PERIOD: u32 = 32256; /// A globally-unique identifier for a set of consensus rules within the Zcash chain. @@ -455,7 +531,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,9 +548,12 @@ pub enum BranchId { Canopy, /// The consensus rules deployed by [`NetworkUpgrade::Nu5`]. Nu5, + /// The consensus rules deployed by [`NetworkUpgrade::Nu6`]. + #[cfg(zcash_unstable = "nu6")] + Nu6, /// Candidates for future consensus rules; this branch will never /// activate on mainnet. - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] ZFuture, } @@ -492,7 +571,9 @@ impl TryFrom for BranchId { 0xf5b9_230b => Ok(BranchId::Heartwood), 0xe9ff_75a6 => Ok(BranchId::Canopy), 0xc2d6_d0b4 => Ok(BranchId::Nu5), - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "nu6")] + 0xc8e7_1055 => Ok(BranchId::Nu6), + #[cfg(zcash_unstable = "zfuture")] 0xffff_ffff => Ok(BranchId::ZFuture), _ => Err("Unknown consensus branch ID"), } @@ -509,7 +590,9 @@ impl From for u32 { BranchId::Heartwood => 0xf5b9_230b, BranchId::Canopy => 0xe9ff_75a6, BranchId::Nu5 => 0xc2d6_d0b4, - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "nu6")] + BranchId::Nu6 => 0xc8e7_1055, + #[cfg(zcash_unstable = "zfuture")] BranchId::ZFuture => 0xffff_ffff, } } @@ -575,13 +658,15 @@ impl BranchId { .activation_height(NetworkUpgrade::Canopy) .map(|lower| (lower, params.activation_height(NetworkUpgrade::Nu5))), BranchId::Nu5 => params.activation_height(NetworkUpgrade::Nu5).map(|lower| { - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] let upper = params.activation_height(NetworkUpgrade::ZFuture); - #[cfg(not(feature = "zfuture"))] + #[cfg(not(zcash_unstable = "zfuture"))] let upper = None; (lower, upper) }), - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "nu6")] + BranchId::Nu6 => None, + #[cfg(zcash_unstable = "zfuture")] BranchId::ZFuture => params .activation_height(NetworkUpgrade::ZFuture) .map(|lower| (lower, None)), @@ -609,7 +694,9 @@ pub mod testing { BranchId::Heartwood, BranchId::Canopy, BranchId::Nu5, - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "nu6")] + BranchId::Nu6, + #[cfg(zcash_unstable = "zfuture")] BranchId::ZFuture, ]) } @@ -627,15 +714,21 @@ pub mod testing { ) }) } + + #[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, }; + use std::convert::TryFrom; #[test] fn nu_ordering() { diff --git a/components/zcash_protocol/src/constants.rs b/components/zcash_protocol/src/constants.rs new file mode 100644 index 0000000000..fc56eca937 --- /dev/null +++ b/components/zcash_protocol/src/constants.rs @@ -0,0 +1,5 @@ +//! Network-specific Zcash constants. + +pub mod mainnet; +pub mod regtest; +pub mod testnet; diff --git a/components/zcash_protocol/src/constants/mainnet.rs b/components/zcash_protocol/src/constants/mainnet.rs new file mode 100644 index 0000000000..98c81caa25 --- /dev/null +++ b/components/zcash_protocol/src/constants/mainnet.rs @@ -0,0 +1,52 @@ +//! Constants for the Zcash main network. + +/// The mainnet coin type for ZEC, as defined by [SLIP 44]. +/// +/// [SLIP 44]: https://github.com/satoshilabs/slips/blob/master/slip-0044.md +pub const COIN_TYPE: u32 = 133; + +/// The HRP for a Bech32-encoded mainnet Sapling [`ExtendedSpendingKey`]. +/// +/// Defined in [ZIP 32]. +/// +/// [`ExtendedSpendingKey`]: https://docs.rs/sapling-crypto/latest/sapling_crypto/zip32/struct.ExtendedSpendingKey.html +/// [ZIP 32]: https://github.com/zcash/zips/blob/master/zip-0032.rst +pub const HRP_SAPLING_EXTENDED_SPENDING_KEY: &str = "secret-extended-key-main"; + +/// The HRP for a Bech32-encoded mainnet [`ExtendedFullViewingKey`]. +/// +/// Defined in [ZIP 32]. +/// +/// [`ExtendedFullViewingKey`]: https://docs.rs/sapling-crypto/latest/sapling_crypto/zip32/struct.ExtendedFullViewingKey.html +/// [ZIP 32]: https://github.com/zcash/zips/blob/master/zip-0032.rst +pub const HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY: &str = "zxviews"; + +/// The HRP for a Bech32-encoded mainnet Sapling [`PaymentAddress`]. +/// +/// Defined in section 5.6.4 of the [Zcash Protocol Specification]. +/// +/// [`PaymentAddress`]: https://docs.rs/sapling-crypto/latest/sapling_crypto/struct.PaymentAddress.html +/// [Zcash Protocol Specification]: https://github.com/zcash/zips/blob/master/protocol/protocol.pdf +pub const HRP_SAPLING_PAYMENT_ADDRESS: &str = "zs"; + +/// The prefix for a Base58Check-encoded mainnet Sprout address. +/// +/// Defined in the [Zcash Protocol Specification section 5.6.3][sproutpaymentaddrencoding]. +/// +/// [sproutpaymentaddrencoding]: https://zips.z.cash/protocol/protocol.pdf#sproutpaymentaddrencoding +pub const B58_SPROUT_ADDRESS_PREFIX: [u8; 2] = [0x16, 0x9a]; + +/// The prefix for a Base58Check-encoded mainnet [`PublicKeyHash`]. +/// +/// [`PublicKeyHash`]: https://docs.rs/zcash_primitives/latest/zcash_primitives/legacy/enum.TransparentAddress.html +pub const B58_PUBKEY_ADDRESS_PREFIX: [u8; 2] = [0x1c, 0xb8]; + +/// The prefix for a Base58Check-encoded mainnet [`ScriptHash`]. +/// +/// [`ScriptHash`]: https://docs.rs/zcash_primitives/latest/zcash_primitives/legacy/enum.TransparentAddress.html +pub const B58_SCRIPT_ADDRESS_PREFIX: [u8; 2] = [0x1c, 0xbd]; + +/// The HRP for a Bech32m-encoded mainnet [ZIP 320] TEX address. +/// +/// [ZIP 320]: https://zips.z.cash/zip-0320 +pub const HRP_TEX_ADDRESS: &str = "tex"; diff --git a/components/zcash_protocol/src/constants/regtest.rs b/components/zcash_protocol/src/constants/regtest.rs new file mode 100644 index 0000000000..001baa7ea4 --- /dev/null +++ b/components/zcash_protocol/src/constants/regtest.rs @@ -0,0 +1,59 @@ +//! # 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"; diff --git a/components/zcash_protocol/src/constants/testnet.rs b/components/zcash_protocol/src/constants/testnet.rs new file mode 100644 index 0000000000..023926546e --- /dev/null +++ b/components/zcash_protocol/src/constants/testnet.rs @@ -0,0 +1,52 @@ +//! Constants for the Zcash test network. + +/// The testnet coin type for ZEC, as defined by [SLIP 44]. +/// +/// [SLIP 44]: https://github.com/satoshilabs/slips/blob/master/slip-0044.md +pub const COIN_TYPE: u32 = 1; + +/// The HRP for a Bech32-encoded testnet Sapling [`ExtendedSpendingKey`]. +/// +/// Defined in [ZIP 32]. +/// +/// [`ExtendedSpendingKey`]: https://docs.rs/sapling-crypto/latest/sapling_crypto/zip32/struct.ExtendedSpendingKey.html +/// [ZIP 32]: https://github.com/zcash/zips/blob/master/zip-0032.rst +pub const HRP_SAPLING_EXTENDED_SPENDING_KEY: &str = "secret-extended-key-test"; + +/// The HRP for a Bech32-encoded testnet Sapling [`ExtendedFullViewingKey`]. +/// +/// Defined in [ZIP 32]. +/// +/// [`ExtendedFullViewingKey`]: https://docs.rs/sapling-crypto/latest/sapling_crypto/zip32/struct.ExtendedFullViewingKey.html +/// [ZIP 32]: https://github.com/zcash/zips/blob/master/zip-0032.rst +pub const HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY: &str = "zxviewtestsapling"; + +/// The HRP for a Bech32-encoded testnet Sapling [`PaymentAddress`]. +/// +/// Defined in section 5.6.4 of the [Zcash Protocol Specification]. +/// +/// [`PaymentAddress`]: https://docs.rs/sapling-crypto/latest/sapling_crypto/struct.PaymentAddress.html +/// [Zcash Protocol Specification]: https://github.com/zcash/zips/blob/master/protocol/protocol.pdf +pub const HRP_SAPLING_PAYMENT_ADDRESS: &str = "ztestsapling"; + +/// The prefix for a Base58Check-encoded testnet Sprout address. +/// +/// Defined in the [Zcash Protocol Specification section 5.6.3][sproutpaymentaddrencoding]. +/// +/// [sproutpaymentaddrencoding]: https://zips.z.cash/protocol/protocol.pdf#sproutpaymentaddrencoding +pub const B58_SPROUT_ADDRESS_PREFIX: [u8; 2] = [0x16, 0xb6]; + +/// The prefix for a Base58Check-encoded testnet transparent [`PublicKeyHash`]. +/// +/// [`PublicKeyHash`]: https://docs.rs/zcash_primitives/latest/zcash_primitives/legacy/enum.TransparentAddress.html +pub const B58_PUBKEY_ADDRESS_PREFIX: [u8; 2] = [0x1d, 0x25]; + +/// The prefix for a Base58Check-encoded testnet transparent [`ScriptHash`]. +/// +/// [`ScriptHash`]: https://docs.rs/zcash_primitives/latest/zcash_primitives/legacy/enum.TransparentAddress.html +pub const B58_SCRIPT_ADDRESS_PREFIX: [u8; 2] = [0x1c, 0xba]; + +/// The HRP for a Bech32m-encoded testnet [ZIP 320] TEX address. +/// +/// [ZIP 320]: https://zips.z.cash/zip-0320 +pub const HRP_TEX_ADDRESS: &str = "textest"; diff --git a/components/zcash_protocol/src/lib.rs b/components/zcash_protocol/src/lib.rs new file mode 100644 index 0000000000..f73564751d --- /dev/null +++ b/components/zcash_protocol/src/lib.rs @@ -0,0 +1,59 @@ +//! *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. +//! +//! ## 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)] + +use core::fmt; + +pub mod consensus; +pub mod constants; +#[cfg(feature = "local-consensus")] +pub mod local_consensus; +pub mod memo; +pub mod value; + +/// 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..5d277cf1be --- /dev/null +++ b/components/zcash_protocol/src/local_consensus.rs @@ -0,0 +1,219 @@ +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 +/// ``` +/// 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)), +/// }; +/// ``` +/// +#[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, + #[cfg(zcash_unstable = "nu6")] + pub nu6: Option, + #[cfg(zcash_unstable = "zfuture")] + pub z_future: Option, +} + +/// Parameters implementation for `LocalNetwork` +impl Parameters for LocalNetwork { + fn network_type(&self) -> NetworkType { + NetworkType::Regtest + } + + fn activation_height(&self, nu: NetworkUpgrade) -> Option { + match nu { + NetworkUpgrade::Overwinter => self.overwinter, + NetworkUpgrade::Sapling => self.sapling, + NetworkUpgrade::Blossom => self.blossom, + NetworkUpgrade::Heartwood => self.heartwood, + NetworkUpgrade::Canopy => self.canopy, + NetworkUpgrade::Nu5 => self.nu5, + #[cfg(zcash_unstable = "nu6")] + NetworkUpgrade::Nu6 => self.nu6, + #[cfg(zcash_unstable = "zfuture")] + NetworkUpgrade::ZFuture => self.z_future, + } + } +} + +#[cfg(test)] +mod tests { + use crate::{ + consensus::{BlockHeight, NetworkConstants, NetworkUpgrade, Parameters}, + constants, + local_consensus::LocalNetwork, + }; + + #[test] + fn regtest_nu_activation() { + let expected_overwinter = BlockHeight::from_u32(1); + let expected_sapling = BlockHeight::from_u32(2); + let expected_blossom = BlockHeight::from_u32(3); + let expected_heartwood = BlockHeight::from_u32(4); + let expected_canopy = BlockHeight::from_u32(5); + let expected_nu5 = BlockHeight::from_u32(6); + #[cfg(zcash_unstable = "nu6")] + let expected_nu6 = BlockHeight::from_u32(7); + #[cfg(zcash_unstable = "zfuture")] + let expected_z_future = BlockHeight::from_u32(7); + + 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), + #[cfg(zcash_unstable = "nu6")] + nu6: Some(expected_nu6), + #[cfg(zcash_unstable = "zfuture")] + z_future: Some(expected_z_future), + }; + + assert!(regtest.is_nu_active(NetworkUpgrade::Overwinter, expected_overwinter)); + assert!(regtest.is_nu_active(NetworkUpgrade::Sapling, expected_sapling)); + assert!(regtest.is_nu_active(NetworkUpgrade::Blossom, expected_blossom)); + assert!(regtest.is_nu_active(NetworkUpgrade::Heartwood, expected_heartwood)); + assert!(regtest.is_nu_active(NetworkUpgrade::Canopy, expected_canopy)); + assert!(regtest.is_nu_active(NetworkUpgrade::Nu5, expected_nu5)); + #[cfg(zcash_unstable = "nu6")] + assert!(regtest.is_nu_active(NetworkUpgrade::Nu6, expected_nu6)); + #[cfg(zcash_unstable = "zfuture")] + assert!(!regtest.is_nu_active(NetworkUpgrade::ZFuture, expected_nu5)); + } + + #[test] + fn regtest_activation_heights() { + let expected_overwinter = BlockHeight::from_u32(1); + let expected_sapling = BlockHeight::from_u32(2); + let expected_blossom = BlockHeight::from_u32(3); + let expected_heartwood = BlockHeight::from_u32(4); + let expected_canopy = BlockHeight::from_u32(5); + let expected_nu5 = BlockHeight::from_u32(6); + #[cfg(zcash_unstable = "nu6")] + let expected_nu6 = BlockHeight::from_u32(7); + #[cfg(zcash_unstable = "zfuture")] + let expected_z_future = BlockHeight::from_u32(7); + + 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), + #[cfg(zcash_unstable = "nu6")] + nu6: Some(expected_nu6), + #[cfg(zcash_unstable = "zfuture")] + z_future: Some(expected_z_future), + }; + + assert_eq!( + regtest.activation_height(NetworkUpgrade::Overwinter), + Some(expected_overwinter) + ); + assert_eq!( + regtest.activation_height(NetworkUpgrade::Sapling), + Some(expected_sapling) + ); + assert_eq!( + regtest.activation_height(NetworkUpgrade::Blossom), + Some(expected_blossom) + ); + assert_eq!( + regtest.activation_height(NetworkUpgrade::Heartwood), + Some(expected_heartwood) + ); + assert_eq!( + regtest.activation_height(NetworkUpgrade::Canopy), + Some(expected_canopy) + ); + assert_eq!( + regtest.activation_height(NetworkUpgrade::Nu5), + Some(expected_nu5) + ); + #[cfg(zcash_unstable = "zfuture")] + assert_eq!( + regtest.activation_height(NetworkUpgrade::ZFuture), + Some(expected_z_future) + ); + } + + #[test] + fn regtests_constants() { + let expected_overwinter = BlockHeight::from_u32(1); + let expected_sapling = BlockHeight::from_u32(2); + let expected_blossom = BlockHeight::from_u32(3); + let expected_heartwood = BlockHeight::from_u32(4); + let expected_canopy = BlockHeight::from_u32(5); + let expected_nu5 = BlockHeight::from_u32(6); + #[cfg(zcash_unstable = "nu6")] + let expected_nu6 = BlockHeight::from_u32(7); + #[cfg(zcash_unstable = "zfuture")] + let expected_z_future = BlockHeight::from_u32(7); + + 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), + #[cfg(zcash_unstable = "nu6")] + nu6: Some(expected_nu6), + #[cfg(zcash_unstable = "zfuture")] + z_future: Some(expected_z_future), + }; + + assert_eq!(regtest.coin_type(), constants::regtest::COIN_TYPE); + assert_eq!( + regtest.hrp_sapling_extended_spending_key(), + constants::regtest::HRP_SAPLING_EXTENDED_SPENDING_KEY + ); + assert_eq!( + regtest.hrp_sapling_extended_full_viewing_key(), + constants::regtest::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY + ); + assert_eq!( + regtest.hrp_sapling_payment_address(), + constants::regtest::HRP_SAPLING_PAYMENT_ADDRESS + ); + assert_eq!( + regtest.b58_pubkey_address_prefix(), + constants::regtest::B58_PUBKEY_ADDRESS_PREFIX + ); + assert_eq!( + regtest.b58_script_address_prefix(), + constants::regtest::B58_SCRIPT_ADDRESS_PREFIX + ); + } +} diff --git a/zcash_primitives/src/memo.rs b/components/zcash_protocol/src/memo.rs similarity index 97% rename from zcash_primitives/src/memo.rs rename to components/zcash_protocol/src/memo.rs index 8143eec69d..10258a52d9 100644 --- a/zcash_primitives/src/memo.rs +++ b/components/zcash_protocol/src/memo.rs @@ -28,7 +28,7 @@ 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), TooLong(usize), @@ -144,9 +144,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 +172,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 +192,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())), } } } diff --git a/components/zcash_protocol/src/value.rs b/components/zcash_protocol/src/value.rs new file mode 100644 index 0000000000..395ac8edc2 --- /dev/null +++ b/components/zcash_protocol/src/value.rs @@ -0,0 +1,521 @@ +use std::convert::{Infallible, TryFrom}; +use std::error; +use std::iter::Sum; +use std::ops::{Add, Mul, Neg, Sub}; + +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); + +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>(iter: I) -> Self { + iter.fold(Some(ZatBalance::zero()), |acc, a| acc? + a) + } +} + +impl<'a> Sum<&'a ZatBalance> for Option { + fn sum>(iter: I) -> Self { + iter.fold(Some(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); + +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 + } +} + +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: usize) -> Option { + Zatoshis::from_u64(self.0.checked_mul(u64::try_from(rhs).ok()?)?).ok() + } +} + +impl Sum for Option { + fn sum>(iter: I) -> Self { + iter.fold(Some(Zatoshis::ZERO), |acc, a| acc? + a) + } +} + +impl<'a> Sum<&'a Zatoshis> for Option { + fn sum>(iter: I) -> Self { + iter.fold(Some(Zatoshis::ZERO), |acc, a| acc? + *a) + } +} + +/// 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 error::Error for BalanceError {} + +impl std::fmt::Display for BalanceError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::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..0cdc4ecb6f --- /dev/null +++ b/components/zip321/CHANGELOG.md @@ -0,0 +1,34 @@ +# 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] +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` diff --git a/components/zip321/Cargo.toml b/components/zip321/Cargo.toml new file mode 100644 index 0000000000..8423f72e9f --- /dev/null +++ b/components/zip321/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "zip321" +description = "Parsing functions and data types for Zcash ZIP 321 Payment Request URIs" +version = "0.0.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 + +# - Parsing and Encoding +nom = "7" +base64.workspace = true +percent-encoding.workspace = true + +[dev-dependencies] +zcash_address = { workspace = true, features = ["test-dependencies"] } +zcash_protocol = { workspace = true, features = ["test-dependencies"] } +proptest.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..91974da43a 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) + .fold(Ok(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) - } + 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,67 @@ 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 zcash_address::{testing::arb_address, ZcashAddress}; + use zcash_protocol::{ + consensus::NetworkType, + memo::{Memo, MemoBytes}, + value::{testing::arb_zatoshis, Zatoshis}, }; - use crate::address::RecipientAddress; + #[cfg(feature = "local-consensus")] + use zcash_protocol::{local_consensus::LocalNetwork, BlockHeight}; 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 +898,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 +908,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 +936,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 +993,163 @@ 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)) + ); + } + + #[cfg(feature = "local-consensus")] + #[test] + fn test_zip321_spec_regtest_valid_examples() { + let params = 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)), + z_future: Some(BlockHeight::from_u32(1)), + }; + let valid_1 = "zcash:zregtestsapling1qqqqqqqqqqqqqqqqqqcguyvaw2vjk4sdyeg0lc970u659lvhqq7t0np6hlup5lusxle7505hlz3?amount=1&memo=VGhpcyBpcyBhIHNpbXBsZSBtZW1vLg&message=Thank%20you%20for%20your%20purchase"; + let v1r = TransactionRequest::from_uri(¶ms, 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 +1157,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 +1167,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..79248160da --- /dev/null +++ b/deny.toml @@ -0,0 +1,51 @@ +# 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 = "matchit", allow = ["BSD-3-Clause"] }, + { name = "minreq", allow = ["ISC"] }, + { name = "ring", allow = ["LicenseRef-ring"] }, + { name = "rustls-webpki", allow = ["ISC"] }, + { name = "secp256k1", allow = ["CC0-1.0"] }, + { name = "secp256k1-sys", allow = ["CC0-1.0"] }, + { name = "subtle", allow = ["BSD-3-Clause"] }, + { name = "unicode-ident", allow = ["Unicode-DFS-2016"] }, + { name = "untrusted", allow = ["ISC"] }, + { name = "webpki-roots", allow = ["MPL-2.0"] }, +] + +[[licenses.clarify]] +name = "ring" +expression = "LicenseRef-ring" +license-files = [ + { path = "LICENSE", hash = 0xbd0eed23 }, +] diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 5ecda6e495..cad9254a9c 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.65.0" +channel = "1.70.0" components = [ "clippy", "rustfmt" ] diff --git a/supply-chain/audits.toml b/supply-chain/audits.toml new file mode 100644 index 0000000000..4a1f0b0177 --- /dev/null +++ b/supply-chain/audits.toml @@ -0,0 +1,238 @@ + +# 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] + +[[trusted.equihash]] +criteria = "safe-to-deploy" +user-id = 6289 # str4d +start = "2020-06-26" +end = "2025-04-22" + +[[trusted.f4jumble]] +criteria = ["safe-to-deploy", "crypto-reviewed"] +user-id = 6289 # str4d +start = "2021-09-22" +end = "2025-04-22" + +[[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_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 = 6289 # str4d +start = "2021-12-17" +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 = 169181 # Kris Nuttycombe (nuttycom) +start = "2023-02-28" +end = "2025-04-22" + +[[trusted.orchard]] +criteria = ["safe-to-deploy", "crypto-reviewed", "license-reviewed"] +user-id = 6289 # str4d +start = "2021-01-07" +end = "2025-04-22" + +[[trusted.orchard]] +criteria = ["safe-to-deploy", "crypto-reviewed", "license-reviewed"] +user-id = 1244 # ebfull +start = "2022-10-19" +end = "2025-04-22" + +[[trusted.sapling-crypto]] +criteria = ["safe-to-deploy", "crypto-reviewed"] +user-id = 6289 # str4d +start = "2024-01-26" +end = "2025-04-22" + +[[trusted.shardtree]] +criteria = "safe-to-deploy" +user-id = 169181 # Kris Nuttycombe (nuttycom) +start = "2022-12-15" +end = "2025-04-22" + +[[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_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_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 # str4d +start = "2021-03-07" +end = "2025-04-22" + +[[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 = 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_extensions]] +criteria = "safe-to-deploy" +user-id = 6289 # 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 # 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", "crypto-reviewed", "license-reviewed"] +user-id = 6289 # str4d +start = "2021-03-26" +end = "2025-04-22" + +[[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_proofs]] +criteria = ["safe-to-deploy", "crypto-reviewed", "license-reviewed"] +user-id = 6289 # str4d +start = "2021-03-26" +end = "2025-04-22" + +[[trusted.zcash_protocol]] +criteria = "safe-to-deploy" +user-id = 169181 # Kris Nuttycombe (nuttycom) +start = "2024-01-27" +end = "2025-04-22" + +[[trusted.zcash_spec]] +criteria = ["safe-to-deploy", "crypto-reviewed", "license-reviewed"] +user-id = 6289 # str4d +start = "2023-12-07" +end = "2025-04-22" + +[[trusted.zip32]] +criteria = "safe-to-deploy" +user-id = 6289 # str4d +start = "2023-12-06" +end = "2025-04-22" + +[[trusted.zip321]] +criteria = "safe-to-deploy" +user-id = 169181 # Kris Nuttycombe (nuttycom) +start = "2024-01-15" +end = "2025-04-22" diff --git a/supply-chain/config.toml b/supply-chain/config.toml new file mode 100644 index 0000000000..5f5146c50c --- /dev/null +++ b/supply-chain/config.toml @@ -0,0 +1,929 @@ + +# cargo-vet config file + +[cargo-vet] +version = "0.9" + +[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.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.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.allocator-api2]] +version = "0.2.16" +criteria = "safe-to-deploy" + +[[exemptions.arrayvec]] +version = "0.7.4" +criteria = "safe-to-deploy" + +[[exemptions.assert_matches]] +version = "1.5.0" +criteria = "safe-to-deploy" + +[[exemptions.async-trait]] +version = "0.1.78" +criteria = "safe-to-deploy" + +[[exemptions.axum]] +version = "0.6.20" +criteria = "safe-to-deploy" + +[[exemptions.axum-core]] +version = "0.3.4" +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.0.1" +criteria = "safe-to-deploy" + +[[exemptions.bech32]] +version = "0.9.1" +criteria = "safe-to-deploy" + +[[exemptions.bellman]] +version = "0.14.0" +criteria = "safe-to-deploy" + +[[exemptions.bip0039]] +version = "0.10.1" +criteria = "safe-to-deploy" + +[[exemptions.bitflags]] +version = "1.3.2" +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.bls12_381]] +version = "0.8.0" +criteria = "safe-to-deploy" + +[[exemptions.bs58]] +version = "0.5.0" +criteria = "safe-to-deploy" + +[[exemptions.bytemuck]] +version = "1.14.0" +criteria = "safe-to-run" + +[[exemptions.byteorder]] +version = "1.5.0" +criteria = "safe-to-deploy" + +[[exemptions.bytes]] +version = "1.5.0" +criteria = "safe-to-deploy" + +[[exemptions.cast]] +version = "0.3.0" +criteria = "safe-to-run" + +[[exemptions.cbc]] +version = "0.1.2" +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.ciborium]] +version = "0.2.1" +criteria = "safe-to-run" + +[[exemptions.ciborium-io]] +version = "0.2.1" +criteria = "safe-to-run" + +[[exemptions.ciborium-ll]] +version = "0.2.1" +criteria = "safe-to-run" + +[[exemptions.cipher]] +version = "0.4.4" +criteria = "safe-to-deploy" + +[[exemptions.clap]] +version = "3.2.25" +criteria = "safe-to-run" + +[[exemptions.const-oid]] +version = "0.9.6" +criteria = "safe-to-deploy" + +[[exemptions.cpp_demangle]] +version = "0.4.3" +criteria = "safe-to-run" + +[[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.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.16" +criteria = "safe-to-deploy" + +[[exemptions.crypto-bigint]] +version = "0.5.5" +criteria = "safe-to-deploy" + +[[exemptions.daggy]] +version = "0.8.0" +criteria = "safe-to-deploy" + +[[exemptions.der]] +version = "0.7.9" +criteria = "safe-to-deploy" + +[[exemptions.digest]] +version = "0.10.7" +criteria = "safe-to-deploy" + +[[exemptions.ecdsa]] +version = "0.16.9" +criteria = "safe-to-deploy" + +[[exemptions.elliptic-curve]] +version = "0.13.7" +criteria = "safe-to-deploy" + +[[exemptions.errno]] +version = "0.3.6" +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.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.fpe]] +version = "0.6.1" +criteria = "safe-to-deploy" + +[[exemptions.funty]] +version = "2.0.0" +criteria = "safe-to-deploy" + +[[exemptions.futures-macro]] +version = "0.3.29" +criteria = "safe-to-deploy" + +[[exemptions.futures-sink]] +version = "0.3.29" +criteria = "safe-to-deploy" + +[[exemptions.futures-task]] +version = "0.3.29" +criteria = "safe-to-deploy" + +[[exemptions.futures-util]] +version = "0.3.29" +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.0" +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.hashbrown]] +version = "0.14.2" +criteria = "safe-to-deploy" + +[[exemptions.hashlink]] +version = "0.8.4" +criteria = "safe-to-deploy" + +[[exemptions.hdwallet]] +version = "0.4.1" +criteria = "safe-to-deploy" + +[[exemptions.hermit-abi]] +version = "0.1.19" +criteria = "safe-to-run" + +[[exemptions.hermit-abi]] +version = "0.3.3" +criteria = "safe-to-deploy" + +[[exemptions.home]] +version = "0.5.5" +criteria = "safe-to-deploy" + +[[exemptions.http]] +version = "0.2.9" +criteria = "safe-to-deploy" + +[[exemptions.http-body]] +version = "0.4.5" +criteria = "safe-to-deploy" + +[[exemptions.httparse]] +version = "1.8.0" +criteria = "safe-to-deploy" + +[[exemptions.hyper]] +version = "0.14.27" +criteria = "safe-to-deploy" + +[[exemptions.hyper-timeout]] +version = "0.4.1" +criteria = "safe-to-deploy" + +[[exemptions.indexmap]] +version = "1.9.3" +criteria = "safe-to-deploy" + +[[exemptions.indexmap]] +version = "2.1.0" +criteria = "safe-to-deploy" + +[[exemptions.inferno]] +version = "0.11.17" +criteria = "safe-to-run" + +[[exemptions.itertools]] +version = "0.11.0" +criteria = "safe-to-deploy" + +[[exemptions.itoa]] +version = "1.0.9" +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.k256]] +version = "0.13.2" +criteria = "safe-to-deploy" + +[[exemptions.libc]] +version = "0.2.150" +criteria = "safe-to-deploy" + +[[exemptions.libm]] +version = "0.2.2" +criteria = "safe-to-deploy" + +[[exemptions.libsqlite3-sys]] +version = "0.26.0" +criteria = "safe-to-deploy" + +[[exemptions.linux-raw-sys]] +version = "0.4.11" +criteria = "safe-to-deploy" + +[[exemptions.lock_api]] +version = "0.4.11" +criteria = "safe-to-run" + +[[exemptions.matchit]] +version = "0.7.3" +criteria = "safe-to-deploy" + +[[exemptions.memchr]] +version = "2.6.4" +criteria = "safe-to-deploy" + +[[exemptions.memmap2]] +version = "0.5.10" +criteria = "safe-to-run" + +[[exemptions.memoffset]] +version = "0.9.0" +criteria = "safe-to-deploy" + +[[exemptions.memuse]] +version = "0.2.1" +criteria = "safe-to-deploy" + +[[exemptions.mime]] +version = "0.3.17" +criteria = "safe-to-deploy" + +[[exemptions.minimal-lexical]] +version = "0.2.1" +criteria = "safe-to-deploy" + +[[exemptions.minreq]] +version = "2.11.0" +criteria = "safe-to-deploy" + +[[exemptions.mio]] +version = "0.8.9" +criteria = "safe-to-deploy" + +[[exemptions.multimap]] +version = "0.8.3" +criteria = "safe-to-deploy" + +[[exemptions.nonempty]] +version = "0.7.0" +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.os_str_bytes]] +version = "6.6.1" +criteria = "safe-to-run" + +[[exemptions.pairing]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.parking_lot_core]] +version = "0.9.9" +criteria = "safe-to-run" + +[[exemptions.password-hash]] +version = "0.3.2" +criteria = "safe-to-deploy" + +[[exemptions.pasta_curves]] +version = "0.5.1" +criteria = "safe-to-deploy" + +[[exemptions.pbkdf2]] +version = "0.10.1" +criteria = "safe-to-deploy" + +[[exemptions.petgraph]] +version = "0.6.4" +criteria = "safe-to-deploy" + +[[exemptions.pin-project]] +version = "1.1.3" +criteria = "safe-to-deploy" + +[[exemptions.pin-project-internal]] +version = "1.1.3" +criteria = "safe-to-deploy" + +[[exemptions.pkcs8]] +version = "0.10.2" +criteria = "safe-to-deploy" + +[[exemptions.pkg-config]] +version = "0.3.27" +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.pprof]] +version = "0.11.1" +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.primitive-types]] +version = "0.12.2" +criteria = "safe-to-deploy" + +[[exemptions.proptest]] +version = "1.3.1" +criteria = "safe-to-deploy" + +[[exemptions.prost]] +version = "0.12.1" +criteria = "safe-to-deploy" + +[[exemptions.prost-build]] +version = "0.12.1" +criteria = "safe-to-deploy" + +[[exemptions.prost-derive]] +version = "0.12.1" +criteria = "safe-to-deploy" + +[[exemptions.prost-types]] +version = "0.12.1" +criteria = "safe-to-deploy" + +[[exemptions.quick-error]] +version = "1.2.3" +criteria = "safe-to-deploy" + +[[exemptions.quick-xml]] +version = "0.26.0" +criteria = "safe-to-run" + +[[exemptions.radium]] +version = "0.7.0" +criteria = "safe-to-deploy" + +[[exemptions.rand]] +version = "0.8.5" +criteria = "safe-to-deploy" + +[[exemptions.reddsa]] +version = "0.5.1" +criteria = "safe-to-deploy" + +[[exemptions.redox_syscall]] +version = "0.4.1" +criteria = "safe-to-deploy" + +[[exemptions.regex]] +version = "1.10.2" +criteria = "safe-to-deploy" + +[[exemptions.regex-automata]] +version = "0.4.3" +criteria = "safe-to-deploy" + +[[exemptions.regex-syntax]] +version = "0.7.5" +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.20" +criteria = "safe-to-deploy" + +[[exemptions.ring]] +version = "0.17.5" +criteria = "safe-to-deploy" + +[[exemptions.ripemd]] +version = "0.1.3" +criteria = "safe-to-deploy" + +[[exemptions.rusqlite]] +version = "0.29.0" +criteria = "safe-to-deploy" + +[[exemptions.rustix]] +version = "0.38.21" +criteria = "safe-to-deploy" + +[[exemptions.rustls]] +version = "0.21.8" +criteria = "safe-to-deploy" + +[[exemptions.rustls-webpki]] +version = "0.101.7" +criteria = "safe-to-deploy" + +[[exemptions.rusty-fork]] +version = "0.3.0" +criteria = "safe-to-deploy" + +[[exemptions.ryu]] +version = "1.0.15" +criteria = "safe-to-run" + +[[exemptions.schemer]] +version = "0.2.1" +criteria = "safe-to-deploy" + +[[exemptions.schemer-rusqlite]] +version = "0.2.2" +criteria = "safe-to-deploy" + +[[exemptions.scopeguard]] +version = "1.2.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.26.0" +criteria = "safe-to-deploy" + +[[exemptions.secp256k1-sys]] +version = "0.8.1" +criteria = "safe-to-deploy" + +[[exemptions.secrecy]] +version = "0.8.0" +criteria = "safe-to-deploy" + +[[exemptions.serde]] +version = "1.0.192" +criteria = "safe-to-deploy" + +[[exemptions.serde_derive]] +version = "1.0.192" +criteria = "safe-to-deploy" + +[[exemptions.sha2]] +version = "0.10.8" +criteria = "safe-to-deploy" + +[[exemptions.signature]] +version = "2.2.0" +criteria = "safe-to-deploy" + +[[exemptions.slab]] +version = "0.4.9" +criteria = "safe-to-deploy" + +[[exemptions.smallvec]] +version = "1.11.1" +criteria = "safe-to-deploy" + +[[exemptions.socket2]] +version = "0.4.10" +criteria = "safe-to-deploy" + +[[exemptions.socket2]] +version = "0.5.5" +criteria = "safe-to-deploy" + +[[exemptions.spin]] +version = "0.5.2" +criteria = "safe-to-deploy" + +[[exemptions.spin]] +version = "0.9.8" +criteria = "safe-to-deploy" + +[[exemptions.spki]] +version = "0.7.3" +criteria = "safe-to-deploy" + +[[exemptions.str_stack]] +version = "0.1.0" +criteria = "safe-to-run" + +[[exemptions.subtle]] +version = "2.4.1" +criteria = "safe-to-deploy" + +[[exemptions.symbolic-common]] +version = "10.2.1" +criteria = "safe-to-run" + +[[exemptions.symbolic-demangle]] +version = "10.2.1" +criteria = "safe-to-run" + +[[exemptions.syn]] +version = "2.0.53" +criteria = "safe-to-deploy" + +[[exemptions.sync_wrapper]] +version = "0.1.2" +criteria = "safe-to-deploy" + +[[exemptions.tempfile]] +version = "3.8.1" +criteria = "safe-to-deploy" + +[[exemptions.time]] +version = "0.3.23" +criteria = "safe-to-deploy" + +[[exemptions.tinytemplate]] +version = "1.2.1" +criteria = "safe-to-run" + +[[exemptions.tokio]] +version = "1.35.1" +criteria = "safe-to-deploy" + +[[exemptions.tokio-io-timeout]] +version = "1.2.0" +criteria = "safe-to-deploy" + +[[exemptions.tokio-util]] +version = "0.7.10" +criteria = "safe-to-deploy" + +[[exemptions.tonic]] +version = "0.10.2" +criteria = "safe-to-deploy" + +[[exemptions.tonic-build]] +version = "0.10.2" +criteria = "safe-to-deploy" + +[[exemptions.tower]] +version = "0.4.13" +criteria = "safe-to-deploy" + +[[exemptions.tower-layer]] +version = "0.3.2" +criteria = "safe-to-deploy" + +[[exemptions.tower-service]] +version = "0.3.2" +criteria = "safe-to-deploy" + +[[exemptions.tracing]] +version = "0.1.40" +criteria = "safe-to-deploy" + +[[exemptions.tracing-attributes]] +version = "0.1.27" +criteria = "safe-to-deploy" + +[[exemptions.tracing-core]] +version = "0.1.32" +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.untrusted]] +version = "0.9.0" +criteria = "safe-to-deploy" + +[[exemptions.uuid]] +version = "1.5.0" +criteria = "safe-to-deploy" + +[[exemptions.wait-timeout]] +version = "0.2.0" +criteria = "safe-to-deploy" + +[[exemptions.walkdir]] +version = "2.4.0" +criteria = "safe-to-run" + +[[exemptions.wasi]] +version = "0.11.0+wasi-snapshot-preview1" +criteria = "safe-to-deploy" + +[[exemptions.wasm-bindgen]] +version = "0.2.88" +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.wasm-bindgen-macro-support]] +version = "0.2.88" +criteria = "safe-to-deploy" + +[[exemptions.wasm-bindgen-shared]] +version = "0.2.88" +criteria = "safe-to-deploy" + +[[exemptions.web-sys]] +version = "0.3.65" +criteria = "safe-to-deploy" + +[[exemptions.which]] +version = "4.4.2" +criteria = "safe-to-deploy" + +[[exemptions.winapi]] +version = "0.3.9" +criteria = "safe-to-deploy" + +[[exemptions.winapi-i686-pc-windows-gnu]] +version = "0.4.0" +criteria = "safe-to-deploy" + +[[exemptions.winapi-x86_64-pc-windows-gnu]] +version = "0.4.0" +criteria = "safe-to-deploy" + +[[exemptions.wyz]] +version = "0.5.1" +criteria = "safe-to-deploy" + +[[exemptions.xdg]] +version = "2.5.2" +criteria = "safe-to-deploy" + +[[exemptions.zerocopy]] +version = "0.7.25" +criteria = "safe-to-deploy" + +[[exemptions.zerocopy-derive]] +version = "0.7.25" +criteria = "safe-to-deploy" + +[[exemptions.zeroize]] +version = "1.6.0" +criteria = "safe-to-deploy" + +[[exemptions.zeroize_derive]] +version = "1.4.2" +criteria = "safe-to-deploy" diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock new file mode 100644 index 0000000000..97a43b7754 --- /dev/null +++ b/supply-chain/imports.lock @@ -0,0 +1,1444 @@ + +# cargo-vet imports lock + +[[publisher.bumpalo]] +version = "3.14.0" +when = "2023-09-14" +user-id = 696 +user-login = "fitzgen" +user-name = "Nick Fitzgerald" + +[[publisher.equihash]] +version = "0.2.0" +when = "2022-06-24" +user-id = 6289 +user-login = "str4d" + +[[publisher.f4jumble]] +version = "0.1.0" +when = "2022-05-10" +user-id = 6289 +user-login = "str4d" + +[[publisher.halo2_gadgets]] +version = "0.3.0" +when = "2023-03-22" +user-id = 1244 +user-login = "ebfull" + +[[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_proofs]] +version = "0.3.0" +when = "2023-03-22" +user-id = 1244 +user-login = "ebfull" + +[[publisher.incrementalmerkletree]] +version = "0.5.1" +when = "2024-03-25" +user-id = 169181 +user-login = "nuttycom" +user-name = "Kris Nuttycombe" + +[[publisher.orchard]] +version = "0.8.0" +when = "2024-03-25" +user-id = 6289 +user-login = "str4d" + +[[publisher.sapling-crypto]] +version = "0.1.3" +when = "2024-03-25" +user-id = 6289 +user-login = "str4d" + +[[publisher.shardtree]] +version = "0.3.0" +when = "2024-03-25" +user-id = 169181 +user-login = "nuttycom" +user-name = "Kris Nuttycombe" + +[[publisher.unicode-normalization]] +version = "0.1.22" +when = "2022-09-16" +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-targets]] +version = "0.48.5" +when = "2023-08-18" +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_msvc]] +version = "0.48.5" +when = "2023-08-18" +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_msvc]] +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.48.5" +when = "2023-08-18" +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_msvc]] +version = "0.48.5" +when = "2023-08-18" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.zcash_address]] +version = "0.3.2" +when = "2024-03-06" +user-id = 6289 +user-login = "str4d" + +[[publisher.zcash_client_backend]] +version = "0.12.1" +when = "2024-03-27" +user-id = 169181 +user-login = "nuttycom" +user-name = "Kris Nuttycombe" + +[[publisher.zcash_client_sqlite]] +version = "0.10.3" +when = "2024-04-08" +user-id = 169181 +user-login = "nuttycom" +user-name = "Kris Nuttycombe" + +[[publisher.zcash_encoding]] +version = "0.2.0" +when = "2022-10-19" +user-id = 1244 +user-login = "ebfull" + +[[publisher.zcash_extensions]] +version = "0.0.0" +when = "2020-04-24" +user-id = 6289 +user-login = "str4d" + +[[publisher.zcash_history]] +version = "0.4.0" +when = "2024-03-01" +user-id = 6289 +user-login = "str4d" + +[[publisher.zcash_keys]] +version = "0.2.0" +when = "2024-03-25" +user-id = 169181 +user-login = "nuttycom" +user-name = "Kris Nuttycombe" + +[[publisher.zcash_note_encryption]] +version = "0.4.0" +when = "2023-06-06" +user-id = 169181 +user-login = "nuttycom" +user-name = "Kris Nuttycombe" + +[[publisher.zcash_primitives]] +version = "0.15.0" +when = "2024-03-25" +user-id = 6289 +user-login = "str4d" + +[[publisher.zcash_proofs]] +version = "0.15.0" +when = "2024-03-25" +user-id = 6289 +user-login = "str4d" + +[[publisher.zcash_protocol]] +version = "0.1.1" +when = "2024-03-25" +user-id = 169181 +user-login = "nuttycom" +user-name = "Kris Nuttycombe" + +[[publisher.zcash_spec]] +version = "0.1.0" +when = "2023-12-07" +user-id = 6289 +user-login = "str4d" + +[[publisher.zip32]] +version = "0.1.1" +when = "2024-03-14" +user-id = 6289 +user-login = "str4d" + +[[publisher.zip321]] +version = "0.0.0" +when = "2024-01-15" +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 = "2024-03-10" + +[[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.block-buffer]] +who = "Benjamin Bouvier " +criteria = "safe-to-deploy" +delta = "0.9.0 -> 0.10.2" + +[[audits.bytecode-alliance.audits.cc]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "1.0.73" +notes = "I am the author of this crate." + +[[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.crypto-common]] +who = "Benjamin Bouvier " +criteria = "safe-to-deploy" +version = "0.1.3" + +[[audits.bytecode-alliance.audits.futures-channel]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.3.27" +notes = "build.rs is just detecting the target and setting cfg. unsafety is for implementing a concurrency primitives using atomics and unsafecell, and is not obviously incorrect (this is the sort of thing I wouldn't certify as correct without formal methods)" + +[[audits.bytecode-alliance.audits.futures-core]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.3.27" +notes = "Unsafe used to implement a concurrency primitive AtomicWaker. Well-commented and not obviously incorrect. Like my other audits of these concurrency primitives inside the futures family, I couldn't certify that it is correct without formal methods, but that is out of scope for this vetting." + +[[audits.bytecode-alliance.audits.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.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.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.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.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.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.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.async-stream]] +who = "Tyler Mandry " +criteria = "safe-to-deploy" +version = "0.3.4" +notes = "Reviewed on https://fxrev.dev/761470" +aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.async-stream]] +who = "David Koloski " +criteria = "safe-to-deploy" +delta = "0.3.4 -> 0.3.5" +notes = "Reviewed on https://fxrev.dev/906795" +aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.async-stream-impl]] +who = "Tyler Mandry " +criteria = "safe-to-deploy" +version = "0.3.4" +notes = "Reviewed on https://fxrev.dev/761470" +aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.async-stream-impl]] +who = "David Koloski " +criteria = "safe-to-deploy" +delta = "0.3.4 -> 0.3.5" +notes = "Reviewed on https://fxrev.dev/906795" +aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.atty]] +who = "Android Legacy" +criteria = "safe-to-run" +version = "0.2.14" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.bitflags]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "2.4.2" +notes = """ +Audit notes: + +* I've checked for any discussion in Google-internal cl/546819168 (where audit + of version 2.3.3 happened) +* `src/lib.rs` contains `#![cfg_attr(not(test), forbid(unsafe_code))]` +* There are 2 cases of `unsafe` in `src/external.rs` but they seem to be + correct in a straightforward way - they just propagate the marker trait's + impl (e.g. `impl bytemuck::Pod`) from the inner to the outer type +* Additional discussion and/or notes may be found in https://crrev.com/c/5238056 +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.cfg-if]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "1.0.0" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.clap_lex]] +who = "ChromeOS" +criteria = "safe-to-run" +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.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.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.itertools]] +who = "ChromeOS" +criteria = "safe-to-run" +version = "0.10.5" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/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.parking_lot]] +who = "George Burgess IV " +criteria = "safe-to-run" +version = "0.11.2" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.parking_lot]] +who = "George Burgess IV " +criteria = "safe-to-run" +delta = "0.11.2 -> 0.12.1" +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.quote]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.0.35" +notes = """ +Grepped for \"unsafe\", \"crypt\", \"cipher\", \"fs\", \"net\" - there were no hits +(except for benign \"net\" hit in tests and \"fs\" hit in README.md) +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.same-file]] +who = "Android Legacy" +criteria = "safe-to-run" +version = "1.0.6" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.serde_json]] +who = "danakj@chromium.org" +criteria = "safe-to-run" +version = "1.0.108" +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.stable_deref_trait]] +who = "George Burgess IV " +criteria = "safe-to-run" +version = "1.2.0" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/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.syn]] +who = "danakj@chromium.org" +criteria = "safe-to-run" +version = "1.0.109" +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.tinyvec]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.6.0" +notes = """ +Grepped for `-i cipher`, `-i crypto`, `'\bfs\b'``, `'\bnet\b'``, `'\bunsafe\b'`` +and there were no hits except for some \"unsafe\" appearing in comments: + +``` +src/arrayvec.rs: // Note: This shouldn't use A::CAPACITY, because unsafe code can't rely on +src/lib.rs://! All of this is done with no `unsafe` code within the crate. Technically the +src/lib.rs://! `Vec` type from the standard library uses `unsafe` internally, but *this +src/lib.rs://! crate* introduces no new `unsafe` code into your project. +src/array.rs:/// Just a reminder: this trait is 100% safe, which means that `unsafe` code +``` + +This crate has been added to Chromium in +https://source.chromium.org/chromium/chromium/src/+/24773c33e1b7a1b5069b9399fd034375995f290b +""" +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.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.winapi-util]] +who = "danakj@chromium.org" +criteria = "safe-to-run" +version = "0.1.6" +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.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.crunchy]] +who = "David Cook " +criteria = "safe-to-deploy" +version = "0.2.2" + +[[audits.isrg.audits.hmac]] +who = "David Cook " +criteria = "safe-to-deploy" +version = "0.12.1" + +[[audits.isrg.audits.num-bigint]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.4.3 -> 0.4.4" + +[[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.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-core]] +who = "Brandon Pitman " +criteria = "safe-to-deploy" +delta = "1.10.2 -> 1.11.0" + +[[audits.isrg.audits.rayon-core]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "1.11.0 -> 1.12.0" + +[[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.mozilla.wildcard-audits.unicode-normalization]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +user-id = 1139 # Manish Goregaokar (Manishearth) +start = "2019-11-06" +end = "2024-05-03" +notes = "All code written or reviewed by Manish" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.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.autocfg]] +who = "Josh Stone " +criteria = "safe-to-deploy" +version = "1.1.0" +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.bit-set]] +who = "Aria Beingessner " +criteria = "safe-to-deploy" +version = "0.5.2" +notes = "Another crate I own via contain-rs that is ancient and maintenance mode, no known issues." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bit-set]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.5.2 -> 0.5.3" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bit-vec]] +who = "Aria Beingessner " +criteria = "safe-to-deploy" +version = "0.6.3" +notes = "Another crate I own via contain-rs that is ancient and in maintenance mode but otherwise perfectly fine." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.block-buffer]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.10.2 -> 0.10.3" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.cc]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "1.0.73 -> 1.0.78" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.cc]] +who = "Jan-Erik Rediger " +criteria = "safe-to-deploy" +delta = "1.0.78 -> 1.0.83" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/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.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 = "Nika Layzell " +criteria = "safe-to-deploy" +version = "1.6.1" +notes = """ +Straightforward crate providing the Either enum and trait implementations with +no unsafe code. +""" +aggregated-from = "https://raw.githubusercontent.com/mozilla/cargo-vet/main/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.fastrand]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "1.9.0 -> 2.0.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.fnv]] +who = "Bobby Holley " +criteria = "safe-to-deploy" +version = "1.0.7" +notes = "Simple hasher implementation with no unsafe code." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.futures-channel]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.3.27 -> 0.3.28" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.futures-core]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.3.27 -> 0.3.28" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.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.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.lazy_static]] +who = "Nika Layzell " +criteria = "safe-to-deploy" +version = "1.4.0" +notes = "I have read over the macros, and audited the unsafe code." +aggregated-from = "https://raw.githubusercontent.com/mozilla/cargo-vet/main/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.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-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.rand_core]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.6.3 -> 0.6.4" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.rayon]] +who = "Josh Stone " +criteria = "safe-to-deploy" +version = "1.5.3" +notes = "All code written or reviewed by Josh Stone or Niko Matsakis." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.rayon]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "1.5.3 -> 1.6.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.rayon-core]] +who = "Josh Stone " +criteria = "safe-to-deploy" +version = "1.9.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-core]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "1.9.3 -> 1.10.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.rayon-core]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "1.10.1 -> 1.10.2" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.rustversion]] +who = "Bobby Holley " +criteria = "safe-to-deploy" +version = "1.0.9" +notes = """ +This crate has a build-time component and procedural macro logic, which I looked +at enough to convince myself it wasn't going to do anything dramatically wrong. +I don't think logic bugs in the version parsing etc can realistically introduce +a security vulnerability. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.rustversion]] +who = "Jan-Erik Rediger " +criteria = "safe-to-deploy" +delta = "1.0.9 -> 1.0.14" +notes = "Doc updates, minimal CI changes and a fix to build-script reruns" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.textwrap]] +who = "Jan-Erik Rediger " +criteria = "safe-to-deploy" +version = "0.15.0" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.textwrap]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.15.0 -> 0.15.2" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.textwrap]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.15.2 -> 0.16.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" +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-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.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.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.base64]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.21.3 -> 0.21.4" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.base64]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.21.4 -> 0.21.5" +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.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.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.fastrand]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "2.0.0 -> 2.0.1" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.futures-channel]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.28 -> 0.3.29" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.futures-core]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.28 -> 0.3.29" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.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.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.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.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.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.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.regex-syntax]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.7.5 -> 0.8.2" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.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.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-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.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.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" diff --git a/zcash_client_backend/CHANGELOG.md b/zcash_client_backend/CHANGELOG.md index c896b6d702..ec95efb2dd 100644 --- a/zcash_client_backend/CHANGELOG.md +++ b/zcash_client_backend/CHANGELOG.md @@ -6,24 +6,629 @@ and this library adheres to Rust's notion of [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +### Added +- `zcash_client_backend::data_api`: + - `chain::BlockCache` trait, behind the `sync` feature flag. +- `zcash_client_backend::scanning`: + - `testing` module +- `zcash_client_backend::sync` module, behind the `sync` feature flag. + +### Changed +- `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`: + - `error::Error` has a new `Address` variant. + - `wallet::input_selection::InputSelectorError` has a new `Address` variant. +- `zcash_client_backend::proto::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 `Recipient` type now wraps a + `zcash_address::ZcashAddress`. This simplifies the process of tracking the + original address to which value was sent. + +## [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` dependendencies. 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 + 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_sellection::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 diff --git a/zcash_client_backend/Cargo.toml b/zcash_client_backend/Cargo.toml index bcd0038e33..13f9513273 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.12.1" 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,85 +20,156 @@ exclude = ["*.proto"] [package.metadata.cargo-udeps.ignore] development = ["zcash_proofs"] +[package.metadata.docs.rs] +# Manually specify features while `orchard` is not in the public API. +#all-features = true +features = [ + "lightwalletd-tonic", + "transparent-inputs", + "test-dependencies", + "unstable", + "unstable-serialization", + "unstable-spanning-tree", +] +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 +zcash_protocol.workspace = true +zip32.workspace = true +zip321.workspace = 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 # - Errors -hdwallet = { version = "0.4", optional = true } +hdwallet = { 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 } +proptest = { workspace = true, optional = true } +jubjub = { workspace = true, optional = true } # - ZIP 321 nom = "7" # 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 [build-dependencies] -tonic-build = "0.9" +tonic-build = { workspace = true, features = ["prost"] } which = "4" [dev-dependencies] -assert_matches = "1.5" +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_core.workspace = true +shardtree = { workspace = true, features = ["test-dependencies"] } +zcash_proofs.workspace = true +zcash_address = { workspace = true, features = ["test-dependencies"] } +zcash_keys = { workspace = true, features = ["test-dependencies"] } +tokio = { version = "1.21.0", features = ["rt-multi-thread"] } + +time = ">=0.3.22, <0.3.24" # time 0.3.24 has MSRV 1.67 [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"] + +## 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:hdwallet", + "zcash_keys/transparent-inputs", + "zcash_primitives/transparent-inputs", +] + +## Enables receiving and spending Orchard funds. +orchard = ["dep:orchard", "zcash_keys/orchard"] + +## Exposes a wallet synchronization function that implements the necessary state machine. +sync = [ + "lightwalletd-tonic", + "dep:async-trait", + "dep:futures-util", +] + +## Exposes APIs that are useful for testing, such as `proptest` strategies. test-dependencies = [ - "proptest", - "orchard/test-dependencies", + "dep:proptest", + "dep:jubjub", + "orchard?/test-dependencies", + "zcash_keys/test-dependencies", "zcash_primitives/test-dependencies", - "incrementalmerkletree/test-dependencies" + "incrementalmerkletree/test-dependencies", ] -unstable = ["byteorder"] + +#! ### 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 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..396738bdb5 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(&[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..809b109e8c 100644 --- a/zcash_client_backend/examples/diversify-address.rs +++ b/zcash_client_backend/examples/diversify-address.rs @@ -1,8 +1,9 @@ use gumdrop::Options; +use sapling::zip32::ExtendedFullViewingKey; use zcash_client_backend::encoding::{decode_extended_full_viewing_key, encode_payment_address}; use zcash_primitives::{ constants::{mainnet, testnet}, - zip32::{DiversifierIndex, ExtendedFullViewingKey}, + zip32::DiversifierIndex, }; fn parse_viewing_key(s: &str) -> Result<(ExtendedFullViewingKey, bool), &'static str> { @@ -17,16 +18,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..e39e5225d3 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. 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..950bb3406c --- /dev/null +++ b/zcash_client_backend/proto/proposal.proto @@ -0,0 +1,137 @@ +// 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 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 a change 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 output values. + repeated ChangeValue proposedChange = 1; + // The fee to be paid by the proposed transaction, in zatoshis. + uint64 feeRequired = 2; +} + +// A proposed change output. If the transparent value pool is selected, +// the `memo` field must be null. +message ChangeValue { + // The value of a change output to be created, in zatoshis. + uint64 value = 1; + // The value pool in which the change output should be created. + ValuePool valuePool = 2; + // The optional memo that should be associated with the newly created change output. + // Memos must not be present for transparent change outputs. + MemoBytes memo = 3; +} + +// 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..0945661478 100644 --- a/zcash_client_backend/proto/service.proto +++ b/zcash_client_backend/proto/service.proto @@ -118,6 +118,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 { @@ -142,8 +158,12 @@ service CompactTxStreamer { 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 +197,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..2721219ae5 100644 --- a/zcash_client_backend/src/data_api.rs +++ b/zcash_client_backend/src/data_api.rs @@ -1,120 +1,761 @@ -//! Interfaces for wallet data persistence & low-level wallet utilities. - -use std::cmp; -use std::collections::HashMap; -use std::fmt::Debug; +//! # Utilities for Zcash wallet construction +//! +//! This module defines a set of APIs for wallet data persistence, and provides a suite of methods +//! based upon these APIs that can be used to implement a fully functional Zcash wallet. At +//! present, the interfaces provided here are built primarily around the use of a source of +//! [`CompactBlock`] data such as the Zcash Light Client Protocol as defined in +//! [ZIP 307](https://zips.z.cash/zip-0307) but they may be generalized to full-block use cases in +//! the future. +//! +//! ## Important Concepts +//! +//! There are several important operations that a Zcash wallet must perform that distinguish Zcash +//! wallet design from wallets for other cryptocurrencies. +//! +//! * Viewing Keys: Wallets based upon this module are built around the capabilities of Zcash +//! [`UnifiedFullViewingKey`]s; the wallet backend provides no facilities for the storage +//! of spending keys, and spending keys must be provided by the caller in order to perform +//! transaction creation operations. +//! * Blockchain Scanning: A Zcash wallet must download and trial-decrypt each transaction on the +//! Zcash blockchain using one or more Viewing Keys in order to find new shielded transaction +//! outputs (generally termed "notes") belonging to the wallet. The primary entrypoint for this +//! functionality is the [`scan_cached_blocks`] method. See the [`chain`] module for additional +//! details. +//! * Witness Updates: In order to spend a shielded note, the wallet must be able to compute the +//! Merkle path to that note in the global note commitment tree. When [`scan_cached_blocks`] is +//! used to process a range of blocks, the note commitment tree is updated with the note +//! commitments for the blocks in that range. +//! * Transaction Construction: The [`wallet`] module provides functions for creating Zcash +//! transactions that spend funds belonging to the wallet. +//! +//! ## Core Traits +//! +//! The utility functions described above depend upon four important traits defined in this +//! module, which between them encompass the data storage requirements of a light wallet. +//! The relevant traits are [`InputSource`], [`WalletRead`], [`WalletWrite`], and +//! [`WalletCommitmentTrees`]. A complete implementation of the data storage layer for a wallet +//! will include an implementation of all four of these traits. See the [`zcash_client_sqlite`] +//! crate for a complete example of the implementation of these traits. +//! +//! ## Accounts +//! +//! The operation of the [`InputSource`], [`WalletRead`] and [`WalletWrite`] traits is built around +//! the concept of a wallet having one or more accounts, with a unique `AccountId` for each +//! account. +//! +//! An account identifier corresponds to at most a single [`UnifiedSpendingKey`]'s worth of spend +//! authority, with the received and spent notes of that account tracked via the corresponding +//! [`UnifiedFullViewingKey`]. Both received notes and change spendable by that spending authority +//! (both the external and internal parts of that key, as defined by +//! [ZIP 316](https://zips.z.cash/zip-0316)) will be interpreted as belonging to that account. +//! +//! [`CompactBlock`]: crate::proto::compact_formats::CompactBlock +//! [`scan_cached_blocks`]: crate::data_api::chain::scan_cached_blocks +//! [`zcash_client_sqlite`]: https://crates.io/crates/zcash_client_sqlite +//! [`TransactionRequest`]: crate::zip321::TransactionRequest +//! [`propose_shielding`]: crate::data_api::wallet::propose_shielding + +use std::{ + collections::HashMap, + fmt::Debug, + hash::Hash, + io, + num::{NonZeroU32, TryFromIntError}, +}; +use incrementalmerkletree::{frontier::Frontier, Retention}; +use nonempty::NonEmpty; use secrecy::SecretVec; +use shardtree::{error::ShardTreeError, store::ShardStore, ShardTree}; +use zip32::fingerprint::SeedFingerprint; + +use self::{ + chain::{ChainState, CommitmentTreeRoot}, + scanning::ScanRange, +}; +use crate::{ + address::UnifiedAddress, + decrypt::DecryptedOutput, + keys::{ + UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedIncomingViewingKey, UnifiedSpendingKey, + }, + proto::service::TreeState, + wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput, WalletTx}, + ShieldedProtocol, +}; use zcash_primitives::{ block::BlockHash, consensus::BlockHeight, - legacy::TransparentAddress, memo::{Memo, MemoBytes}, - sapling, transaction::{ - components::{amount::Amount, OutPoint}, + components::amount::{BalanceError, NonNegativeAmount}, Transaction, TxId, }, - zip32::{AccountId, ExtendedFullViewingKey}, }; -use crate::{ - address::{AddressMetadata, UnifiedAddress}, - decrypt::DecryptedOutput, - keys::{UnifiedFullViewingKey, UnifiedSpendingKey}, - wallet::{ReceivedSaplingNote, WalletTransparentOutput, WalletTx}, +#[cfg(feature = "transparent-inputs")] +use { + crate::wallet::TransparentAddressMetadata, + zcash_primitives::{legacy::TransparentAddress, transaction::components::OutPoint}, }; +#[cfg(any(test, feature = "test-dependencies"))] +use zcash_primitives::consensus::NetworkUpgrade; + pub mod chain; pub mod error; +pub mod scanning; pub mod wallet; +/// 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, } -/// Read-only operations required for light wallet functions. +/// Balance information for a value within a single pool in an account. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Balance { + spendable_value: NonNegativeAmount, + change_pending_confirmation: NonNegativeAmount, + value_pending_spendability: NonNegativeAmount, +} + +impl Balance { + /// The [`Balance`] value having zero values for all its fields. + pub const ZERO: Self = Self { + spendable_value: NonNegativeAmount::ZERO, + change_pending_confirmation: NonNegativeAmount::ZERO, + value_pending_spendability: NonNegativeAmount::ZERO, + }; + + fn check_total_adding( + &self, + value: NonNegativeAmount, + ) -> 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) -> NonNegativeAmount { + self.spendable_value + } + + /// Adds the specified value to the spendable total, checking for overflow. + pub fn add_spendable_value(&mut self, value: NonNegativeAmount) -> 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) -> NonNegativeAmount { + 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: NonNegativeAmount, + ) -> 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) -> NonNegativeAmount { + 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: NonNegativeAmount, + ) -> 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) -> NonNegativeAmount { + (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, irrespective of + /// confirmation depth. + /// + /// Unshielded balances are not subject to confirmation-depth constraints, because the only + /// possible operation on a transparent balance is to shield it, it is possible to create a + /// zero-conf transaction to perform that shielding, and the resulting shielded notes will be + /// subject to normal confirmation rules. + unshielded: NonNegativeAmount, +} + +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: NonNegativeAmount::ZERO, + }; + + fn check_total(&self) -> Result { + (self.sapling_balance.total() + self.orchard_balance.total() + self.unshielded) + .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. + pub fn unshielded(&self) -> NonNegativeAmount { + self.unshielded + } + + /// Adds the specified value to the unshielded total, checking for overflow of + /// the total account balance. + pub fn add_unshielded_value(&mut self, value: NonNegativeAmount) -> Result<(), BalanceError> { + self.unshielded = (self.unshielded + value).ok_or(BalanceError::Overflow)?; + self.check_total()?; + Ok(()) + } + + /// Returns the total value of funds belonging to the account. + pub fn total(&self) -> NonNegativeAmount { + (self.sapling_balance.total() + self.orchard_balance.total() + self.unshielded) + .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) -> NonNegativeAmount { + (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) -> NonNegativeAmount { + (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) -> NonNegativeAmount { + (self.sapling_balance.value_pending_spendability + + self.orchard_balance.value_pending_spendability) + .expect("Account balance cannot overflow MAX_MONEY") + } +} + +/// The kinds of accounts supported by `zcash_client_backend`. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum AccountSource { + /// An account derived from a known seed. + Derived { + seed_fingerprint: SeedFingerprint, + account_index: zip32::AccountId, + }, + + /// An account imported from a viewing key. + Imported, +} + +/// A set of capabilities that a client account must provide. +pub trait Account { + /// Returns the unique identifier for the account. + fn id(&self) -> AccountId; + + /// Returns whether this account is derived or imported, and the derivation parameters + /// if applicable. + fn source(&self) -> AccountSource; + + /// 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) { + fn id(&self) -> A { + self.0 + } + + fn source(&self) -> AccountSource { + AccountSource::Imported + } + + fn ufvk(&self) -> Option<&UnifiedFullViewingKey> { + Some(&self.1) + } + + fn uivk(&self) -> UnifiedIncomingViewingKey { + self.1.to_unified_incoming_viewing_key() + } +} + +#[cfg(any(test, feature = "test-dependencies"))] +impl Account for (A, UnifiedIncomingViewingKey) { + fn id(&self) -> A { + self.0 + } + + fn source(&self) -> AccountSource { + AccountSource::Imported + } + + fn ufvk(&self) -> Option<&UnifiedFullViewingKey> { + None + } + + fn uivk(&self) -> UnifiedIncomingViewingKey { + self.1.clone() + } +} + +/// 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 potentially-spendable value of unspent outputs in the wallet. /// -/// This trait defines the read-only portion of the storage interface atop which -/// higher-level wallet operations are implemented. It serves to allow wallet functions to -/// be abstracted away from any particular data storage substrate. -pub trait WalletRead { +/// The 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, + scan_progress: Option>, + 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, + scan_progress: Option>, + next_sapling_subtree_index: u64, + #[cfg(feature = "orchard")] next_orchard_subtree_index: u64, + ) -> Self { + Self { + account_balances, + chain_tip_height, + fully_scanned_height, + scan_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 shielded outputs, in terms of the ratio between notes + /// scanned and the total number of notes added to the chain since the wallet birthday. + /// + /// This ratio should only be used to compute progress percentages, and the numerator and + /// denominator should not be treated as authoritative note counts. Returns `None` if the + /// wallet is unable to determine the size of the note commitment tree. + pub fn scan_progress(&self) -> Option> { + self.scan_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>, +} + +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() + } + + /// Returns the set of spendable Orchard notes. + #[cfg(feature = "orchard")] + pub fn orchard(&self) -> &[ReceivedNote] { + self.orchard.as_ref() + } + + /// Computes the total value of Sapling notes. + pub fn sapling_value(&self) -> Result { + self.sapling + .iter() + .try_fold(NonNegativeAmount::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(NonNegativeAmount::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() + } +} + +/// A trait representing the capability to query a data store for unspent transaction outputs +/// belonging to a wallet. +pub trait InputSource { /// The type of errors produced by a wallet backend. - type Error; + type Error: Debug; - /// Backend-specific note identifier. + /// Backend-specific account identifier. /// - /// For example, this might be a database identifier type + /// 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 NoteRef: Copy + Debug + Eq + Ord; + type AccountId: Copy + Debug + Eq + Hash; - /// Backend-specific transaction identifier. + /// Backend-specific note identifier. /// - /// For example, this might be a database identifier type - /// or a TxId if the backend is able to support that type - /// directly. - type TxRef: Copy + Debug + Eq + Ord; + /// For example, this might be a database identifier type or a UUID. + type NoteRef: Copy + Debug + Eq + Ord; - /// Returns the minimum and maximum block heights for stored blocks. + /// Fetches a spendable note by indexing into a transaction's shielded outputs for the + /// specified shielded protocol. /// - /// This will return `Ok(None)` if no block data is present in the database. - fn block_height_extrema(&self) -> Result, Self::Error>; + /// 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: NonNegativeAmount, + sources: &[ShieldedProtocol], + anchor_height: BlockHeight, + exclude: &[Self::NoteRef], + ) -> 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. + /// Fetches a spendable transparent output. /// - /// This will return `Ok(None)` if no block data is present in the database. - fn get_target_and_anchor_heights( + /// Returns `Ok(None)` if the UTXO is not known to belong to the wallet or is not + /// spendable. + #[cfg(feature = "transparent-inputs")] + fn get_unspent_transparent_output( &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) - }) - }) + _outpoint: &OutPoint, + ) -> Result, Self::Error> { + Ok(None) } - /// Returns the minimum block height corresponding to an unspent note in the wallet. - fn get_min_unspent_height(&self) -> Result, Self::Error>; + /// Returns a list of unspent transparent UTXOs that appear in the chain at heights up to and + /// including `max_height`. + #[cfg(feature = "transparent-inputs")] + fn get_unspent_transparent_outputs( + &self, + _address: &TransparentAddress, + _max_height: BlockHeight, + _exclude: &[OutPoint], + ) -> Result, Self::Error> { + Ok(vec![]) + } +} - /// 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>; +/// Read-only operations required for light wallet functions. +/// +/// This trait defines the read-only portion of the storage interface atop which +/// higher-level wallet operations are implemented. It serves to allow wallet functions to +/// be abstracted away from any particular data storage substrate. +pub trait WalletRead { + /// The type of errors that may be generated when querying a wallet data store. + type Error: Debug; - /// Returns the block hash for the block at the maximum height known - /// in stored data. + /// The type of the account identifier. /// - /// 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()) - } + /// 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; - /// 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>; + /// The concrete account type used by this wallet backend. + type Account: Account; + + /// Returns a vector with the IDs of all accounts known to this wallet. + fn get_account_ids(&self) -> Result, Self::Error>; + + /// Returns the account corresponding to the given ID, if any. + fn get_account( + &self, + account_id: Self::AccountId, + ) -> Result, Self::Error>; + + /// Returns the account corresponding to a given [`SeedFingerprint`] and + /// [`zip32::AccountId`], if any. + fn get_derived_account( + &self, + seed: &SeedFingerprint, + account_id: zip32::AccountId, + ) -> Result, Self::Error>; + + /// Verifies that the given seed corresponds to the viewing key for the specified account. + /// + /// Returns: + /// - `Ok(true)` if the viewing key for the specified account can be derived from the + /// provided seed. + /// - `Ok(false)` if the derived viewing key does not match, or the specified account is not + /// present in the database. + /// - `Err(_)` if a Unified Spending Key cannot be derived from the seed for the + /// specified account or the account has no known ZIP-32 derivation. + fn validate_seed( + &self, + account_id: Self::AccountId, + seed: &SecretVec, + ) -> Result; + + /// Checks whether the given seed is relevant to any of the derived accounts (where + /// [`Account::source`] is [`AccountSource::Derived`]) in the wallet. + /// + /// This API does not check whether the seed is relevant to any imported account, + /// because that would require brute-forcing the ZIP 32 account index space. + fn seed_relevance_to_derived_accounts( + &self, + seed: &SecretVec, + ) -> Result, Self::Error>; + + /// Returns the account corresponding to a given [`UnifiedFullViewingKey`], if any. + fn get_account_for_ufvk( + &self, + ufvk: &UnifiedFullViewingKey, + ) -> Result, Self::Error>; /// Returns the most recently generated unified address for the specified account, if the /// account identifier specified refers to a valid account for this wallet. @@ -123,135 +764,411 @@ pub trait WalletRead { /// account. fn get_current_address( &self, - account: AccountId, + account: Self::AccountId, ) -> Result, Self::Error>; - /// Returns all unified full viewing keys known to this wallet. - fn get_unified_full_viewing_keys( - &self, - ) -> Result, Self::Error>; + /// Returns the 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 account id corresponding to a given [`UnifiedFullViewingKey`], if any. - fn get_account_for_ufvk( - &self, - ufvk: &UnifiedFullViewingKey, - ) -> Result, Self::Error>; + /// 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>; - /// Checks whether the specified extended full viewing key is associated with the account. - fn is_valid_account_extfvk( + /// Returns the wallet balances and sync status for an account given the specified minimum + /// number of confirmations, or `Ok(None)` if the wallet has no balance data available. + fn get_wallet_summary( &self, - account: AccountId, - extfvk: &ExtendedFullViewingKey, - ) -> Result; + min_confirmations: u32, + ) -> Result>, Self::Error>; + + /// Returns the height of the chain as known to the wallet as of the most recent call to + /// [`WalletWrite::update_chain_tip`]. + /// + /// This will return `Ok(None)` if the height of the current consensus chain tip is unknown. + fn chain_height(&self) -> Result, Self::Error>; + + /// Returns the block hash for the block at the given height, if the + /// associated block data is available. Returns `Ok(None)` if the hash + /// is not found in the database. + fn get_block_hash(&self, block_height: BlockHeight) -> Result, Self::Error>; + + /// Returns the available block metadata for the block at the specified height, if any. + fn block_metadata(&self, height: BlockHeight) -> Result, Self::Error>; + + /// Returns the metadata for the block at the height to which the wallet has been fully + /// scanned. + /// + /// This is the height for which the wallet has fully trial-decrypted this and all preceding + /// blocks above the wallet's birthday height. Along with this height, this method returns + /// metadata describing the state of the wallet's note commitment trees as of the end of that + /// block. + fn block_fully_scanned(&self) -> Result, Self::Error>; + + /// Returns the block height and hash for the block at the maximum scanned block height. + /// + /// This will return `Ok(None)` if no blocks have been scanned. + fn get_max_height_hash(&self) -> Result, Self::Error>; - /// Returns the wallet balance for an account as of the specified block height. + /// Returns block metadata for the maximum height that the wallet has scanned. /// - /// 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( + /// If the wallet is fully synced, this will be equivalent to `block_fully_scanned`; + /// otherwise the maximal scanned height is likely to be greater than the fully scanned height + /// due to the fact that out-of-order scanning can leave gaps. + fn block_max_scanned(&self) -> Result, Self::Error>; + + /// Returns a vector of suggested scan ranges based upon the current wallet state. + /// + /// This method should only be used in cases where the [`CompactBlock`] data that will be made + /// available to `scan_cached_blocks` for the requested block ranges includes note commitment + /// tree size information for each block; or else the scan is likely to fail if notes belonging + /// to the wallet are detected. + /// + /// The returned range(s) may include block heights beyond the current chain tip. Ranges are + /// returned in order of descending priority, and higher-priority ranges should always be + /// scanned before lower-priority ranges; in particular, ranges with [`ScanPriority::Verify`] + /// priority must always be scanned first in order to avoid blockchain continuity errors in the + /// case of a reorg. + /// + /// [`CompactBlock`]: crate::proto::compact_formats::CompactBlock + /// [`ScanPriority::Verify`]: crate::data_api::scanning::ScanPriority + fn suggest_scan_ranges(&self) -> Result, Self::Error>; + + /// Returns the default target height (for the block in which a new + /// transaction would be mined) and anchor height (to use for a new + /// transaction), given the range of block heights that the backend + /// knows about. + /// + /// This will return `Ok(None)` if no block data is present in the database. + fn get_target_and_anchor_heights( &self, - account: AccountId, - anchor_height: BlockHeight, - ) -> Result; + min_confirmations: NonZeroU32, + ) -> 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 block height in which the specified transaction was mined, or `Ok(None)` if the + /// transaction is not in the main chain. + fn get_tx_height(&self, txid: TxId) -> Result, Self::Error>; + + /// Returns all unified full viewing keys known to this wallet. + fn get_unified_full_viewing_keys( + &self, + ) -> Result, Self::Error>; /// Returns the memo for a note. /// - /// 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>; + /// 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, id_tx: Self::TxRef) -> Result; + fn get_transaction(&self, txid: TxId) -> Result, Self::Error>; - /// Returns the note commitment tree at the specified block height. - fn get_commitment_tree( + /// 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, - block_height: BlockHeight, - ) -> Result, Self::Error>; + query: NullifierQuery, + ) -> Result, Self::Error>; - /// Returns the incremental witnesses as of the specified block height. - #[allow(clippy::type_complexity)] - fn get_witnesses( + /// 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, - block_height: BlockHeight, - ) -> Result, Self::Error>; + query: NullifierQuery, + ) -> Result, Self::Error>; - /// 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( + /// 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. + #[cfg(feature = "transparent-inputs")] + fn get_transparent_receivers( &self, - query: NullifierQuery, - ) -> Result, Self::Error>; + _account: Self::AccountId, + ) -> Result>, Self::Error> { + Ok(HashMap::new()) + } - /// Return all unspent Sapling notes. - fn get_spendable_sapling_notes( + /// Returns a mapping from transparent receiver to not-yet-shielded UTXO balance, + /// for each address associated with a nonzero balance. + #[cfg(feature = "transparent-inputs")] + fn get_transparent_balances( &self, - account: AccountId, - anchor_height: BlockHeight, - exclude: &[Self::NoteRef], - ) -> Result>, Self::Error>; + _account: Self::AccountId, + _max_height: BlockHeight, + ) -> Result, Self::Error> { + Ok(HashMap::new()) + } +} + +/// 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 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 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 set of all transparent receivers associated with the given account. + /// Returns the vector of nullifiers for each transaction in the block. /// - /// 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>; + /// 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 a list of unspent transparent UTXOs that appear in the chain at heights up to and - /// including `max_height`. - fn get_unspent_transparent_outputs( - &self, - address: &TransparentAddress, - max_height: BlockHeight, - exclude: &[OutPoint], - ) -> Result, Self::Error>; + /// Returns the ordered list of note commitments to be added to the note commitment + /// tree. + pub fn commitments(&self) -> &[(NoteCommitment, Retention)] { + &self.commitments + } +} - /// 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>; +/// A struct used to return the vectors of note commitments for a [`ScannedBlock`] +/// as owned values. +pub struct ScannedBlockCommitments { + /// The ordered vector of note commitments for Sapling outputs of the block. + pub sapling: Vec<(sapling::Node, Retention)>, + /// The ordered vector of note commitments for Orchard outputs of the block. + /// Present only when the `orchard` feature is enabled. + #[cfg(feature = "orchard")] + pub orchard: Vec<(orchard::tree::MerkleHashOrchard, Retention)>, } /// The subset of information that is relevant to this wallet that has been /// decrypted and extracted from a [`CompactBlock`]. /// /// [`CompactBlock`]: crate::proto::compact_formats::CompactBlock -pub struct PrunedBlock<'a> { - pub block_height: BlockHeight, - pub block_hash: BlockHash, - pub block_time: u32, - pub commitment_tree: &'a sapling::CommitmentTree, - pub transactions: &'a Vec>, +pub struct ScannedBlock { + block_height: BlockHeight, + block_hash: BlockHash, + block_time: u32, + transactions: Vec>, + sapling: ScannedBundles, + #[cfg(feature = "orchard")] + orchard: ScannedBundles, +} + +impl ScannedBlock { + /// Constructs a new `ScannedBlock` + pub(crate) fn from_parts( + block_height: BlockHeight, + block_hash: BlockHash, + block_time: u32, + transactions: Vec>, + sapling: ScannedBundles, + #[cfg(feature = "orchard")] orchard: ScannedBundles< + orchard::tree::MerkleHashOrchard, + orchard::note::Nullifier, + >, + ) -> Self { + Self { + block_height, + block_hash, + block_time, + transactions, + sapling, + #[cfg(feature = "orchard")] + orchard, + } + } + + /// Returns the height of the block that was scanned. + pub fn height(&self) -> BlockHeight { + self.block_height + } + + /// Returns the block hash of the block that was scanned. + pub fn block_hash(&self) -> BlockHash { + self.block_hash + } + + /// Returns the block time of the block that was scanned, as a Unix timestamp in seconds. + pub fn block_time(&self) -> u32 { + self.block_time + } + + /// Returns the list of transactions from this block that are relevant to the wallet. + pub fn transactions(&self) -> &[WalletTx] { + &self.transactions + } + + /// Returns the Sapling note commitment tree and nullifier data for the block. + pub fn sapling(&self) -> &ScannedBundles { + &self.sapling + } + + /// Returns the Orchard note commitment tree and nullifier data for the block. + #[cfg(feature = "orchard")] + pub fn orchard( + &self, + ) -> &ScannedBundles { + &self.orchard + } + + /// Consumes `self` and returns the lists of Sapling and Orchard note commitments associated + /// with the scanned block as an owned value. + pub fn into_commitments(self) -> ScannedBlockCommitments { + ScannedBlockCommitments { + sapling: self.sapling.commitments, + #[cfg(feature = "orchard")] + orchard: self.orchard.commitments, + } + } + + /// Returns the [`BlockMetadata`] corresponding to the scanned block. + pub fn to_block_metadata(&self) -> BlockMetadata { + BlockMetadata { + block_height: self.block_height, + block_hash: self.block_hash, + sapling_tree_size: Some(self.sapling.final_tree_size), + #[cfg(feature = "orchard")] + orchard_tree_size: Some(self.orchard.final_tree_size), + } + } } /// A transaction that was detected during scanning of the blockchain, -/// including its decrypted Sapling 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> { + 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( + tx: &'a Transaction, + sapling_outputs: Vec>, + #[cfg(feature = "orchard")] orchard_outputs: Vec< + DecryptedOutput, + >, + ) -> Self { + Self { + tx, + sapling_outputs, + #[cfg(feature = "orchard")] + orchard_outputs, + } + } + + /// 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 +1176,98 @@ 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, + account: AccountId, + outputs: Vec>, + fee_amount: NonNegativeAmount, #[cfg(feature = "transparent-inputs")] - pub utxos_spent: Vec, + utxos_spent: Vec, } -/// 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. + pub fn new( + tx: &'a Transaction, + created: time::OffsetDateTime, + account: AccountId, + outputs: Vec>, + fee_amount: NonNegativeAmount, + #[cfg(feature = "transparent-inputs")] utxos_spent: Vec, + ) -> Self { + Self { + tx, + created, + 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.as_ref() + } + /// Returns the fee paid by the transaction. + pub fn fee_amount(&self) -> NonNegativeAmount { + 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.as_ref() + } } -/// 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: NonNegativeAmount, 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: NonNegativeAmount, 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,23 +1277,152 @@ 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 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) -> NonNegativeAmount { 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 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, + ) + } - /// 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() + #[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) } } @@ -351,25 +1432,41 @@ 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. + /// + /// 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 `birthday.height()` is below the current chain tip, this operation will + /// trigger a re-scan of the blocks at and above the provided height. The birthday height is + /// defined as the minimum block height that will be scanned for funds belonging to the wallet. /// - /// Returns the account identifier for the newly-created wallet database entry, along - /// with the associated [`UnifiedSpendingKey`]. + /// For new wallets, 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. /// - /// 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. + /// 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. /// - /// 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). + /// By convention, wallets should only allow a new account to be generated after confirmed + /// funds have been received by the currently-available account (in order to enable automated + /// account recovery). + /// + /// 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, seed: &SecretVec, - ) -> Result<(AccountId, UnifiedSpendingKey), Self::Error>; + birthday: &AccountBirthday, + ) -> Result<(Self::AccountId, UnifiedSpendingKey), Self::Error>; /// Generates and persists the next available diversified address, given the current /// addresses known to the wallet. @@ -378,29 +1475,51 @@ pub trait WalletWrite: WalletRead { /// account. fn get_next_available_address( &mut self, - account: AccountId, + account: Self::AccountId, + request: UnifiedAddressRequest, ) -> 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( + /// Updates the wallet's view of the blockchain. + /// + /// This method is used to provide the wallet with information about the state of the + /// blockchain, and detect any previously scanned data that needs to be re-validated + /// before proceeding with scanning. It should be called at wallet startup prior to calling + /// [`WalletRead::suggest_scan_ranges`] in order to provide the wallet with the information it + /// needs to correctly prioritize scanning operations. + fn update_chain_tip(&mut self, tip_height: BlockHeight) -> Result<(), Self::Error>; + + /// Updates the state of the wallet database by persisting the provided block information, + /// along with the note commitments that were detected when scanning the block for transactions + /// pertaining to this wallet. + /// + /// ### Arguments + /// - `from_state` must be the chain state for the block height prior to the first + /// block in `blocks`. + /// - `blocks` must be sequential, in order of increasing block height. + fn put_blocks( + &mut self, + from_state: &ChainState, + blocks: Vec>, + ) -> Result<(), Self::Error>; + + /// Adds a transparent UTXO received by the wallet to the data store. + fn put_received_transparent_utxo( &mut self, - block: &PrunedBlock, - updated_witnesses: &[(Self::NoteRef, sapling::IncrementalWitness)], - ) -> Result, Self::Error>; + output: &WalletTransparentOutput, + ) -> Result; /// Caches a decrypted transaction in the persistent wallet store. fn store_decrypted_tx( &mut self, - received_tx: DecryptedTransaction, - ) -> Result; + received_tx: DecryptedTransaction, + ) -> Result<(), Self::Error>; /// 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; + fn store_sent_tx( + &mut self, + sent_tx: &SentTransaction, + ) -> Result<(), Self::Error>; /// Truncates the wallet database to the specified height. /// @@ -416,176 +1535,331 @@ pub trait WalletWrite: WalletRead { /// /// 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>; +} - /// Adds a transparent UTXO received by the wallet to the data store. - fn put_received_transparent_utxo( +/// This trait describes a capability for manipulating wallet note commitment trees. +/// +/// At present, this only serves the Sapling protocol, but it will be modified to +/// also provide operations related to Orchard note commitment trees in the future. +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, - output: &WalletTransparentOutput, - ) -> Result; + 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>; } #[cfg(feature = "test-dependencies")] pub mod testing { + use incrementalmerkletree::Address; use secrecy::{ExposeSecret, SecretVec}; - use std::collections::HashMap; + use shardtree::{error::ShardTreeError, store::memory::MemoryShardStore, ShardTree}; + use std::{collections::HashMap, convert::Infallible, num::NonZeroU32}; + use zip32::fingerprint::SeedFingerprint; use zcash_primitives::{ block::BlockHash, consensus::{BlockHeight, Network}, - legacy::TransparentAddress, memo::Memo, - sapling, - transaction::{ - components::{Amount, OutPoint}, - Transaction, TxId, - }, - zip32::{AccountId, ExtendedFullViewingKey}, + transaction::{components::amount::NonNegativeAmount, Transaction, TxId}, }; use crate::{ - address::{AddressMetadata, UnifiedAddress}, - keys::{UnifiedFullViewingKey, UnifiedSpendingKey}, - wallet::{ReceivedSaplingNote, WalletTransparentOutput}, + address::UnifiedAddress, + keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey}, + wallet::{Note, NoteId, ReceivedNote, WalletTransparentOutput}, + ShieldedProtocol, }; use super::{ - DecryptedTransaction, NullifierQuery, PrunedBlock, SentTransaction, WalletRead, WalletWrite, + chain::{ChainState, CommitmentTreeRoot}, + scanning::ScanRange, + AccountBirthday, BlockMetadata, DecryptedTransaction, InputSource, NullifierQuery, + ScannedBlock, SeedRelevance, SentTransaction, SpendableNotes, WalletCommitmentTrees, + WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT, }; + #[cfg(feature = "transparent-inputs")] + use {crate::wallet::TransparentAddressMetadata, zcash_primitives::legacy::TransparentAddress}; + + #[cfg(feature = "orchard")] + use super::ORCHARD_SHARD_HEIGHT; + pub struct MockWalletDb { pub network: Network, + pub sapling_tree: ShardTree< + MemoryShardStore, + { SAPLING_SHARD_HEIGHT * 2 }, + SAPLING_SHARD_HEIGHT, + >, + #[cfg(feature = "orchard")] + pub orchard_tree: ShardTree< + MemoryShardStore, + { ORCHARD_SHARD_HEIGHT * 2 }, + ORCHARD_SHARD_HEIGHT, + >, } - impl WalletRead for MockWalletDb { + impl MockWalletDb { + 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 TxRef = TxId; + type AccountId = u32; - fn block_height_extrema(&self) -> Result, Self::Error> { + fn get_spendable_note( + &self, + _txid: &TxId, + _protocol: ShieldedProtocol, + _index: u32, + ) -> Result>, Self::Error> { Ok(None) } - fn get_min_unspent_height(&self) -> Result, Self::Error> { - Ok(None) + fn select_spendable_notes( + &self, + _account: Self::AccountId, + _target_value: NonNegativeAmount, + _sources: &[ShieldedProtocol], + _anchor_height: BlockHeight, + _exclude: &[Self::NoteRef], + ) -> Result, Self::Error> { + Ok(SpendableNotes::empty()) } + } - fn get_block_hash( + 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, - _block_height: BlockHeight, - ) -> Result, Self::Error> { + _account_id: Self::AccountId, + ) -> Result, Self::Error> { Ok(None) } - fn get_tx_height(&self, _txid: TxId) -> Result, Self::Error> { + fn get_derived_account( + &self, + _seed: &SeedFingerprint, + _account_id: zip32::AccountId, + ) -> Result, Self::Error> { Ok(None) } - fn get_current_address( + fn validate_seed( &self, - _account: AccountId, - ) -> Result, Self::Error> { - Ok(None) + _account_id: Self::AccountId, + _seed: &SecretVec, + ) -> Result { + Ok(false) } - fn get_unified_full_viewing_keys( + fn seed_relevance_to_derived_accounts( &self, - ) -> Result, Self::Error> { - Ok(HashMap::new()) + _seed: &SecretVec, + ) -> Result, Self::Error> { + Ok(SeedRelevance::NoAccounts) } fn get_account_for_ufvk( &self, _ufvk: &UnifiedFullViewingKey, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { Ok(None) } - fn is_valid_account_extfvk( + fn get_current_address( &self, - _account: AccountId, - _extfvk: &ExtendedFullViewingKey, - ) -> Result { - Ok(false) + _account: Self::AccountId, + ) -> Result, Self::Error> { + Ok(None) } - fn get_balance_at( + fn get_account_birthday( &self, - _account: AccountId, - _anchor_height: BlockHeight, - ) -> Result { - Ok(Amount::zero()) + _account: Self::AccountId, + ) -> Result { + Err(()) } - fn get_memo(&self, _id_note: Self::NoteRef) -> Result, Self::Error> { + fn get_wallet_birthday(&self) -> Result, Self::Error> { Ok(None) } - fn get_transaction(&self, _id_tx: Self::TxRef) -> Result { - Err(()) + fn get_wallet_summary( + &self, + _min_confirmations: u32, + ) -> Result>, Self::Error> { + Ok(None) + } + + fn chain_height(&self) -> Result, Self::Error> { + Ok(None) } - fn get_commitment_tree( + fn get_block_hash( &self, _block_height: BlockHeight, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { Ok(None) } - #[allow(clippy::type_complexity)] - fn get_witnesses( + fn block_metadata( &self, - _block_height: BlockHeight, - ) -> Result, Self::Error> { - Ok(Vec::new()) + _height: BlockHeight, + ) -> Result, Self::Error> { + Ok(None) } - fn get_sapling_nullifiers( + 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, - _query: NullifierQuery, - ) -> Result, Self::Error> { - Ok(Vec::new()) + _min_confirmations: NonZeroU32, + ) -> Result, Self::Error> { + Ok(None) + } + + fn get_min_unspent_height(&self) -> Result, Self::Error> { + Ok(None) + } + + fn get_tx_height(&self, _txid: TxId) -> Result, Self::Error> { + Ok(None) } - fn get_spendable_sapling_notes( + fn get_unified_full_viewing_keys( &self, - _account: AccountId, - _anchor_height: BlockHeight, - _exclude: &[Self::NoteRef], - ) -> Result>, Self::Error> { - Ok(Vec::new()) + ) -> Result, Self::Error> { + Ok(HashMap::new()) } - fn select_spendable_sapling_notes( + 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, - _account: AccountId, - _target_value: Amount, - _anchor_height: BlockHeight, - _exclude: &[Self::NoteRef], - ) -> Result>, Self::Error> { + _query: NullifierQuery, + ) -> Result, Self::Error> { Ok(Vec::new()) } - fn get_transparent_receivers( + #[cfg(feature = "orchard")] + fn get_orchard_nullifiers( &self, - _account: AccountId, - ) -> Result, Self::Error> { - Ok(HashMap::new()) + _query: NullifierQuery, + ) -> Result, Self::Error> { + Ok(Vec::new()) } - fn get_unspent_transparent_outputs( + #[cfg(feature = "transparent-inputs")] + fn get_transparent_receivers( &self, - _address: &TransparentAddress, - _anchor_height: BlockHeight, - _exclude: &[OutPoint], - ) -> Result, Self::Error> { - Ok(Vec::new()) + _account: Self::AccountId, + ) -> Result>, Self::Error> + { + Ok(HashMap::new()) } + #[cfg(feature = "transparent-inputs")] fn get_transparent_balances( &self, - _account: AccountId, + _account: Self::AccountId, _max_height: BlockHeight, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { Ok(HashMap::new()) } } @@ -596,41 +1870,47 @@ pub mod testing { fn create_account( &mut self, seed: &SecretVec, - ) -> Result<(AccountId, UnifiedSpendingKey), Self::Error> { - let account = AccountId::from(0); + _birthday: &AccountBirthday, + ) -> Result<(Self::AccountId, UnifiedSpendingKey), Self::Error> { + let account = zip32::AccountId::ZERO; UnifiedSpendingKey::from_seed(&self.network, seed.expose_secret(), account) - .map(|k| (account, k)) + .map(|k| (u32::from(account), k)) .map_err(|_| ()) } fn get_next_available_address( &mut self, - _account: AccountId, + _account: Self::AccountId, + _request: UnifiedAddressRequest, ) -> Result, Self::Error> { Ok(None) } #[allow(clippy::type_complexity)] - fn advance_by_block( + fn put_blocks( &mut self, - _block: &PrunedBlock, - _updated_witnesses: &[(Self::NoteRef, sapling::IncrementalWitness)], - ) -> Result, Self::Error> { - Ok(vec![]) + _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 { - Ok(TxId::from_bytes([0u8; 32])) + _received_tx: DecryptedTransaction, + ) -> Result<(), Self::Error> { + Ok(()) } fn store_sent_tx( &mut self, - _sent_tx: &SentTransaction, - ) -> Result { - Ok(TxId::from_bytes([0u8; 32])) + _sent_tx: &SentTransaction, + ) -> Result<(), Self::Error> { + Ok(()) } fn truncate_to_height(&mut self, _block_height: BlockHeight) -> Result<(), Self::Error> { @@ -645,4 +1925,78 @@ pub mod testing { Ok(0) } } + + impl WalletCommitmentTrees for MockWalletDb { + type Error = Infallible; + type SaplingShardStore<'a> = MemoryShardStore; + + 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], + ) -> Result<(), ShardTreeError> { + self.with_sapling_tree_mut(|t| { + for (root, i) in roots.iter().zip(0u64..) { + let root_addr = + 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; + + #[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], + ) -> Result<(), ShardTreeError> { + self.with_orchard_tree_mut(|t| { + for (root, i) in roots.iter().zip(0u64..) { + let root_addr = + 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/chain.rs b/zcash_client_backend/src/data_api/chain.rs index 44736228df..f458bc8b2a 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,180 @@ //! # 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 incrementalmerkletree::frontier::Frontier; +use subtle::ConditionallySelectable; use zcash_primitives::{ block::BlockHash, consensus::{self, BlockHeight}, - sapling::{self, note_encryption::PreparedIncomingViewingKey, Nullifier}, - zip32::Scope, }; 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::WalletRead; -use super::NullifierQuery; +/// 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. + 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,260 +219,479 @@ 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")] @@ -389,14 +708,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..2c10db70ca 100644 --- a/zcash_client_backend/src/data_api/error.rs +++ b/zcash_client_backend/src/data_api/error.rs @@ -2,42 +2,55 @@ 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_primitives::transaction::components::amount::NonNegativeAmount; +use zcash_primitives::transaction::{ + builder, + components::{amount::BalanceError, transparent}, }; +use crate::address::UnifiedAddress; use crate::data_api::wallet::input_selection::InputSelectorError; +use crate::proposal::ProposalError; +use crate::PoolType; #[cfg(feature = "transparent-inputs")] -use zcash_primitives::{legacy::TransparentAddress, zip32::DiversifierIndex}; +use zcash_primitives::legacy::TransparentAddress; + +use crate::wallet::NoteId; /// 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 transaction proposal construction + Proposal(ProposalError), + + /// The proposal was structurally valid, but spending shielded outputs of prior multi-step + /// transaction steps is not yet supported. + ProposalNotSupported, + /// 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), - /// 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: NonNegativeAmount, + required: NonNegativeAmount, + }, /// The wallet must first perform a scan of the blockchain before other /// operations can be performed. @@ -49,26 +62,42 @@ 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 + /// for which no spending key is available. + /// + /// The argument is the address of the note or UTXO being spent. + NoSpendingKey(String), + /// A note being spent does not correspond to either the internal or external /// full viewing key for an account. - NoteMismatch(NoteRef), + NoteMismatch(NoteId), - #[cfg(feature = "transparent-inputs")] - AddressNotRecognized(TransparentAddress), + /// An error occurred parsing the address from a payment request. + Address(ConversionError<&'static str>), #[cfg(feature = "transparent-inputs")] - ChildIndexOutOfRange(DiversifierIndex), + AddressNotRecognized(TransparentAddress), } -impl fmt::Display for Error +impl fmt::Display for Error where DE: fmt::Display, + CE: fmt::Display, SE: fmt::Display, FE: fmt::Display, - N: fmt::Display, { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match &self { + match self { Error::DataSource(e) => { write!( f, @@ -76,18 +105,27 @@ 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::Proposal(e) => { + write!(f, "Input selection attempted to construct an invalid proposal: {}", e) + } + Error::ProposalNotSupported => { + write!( + f, + "The proposal was valid, but spending shielded outputs of prior transaction steps is not yet supported." + ) + } 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::BalanceError(e) => write!( f, "The value lies outside the valid range of Zcash amounts: {:?}.", @@ -96,64 +134,75 @@ 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().map(|(i, tc)| format!("{}{:?}", if i > 0 { ", " } else { "" }, tc)).collect::() + ), + Error::NoSpendingKey(addr) => write!(f, "No spending key available for address: {}", addr), + 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) => { - write!( - f, - "The diversifier index {:?} is out of range for transparent addresses.", - i - ) - } } } } -impl error::Error for Error +impl error::Error for Error where DE: Debug + Display + error::Error + 'static, + CE: Debug + Display + error::Error + 'static, SE: Debug + Display + error::Error + 'static, FE: Debug + Display + 'static, - N: Debug + Display, { 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), _ => None, } } } -impl From> for Error { +impl From> for Error { fn from(e: builder::Error) -> Self { Error::Builder(e) } } -impl From for Error { +impl From for Error { fn from(e: BalanceError) -> Self { Error::BalanceError(e) } } -impl From> for Error { +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::Proposal(e) => Error::Proposal(e), InputSelectorError::InsufficientFunds { available, required, @@ -161,18 +210,26 @@ 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 { +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) + } +} 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..a03abd62f8 --- /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_primitives::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..58631de8fb --- /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_primitives::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_primitives::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_primitives::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/wallet.rs b/zcash_client_backend/src/data_api/wallet.rs index d96a47e284..69cc88f932 100644 --- a/zcash_client_backend/src/data_api/wallet.rs +++ b/zcash_client_backend/src/data_api/wallet.rs @@ -1,47 +1,85 @@ -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 sapling::{ + note_encryption::{try_sapling_note_decryption, PreparedIncomingViewingKey}, + prover::{OutputProver, SpendProver}, }; +use std::num::NonZeroU32; +use super::InputSource; use crate::{ - address::RecipientAddress, + address::Address, 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}, + fees::{self, DustOutputPolicy}, keys::UnifiedSpendingKey, - wallet::{OvkPolicy, ReceivedSaplingNote}, + proposal::{self, Proposal, ProposalError}, + wallet::{Note, OvkPolicy, Recipient}, zip321::{self, Payment}, + PoolType, ShieldedProtocol, }; - -pub mod input_selection; -use input_selection::{GreedyInputSelector, GreedyInputSelectorError, InputSelector}; +use zcash_primitives::transaction::{ + builder::{BuildConfig, BuildResult, Builder}, + components::{amount::NonNegativeAmount, sapling::zip212_enforcement}, + fees::{zip317::FeeError as Zip317FeeError, FeeRule, StandardFeeRule}, + Transaction, TxId, +}; +use zcash_protocol::{ + consensus::{self, BlockHeight, NetworkUpgrade}, + memo::MemoBytes, +}; +use zip32::Scope; #[cfg(feature = "transparent-inputs")] use { - crate::wallet::WalletTransparentOutput, - zcash_primitives::{ - legacy::TransparentAddress, sapling::keys::OutgoingViewingKey, - transaction::components::amount::NonNegativeAmount, - }, + input_selection::ShieldingSelector, + std::convert::Infallible, + zcash_keys::encoding::AddressCodec, + zcash_primitives::legacy::TransparentAddress, + zcash_primitives::transaction::components::{OutPoint, TxOut}, +}; + +pub mod input_selection; +use input_selection::{ + GreedyInputSelector, GreedyInputSelectorError, InputSelector, InputSelectorError, }; /// Scans a [`Transaction`] for any information that can be decrypted by the accounts in @@ -62,26 +100,21 @@ where // for mempool transactions. let height = data .get_tx_height(tx.txid())? - .or(data - .block_height_extrema()? - .map(|(_, max_height)| max_height + 1)) + .or(data.chain_height()?.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 { - tx, - sapling_outputs: &decrypt_transaction(params, height, tx, &ufvks), - })?; + data.store_decrypted_tx(decrypt_transaction(params, height, tx, &ufvks))?; Ok(()) } #[allow(clippy::needless_doctest_main)] -/// Creates a transaction paying the specified address from the given account. +/// Creates a transaction or series of transactions paying the specified address from +/// the given account, and the [`TxId`] corresponding to each newly-created transaction. /// -/// 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. +/// These transactions can be retrieved from the underlying data store using the +/// [`WalletRead::get_transaction`] method. /// /// Do not call this multiple times in parallel, or you will generate transactions that /// double-spend the same notes. @@ -106,7 +139,10 @@ where /// 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. /// * `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. @@ -117,14 +153,15 @@ where /// 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. +/// spent. A value of 10 confirmations is recommended and 0-conf transactions are +/// not supported. +/// * `change_memo`: A memo to be included in the change output /// /// # Examples /// /// ``` -/// # #[cfg(feature = "test-dependencies")] +/// # #[cfg(all(feature = "test-dependencies", feature = "local-prover"))] /// # { -/// use tempfile::NamedTempFile; /// use zcash_primitives::{ /// consensus::{self, Network}, /// constants::testnet::COIN_TYPE, @@ -133,7 +170,7 @@ where /// }; /// use zcash_proofs::prover::LocalTxProver; /// use zcash_client_backend::{ -/// keys::UnifiedSpendingKey, +/// keys::{UnifiedSpendingKey, UnifiedAddressRequest}, /// data_api::{wallet::create_spend_to_address, error::Error, testing}, /// wallet::OvkPolicy, /// }; @@ -159,8 +196,9 @@ where /// }; /// /// let account = AccountId::from(0); +/// let req = UnifiedAddressRequest::new(false, true, true); /// let usk = UnifiedSpendingKey::from_seed(&Network::TestNetwork, &[0; 32][..], account).unwrap(); -/// let to = usk.to_unified_full_viewing_key().default_address().0.into(); +/// let to = usk.to_unified_full_viewing_key().default_address(req).0.into(); /// /// let mut db_read = testing::MockWalletDb { /// network: Network::TestNetwork @@ -175,78 +213,88 @@ where /// Amount::from_u64(1).unwrap(), /// None, /// OvkPolicy::Sender, -/// 10 +/// 10, +/// None /// ) /// /// # } /// # } /// ``` -/// [`sapling::TxProver`]: zcash_primitives::sapling::prover::TxProver +/// +/// [`sapling::SpendProver`]: sapling::prover::SpendProver +/// [`sapling::OutputProver`]: sapling::prover::OutputProver #[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." + note = "Use `propose_transfer` and `create_proposed_transactions` 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( wallet_db: &mut DbT, params: &ParamsT, - prover: impl SaplingProver, + spend_prover: &impl SpendProver, + output_prover: &impl OutputProver, usk: &UnifiedSpendingKey, - to: &RecipientAddress, - amount: Amount, + to: &Address, + amount: NonNegativeAmount, memo: Option, ovk_policy: OvkPolicy, - min_confirmations: u32, + min_confirmations: NonZeroU32, + change_memo: Option, + fallback_change_pool: ShieldedProtocol, ) -> Result< - DbT::TxRef, + NonEmpty, Error< - DbT::Error, - GreedyInputSelectorError, - Infallible, - DbT::NoteRef, + ::Error, + ::Error, + GreedyInputSelectorError, + Zip317FeeError, >, > where ParamsT: consensus::Parameters + Clone, - DbT: WalletWrite, - DbT::NoteRef: Copy + Eq + Ord, + DbT: InputSource, + DbT: WalletWrite< + Error = ::Error, + AccountId = ::AccountId, + >, + DbT: WalletCommitmentTrees, { - let req = zip321::TransactionRequest::new(vec![Payment { - recipient_address: to.clone(), + let account = wallet_db + .get_account_for_ufvk(&usk.to_unified_full_viewing_key()) + .map_err(Error::DataSource)? + .ok_or(Error::KeyNotRecognized)?; + + #[allow(deprecated)] + let proposal = propose_standard_transfer_to_address( + wallet_db, + params, + StandardFeeRule::PreZip313, + account.id(), + min_confirmations, + to, amount, memo, - label: None, - message: None, - other_params: vec![], - }]) - .expect( - "It should not be possible for this to violate ZIP 321 request construction invariants.", - ); + change_memo, + fallback_change_pool, + )?; - #[allow(deprecated)] - let fee_rule = fixed::FeeRule::standard(); - let change_strategy = fees::fixed::SingleOutputChangeStrategy::new(fee_rule); - spend( + create_proposed_transactions( wallet_db, params, - prover, - &GreedyInputSelector::::new(change_strategy, DustOutputPolicy::default()), + spend_prover, + output_prover, usk, - req, ovk_policy, - min_confirmations, + &proposal, ) } -/// 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). +/// Constructs a transaction or series of transactions that send funds as specified +/// by the `request` argument, stores them to the wallet's "sent transactions" data +/// store, and returns the [`TxId`] for each transaction constructed. /// -/// This procedure uses the wallet's underlying note selection algorithm to choose -/// inputs of sufficient value to satisfy the request, if possible. +/// The newly-created transactions can be retrieved from the underlying data store using the +/// [`WalletRead::get_transaction`] method. /// /// Do not call this multiple times in parallel, or you will generate transactions that /// double-spend the same notes. @@ -271,7 +319,10 @@ where /// 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`] that will be used to select available /// inputs from the wallet database, choose change amounts and compute required /// transaction fees. @@ -284,30 +335,42 @@ where /// 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. +/// 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 #[allow(clippy::too_many_arguments)] #[allow(clippy::type_complexity)] +#[deprecated(note = "Use `propose_transfer` and `create_proposed_transactions` instead.")] pub fn spend( wallet_db: &mut DbT, params: &ParamsT, - prover: impl SaplingProver, + spend_prover: &impl SpendProver, + output_prover: &impl OutputProver, input_selector: &InputsT, usk: &UnifiedSpendingKey, request: zip321::TransactionRequest, ovk_policy: OvkPolicy, - min_confirmations: u32, + min_confirmations: NonZeroU32, ) -> Result< - DbT::TxRef, - Error::Error, DbT::NoteRef>, + NonEmpty, + Error< + ::Error, + ::Error, + InputsT::Error, + ::Error, + >, > where - DbT: WalletWrite, - DbT::TxRef: Copy + Debug, - DbT::NoteRef: Copy + Eq + Ord, + DbT: InputSource, + DbT: WalletWrite< + Error = ::Error, + AccountId = ::AccountId, + >, + DbT: WalletCommitmentTrees, ParamsT: consensus::Parameters + Clone, - InputsT: InputSelector, + InputsT: InputSelector, { let account = wallet_db .get_account_for_ufvk(&usk.to_unified_full_viewing_key()) @@ -317,59 +380,160 @@ where let proposal = propose_transfer( wallet_db, params, - account, + account.id(), input_selector, request, min_confirmations, )?; - create_proposed_transaction(wallet_db, params, prover, usk, ovk_policy, proposal, None) + create_proposed_transactions( + wallet_db, + params, + spend_prover, + output_prover, + usk, + ovk_policy, + &proposal, + ) } -/// 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`]. +/// Select transaction inputs, compute fees, and construct a proposal for a transaction or series +/// of transactions that can then be authorized and made ready for submission to the network with +/// [`create_proposed_transactions`]. #[allow(clippy::too_many_arguments)] #[allow(clippy::type_complexity)] -pub fn propose_transfer( +pub fn propose_transfer( wallet_db: &mut DbT, params: &ParamsT, - spend_from_account: AccountId, + spend_from_account: ::AccountId, input_selector: &InputsT, request: zip321::TransactionRequest, - min_confirmations: u32, + min_confirmations: NonZeroU32, ) -> Result< - Proposal, - Error::Error, DbT::NoteRef>, + Proposal::NoteRef>, + Error< + ::Error, + CommitmentTreeErrT, + InputsT::Error, + ::Error, + >, > where - DbT: WalletWrite, - DbT::NoteRef: Copy + Eq + Ord, + DbT: WalletRead + InputSource::Error>, + ::NoteRef: Copy + Eq + Ord, ParamsT: consensus::Parameters + Clone, - InputsT: InputSelector, + 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))?; + .map_err(|e| Error::from(InputSelectorError::DataSource(e)))? + .ok_or_else(|| Error::from(InputSelectorError::SyncRequired))?; input_selector .propose_transaction( params, wallet_db, - spend_from_account, - anchor_height, target_height, + anchor_height, + spend_from_account, request, ) .map_err(Error::from) } +/// Proposes making a payment to the specified address from the given account. +/// +/// 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. +/// +/// This method uses the basic [`GreedyInputSelector`] for input selection. +/// +/// Parameters: +/// * `wallet_db`: A read/write reference to the wallet database. +/// * `params`: Consensus parameters. +/// * `fee_rule`: The fee rule to use in creating the transaction. +/// * `spend_from_account`: The unified account that controls the funds that will be spent +/// in the resulting transaction. This procedure will return an error if the +/// account ID does not correspond to an account known to the wallet. +/// * `min_confirmations`: The minimum number of confirmations that a previously +/// received note must have in the blockchain in order to be considered for being +/// spent. A value of 10 confirmations is recommended and 0-conf transactions are +/// not supported. +/// * `to`: The address to which `amount` will be paid. +/// * `amount`: The amount to send. +/// * `memo`: A memo to be included in the output to the recipient. +/// * `change_memo`: A memo to be included in any change output that is created. +/// * `fallback_change_pool`: The shielded pool to which change should be sent if +/// automatic change pool determination fails. +#[allow(clippy::too_many_arguments)] +#[allow(clippy::type_complexity)] +pub fn propose_standard_transfer_to_address( + wallet_db: &mut DbT, + params: &ParamsT, + fee_rule: StandardFeeRule, + spend_from_account: ::AccountId, + min_confirmations: NonZeroU32, + to: &Address, + amount: NonNegativeAmount, + memo: Option, + change_memo: Option, + fallback_change_pool: ShieldedProtocol, +) -> Result< + Proposal, + Error< + ::Error, + CommitmentTreeErrT, + GreedyInputSelectorError, + Zip317FeeError, + >, +> +where + ParamsT: consensus::Parameters + Clone, + DbT: InputSource, + DbT: WalletRead< + Error = ::Error, + AccountId = ::AccountId, + >, + DbT::NoteRef: Copy + Eq + Ord, +{ + let request = zip321::TransactionRequest::new(vec![Payment::new( + to.to_zcash_address(params), + amount, + memo, + None, + None, + vec![], + ) + .ok_or(Error::MemoForbidden)?]) + .expect( + "It should not be possible for this to violate ZIP 321 request construction invariants.", + ); + + let change_strategy = fees::standard::SingleOutputChangeStrategy::new( + fee_rule, + change_memo, + fallback_change_pool, + ); + let input_selector = + GreedyInputSelector::::new(change_strategy, DustOutputPolicy::default()); + + propose_transfer( + wallet_db, + params, + spend_from_account, + &input_selector, + request, + min_confirmations, + ) +} + +/// Constructs a proposal to shield all of the funds belonging to the provided set of +/// addresses. #[cfg(feature = "transparent-inputs")] #[allow(clippy::too_many_arguments)] #[allow(clippy::type_complexity)] -pub fn propose_shielding( +pub fn propose_shielding( wallet_db: &mut DbT, params: &ParamsT, input_selector: &InputsT, @@ -377,19 +541,23 @@ pub fn propose_shielding( from_addrs: &[TransparentAddress], min_confirmations: u32, ) -> Result< - Proposal, - Error::Error, DbT::NoteRef>, + Proposal, + Error< + ::Error, + CommitmentTreeErrT, + InputsT::Error, + ::Error, + >, > where ParamsT: consensus::Parameters, - DbT: WalletWrite, - DbT::NoteRef: Copy + Eq + Ord, - InputsT: InputSelector, + DbT: WalletRead + InputSource::Error>, + InputsT: ShieldingSelector, { - 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( @@ -397,246 +565,663 @@ where wallet_db, shielding_threshold, from_addrs, - latest_anchor, - target_height, + 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. +/// 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< + NonEmpty, + Error< + ::Error, + ::Error, + InputsErrT, + FeeRuleT::Error, + >, +> where - DbT: WalletWrite, - DbT::TxRef: Copy + Debug, - DbT::NoteRef: Copy + Eq + Ord, + DbT: WalletWrite + WalletCommitmentTrees, + ParamsT: consensus::Parameters + Clone, + FeeRuleT: FeeRule, +{ + let mut step_results = Vec::with_capacity(proposal.steps().len()); + for step in proposal.steps() { + let step_result = create_proposed_transaction( + wallet_db, + params, + spend_prover, + output_prover, + usk, + ovk_policy.clone(), + proposal.fee_rule(), + proposal.min_target_height(), + &step_results, + step, + )?; + step_results.push((step, step_result)); + } + + Ok(NonEmpty::from_vec( + step_results + .iter() + .map(|(_, r)| r.transaction().txid()) + .collect(), + ) + .expect("proposal.steps is NonEmpty")) +} + +#[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, + ovk_policy: OvkPolicy, + fee_rule: &FeeRuleT, + min_target_height: BlockHeight, + prior_step_results: &[(&proposal::Step, BuildResult)], + proposal_step: &proposal::Step, +) -> Result< + BuildResult, + Error< + ::Error, + ::Error, + InputsErrT, + FeeRuleT::Error, + >, +> +where + DbT: WalletWrite + WalletCommitmentTrees, ParamsT: consensus::Parameters + Clone, FeeRuleT: FeeRule, { + // TODO: Spending shielded outputs of prior multi-step transaction steps is not yet + // supported. Maybe support this 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. + for s_ref in proposal_step.prior_step_inputs() { + prior_step_results.get(s_ref.step_index()).map_or_else( + || { + // Return an error in case the step index doesn't match up with a step + Err(Error::Proposal(ProposalError::ReferenceError(*s_ref))) + }, + |step| match s_ref.output_index() { + proposal::StepOutputIndex::Payment(i) => { + let prior_pool = step + .0 + .payment_pools() + .get(&i) + .ok_or(Error::Proposal(ProposalError::ReferenceError(*s_ref)))?; + + if matches!(prior_pool, PoolType::Shielded(_)) { + Err(Error::ProposalNotSupported) + } else { + Ok(()) + } + } + proposal::StepOutputIndex::Change(_) => { + // Only shielded change is supported by zcash_client_backend, so multi-step + // transactions cannot yet spend prior transactions' change outputs. + Err(Error::ProposalNotSupported) + } + }, + )?; + } + let account = 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 (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())? + .into(); - // 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, - }; + let sapling_inputs = inputs + .notes() + .iter() + .filter_map(|selected| match selected.note() { + Note::Sapling(note) => { + let key = match selected.spending_key_scope() { + Scope::External => usk.sapling().clone(), + Scope::Internal => usk.sapling().derive_internal(), + }; - let internal_ovk = || { - #[cfg(feature = "transparent-inputs")] - return if proposal.is_shielding() { - Some(OutgoingViewingKey( - usk.transparent() - .to_account_pubkey() - .internal_ovk() - .as_bytes(), - )) + sapling_tree + .witness_at_checkpoint_id_caching( + selected.note_commitment_tree_position(), + &inputs.anchor_height(), + ) + .map(|merkle_path| Some((key, note, merkle_path))) + .map_err(Error::from) + .transpose() + } + #[cfg(feature = "orchard")] + Note::Orchard(_) => None, + }) + .collect::, Error<_, _, _, _>>>()?; + + Ok((Some(anchor), sapling_inputs)) + }) + }, + )? } else { - Some(dfvk.to_ovk(Scope::Internal)) + (None, vec![]) }; - #[cfg(not(feature = "transparent-inputs"))] - Some(dfvk.to_ovk(Scope::Internal)) - }; + #[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())? + .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(), + ) + .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()); + 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))?; + for (sapling_key, sapling_note, merkle_path) in sapling_inputs.into_iter() { + builder.add_sapling_spend(&sapling_key, sapling_note.clone(), merkle_path)?; + } - builder.add_sapling_spend(key, selected.diversifier, note, merkle_path)?; + #[cfg(feature = "orchard")] + for (orchard_note, merkle_path) in orchard_inputs.into_iter() { + builder.add_orchard_spend(usk.orchard(), *orchard_note, merkle_path.into())?; } #[cfg(feature = "transparent-inputs")] - let utxos = { + let utxos_spent = { let known_addrs = wallet_db .get_transparent_receivers(account) .map_err(Error::DataSource)?; - let mut utxos: Vec = vec![]; - for utxo in proposal.transparent_inputs() { - utxos.push(utxo.clone()); - - let diversifier_index = known_addrs - .get(utxo.recipient_address()) - .ok_or_else(|| Error::AddressNotRecognized(*utxo.recipient_address()))? - .diversifier_index(); - - let child_index = u32::try_from(*diversifier_index) - .map_err(|_| Error::ChildIndexOutOfRange(*diversifier_index))?; + let mut utxos_spent: Vec = vec![]; + let mut add_transparent_input = |addr: &TransparentAddress, + outpoint: OutPoint, + utxo: TxOut| + -> Result< + (), + Error< + ::Error, + ::Error, + InputsErrT, + FeeRuleT::Error, + >, + > { + let address_metadata = known_addrs + .get(addr) + .ok_or(Error::AddressNotRecognized(*addr))? + .clone() + .ok_or_else(|| Error::NoSpendingKey(addr.encode(params)))?; let secret_key = usk .transparent() - .derive_external_secret_key(child_index) + .derive_secret_key(address_metadata.scope(), address_metadata.address_index()) .unwrap(); - builder.add_transparent_input( - secret_key, + utxos_spent.push(outpoint.clone()); + builder.add_transparent_input(secret_key, outpoint, utxo)?; + + Ok(()) + }; + + for utxo in proposal_step.transparent_inputs() { + add_transparent_input( + utxo.recipient_address(), utxo.outpoint().clone(), utxo.txout().clone(), )?; } - utxos + for input_ref in proposal_step.prior_step_inputs() { + match input_ref.output_index() { + proposal::StepOutputIndex::Payment(i) => { + // We know based upon the earlier check that this must be a transparent input, + // We also know that transparent outputs for that previous step were added to + // the transaction in payment index order, so we can use dead reckoning to + // figure out which output it ended up being. + let (prior_step, result) = &prior_step_results[input_ref.step_index()]; + let recipient_address = &prior_step + .transaction_request() + .payments() + .get(&i) + .expect("Payment step references are checked at construction") + .recipient_address() + .clone() + .convert_if_network(params.network_type())?; + + let recipient_taddr = match recipient_address { + Address::Transparent(t) => Some(t), + Address::Unified(uaddr) => uaddr.transparent(), + _ => None, + } + .ok_or(Error::ProposalNotSupported)?; + let outpoint = OutPoint::new( + result.transaction().txid().into(), + u32::try_from( + prior_step + .payment_pools() + .iter() + .filter(|(_, pool)| pool == &&PoolType::Transparent) + .take_while(|(j, _)| j <= &&i) + .count() + - 1, + ) + .expect("Transparent output index fits into a u32"), + ); + let utxo = &result + .transaction() + .transparent_bundle() + .ok_or(Error::Proposal(ProposalError::ReferenceError(*input_ref)))? + .vout[outpoint.n() as usize]; + + add_transparent_input(recipient_taddr, outpoint, utxo.clone())?; + } + proposal::StepOutputIndex::Change(_) => unreachable!(), + } + } + utxos_spent + }; + + #[cfg(feature = "orchard")] + let orchard_fvk: orchard::keys::FullViewingKey = usk.orchard().into(); + + #[cfg(feature = "orchard")] + let orchard_external_ovk = match &ovk_policy { + OvkPolicy::Sender => Some(orchard_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 Some(orchard::keys::OutgoingViewingKey::from( + usk.transparent() + .to_account_pubkey() + .internal_ovk() + .as_bytes(), + )); + } + + Some(orchard_fvk.to_ovk(Scope::Internal)) }; + let sapling_dfvk = usk.sapling().to_diversifiable_full_viewing_key(); + + // Apply the outgoing viewing key policy. + let sapling_external_ovk = match &ovk_policy { + OvkPolicy::Sender => Some(sapling_dfvk.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 Some(sapling::keys::OutgoingViewingKey( + usk.transparent() + .to_account_pubkey() + .internal_ovk() + .as_bytes(), + )); + } + + Some(sapling_dfvk.to_ovk(Scope::Internal)) + }; + + #[cfg(feature = "orchard")] + let mut orchard_output_meta = vec![]; 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), - )?; - sapling_output_meta.push(( - Recipient::Unified(ua.clone(), PoolType::Sapling), - payment.amount, - payment.memo.clone(), - )); + for (payment, output_pool) in proposal_step + .payment_pools() + .iter() + .map(|(idx, output_pool)| { + let payment = proposal_step + .transaction_request() + .payments() + .get(idx) + .expect( + "The mapping between payment index and payment is checked in step construction", + ); + (payment, output_pool) + }) + { + let recipient_address: Address = payment + .recipient_address() + .clone() + .convert_if_network(params.network_type())?; + + match recipient_address { + Address::Unified(ua) => { + let memo = payment.memo().map_or_else(MemoBytes::empty, |m| m.clone()); + + match output_pool { + #[cfg(not(feature = "orchard"))] + PoolType::Shielded(ShieldedProtocol::Orchard) => { + return Err(Error::ProposalNotSupported); + } + #[cfg(feature = "orchard")] + PoolType::Shielded(ShieldedProtocol::Orchard) => { + builder.add_orchard_output( + orchard_external_ovk.clone(), + *ua.orchard().expect("The mapping between payment pool and receiver is checked in step construction"), + payment.amount().into(), + memo.clone(), + )?; + orchard_output_meta.push(( + Recipient::External( + payment.recipient_address().clone(), + PoolType::Shielded(ShieldedProtocol::Orchard), + ), + payment.amount(), + Some(memo), + )); + } + + PoolType::Shielded(ShieldedProtocol::Sapling) => { + builder.add_sapling_output( + sapling_external_ovk, + *ua.sapling().expect("The mapping between payment pool and receiver is checked in step construction"), + payment.amount(), + memo.clone(), + )?; + sapling_output_meta.push(( + Recipient::External( + payment.recipient_address().clone(), + PoolType::Shielded(ShieldedProtocol::Sapling), + ), + payment.amount(), + Some(memo), + )); + } + + PoolType::Transparent => { + if payment.memo().is_some() { + return Err(Error::MemoForbidden); + } else { + builder.add_transparent_output( + ua.transparent().expect("The mapping between payment pool and receiver is checked in step construction."), + payment.amount() + )?; + } + } + } } - RecipientAddress::Shielded(addr) => { + Address::Sapling(addr) => { + let memo = payment.memo().map_or_else(MemoBytes::empty, |m| m.clone()); builder.add_sapling_output( - external_ovk, - *addr, - payment.amount, - payment.memo.clone().unwrap_or_else(MemoBytes::empty), + sapling_external_ovk, + addr, + payment.amount(), + memo.clone(), )?; sapling_output_meta.push(( - Recipient::Sapling(*addr), - payment.amount, - payment.memo.clone(), + Recipient::External(payment.recipient_address().clone(), PoolType::SAPLING), + payment.amount(), + Some(memo), )); } - RecipientAddress::Transparent(to) => { - if payment.memo.is_some() { + Address::Transparent(to) => { + if payment.memo().is_some() { return Err(Error::MemoForbidden); } else { - builder.add_transparent_output(to, payment.amount)?; + builder.add_transparent_output(&to, payment.amount())?; } - transparent_output_meta.push((*to, payment.amount)); + transparent_output_meta.push(( + Recipient::External(payment.recipient_address().clone(), PoolType::TRANSPARENT), + to, + payment.amount(), + )); } } } - 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()); + match change_value.output_pool() { + ShieldedProtocol::Sapling => { builder.add_sapling_output( - internal_ovk(), - dfvk.change_address().1, - *amount, - MemoBytes::empty(), + sapling_internal_ovk(), + sapling_dfvk.change_address().1, + change_value.value(), + memo.clone(), )?; sapling_output_meta.push(( - Recipient::InternalAccount(account, PoolType::Sapling), - *amount, - change_memo.clone(), + Recipient::InternalAccount { + receiving_account: account, + external_address: None, + note: PoolType::Shielded(ShieldedProtocol::Sapling), + }, + change_value.value(), + Some(memo), )) } + ShieldedProtocol::Orchard => { + #[cfg(not(feature = "orchard"))] + return Err(Error::UnsupportedChangeType(PoolType::Shielded( + ShieldedProtocol::Orchard, + ))); + + #[cfg(feature = "orchard")] + { + builder.add_orchard_output( + orchard_internal_ovk(), + orchard_fvk.address_at(0u32, orchard::keys::Scope::Internal), + change_value.value().into(), + memo.clone(), + )?; + orchard_output_meta.push(( + Recipient::InternalAccount { + receiving_account: account, + external_address: None, + note: PoolType::Shielded(ShieldedProtocol::Orchard), + }, + change_value.value(), + Some(memo), + )) + } + } } } // Build the transaction with the specified fee rule - let (tx, sapling_build_meta) = builder.build(&prover, proposal.fee_rule())?; + let build_result = builder.build(OsRng, spend_prover, output_prover, fee_rule)?; + + #[cfg(feature = "orchard")] + let orchard_internal_ivk = orchard_fvk.to_ivk(orchard::keys::Scope::Internal); + #[cfg(feature = "orchard")] + let orchard_outputs = + 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 internal_ivk = PreparedIncomingViewingKey::new(&dfvk.to_ivk(Scope::Internal)); + let recipient = recipient + .map_internal_account_note(|pool| { + assert!(pool == PoolType::Shielded(ShieldedProtocol::Orchard)); + build_result + .transaction() + .orchard_bundle() + .and_then(|bundle| { + bundle + .decrypt_output_with_key(output_index, &orchard_internal_ivk) + .map(|(note, _, _)| Note::Orchard(note)) + }) + }) + .internal_account_note_transpose_option() + .expect("Wallet-internal outputs must be decryptable with the wallet's IVK"); + + SentTransactionOutput::from_parts(output_index, recipient, value, memo) + }); + + let sapling_internal_ivk = + PreparedIncomingViewingKey::new(&sapling_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 + let output_index = build_result + .sapling_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 - }; + .expect("An output should exist in the transaction for each Sapling payment."); + + let recipient = recipient + .map_internal_account_note(|pool| { + assert!(pool == PoolType::Shielded(ShieldedProtocol::Sapling)); + 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)) + }) + }) + .internal_account_note_transpose_option() + .expect("Wallet-internal outputs must be decryptable with the wallet's IVK"); - SentTransactionOutput::from_parts(output_index, recipient, value, memo, received_as) + SentTransactionOutput::from_parts(output_index, recipient, value, memo) }); - let transparent_outputs = transparent_output_meta.into_iter().map(|(addr, value)| { - let script = addr.script(); - let output_index = tx - .transparent_bundle() - .and_then(|b| { - b.vout - .iter() - .enumerate() - .find(|(_, tx_out)| tx_out.script_pubkey == script) - }) - .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 transparent_outputs = + transparent_output_meta + .into_iter() + .map(|(recipient, addr, value)| { + let script = addr.script(); + let output_index = build_result + .transaction() + .transparent_bundle() + .and_then(|b| { + b.vout + .iter() + .enumerate() + .find(|(_, tx_out)| tx_out.script_pubkey == script) + }) + .map(|(index, _)| index) + .expect( + "An output should exist in the transaction for each transparent payment.", + ); + + SentTransactionOutput::from_parts(output_index, recipient, value, None) + }); + + let mut outputs = vec![]; + #[cfg(feature = "orchard")] + outputs.extend(orchard_outputs); + outputs.extend(sapling_outputs); + outputs.extend(transparent_outputs); wallet_db .store_sent_tx(&SentTransaction { - tx: &tx, + tx: build_result.transaction(), created: time::OffsetDateTime::now_utc(), account, - outputs: sapling_outputs.chain(transparent_outputs).collect(), - fee_amount: proposal.balance().fee_required(), + outputs, + fee_amount: proposal_step.balance().fee_required(), #[cfg(feature = "transparent-inputs")] - utxos_spent: utxos.iter().map(|utxo| utxo.outpoint().clone()).collect(), + utxos_spent, }) - .map_err(Error::DataSource) + .map_err(Error::DataSource)?; + + Ok(build_result) } -/// 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,37 +1233,39 @@ 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( wallet_db: &mut DbT, params: &ParamsT, - prover: impl SaplingProver, + spend_prover: &impl SpendProver, + output_prover: &impl OutputProver, input_selector: &InputsT, shielding_threshold: NonNegativeAmount, usk: &UnifiedSpendingKey, from_addrs: &[TransparentAddress], - memo: &MemoBytes, min_confirmations: u32, ) -> Result< - DbT::TxRef, - Error::Error, DbT::NoteRef>, + NonEmpty, + Error< + ::Error, + ::Error, + InputsT::Error, + ::Error, + >, > where ParamsT: consensus::Parameters, - DbT: WalletWrite, - DbT::NoteRef: Copy + Eq + Ord, - InputsT: InputSelector, + DbT: WalletWrite + WalletCommitmentTrees + InputSource::Error>, + InputsT: ShieldingSelector, { let proposal = propose_shielding( wallet_db, @@ -689,41 +1276,13 @@ where 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..ebbeef66a4 100644 --- a/zcash_client_backend/src/data_api/wallet/input_selection.rs +++ b/zcash_client_backend/src/data_api/wallet/input_selection.rs @@ -1,40 +1,71 @@ //! 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 nonempty::NonEmpty; +use zcash_address::ConversionError; use zcash_primitives::{ consensus::{self, BlockHeight}, - legacy::TransparentAddress, transaction::{ components::{ - amount::{Amount, BalanceError, NonNegativeAmount}, - sapling::fees as sapling, - OutPoint, TxOut, + amount::{BalanceError, NonNegativeAmount}, + TxOut, }, fees::FeeRule, }, - zip32::AccountId, }; use crate::{ - address::{RecipientAddress, UnifiedAddress}, - data_api::WalletRead, - fees::{ChangeError, ChangeStrategy, DustOutputPolicy, TransactionBalance}, - wallet::{ReceivedSaplingNote, WalletTransparentOutput}, + address::{Address, UnifiedAddress}, + data_api::{InputSource, SimpleNoteRetention, SpendableNotes}, + fees::{sapling, ChangeError, ChangeStrategy, DustOutputPolicy}, + proposal::{Proposal, ProposalError, ShieldedInputs}, + wallet::WalletTransparentOutput, zip321::TransactionRequest, + PoolType, ShieldedProtocol, +}; + +#[cfg(feature = "transparent-inputs")] +use { + std::collections::BTreeSet, std::convert::Infallible, + zcash_primitives::legacy::TransparentAddress, + zcash_primitives::transaction::components::OutPoint, }; +#[cfg(feature = "orchard")] +use crate::fees::orchard as orchard_fees; + /// The type of errors that may be produced in input selection. +#[derive(Debug)] pub enum InputSelectorError { /// An error occurred accessing the underlying data store. DataSource(DbErrT), /// An error occurred specific to the provided input selector's selection rules. Selection(SelectorErrT), + /// 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: NonNegativeAmount, + required: NonNegativeAmount, + }, + /// The data source does not have enough information to choose an expiry height + /// for the transaction. + SyncRequired, +} + +impl From> for InputSelectorError { + fn from(value: ConversionError<&'static str>) -> Self { + InputSelectorError::Address(value) + } } impl fmt::Display for InputSelectorError { @@ -50,61 +81,48 @@ impl fmt::Display for InputSelectorError { write!(f, "Note selection encountered the following error: {}", 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 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 - } - /// 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 error::Error for InputSelectorError +where + DE: Debug + Display + error::Error + 'static, + SE: Debug + Display + error::Error + 'static, +{ + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match &self { + Self::DataSource(e) => Some(e), + Self::Selection(e) => Some(e), + Self::Proposal(e) => Some(e), + _ => None, + } } } @@ -116,12 +134,12 @@ 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 data source that the input selector expects to access to obtain input Sapling + /// 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; /// The type of the fee rule that this input selector uses when computing fees. type FeeRule: FeeRule; @@ -139,22 +157,38 @@ 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( &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< - Proposal::DataSource as WalletRead>::NoteRef>, - InputSelectorError<<::DataSource as WalletRead>::Error, Self::Error>, + Proposal::NoteRef>, + InputSelectorError<::Error, Self::Error>, > where ParamsT: consensus::Parameters; +} + +/// 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; + /// The type of the fee rule that this input selector uses when computing fees. + type FeeRule: FeeRule; /// Performs input selection and returns a proposal for the construction of a shielding /// transaction. @@ -164,19 +198,18 @@ 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( &self, params: &ParamsT, - wallet_db: &Self::DataSource, + wallet_db: &Self::InputSource, shielding_threshold: NonNegativeAmount, source_addrs: &[TransparentAddress], - confirmed_height: BlockHeight, target_height: BlockHeight, + min_confirmations: u32, ) -> Result< - Proposal::DataSource as WalletRead>::NoteRef>, - InputSelectorError<<::DataSource as WalletRead>::Error, Self::Error>, + Proposal, + InputSelectorError<::Error, Self::Error>, > where ParamsT: consensus::Parameters; @@ -238,17 +271,35 @@ impl From } } -pub(crate) struct SaplingPayment(Amount); +pub(crate) struct SaplingPayment(NonNegativeAmount); #[cfg(test)] impl SaplingPayment { - pub(crate) fn new(amount: Amount) -> Self { + pub(crate) fn new(amount: NonNegativeAmount) -> Self { SaplingPayment(amount) } } impl sapling::OutputView for SaplingPayment { - fn value(&self) -> Amount { + fn value(&self) -> NonNegativeAmount { + self.0 + } +} + +#[cfg(feature = "orchard")] +pub(crate) struct OrchardPayment(NonNegativeAmount); + +#[cfg(test)] +#[cfg(feature = "orchard")] +impl OrchardPayment { + pub(crate) fn new(amount: NonNegativeAmount) -> Self { + OrchardPayment(amount) + } +} + +#[cfg(feature = "orchard")] +impl orchard_fees::OutputView for OrchardPayment { + fn value(&self) -> NonNegativeAmount { self.0 } } @@ -256,8 +307,8 @@ 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. +/// This implementation performs input selection using methods available via the +/// [`InputSource`] interface. pub struct GreedyInputSelector { change_strategy: ChangeT, dust_output_policy: DustOutputPolicy, @@ -278,96 +329,180 @@ impl GreedyInputSelector { impl InputSelector for GreedyInputSelector where - DbT: WalletRead, + DbT: InputSource, ChangeT: ChangeStrategy, ChangeT::FeeRule: Clone, { type Error = GreedyInputSelectorError; - type DataSource = DbT; + type InputSource = DbT; type FeeRule = ChangeT::FeeRule; #[allow(clippy::type_complexity)] 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> + ) -> Result< + Proposal, + InputSelectorError<::Error, Self::Error>, + > where ParamsT: consensus::Parameters, + Self::InputSource: InputSource, { 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(); + 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(), + }); } - RecipientAddress::Shielded(_) => { - push_sapling(); + Address::Sapling(_) => { + payment_pools.insert(*idx, PoolType::Shielded(ShieldedProtocol::Sapling)); + sapling_outputs.push(SaplingPayment(payment.amount())); } - RecipientAddress::Unified(addr) => { + Address::Unified(addr) => { + #[cfg(feature = "orchard")] + if addr.orchard().is_some() { + payment_pools.insert(*idx, PoolType::Shielded(ShieldedProtocol::Orchard)); + orchard_outputs.push(OrchardPayment(payment.amount())); + continue; + } + 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())), - )); + payment_pools.insert(*idx, PoolType::Shielded(ShieldedProtocol::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 = NonNegativeAmount::ZERO; + let mut amount_required = NonNegativeAmount::ZERO; let mut exclude: Vec = vec![]; // This loop is guaranteed to terminate because on each iteration we check that the amount // of funds selected is strictly increasing. The loop will either return a successful // result or the wallet will eventually run out of funds to select. loop { + #[cfg(not(feature = "orchard"))] + let use_sapling = true; + #[cfg(feature = "orchard")] + let (use_sapling, use_orchard) = { + let (sapling_input_total, orchard_input_total) = ( + shielded_inputs.sapling_value()?, + shielded_inputs.orchard_value()?, + ); + + // Use Sapling inputs if there are no Orchard outputs or there are not sufficient + // Orchard outputs to cover the amount required. + let use_sapling = + orchard_outputs.is_empty() || amount_required > orchard_input_total; + // Use Orchard inputs if there are insufficient Sapling funds to cover the amount + // reqiuired. + 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 balance = self.change_strategy.compute_balance( params, target_height, &Vec::::new(), &transparent_outputs, - &sapling_inputs, - &sapling_outputs, + &( + ::sapling::builder::BundleType::DEFAULT, + &sapling_inputs[..], + &sapling_outputs[..], + ), + #[cfg(feature = "orchard")] + &( + ::orchard::builder::BundleType::DEFAULT, + &orchard_inputs[..], + &orchard_outputs[..], + ), &self.dust_output_policy, ); match balance { Ok(balance) => { - return Ok(Proposal { + return Proposal::single_step( transaction_request, - transparent_inputs: vec![], - sapling_inputs, + payment_pools, + vec![], + 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)), balance, - fee_rule: (*self.change_strategy.fee_rule()).clone(), + (*self.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; @@ -375,16 +510,22 @@ where Err(other) => return Err(other.into()), } - 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, + 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,23 +538,44 @@ where } } } +} + +#[cfg(feature = "transparent-inputs")] +impl ShieldingSelector for GreedyInputSelector +where + DbT: InputSource, + ChangeT: ChangeStrategy, + ChangeT::FeeRule: Clone, +{ + type Error = GreedyInputSelectorError; + type InputSource = DbT; + type FeeRule = ChangeT::FeeRule; #[allow(clippy::type_complexity)] fn propose_shielding( &self, params: &ParamsT, - wallet_db: &Self::DataSource, + wallet_db: &Self::InputSource, shielding_threshold: NonNegativeAmount, source_addrs: &[TransparentAddress], - confirmed_height: BlockHeight, target_height: BlockHeight, - ) -> Result, InputSelectorError> + min_confirmations: u32, + ) -> Result< + Proposal, + InputSelectorError<::Error, Self::Error>, + > where ParamsT: consensus::Parameters, { let mut transparent_inputs: Vec = source_addrs .iter() - .map(|taddr| wallet_db.get_unspent_transparent_outputs(taddr, confirmed_height, &[])) + .map(|taddr| { + wallet_db.get_unspent_transparent_outputs( + taddr, + target_height - min_confirmations, + &[], + ) + }) .collect::>, _>>() .map_err(InputSelectorError::DataSource)? .into_iter() @@ -425,8 +587,17 @@ where target_height, &transparent_inputs, &Vec::::new(), - &Vec::>::new(), - &Vec::::new(), + &( + ::sapling::builder::BundleType::DEFAULT, + &Vec::::new()[..], + &Vec::::new()[..], + ), + #[cfg(feature = "orchard")] + &( + orchard::builder::BundleType::DEFAULT, + &Vec::::new()[..], + &Vec::::new()[..], + ), &self.dust_output_policy, ); @@ -441,8 +612,17 @@ where target_height, &transparent_inputs, &Vec::::new(), - &Vec::>::new(), - &Vec::::new(), + &( + ::sapling::builder::BundleType::DEFAULT, + &Vec::::new()[..], + &Vec::::new()[..], + ), + #[cfg(feature = "orchard")] + &( + orchard::builder::BundleType::DEFAULT, + &Vec::::new()[..], + &Vec::::new()[..], + ), &self.dust_output_policy, )? } @@ -451,20 +631,22 @@ where } }; - 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(), + (*self.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..51f9abcdbd 100644 --- a/zcash_client_backend/src/decrypt.rs +++ b/zcash_client_backend/src/decrypt.rs @@ -1,19 +1,19 @@ use std::collections::HashMap; +use sapling::note_encryption::{PreparedIncomingViewingKey, SaplingDomain}; +use zcash_note_encryption::{try_note_decryption, try_output_recovery_with_ovk}; use zcash_primitives::{ consensus::{self, BlockHeight}, memo::MemoBytes, - sapling::{ - self, - note_encryption::{ - try_sapling_note_decryption, try_sapling_output_recovery, PreparedIncomingViewingKey, - }, - }, + transaction::components::{amount::NonNegativeAmount, sapling::zip212_enforcement}, transaction::Transaction, - zip32::{AccountId, Scope}, + zip32::Scope, }; -use crate::keys::UnifiedFullViewingKey; +use crate::{data_api::DecryptedTransaction, 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 +30,92 @@ 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) -> NonNegativeAmount { + NonNegativeAmount::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) -> NonNegativeAmount { + NonNegativeAmount::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( +pub fn decrypt_transaction<'a, P: consensus::Parameters, AccountId: Copy>( params: &P, height: BlockHeight, - tx: &Transaction, + tx: &'a Transaction, ufvks: &HashMap, -) -> Vec> { - tx.sapling_bundle() +) -> DecryptedTransaction<'a, AccountId> { + let zip212_enforcement = zip212_enforcement(params, height); + let sapling_bundle = tx.sapling_bundle(); + let sapling_outputs = sapling_bundle .iter() .flat_map(|bundle| { ufvks .iter() - .flat_map(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 +127,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); + let account = account; + 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( + 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..9106a3883a 100644 --- a/zcash_client_backend/src/fees.rs +++ b/zcash_client_backend/src/fees.rs @@ -2,32 +2,81 @@ use std::fmt; use zcash_primitives::{ consensus::{self, BlockHeight}, + memo::MemoBytes, transaction::{ components::{ - amount::{Amount, BalanceError}, - sapling::fees as sapling, - transparent::fees as transparent, + amount::{BalanceError, NonNegativeAmount}, OutPoint, }, - fees::FeeRule, + fees::{transparent, FeeRule}, }, }; +use crate::ShieldedProtocol; + +pub(crate) mod common; 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. #[derive(Clone, Debug, PartialEq, Eq)] -pub enum ChangeValue { - Sapling(Amount), +pub struct ChangeValue { + output_pool: ShieldedProtocol, + value: NonNegativeAmount, + memo: Option, } impl ChangeValue { - pub fn value(&self) -> Amount { - match self { - ChangeValue::Sapling(value) => *value, + /// Constructs a new change value from its constituent parts. + pub fn new( + output_pool: ShieldedProtocol, + value: NonNegativeAmount, + memo: Option, + ) -> Self { + Self { + output_pool, + value, + memo, + } + } + + /// Constructs a new change value that will be created as a Sapling output. + pub fn sapling(value: NonNegativeAmount, memo: Option) -> Self { + Self { + output_pool: ShieldedProtocol::Sapling, + value, + memo, + } + } + + /// Constructs a new change value that will be created as an Orchard output. + #[cfg(feature = "orchard")] + pub fn orchard(value: NonNegativeAmount, memo: Option) -> Self { + Self { + output_pool: ShieldedProtocol::Orchard, + value, + memo, } } + + /// Returns the pool to which the change output should be sent. + pub fn output_pool(&self) -> ShieldedProtocol { + self.output_pool + } + + /// Returns the value of the change output to be created, in zatoshis. + pub fn value(&self) -> NonNegativeAmount { + self.value + } + + /// Returns the memo to be associated with the change output. + pub fn memo(&self) -> Option<&MemoBytes> { + self.memo.as_ref() + } } /// The amount of change and fees required to make a transaction's inputs and @@ -36,38 +85,46 @@ impl ChangeValue { #[derive(Clone, Debug, PartialEq, Eq)] pub struct TransactionBalance { proposed_change: Vec, - fee_required: Amount, - total: Amount, + fee_required: NonNegativeAmount, + + // A cache for the sum of proposed change and fee; we compute it on construction anyway, so we + // cache the resulting value. + total: NonNegativeAmount, } 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: NonNegativeAmount, + ) -> 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) -> NonNegativeAmount { self.fee_required } /// Returns the sum of the proposed change outputs and the required fee. - pub fn total(&self) -> Amount { + pub fn total(&self) -> NonNegativeAmount { self.total } } @@ -79,10 +136,10 @@ pub enum ChangeError { /// required outputs and fees. InsufficientFunds { /// The total of the inputs provided to change selection - available: Amount, + available: NonNegativeAmount, /// The total amount of input value required to fund the requested outputs, /// including the required fees. - required: Amount, + required: NonNegativeAmount, }, /// 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 @@ -90,11 +147,43 @@ pub enum ChangeError { DustInputs { /// The outpoints corresponding to transparent inputs having no current economic value. transparent: Vec, - /// The identifiers for Sapling inputs having not current economic value + /// The identifiers for Sapling inputs having no current economic value sapling: Vec, + /// The identifiers for Orchard inputs having no current economic value + #[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 ChangeError { + pub(crate) fn map E0>(self, f: F) -> ChangeError { + match self { + ChangeError::InsufficientFunds { + available, + required, + } => ChangeError::InsufficientFunds { + available, + required, + }, + ChangeError::DustInputs { + transparent, + sapling, + #[cfg(feature = "orchard")] + orchard, + } => ChangeError::DustInputs { + transparent, + sapling, + #[cfg(feature = "orchard")] + orchard, + }, + ChangeError::StrategyError(e) => ChangeError::StrategyError(f(e)), + ChangeError::BundleError(e) => ChangeError::BundleError(e), + } + } } impl fmt::Display for ChangeError { @@ -106,20 +195,38 @@ 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 + ) + } } } } @@ -147,7 +254,7 @@ pub enum DustAction { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct DustOutputPolicy { action: DustAction, - dust_threshold: Option, + dust_threshold: Option, } impl DustOutputPolicy { @@ -157,7 +264,7 @@ impl DustOutputPolicy { /// 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 /// 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, @@ -170,7 +277,7 @@ impl DustOutputPolicy { } /// 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 } } @@ -205,20 +312,25 @@ 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], + sapling: &impl sapling::BundleView, + #[cfg(feature = "orchard")] orchard: &impl orchard::BundleView, dust_output_policy: &DustOutputPolicy, ) -> 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 zcash_primitives::transaction::{ + components::{ + amount::NonNegativeAmount, + transparent::{OutPoint, TxOut}, + }, + fees::transparent, }; + use super::sapling; + + #[derive(Debug)] pub(crate) struct TestTransparentInput { pub outpoint: OutPoint, pub coin: TxOut, @@ -235,14 +347,14 @@ pub(crate) mod tests { pub(crate) struct TestSaplingInput { pub note_id: u32, - pub value: Amount, + pub value: NonNegativeAmount, } impl sapling::InputView for TestSaplingInput { fn note_id(&self) -> &u32 { &self.note_id } - fn value(&self) -> Amount { + fn value(&self) -> NonNegativeAmount { 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..a8a791a527 --- /dev/null +++ b/zcash_client_backend/src/fees/common.rs @@ -0,0 +1,241 @@ +use zcash_primitives::{ + consensus::{self, BlockHeight}, + memo::MemoBytes, + transaction::{ + components::amount::{BalanceError, NonNegativeAmount}, + fees::{transparent, FeeRule}, + }, +}; + +use crate::ShieldedProtocol; + +use super::{ + sapling as sapling_fees, ChangeError, ChangeValue, DustAction, DustOutputPolicy, + TransactionBalance, +}; + +#[cfg(feature = "orchard")] +use super::orchard as orchard_fees; + +pub(crate) struct NetFlows { + t_in: NonNegativeAmount, + t_out: NonNegativeAmount, + sapling_in: NonNegativeAmount, + sapling_out: NonNegativeAmount, + orchard_in: NonNegativeAmount, + orchard_out: NonNegativeAmount, +} + +#[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, +) -> 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) + .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(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 = NonNegativeAmount::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 = NonNegativeAmount::ZERO; + + Ok(NetFlows { + t_in, + t_out, + sapling_in, + sapling_out, + orchard_in, + orchard_out, + }) +} + +pub(crate) fn single_change_output_policy( + _net_flows: &NetFlows, + _fallback_change_pool: ShieldedProtocol, +) -> Result<(ShieldedProtocol, usize, usize), ChangeError> +where + E: From + From, +{ + // TODO: implement a less naive strategy for selecting the pool to which change will be sent. + #[cfg(feature = "orchard")] + let (change_pool, sapling_change, orchard_change) = + 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, 0, 1) + } 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, 1, 0) + } else { + // This is a fully-transparent transaction, so the caller gets to decide + // where to shield change. + match _fallback_change_pool { + ShieldedProtocol::Orchard => (_fallback_change_pool, 0, 1), + ShieldedProtocol::Sapling => (_fallback_change_pool, 1, 0), + } + }; + #[cfg(not(feature = "orchard"))] + let (change_pool, sapling_change, orchard_change) = (ShieldedProtocol::Sapling, 1, 0); + + Ok((change_pool, sapling_change, orchard_change)) +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn single_change_output_balance< + P: consensus::Parameters, + NoteRefT: Clone, + F: FeeRule, + E, +>( + params: &P, + fee_rule: &F, + 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, + dust_output_policy: &DustOutputPolicy, + default_dust_threshold: NonNegativeAmount, + change_memo: Option, + _fallback_change_pool: ShieldedProtocol, +) -> Result> +where + E: From + From, +{ + 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, + )?; + let (change_pool, sapling_change, _orchard_change) = + single_change_output_policy::(&net_flows, _fallback_change_pool)?; + + let sapling_input_count = sapling + .bundle_type() + .num_spends(sapling.inputs().len()) + .map_err(ChangeError::BundleError)?; + let sapling_output_count = sapling + .bundle_type() + .num_outputs( + sapling.inputs().len(), + sapling.outputs().len() + sapling_change, + ) + .map_err(ChangeError::BundleError)?; + + #[cfg(feature = "orchard")] + let orchard_action_count = orchard + .bundle_type() + .num_actions( + orchard.inputs().len(), + orchard.outputs().len() + _orchard_change, + ) + .map_err(ChangeError::BundleError)?; + #[cfg(not(feature = "orchard"))] + let orchard_action_count = 0; + + let fee_amount = fee_rule + .fee_required( + params, + target_height, + transparent_inputs, + transparent_outputs, + sapling_input_count, + sapling_output_count, + orchard_action_count, + ) + .map_err(|fee_error| ChangeError::StrategyError(E::from(fee_error)))?; + + let total_in = + (net_flows.t_in + net_flows.sapling_in + net_flows.orchard_in).ok_or_else(overflow)?; + let total_out = (net_flows.t_out + net_flows.sapling_out + net_flows.orchard_out + fee_amount) + .ok_or_else(overflow)?; + + let proposed_change = (total_in - total_out).ok_or(ChangeError::InsufficientFunds { + available: total_in, + required: total_out, + })?; + + if proposed_change.is_zero() { + TransactionBalance::new(vec![], fee_amount).map_err(|_| overflow()) + } else { + let dust_threshold = dust_output_policy + .dust_threshold() + .unwrap_or(default_dust_threshold); + + if proposed_change < dust_threshold { + 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::new(change_pool, proposed_change, change_memo)], + fee_amount, + ) + .map_err(|_| overflow()), + DustAction::AddDustToFee => TransactionBalance::new( + vec![], + (fee_amount + proposed_change).ok_or_else(overflow)?, + ) + .map_err(|_| overflow()), + } + } else { + TransactionBalance::new( + vec![ChangeValue::new(change_pool, proposed_change, change_memo)], + fee_amount, + ) + .map_err(|_| overflow()) + } + } +} diff --git a/zcash_client_backend/src/fees/fixed.rs b/zcash_client_backend/src/fees/fixed.rs index 28b309bb5a..a20ab3ef13 100644 --- a/zcash_client_backend/src/fees/fixed.rs +++ b/zcash_client_backend/src/fees/fixed.rs @@ -1,32 +1,48 @@ //! Change strategies designed for use with a fixed fee. -use std::cmp::Ordering; use zcash_primitives::{ consensus::{self, BlockHeight}, + memo::MemoBytes, transaction::{ - components::{ - amount::{Amount, BalanceError}, - sapling::fees as sapling, - transparent::fees as transparent, - }, - fees::{fixed::FeeRule as FixedFeeRule, FeeRule}, + components::amount::BalanceError, + fees::{fixed::FeeRule as FixedFeeRule, transparent}, }, }; +use crate::ShieldedProtocol; + use super::{ - ChangeError, ChangeStrategy, ChangeValue, DustAction, DustOutputPolicy, TransactionBalance, + common::single_change_output_balance, sapling as sapling_fees, ChangeError, ChangeStrategy, + DustOutputPolicy, TransactionBalance, }; -/// A change strategy that and proposes change as a single output to the most current supported +#[cfg(feature = "orchard")] +use super::orchard as orchard_fees; + +/// A change strategy that 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: FixedFeeRule, + change_memo: Option, + fallback_change_pool: ShieldedProtocol, } impl SingleOutputChangeStrategy { - /// Constructs a new [`SingleOutputChangeStrategy`] with the specified fee rule. - pub fn new(fee_rule: FixedFeeRule) -> Self { - Self { fee_rule } + /// 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, + ) -> Self { + Self { + fee_rule, + change_memo, + fallback_change_pool, + } } } @@ -44,120 +60,36 @@ impl ChangeStrategy for SingleOutputChangeStrategy { target_height: BlockHeight, transparent_inputs: &[impl transparent::InputView], transparent_outputs: &[impl transparent::OutputView], - sapling_inputs: &[impl sapling::InputView], - sapling_outputs: &[impl sapling::OutputView], + sapling: &impl sapling_fees::BundleView, + #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView, dust_output_policy: &DustOutputPolicy, ) -> 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()) - } - } - } - } + single_change_output_balance( + params, + &self.fee_rule, + target_height, + transparent_inputs, + transparent_outputs, + sapling, + #[cfg(feature = "orchard")] + orchard, + dust_output_policy, + self.fee_rule().fixed_fee(), + self.change_memo.clone(), + self.fallback_change_pool, + ) } } #[cfg(test)] mod tests { + #[cfg(feature = "orchard")] + use std::convert::Infallible; + use zcash_primitives::{ consensus::{Network, NetworkUpgrade, Parameters}, transaction::{ - components::{amount::Amount, transparent::TxOut}, + components::{amount::NonNegativeAmount, transparent::TxOut}, fees::fixed::FeeRule as FixedFeeRule, }, }; @@ -169,13 +101,15 @@ mod tests { tests::{TestSaplingInput, TestTransparentInput}, ChangeError, ChangeStrategy, ChangeValue, DustOutputPolicy, }, + ShieldedProtocol, }; #[test] fn change_without_dust() { #[allow(deprecated)] let fee_rule = FixedFeeRule::standard(); - let change_strategy = SingleOutputChangeStrategy::new(fee_rule); + let change_strategy = + SingleOutputChangeStrategy::new(fee_rule, None, ShieldedProtocol::Sapling); // spend a single Sapling note that is sufficient to pay the fee let result = change_strategy.compute_balance( @@ -185,18 +119,30 @@ mod tests { .unwrap(), &Vec::::new(), &Vec::::new(), - &[TestSaplingInput { - note_id: 0, - value: Amount::from_u64(60000).unwrap(), - }], - &[SaplingPayment::new(Amount::from_u64(40000).unwrap())], + &( + sapling::builder::BundleType::DEFAULT, + &[TestSaplingInput { + note_id: 0, + value: NonNegativeAmount::const_from_u64(60000), + }][..], + &[SaplingPayment::new(NonNegativeAmount::const_from_u64( + 40000, + ))][..], + ), + #[cfg(feature = "orchard")] + &( + orchard::builder::BundleType::DEFAULT, + &[] as &[Infallible], + &[] as &[Infallible], + ), &DustOutputPolicy::default(), ); 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(NonNegativeAmount::const_from_u64(10000), None)] && + balance.fee_required() == NonNegativeAmount::const_from_u64(10000) ); } @@ -204,7 +150,8 @@ mod tests { fn dust_change() { #[allow(deprecated)] let fee_rule = FixedFeeRule::standard(); - let change_strategy = SingleOutputChangeStrategy::new(fee_rule); + let change_strategy = + SingleOutputChangeStrategy::new(fee_rule, None, ShieldedProtocol::Sapling); // spend a single Sapling note that is sufficient to pay the fee let result = change_strategy.compute_balance( @@ -214,25 +161,36 @@ mod tests { .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())], + &( + sapling::builder::BundleType::DEFAULT, + &[ + TestSaplingInput { + note_id: 0, + value: NonNegativeAmount::const_from_u64(40000), + }, + // enough to pay a fee, plus dust + TestSaplingInput { + note_id: 0, + value: NonNegativeAmount::const_from_u64(10100), + }, + ][..], + &[SaplingPayment::new(NonNegativeAmount::const_from_u64( + 40000, + ))][..], + ), + #[cfg(feature = "orchard")] + &( + orchard::builder::BundleType::DEFAULT, + &[] as &[Infallible], + &[] as &[Infallible], + ), &DustOutputPolicy::default(), ); assert_matches!( result, Err(ChangeError::InsufficientFunds { available, required }) - if available == Amount::from_u64(50100).unwrap() && required == Amount::from_u64(60000).unwrap() + if available == NonNegativeAmount::const_from_u64(50100) && required == NonNegativeAmount::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..ac90a01130 --- /dev/null +++ b/zcash_client_backend/src/fees/orchard.rs @@ -0,0 +1,73 @@ +//! Types related to computation of fees and change related to the Orchard components +//! of a transaction. + +use std::convert::Infallible; +use zcash_primitives::transaction::components::amount::NonNegativeAmount; + +use orchard::builder::BundleType; + +/// 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 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) -> NonNegativeAmount; +} + +impl InputView for Infallible { + fn note_id(&self) -> &N { + unreachable!() + } + fn value(&self) -> NonNegativeAmount { + 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) -> NonNegativeAmount; +} + +impl OutputView for Infallible { + fn value(&self) -> NonNegativeAmount { + 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..fa7ef6157e --- /dev/null +++ b/zcash_client_backend/src/fees/sapling.rs @@ -0,0 +1,93 @@ +//! 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_primitives::transaction::components::amount::NonNegativeAmount; + +/// 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 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) -> NonNegativeAmount; +} + +impl InputView for Infallible { + fn note_id(&self) -> &N { + unreachable!() + } + fn value(&self) -> NonNegativeAmount { + 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) -> NonNegativeAmount { + NonNegativeAmount::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) -> NonNegativeAmount; +} + +impl OutputView for OutputInfo { + fn value(&self) -> NonNegativeAmount { + NonNegativeAmount::try_from(self.value().inner()) + .expect("Output values should be checked at construction.") + } +} + +impl OutputView for Infallible { + fn value(&self) -> NonNegativeAmount { + 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..fd146b5105 --- /dev/null +++ b/zcash_client_backend/src/fees/standard.rs @@ -0,0 +1,123 @@ +//! Change strategies designed for use with a standard fee. + +use zcash_primitives::{ + consensus::{self, BlockHeight}, + memo::MemoBytes, + transaction::{ + components::amount::NonNegativeAmount, + fees::{ + fixed::FeeRule as FixedFeeRule, + transparent, + zip317::{FeeError as Zip317FeeError, FeeRule as Zip317FeeRule}, + StandardFeeRule, + }, + }, +}; + +use crate::ShieldedProtocol; + +use super::{ + fixed, sapling as sapling_fees, zip317, ChangeError, ChangeStrategy, DustOutputPolicy, + TransactionBalance, +}; + +#[cfg(feature = "orchard")] +use super::orchard as orchard_fees; + +/// A change strategy that 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: StandardFeeRule, + change_memo: Option, + fallback_change_pool: ShieldedProtocol, +} + +impl SingleOutputChangeStrategy { + /// Constructs a new [`SingleOutputChangeStrategy`] with the specified ZIP 317 + /// fee parameters. + /// + /// `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: StandardFeeRule, + change_memo: Option, + fallback_change_pool: ShieldedProtocol, + ) -> Self { + Self { + fee_rule, + change_memo, + fallback_change_pool, + } + } +} + +impl ChangeStrategy for SingleOutputChangeStrategy { + type FeeRule = StandardFeeRule; + type Error = Zip317FeeError; + + fn fee_rule(&self) -> &Self::FeeRule { + &self.fee_rule + } + + 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, + dust_output_policy: &DustOutputPolicy, + ) -> Result> { + #[allow(deprecated)] + match self.fee_rule() { + StandardFeeRule::PreZip313 => fixed::SingleOutputChangeStrategy::new( + FixedFeeRule::non_standard(NonNegativeAmount::const_from_u64(10000)), + self.change_memo.clone(), + self.fallback_change_pool, + ) + .compute_balance( + params, + target_height, + transparent_inputs, + transparent_outputs, + sapling, + #[cfg(feature = "orchard")] + orchard, + dust_output_policy, + ) + .map_err(|e| e.map(Zip317FeeError::Balance)), + StandardFeeRule::Zip313 => fixed::SingleOutputChangeStrategy::new( + FixedFeeRule::non_standard(NonNegativeAmount::const_from_u64(1000)), + self.change_memo.clone(), + self.fallback_change_pool, + ) + .compute_balance( + params, + target_height, + transparent_inputs, + transparent_outputs, + sapling, + #[cfg(feature = "orchard")] + orchard, + dust_output_policy, + ) + .map_err(|e| e.map(Zip317FeeError::Balance)), + StandardFeeRule::Zip317 => zip317::SingleOutputChangeStrategy::new( + Zip317FeeRule::standard(), + self.change_memo.clone(), + self.fallback_change_pool, + ) + .compute_balance( + params, + target_height, + transparent_inputs, + transparent_outputs, + sapling, + #[cfg(feature = "orchard")] + orchard, + dust_output_policy, + ), + } + } +} diff --git a/zcash_client_backend/src/fees/zip317.rs b/zcash_client_backend/src/fees/zip317.rs index 142bf7ce65..e3acacd590 100644 --- a/zcash_client_backend/src/fees/zip317.rs +++ b/zcash_client_backend/src/fees/zip317.rs @@ -3,38 +3,50 @@ //! 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::{ 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, + transaction::fees::{ + transparent, + zip317::{FeeError as Zip317FeeError, FeeRule as Zip317FeeRule}, }, }; +use crate::ShieldedProtocol; + use super::{ - ChangeError, ChangeStrategy, ChangeValue, DustAction, DustOutputPolicy, TransactionBalance, + common::{calculate_net_flows, single_change_output_balance, single_change_output_policy}, + sapling as sapling_fees, ChangeError, ChangeStrategy, DustOutputPolicy, TransactionBalance, }; -/// A change strategy that and proposes change as a single output to the most current supported +#[cfg(feature = "orchard")] +use super::orchard as orchard_fees; + +/// A change strategy that 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, + change_memo: Option, + fallback_change_pool: ShieldedProtocol, } 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: Zip317FeeRule, + change_memo: Option, + fallback_change_pool: ShieldedProtocol, + ) -> Self { + Self { + fee_rule, + change_memo, + fallback_change_pool, + } } } @@ -52,8 +64,8 @@ impl ChangeStrategy for SingleOutputChangeStrategy { target_height: BlockHeight, transparent_inputs: &[impl transparent::InputView], transparent_outputs: &[impl transparent::OutputView], - sapling_inputs: &[impl sapling::InputView], - sapling_outputs: &[impl sapling::OutputView], + sapling: &impl sapling_fees::BundleView, + #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView, dust_output_policy: &DustOutputPolicy, ) -> Result> { let mut transparent_dust: Vec<_> = transparent_inputs @@ -69,43 +81,85 @@ impl ChangeStrategy for SingleOutputChangeStrategy { }) .collect(); - let mut sapling_dust: Vec<_> = sapling_inputs + let mut sapling_dust: Vec<_> = sapling + .inputs() + .iter() + .filter_map(|i| { + if sapling_fees::InputView::::value(i) < self.fee_rule.marginal_fee() { + Some(sapling_fees::InputView::::note_id(i).clone()) + } else { + None + } + }) + .collect(); + + #[cfg(feature = "orchard")] + let mut orchard_dust: Vec = orchard + .inputs() .iter() .filter_map(|i| { - if i.value() < self.fee_rule.marginal_fee() { - Some(i.note_id().clone()) + if orchard_fees::InputView::::value(i) < self.fee_rule.marginal_fee() { + Some(orchard_fees::InputView::::note_id(i).clone()) } else { None } }) .collect(); + #[cfg(not(feature = "orchard"))] + let mut orchard_dust: Vec = vec![]; // 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()) { + if !(transparent_dust.is_empty() && sapling_dust.is_empty() && orchard_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); + // We add one to either the Sapling or Orchard outputs for the (single) + // change output. Note that this means that wallet-internal shielding + // transactions are an opportunity to spend a dust note. + let net_flows = calculate_net_flows::( + transparent_inputs, + transparent_outputs, + sapling, + #[cfg(feature = "orchard")] + orchard, + )?; + let (_, sapling_change, orchard_change) = + single_change_output_policy::( + &net_flows, + self.fallback_change_pool, + )?; + + let s_non_dust = sapling.inputs().len() - sapling_dust.len(); + let s_allowed_dust = + (sapling.outputs().len() + sapling_change).saturating_sub(s_non_dust); + + #[cfg(feature = "orchard")] + let (orchard_inputs_len, orchard_outputs_len) = + (orchard.inputs().len(), orchard.outputs().len()); + #[cfg(not(feature = "orchard"))] + let (orchard_inputs_len, orchard_outputs_len) = (0, 0); + + let o_non_dust = orchard_inputs_len - orchard_dust.len(); + let o_allowed_dust = (orchard_outputs_len + orchard_change).saturating_sub(o_non_dust); let available_grace_inputs = self .fee_rule .grace_actions() .saturating_sub(t_non_dust) - .saturating_sub(s_non_dust); + .saturating_sub(s_non_dust) + .saturating_sub(o_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); + let mut o_disallowed_dust = orchard_dust.len().saturating_sub(o_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. + // and then to Sapling dust followed by Orchard 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); @@ -113,6 +167,12 @@ impl ChangeStrategy for SingleOutputChangeStrategy { .saturating_sub(t_grace_dust) .saturating_sub(s_disallowed_dust); s_disallowed_dust = s_disallowed_dust.saturating_sub(s_grace_dust); + + let o_grace_dust = available_grace_inputs + .saturating_sub(t_grace_dust) + .saturating_sub(s_grace_dust) + .saturating_sub(o_disallowed_dust); + o_disallowed_dust = o_disallowed_dust.saturating_sub(o_grace_dust); } // Truncate the lists of inputs to be disregarded in input selection to just the @@ -122,111 +182,47 @@ impl ChangeStrategy for SingleOutputChangeStrategy { transparent_dust.truncate(t_disallowed_dust); sapling_dust.reverse(); sapling_dust.truncate(s_disallowed_dust); + orchard_dust.reverse(); + orchard_dust.truncate(o_disallowed_dust); - if !(transparent_dust.is_empty() && sapling_dust.is_empty()) { + if !(transparent_dust.is_empty() && sapling_dust.is_empty() && orchard_dust.is_empty()) + { return Err(ChangeError::DustInputs { transparent: transparent_dust, sapling: sapling_dust, + #[cfg(feature = "orchard")] + orchard: orchard_dust, }); } } - 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_change_output_balance( + params, + &self.fee_rule, + target_height, + transparent_inputs, + transparent_outputs, + sapling, + #[cfg(feature = "orchard")] + orchard, + dust_output_policy, + self.fee_rule.marginal_fee(), + self.change_memo.clone(), + self.fallback_change_pool, + ) } } #[cfg(test)] mod tests { + use std::convert::Infallible; + use zcash_primitives::{ consensus::{Network, NetworkUpgrade, Parameters}, legacy::Script, transaction::{ - components::{amount::Amount, transparent::TxOut}, + components::{amount::NonNegativeAmount, transparent::TxOut}, fees::zip317::FeeRule as Zip317FeeRule, }, }; @@ -238,11 +234,19 @@ mod tests { tests::{TestSaplingInput, TestTransparentInput}, ChangeError, ChangeStrategy, ChangeValue, DustOutputPolicy, }, + ShieldedProtocol, }; + #[cfg(feature = "orchard")] + use crate::data_api::wallet::input_selection::OrchardPayment; + #[test] fn change_without_dust() { - let change_strategy = SingleOutputChangeStrategy::new(Zip317FeeRule::standard()); + let change_strategy = SingleOutputChangeStrategy::new( + Zip317FeeRule::standard(), + None, + ShieldedProtocol::Sapling, + ); // spend a single Sapling note that is sufficient to pay the fee let result = change_strategy.compute_balance( @@ -252,24 +256,83 @@ mod tests { .unwrap(), &Vec::::new(), &Vec::::new(), - &[TestSaplingInput { - note_id: 0, - value: Amount::from_u64(55000).unwrap(), - }], - &[SaplingPayment::new(Amount::from_u64(40000).unwrap())], + &( + sapling::builder::BundleType::DEFAULT, + &[TestSaplingInput { + note_id: 0, + value: NonNegativeAmount::const_from_u64(55000), + }][..], + &[SaplingPayment::new(NonNegativeAmount::const_from_u64( + 40000, + ))][..], + ), + #[cfg(feature = "orchard")] + &( + orchard::builder::BundleType::DEFAULT, + &Vec::::new()[..], + &Vec::::new()[..], + ), + &DustOutputPolicy::default(), + ); + + assert_matches!( + result, + Ok(balance) if + balance.proposed_change() == [ChangeValue::sapling(NonNegativeAmount::const_from_u64(5000), None)] && + balance.fee_required() == NonNegativeAmount::const_from_u64(10000) + ); + } + + #[test] + #[cfg(feature = "orchard")] + fn cross_pool_change_without_dust() { + let change_strategy = SingleOutputChangeStrategy::new( + Zip317FeeRule::standard(), + None, + ShieldedProtocol::Orchard, + ); + + // 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(), + &Vec::::new(), + &Vec::::new(), + &( + sapling::builder::BundleType::DEFAULT, + &[TestSaplingInput { + note_id: 0, + value: NonNegativeAmount::const_from_u64(55000), + }][..], + &Vec::::new()[..], + ), + &( + orchard::builder::BundleType::DEFAULT, + &Vec::::new()[..], + &[OrchardPayment::new(NonNegativeAmount::const_from_u64( + 30000, + ))][..], + ), &DustOutputPolicy::default(), ); 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(NonNegativeAmount::const_from_u64(5000), None)] && + balance.fee_required() == NonNegativeAmount::const_from_u64(20000) ); } #[test] fn change_with_transparent_payments() { - let change_strategy = SingleOutputChangeStrategy::new(Zip317FeeRule::standard()); + let change_strategy = SingleOutputChangeStrategy::new( + Zip317FeeRule::standard(), + None, + ShieldedProtocol::Sapling, + ); // spend a single Sapling note that is sufficient to pay the fee let result = change_strategy.compute_balance( @@ -279,27 +342,40 @@ mod tests { .unwrap(), &Vec::::new(), &[TxOut { - value: Amount::from_u64(40000).unwrap(), + value: NonNegativeAmount::const_from_u64(40000), script_pubkey: Script(vec![]), }], - &[TestSaplingInput { - note_id: 0, - value: Amount::from_u64(55000).unwrap(), - }], - &Vec::::new(), + &( + sapling::builder::BundleType::DEFAULT, + &[TestSaplingInput { + note_id: 0, + value: NonNegativeAmount::const_from_u64(55000), + }][..], + &Vec::::new()[..], + ), + #[cfg(feature = "orchard")] + &( + orchard::builder::BundleType::DEFAULT, + &Vec::::new()[..], + &Vec::::new()[..], + ), &DustOutputPolicy::default(), ); assert_matches!( result, Ok(balance) if balance.proposed_change().is_empty() - && balance.fee_required() == Amount::from_u64(15000).unwrap() + && balance.fee_required() == NonNegativeAmount::const_from_u64(15000) ); } #[test] fn change_with_allowable_dust() { - let change_strategy = SingleOutputChangeStrategy::new(Zip317FeeRule::standard()); + let change_strategy = SingleOutputChangeStrategy::new( + Zip317FeeRule::standard(), + None, + ShieldedProtocol::Sapling, + ); // spend a single Sapling note that is sufficient to pay the fee let result = change_strategy.compute_balance( @@ -309,30 +385,45 @@ mod tests { .unwrap(), &Vec::::new(), &Vec::::new(), - &[ - TestSaplingInput { - note_id: 0, - value: Amount::from_u64(49000).unwrap(), - }, - TestSaplingInput { - note_id: 1, - value: Amount::from_u64(1000).unwrap(), - }, - ], - &[SaplingPayment::new(Amount::from_u64(40000).unwrap())], + &( + sapling::builder::BundleType::DEFAULT, + &[ + TestSaplingInput { + note_id: 0, + value: NonNegativeAmount::const_from_u64(49000), + }, + TestSaplingInput { + note_id: 1, + value: NonNegativeAmount::const_from_u64(1000), + }, + ][..], + &[SaplingPayment::new(NonNegativeAmount::const_from_u64( + 40000, + ))][..], + ), + #[cfg(feature = "orchard")] + &( + orchard::builder::BundleType::DEFAULT, + &Vec::::new()[..], + &Vec::::new()[..], + ), &DustOutputPolicy::default(), ); assert_matches!( result, Ok(balance) if balance.proposed_change().is_empty() - && balance.fee_required() == Amount::from_u64(10000).unwrap() + && balance.fee_required() == NonNegativeAmount::const_from_u64(10000) ); } #[test] fn change_with_disallowed_dust() { - let change_strategy = SingleOutputChangeStrategy::new(Zip317FeeRule::standard()); + let change_strategy = SingleOutputChangeStrategy::new( + Zip317FeeRule::standard(), + None, + ShieldedProtocol::Sapling, + ); // spend a single Sapling note that is sufficient to pay the fee let result = change_strategy.compute_balance( @@ -342,21 +433,32 @@ mod tests { .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())], + &( + sapling::builder::BundleType::DEFAULT, + &[ + TestSaplingInput { + note_id: 0, + value: NonNegativeAmount::const_from_u64(29000), + }, + TestSaplingInput { + note_id: 1, + value: NonNegativeAmount::const_from_u64(20000), + }, + TestSaplingInput { + note_id: 2, + value: NonNegativeAmount::const_from_u64(1000), + }, + ][..], + &[SaplingPayment::new(NonNegativeAmount::const_from_u64( + 40000, + ))][..], + ), + #[cfg(feature = "orchard")] + &( + orchard::builder::BundleType::DEFAULT, + &Vec::::new()[..], + &Vec::::new()[..], + ), &DustOutputPolicy::default(), ); 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..b13434b21a 100644 --- a/zcash_client_backend/src/lib.rs +++ b/zcash_client_backend/src/lib.rs @@ -2,25 +2,86 @@ //! //! `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 use zcash_keys::address; pub mod data_api; mod decrypt; -pub mod encoding; +pub use zcash_keys::encoding; pub mod fees; -pub mod keys; +pub use zcash_keys::keys; +pub mod proposal; pub mod proto; pub mod scan; +pub mod scanning; pub mod wallet; -pub mod welding_rig; -pub mod zip321; +pub use zip321; + +#[cfg(feature = "sync")] +pub mod sync; + +#[cfg(feature = "unstable-serialization")] +pub mod serialization; pub use decrypt::{decrypt_transaction, DecryptedOutput, TransferType}; +pub use zcash_protocol::{PoolType, ShieldedProtocol}; #[cfg(test)] #[macro_use] diff --git a/zcash_client_backend/src/proposal.rs b/zcash_client_backend/src/proposal.rs new file mode 100644 index 0000000000..9fe25934f5 --- /dev/null +++ b/zcash_client_backend/src/proposal.rs @@ -0,0 +1,565 @@ +//! 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::{ + consensus::BlockHeight, + transaction::{components::amount::NonNegativeAmount, TxId}, +}; + +use crate::{ + fees::TransactionBalance, + wallet::{Note, ReceivedNote, WalletTransparentOutput}, + zip321::TransactionRequest, + PoolType, ShieldedProtocol, +}; + +/// 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: NonNegativeAmount, + output_total: NonNegativeAmount, + }, + /// 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, + /// 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, +} + +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::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." + ), + } + } +} + +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::Shielded(ShieldedProtocol::Sapling), + #[cfg(feature = "orchard")] + Note::Orchard(_) => PoolType::Shielded(ShieldedProtocol::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)] +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)] +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) + .fold(Ok(NonNegativeAmount::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()) + .fold(Some(NonNegativeAmount::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() + .fold(Some(NonNegativeAmount::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 == NonNegativeAmount::ZERO + || shielded_input_total > NonNegativeAmount::ZERO + || request_total > NonNegativeAmount::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 { + match pool_type { + PoolType::Transparent => { + self.is_shielding + || !self.transparent_inputs.is_empty() + || self + .payment_pools() + .values() + .any(|pool| matches!(pool, PoolType::Transparent)) + } + PoolType::Shielded(ShieldedProtocol::Sapling) => { + let sapling_in = self.shielded_inputs.iter().any(|s_in| { + s_in.notes() + .iter() + .any(|note| matches!(note.note(), Note::Sapling(_))) + }); + let sapling_out = self + .payment_pools() + .values() + .any(|pool| matches!(pool, PoolType::Shielded(ShieldedProtocol::Sapling))); + let sapling_change = self + .balance + .proposed_change() + .iter() + .any(|c| c.output_pool() == ShieldedProtocol::Sapling); + + sapling_in || sapling_out || sapling_change + } + PoolType::Shielded(ShieldedProtocol::Orchard) => { + #[cfg(not(feature = "orchard"))] + let orchard_in = false; + #[cfg(feature = "orchard")] + let orchard_in = self.shielded_inputs.iter().any(|s_in| { + s_in.notes() + .iter() + .any(|note| matches!(note.note(), Note::Orchard(_))) + }); + let orchard_out = self + .payment_pools() + .values() + .any(|pool| matches!(pool, PoolType::Shielded(ShieldedProtocol::Orchard))); + let orchard_change = self + .balance + .proposed_change() + .iter() + .any(|c| c.output_pool() == ShieldedProtocol::Orchard); + + orchard_in || orchard_out || orchard_change + } + } + } +} + +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 9233354b1c..65d4c483cb 100644 --- a/zcash_client_backend/src/proto.rs +++ b/zcash_client_backend/src/proto.rs @@ -1,21 +1,48 @@ //! 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}, consensus::BlockHeight, - sapling::{note::ExtractedNoteCommitment, Nullifier}, - transaction::{components::sapling, TxId}, + memo::{self, MemoBytes}, + merkle_tree::read_commitment_tree, + transaction::{components::amount::NonNegativeAmount, fees::StandardFeeRule, TxId}, +}; + +use crate::{ + data_api::{chain::ChainState, InputSource}, + fees::{ChangeValue, TransactionBalance}, + proposal::{Proposal, ProposalError, ShieldedInputs, Step, StepOutput, StepOutputIndex}, + zip321::{TransactionRequest, Zip321Error}, + PoolType, ShieldedProtocol, }; -use orchard::note_encryption_v3::COMPACT_NOTE_SIZE_V3 as COMPACT_NOTE_SIZE; -use zcash_note_encryption::EphemeralKeyBytes; +#[cfg(feature = "transparent-inputs")] +use zcash_primitives::transaction::components::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)] @@ -114,10 +141,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(), @@ -126,20 +155,658 @@ 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 proposal did not correctly specify a standard fee rule. + FeeRuleNotSpecified, + /// 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), +} + +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::FeeRuleNotSpecified => { + write!(f, "Proposal did not specify a known fee rule.") + } + 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 + ), + } + } +} + +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::Shielded(ShieldedProtocol::Sapling)), + Ok(proposal::ValuePool::Orchard) => Ok(PoolType::Shielded(ShieldedProtocol::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(), + }), + }) + .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(); + + #[allow(deprecated)] + proposal::Proposal { + proto_version: PROPOSAL_SER_V1, + fee_rule: match value.fee_rule() { + StandardFeeRule::PreZip313 => proposal::FeeRule::PreZip313, + StandardFeeRule::Zip313 => proposal::FeeRule::Zip313, + 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 => { + #[allow(deprecated)] + let fee_rule = match self.fee_rule() { + proposal::FeeRule::PreZip313 => StandardFeeRule::PreZip313, + proposal::FeeRule::Zip313 => StandardFeeRule::Zip313, + proposal::FeeRule::Zip317 => StandardFeeRule::Zip317, + proposal::FeeRule::NotSpecified => { + return Err(ProposalDecodingError::FeeRuleNotSpecified); + } + }; + + 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>>()?; + + #[cfg(not(feature = "transparent-inputs"))] + let transparent_inputs = vec![]; + #[cfg(feature = "transparent-inputs")] + 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( + 1, + )); + + #[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 = NonNegativeAmount::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()? { + PoolType::Shielded(ShieldedProtocol::Sapling) => { + Ok(ChangeValue::sapling(value, memo)) + } + #[cfg(feature = "orchard")] + PoolType::Shielded(ShieldedProtocol::Orchard) => { + Ok(ChangeValue::orchard(value, memo)) + } + PoolType::Transparent if memo.is_some() => { + Err(ProposalDecodingError::TransparentMemo) + } + t => Err(ProposalDecodingError::InvalidChangeRecipient(t)), + } + }) + .collect::, _>>()?, + NonNegativeAmount::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..44455378f7 100644 --- a/zcash_client_backend/src/proto/compact_formats.rs +++ b/zcash_client_backend/src/proto/compact_formats.rs @@ -1,3 +1,16 @@ +/// Information about the state of the chain as of a given block. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, 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 @@ -26,7 +39,12 @@ 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. @@ -57,35 +75,35 @@ 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. +/// 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. #[allow(clippy::derive_partial_eq_without_eq)] #[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) +/// A compact representation of an [Orchard Action](). #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct CompactOrchardAction { diff --git a/zcash_client_backend/src/proto/proposal.rs b/zcash_client_backend/src/proto/proposal.rs new file mode 100644 index 0000000000..a17b83bf8b --- /dev/null +++ b/zcash_client_backend/src/proto/proposal.rs @@ -0,0 +1,227 @@ +/// A data structure that describes a series of transactions to be created. +#[allow(clippy::derive_partial_eq_without_eq)] +#[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. +#[allow(clippy::derive_partial_eq_without_eq)] +#[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. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, 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. +#[allow(clippy::derive_partial_eq_without_eq)] +#[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 a payment in a prior step of the proposal. This payment must +/// belong to the wallet. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PriorStepOutput { + #[prost(uint32, tag = "1")] + pub step_index: u32, + #[prost(uint32, tag = "2")] + pub payment_index: u32, +} +/// A reference a change output from a prior step of the proposal. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, 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. +#[allow(clippy::derive_partial_eq_without_eq)] +#[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 { + #[allow(clippy::derive_partial_eq_without_eq)] + #[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. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct TransactionBalance { + /// A list of change 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 output. If the transparent value pool is selected, +/// the `memo` field must be null. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ChangeValue { + /// The value of a change output to be created, in zatoshis. + #[prost(uint64, tag = "1")] + pub value: u64, + /// The value pool in which the change output should be created. + #[prost(enumeration = "ValuePool", tag = "2")] + pub value_pool: i32, + /// The optional memo that should be associated with the newly created change output. + /// Memos must not be present for transparent change outputs. + #[prost(message, optional, tag = "3")] + pub memo: ::core::option::Option, +} +/// An object wrapper for memo bytes, to facilitate representing the +/// `change_memo == None` case. +#[allow(clippy::derive_partial_eq_without_eq)] +#[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 { + ValuePool::PoolNotSpecified => "PoolNotSpecified", + ValuePool::Transparent => "Transparent", + ValuePool::Sapling => "Sapling", + ValuePool::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 { + FeeRule::NotSpecified => "FeeRuleNotSpecified", + FeeRule::PreZip313 => "PreZip313", + FeeRule::Zip313 => "Zip313", + FeeRule::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..ccfd3fed7a 100644 --- a/zcash_client_backend/src/proto/service.rs +++ b/zcash_client_backend/src/proto/service.rs @@ -187,6 +187,32 @@ pub struct TreeState { #[prost(string, tag = "6")] pub orchard_tree: ::prost::alloc::string::String, } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, 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, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[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)] @@ -222,7 +248,34 @@ 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 { + ShieldedProtocol::Sapling => "sapling", + ShieldedProtocol::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)] use tonic::codegen::*; @@ -231,17 +284,6 @@ pub mod compact_tx_streamer_client { 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, @@ -366,6 +408,37 @@ 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::new( + tonic::Code::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, @@ -399,6 +472,39 @@ 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::new( + tonic::Code::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, @@ -671,6 +777,38 @@ 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::new( + tonic::Code::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, diff --git a/zcash_client_backend/src/scan.rs b/zcash_client_backend/src/scan.rs index 8904757ac3..0eb9abedb5 100644 --- a/zcash_client_backend/src/scan.rs +++ b/zcash_client_backend/src/scan.rs @@ -8,36 +8,107 @@ use std::sync::{ }; use memuse::DynamicUsage; -use zcash_note_encryption::{batch, BatchDomain, Domain, ShieldedOutput}; +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; + + // Once we reach MSRV 1.75.0, this can return `impl Iterator`. + fn batch_decrypt( + tags: &[IvkTag], + ivks: &[D::IncomingViewingKey], + outputs: &[(D, Output)], + ) -> Vec>>; +} + +/// A decryptor of outputs as encoded in transactions. +pub(crate) struct FullDecryptor; + +impl> Decryptor + for FullDecryptor +{ + type Memo = D::Memo; + + fn batch_decrypt( + tags: &[IvkTag], + ivks: &[D::IncomingViewingKey], + outputs: &[(D, Output)], + ) -> Vec>> { + 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, + }) + }) + .collect() + } +} + +/// 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)], + ) -> Vec>> { + 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: (), + }) + }) + .collect() + } +} + /// A value correlated with an output index. struct OutputIndex { /// The index of the output within the corresponding shielded bundle. @@ -46,12 +117,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 +136,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(); @@ -84,7 +155,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); @@ -208,8 +279,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 +290,16 @@ pub(crate) struct Batch> { /// all be part of the same struct, which would also track the output index /// (that is captured in the outer `OutputIndex` of each `OutputReplier`). outputs: Vec<(D, Output)>, - repliers: Vec>, + 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 +325,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 +348,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 +372,16 @@ where assert_eq!(outputs.len(), repliers.len()); - let decryption_results = batch::try_compact_note_decryption(&ivks, &outputs); + let decryption_results = Dec::batch_decrypt(&tags, &ivks, &outputs); for (decryption_result, OutputReplier(replier)) in decryption_results.into_iter().zip(repliers.into_iter()) { // 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 +393,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 +440,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 +488,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 +510,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 +528,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 +568,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..78db76aefb --- /dev/null +++ b/zcash_client_backend/src/scanning.rs @@ -0,0 +1,1533 @@ +//! 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::{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::{ + consensus::{self, BlockHeight, NetworkUpgrade}, + transaction::{components::sapling::zip212_enforcement, TxId}, +}; +use zip32::Scope; + +use crate::{ + data_api::{BlockMetadata, ScannedBlock, ScannedBundles}, + proto::compact_formats::CompactBlock, + scan::{Batch, BatchRunner, CompactDecryptor, DecryptedOutput, Tasks}, + wallet::{WalletOutput, WalletSpend, WalletTx}, + ShieldedProtocol, +}; + +#[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::Sapling, + 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, + is_marked, + }, + (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, + consensus::{BlockHeight, Network}, + memo::MemoBytes, + transaction::components::{amount::NonNegativeAmount, sapling::zip212_enforcement}, + }; + + 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().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; 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: NonNegativeAmount, + tx_after: bool, + initial_tree_sizes: Option<(u32, u32)>, + ) -> CompactBlock { + let zip212_enforcement = zip212_enforcement(&Network::TestNetwork, height); + let to = dfvk.default_address().1; + + // Create a fake Note for the account + let mut rng = OsRng; + let rseed = generate_random_rseed(zip212_enforcement, &mut rng); + let note = sapling::Note::from_parts(to, NoteValue::from_raw(value.into()), rseed); + let encryptor = sapling_note_encryption( + Some(dfvk.fvk().ovk), + note.clone(), + *MemoBytes::empty().as_array(), + &mut rng, + ); + let cmu = note.cmu().to_bytes().to_vec(); + let ephemeral_key = SaplingDomain::epk_bytes(encryptor.epk()).0.to_vec(); + let enc_ciphertext = encryptor.encrypt_note_plaintext(); + + // Create a fake CompactBlock containing the note + let mut cb = CompactBlock { + hash: { + let mut hash = vec![0; 32]; + rng.fill_bytes(&mut hash); + hash + }, + prev_hash: prev_hash.0.to_vec(), + height: height.into(), + ..Default::default() + }; + + // Add a random Sapling tx before ours + { + let mut tx = random_compact_tx(&mut rng); + tx.index = cb.vtx.len() as u64; + cb.vtx.push(tx); + } + + let cspend = CompactSaplingSpend { nf: nf.0.to_vec() }; + let cout = CompactSaplingOutput { + cmu, + ephemeral_key, + ciphertext: enc_ciphertext.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.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::{Position, Retention}; + use sapling::Nullifier; + use zcash_keys::keys::UnifiedSpendingKey; + use zcash_primitives::{ + block::BlockHash, + consensus::{BlockHeight, Network}, + transaction::components::amount::NonNegativeAmount, + 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, + NonNegativeAmount::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(), + is_marked: true + } + ] + ); + } + + 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, + NonNegativeAmount::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(), + is_marked: false + } + ] + ); + } + + 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(), + NonNegativeAmount::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(), + is_marked: false + } + ] + ); + } +} 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..b7b0f88e52 --- /dev/null +++ b/zcash_client_backend/src/sync.rs @@ -0,0 +1,491 @@ +//! Implementation of the synchronization flow described in the crate root. +//! +//! This is currently a simple implementation that does not yet implement a few features: +//! +//! - Block batches are not downloaded in parallel with scanning. +//! - Transactions are not enhanced once detected (that is, after an output is detected in +//! a transaction, the full transaction is not downloaded and scanned). +//! - There is no mechanism for notifying the caller of progress updates. +//! - There is no mechanism for interrupting the synchronization flow, other than ending +//! the process. + +use std::fmt; + +use futures_util::TryStreamExt; +use shardtree::error::ShardTreeError; +use subtle::ConditionallySelectable; +use tonic::{ + body::BoxBody, + client::GrpcService, + codegen::{Body, Bytes, StdError}, +}; +use tracing::{debug, info}; +use zcash_primitives::{ + consensus::{BlockHeight, Parameters}, + merkle_tree::HashSer, +}; + +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; + +/// 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?; + + // 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); + // Hack to work around a bug in the initial lightwalletd implementation. + request.max_entries = 65536; + + 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); + // Hack to work around a bug in the initial lightwalletd implementation. + request.max_entries = 65536; + 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()), + } +} + +/// 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/wallet.rs b/zcash_client_backend/src/wallet.rs index ba58340b3d..7d555b07f6 100644 --- a/zcash_client_backend/src/wallet.rs +++ b/zcash_client_backend/src/wallet.rs @@ -1,34 +1,193 @@ //! Structs representing transaction data scanned from the block chain by a wallet or //! light client. +use incrementalmerkletree::Position; +use zcash_address::ZcashAddress; use zcash_note_encryption::EphemeralKeyBytes; use zcash_primitives::{ consensus::BlockHeight, - keys::OutgoingViewingKey, legacy::TransparentAddress, - sapling, transaction::{ components::{ - sapling::fees as sapling_fees, - transparent::{self, OutPoint, TxOut}, - Amount, + amount::NonNegativeAmount, + transparent::{OutPoint, TxOut}, }, + fees::transparent as transparent_fees, TxId, }, - zip32::AccountId, + zip32::Scope, }; +use zcash_protocol::value::BalanceError; -/// A subset of a [`Transaction`] relevant to wallets and light clients. +use crate::{fees::sapling as sapling_fees, PoolType, ShieldedProtocol}; + +#[cfg(feature = "orchard")] +use crate::fees::orchard as orchard_fees; + +#[cfg(feature = "transparent-inputs")] +use zcash_primitives::legacy::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 + } + + /// 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 (and, for +/// unified addresses, the pool to which the payment is sent) in the case of an 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 { + External(ZcashAddress, PoolType), + InternalAccount { + receiving_account: AccountId, + external_address: Option, + note: N, + }, +} + +impl Recipient { + pub fn map_internal_account_note B>(self, f: F) -> Recipient { + match self { + Recipient::External(addr, pool) => Recipient::External(addr, pool), + Recipient::InternalAccount { + receiving_account, + external_address, + note, + } => Recipient::InternalAccount { + receiving_account, + external_address, + note: f(note), + }, + } + } +} + +impl Recipient> { + pub fn internal_account_note_transpose_option(self) -> Option> { + match self { + Recipient::External(addr, pool) => Some(Recipient::External(addr, pool)), + Recipient::InternalAccount { + receiving_account, + external_address, + note, + } => note.map(|n0| Recipient::InternalAccount { + receiving_account, + external_address, + note: n0, + }), + } + } +} + +/// 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, @@ -68,12 +227,12 @@ impl WalletTransparentOutput { &self.recipient_address } - pub fn value(&self) -> Amount { + pub fn value(&self) -> NonNegativeAmount { self.txout.value } } -impl transparent::fees::InputView for WalletTransparentOutput { +impl transparent_fees::InputView for WalletTransparentOutput { fn outpoint(&self) -> &OutPoint { &self.outpoint } @@ -82,116 +241,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) -> NonNegativeAmount { + 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) => NonNegativeAmount::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) -> NonNegativeAmount { + 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) -> NonNegativeAmount { + 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) -> NonNegativeAmount { + 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) -> NonNegativeAmount { + self.note + .value() + .inner() + .try_into() + .expect("Orchard note values are indirectly checked by consensus.") } } @@ -202,23 +539,67 @@ 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. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg(feature = "transparent-inputs")] +pub struct TransparentAddressMetadata { + scope: TransparentKeyScope, + address_index: NonHardenedChildIndex, +} + +#[cfg(feature = "transparent-inputs")] +impl TransparentAddressMetadata { + 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..817eb7e21e 100644 --- a/zcash_client_sqlite/CHANGELOG.md +++ b/zcash_client_sqlite/CHANGELOG.md @@ -6,13 +6,162 @@ and this library adheres to Rust's notion of [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +## [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..a70acb25b7 100644 --- a/zcash_client_sqlite/Cargo.toml +++ b/zcash_client_sqlite/Cargo.toml @@ -1,69 +1,134 @@ [package] name = "zcash_client_sqlite" description = "An SQLite-based Zcash light client" -version = "0.7.1" +version = "0.10.3" 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 = ["orchard", "sapling"] } +zcash_primitives.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.) # - Errors -bs58 = { version = "0.5", features = ["check"] } -hdwallet = { version = "0.4", optional = true } +bs58.workspace = true +hdwallet = { workspace = true, optional = true } # - Logging and metrics -tracing = "0.1" +tracing.workspace = true -# - Protobuf interfaces -prost = "0.11" +# - Serialization +byteorder.workspace = true +nonempty.workspace = true +prost.workspace = true +group.workspace = true +jubjub.workspace = true # - Secret management -secrecy = "0.8" +secrecy.workspace = true +subtle.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"] } +# 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.29.0", features = ["bundled", "time", "array"] } schemer = "0.2" -schemer-rusqlite = "0.2.1" -time = "0.2" +schemer-rusqlite = "0.2.2" +time = "0.3.22" uuid = "1.1" # Dependencies used internally: # (Breaking upgrades to these are usually backwards-compatible, but check MSRVs.) +document-features.workspace = true +maybe-rayon.workspace = true [dev-dependencies] -assert_matches = "1.5" -proptest = "1.0.0" -rand_core = "0.6" +assert_matches.workspace = true +bls12_381.workspace = true +incrementalmerkletree = { workspace = true, features = ["test-dependencies"] } +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 regex = "1.4" 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"] } +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"] } [features] -mainnet = [] +default = ["multicore"] + +## Enables multithreading support for creating proofs and building subtrees. +multicore = ["maybe-rayon/threads", "zcash_primitives/multicore"] + +## Enables support for storing data related to the sending and receiving of +## Orchard funds. +orchard = ["dep:orchard", "zcash_client_backend/orchard", "zcash_keys/orchard"] + +## Exposes APIs that are useful for testing, such as `proptest` strategies. test-dependencies = [ + "incrementalmerkletree/test-dependencies", "zcash_primitives/test-dependencies", "zcash_client_backend/test-dependencies", + "incrementalmerkletree/test-dependencies", +] + +## Enables receiving transparent funds and sending to transparent recipients +transparent-inputs = [ + "dep:hdwallet", + "zcash_keys/transparent-inputs", + "zcash_client_backend/transparent-inputs" ] -transparent-inputs = ["hdwallet", "zcash_client_backend/transparent-inputs"] + +#! ### 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 = [] + [lib] bench = false 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..028b2980f3 100644 --- a/zcash_client_sqlite/src/chain.rs +++ b/zcash_client_sqlite/src/chain.rs @@ -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,98 @@ 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 crate::{testing, wallet::sapling::tests::SaplingPoolTester}; + + #[cfg(feature = "orchard")] + use crate::wallet::orchard::tests::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::() } + // FIXME: This requires test framework fixes to pass. #[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::() + } - // 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] + #[cfg(feature = "orchard")] + fn data_db_truncation_orchard() { + testing::pool::data_db_truncation::() + } + + #[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/error.rs b/zcash_client_sqlite/src/error.rs index cfd326b9c5..2f961853c0 100644 --- a/zcash_client_sqlite/src/error.rs +++ b/zcash_client_sqlite/src/error.rs @@ -3,13 +3,21 @@ use std::error; use std::fmt; -use zcash_client_backend::encoding::{Bech32DecodeError, TransparentCodecError}; -use zcash_primitives::{consensus::BlockHeight, zip32::AccountId}; +use shardtree::error::ShardTreeError; +use zcash_address::ParseError; +use zcash_client_backend::PoolType; +use zcash_keys::keys::AddressGenerationError; +use zcash_primitives::zip32; +use zcash_primitives::{consensus::BlockHeight, transaction::components::amount::BalanceError}; -use crate::PRUNING_HEIGHT; +use crate::wallet::commitment_tree; +use crate::PRUNING_DEPTH; #[cfg(feature = "transparent-inputs")] -use zcash_primitives::legacy::TransparentAddress; +use { + zcash_client_backend::encoding::TransparentCodecError, + zcash_primitives::legacy::TransparentAddress, +}; /// The primary error type for the SQLite wallet backend. #[derive(Debug)] @@ -23,15 +31,11 @@ pub enum SqliteClientError { /// The rcm value for a note cannot be decoded to a valid JubJub point. InvalidNote, - /// The note id associated with a witness being stored corresponds to a - /// sent note, not a received note. - InvalidNoteId, - /// Illegal attempt to reinitialize an already-initialized wallet database. TableNotEmpty, - /// A Bech32-encoded key or address decoding error - Bech32DecodeError(Bech32DecodeError), + /// A Zcash key or address decoding error + DecodingError(ParseError), /// An error produced in legacy transparent address derivation #[cfg(feature = "transparent-inputs")] @@ -39,6 +43,7 @@ pub enum SqliteClientError { /// An error encountered in decoding a transparent address from its /// serialized form. + #[cfg(feature = "transparent-inputs")] TransparentAddress(TransparentCodecError), /// Wrapper for rusqlite errors. @@ -50,18 +55,31 @@ pub enum SqliteClientError { /// A received memo cannot be interpreted as a UTF-8 string. InvalidMemo(zcash_primitives::memo::Error), - /// A requested rewind would violate invariants of the - /// storage layer. The payload returned with this error is - /// (safe rewind height, requested height). + /// An attempt to update block data would overwrite the current hash for a block with a + /// different hash. This indicates that a required rewind was not performed. + BlockConflict(BlockHeight), + + /// A range of blocks provided to the database as a unit was non-sequential + NonSequentialBlocks, + + /// A requested rewind would violate invariants of the storage layer. The payload returned with + /// this error is (safe rewind height, requested height). RequestedRewindInvalid(BlockHeight, BlockHeight), - /// The space of allocatable diversifier indices has been exhausted for - /// the given account. - DiversifierIndexOutOfRange, + /// An error occurred in generating a Zcash address. + AddressGeneration(AddressGenerationError), + + /// The account for which information was requested does not belong to the wallet. + AccountUnknown, + + /// The account was imported, and ZIP-32 derivation information is not known for it. + UnknownZip32Derivation, - /// An error occurred deriving a spending key from a seed and an account - /// identifier. - KeyDerivationError(AccountId), + /// 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 initialize the accounts table with a discontinuous /// set of account identifiers. @@ -74,15 +92,36 @@ pub enum SqliteClientError { /// 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), } 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 +135,33 @@ 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), + 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_DEPTH, h, r), + SqliteClientError::DecodingError(e) => write!(f, "{}", e), #[cfg(feature = "transparent-inputs")] SqliteClientError::HdwalletError(e) => write!(f, "{:?}", e), + #[cfg(feature = "transparent-inputs")] SqliteClientError::TransparentAddress(e) => write!(f, "{}", e), SqliteClientError::TableNotEmpty => write!(f, "Table is not empty"), SqliteClientError::DbError(e) => write!(f, "{}", e), SqliteClientError::Io(e) => write!(f, "{}", e), SqliteClientError::InvalidMemo(e) => write!(f, "{}", e), - SqliteClientError::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::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(acct_id) => write!(f, "Key derivation failed for account {}", u32::from(*acct_id)), + SqliteClientError::BadAccountData(e) => write!(f, "Failed to add account: {}", e), SqliteClientError::AccountIdDiscontinuity => write!(f, "Wallet account identifiers must be sequential."), SqliteClientError::AccountIdOutOfRange => write!(f, "Wallet account identifiers must be less than 0x7FFFFFFF."), #[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), } } } @@ -129,10 +177,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) } } @@ -149,6 +196,7 @@ impl From for SqliteClientError { } } +#[cfg(feature = "transparent-inputs")] impl From for SqliteClientError { fn from(e: TransparentCodecError) -> Self { SqliteClientError::TransparentAddress(e) @@ -160,3 +208,21 @@ impl From for SqliteClientError { 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) + } +} diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index 60cc142ae0..733c35973a 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,69 @@ //! [`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::{Position, Retention}; +use maybe_rayon::{ + prelude::{IndexedParallelIterator, ParallelIterator}, + slice::ParallelSliceMut, +}; +use nonempty::NonEmpty; +use rusqlite::{self, Connection}; use secrecy::{ExposeSecret, SecretVec}; -use std::collections::HashMap; -use std::fmt; -use std::path::Path; +use shardtree::{error::ShardTreeError, ShardTree}; +use std::{ + borrow::Borrow, collections::HashMap, convert::AsRef, fmt, num::NonZeroU32, ops::Range, + path::Path, +}; +use subtle::ConditionallySelectable; +use tracing::{debug, trace, warn}; +use zcash_client_backend::{ + address::UnifiedAddress, + data_api::{ + self, + chain::{BlockSource, ChainState, CommitmentTreeRoot}, + scanning::{ScanPriority, ScanRange}, + Account, AccountBirthday, AccountSource, BlockMetadata, DecryptedTransaction, InputSource, + NullifierQuery, ScannedBlock, SeedRelevance, SentTransaction, SpendableNotes, + WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT, + }, + keys::{ + AddressGenerationError, UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey, + }, + proto::compact_formats::CompactBlock, + wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput}, + DecryptedOutput, PoolType, ShieldedProtocol, TransferType, +}; +use zcash_keys::address::Receiver; 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}, + transaction::{components::amount::NonNegativeAmount, Transaction, TxId}, + zip32::{self, DiversifierIndex, Scope}, }; +use zip32::fingerprint::SeedFingerprint; -use zcash_client_backend::{ - address::{AddressMetadata, UnifiedAddress}, - data_api::{ - self, chain::BlockSource, DecryptedTransaction, NullifierQuery, PoolType, PrunedBlock, - Recipient, SentTransaction, WalletRead, WalletWrite, - }, - keys::{UnifiedFullViewingKey, UnifiedSpendingKey}, - proto::compact_formats::CompactBlock, - wallet::{ReceivedSaplingNote, WalletTransparentOutput}, - DecryptedOutput, TransferType, +use crate::{error::SqliteClientError, wallet::commitment_tree::SqliteShardStore}; + +#[cfg(feature = "orchard")] +use { + incrementalmerkletree::frontier::Frontier, + shardtree::store::{Checkpoint, ShardStore}, + std::collections::BTreeMap, + zcash_client_backend::data_api::ORCHARD_SHARD_HEIGHT, }; -use crate::error::SqliteClientError; +#[cfg(feature = "transparent-inputs")] +use { + zcash_client_backend::wallet::TransparentAddressMetadata, + zcash_primitives::{legacy::TransparentAddress, transaction::components::OutPoint}, +}; #[cfg(feature = "unstable")] use { @@ -72,31 +98,63 @@ use { std::{fs, io}, }; -mod prepared; -pub use prepared::DataConnStmtCache; - pub mod chain; pub mod error; pub mod wallet; +use wallet::{ + commitment_tree::{self, put_shard_roots}, + SubtreeScanProgress, +}; + +#[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: bool = false; +#[cfg(feature = "orchard")] +pub(crate) const UA_ORCHARD: bool = true; + +#[cfg(not(feature = "transparent-inputs"))] +pub(crate) const UA_TRANSPARENT: bool = false; +#[cfg(feature = "transparent-inputs")] +pub(crate) const UA_TRANSPARENT: bool = true; + +pub(crate) const DEFAULT_UA_REQUEST: UnifiedAddressRequest = + UnifiedAddressRequest::unsafe_new(UA_ORCHARD, true, UA_TRANSPARENT); + +/// The ID type for accounts. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)] +pub struct AccountId(u32); + +impl ConditionallySelectable for AccountId { + fn conditional_select(a: &Self, b: &Self, choice: subtle::Choice) -> Self { + AccountId(ConditionallySelectable::conditional_select( + &a.0, &b.0, choice, + )) + } } -impl fmt::Display for NoteId { +/// An opaque type for received note identifiers. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct ReceivedNoteId(pub(crate) ShieldedProtocol, pub(crate) i64); + +impl fmt::Display for ReceivedNoteId { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - NoteId::SentNoteId(id) => write!(f, "Sent Note {}", id), - NoteId::ReceivedNoteId(id) => write!(f, "Received Note {}", id), + ReceivedNoteId(protocol, id) => write!(f, "Received {:?} Note: {}", protocol, id), } } } @@ -107,12 +165,21 @@ impl fmt::Display for NoteId { pub struct UtxoId(pub i64); /// A wrapper for the SQLite connection to the wallet database. -pub struct WalletDb

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

{ +/// A wrapper for a SQLite transaction affecting the wallet database. +pub struct SqlTransaction<'conn>(pub(crate) &'conn rusqlite::Transaction<'conn>); + +impl Borrow for SqlTransaction<'_> { + fn borrow(&self) -> &rusqlite::Connection { + self.0 + } +} + +impl WalletDb { /// Construct a connection to the wallet database stored at the specified path. pub fn for_path>(path: F, params: P) -> Result { Connection::open(path).and_then(move |conn| { @@ -121,542 +188,1122 @@ impl WalletDb

{ }) } - /// 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) + pub fn transactionally>(&mut self, f: F) -> Result + where + F: FnOnce(&mut WalletDb, P>) -> Result, + { + let tx = self.conn.transaction()?; + let mut wdb = WalletDb { + conn: SqlTransaction(&tx), + params: self.params.clone(), + }; + let result = f(&mut wdb)?; + tx.commit()?; + Ok(result) } } -impl WalletRead for WalletDb

{ +impl, P: consensus::Parameters> InputSource for WalletDb { type Error = SqliteClientError; - type NoteRef = NoteId; - type TxRef = i64; - - fn block_height_extrema(&self) -> Result, Self::Error> { - wallet::block_height_extrema(self).map_err(SqliteClientError::from) - } + type NoteRef = ReceivedNoteId; + type AccountId = AccountId; - fn get_min_unspent_height(&self) -> Result, Self::Error> { - wallet::get_min_unspent_height(self).map_err(SqliteClientError::from) - } - - fn get_block_hash(&self, block_height: BlockHeight) -> Result, Self::Error> { - wallet::get_block_hash(self, block_height).map_err(SqliteClientError::from) - } - - fn get_tx_height(&self, txid: TxId) -> Result, Self::Error> { - wallet::get_tx_height(self, txid).map_err(SqliteClientError::from) - } - - fn get_unified_full_viewing_keys( + 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))); - fn get_account_for_ufvk( - &self, - ufvk: &UnifiedFullViewingKey, - ) -> Result, Self::Error> { - wallet::get_account_for_ufvk(self, ufvk) + #[cfg(not(feature = "orchard"))] + return Err(SqliteClientError::UnsupportedPoolType(PoolType::Shielded( + ShieldedProtocol::Orchard, + ))); + } + } } - fn get_current_address( + fn select_spendable_notes( &self, account: AccountId, - ) -> Result, Self::Error> { - wallet::get_current_address(self, account).map(|res| res.map(|(addr, _)| addr)) + target_value: NonNegativeAmount, + _sources: &[ShieldedProtocol], + anchor_height: BlockHeight, + exclude: &[Self::NoteRef], + ) -> Result, Self::Error> { + Ok(SpendableNotes::new( + wallet::sapling::select_spendable_sapling_notes( + self.conn.borrow(), + &self.params, + account, + target_value, + anchor_height, + exclude, + )?, + #[cfg(feature = "orchard")] + wallet::orchard::select_spendable_orchard_notes( + self.conn.borrow(), + &self.params, + account, + target_value, + anchor_height, + exclude, + )?, + )) } - fn is_valid_account_extfvk( + #[cfg(feature = "transparent-inputs")] + fn get_unspent_transparent_output( &self, - account: AccountId, - extfvk: &ExtendedFullViewingKey, - ) -> Result { - wallet::is_valid_account_extfvk(self, account, extfvk) + outpoint: &OutPoint, + ) -> Result, Self::Error> { + wallet::get_unspent_transparent_output(self.conn.borrow(), outpoint) } - fn get_balance_at( + #[cfg(feature = "transparent-inputs")] + fn get_unspent_transparent_outputs( &self, - account: AccountId, - anchor_height: BlockHeight, - ) -> Result { - wallet::get_balance_at(self, account, anchor_height) + address: &TransparentAddress, + max_height: BlockHeight, + exclude: &[OutPoint], + ) -> Result, Self::Error> { + wallet::get_unspent_transparent_outputs( + self.conn.borrow(), + &self.params, + address, + max_height, + exclude, + ) } +} - fn get_transaction(&self, id_tx: i64) -> Result { - wallet::get_transaction(self, id_tx) - } +impl, P: consensus::Parameters> WalletRead for WalletDb { + type Error = SqliteClientError; + type AccountId = AccountId; + type Account = wallet::Account; - 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_ids(&self) -> Result, Self::Error> { + wallet::get_account_ids(self.conn.borrow()) } - fn get_commitment_tree( + fn get_account( &self, - block_height: BlockHeight, - ) -> Result, Self::Error> { - wallet::sapling::get_sapling_commitment_tree(self, block_height) + account_id: Self::AccountId, + ) -> Result, Self::Error> { + wallet::get_account(self.conn.borrow(), &self.params, account_id) } - #[allow(clippy::type_complexity)] - fn get_witnesses( + fn get_derived_account( &self, - block_height: BlockHeight, - ) -> Result, Self::Error> { - wallet::sapling::get_sapling_witnesses(self, block_height) + seed: &SeedFingerprint, + account_id: zip32::AccountId, + ) -> Result, Self::Error> { + wallet::get_derived_account(self.conn.borrow(), &self.params, seed, account_id) } - fn get_sapling_nullifiers( + fn validate_seed( &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), + account_id: Self::AccountId, + seed: &SecretVec, + ) -> Result { + if let Some(account) = self.get_account(account_id)? { + if let AccountSource::Derived { + seed_fingerprint, + account_index, + } = account.source() + { + wallet::seed_matches_derived_account( + &self.params, + seed, + &seed_fingerprint, + account_index, + &account.uivk(), + ) + } else { + Err(SqliteClientError::UnknownZip32Derivation) + } + } else { + // Missing account is documented to return false. + Ok(false) } } - fn get_spendable_sapling_notes( + fn seed_relevance_to_derived_accounts( &self, - account: AccountId, - anchor_height: BlockHeight, - exclude: &[Self::NoteRef], - ) -> Result>, Self::Error> { - wallet::sapling::get_spendable_sapling_notes(self, account, anchor_height, exclude) - } + 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 { + seed_fingerprint, + account_index, + } = account.source() + { + has_derived = true; + + if wallet::seed_matches_derived_account( + &self.params, + seed, + &seed_fingerprint, + account_index, + &account.uivk(), + )? { + // The seed is relevant to this account. + relevant_account_ids.push(account_id); + } + } + } - fn select_spendable_sapling_notes( - &self, - account: AccountId, - target_value: Amount, - anchor_height: BlockHeight, - exclude: &[Self::NoteRef], - ) -> Result>, Self::Error> { - wallet::sapling::select_spendable_sapling_notes( - self, - account, - target_value, - anchor_height, - exclude, + 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_transparent_receivers( + fn get_account_for_ufvk( &self, - _account: AccountId, - ) -> Result, Self::Error> { - #[cfg(feature = "transparent-inputs")] - return wallet::get_transparent_receivers(&self.params, &self.conn, _account); - - #[cfg(not(feature = "transparent-inputs"))] - panic!( - "The wallet must be compiled with the transparent-inputs feature to use this method." - ); + ufvk: &UnifiedFullViewingKey, + ) -> Result, Self::Error> { + wallet::get_account_for_ufvk(self.conn.borrow(), &self.params, ufvk) } - fn get_unspent_transparent_outputs( + fn get_current_address( &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); - - #[cfg(not(feature = "transparent-inputs"))] - panic!( - "The wallet must be compiled with the transparent-inputs feature to use this method." - ); + account: AccountId, + ) -> Result, Self::Error> { + wallet::get_current_address(self.conn.borrow(), &self.params, account) + .map(|res| res.map(|(addr, _)| addr)) } - 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); - - #[cfg(not(feature = "transparent-inputs"))] - panic!( - "The wallet must be compiled with the transparent-inputs feature to use this method." - ); + fn get_account_birthday(&self, account: AccountId) -> Result { + wallet::account_birthday(self.conn.borrow(), account).map_err(SqliteClientError::from) } -} -impl<'a, P: consensus::Parameters> WalletRead for DataConnStmtCache<'a, P> { - type Error = SqliteClientError; - type NoteRef = NoteId; - type TxRef = i64; + fn get_wallet_birthday(&self) -> Result, Self::Error> { + wallet::wallet_birthday(self.conn.borrow()).map_err(SqliteClientError::from) + } - fn block_height_extrema(&self) -> Result, Self::Error> { - self.wallet_db.block_height_extrema() + fn get_wallet_summary( + &self, + min_confirmations: u32, + ) -> Result>, Self::Error> { + // This will return a runtime error if we call `get_wallet_summary` from two + // threads at the same time, as transactions cannot nest. + wallet::get_wallet_summary( + &self.conn.borrow().unchecked_transaction()?, + &self.params, + min_confirmations, + &SubtreeScanProgress, + ) } - fn get_min_unspent_height(&self) -> Result, Self::Error> { - self.wallet_db.get_min_unspent_height() + fn chain_height(&self) -> Result, Self::Error> { + wallet::scan_queue_extrema(self.conn.borrow()) + .map(|h| h.map(|range| *range.end())) + .map_err(SqliteClientError::from) } fn get_block_hash(&self, block_height: BlockHeight) -> Result, Self::Error> { - self.wallet_db.get_block_hash(block_height) + wallet::get_block_hash(self.conn.borrow(), block_height).map_err(SqliteClientError::from) } - fn get_tx_height(&self, txid: TxId) -> Result, Self::Error> { - self.wallet_db.get_tx_height(txid) + fn block_metadata(&self, height: BlockHeight) -> Result, Self::Error> { + wallet::block_metadata(self.conn.borrow(), &self.params, height) } - fn get_unified_full_viewing_keys( - &self, - ) -> Result, Self::Error> { - self.wallet_db.get_unified_full_viewing_keys() + fn block_fully_scanned(&self) -> Result, Self::Error> { + wallet::block_fully_scanned(self.conn.borrow(), &self.params) } - fn get_account_for_ufvk( - &self, - ufvk: &UnifiedFullViewingKey, - ) -> Result, Self::Error> { - self.wallet_db.get_account_for_ufvk(ufvk) + fn get_max_height_hash(&self) -> Result, Self::Error> { + wallet::get_max_height_hash(self.conn.borrow()).map_err(SqliteClientError::from) } - fn get_current_address( - &self, - account: AccountId, - ) -> Result, Self::Error> { - self.wallet_db.get_current_address(account) + fn block_max_scanned(&self) -> Result, Self::Error> { + wallet::block_max_scanned(self.conn.borrow(), &self.params) } - fn is_valid_account_extfvk( - &self, - account: AccountId, - extfvk: &ExtendedFullViewingKey, - ) -> Result { - self.wallet_db.is_valid_account_extfvk(account, extfvk) + fn suggest_scan_ranges(&self) -> Result, Self::Error> { + wallet::scanning::suggest_scan_ranges(self.conn.borrow(), ScanPriority::Historic) + .map_err(SqliteClientError::from) } - fn get_balance_at( + fn get_target_and_anchor_heights( &self, - account: AccountId, - anchor_height: BlockHeight, - ) -> Result { - self.wallet_db.get_balance_at(account, anchor_height) + min_confirmations: NonZeroU32, + ) -> Result, Self::Error> { + wallet::get_target_and_anchor_heights(self.conn.borrow(), min_confirmations) + .map_err(SqliteClientError::from) } - fn get_transaction(&self, id_tx: i64) -> Result { - self.wallet_db.get_transaction(id_tx) + fn get_min_unspent_height(&self) -> Result, Self::Error> { + wallet::get_min_unspent_height(self.conn.borrow()).map_err(SqliteClientError::from) } - fn get_memo(&self, id_note: Self::NoteRef) -> Result, Self::Error> { - self.wallet_db.get_memo(id_note) + fn get_tx_height(&self, txid: TxId) -> Result, Self::Error> { + wallet::get_tx_height(self.conn.borrow(), txid).map_err(SqliteClientError::from) } - fn get_commitment_tree( + fn get_unified_full_viewing_keys( &self, - block_height: BlockHeight, - ) -> Result, Self::Error> { - self.wallet_db.get_commitment_tree(block_height) + ) -> Result, Self::Error> { + wallet::get_unified_full_viewing_keys(self.conn.borrow(), &self.params) } - #[allow(clippy::type_complexity)] - fn get_witnesses( - &self, - block_height: BlockHeight, - ) -> Result, Self::Error> { - self.wallet_db.get_witnesses(block_height) + 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_sapling_nullifiers( - &self, - query: data_api::NullifierQuery, - ) -> Result, Self::Error> { - self.wallet_db.get_sapling_nullifiers(query) + 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_spendable_sapling_notes( + fn get_sapling_nullifiers( &self, - account: AccountId, - anchor_height: BlockHeight, - exclude: &[Self::NoteRef], - ) -> Result>, Self::Error> { - self.wallet_db - .get_spendable_sapling_notes(account, anchor_height, exclude) + query: NullifierQuery, + ) -> Result, Self::Error> { + wallet::sapling::get_sapling_nullifiers(self.conn.borrow(), query) } - fn select_spendable_sapling_notes( + #[cfg(feature = "orchard")] + fn get_orchard_nullifiers( &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) + query: NullifierQuery, + ) -> Result, Self::Error> { + wallet::orchard::get_orchard_nullifiers(self.conn.borrow(), query) } + #[cfg(feature = "transparent-inputs")] fn get_transparent_receivers( &self, account: AccountId, - ) -> Result, Self::Error> { - self.wallet_db.get_transparent_receivers(account) - } - - fn get_unspent_transparent_outputs( - &self, - address: &TransparentAddress, - max_height: BlockHeight, - exclude: &[OutPoint], - ) -> Result, Self::Error> { - self.wallet_db - .get_unspent_transparent_outputs(address, max_height, exclude) + ) -> Result>, Self::Error> { + wallet::get_transparent_receivers(self.conn.borrow(), &self.params, account) } + #[cfg(feature = "transparent-inputs")] fn get_transparent_balances( &self, account: AccountId, max_height: BlockHeight, - ) -> Result, Self::Error> { - self.wallet_db.get_transparent_balances(account, max_height) - } -} - -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) - } - 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 - ) - } - } - } + ) -> Result, Self::Error> { + wallet::get_transparent_balances(self.conn.borrow(), &self.params, account, max_height) } } -impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> { +impl WalletWrite for WalletDb { type UtxoRef = UtxoId; fn create_account( &mut self, seed: &SecretVec, + birthday: &AccountBirthday, ) -> 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); - } - - let usk = UnifiedSpendingKey::from_seed( - &stmts.wallet_db.params, - seed.expose_secret(), - account, - ) - .map_err(|_| SqliteClientError::KeyDerivationError(account))?; + 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 account_index = wallet::max_zip32_account_index(wdb.conn.0, &seed_fingerprint)? + .map(|a| a.next().ok_or(SqliteClientError::AccountIdOutOfRange)) + .transpose()? + .unwrap_or(zip32::AccountId::ZERO); + + 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(); - wallet::add_account(stmts.wallet_db, account, &ufvk)?; + let account_id = wallet::add_account( + wdb.conn.0, + &wdb.params, + AccountSource::Derived { + seed_fingerprint, + account_index, + }, + wallet::ViewingKey::Full(Box::new(ufvk)), + birthday, + )?; - Ok((account, usk)) + Ok((account_id, usk)) }) } fn get_next_available_address( &mut self, account: AccountId, + request: UnifiedAddressRequest, ) -> 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(), - }; + self.transactionally( + |wdb| match wdb.get_unified_full_viewing_keys()?.get(&account) { + Some(ufvk) => { + let search_from = + match wallet::get_current_address(wdb.conn.0, &wdb.params, account)? { + Some((_, mut last_diversifier_index)) => { + last_diversifier_index.increment().map_err(|_| { + AddressGenerationError::DiversifierSpaceExhausted + })?; + last_diversifier_index + } + None => DiversifierIndex::default(), + }; - let (addr, diversifier_index) = ufvk - .find_address(search_from) - .ok_or(SqliteClientError::DiversifierIndexOutOfRange)?; + let (addr, diversifier_index) = ufvk.find_address(search_from, request)?; - self.stmt_insert_address(account, diversifier_index, &addr)?; + wallet::insert_address( + wdb.conn.0, + &wdb.params, + account, + diversifier_index, + &addr, + )?; - Ok(Some(addr)) - } - None => Ok(None), - } + Ok(Some(addr)) + } + None => Ok(None), + }, + ) + } + + fn update_chain_tip(&mut self, tip_height: BlockHeight) -> Result<(), Self::Error> { + let tx = self.conn.transaction()?; + wallet::scanning::update_chain_tip(&tx, &self.params, tip_height)?; + tx.commit()?; + Ok(()) } - #[tracing::instrument(skip_all, fields(height = u32::from(block.block_height)))] + #[tracing::instrument(skip_all, fields(height = blocks.first().map(|b| u32::from(b.height())), count = blocks.len()))] #[allow(clippy::type_complexity)] - fn advance_by_block( + fn put_blocks( &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)?; + from_state: &ChainState, + blocks: Vec>, + ) -> Result<(), Self::Error> { + struct BlockPositions { + height: BlockHeight, + sapling_start_position: Position, + #[cfg(feature = "orchard")] + orchard_start_position: Position, + } - // 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())?; + self.transactionally(|wdb| { + let start_positions = blocks.first().map(|block| BlockPositions { + height: block.height(), + sapling_start_position: Position::from( + u64::from(block.sapling().final_tree_size()) + - u64::try_from(block.sapling().commitments().len()).unwrap(), + ), + #[cfg(feature = "orchard")] + orchard_start_position: Position::from( + u64::from(block.orchard().final_tree_size()) + - u64::try_from(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![]; + for block in blocks.into_iter() { + if last_scanned_height + .iter() + .any(|prev| block.height() != *prev + 1) + { + return Err(SqliteClientError::NonSequentialBlocks); } - for output in &tx.sapling_outputs { - let received_note_id = wallet::sapling::put_received_note(up, output, tx_row)?; + // Insert the block into the database. + wallet::put_block( + wdb.conn.0, + block.height(), + block.block_hash(), + block.block_time(), + block.sapling().final_tree_size(), + block.sapling().commitments().len().try_into().unwrap(), + #[cfg(feature = "orchard")] + block.orchard().final_tree_size(), + #[cfg(feature = "orchard")] + block.orchard().commitments().len().try_into().unwrap(), + )?; + + for tx in block.transactions() { + let tx_row = wallet::put_tx_meta(wdb.conn.0, tx, block.height())?; + + // Mark notes as spent and remove them from the scanning cache + for spend in tx.sapling_spends() { + wallet::sapling::mark_sapling_note_spent(wdb.conn.0, tx_row, spend.nf())?; + } + #[cfg(feature = "orchard")] + for spend in tx.orchard_spends() { + wallet::orchard::mark_orchard_note_spent(wdb.conn.0, tx_row, spend.nf())?; + } - // Save witness for note. - new_witnesses.push((received_note_id, output.witness().clone())); + 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::<_, Scope>( + wdb.conn.0, + ShieldedProtocol::Sapling, + nf, + ) + }) + .transpose()? + .flatten(); + + wallet::sapling::put_received_note(wdb.conn.0, output, tx_row, spent_in)?; + } + #[cfg(feature = "orchard")] + for output in tx.orchard_outputs() { + // Check whether this note was spent in a later block range that + // we previously scanned. + let spent_in = output + .nf() + .map(|nf| { + wallet::query_nullifier_map::<_, Scope>( + wdb.conn.0, + ShieldedProtocol::Orchard, + &nf.to_bytes(), + ) + }) + .transpose()? + .flatten(); + + wallet::orchard::put_received_note(wdb.conn.0, output, tx_row, spent_in)?; + } } + + // Insert the new nullifiers from this block into the nullifier map. + wallet::insert_nullifier_map( + wdb.conn.0, + block.height(), + ShieldedProtocol::Sapling, + block.sapling().nullifier_map(), + )?; + #[cfg(feature = "orchard")] + wallet::insert_nullifier_map( + wdb.conn.0, + block.height(), + ShieldedProtocol::Orchard, + &block + .orchard() + .nullifier_map() + .iter() + .map(|(txid, idx, nfs)| { + (*txid, *idx, nfs.iter().map(|nf| nf.to_bytes()).collect()) + }) + .collect::>(), + )?; + + note_positions.extend(block.transactions().iter().flat_map(|wtx| { + let iter = wtx.sapling_outputs().iter().map(|out| { + ( + ShieldedProtocol::Sapling, + out.note_commitment_tree_position(), + ) + }); + #[cfg(feature = "orchard")] + let iter = iter.chain(wtx.orchard_outputs().iter().map(|out| { + ( + ShieldedProtocol::Orchard, + out.note_commitment_tree_position(), + ) + })); + + iter + })); + + last_scanned_height = Some(block.height()); + let block_commitments = block.into_commitments(); + trace!( + "Sapling commitments for {:?}: {:?}", + last_scanned_height, + block_commitments + .sapling + .iter() + .map(|(_, r)| *r) + .collect::>() + ); + #[cfg(feature = "orchard")] + trace!( + "Orchard commitments for {:?}: {:?}", + last_scanned_height, + block_commitments + .orchard + .iter() + .map(|(_, r)| *r) + .collect::>() + ); + + sapling_commitments.extend(block_commitments.sapling.into_iter().map(Some)); + #[cfg(feature = "orchard")] + orchard_commitments.extend(block_commitments.orchard.into_iter().map(Some)); + } + + // Prune the 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), + )?; } - // Insert current new_witnesses into the database. - for (received_note_id, witness) in updated_witnesses.iter().chain(new_witnesses.iter()) + // We will have a start position and a last scanned height in all cases where + // `blocks` is non-empty. + if let Some((start_positions, last_scanned_height)) = + start_positions.zip(last_scanned_height) { - if let NoteId::ReceivedNoteId(rnid) = *received_note_id { - wallet::sapling::insert_witness(up, rnid, witness, block.block_height)?; - } else { - return Err(SqliteClientError::InvalidNoteId); + // 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::>() } - } - // Prune the stored witnesses (we only expect rollbacks of at most PRUNING_HEIGHT blocks). - wallet::prune_witnesses(up, block.block_height - PRUNING_HEIGHT)?; + #[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() + ); + sapling_tree.insert_frontier( + from_state.final_sapling_tree().clone(), + Retention::Checkpoint { + id: from_state.block_height(), + is_marked: false, + }, + )?; + + for (tree, checkpoints) in &mut sapling_subtrees_iter { + sapling_tree.insert_tree(tree, checkpoints)?; + } - // Update now-expired transactions that didn't get mined. - wallet::update_expired_notes(up, block.block_height)?; + // 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() + ); + orchard_tree.insert_frontier( + from_state.final_orchard_tree().clone(), + Retention::Checkpoint { + id: from_state.block_height(), + is_marked: false, + }, + )?; + + for (tree, checkpoints) in &mut orchard_subtrees { + orchard_tree.insert_tree(tree, checkpoints)?; + } - Ok(new_witnesses) + // 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 wallet::put_received_transparent_utxo(&self.conn, &self.params, _output); + + #[cfg(not(feature = "transparent-inputs"))] + panic!( + "The wallet must be compiled with the transparent-inputs feature to use this method." + ); + } + fn store_decrypted_tx( &mut self, - d_tx: DecryptedTransaction, - ) -> Result { - self.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) + d_tx: DecryptedTransaction, + ) -> Result<(), Self::Error> { + self.transactionally(|wdb| { + let tx_ref = wallet::put_tx_data(wdb.conn.0, d_tx.tx(), None, None)?; + let funding_accounts = wallet::get_funding_accounts(wdb.conn.0, d_tx.tx())?; + 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() + ) + } + + for output in d_tx.sapling_outputs() { + match output.transfer_type() { + TransferType::Outgoing => { + let recipient = { + let receiver = Receiver::Sapling(output.note().recipient()); + let wallet_address = wallet::select_receiving_address( + &wdb.params, + wdb.conn.0, + *output.account(), + &receiver + )?.unwrap_or_else(|| + receiver.to_zcash_address(wdb.params.network_type()) + ); + + Recipient::External(wallet_address, PoolType::Shielded(ShieldedProtocol::Sapling)) }; wallet::put_sent_output( - up, - output.account, + wdb.conn.0, + *output.account(), tx_ref, - output.index, + 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), + output.note_value(), + Some(output.memo()), )?; + } + TransferType::WalletInternal => { + wallet::sapling::put_received_note(wdb.conn.0, output, tx_ref, None)?; - if matches!(recipient, Recipient::InternalAccount(_, _)) { - wallet::sapling::put_received_note(up, output, tx_ref)?; - } + let recipient = Recipient::InternalAccount { + receiving_account: *output.account(), + external_address: None, + note: Note::Sapling(output.note().clone()), + }; + + wallet::put_sent_output( + wdb.conn.0, + *output.account(), + tx_ref, + output.index(), + &recipient, + output.note_value(), + Some(output.memo()), + )?; } 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); - } + wallet::sapling::put_received_note(wdb.conn.0, output, tx_ref, None)?; + + if let Some(account_id) = funding_account { + let recipient = Recipient::InternalAccount { + receiving_account: *output.account(), + external_address: { + let receiver = Receiver::Sapling(output.note().recipient()); + Some(wallet::select_receiving_address( + &wdb.params, + wdb.conn.0, + *output.account(), + &receiver + )?.unwrap_or_else(|| + receiver.to_zcash_address(wdb.params.network_type()) + )) + }, + note: Note::Sapling(output.note().clone()), + }; + + wallet::put_sent_output( + wdb.conn.0, + account_id, + tx_ref, + output.index(), + &recipient, + output.note_value(), + Some(output.memo()), + )?; } + } + } + } + + #[cfg(feature = "orchard")] + for output in d_tx.orchard_outputs() { + match output.transfer_type() { + TransferType::Outgoing => { + let recipient = { + let receiver = Receiver::Orchard(output.note().recipient()); + let wallet_address = wallet::select_receiving_address( + &wdb.params, + wdb.conn.0, + *output.account(), + &receiver + )?.unwrap_or_else(|| + receiver.to_zcash_address(wdb.params.network_type()) + ); + + Recipient::External(wallet_address, PoolType::Shielded(ShieldedProtocol::Orchard)) + }; + + wallet::put_sent_output( + wdb.conn.0, + *output.account(), + tx_ref, + output.index(), + &recipient, + output.note_value(), + Some(output.memo()), + )?; + } + TransferType::WalletInternal => { + wallet::orchard::put_received_note(wdb.conn.0, output, tx_ref, None)?; + + let recipient = Recipient::InternalAccount { + receiving_account: *output.account(), + external_address: None, + note: Note::Orchard(*output.note()), + }; - wallet::sapling::put_received_note(up, output, tx_ref)?; + wallet::put_sent_output( + wdb.conn.0, + *output.account(), + tx_ref, + output.index(), + &recipient, + output.note_value(), + Some(output.memo()), + )?; + } + TransferType::Incoming => { + wallet::orchard::put_received_note(wdb.conn.0, output, tx_ref, None)?; + + if let Some(account_id) = funding_account { + // Even if the recipient address is external, record the send as internal. + let recipient = Recipient::InternalAccount { + receiving_account: *output.account(), + external_address: { + let receiver = Receiver::Orchard(output.note().recipient()); + Some(wallet::select_receiving_address( + &wdb.params, + wdb.conn.0, + *output.account(), + &receiver + )?.unwrap_or_else(|| + receiver.to_zcash_address(wdb.params.network_type()) + )) + }, + note: Note::Orchard(*output.note()), + }; + + wallet::put_sent_output( + wdb.conn.0, + 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()) { - wallet::mark_transparent_utxo_spent(up, tx_ref, &txin.prevout)?; + for txin in d_tx + .tx() + .transparent_bundle() + .iter() + .flat_map(|b| b.vin.iter()) + { + wallet::mark_transparent_utxo_spent(wdb.conn.0, tx_ref, &txin.prevout)?; } // 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 + if d_tx + .tx() + .transparent_bundle() + .iter() + .any(|b| !b.vout.is_empty()) + { + // If the transaction contains 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() { + let funding_accounts = wallet::get_funding_accounts(wdb.conn.0, d_tx.tx())?; + let funding_account = funding_accounts.iter().next().copied(); + if let Some(account_id) = funding_account { + if funding_accounts.len() > 1 { + warn!( + "More than one wallet account detected as funding transaction {:?}, selecting {:?}", + d_tx.tx().txid(), + account_id + ) + } + + 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() { + let receiver = Receiver::Transparent(address); + + #[cfg(feature = "transparent-inputs")] + let recipient_addr = wallet::select_receiving_address( + &wdb.params, + wdb.conn.0, + account_id, + &receiver + )?.unwrap_or_else(|| + receiver.to_zcash_address(wdb.params.network_type()) + ); + + #[cfg(not(feature = "transparent-inputs"))] + let recipient_addr = receiver.to_zcash_address(wdb.params.network_type()); + + let recipient = Recipient::External(recipient_addr, PoolType::Transparent); + wallet::put_sent_output( - up, - *account_id, + wdb.conn.0, + account_id, tx_ref, output_index, - &Recipient::Transparent(address), + &recipient, txout.value, - None + None, )?; } } } } - Ok(tx_ref) + + Ok(()) }) } - fn store_sent_tx(&mut self, sent_tx: &SentTransaction) -> Result { - // Update the database atomically, to ensure the result is internally consistent. - self.transactionally(|up| { + fn store_sent_tx(&mut self, sent_tx: &SentTransaction) -> Result<(), Self::Error> { + self.transactionally(|wdb| { let tx_ref = wallet::put_tx_data( - up, - sent_tx.tx, - Some(sent_tx.fee_amount), - Some(sent_tx.created), + wdb.conn.0, + sent_tx.tx(), + Some(sent_tx.fee_amount()), + Some(sent_tx.created()), )?; // Mark notes as spent. @@ -667,57 +1314,283 @@ impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> { // // 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() { + 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())?; + wallet::sapling::mark_sapling_note_spent( + wdb.conn.0, + tx_ref, + spend.nullifier(), + )?; } } + if let Some(_bundle) = sent_tx.tx().orchard_bundle() { + #[cfg(feature = "orchard")] + for action in _bundle.actions() { + wallet::orchard::mark_orchard_note_spent( + wdb.conn.0, + tx_ref, + action.nullifier(), + )?; + } + + #[cfg(not(feature = "orchard"))] + panic!("Sent a transaction with Orchard Actions without `orchard` enabled?"); + } #[cfg(feature = "transparent-inputs")] - for utxo_outpoint in &sent_tx.utxos_spent { - wallet::mark_transparent_utxo_spent(up, tx_ref, utxo_outpoint)?; + for utxo_outpoint in sent_tx.utxos_spent() { + wallet::mark_transparent_utxo_spent(wdb.conn.0, tx_ref, utxo_outpoint)?; } - 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, - )?; + for output in sent_tx.outputs() { + wallet::insert_sent_output(wdb.conn.0, tx_ref, *sent_tx.account_id(), output)?; + + match output.recipient() { + Recipient::InternalAccount { + receiving_account, + note: Note::Sapling(note), + .. + } => { + wallet::sapling::put_received_note( + wdb.conn.0, + &DecryptedOutput::new( + output.output_index(), + note.clone(), + *receiving_account, + output + .memo() + .map_or_else(MemoBytes::empty, |memo| memo.clone()), + TransferType::WalletInternal, + ), + tx_ref, + None, + )?; + } + #[cfg(feature = "orchard")] + Recipient::InternalAccount { + receiving_account, + note: Note::Orchard(note), + .. + } => { + wallet::orchard::put_received_note( + wdb.conn.0, + &DecryptedOutput::new( + output.output_index(), + *note, + *receiving_account, + output + .memo() + .map_or_else(MemoBytes::empty, |memo| memo.clone()), + TransferType::WalletInternal, + ), + tx_ref, + None, + )?; + } + _ => (), } } - // Return the row number of the transaction, so the caller can fetch it for sending. - Ok(tx_ref) + Ok(()) }) } fn truncate_to_height(&mut self, block_height: BlockHeight) -> Result<(), Self::Error> { - wallet::truncate_to_height(self.wallet_db, block_height) + self.transactionally(|wdb| { + wallet::truncate_to_height(wdb.conn.0, &wdb.params, block_height) + }) } +} - fn put_received_transparent_utxo( +impl WalletCommitmentTrees for WalletDb { + type Error = commitment_tree::Error; + type SaplingShardStore<'a> = + SqliteShardStore<&'a rusqlite::Transaction<'a>, sapling::Node, SAPLING_SHARD_HEIGHT>; + + fn with_sapling_tree_mut(&mut self, mut callback: F) -> Result + where + for<'a> F: FnMut( + &'a mut ShardTree< + Self::SaplingShardStore<'a>, + { sapling::NOTE_COMMITMENT_TREE_DEPTH }, + SAPLING_SHARD_HEIGHT, + >, + ) -> Result, + E: From>, + { + let tx = self + .conn + .transaction() + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?; + let shard_store = SqliteShardStore::from_connection(&tx, SAPLING_TABLES_PREFIX) + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?; + let result = { + let mut shardtree = ShardTree::new(shard_store, PRUNING_DEPTH.try_into().unwrap()); + callback(&mut shardtree)? + }; + + tx.commit() + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?; + Ok(result) + } + + fn put_sapling_subtree_roots( &mut self, - _output: &WalletTransparentOutput, - ) -> Result { - #[cfg(feature = "transparent-inputs")] - return wallet::put_received_transparent_utxo(self, _output); + start_index: u64, + roots: &[CommitmentTreeRoot], + ) -> Result<(), ShardTreeError> { + let tx = self + .conn + .transaction() + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?; + put_shard_roots::<_, { sapling::NOTE_COMMITMENT_TREE_DEPTH }, SAPLING_SHARD_HEIGHT>( + &tx, + SAPLING_TABLES_PREFIX, + start_index, + roots, + )?; + tx.commit() + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?; + Ok(()) + } + + #[cfg(feature = "orchard")] + type OrchardShardStore<'a> = SqliteShardStore< + &'a rusqlite::Transaction<'a>, + orchard::tree::MerkleHashOrchard, + ORCHARD_SHARD_HEIGHT, + >; + + #[cfg(feature = "orchard")] + fn with_orchard_tree_mut(&mut self, mut callback: F) -> Result + where + for<'a> F: FnMut( + &'a mut ShardTree< + Self::OrchardShardStore<'a>, + { ORCHARD_SHARD_HEIGHT * 2 }, + ORCHARD_SHARD_HEIGHT, + >, + ) -> Result, + E: From>, + { + let tx = self + .conn + .transaction() + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?; + let shard_store = SqliteShardStore::from_connection(&tx, ORCHARD_TABLES_PREFIX) + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?; + let result = { + let mut shardtree = ShardTree::new(shard_store, PRUNING_DEPTH.try_into().unwrap()); + callback(&mut shardtree)? + }; - #[cfg(not(feature = "transparent-inputs"))] - panic!( - "The wallet must be compiled with the transparent-inputs feature to use this method." + tx.commit() + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?; + Ok(result) + } + + #[cfg(feature = "orchard")] + fn put_orchard_subtree_roots( + &mut self, + start_index: u64, + roots: &[CommitmentTreeRoot], + ) -> Result<(), ShardTreeError> { + let tx = self + .conn + .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(()) + } +} + +impl<'conn, P: consensus::Parameters> WalletCommitmentTrees for WalletDb, P> { + type Error = commitment_tree::Error; + type SaplingShardStore<'a> = + SqliteShardStore<&'a rusqlite::Transaction<'a>, sapling::Node, SAPLING_SHARD_HEIGHT>; + + fn with_sapling_tree_mut(&mut self, mut callback: F) -> Result + where + for<'a> F: FnMut( + &'a mut ShardTree< + Self::SaplingShardStore<'a>, + { sapling::NOTE_COMMITMENT_TREE_DEPTH }, + SAPLING_SHARD_HEIGHT, + >, + ) -> Result, + E: From>, + { + let mut shardtree = ShardTree::new( + SqliteShardStore::from_connection(self.conn.0, SAPLING_TABLES_PREFIX) + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?, + PRUNING_DEPTH.try_into().unwrap(), ); + let result = callback(&mut shardtree)?; + + Ok(result) + } + + fn put_sapling_subtree_roots( + &mut self, + start_index: u64, + roots: &[CommitmentTreeRoot], + ) -> Result<(), ShardTreeError> { + put_shard_roots::<_, { sapling::NOTE_COMMITMENT_TREE_DEPTH }, SAPLING_SHARD_HEIGHT>( + self.conn.0, + SAPLING_TABLES_PREFIX, + start_index, + roots, + ) + } + + #[cfg(feature = "orchard")] + type OrchardShardStore<'a> = SqliteShardStore< + &'a rusqlite::Transaction<'a>, + orchard::tree::MerkleHashOrchard, + ORCHARD_SHARD_HEIGHT, + >; + + #[cfg(feature = "orchard")] + fn with_orchard_tree_mut(&mut self, mut callback: F) -> Result + where + for<'a> F: FnMut( + &'a mut ShardTree< + Self::OrchardShardStore<'a>, + { ORCHARD_SHARD_HEIGHT * 2 }, + ORCHARD_SHARD_HEIGHT, + >, + ) -> Result, + E: From>, + { + let mut shardtree = ShardTree::new( + SqliteShardStore::from_connection(self.conn.0, ORCHARD_TABLES_PREFIX) + .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?, + PRUNING_DEPTH.try_into().unwrap(), + ); + let result = callback(&mut shardtree)?; + + Ok(result) + } + + #[cfg(feature = "orchard")] + fn put_orchard_subtree_roots( + &mut self, + start_index: u64, + roots: &[CommitmentTreeRoot], + ) -> Result<(), ShardTreeError> { + put_shard_roots::<_, { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }, ORCHARD_SHARD_HEIGHT>( + self.conn.0, + ORCHARD_TABLES_PREFIX, + start_index, + roots, + ) } } @@ -734,17 +1607,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) } @@ -806,6 +1676,7 @@ pub enum FsBlockDbError { InvalidBlockstoreRoot(PathBuf), InvalidBlockPath(PathBuf), CorruptedData(String), + CacheMiss(BlockHeight), } #[cfg(feature = "unstable")] @@ -839,7 +1710,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 +1735,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 +1787,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 +1841,13 @@ impl std::fmt::Display for FsBlockDbError { e, ) } + FsBlockDbError::CacheMiss(height) => { + write!( + f, + "Requested height {} does not exist in the block cache", + height + ) + } } } } @@ -982,353 +1858,99 @@ 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 secrecy::SecretVec; + use zcash_client_backend::data_api::{WalletRead, WalletWrite}; + use zcash_primitives::block::BlockHash; - use crate::{ - wallet::init::{init_accounts_table, init_wallet_db}, - AccountId, WalletDb, - }; - - use super::BlockDb; + use crate::{testing::TestBuilder, AccountId, DEFAULT_UA_REQUEST}; #[cfg(feature = "unstable")] - use super::{ - chain::{init::init_blockmeta_db, BlockMeta}, - FsBlockDb, + use { + crate::testing::AddressType, zcash_client_backend::keys::sapling, + zcash_primitives::transaction::components::amount::NonNegativeAmount, }; - #[cfg(feature = "mainnet")] - pub(crate) fn network() -> Network { - Network::MainNetwork - } - - #[cfg(not(feature = "mainnet"))] - pub(crate) fn network() -> Network { - Network::TestNetwork - } - - #[cfg(feature = "mainnet")] - pub(crate) fn sapling_activation_height() -> BlockHeight { - Network::MainNetwork - .activation_height(NetworkUpgrade::Sapling) - .unwrap() - } - - #[cfg(not(feature = "mainnet"))] - pub(crate) fn sapling_activation_height() -> BlockHeight { - Network::TestNetwork - .activation_height(NetworkUpgrade::Sapling) - .unwrap() - } - - #[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) - } - - #[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(); - - #[cfg(feature = "transparent-inputs")] - let (tkey, taddr) = { - let tkey = legacy::keys::AccountPrivKey::from_seed(&network(), &seed, account) + #[test] + fn validate_seed() { + let st = TestBuilder::new() + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + let account = st.test_account().unwrap(); + + assert!({ + st.wallet() + .validate_seed(account.account_id(), st.test_seed().unwrap()) .unwrap() - .to_account_pubkey(); - let taddr = tkey.derive_external_ivk().unwrap().default_address().0; - (Some(tkey), Some(taddr)) - }; - - #[cfg(not(feature = "transparent-inputs"))] - let taddr = None; - - 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, - }; - - // 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(); - - CompactSaplingOutput { - cmu, - ephemeral_key, - ciphertext: enc_ciphertext.as_ref()[..52].to_vec(), - } }); - // 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(); - - CompactSaplingOutput { - cmu, - ephemeral_key, - ciphertext: enc_ciphertext.as_ref()[..52].to_vec(), - } + // check that passing an invalid account results in a failure + assert!({ + let wrong_account_index = AccountId(3); + !st.wallet() + .validate_seed(wrong_account_index, st.test_seed().unwrap()) + .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 - } - - /// 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,]) - .unwrap(); - } - - #[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(), - }; - - let blocks_dir = 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 + // check that passing an invalid seed results in a failure + assert!({ + !st.wallet() + .validate_seed(account.account_id(), &SecretVec::new(vec![1u8; 32])) + .unwrap() + }); } #[test] pub(crate) fn get_next_available_address() { - use tempfile::NamedTempFile; - - let data_file = NamedTempFile::new().unwrap(); - let mut db_data = WalletDb::for_path(data_file.path(), network()).unwrap(); - - let account = AccountId::from(0); - init_wallet_db(&mut db_data, None).unwrap(); - let _ = init_test_accounts_table_ufvk(&db_data); - - let current_addr = db_data.get_current_address(account).unwrap(); + let mut st = TestBuilder::new() + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + let account = st.test_account().cloned().unwrap(); + + let current_addr = st + .wallet() + .get_current_address(account.account_id()) + .unwrap(); assert!(current_addr.is_some()); - let mut update_ops = db_data.get_update_ops().unwrap(); - let addr2 = update_ops.get_next_available_address(account).unwrap(); + let addr2 = st + .wallet_mut() + .get_next_available_address(account.account_id(), DEFAULT_UA_REQUEST) + .unwrap(); assert!(addr2.is_some()); assert_ne!(current_addr, addr2); - let addr2_cur = db_data.get_current_address(account).unwrap(); + let addr2_cur = st + .wallet() + .get_current_address(account.account_id()) + .unwrap(); assert_eq!(addr2, addr2_cur); } #[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}; - - 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(), network()).unwrap(); - init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap(); - // Add an account to the wallet. - let (ufvk, taddr) = init_test_accounts_table_ufvk(&db_data); - let taddr = taddr.unwrap(); - - let receivers = db_data.get_transparent_receivers(0.into()).unwrap(); + let st = TestBuilder::new() + .with_block_cache() + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + let account = st.test_account().unwrap(); + let ufvk = account.usk().to_unified_full_viewing_key(); + let (taddr, _) = account.usk().default_transparent_address(); + + let receivers = st + .wallet() + .get_transparent_receivers(account.account_id()) + .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(DEFAULT_UA_REQUEST) + .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)); @@ -1337,72 +1959,47 @@ mod tests { #[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_primitives::consensus::NetworkConstants; + use zcash_primitives::zip32; + + let mut st = TestBuilder::new().with_fs_block_cache().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.wallet().params.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(), + NonNegativeAmount::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(), + NonNegativeAmount::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..89bde533bf --- /dev/null +++ b/zcash_client_sqlite/src/testing.rs @@ -0,0 +1,1902 @@ +use std::fmt; +use std::num::NonZeroU32; +use std::{collections::BTreeMap, convert::Infallible}; + +#[cfg(feature = "unstable")] +use std::fs::File; + +use group::ff::Field; +use incrementalmerkletree::{Position, Retention}; +use nonempty::NonEmpty; +use prost::Message; +use rand_chacha::ChaChaRng; +use rand_core::{CryptoRng, RngCore, SeedableRng}; +use rusqlite::{params, Connection}; +use secrecy::{Secret, SecretVec}; + +use shardtree::error::ShardTreeError; +use tempfile::NamedTempFile; + +#[cfg(feature = "unstable")] +use tempfile::TempDir; + +use sapling::{ + note_encryption::{sapling_note_encryption, SaplingDomain}, + util::generate_random_rseed, + zip32::DiversifiableFullViewingKey, + Note, Nullifier, +}; +#[allow(deprecated)] +use zcash_client_backend::{ + address::Address, + data_api::{ + self, + chain::{scan_cached_blocks, BlockSource, CommitmentTreeRoot, ScanSummary}, + wallet::{ + create_proposed_transactions, create_spend_to_address, + input_selection::{GreedyInputSelector, GreedyInputSelectorError, InputSelector}, + propose_standard_transfer_to_address, propose_transfer, spend, + }, + AccountBalance, AccountBirthday, WalletCommitmentTrees, WalletRead, WalletSummary, + WalletWrite, + }, + keys::UnifiedSpendingKey, + proposal::Proposal, + proto::compact_formats::{ + self as compact, CompactBlock, CompactSaplingOutput, CompactSaplingSpend, CompactTx, + }, + proto::proposal, + wallet::OvkPolicy, + zip321, +}; +use zcash_client_backend::{ + data_api::chain::ChainState, + fees::{standard, DustOutputPolicy}, + ShieldedProtocol, +}; +use zcash_note_encryption::Domain; +use zcash_primitives::{ + block::BlockHash, + consensus::{self, BlockHeight, NetworkUpgrade, Parameters}, + memo::{Memo, MemoBytes}, + transaction::{ + components::{amount::NonNegativeAmount, sapling::zip212_enforcement}, + fees::{zip317::FeeError as Zip317FeeError, FeeRule, StandardFeeRule}, + Transaction, TxId, + }, + zip32::DiversifierIndex, +}; +use zcash_protocol::local_consensus::LocalNetwork; +use zcash_protocol::value::{ZatBalance, Zatoshis}; + +use crate::{ + chain::init::init_cache_database, + error::SqliteClientError, + wallet::{ + commitment_tree, get_wallet_summary, init::init_wallet_db, sapling::tests::test_prover, + SubtreeScanProgress, + }, + AccountId, ReceivedNoteId, WalletDb, +}; + +use super::BlockDb; + +#[cfg(feature = "orchard")] +use { + group::ff::PrimeField, orchard::tree::MerkleHashOrchard, pasta_curves::pallas, + zcash_client_backend::proto::compact_formats::CompactOrchardAction, +}; + +#[cfg(feature = "transparent-inputs")] +use { + zcash_client_backend::data_api::wallet::{ + input_selection::ShieldingSelector, propose_shielding, shield_transparent_funds, + }, + zcash_primitives::legacy::TransparentAddress, +}; + +#[cfg(feature = "unstable")] +use crate::{ + chain::{init::init_blockmeta_db, BlockMeta}, + FsBlockDb, +}; + +pub(crate) mod pool; + +pub(crate) struct InitialChainState { + pub(crate) chain_state: ChainState, + pub(crate) prior_sapling_roots: Vec>, + #[cfg(feature = "orchard")] + pub(crate) prior_orchard_roots: Vec>, +} + +/// A builder for a `zcash_client_sqlite` test. +pub(crate) struct TestBuilder { + rng: ChaChaRng, + network: LocalNetwork, + cache: Cache, + initial_chain_state: Option, + account_birthday: Option, +} + +impl TestBuilder<()> { + 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)), + #[cfg(zcash_unstable = "nu6")] + nu6: None, + #[cfg(zcash_unstable = "zfuture")] + z_future: None, + }; + + /// Constructs a new test. + pub(crate) fn new() -> Self { + TestBuilder { + rng: ChaChaRng::seed_from_u64(0), + // Use a fake network where Sapling through NU5 activate at the same height. + // We pick 100,000 to be large enough to handle any hard-coded test offsets. + network: Self::DEFAULT_NETWORK, + cache: (), + initial_chain_state: None, + account_birthday: None, + } + } + + /// Adds a [`BlockDb`] cache to the test. + pub(crate) fn with_block_cache(self) -> TestBuilder { + TestBuilder { + rng: self.rng, + network: self.network, + cache: BlockCache::new(), + initial_chain_state: self.initial_chain_state, + account_birthday: self.account_birthday, + } + } + + /// Adds a [`FsBlockDb`] cache to the test. + #[cfg(feature = "unstable")] + pub(crate) fn with_fs_block_cache(self) -> TestBuilder { + TestBuilder { + rng: self.rng, + network: self.network, + cache: FsBlockCache::new(), + initial_chain_state: self.initial_chain_state, + account_birthday: self.account_birthday, + } + } +} + +impl TestBuilder { + pub(crate) 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 + } + + pub(crate) fn with_account_birthday( + mut self, + birthday: impl FnOnce( + &mut ChaChaRng, + &LocalNetwork, + Option<&InitialChainState>, + ) -> AccountBirthday, + ) -> Self { + assert!(self.account_birthday.is_none()); + self.account_birthday = Some(birthday( + &mut self.rng, + &self.network, + self.initial_chain_state.as_ref(), + )); + self + } + + pub(crate) 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 + } + + pub(crate) 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 + } + + /// Builds the state for this test. + pub(crate) fn build(self) -> TestState { + let data_file = NamedTempFile::new().unwrap(); + let mut db_data = WalletDb::for_path(data_file.path(), self.network).unwrap(); + init_wallet_db(&mut db_data, None).unwrap(); + + let mut cached_blocks = BTreeMap::new(); + + if let Some(initial_state) = &self.initial_chain_state { + db_data + .put_sapling_subtree_roots(0, &initial_state.prior_sapling_roots) + .unwrap(); + db_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(), + is_marked: false, + }, + ) + }) + .unwrap(); + + #[cfg(feature = "orchard")] + { + db_data + .put_orchard_subtree_roots(0, &initial_state.prior_orchard_roots) + .unwrap(); + db_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(), + is_marked: false, + }, + ) + }) + .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_id, usk) = db_data.create_account(&seed, &birthday).unwrap(); + ( + seed, + TestAccount { + account_id, + usk, + birthday, + }, + ) + }); + + TestState { + cache: self.cache, + cached_blocks, + latest_block_height: self + .initial_chain_state + .map(|s| s.chain_state.block_height()), + _data_file: data_file, + db_data, + test_account, + rng: self.rng, + } + } +} + +#[derive(Clone, Debug)] +pub(crate) struct CachedBlock { + chain_state: ChainState, + sapling_end_size: u32, + orchard_end_size: u32, +} + +impl CachedBlock { + fn none(sapling_activation_height: BlockHeight) -> Self { + Self { + chain_state: ChainState::empty(sapling_activation_height, BlockHash([0; 32])), + sapling_end_size: 0, + orchard_end_size: 0, + } + } + + 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, + } + } + + fn height(&self) -> BlockHeight { + self.chain_state.block_height() + } +} + +#[derive(Clone)] +pub(crate) struct TestAccount { + account_id: AccountId, + usk: UnifiedSpendingKey, + birthday: AccountBirthday, +} + +impl TestAccount { + pub(crate) fn account_id(&self) -> AccountId { + self.account_id + } + + pub(crate) fn usk(&self) -> &UnifiedSpendingKey { + &self.usk + } + + pub(crate) fn birthday(&self) -> &AccountBirthday { + &self.birthday + } +} + +/// The state for a `zcash_client_sqlite` test. +pub(crate) struct TestState { + cache: Cache, + cached_blocks: BTreeMap, + latest_block_height: Option, + _data_file: NamedTempFile, + db_data: WalletDb, + test_account: Option<(SecretVec, TestAccount)>, + rng: ChaChaRng, +} + +impl TestState +where + ::Error: fmt::Debug, +{ + /// Exposes an immutable reference to the test's [`BlockSource`]. + #[cfg(feature = "unstable")] + pub(crate) fn cache(&self) -> &Cache::BlockSource { + self.cache.block_source() + } + + pub(crate) 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(crate) fn generate_next_block( + &mut self, + fvk: &Fvk, + req: AddressType, + value: NonNegativeAmount, + ) -> (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, nf) = self.generate_block_at( + height, + prior_cached_block.chain_state.block_hash(), + fvk, + req, + value, + prior_cached_block.sapling_end_size, + prior_cached_block.orchard_end_size, + false, + ); + + (height, res, nf) + } + + /// 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(crate) 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::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 a single output of + /// the given value, 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(crate) fn generate_block_at( + &mut self, + height: BlockHeight, + prev_hash: BlockHash, + fvk: &Fvk, + req: AddressType, + value: NonNegativeAmount, + initial_sapling_tree_size: u32, + initial_orchard_tree_size: u32, + allow_broken_hash_chain: bool, + ) -> (Cache::InsertResult, Fvk::Nullifier) { + 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, nf) = fake_compact_block( + &self.network(), + height, + prev_hash, + fvk, + req, + value, + 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, nf) + } + + /// Creates a fake block at the expected next height spending the given note, and + /// inserts it into the cache. + pub(crate) fn generate_next_block_spending( + &mut self, + fvk: &Fvk, + note: (Fvk::Nullifier, NonNegativeAmount), + to: impl Into

, + value: NonNegativeAmount, + ) -> (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(crate) 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(crate) 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) + } + + /// Invokes [`scan_cached_blocks`] with the given arguments, expecting success. + pub(crate) 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(crate) fn try_scan_cached_blocks( + &mut self, + from_height: BlockHeight, + limit: usize, + ) -> Result< + ScanSummary, + data_api::chain::error::Error< + SqliteClientError, + ::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.db_data, + from_height, + &prior_cached_block.chain_state, + limit, + ); + result + } + + /// 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(crate) fn reset(&mut self) -> NamedTempFile { + let network = self.network(); + self.latest_block_height = None; + let tf = std::mem::replace(&mut self._data_file, NamedTempFile::new().unwrap()); + self.db_data = WalletDb::for_path(self._data_file.path(), network).unwrap(); + self.test_account = None; + init_wallet_db(&mut self.db_data, None).unwrap(); + tf + } + + // /// Reset the latest cached block to the most recent one in the cache database. + // #[allow(dead_code)] + // pub(crate) 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(); + // } +} + +impl TestState { + /// Exposes an immutable reference to the test's [`WalletDb`]. + pub(crate) fn wallet(&self) -> &WalletDb { + &self.db_data + } + + /// Exposes a mutable reference to the test's [`WalletDb`]. + pub(crate) fn wallet_mut(&mut self) -> &mut WalletDb { + &mut self.db_data + } + + /// Exposes the test framework's source of randomness. + pub(crate) fn rng_mut(&mut self) -> &mut ChaChaRng { + &mut self.rng + } + + /// Exposes the network in use. + pub(crate) fn network(&self) -> LocalNetwork { + self.db_data.params + } + + /// Convenience method for obtaining the Sapling activation height for the network under test. + pub(crate) fn sapling_activation_height(&self) -> BlockHeight { + self.db_data + .params + .activation_height(NetworkUpgrade::Sapling) + .expect("Sapling activation height must be known.") + } + + /// Exposes the test seed, if enabled via [`TestBuilder::with_test_account`]. + pub(crate) fn test_seed(&self) -> Option<&SecretVec> { + self.test_account.as_ref().map(|(seed, _)| seed) + } + + /// Exposes the test account, if enabled via [`TestBuilder::with_test_account`]. + pub(crate) fn test_account(&self) -> Option<&TestAccount> { + self.test_account.as_ref().map(|(_, acct)| acct) + } + + /// Exposes the test account's Sapling DFVK, if enabled via [`TestBuilder::with_test_account`]. + pub(crate) fn test_account_sapling(&self) -> Option { + self.test_account + .as_ref() + .and_then(|(_, acct)| acct.usk.to_unified_full_viewing_key().sapling().cloned()) + } + + /// Exposes the test account's Sapling DFVK, if enabled via [`TestBuilder::with_test_account`]. + #[cfg(feature = "orchard")] + pub(crate) fn test_account_orchard(&self) -> Option { + self.test_account + .as_ref() + .and_then(|(_, acct)| acct.usk.to_unified_full_viewing_key().orchard().cloned()) + } + + /// Insert shard roots for both trees. + pub(crate) fn put_subtree_roots( + &mut self, + sapling_start_index: u64, + sapling_roots: &[CommitmentTreeRoot], + #[cfg(feature = "orchard")] orchard_start_index: u64, + #[cfg(feature = "orchard")] orchard_roots: &[CommitmentTreeRoot], + ) -> Result<(), ShardTreeError> { + 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(()) + } + + /// Invokes [`create_spend_to_address`] with the given arguments. + #[allow(deprecated)] + #[allow(clippy::type_complexity)] + #[allow(clippy::too_many_arguments)] + pub(crate) fn create_spend_to_address( + &mut self, + usk: &UnifiedSpendingKey, + to: &Address, + amount: NonNegativeAmount, + memo: Option, + ovk_policy: OvkPolicy, + min_confirmations: NonZeroU32, + change_memo: Option, + fallback_change_pool: ShieldedProtocol, + ) -> Result< + NonEmpty, + data_api::error::Error< + SqliteClientError, + commitment_tree::Error, + GreedyInputSelectorError, + Zip317FeeError, + >, + > { + let params = self.network(); + let prover = test_prover(); + create_spend_to_address( + &mut self.db_data, + ¶ms, + &prover, + &prover, + usk, + to, + amount, + memo, + ovk_policy, + min_confirmations, + change_memo, + fallback_change_pool, + ) + } + + /// Invokes [`spend`] with the given arguments. + #[allow(clippy::type_complexity)] + pub(crate) fn spend( + &mut self, + input_selector: &InputsT, + usk: &UnifiedSpendingKey, + request: zip321::TransactionRequest, + ovk_policy: OvkPolicy, + min_confirmations: NonZeroU32, + ) -> Result< + NonEmpty, + data_api::error::Error< + SqliteClientError, + commitment_tree::Error, + InputsT::Error, + ::Error, + >, + > + where + InputsT: InputSelector>, + { + #![allow(deprecated)] + let params = self.network(); + let prover = test_prover(); + spend( + &mut self.db_data, + ¶ms, + &prover, + &prover, + input_selector, + usk, + request, + ovk_policy, + min_confirmations, + ) + } + + /// Invokes [`propose_transfer`] with the given arguments. + #[allow(clippy::type_complexity)] + pub(crate) fn propose_transfer( + &mut self, + spend_from_account: AccountId, + input_selector: &InputsT, + request: zip321::TransactionRequest, + min_confirmations: NonZeroU32, + ) -> Result< + Proposal, + data_api::error::Error< + SqliteClientError, + Infallible, + InputsT::Error, + ::Error, + >, + > + where + InputsT: InputSelector>, + { + let params = self.network(); + propose_transfer::<_, _, _, Infallible>( + &mut self.db_data, + ¶ms, + spend_from_account, + input_selector, + request, + min_confirmations, + ) + } + + /// Invokes [`propose_standard_transfer`] with the given arguments. + #[allow(clippy::type_complexity)] + #[allow(clippy::too_many_arguments)] + pub(crate) fn propose_standard_transfer( + &mut self, + spend_from_account: AccountId, + fee_rule: StandardFeeRule, + min_confirmations: NonZeroU32, + to: &Address, + amount: NonNegativeAmount, + memo: Option, + change_memo: Option, + fallback_change_pool: ShieldedProtocol, + ) -> Result< + Proposal, + data_api::error::Error< + SqliteClientError, + CommitmentTreeErrT, + GreedyInputSelectorError, + Zip317FeeError, + >, + > { + let params = self.network(); + let result = propose_standard_transfer_to_address::<_, _, CommitmentTreeErrT>( + &mut self.db_data, + ¶ms, + 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. + #[cfg(feature = "transparent-inputs")] + #[allow(clippy::type_complexity)] + #[allow(dead_code)] + pub(crate) fn propose_shielding( + &mut self, + input_selector: &InputsT, + shielding_threshold: NonNegativeAmount, + from_addrs: &[TransparentAddress], + min_confirmations: u32, + ) -> Result< + Proposal, + data_api::error::Error< + SqliteClientError, + Infallible, + InputsT::Error, + ::Error, + >, + > + where + InputsT: ShieldingSelector>, + { + let params = self.network(); + propose_shielding::<_, _, _, Infallible>( + &mut self.db_data, + ¶ms, + input_selector, + shielding_threshold, + from_addrs, + min_confirmations, + ) + } + + /// Invokes [`create_proposed_transactions`] with the given arguments. + pub(crate) fn create_proposed_transactions( + &mut self, + usk: &UnifiedSpendingKey, + ovk_policy: OvkPolicy, + proposal: &Proposal, + ) -> Result< + NonEmpty, + data_api::error::Error< + SqliteClientError, + commitment_tree::Error, + InputsErrT, + FeeRuleT::Error, + >, + > + where + FeeRuleT: FeeRule, + { + let params = self.network(); + let prover = test_prover(); + create_proposed_transactions( + &mut self.db_data, + ¶ms, + &prover, + &prover, + usk, + ovk_policy, + proposal, + ) + } + + /// Invokes [`shield_transparent_funds`] with the given arguments. + #[cfg(feature = "transparent-inputs")] + #[allow(clippy::type_complexity)] + pub(crate) fn shield_transparent_funds( + &mut self, + input_selector: &InputsT, + shielding_threshold: NonNegativeAmount, + usk: &UnifiedSpendingKey, + from_addrs: &[TransparentAddress], + min_confirmations: u32, + ) -> Result< + NonEmpty, + data_api::error::Error< + SqliteClientError, + commitment_tree::Error, + InputsT::Error, + ::Error, + >, + > + where + InputsT: ShieldingSelector>, + { + let params = self.network(); + let prover = test_prover(); + shield_transparent_funds( + &mut self.db_data, + ¶ms, + &prover, + &prover, + input_selector, + shielding_threshold, + usk, + from_addrs, + min_confirmations, + ) + } + + fn with_account_balance T>( + &self, + account: AccountId, + min_confirmations: u32, + f: F, + ) -> T { + let binding = self.get_wallet_summary(min_confirmations).unwrap(); + f(binding.account_balances().get(&account).unwrap()) + } + + pub(crate) fn get_total_balance(&self, account: AccountId) -> NonNegativeAmount { + self.with_account_balance(account, 0, |balance| balance.total()) + } + + pub(crate) fn get_spendable_balance( + &self, + account: AccountId, + min_confirmations: u32, + ) -> NonNegativeAmount { + self.with_account_balance(account, min_confirmations, |balance| { + balance.spendable_value() + }) + } + + pub(crate) fn get_pending_shielded_balance( + &self, + account: AccountId, + min_confirmations: u32, + ) -> NonNegativeAmount { + self.with_account_balance(account, min_confirmations, |balance| { + balance.value_pending_spendability() + balance.change_pending_confirmation() + }) + .unwrap() + } + + #[allow(dead_code)] + pub(crate) fn get_pending_change( + &self, + account: AccountId, + min_confirmations: u32, + ) -> NonNegativeAmount { + self.with_account_balance(account, min_confirmations, |balance| { + balance.change_pending_confirmation() + }) + } + + pub(crate) fn get_wallet_summary( + &self, + min_confirmations: u32, + ) -> Option> { + get_wallet_summary( + &self.wallet().conn.unchecked_transaction().unwrap(), + &self.wallet().params, + min_confirmations, + &SubtreeScanProgress, + ) + .unwrap() + } + + /// Returns a vector of transaction summaries + pub(crate) fn get_tx_history( + &self, + ) -> Result>, SqliteClientError> { + let mut stmt = self.wallet().conn.prepare_cached( + "SELECT * + FROM v_transactions + ORDER BY mined_height DESC, tx_index DESC", + )?; + + let results = stmt + .query_and_then::, SqliteClientError, _, _>([], |row| { + Ok(TransactionSummary { + account_id: AccountId(row.get("account_id")?), + txid: TxId::from_bytes(row.get("txid")?), + expiry_height: row + .get::<_, Option>("expiry_height")? + .map(BlockHeight::from), + mined_height: row + .get::<_, Option>("mined_height")? + .map(BlockHeight::from), + account_value_delta: ZatBalance::from_i64(row.get("account_balance_delta")?)?, + fee_paid: row + .get::<_, Option>("fee_paid")? + .map(Zatoshis::from_nonnegative_i64) + .transpose()?, + has_change: row.get("has_change")?, + sent_note_count: row.get("sent_note_count")?, + received_note_count: row.get("received_note_count")?, + memo_count: row.get("memo_count")?, + expired_unmined: row.get("expired_unmined")?, + }) + })? + .collect::, _>>()?; + + Ok(results) + } + + #[allow(dead_code)] // used only for tests that are flagged off by default + pub(crate) fn get_checkpoint_history( + &self, + ) -> Result)>, SqliteClientError> { + let mut stmt = self.wallet().conn.prepare_cached( + "SELECT checkpoint_id, 2 AS pool, position FROM sapling_tree_checkpoints + UNION + SELECT checkpoint_id, 3 AS pool, position FROM orchard_tree_checkpoints + ORDER BY checkpoint_id", + )?; + + let results = stmt + .query_and_then::<_, SqliteClientError, _, _>([], |row| { + Ok(( + BlockHeight::from(row.get::<_, u32>(0)?), + match row.get::<_, i64>(1)? { + 2 => ShieldedProtocol::Sapling, + 3 => ShieldedProtocol::Orchard, + _ => unreachable!(), + }, + row.get::<_, Option>(2)?.map(Position::from), + )) + })? + .collect::, _>>()?; + + Ok(results) + } +} + +pub(crate) struct TransactionSummary { + account_id: AccountId, + txid: TxId, + expiry_height: Option, + mined_height: Option, + account_value_delta: ZatBalance, + fee_paid: Option, + has_change: bool, + sent_note_count: usize, + received_note_count: usize, + memo_count: usize, + expired_unmined: bool, +} + +#[allow(dead_code)] +impl TransactionSummary { + pub(crate) fn account_id(&self) -> &AccountId { + &self.account_id + } + + pub(crate) fn txid(&self) -> TxId { + self.txid + } + + pub(crate) fn expiry_height(&self) -> Option { + self.expiry_height + } + + pub(crate) fn mined_height(&self) -> Option { + self.mined_height + } + + pub(crate) fn account_value_delta(&self) -> ZatBalance { + self.account_value_delta + } + + pub(crate) fn fee_paid(&self) -> Option { + self.fee_paid + } + + pub(crate) fn has_change(&self) -> bool { + self.has_change + } + + pub(crate) fn sent_note_count(&self) -> usize { + self.sent_note_count + } + + pub(crate) fn received_note_count(&self) -> usize { + self.received_note_count + } + + pub(crate) fn expired_unmined(&self) -> bool { + self.expired_unmined + } + + pub(crate) fn memo_count(&self) -> usize { + self.memo_count + } +} + +/// Trait used by tests that require a full viewing key. +pub(crate) trait TestFvk { + type Nullifier; + + fn sapling_ovk(&self) -> Option; + + #[cfg(feature = "orchard")] + fn orchard_ovk(&self, scope: zip32::Scope) -> Option; + + fn add_spend( + &self, + ctx: &mut CompactTx, + nf: Self::Nullifier, + rng: &mut R, + ); + + #[allow(clippy::too_many_arguments)] + fn add_output( + &self, + ctx: &mut CompactTx, + params: &P, + height: BlockHeight, + req: AddressType, + value: NonNegativeAmount, + initial_sapling_tree_size: u32, + rng: &mut R, + ) -> Self::Nullifier; + + #[allow(clippy::too_many_arguments)] + fn add_logical_action( + &self, + ctx: &mut CompactTx, + params: &P, + height: BlockHeight, + nf: Self::Nullifier, + req: AddressType, + value: NonNegativeAmount, + 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, + ) + } +} + +impl TestFvk for DiversifiableFullViewingKey { + type Nullifier = Nullifier; + + fn sapling_ovk(&self) -> Option { + Some(self.fvk().ovk) + } + + #[cfg(feature = "orchard")] + fn orchard_ovk(&self, _: zip32::Scope) -> Option { + 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: NonNegativeAmount, + 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) + } +} + +#[cfg(feature = "orchard")] +impl TestFvk for orchard::keys::FullViewingKey { + type Nullifier = orchard::note::Nullifier; + + fn sapling_ovk(&self) -> Option { + None + } + + fn orchard_ovk(&self, scope: zip32::Scope) -> Option { + 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, + NonNegativeAmount::ZERO, + self.orchard_ovk(zip32::Scope::Internal), + rng, + ); + ctx.actions.push(cact); + } + + fn add_output( + &self, + ctx: &mut CompactTx, + _: &P, + _: BlockHeight, + req: AddressType, + value: NonNegativeAmount, + _: u32, + mut rng: &mut R, + ) -> Self::Nullifier { + // Generate a dummy nullifier + 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, + req: AddressType, + value: NonNegativeAmount, + _: u32, + rng: &mut R, + ) -> Self::Nullifier { + 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); + + // Return the nullifier of the newly created output note + note.nullifier(self) + } +} + +#[allow(dead_code)] +pub(crate) enum AddressType { + DefaultExternal, + DiversifiedExternal(DiversifierIndex), + 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: NonNegativeAmount, + ovk: Option, + rng: &mut R, +) -> (CompactSaplingOutput, sapling::Note) { + let rseed = generate_random_rseed(zip212_enforcement(params, height), rng); + let note = Note::from_parts( + recipient, + sapling::value::NoteValue::from_raw(value.into_u64()), + rseed, + ); + let encryptor = sapling_note_encryption(ovk, note.clone(), *MemoBytes::empty().as_array(), rng); + let cmu = note.cmu().to_bytes().to_vec(); + let ephemeral_key = SaplingDomain::epk_bytes(encryptor.epk()).0.to_vec(); + let enc_ciphertext = encryptor.encrypt_note_plaintext(); + + ( + CompactSaplingOutput { + cmu, + ephemeral_key, + ciphertext: enc_ciphertext.as_ref()[..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: NonNegativeAmount, + ovk: Option, + 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().as_ref()[..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 +} + +/// 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. +#[allow(clippy::too_many_arguments)] +fn fake_compact_block( + params: &P, + height: BlockHeight, + prev_hash: BlockHash, + fvk: &Fvk, + req: AddressType, + value: NonNegativeAmount, + initial_sapling_tree_size: u32, + initial_orchard_tree_size: u32, + mut rng: impl RngCore + CryptoRng, +) -> (CompactBlock, Fvk::Nullifier) { + // Create a fake CompactBlock containing the note + let mut ctx = fake_compact_tx(&mut rng); + let nf = fvk.add_output( + &mut ctx, + params, + height, + req, + value, + initial_sapling_tree_size, + &mut rng, + ); + + let cb = fake_compact_block_from_compact_tx( + ctx, + height, + prev_hash, + initial_sapling_tree_size, + initial_orchard_tree_size, + rng, + ); + (cb, nf) +} + +/// 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, NonNegativeAmount), + fvk: &Fvk, + to: Address, + value: NonNegativeAmount, + 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(_) => 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::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(crate) trait TestCache { + 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(&self, cb: &CompactBlock) -> Self::InsertResult; +} + +pub(crate) struct BlockCache { + _cache_file: NamedTempFile, + db_cache: BlockDb, +} + +impl BlockCache { + 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 BlockSource = BlockDb; + type InsertResult = (); + + fn block_source(&self) -> &Self::BlockSource { + &self.db_cache + } + + fn insert(&self, cb: &CompactBlock) { + let cb_bytes = cb.encode_to_vec(); + self.db_cache + .0 + .prepare("INSERT INTO compactblocks (height, data) VALUES (?, ?)") + .unwrap() + .execute(params![u32::from(cb.height()), cb_bytes,]) + .unwrap(); + } +} + +#[cfg(feature = "unstable")] +pub(crate) struct FsBlockCache { + fsblockdb_root: TempDir, + db_meta: FsBlockDb, +} + +#[cfg(feature = "unstable")] +impl FsBlockCache { + 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 BlockSource = FsBlockDb; + type InsertResult = BlockMeta; + + fn block_source(&self) -> &Self::BlockSource { + &self.db_meta + } + + fn insert(&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 + } +} + +pub(crate) fn input_selector( + fee_rule: StandardFeeRule, + change_memo: Option<&str>, + fallback_change_pool: ShieldedProtocol, +) -> GreedyInputSelector< + WalletDb, + standard::SingleOutputChangeStrategy, +> { + let change_memo = change_memo.map(|m| MemoBytes::from(m.parse::().unwrap())); + let change_strategy = + standard::SingleOutputChangeStrategy::new(fee_rule, change_memo, fallback_change_pool); + GreedyInputSelector::new(change_strategy, 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( + db_data: &WalletDb, + proposal: &Proposal, +) { + let proposal_proto = proposal::Proposal::from_standard_proposal(proposal); + let deserialized_proposal = proposal_proto.try_into_standard_proposal(db_data); + assert_matches!(deserialized_proposal, Ok(r) if &r == proposal); +} diff --git a/zcash_client_sqlite/src/testing/pool.rs b/zcash_client_sqlite/src/testing/pool.rs new file mode 100644 index 0000000000..33e7bcca59 --- /dev/null +++ b/zcash_client_sqlite/src/testing/pool.rs @@ -0,0 +1,2221 @@ +//! Test logic involving a single shielded pool. +//! +//! Generalised for sharing across the Sapling and Orchard implementations. + +use std::{ + convert::Infallible, + num::{NonZeroU32, NonZeroU8}, +}; + +use incrementalmerkletree::{frontier::Frontier, Level}; +use rand_core::RngCore; +use rusqlite::params; +use secrecy::Secret; +use shardtree::error::ShardTreeError; +use zcash_primitives::{ + block::BlockHash, + consensus::{BranchId, NetworkUpgrade, Parameters}, + legacy::TransparentAddress, + memo::{Memo, MemoBytes}, + transaction::{ + components::amount::NonNegativeAmount, + fees::{ + fixed::FeeRule as FixedFeeRule, zip317::FeeError as Zip317FeeError, StandardFeeRule, + }, + Transaction, + }, + zip32::Scope, +}; + +use zcash_client_backend::{ + address::Address, + data_api::{ + self, + chain::{self, ChainState, CommitmentTreeRoot, ScanSummary}, + error::Error, + wallet::{ + decrypt_and_store_transaction, + input_selection::{GreedyInputSelector, GreedyInputSelectorError}, + }, + AccountBirthday, DecryptedTransaction, Ratio, WalletRead, WalletSummary, WalletWrite, + }, + decrypt_transaction, + fees::{fixed, standard, DustOutputPolicy}, + keys::UnifiedSpendingKey, + scanning::ScanError, + wallet::{Note, OvkPolicy, ReceivedNote}, + zip321::{self, Payment, TransactionRequest}, + ShieldedProtocol, +}; +use zcash_protocol::consensus::BlockHeight; + +use super::TestFvk; +use crate::{ + error::SqliteClientError, + testing::{input_selector, AddressType, BlockCache, InitialChainState, TestBuilder, TestState}, + wallet::{block_max_scanned, commitment_tree, parse_scope, truncate_to_height}, + AccountId, NoteId, ReceivedNoteId, +}; + +#[cfg(feature = "transparent-inputs")] +use { + zcash_client_backend::{ + fees::TransactionBalance, proposal::Step, wallet::WalletTransparentOutput, PoolType, + }, + zcash_primitives::transaction::components::{OutPoint, TxOut}, +}; + +pub(crate) type OutputRecoveryError = Error< + SqliteClientError, + commitment_tree::Error, + GreedyInputSelectorError, + Zip317FeeError, +>; + +/// Trait that exposes the pool-specific types and operations necessary to run the +/// single-shielded-pool tests on a given pool. +pub(crate) trait ShieldedPoolTester { + const SHIELDED_PROTOCOL: ShieldedProtocol; + const TABLES_PREFIX: &'static str; + + 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>; + + fn next_subtree_index(s: &WalletSummary) -> u64; + + fn select_spendable_notes( + st: &TestState, + account: AccountId, + target_value: NonNegativeAmount, + anchor_height: BlockHeight, + exclude: &[ReceivedNoteId], + ) -> Result>, SqliteClientError>; + + fn decrypted_pool_outputs_count(d_tx: &DecryptedTransaction<'_, AccountId>) -> usize; + + fn with_decrypted_pool_memos( + d_tx: &DecryptedTransaction<'_, AccountId>, + f: impl FnMut(&MemoBytes), + ); + + fn try_output_recovery( + st: &TestState, + height: BlockHeight, + tx: &Transaction, + fvk: &Self::Fvk, + ) -> Result, OutputRecoveryError>; + + fn received_note_count(summary: &ScanSummary) -> usize; +} + +pub(crate) fn send_single_step_proposed_transfer() { + let mut st = TestBuilder::new() + .with_block_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 = NonNegativeAmount::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.account_id()), value); + assert_eq!(st.get_spendable_balance(account.account_id(), 1), value); + + assert_eq!( + block_max_scanned(&st.wallet().conn, &st.wallet().params) + .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()), + NonNegativeAmount::const_from_u64(10000), + )]) + .unwrap(); + + // TODO: This test was originally written to use the pre-zip-313 fee rule + // and has not yet been updated. + #[allow(deprecated)] + let fee_rule = StandardFeeRule::PreZip313; + + let change_memo = "Test change memo".parse::().unwrap(); + let change_strategy = standard::SingleOutputChangeStrategy::new( + fee_rule, + Some(change_memo.clone().into()), + T::SHIELDED_PROTOCOL, + ); + let input_selector = &GreedyInputSelector::new(change_strategy, DustOutputPolicy::default()); + + let proposal = st + .propose_transfer( + account.account_id(), + input_selector, + 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.account_id(), + account.usk().to_unified_full_viewing_key(), + )] + .into_iter() + .collect(); + let d_tx = decrypt_transaction(&st.network(), h + 1, &tx, &ufvks); + assert_eq!(T::decrypted_pool_outputs_count(&d_tx), 2); + + let mut found_tx_change_memo = false; + let mut found_tx_empty_memo = false; + T::with_decrypted_pool_memos(&d_tx, |memo| { + if Memo::try_from(memo).unwrap() == change_memo { + found_tx_change_memo = true + } + if Memo::try_from(memo).unwrap() == Memo::Empty { + found_tx_empty_memo = true + } + }); + assert!(found_tx_change_memo); + assert!(found_tx_empty_memo); + + // Verify that the stored sent notes match what we're expecting + let sent_note_ids = { + let mut stmt_sent_notes = st + .wallet() + .conn + .prepare( + "SELECT output_index + FROM sent_notes + JOIN transactions ON transactions.id_tx = sent_notes.tx + WHERE transactions.txid = ?", + ) + .unwrap(); + + stmt_sent_notes + .query(rusqlite::params![sent_tx_id.as_ref()]) + .unwrap() + .mapped(|row| Ok(NoteId::new(sent_tx_id, T::SHIELDED_PROTOCOL, row.get(0)?))) + .collect::, _>>() + .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.get_tx_history().unwrap(); + assert_eq!(tx_history.len(), 2); + + assert_matches!( + decrypt_and_store_transaction(&st.network(), st.wallet_mut(), &tx), + Ok(_) + ); +} + +#[cfg(feature = "transparent-inputs")] +pub(crate) fn send_multi_step_proposed_transfer() { + use nonempty::NonEmpty; + use zcash_client_backend::proposal::{Proposal, StepOutput, StepOutputIndex}; + use zcash_primitives::legacy::keys::IncomingViewingKey; + + let mut st = TestBuilder::new() + .with_block_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 = NonNegativeAmount::const_from_u64(65000); + 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.account_id()), value); + assert_eq!(st.get_spendable_balance(account.account_id(), 1), value); + + assert_eq!( + block_max_scanned(&st.wallet().conn, &st.wallet().params) + .unwrap() + .unwrap() + .block_height(), + h + ); + + // Generate a single-step proposal. Then, instead of executing that proposal, + // we will use its only step as the first step in a multi-step proposal that + // spends the first step's output. + + // The first step will deshield to the wallet's default transparent address + let to0 = Address::Transparent(account.usk().default_transparent_address().0); + let request0 = zip321::TransactionRequest::new(vec![Payment::without_memo( + to0.to_zcash_address(&st.network()), + NonNegativeAmount::const_from_u64(50000), + )]) + .unwrap(); + + let fee_rule = StandardFeeRule::Zip317; + let input_selector = GreedyInputSelector::new( + standard::SingleOutputChangeStrategy::new(fee_rule, None, T::SHIELDED_PROTOCOL), + DustOutputPolicy::default(), + ); + let proposal0 = st + .propose_transfer( + account.account_id(), + &input_selector, + request0, + NonZeroU32::new(1).unwrap(), + ) + .unwrap(); + + let min_target_height = proposal0.min_target_height(); + let step0 = &proposal0.steps().head; + + assert!(step0.balance().proposed_change().is_empty()); + assert_eq!( + step0.balance().fee_required(), + NonNegativeAmount::const_from_u64(15000) + ); + + // We'll use an internal transparent address that hasn't been added to the wallet + // to simulate an external transparent recipient. + let to1 = Address::Transparent( + account + .usk() + .transparent() + .to_account_pubkey() + .derive_internal_ivk() + .unwrap() + .default_address() + .0, + ); + let request1 = zip321::TransactionRequest::new(vec![Payment::without_memo( + to1.to_zcash_address(&st.network()), + NonNegativeAmount::const_from_u64(40000), + )]) + .unwrap(); + + let step1 = Step::from_parts( + &[step0.clone()], + request1, + [(0, PoolType::Transparent)].into_iter().collect(), + vec![], + None, + vec![StepOutput::new(0, StepOutputIndex::Payment(0))], + TransactionBalance::new(vec![], NonNegativeAmount::const_from_u64(10000)).unwrap(), + false, + ) + .unwrap(); + + let proposal = Proposal::multi_step( + fee_rule, + min_target_height, + NonEmpty::from_vec(vec![step0.clone(), step1]).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() == 2); + let txids = create_proposed_result.unwrap(); + + // Verify that the stored sent outputs match what we're expecting + let mut stmt_sent = st + .wallet() + .conn + .prepare( + "SELECT value + FROM sent_notes + JOIN transactions ON transactions.id_tx = sent_notes.tx + WHERE transactions.txid = ?", + ) + .unwrap(); + + let confirmed_sent = txids + .iter() + .map(|sent_txid| { + // check that there's a sent output with the correct value corresponding to + stmt_sent + .query(rusqlite::params![sent_txid.as_ref()]) + .unwrap() + .mapped(|row| { + let value: u32 = row.get(0)?; + Ok((sent_txid, value)) + }) + .collect::, _>>() + .unwrap() + }) + .collect::>(); + + assert_eq!( + confirmed_sent.get(0).and_then(|v| v.get(0)), + Some(&(&txids[0], 50000)) + ); + assert_eq!( + confirmed_sent.get(1).and_then(|v| v.get(0)), + Some(&(&txids[1], 40000)) + ); +} + +#[allow(deprecated)] +pub(crate) fn create_to_address_fails_on_incorrect_usk() { + let mut st = TestBuilder::new() + .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(); + + // Attempting to spend with a USK that is not in the wallet results in an error + assert_matches!( + st.create_spend_to_address( + &usk1, + &to, + NonNegativeAmount::const_from_u64(1), + None, + OvkPolicy::Sender, + NonZeroU32::new(1).unwrap(), + None, + T::SHIELDED_PROTOCOL, + ), + Err(data_api::error::Error::KeyNotRecognized) + ); +} + +#[allow(deprecated)] +pub(crate) fn proposal_fails_with_no_blocks() { + let mut st = TestBuilder::new() + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account_id = st.test_account().unwrap().account_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::PreZip313, + NonZeroU32::new(1).unwrap(), + &to, + NonNegativeAmount::const_from_u64(1), + None, + None, + T::SHIELDED_PROTOCOL, + ), + Err(data_api::error::Error::ScanRequired) + ); +} + +pub(crate) fn spend_fails_on_unverified_notes() { + let mut st = TestBuilder::new() + .with_block_cache() + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account = st.test_account().cloned().unwrap(); + let account_id = account.account_id(); + let dfvk = T::test_account_fvk(&st); + + // Add funds to the wallet in a single note + let value = NonNegativeAmount::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), + NonNegativeAmount::ZERO + ); + + // Wallet is fully scanned + let summary = st.get_wallet_summary(1); + assert_eq!( + summary.and_then(|s| s.scan_progress()), + 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.and_then(|s| s.scan_progress()), + 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, + NonNegativeAmount::const_from_u64(70000), + None, + None, + T::SHIELDED_PROTOCOL, + ), + Err(data_api::error::Error::InsufficientFunds { + available, + required + }) + if available == NonNegativeAmount::const_from_u64(50000) + && required == NonNegativeAmount::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 * 10).unwrap()); + + // Spend still fails + assert_matches!( + st.propose_standard_transfer::( + account_id, + StandardFeeRule::Zip317, + NonZeroU32::new(10).unwrap(), + &to, + NonNegativeAmount::const_from_u64(70000), + None, + None, + T::SHIELDED_PROTOCOL, + ), + Err(data_api::error::Error::InsufficientFunds { + available, + required + }) + if available == NonNegativeAmount::const_from_u64(50000) + && required == NonNegativeAmount::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 * 11).unwrap()); + // Spendable balance at 10 confirmations is value * 2. + assert_eq!( + st.get_spendable_balance(account_id, 10), + (value * 2).unwrap() + ); + assert_eq!( + st.get_pending_shielded_balance(account_id, 10), + (value * 9).unwrap() + ); + + // Should now be able to generate a proposal + let amount_sent = NonNegativeAmount::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 * 11).unwrap() + - (amount_sent + NonNegativeAmount::from_u64(10000).unwrap()).unwrap()) + .unwrap() + ); +} + +pub(crate) fn spend_fails_on_locked_notes() { + let mut st = TestBuilder::new() + .with_block_cache() + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account = st.test_account().cloned().unwrap(); + let account_id = account.account_id(); + let dfvk = T::test_account_fvk(&st); + + // TODO: This test was originally written to use the pre-zip-313 fee rule + // and has not yet been updated. + #[allow(deprecated)] + let fee_rule = StandardFeeRule::PreZip313; + + // Add funds to the wallet in a single note + let value = NonNegativeAmount::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, + NonNegativeAmount::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, + NonNegativeAmount::const_from_u64(2000), + None, + None, + T::SHIELDED_PROTOCOL, + ), + Err(data_api::error::Error::InsufficientFunds { + available, + required + }) + if available == NonNegativeAmount::ZERO && required == NonNegativeAmount::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, + NonNegativeAmount::const_from_u64(2000), + None, + None, + T::SHIELDED_PROTOCOL, + ), + Err(data_api::error::Error::InsufficientFunds { + available, + required + }) + if available == NonNegativeAmount::ZERO && required == NonNegativeAmount::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 = NonNegativeAmount::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 + NonNegativeAmount::from_u64(10000).unwrap()).unwrap()).unwrap() + ); +} + +pub(crate) fn ovk_policy_prevents_recovery_from_chain() { + let mut st = TestBuilder::new() + .with_block_cache() + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account = st.test_account().cloned().unwrap(); + let account_id = account.account_id(); + let dfvk = T::test_account_fvk(&st); + + // Add funds to the wallet in a single note + let value = NonNegativeAmount::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); + + // TODO: This test was originally written to use the pre-zip-313 fee rule + // and has not yet been updated. + #[allow(deprecated)] + let fee_rule = StandardFeeRule::PreZip313; + + #[allow(clippy::type_complexity)] + let send_and_recover_with_policy = |st: &mut TestState, + ovk_policy| + -> Result< + Option<(Note, Address, MemoBytes)>, + Error< + SqliteClientError, + commitment_tree::Error, + GreedyInputSelectorError, + Zip317FeeError, + >, + > { + let min_confirmations = NonZeroU32::new(1).unwrap(); + let proposal = st.propose_standard_transfer( + account_id, + fee_rule, + min_confirmations, + &addr2, + NonNegativeAmount::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 raw_tx: Vec<_> = st + .wallet() + .conn + .query_row( + "SELECT raw FROM transactions + WHERE txid = ?", + [txid.as_ref()], + |row| row.get(0), + ) + .unwrap(); + let tx = Transaction::read(&raw_tx[..], BranchId::Canopy).unwrap(); + + T::try_output_recovery(st, 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(crate) fn spend_succeeds_to_t_addr_zero_change() { + let mut st = TestBuilder::new() + .with_block_cache() + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account = st.test_account().cloned().unwrap(); + let account_id = account.account_id(); + let dfvk = T::test_account_fvk(&st); + + // Add funds to the wallet in a single note + let value = NonNegativeAmount::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 at 1 confirmation. + assert_eq!(st.get_total_balance(account_id), value); + assert_eq!(st.get_spendable_balance(account_id, 1), value); + + // TODO: This test was originally written to use the pre-zip-313 fee rule + // and has not yet been updated. + #[allow(deprecated)] + let fee_rule = StandardFeeRule::PreZip313; + + // 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, + NonNegativeAmount::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(crate) fn change_note_spends_succeed() { + let mut st = TestBuilder::new() + .with_block_cache() + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account = st.test_account().cloned().unwrap(); + let account_id = account.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 = NonNegativeAmount::const_from_u64(60000); + 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), + NonNegativeAmount::ZERO + ); + + let change_note_scope = st.wallet().conn.query_row( + &format!( + "SELECT recipient_key_scope + FROM {}_received_notes + WHERE value = ?", + T::TABLES_PREFIX, + ), + params![u64::from(value)], + |row| Ok(parse_scope(row.get(0)?)), + ); + assert_matches!(change_note_scope, Ok(Some(Scope::Internal))); + + // TODO: This test was originally written to use the pre-zip-313 fee rule + // and has not yet been updated. + #[allow(deprecated)] + let fee_rule = StandardFeeRule::PreZip313; + + // 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, + NonNegativeAmount::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(crate) fn external_address_change_spends_detected_in_restore_from_seed< + T: ShieldedPoolTester, +>() { + let mut st = TestBuilder::new().with_block_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 (account_id, usk) = st.wallet_mut().create_account(&seed, &birthday).unwrap(); + let dfvk = T::sk_to_fvk(T::usk_to_sk(&usk)); + + let (account2, usk2) = st.wallet_mut().create_account(&seed, &birthday).unwrap(); + let dfvk2 = T::sk_to_fvk(T::usk_to_sk(&usk2)); + + // Add funds to the wallet in a single note + let value = NonNegativeAmount::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(account_id), value); + assert_eq!(st.get_spendable_balance(account_id, 1), value); + assert_eq!(st.get_total_balance(account2), NonNegativeAmount::ZERO); + + let amount_sent = NonNegativeAmount::from_u64(20000).unwrap(); + let amount_legacy_change = NonNegativeAmount::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(); + + #[allow(deprecated)] + let fee_rule = FixedFeeRule::standard(); + let input_selector = GreedyInputSelector::new( + fixed::SingleOutputChangeStrategy::new(fee_rule, None, T::SHIELDED_PROTOCOL), + DustOutputPolicy::default(), + ); + + let txid = st + .spend( + &input_selector, + &usk, + req, + OvkPolicy::Sender, + NonZeroU32::new(1).unwrap(), + ) + .unwrap()[0]; + + let amount_left = (value - (amount_sent + fee_rule.fixed_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(account_id, 1), pending_change); + // We spent the only note so we only have pending change. + assert_eq!(st.get_total_balance(account_id), 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(account_id), amount_left); + + st.reset(); + + // Account creation and DFVK derivation should be deterministic. + let (_, restored_usk) = st.wallet_mut().create_account(&seed, &birthday).unwrap(); + assert!(T::fvks_equal( + &T::sk_to_fvk(T::usk_to_sk(&restored_usk)), + &dfvk, + )); + + let (_, restored_usk2) = st.wallet_mut().create_account(&seed, &birthday).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(account_id), amount_left); +} + +#[allow(dead_code)] +pub(crate) fn zip317_spend() { + let mut st = TestBuilder::new() + .with_block_cache() + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account = st.test_account().cloned().unwrap(); + let account_id = account.account_id(); + let dfvk = T::test_account_fvk(&st); + + // Add funds to the wallet + let (h1, _, _) = st.generate_next_block( + &dfvk, + AddressType::Internal, + NonNegativeAmount::const_from_u64(50000), + ); + + // Add 10 dust notes to the wallet + for _ in 1..=10 { + st.generate_next_block( + &dfvk, + AddressType::DefaultExternal, + NonNegativeAmount::const_from_u64(1000), + ); + } + + st.scan_cached_blocks(h1, 11); + + // Spendable balance matches total balance + let total = NonNegativeAmount::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 = input_selector(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()), + NonNegativeAmount::const_from_u64(50000), + )]) + .unwrap(); + + assert_matches!( + st.spend( + &input_selector, + account.usk(), + req, + OvkPolicy::Sender, + NonZeroU32::new(1).unwrap(), + ), + Err(Error::InsufficientFunds { available, required }) + if available == NonNegativeAmount::const_from_u64(51000) + && required == NonNegativeAmount::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()), + NonNegativeAmount::const_from_u64(41000), + )]) + .unwrap(); + + let txid = st + .spend( + &input_selector, + 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 - NonNegativeAmount::const_from_u64(10000)).unwrap() + ); +} + +#[cfg(feature = "transparent-inputs")] +pub(crate) fn shield_transparent() { + let mut st = TestBuilder::new() + .with_block_cache() + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account = st.test_account().cloned().unwrap(); + let dfvk = T::test_account_fvk(&st); + + let uaddr = st + .wallet() + .get_current_address(account.account_id()) + .unwrap() + .unwrap(); + let taddr = uaddr.transparent().unwrap(); + + // Ensure that the wallet has at least one block + let (h, _, _) = st.generate_next_block( + &dfvk, + AddressType::Internal, + NonNegativeAmount::const_from_u64(50000), + ); + st.scan_cached_blocks(h, 1); + + let utxo = WalletTransparentOutput::from_parts( + OutPoint::new([1u8; 32], 1), + TxOut { + value: NonNegativeAmount::const_from_u64(10000), + script_pubkey: taddr.script(), + }, + h, + ) + .unwrap(); + + let res0 = st.wallet_mut().put_received_transparent_utxo(&utxo); + assert!(matches!(res0, Ok(_))); + + // TODO: This test was originally written to use the pre-zip-313 fee rule + // and has not yet been updated. + #[allow(deprecated)] + let fee_rule = StandardFeeRule::PreZip313; + + let input_selector = GreedyInputSelector::new( + standard::SingleOutputChangeStrategy::new(fee_rule, None, T::SHIELDED_PROTOCOL), + DustOutputPolicy::default(), + ); + + assert_matches!( + st.shield_transparent_funds( + &input_selector, + NonNegativeAmount::from_u64(10000).unwrap(), + account.usk(), + &[*taddr], + 1 + ), + Ok(_) + ); +} + +// FIXME: This requires fixes to the test framework. +#[allow(dead_code)] +pub(crate) fn birthday_in_anchor_shard() { + // 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_block_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 = NonNegativeAmount::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, + NonNegativeAmount::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.account_id(); + let spendable = T::select_spendable_notes( + &st, + account_id, + NonNegativeAmount::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, + NonNegativeAmount::const_from_u64(300000), + received_tx_height + 10, + &[], + ) + .unwrap(); + + assert_eq!(spendable.len(), 1); +} + +pub(crate) fn checkpoint_gaps() { + let mut st = TestBuilder::new() + .with_block_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, + NonNegativeAmount::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 = NonNegativeAmount::const_from_u64(10000); + st.generate_block_at( + account.birthday().height() + 10, + BlockHash([0; 32]), + ¬_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); + + // Fake that everything has been scanned + st.wallet() + .conn + .execute_batch("UPDATE scan_queue SET priority = 10") + .unwrap(); + + // Verify that our note is considered spendable + let spendable = T::select_spendable_notes( + &st, + account.account_id(), + NonNegativeAmount::const_from_u64(300000), + account.birthday().height() + 5, + &[], + ) + .unwrap(); + assert_eq!(spendable.len(), 1); + + // Attempt to spend the note with 5 confirmations + let to = T::fvk_default_address(¬_our_key); + assert_matches!( + st.create_spend_to_address( + account.usk(), + &to, + NonNegativeAmount::const_from_u64(10000), + None, + OvkPolicy::Sender, + NonZeroU32::new(5).unwrap(), + None, + T::SHIELDED_PROTOCOL, + ), + Ok(_) + ); +} + +#[cfg(feature = "orchard")] +pub(crate) fn pool_crossing_required() { + let mut st = TestBuilder::new() + .with_block_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 = NonNegativeAmount::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.account_id()), initial_balance); + assert_eq!( + st.get_spendable_balance(account.account_id(), 1), + initial_balance + ); + + let transfer_amount = NonNegativeAmount::const_from_u64(200000); + let p0_to_p1 = zip321::TransactionRequest::new(vec![Payment::without_memo( + p1_to.to_zcash_address(&st.network()), + transfer_amount, + )]) + .unwrap(); + + let fee_rule = StandardFeeRule::Zip317; + let input_selector = GreedyInputSelector::new( + standard::SingleOutputChangeStrategy::new(fee_rule, None, P1::SHIELDED_PROTOCOL), + DustOutputPolicy::default(), + ); + let proposal0 = st + .propose_transfer( + account.account_id(), + &input_selector, + 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 = NonNegativeAmount::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.get(0).unwrap(); + // Since this is a cross-pool transfer, change will be sent to the preferred pool. + assert_eq!( + change_output.output_pool(), + 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.account_id()), + (initial_balance - expected_fee).unwrap() + ); + assert_eq!( + st.get_spendable_balance(account.account_id(), 1), + (initial_balance - expected_fee).unwrap() + ); +} + +#[cfg(feature = "orchard")] +pub(crate) fn fully_funded_fully_private() { + let mut st = TestBuilder::new() + .with_block_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 = NonNegativeAmount::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 * 2).unwrap(); + assert_eq!(st.get_total_balance(account.account_id()), initial_balance); + assert_eq!( + st.get_spendable_balance(account.account_id(), 1), + initial_balance + ); + + let transfer_amount = NonNegativeAmount::const_from_u64(200000); + let p0_to_p1 = zip321::TransactionRequest::new(vec![Payment::without_memo( + p1_to.to_zcash_address(&st.network()), + transfer_amount, + )]) + .unwrap(); + + let fee_rule = StandardFeeRule::Zip317; + 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). + standard::SingleOutputChangeStrategy::new(fee_rule, None, P0::SHIELDED_PROTOCOL), + DustOutputPolicy::default(), + ); + let proposal0 = st + .propose_transfer( + account.account_id(), + &input_selector, + 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 = NonNegativeAmount::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.get(0).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(), 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.account_id()), + (initial_balance - expected_fee).unwrap() + ); + assert_eq!( + st.get_spendable_balance(account.account_id(), 1), + (initial_balance - expected_fee).unwrap() + ); +} + +#[cfg(feature = "orchard")] +pub(crate) fn fully_funded_send_to_t() { + let mut st = TestBuilder::new() + .with_block_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 = NonNegativeAmount::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 * 2).unwrap(); + assert_eq!(st.get_total_balance(account.account_id()), initial_balance); + assert_eq!( + st.get_spendable_balance(account.account_id(), 1), + initial_balance + ); + + let transfer_amount = NonNegativeAmount::const_from_u64(200000); + let p0_to_p1 = zip321::TransactionRequest::new(vec![Payment::without_memo( + Address::Transparent(p1_to).to_zcash_address(&st.network()), + transfer_amount, + )]) + .unwrap(); + + let fee_rule = StandardFeeRule::Zip317; + 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). + standard::SingleOutputChangeStrategy::new(fee_rule, None, P0::SHIELDED_PROTOCOL), + DustOutputPolicy::default(), + ); + let proposal0 = st + .propose_transfer( + account.account_id(), + &input_selector, + 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 = NonNegativeAmount::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.get(0).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(), ShieldedProtocol::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.account_id()), + (initial_balance - transfer_amount - expected_fee).unwrap() + ); + assert_eq!( + st.get_spendable_balance(account.account_id(), 1), + (initial_balance - transfer_amount - expected_fee).unwrap() + ); +} + +#[cfg(feature = "orchard")] +pub(crate) fn multi_pool_checkpoint() { + let mut st = TestBuilder::new() + .with_block_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.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 = NonNegativeAmount::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 * 3).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 fee_rule = StandardFeeRule::Zip317; + let input_selector = GreedyInputSelector::new( + standard::SingleOutputChangeStrategy::new(fee_rule, None, P1::SHIELDED_PROTOCOL), + DustOutputPolicy::default(), + ); + + // First, send funds just to P0 + let transfer_amount = NonNegativeAmount::const_from_u64(200000); + let p0_transfer = zip321::TransactionRequest::new(vec![Payment::without_memo( + P0::random_address(&mut st.rng).to_zcash_address(&st.network()), + transfer_amount, + )]) + .unwrap(); + let res = st + .spend( + &input_selector, + account.usk(), + p0_transfer, + OvkPolicy::Sender, + NonZeroU32::new(1).unwrap(), + ) + .unwrap(); + st.generate_next_block_including(*res.first()); + + let expected_fee = NonNegativeAmount::const_from_u64(10000); + let expected_change = (note_value - transfer_amount - expected_fee).unwrap(); + assert_eq!( + st.get_total_balance(acct_id), + ((note_value * 2).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 = zip321::TransactionRequest::new(vec![ + Payment::without_memo( + P0::random_address(&mut st.rng).to_zcash_address(&st.network()), + transfer_amount, + ), + Payment::without_memo( + P1::random_address(&mut st.rng).to_zcash_address(&st.network()), + transfer_amount, + ), + ]) + .unwrap(); + let res = st + .spend( + &input_selector, + 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 * 3).unwrap() + - (expected_fee * 3).unwrap()) + .unwrap(); + assert_eq!(st.get_total_balance(acct_id), expected_final); + + use incrementalmerkletree::Position; + let expected_checkpoints_p0: Vec<(BlockHeight, ShieldedProtocol, 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), + P0::SHIELDED_PROTOCOL, + pos.map(Position::from), + ) + }) + .collect(); + + let expected_checkpoints_p1: Vec<(BlockHeight, ShieldedProtocol, 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), + P1::SHIELDED_PROTOCOL, + pos.map(Position::from), + ) + }) + .collect(); + + let actual_checkpoints = st.get_checkpoint_history().unwrap(); + + assert_eq!( + actual_checkpoints + .iter() + .filter(|(_, p, _)| p == &P0::SHIELDED_PROTOCOL) + .cloned() + .collect::>(), + expected_checkpoints_p0 + ); + assert_eq!( + actual_checkpoints + .iter() + .filter(|(_, p, _)| p == &P1::SHIELDED_PROTOCOL) + .cloned() + .collect::>(), + expected_checkpoints_p1 + ); +} + +#[cfg(feature = "orchard")] +pub(crate) fn multi_pool_checkpoints_with_pruning< + P0: ShieldedPoolTester, + P1: ShieldedPoolTester, +>() { + let mut st = TestBuilder::new() + .with_block_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(&mut st.rng); + let p1_fvk = P1::random_fvk(&mut st.rng); + + let note_value = NonNegativeAmount::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(crate) fn valid_chain_states() { + let mut st = TestBuilder::new() + .with_block_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, + NonNegativeAmount::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, + NonNegativeAmount::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(crate) fn invalid_chain_cache_disconnected() { + let mut st = TestBuilder::new() + .with_block_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, + NonNegativeAmount::const_from_u64(5), + ); + let (last_contiguous_height, _, _) = st.generate_next_block( + &dfvk, + AddressType::DefaultExternal, + NonNegativeAmount::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]), + &dfvk, + AddressType::DefaultExternal, + NonNegativeAmount::const_from_u64(8), + 2, + 2, + true, + ); + st.generate_next_block( + &dfvk, + AddressType::DefaultExternal, + NonNegativeAmount::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(crate) fn data_db_truncation() { + let mut st = TestBuilder::new() + .with_block_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 = NonNegativeAmount::const_from_u64(5); + let value2 = NonNegativeAmount::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.account_id(), 1), + (value + value2).unwrap() + ); + + // "Rewind" to height of last scanned block (this is a no-op) + st.wallet_mut() + .transactionally(|wdb| truncate_to_height(wdb.conn.0, &wdb.params, h + 1)) + .unwrap(); + + // Spendable balance should be unaltered + assert_eq!( + st.get_spendable_balance(account.account_id(), 1), + (value + value2).unwrap() + ); + + // Rewind so that one block is dropped + st.wallet_mut() + .transactionally(|wdb| truncate_to_height(wdb.conn.0, &wdb.params, h)) + .unwrap(); + + // Spendable balance should only contain the first received note; + // the rest should be pending. + assert_eq!(st.get_spendable_balance(account.account_id(), 1), value); + assert_eq!( + st.get_pending_shielded_balance(account.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.account_id(), 1), + (value + value2).unwrap() + ); +} + +pub(crate) fn scan_cached_blocks_allows_blocks_out_of_order() { + let mut st = TestBuilder::new() + .with_block_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 = NonNegativeAmount::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.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.account_id()), + NonNegativeAmount::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()), + NonNegativeAmount::const_from_u64(110_000), + )]) + .unwrap(); + + #[allow(deprecated)] + let input_selector = GreedyInputSelector::new( + standard::SingleOutputChangeStrategy::new( + StandardFeeRule::Zip317, + None, + T::SHIELDED_PROTOCOL, + ), + DustOutputPolicy::default(), + ); + assert_matches!( + st.spend( + &input_selector, + account.usk(), + req, + OvkPolicy::Sender, + NonZeroU32::new(1).unwrap(), + ), + Ok(_) + ); +} + +pub(crate) fn scan_cached_blocks_finds_received_notes() { + let mut st = TestBuilder::new() + .with_block_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 = NonNegativeAmount::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.account_id()), value); + + // Create a second fake CompactBlock sending more value to the address + let value2 = NonNegativeAmount::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.account_id()), + (value + value2).unwrap() + ); +} + +// TODO: This test can probably be entirely removed, as the following test duplicates it entirely. +pub(crate) fn scan_cached_blocks_finds_change_notes() { + let mut st = TestBuilder::new() + .with_block_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 = NonNegativeAmount::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.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 = NonNegativeAmount::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.account_id()), + (value - value2).unwrap() + ); +} + +pub(crate) fn scan_cached_blocks_detects_spends_out_of_order() { + let mut st = TestBuilder::new() + .with_block_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 = NonNegativeAmount::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 = NonNegativeAmount::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.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.account_id()), + (value - value2).unwrap() + ); +} diff --git a/zcash_client_sqlite/src/wallet.rs b/zcash_client_sqlite/src/wallet.rs index dbffcaadd2..5e6540d4f3 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,49 +64,238 @@ //! wallet. //! - `memo` the shielded memo associated with the output, if any. -use rusqlite::{named_params, OptionalExtension, ToSql}; -use std::collections::HashMap; +use incrementalmerkletree::Retention; +use rusqlite::{self, named_params, OptionalExtension}; +use secrecy::{ExposeSecret, SecretVec}; +use shardtree::{error::ShardTreeError, store::ShardStore, ShardTree}; +use zip32::fingerprint::SeedFingerprint; + +use std::collections::{HashMap, HashSet}; use std::convert::TryFrom; +use std::io::{self, Cursor}; +use std::num::NonZeroU32; +use std::ops::RangeInclusive; +use tracing::debug; +use zcash_address::ZcashAddress; +use zcash_client_backend::{ + data_api::{ + scanning::{ScanPriority, ScanRange}, + AccountBalance, AccountBirthday, AccountSource, BlockMetadata, Ratio, + SentTransactionOutput, WalletSummary, SAPLING_SHARD_HEIGHT, + }, + encoding::AddressCodec, + keys::UnifiedFullViewingKey, + wallet::{Note, NoteId, Recipient, WalletTx}, + PoolType, ShieldedProtocol, +}; +use zcash_keys::{ + address::{Address, Receiver, UnifiedAddress}, + keys::{ + AddressGenerationError, UnifiedAddressRequest, UnifiedIncomingViewingKey, + UnifiedSpendingKey, + }, +}; use zcash_primitives::{ block::BlockHash, consensus::{self, BlockHeight, BranchId, NetworkUpgrade, Parameters}, memo::{Memo, MemoBytes}, - sapling::CommitmentTree, - transaction::{components::Amount, Transaction, TxId}, - zip32::{ - sapling::{DiversifiableFullViewingKey, ExtendedFullViewingKey}, - AccountId, DiversifierIndex, + merkle_tree::read_commitment_tree, + transaction::{ + components::{amount::NonNegativeAmount, Amount}, + Transaction, TransactionData, TxId, }, }; - -use zcash_client_backend::{ - address::{RecipientAddress, UnifiedAddress}, - data_api::{PoolType, Recipient, SentTransactionOutput}, - keys::UnifiedFullViewingKey, - wallet::WalletTx, -}; +use zip32::{self, DiversifierIndex, Scope}; use crate::{ - error::SqliteClientError, prepared::InsertAddress, DataConnStmtCache, WalletDb, PRUNING_HEIGHT, + error::SqliteClientError, + wallet::commitment_tree::{get_max_checkpointed_height, SqliteShardStore}, + AccountId, SqlTransaction, WalletCommitmentTrees, WalletDb, DEFAULT_UA_REQUEST, PRUNING_DEPTH, + SAPLING_TABLES_PREFIX, }; +use self::scanning::{parse_priority_code, priority_code, replace_queue_entries}; + +#[cfg(feature = "orchard")] +use {crate::ORCHARD_TABLES_PREFIX, zcash_client_backend::data_api::ORCHARD_SHARD_HEIGHT}; + #[cfg(feature = "transparent-inputs")] use { crate::UtxoId, - rusqlite::{params, Connection}, + rusqlite::Row, std::collections::BTreeSet, - zcash_client_backend::{ - address::AddressMetadata, encoding::AddressCodec, wallet::WalletTransparentOutput, - }, + zcash_address::unified::{Encoding, Ivk, Uivk}, + zcash_client_backend::wallet::{TransparentAddressMetadata, WalletTransparentOutput}, zcash_primitives::{ - legacy::{keys::IncomingViewingKey, Script, TransparentAddress}, + legacy::{ + keys::{IncomingViewingKey, NonHardenedChildIndex}, + Script, TransparentAddress, + }, transaction::components::{OutPoint, TxOut}, }, }; +pub mod commitment_tree; +pub(crate) mod common; 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) const BLOCK_SAPLING_FRONTIER_ABSENT: &[u8] = &[0x0]; + +fn parse_account_source( + account_kind: u32, + hd_seed_fingerprint: Option<[u8; 32]>, + hd_account_index: Option, +) -> Result { + match (account_kind, hd_seed_fingerprint, hd_account_index) { + (0, Some(seed_fp), Some(account_index)) => Ok(AccountSource::Derived { + seed_fingerprint: SeedFingerprint::from_bytes(seed_fp), + account_index: zip32::AccountId::try_from(account_index).map_err(|_| { + SqliteClientError::CorruptedData( + "ZIP-32 account ID from wallet DB is out of range.".to_string(), + ) + })?, + }), + (1, None, None) => Ok(AccountSource::Imported), + (0, None, None) | (1, Some(_), Some(_)) => Err(SqliteClientError::CorruptedData( + "Wallet DB account_kind constraint violated".to_string(), + )), + (_, _, _) => Err(SqliteClientError::CorruptedData( + "Unrecognized account_kind".to_string(), + )), + } +} + +fn account_kind_code(value: AccountSource) -> u32 { + match value { + AccountSource::Derived { .. } => 0, + AccountSource::Imported => 1, + } +} + +/// The viewing key that an [`Account`] has available to it. +#[derive(Debug, Clone)] +pub(crate) enum ViewingKey { + /// A full viewing key. + /// + /// This is available to derived accounts, as well as accounts directly imported as + /// full viewing keys. + Full(Box), + + /// An incoming viewing key. + /// + /// Accounts that have this kind of viewing key cannot be used in wallet contexts, + /// because they are unable to maintain an accurate balance. + Incoming(Box), +} + +/// An account stored in a `zcash_client_sqlite` database. +#[derive(Debug, Clone)] +pub struct Account { + account_id: AccountId, + kind: AccountSource, + viewing_key: ViewingKey, +} + +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> { + match &self.viewing_key { + ViewingKey::Full(ufvk) => ufvk.default_address(request), + ViewingKey::Incoming(uivk) => uivk.default_address(request), + } + } +} + +impl zcash_client_backend::data_api::Account for Account { + fn id(&self) -> AccountId { + self.account_id + } + + 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; + + // Keys are not comparable with `Eq`, but addresses are, so we derive what should + // be equivalent addresses for each key and use those to check for key equality. + let uivk_match = + match UnifiedSpendingKey::from_seed(params, &seed.expose_secret()[..], account_index) { + // If we can't derive a USK from the given seed with the account's ZIP 32 + // account index, then we immediately know the UIVK won't match because wallet + // accounts are required to have a known UIVK. + Err(_) => false, + Ok(usk) => UnifiedAddressRequest::all().map_or( + Ok::<_, SqliteClientError>(false), + |ua_request| { + Ok(usk + .to_unified_full_viewing_key() + .default_address(ua_request)? + == uivk.default_address(ua_request)?) + }, + )?, + }; + + 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) + } +} pub(crate) fn pool_code(pool_type: PoolType) -> i64 { // These constants are *incidentally* shared with the typecodes @@ -114,66 +303,243 @@ pub(crate) fn pool_code(pool_type: PoolType) -> i64 { // implementation detail. match pool_type { PoolType::Transparent => 0i64, - PoolType::Sapling => 2i64, + PoolType::Shielded(ShieldedProtocol::Sapling) => 2i64, + PoolType::Shielded(ShieldedProtocol::Orchard) => 3i64, } } -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) +pub(crate) fn scope_code(scope: Scope) -> i64 { + match scope { + Scope::External => 0i64, + Scope::Internal => 1i64, + } } -pub(crate) fn add_account( - wdb: &WalletDb

, - account: AccountId, - key: &UnifiedFullViewingKey, -) -> Result<(), SqliteClientError> { - add_account_internal(&wdb.conn, &wdb.params, "accounts", account, key) +pub(crate) fn parse_scope(code: i64) -> Option { + match code { + 0i64 => Some(Scope::External), + 1i64 => Some(Scope::Internal), + _ => None, + } +} + +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() + } + }) } -pub(crate) fn add_account_internal>( +// Returns the highest used account index for a given seed. +pub(crate) fn max_zip32_account_index( 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], + 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| { + let account_id: Option = row.get(0)?; + account_id + .map(zip32::AccountId::try_from) + .transpose() + .map_err(|_| SqliteClientError::AccountIdOutOfRange) + }, + ) +} + +pub(crate) fn add_account( + conn: &rusqlite::Transaction, + params: &P, + kind: AccountSource, + viewing_key: ViewingKey, + birthday: &AccountBirthday, +) -> Result { + let (hd_seed_fingerprint, hd_account_index) = match kind { + AccountSource::Derived { + seed_fingerprint, + account_index, + } => (Some(seed_fingerprint), Some(account_index)), + AccountSource::Imported => (None, None), + }; + + let orchard_item = viewing_key + .ufvk() + .and_then(|ufvk| ufvk.orchard().map(|k| k.to_bytes())); + 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 account_id: AccountId = conn.query_row( + r#" + INSERT INTO accounts ( + 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_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 + ) + RETURNING id; + "#, + named_params![ + ":account_kind": account_kind_code(kind), + ":hd_seed_fingerprint": hd_seed_fingerprint.as_ref().map(|fp| fp.to_bytes()), + ":hd_account_index": hd_account_index.map(u32::from), + ":ufvk": viewing_key.ufvk().map(|ufvk| ufvk.encode(params)), + ":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) + ], + |row| Ok(AccountId(row.get(0)?)), )?; + let account = Account { + account_id, + kind, + viewing_key, + }; + + // If a birthday frontier is available, insert it into the note commitment tree. If the + // birthday frontier is the empty frontier, we don't need to do anything. + if let Some(frontier) = birthday.sapling_frontier().value() { + debug!("Inserting Sapling frontier into ShardTree: {:?}", frontier); + let shard_store = + SqliteShardStore::<_, ::sapling::Node, SAPLING_SHARD_HEIGHT>::from_connection( + conn, + SAPLING_TABLES_PREFIX, + )?; + let mut shard_tree: ShardTree< + _, + { ::sapling::NOTE_COMMITMENT_TREE_DEPTH }, + SAPLING_SHARD_HEIGHT, + > = ShardTree::new(shard_store, PRUNING_DEPTH.try_into().unwrap()); + shard_tree.insert_frontier_nodes( + frontier.clone(), + Retention::Checkpoint { + // This subtraction is safe, because all leaves in the tree appear in blocks, and + // the invariant that birthday.height() always corresponds to the block for which + // `frontier` is the tree state at the start of the block. Together, this means + // there exists a prior block for which frontier is the tree state at the end of + // the block. + id: birthday.height() - 1, + is_marked: false, + }, + )?; + } + + #[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, + is_marked: false, + }, + )?; + } + + // 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) = scan_queue_extrema(conn)?.map(|range| *range.end()) { + 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. - let (address, d_idx) = key.default_address(); - InsertAddress::new(conn)?.execute(network, account, d_idx, &address)?; + let (address, d_idx) = account.default_address(DEFAULT_UA_REQUEST)?; + insert_address(conn, params, account_id, d_idx, &address)?; - Ok(()) + Ok(account_id) } pub(crate) fn get_current_address( - wdb: &WalletDb

, - account: AccountId, + conn: &rusqlite::Connection, + params: &P, + account_id: AccountId, ) -> Result, SqliteClientError> { // This returns the most recently generated address. - let addr: Option<(String, Vec)> = wdb - .conn + let addr: Option<(String, Vec)> = conn .query_row( "SELECT address, diversifier_index_be - FROM addresses WHERE account = :account + FROM addresses WHERE account_id = :account_id ORDER BY diversifier_index_be DESC LIMIT 1", - named_params![":account": &u32::from(account)], + named_params![":account_id": account_id.0], |row| Ok((row.get(0)?, row.get(1)?)), ) .optional()?; @@ -184,51 +550,90 @@ pub(crate) fn get_current_address( })?; di_be.reverse(); - RecipientAddress::decode(&wdb.params, &addr_str) + 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, DiversifierIndex::from(di_be))) }) .transpose() } +/// Adds the given address and diversifier index to the addresses table. +/// +/// Returns the database row for the newly-inserted address. +pub(crate) 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_id, + 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": account.0, + ":diversifier_index_be": &di_be[..], + ":address": &address.encode(params), + ":cached_transparent_receiver_address": &address.transparent().map(|r| r.encode(params)), + ])?; + + Ok(()) +} + #[cfg(feature = "transparent-inputs")] pub(crate) fn get_transparent_receivers( + conn: &rusqlite::Connection, params: &P, - conn: &Connection, account: AccountId, -) -> Result, SqliteClientError> { - let mut ret = HashMap::new(); +) -> Result>, SqliteClientError> { + let mut ret: HashMap> = HashMap::new(); // Get all UAs derived - let mut ua_query = conn - .prepare("SELECT address, diversifier_index_be FROM addresses WHERE account = :account")?; - let mut rows = ua_query.query(named_params![":account": &u32::from(account)])?; + let mut ua_query = conn.prepare( + "SELECT address, diversifier_index_be FROM addresses WHERE account_id = :account", + )?; + let mut rows = ua_query.query(named_params![":account": account.0])?; 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(|_| { + let mut di: [u8; 11] = di_vec.try_into().map_err(|_| { SqliteClientError::CorruptedData( "Diverisifier index is not an 11-byte value".to_owned(), ) })?; - di_be.reverse(); + di.reverse(); // BE -> LE conversion - let ua = RecipientAddress::decode(params, &ua_str) + 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 { - RecipientAddress::Unified(ua) => Ok(ua), + Address::Unified(ua) => Ok(ua), _ => Err(SqliteClientError::CorruptedData(format!( "Addresses table contains {} which is not a unified address", ua_str, @@ -236,16 +641,37 @@ pub(crate) fn get_transparent_receivers( })?; if let Some(taddr) = ua.transparent() { + let index = NonHardenedChildIndex::from_index( + DiversifierIndex::from(di).try_into().map_err(|_| { + SqliteClientError::CorruptedData( + "Unable to get diversifier for transparent address.".to_string(), + ) + })?, + ) + .ok_or_else(|| { + SqliteClientError::CorruptedData( + "Unexpected hardened index for transparent address.".to_string(), + ) + })?; + ret.insert( *taddr, - AddressMetadata::new(account, DiversifierIndex(di_be)), + Some(TransparentAddressMetadata::new( + Scope::External.into(), + index, + )), ); } } - if let Some((taddr, diversifier_index)) = get_legacy_transparent_address(params, conn, account)? - { - ret.insert(taddr, AddressMetadata::new(account, diversifier_index)); + if let Some((taddr, child_index)) = get_legacy_transparent_address(params, conn, account)? { + ret.insert( + taddr, + Some(TransparentAddressMetadata::new( + Scope::External.into(), + child_index, + )), + ); } Ok(ret) @@ -254,61 +680,67 @@ pub(crate) fn get_transparent_receivers( #[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 + conn: &rusqlite::Connection, + account_id: AccountId, +) -> Result, SqliteClientError> { + use zcash_address::unified::Container; + use zcash_primitives::legacy::keys::ExternalIvk; + + // Get the UIVK for the account. + let uivk_str: Option = conn .query_row( - "SELECT ufvk FROM accounts WHERE account = :account", - [u32::from(account)], + "SELECT uivk FROM accounts WHERE id = :account", + [account_id.0], |row| row.get(0), ) .optional()?; - if let Some(ufvk_str) = ufvk_str { - let ufvk = UnifiedFullViewingKey::decode(params, &ufvk_str) - .map_err(SqliteClientError::CorruptedData)?; + if let Some(uivk_str) = uivk_str { + let (network, uivk) = Uivk::decode(&uivk_str) + .map_err(|e| SqliteClientError::CorruptedData(format!("Unable to parse UIVK: {e}")))?; + if params.network_type() != network { + return Err(SqliteClientError::CorruptedData( + "Network type mismatch".to_owned(), + )); + } // 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) + 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) } /// Returns the [`UnifiedFullViewingKey`]s for the wallet. pub(crate) fn get_unified_full_viewing_keys( - wdb: &WalletDb

, + 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 id, 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((AccountId(acct), ufvk))) + } else { + Ok(None) + } })?; 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) @@ -317,119 +749,679 @@ pub(crate) fn get_unified_full_viewing_keys( /// Returns the account id corresponding to a given [`UnifiedFullViewingKey`], /// if any. pub(crate) fn get_account_for_ufvk( - wdb: &WalletDb

, + conn: &rusqlite::Connection, + params: &P, ufvk: &UnifiedFullViewingKey, -) -> Result, SqliteClientError> { - wdb.conn - .query_row( - "SELECT account FROM accounts WHERE ufvk = ?", - [&ufvk.encode(&wdb.params)], +) -> Result, SqliteClientError> { + #[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, account_kind, hd_seed_fingerprint, hd_account_index, ufvk + 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": ufvk.orchard().map(|k| k.to_bytes()), + ":sapling_fvk_item_cache": ufvk.sapling().map(|k| k.to_bytes()), + ":p2pkh_fvk_item_cache": transparent_item, + ], |row| { - let acct: u32 = row.get(0)?; - Ok(AccountId::from(acct)) + let account_id = row.get::<_, u32>(0).map(AccountId)?; + let kind = parse_account_source(row.get(1)?, row.get(2)?, row.get(3)?)?; + + // We looked up the account by FVK components, so the UFVK column must be + // non-null. + let ufvk_str: String = row.get(4)?; + let viewing_key = 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_id, e + )) + })?, + )); + + Ok(Account { + account_id, + kind, + viewing_key, + }) }, - ) - .optional() - .map_err(SqliteClientError::from) + )? + .collect::, _>>()?; + + if accounts.len() > 1 { + Err(SqliteClientError::CorruptedData( + "Mutiple account records matched the provided UFVK".to_owned(), + )) + } else { + Ok(accounts.into_iter().next()) + } } -/// 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) +/// 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: &SeedFingerprint, + account_index: zip32::AccountId, +) -> Result, SqliteClientError> { + let mut stmt = conn.prepare( + "SELECT id, ufvk + FROM accounts + WHERE hd_seed_fingerprint = :hd_seed_fingerprint + AND hd_account_index = :account_id", + )?; + + let mut accounts = stmt.query_and_then::<_, SqliteClientError, _, _>( + named_params![ + ":hd_seed_fingerprint": seed.to_bytes(), + ":hd_account_index": u32::from(account_index), + ], + |row| { + let account_id = row.get::<_, u32>(0).map(AccountId)?; + let ufvk = match row.get::<_, Option>(1)? { + None => Err(SqliteClientError::CorruptedData(format!( + "Missing unified full viewing key for derived account {:?}", + account_id, + ))), + Some(ufvk_str) => UnifiedFullViewingKey::decode(params, &ufvk_str).map_err(|e| { + SqliteClientError::CorruptedData(format!( + "Could not decode unified full viewing key for account {:?}: {}", + account_id, e + )) + }), + }?; + Ok(Account { + account_id, + kind: AccountSource::Derived { + seed_fingerprint: *seed, + account_index, + }, + viewing_key: ViewingKey::Full(Box::new(ufvk)), }) - }) - .optional() - .map_err(SqliteClientError::from) - .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()) + }, + )?; + + accounts.next().transpose() +} + +pub(crate) trait ScanProgress { + fn sapling_scan_progress( + &self, + conn: &rusqlite::Connection, + birthday_height: BlockHeight, + fully_scanned_height: BlockHeight, + chain_tip_height: BlockHeight, + ) -> Result>, SqliteClientError>; + + #[cfg(feature = "orchard")] + fn orchard_scan_progress( + &self, + conn: &rusqlite::Connection, + birthday_height: BlockHeight, + fully_scanned_height: BlockHeight, + chain_tip_height: BlockHeight, + ) -> Result>, SqliteClientError>; +} + +#[derive(Debug)] +pub(crate) struct SubtreeScanProgress; + +impl ScanProgress for SubtreeScanProgress { + #[tracing::instrument(skip(conn))] + fn sapling_scan_progress( + &self, + conn: &rusqlite::Connection, + birthday_height: BlockHeight, + fully_scanned_height: BlockHeight, + chain_tip_height: BlockHeight, + ) -> Result>, SqliteClientError> { + if fully_scanned_height == chain_tip_height { + // Compute the total blocks scanned since the wallet birthday + conn.query_row( + "SELECT SUM(sapling_output_count) + FROM blocks + WHERE height >= :birthday_height", + named_params![":birthday_height": u32::from(birthday_height)], + |row| { + let scanned = row.get::<_, Option>(0)?; + Ok(scanned.map(|n| Ratio::new(n, n))) + }, + ) + .map_err(SqliteClientError::from) + } else { + // Get the starting note commitment tree size from the wallet birthday, or failing that + // from the blocks table. + let start_size = conn + .query_row( + "SELECT birthday_sapling_tree_size + FROM accounts + WHERE birthday_height = :birthday_height", + named_params![":birthday_height": u32::from(birthday_height)], + |row| row.get::<_, Option>(0), + ) + .optional()? + .flatten() + .map(Ok) + .or_else(|| { + conn.query_row( + "SELECT MAX(sapling_commitment_tree_size - sapling_output_count) + FROM blocks + WHERE height <= :start_height", + named_params![":start_height": u32::from(birthday_height)], + |row| row.get::<_, Option>(0), + ) + .optional() + .map(|opt| opt.flatten()) + .transpose() }) - } else { - Ok(false) - } - }) + .transpose()?; + + // Compute the total blocks scanned so far above the starting height + let scanned_count = conn.query_row( + "SELECT SUM(sapling_output_count) + FROM blocks + WHERE height > :start_height", + named_params![":start_height": u32::from(birthday_height)], + |row| row.get::<_, Option>(0), + )?; + + // We don't have complete information on how many outputs will exist in the shard at + // the chain tip without having scanned the chain tip block, so we overestimate by + // computing the maximum possible number of notes directly from the shard indices. + // + // 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. + Ok(conn + .query_row( + "SELECT MIN(shard_index), MAX(shard_index) + FROM sapling_tree_shards + WHERE subtree_end_height > :start_height + OR subtree_end_height IS NULL", + named_params![":start_height": u32::from(birthday_height)], + |row| { + let min_tree_size = row + .get::<_, Option>(0)? + .map(|min_idx| min_idx << SAPLING_SHARD_HEIGHT); + let max_tree_size = row + .get::<_, Option>(1)? + .map(|max_idx| (max_idx + 1) << SAPLING_SHARD_HEIGHT); + Ok(start_size.or(min_tree_size).zip(max_tree_size).map( + |(min_tree_size, max_tree_size)| { + Ratio::new( + scanned_count.unwrap_or(0), + max_tree_size - min_tree_size, + ) + }, + )) + }, + ) + .optional()? + .flatten()) + } + } + + #[cfg(feature = "orchard")] + #[tracing::instrument(skip(conn))] + fn orchard_scan_progress( + &self, + conn: &rusqlite::Connection, + birthday_height: BlockHeight, + fully_scanned_height: BlockHeight, + chain_tip_height: BlockHeight, + ) -> Result>, SqliteClientError> { + if fully_scanned_height == chain_tip_height { + // Compute the total blocks scanned since the wallet birthday + conn.query_row( + "SELECT SUM(orchard_action_count) + FROM blocks + WHERE height >= :birthday_height", + named_params![":birthday_height": u32::from(birthday_height)], + |row| { + let scanned = row.get::<_, Option>(0)?; + Ok(scanned.map(|n| Ratio::new(n, n))) + }, + ) + .map_err(SqliteClientError::from) + } else { + // Compute the starting number of notes directly from the blocks table + let start_size = conn + .query_row( + "SELECT birthday_orchard_tree_size + FROM accounts + WHERE birthday_height = :birthday_height", + named_params![":birthday_height": u32::from(birthday_height)], + |row| row.get::<_, Option>(0), + ) + .optional()? + .flatten() + .map(Ok) + .or_else(|| { + conn.query_row( + "SELECT MAX(orchard_commitment_tree_size - orchard_action_count) + FROM blocks + WHERE height <= :start_height", + named_params![":start_height": u32::from(birthday_height)], + |row| row.get::<_, Option>(0), + ) + .optional() + .map(|opt| opt.flatten()) + .transpose() + }) + .transpose()?; + + // Compute the total blocks scanned so far above the starting height + let scanned_count = conn.query_row( + "SELECT SUM(orchard_action_count) + FROM blocks + WHERE height > :start_height", + named_params![":start_height": u32::from(birthday_height)], + |row| row.get::<_, Option>(0), + )?; + + // We don't have complete information on how many actions will exist in the shard at + // the chain tip without having scanned the chain tip block, so we overestimate by + // computing the maximum possible number of notes directly from the shard indices. + // + // 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. + Ok(conn + .query_row( + "SELECT MIN(shard_index), MAX(shard_index) + FROM orchard_tree_shards + WHERE subtree_end_height > :start_height + OR subtree_end_height IS NULL", + named_params![":start_height": u32::from(birthday_height)], + |row| { + let min_tree_size = row + .get::<_, Option>(0)? + .map(|min_idx| min_idx << ORCHARD_SHARD_HEIGHT); + let max_tree_size = row + .get::<_, Option>(1)? + .map(|max_idx| (max_idx + 1) << ORCHARD_SHARD_HEIGHT); + Ok(start_size.or(min_tree_size).zip(max_tree_size).map( + |(min_tree_size, max_tree_size)| { + Ratio::new( + scanned_count.unwrap_or(0), + max_tree_size - min_tree_size, + ) + }, + )) + }, + ) + .optional()? + .flatten()) + } + } } -/// Returns the balance for the account, including all mined unspent notes that we know -/// about. +/// Returns the spendable balance for the account at the specified height. /// -/// 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)), +/// 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 shielded notes. 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 ScanProgress, +) -> Result>, SqliteClientError> { + let chain_tip_height = match scan_queue_extrema(tx)? { + Some(range) => *range.end(), + None => { + return Ok(None); + } + }; + + let birthday_height = + wallet_birthday(tx)?.expect("If a scan range exists, we know the wallet birthday."); + + let fully_scanned_height = + block_fully_scanned(tx, params)?.map_or(birthday_height - 1, |m| m.block_height()); + let summary_height = (chain_tip_height + 1).saturating_sub(std::cmp::max(min_confirmations, 1)); + + let sapling_scan_progress = progress.sapling_scan_progress( + tx, + birthday_height, + fully_scanned_height, + chain_tip_height, )?; - 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_scan_progress = progress.orchard_scan_progress( + tx, + birthday_height, + fully_scanned_height, + chain_tip_height, + )?; + #[cfg(not(feature = "orchard"))] + let orchard_scan_progress: Option> = None; + + // Treat Sapling and Orchard outputs as having the same cost to scan. + let scan_progress = sapling_scan_progress + .zip(orchard_scan_progress) + .map(|(s, o)| { + Ratio::new( + s.numerator() + o.numerator(), + s.denominator() + o.denominator(), + ) + }) + .or(sapling_scan_progress) + .or(orchard_scan_progress); + + let mut stmt_accounts = tx.prepare_cached("SELECT id FROM accounts")?; + let mut account_balances = stmt_accounts + .query([])? + .and_then(|row| { + Ok::<_, SqliteClientError>((AccountId(row.get::<_, u32>(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, + NonNegativeAmount, + NonNegativeAmount, + NonNegativeAmount, + ) -> Result<(), SqliteClientError>, + { + // If the shard containing the summary height contains any unscanned ranges that start below or + // including that height, none of our 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 n.account_id, n.value, n.is_change, scan_state.max_priority, t.block + FROM {table_prefix}_received_notes n + 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 = AccountId(row.get::<_, u32>(0)?); + + let value_raw = row.get::<_, i64>(1)?; + let value = NonNegativeAmount::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 = NonNegativeAmount::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(()) } -} -/// 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(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); - 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 = "transparent-inputs")] + { + let transparent_trace = tracing::info_span!("stmt_transparent_balances").entered(); + let zero_conf_height = (chain_tip_height + 1).saturating_sub(min_confirmations); + let stable_height = chain_tip_height.saturating_sub(PRUNING_DEPTH); + + let mut stmt_transparent_balances = tx.prepare( + "SELECT u.received_by_account_id, SUM(u.value_zat) + FROM utxos u + WHERE u.height <= :max_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.block IS NOT NULL -- the spending tx is mined + OR tx.expiry_height IS NULL -- the spending tx will not expire + OR tx.expiry_height > :stable_height -- the spending tx is unexpired + ) + GROUP BY u.received_by_account_id", + )?; + let mut rows = stmt_transparent_balances.query(named_params![ + ":max_height": u32::from(zero_conf_height), + ":stable_height": u32::from(stable_height) + ])?; + + while let Some(row) = rows.next()? { + let account = AccountId(row.get(0)?); + let raw_value = row.get(1)?; + let value = NonNegativeAmount::from_nonnegative_i64(raw_value).map_err(|_| { + SqliteClientError::CorruptedData(format!("Negative UTXO value {:?}", raw_value)) + })?; + + if let Some(balances) = account_balances.get_mut(&account) { + balances.add_unshielded_value(value)?; + } + } + drop(transparent_trace); } + + // 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, + scan_progress, + next_sapling_subtree_index, + #[cfg(feature = "orchard")] + next_orchard_subtree_index, + ); + + Ok(Some(summary)) } -/// 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, +/// 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 memo_bytes: Option> = wdb.conn.query_row( - "SELECT memo FROM sapling_received_notes - WHERE id_note = ?", - [id_note], - |row| row.get(0), - )?; + 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::Shielded( + ShieldedProtocol::Orchard, + ))) + } + }; memo_bytes .map(|b| { @@ -440,42 +1432,134 @@ pub(crate) fn get_received_memo

( .transpose() } -/// Looks up a transaction by its internal database identifier. +/// 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( - 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], + 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: u32 = row.get(1)?; - Ok((row.get(0)?, BlockHeight::from(h))) + 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), + )) }, - )?; - - Transaction::read( - &tx_bytes[..], - BranchId::for_height(&wdb.params, block_height), ) - .map_err(SqliteClientError::from) + .optional()? + .map(|(tx_bytes, block_height, expiry_height)| { + // We need to provide a consensus branch ID so that pre-v5 `Transaction` structs + // (which don't commit directly to one) can store it internally. + // - If the transaction is mined, we use the block height to get the correct one. + // - If the transaction is unmined and has a cached non-zero expiry height, we use + // that (relying on the invariant that a transaction can't be mined across a network + // upgrade boundary, so the expiry height must be in the same epoch). + // - Otherwise, we use a placeholder for the initial transaction parse (as the + // consensus branch ID is not used there), and then either use its non-zero expiry + // height or return an error. + if let Some(height) = + block_height.or_else(|| expiry_height.filter(|h| h > &BlockHeight::from(0))) + { + Transaction::read(&tx_bytes[..], BranchId::for_height(params, height)) + .map(|t| (height, t)) + .map_err(SqliteClientError::from) + } else { + let tx_data = Transaction::read(&tx_bytes[..], BranchId::Sprout) + .map_err(SqliteClientError::from)? + .into_data(); + + let expiry_height = tx_data.expiry_height(); + if expiry_height > BlockHeight::from(0) { + TransactionData::from_parts( + tx_data.version(), + BranchId::for_height(params, expiry_height), + tx_data.lock_time(), + expiry_height, + tx_data.transparent_bundle().cloned(), + tx_data.sprout_bundle().cloned(), + tx_data.sapling_bundle().cloned(), + tx_data.orchard_bundle().cloned(), + ) + .freeze() + .map(|t| (expiry_height, t)) + .map_err(SqliteClientError::from) + } else { + Err(SqliteClientError::CorruptedData( + "Consensus branch ID not known, cannot parse this transaction until it is mined" + .to_string(), + )) + } + } + }) + .transpose() } -/// 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, +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> = wdb.conn.query_row( - "SELECT memo FROM sent_notes - WHERE id_note = ?", - [id_note], - |row| row.get(0), - )?; + 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| { @@ -486,75 +1570,412 @@ pub(crate) fn get_sent_memo

( .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: AccountId, +) -> Result { + conn.query_row( + "SELECT birthday_height + FROM accounts + WHERE id = :account_id", + named_params![":account_id": account.0], + |row| row.get::<_, u32>(0).map(BlockHeight::from), + ) + .optional() + .map_err(SqliteClientError::from) + .and_then(|opt| opt.ok_or(SqliteClientError::AccountUnknown)) +} + /// Returns the minimum and maximum heights for blocks stored in the wallet database. -pub(crate) fn block_height_extrema

( - wdb: &WalletDb

, +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( + conn: &rusqlite::Connection, + params: &P, + account_id: AccountId, +) -> Result, SqliteClientError> { + let mut sql = conn.prepare_cached( + r#" + SELECT account_kind, hd_seed_fingerprint, hd_account_index, ufvk, uivk + FROM accounts + WHERE id = :account_id + "#, + )?; + + let mut result = sql.query(named_params![":account_id": account_id.0])?; + let row = result.next()?; + match row { + Some(row) => { + let kind = parse_account_source( + row.get("account_kind")?, + row.get("hd_seed_fingerprint")?, + row.get("hd_account_index")?, + )?; + + 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(SqliteClientError::BadAccountData)?, + )) + } else { + let uivk_str: String = row.get("uivk")?; + ViewingKey::Incoming(Box::new( + UnifiedIncomingViewingKey::decode(params, &uivk_str[..]) + .map_err(SqliteClientError::BadAccountData)?, + )) + }; + + Ok(Some(Account { + account_id, + kind, + viewing_key, + })) + } + None => Ok(None), + } +} + +/// Returns the minimum and maximum heights of blocks in the chain which may be scanned. +pub(crate) fn scan_queue_extrema( + conn: &rusqlite::Connection, +) -> Result>, rusqlite::Error> { + conn.query_row( + "SELECT MIN(block_range_start), MAX(block_range_end) FROM scan_queue", + [], + |row| { + let min_height: Option = row.get(0)?; + let max_height: Option = row.get(1)?; + + // Scan ranges are end-exclusive, so we subtract 1 from `max_height` to obtain the + // height of the last known chain tip; + Ok(min_height + .zip(max_height.map(|h| h.saturating_sub(1))) + .map(|(min, max)| RangeInclusive::new(min.into(), max.into()))) + }, + ) +} + +pub(crate) fn get_target_and_anchor_heights( + conn: &rusqlite::Connection, + min_confirmations: NonZeroU32, ) -> 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)) + match scan_queue_extrema(conn)?.map(|range| *range.end()) { + Some(chain_tip_height) => { + let sapling_anchor_height = get_max_checkpointed_height( + conn, + SAPLING_TABLES_PREFIX, + chain_tip_height, + min_confirmations, + )?; + + #[cfg(feature = "orchard")] + let orchard_anchor_height = get_max_checkpointed_height( + conn, + ORCHARD_TABLES_PREFIX, + chain_tip_height, + min_confirmations, + )?; + + #[cfg(not(feature = "orchard"))] + let orchard_anchor_height: Option = None; + + let anchor_height = sapling_anchor_height + .zip(orchard_anchor_height) + .map(|(s, o)| std::cmp::min(s, o)) + .or(sapling_anchor_height) + .or(orchard_anchor_height); + + Ok(anchor_height.map(|h| (chain_tip_height + 1, h))) + } + None => Ok(None), + } +} + +fn parse_block_metadata( + _params: &P, + row: (BlockHeight, Vec, Option, Vec, Option), +) -> Result { + let (block_height, hash_data, sapling_tree_size_opt, sapling_tree, _orchard_tree_size_opt) = + row; + let sapling_tree_size = sapling_tree_size_opt.map_or_else(|| { + if sapling_tree == BLOCK_SAPLING_FRONTIER_ABSENT { + Err(SqliteClientError::CorruptedData("One of either the Sapling tree size or the legacy Sapling commitment tree must be present.".to_owned())) + } else { + // parse the legacy commitment tree data + read_commitment_tree::< + ::sapling::Node, + _, + { ::sapling::NOTE_COMMITMENT_TREE_DEPTH }, + >(Cursor::new(sapling_tree)) + .map(|tree| tree.size().try_into().unwrap()) + .map_err(SqliteClientError::from) + } + }, Ok)?; + + let block_hash = BlockHash::try_from_slice(&hash_data).ok_or_else(|| { + SqliteClientError::from(io::Error::new( + io::ErrorKind::InvalidData, + format!("Invalid block hash length: {}", hash_data.len()), + )) + })?; + + Ok(BlockMetadata::from_parts( + block_height, + block_hash, + Some(sapling_tree_size), + #[cfg(feature = "orchard")] + if _params + .activation_height(NetworkUpgrade::Nu5) + .iter() + .any(|nu5_activation| &block_height >= nu5_activation) + { + _orchard_tree_size_opt + } else { + Some(0) + }, + )) +} + +#[tracing::instrument(skip(conn, params))] +pub(crate) fn block_metadata( + conn: &rusqlite::Connection, + params: &P, + block_height: BlockHeight, +) -> Result, SqliteClientError> { + conn.query_row( + "SELECT height, hash, sapling_commitment_tree_size, sapling_tree, orchard_commitment_tree_size + FROM blocks + WHERE height = :block_height", + named_params![":block_height": u32::from(block_height)], + |row| { + let height: u32 = row.get(0)?; + let block_hash: Vec = row.get(1)?; + let sapling_tree_size: Option = row.get(2)?; + let sapling_tree: Vec = row.get(3)?; + let orchard_tree_size: Option = row.get(4)?; + Ok(( + BlockHeight::from(height), + block_hash, + sapling_tree_size, + sapling_tree, + orchard_tree_size, + )) + }, + ) + .optional() + .map_err(SqliteClientError::from) + .and_then(|meta_row| meta_row.map(|r| parse_block_metadata(params, r)).transpose()) +} + +#[tracing::instrument(skip_all)] +pub(crate) fn block_fully_scanned( + conn: &rusqlite::Connection, + params: &P, +) -> Result, SqliteClientError> { + if let Some(birthday_height) = wallet_birthday(conn)? { + // We assume that the only way we get a contiguous range of block heights in the `blocks` table + // starting with the birthday block, is if all scanning operations have been performed on those + // blocks. This holds because the `blocks` table is only altered by `WalletDb::put_blocks` via + // `put_block`, and the effective combination of intra-range linear scanning and the nullifier + // map ensures that we discover all wallet-related information within the contiguous range. + // + // We also assume that every contiguous range of block heights in the `blocks` table has a + // single matching entry in the `scan_queue` table with priority "Scanned". This requires no + // bugs in the scan queue update logic, which we have had before. However, a bug here would + // mean that we return a more conservative fully-scanned height, which likely just causes a + // performance regression. + // + // The fully-scanned height is therefore the last height that falls within the first range in + // the scan queue with priority "Scanned". + // SQL query problems. + 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)], + |row| { + let block_range_start = BlockHeight::from_u32(row.get(0)?); + let block_range_end = BlockHeight::from_u32(row.get(1)?); + + // If the start of the earliest scanned range is greater than + // the birthday height, then there is an unscanned range between + // the wallet birthday and that range, so there is no fully + // scanned height. + Ok(if block_range_start <= birthday_height { + // Scan ranges are end-exclusive. + Some(block_range_end - 1) + } else { + None + }) + }, + ) + .optional()? + { + Some(Some(h)) => h, + _ => return Ok(None), + }; + + block_metadata(conn, params, fully_scanned_height) + } else { + Ok(None) + } +} + +pub(crate) fn block_max_scanned( + conn: &rusqlite::Connection, + params: &P, +) -> Result, SqliteClientError> { + conn.query_row( + "SELECT blocks.height, hash, sapling_commitment_tree_size, sapling_tree, orchard_commitment_tree_size + FROM blocks + JOIN (SELECT MAX(height) AS height FROM blocks) blocks_max + ON blocks.height = blocks_max.height", + [], + |row| { + let height: u32 = row.get(0)?; + let block_hash: Vec = row.get(1)?; + let sapling_tree_size: Option = row.get(2)?; + let sapling_tree: Vec = row.get(3)?; + let orchard_tree_size: Option = row.get(4)?; + Ok(( + BlockHeight::from(height), + block_hash, + sapling_tree_size, + sapling_tree, + orchard_tree_size + )) + }, + ) + .optional() + .map_err(SqliteClientError::from) + .and_then(|meta_row| meta_row.map(|r| parse_block_metadata(params, r)).transpose()) } /// Returns the block height at which the specified transaction was mined, /// if any. -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().to_vec()], + |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() +} + +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() } /// 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

, +pub(crate) fn get_min_unspent_height( + conn: &rusqlite::Connection, ) -> 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())) - }, - ) - .map_err(SqliteClientError::from) + let min_sapling: Option = conn.query_row( + "SELECT MIN(tx.block) + FROM sapling_received_notes n + JOIN transactions tx ON tx.id_tx = n.tx + WHERE n.id NOT IN ( + SELECT sapling_received_note_id + FROM sapling_received_note_spends + JOIN transactions tx ON tx.id_tx = transaction_id + WHERE tx.block IS NOT NULL + )", + [], + |row| { + row.get(0) + .map(|maybe_height: Option| maybe_height.map(|height| height.into())) + }, + )?; + #[cfg(feature = "orchard")] + let min_orchard: Option = conn.query_row( + "SELECT MIN(tx.block) + FROM orchard_received_notes n + JOIN transactions tx ON tx.id_tx = n.tx + WHERE n.id NOT IN ( + SELECT orchard_received_note_id + FROM orchard_received_note_spends + JOIN transactions tx ON tx.id_tx = transaction_id + WHERE tx.block IS NOT NULL + )", + [], + |row| { + row.get(0) + .map(|maybe_height: Option| maybe_height.map(|height| height.into())) + }, + )?; + #[cfg(not(feature = "orchard"))] + let min_orchard = None; + + Ok(min_sapling + .zip(min_orchard) + .map(|(s, o)| s.min(o)) + .or(min_sapling) + .or(min_orchard)) } /// Truncates the database to the given height. @@ -564,134 +1985,211 @@ pub(crate) fn get_min_unspent_height

( /// /// This should only be executed inside a transactional context. pub(crate) fn truncate_to_height( - wdb: &WalletDb

, + conn: &rusqlite::Transaction, + params: &P, block_height: BlockHeight, ) -> Result<(), SqliteClientError> { - let sapling_activation_height = wdb - .params + let sapling_activation_height = params .activation_height(NetworkUpgrade::Sapling) - .expect("Sapling activation height mutst be available."); + .expect("Sapling activation height must 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 last_scanned_height = conn.query_row("SELECT MAX(height) FROM blocks", [], |row| { + row.get::<_, Option>(0) + .map(|opt| opt.map_or_else(|| sapling_activation_height - 1, BlockHeight::from)) + })?; - if block_height < last_scanned_height - PRUNING_HEIGHT { - if let Some(h) = get_min_unspent_height(wdb)? { + if block_height < last_scanned_height - PRUNING_DEPTH { + if let Some(h) = get_min_unspent_height(conn)? { if block_height > h { return Err(SqliteClientError::RequestedRewindInvalid(h, block_height)); } } } - // 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)], - )?; + // 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(block_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(block_height + 1)], + )?; - // 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)], - )?; + // If we're removing scanned blocks, we need to truncate the note commitment tree, un-mine + // transactions, and remove received transparent outputs and affected block records from the + // database. + if block_height < last_scanned_height { + // Truncate the note commitment trees + let mut wdb = WalletDb { + conn: SqlTransaction(conn), + params: params.clone(), + }; + wdb.with_sapling_tree_mut(|tree| { + tree.truncate_removing_checkpoint(&block_height).map(|_| ()) + })?; + #[cfg(feature = "orchard")] + wdb.with_orchard_tree_mut(|tree| { + tree.truncate_removing_checkpoint(&block_height).map(|_| ()) + })?; // 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. - // Rewind utxos - wdb.conn.execute( + // Rewind utxos. It is currently necessary to delete these because we do + // not have the full transaction data for the received output. + conn.execute( "DELETE FROM utxos WHERE height > ?", [u32::from(block_height)], )?; // Un-mine transactions. - wdb.conn.execute( - "UPDATE transactions SET block = NULL, tx_index = NULL WHERE block IS NOT NULL AND block > ?", + conn.execute( + "UPDATE transactions SET block = NULL, tx_index = NULL + WHERE block IS NOT NULL AND block > ?", [u32::from(block_height)], )?; - // Now that they aren't depended on, delete scanned blocks. - wdb.conn.execute( + // Now that they aren't depended on, delete un-mined blocks. + conn.execute( "DELETE FROM blocks WHERE height > ?", [u32::from(block_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(block_height)], + )?; } Ok(()) } +#[cfg(feature = "transparent-inputs")] +fn to_unspent_transparent_output(row: &Row) -> Result { + let txid: Vec = row.get("prevout_txid")?; + let mut txid_bytes = [0u8; 32]; + txid_bytes.copy_from_slice(&txid); + + let index: u32 = row.get("prevout_idx")?; + let script_pubkey = Script(row.get("script")?); + let raw_value: i64 = row.get("value_zat")?; + let value = NonNegativeAmount::from_nonnegative_i64(raw_value).map_err(|_| { + SqliteClientError::CorruptedData(format!("Invalid UTXO value: {}", raw_value)) + })?; + let height: u32 = row.get("height")?; + + let outpoint = OutPoint::new(txid_bytes, index); + 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(), + ) + }) +} + +#[cfg(feature = "transparent-inputs")] +pub(crate) fn get_unspent_transparent_output( + conn: &rusqlite::Connection, + outpoint: &OutPoint, +) -> Result, SqliteClientError> { + let mut stmt_select_utxo = conn.prepare_cached( + "SELECT u.prevout_txid, u.prevout_idx, u.script, u.value_zat, u.height + FROM utxos u + WHERE u.prevout_txid = :txid + AND u.prevout_idx = :output_index + 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.block IS NOT NULL -- the spending tx is mined + OR tx.expiry_height IS NULL -- the spending tx will not expire + )", + )?; + + let result: Result, SqliteClientError> = stmt_select_utxo + .query_and_then( + named_params![ + ":txid": outpoint.hash(), + ":output_index": outpoint.n() + ], + to_unspent_transparent_output, + )? + .next() + .transpose(); + + result +} + /// 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

, + conn: &rusqlite::Connection, + params: &P, address: &TransparentAddress, max_height: BlockHeight, exclude: &[OutPoint], ) -> Result, SqliteClientError> { - let mut stmt_blocks = wdb.conn.prepare( + let chain_tip_height = scan_queue_extrema(conn)?.map(|range| *range.end()); + let stable_height = chain_tip_height + .unwrap_or(max_height) + .saturating_sub(PRUNING_DEPTH); + + let mut stmt_utxos = conn.prepare( "SELECT u.prevout_txid, u.prevout_idx, u.script, - u.value_zat, u.height, tx.block as block + u.value_zat, u.height 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", + WHERE u.address = :address + AND u.height <= :max_height + 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.block IS NOT NULL -- the spending tx is mined + OR tx.expiry_height IS NULL -- the spending tx will not expire + OR tx.expiry_height > :stable_height -- the spending tx is unexpired + )", )?; - let addr_str = address.encode(&wdb.params); + let addr_str = address.encode(params); let mut utxos = Vec::::new(); - let mut rows = stmt_blocks.query(params![addr_str, u32::from(max_height)])?; + let mut rows = stmt_utxos.query(named_params![ + ":address": addr_str, + ":max_height": u32::from(max_height), + ":stable_height": u32::from(stable_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) { + let output = to_unspent_transparent_output(row)?; + if excluded.contains(output.outpoint()) { continue; } - 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(), - ) - })?; - utxos.push(output); } @@ -703,27 +2201,43 @@ pub(crate) fn get_unspent_transparent_outputs( /// the provided `max_height`. #[cfg(feature = "transparent-inputs")] pub(crate) fn get_transparent_balances( - wdb: &WalletDb

, + conn: &rusqlite::Connection, + params: &P, account: AccountId, max_height: BlockHeight, -) -> Result, SqliteClientError> { - let mut stmt_blocks = wdb.conn.prepare( +) -> Result, SqliteClientError> { + let chain_tip_height = scan_queue_extrema(conn)?.map(|range| *range.end()); + let stable_height = chain_tip_height + .unwrap_or(max_height) + .saturating_sub(PRUNING_DEPTH); + + let mut stmt_blocks = 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 + WHERE u.received_by_account_id = :account_id + AND u.height <= :max_height + 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.block IS NOT NULL -- the spending tx is mined + OR tx.expiry_height IS NULL -- the spending tx will not expire + OR tx.expiry_height > :stable_height -- the spending tx is unexpired + ) GROUP BY u.address", )?; let mut res = HashMap::new(); - let mut rows = stmt_blocks.query(params![u32::from(account), u32::from(max_height)])?; + let mut rows = stmt_blocks.query(named_params![ + ":account_id": account.0, + ":max_height": u32::from(max_height), + ":stable_height": u32::from(stable_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 taddr = TransparentAddress::decode(params, &taddr_str)?; + let value = NonNegativeAmount::from_nonnegative_i64(row.get(1)?)?; res.insert(taddr, value); } @@ -731,310 +2245,1013 @@ pub(crate) fn get_transparent_balances( Ok(res) } +/// Returns a vector with the IDs of all accounts known to this wallet. +pub(crate) fn get_account_ids( + conn: &rusqlite::Connection, +) -> Result, SqliteClientError> { + let mut stmt = conn.prepare("SELECT id FROM accounts")?; + let mut rows = stmt.query([])?; + let mut result = Vec::new(); + while let Some(row) = rows.next()? { + let id = AccountId(row.get(0)?); + result.push(id); + } + Ok(result) +} + /// Inserts information about a scanned block into the database. -pub(crate) fn insert_block<'a, P>( - stmts: &mut DataConnStmtCache<'a, P>, +#[allow(clippy::too_many_arguments)] +pub(crate) fn put_block( + conn: &rusqlite::Transaction<'_>, block_height: BlockHeight, block_hash: BlockHash, block_time: u32, - commitment_tree: &CommitmentTree, + sapling_commitment_tree_size: u32, + sapling_output_count: u32, + #[cfg(feature = "orchard")] orchard_commitment_tree_size: u32, + #[cfg(feature = "orchard")] orchard_action_count: u32, ) -> Result<(), SqliteClientError> { - stmts.stmt_insert_block(block_height, block_hash, block_time, commitment_tree) + let block_hash_data = conn + .query_row( + "SELECT hash FROM blocks WHERE height = ?", + [u32::from(block_height)], + |row| row.get::<_, Vec>(0), + ) + .optional()?; + + // Ensure that in the case of an upsert, we don't overwrite block data + // with information for a block with a different hash. + if let Some(bytes) = block_hash_data { + let expected_hash = BlockHash::try_from_slice(&bytes).ok_or_else(|| { + SqliteClientError::CorruptedData(format!( + "Invalid block hash at height {}", + u32::from(block_height) + )) + })?; + if expected_hash != block_hash { + return Err(SqliteClientError::BlockConflict(block_height)); + } + } + + let mut stmt_upsert_block = conn.prepare_cached( + "INSERT INTO blocks ( + height, + hash, + time, + sapling_commitment_tree_size, + sapling_output_count, + sapling_tree, + orchard_commitment_tree_size, + orchard_action_count + ) + VALUES ( + :height, + :hash, + :block_time, + :sapling_commitment_tree_size, + :sapling_output_count, + x'00', + :orchard_commitment_tree_size, + :orchard_action_count + ) + ON CONFLICT (height) DO UPDATE + SET hash = :hash, + time = :block_time, + sapling_commitment_tree_size = :sapling_commitment_tree_size, + sapling_output_count = :sapling_output_count, + orchard_commitment_tree_size = :orchard_commitment_tree_size, + orchard_action_count = :orchard_action_count", + )?; + + #[cfg(not(feature = "orchard"))] + let orchard_commitment_tree_size: Option = None; + #[cfg(not(feature = "orchard"))] + let orchard_action_count: Option = None; + + stmt_upsert_block.execute(named_params![ + ":height": u32::from(block_height), + ":hash": &block_hash.0[..], + ":block_time": block_time, + ":sapling_commitment_tree_size": sapling_commitment_tree_size, + ":sapling_output_count": sapling_output_count, + ":orchard_commitment_tree_size": orchard_commitment_tree_size, + ":orchard_action_count": orchard_action_count, + ])?; + + Ok(()) } /// 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, +pub(crate) fn put_tx_meta( + conn: &rusqlite::Connection, + 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) + // 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) +} + +/// 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: AccountId, + 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 WHERE account_id = :account")?; + + let mut result = stmt.query(named_params! { ":account": account.0 })?; + while let Some(row) = result.next()? { + let addr_str = row.get::<_, String>(0)?; + let decoded = addr_str.parse::()?; + if receiver.corresponds(&decoded) { + return Ok(Some(decoded)); + } + } + + Ok(None) + } } } /// Inserts full transaction data into the database. -pub(crate) fn put_tx_data<'a, P>( - stmts: &mut DataConnStmtCache<'a, P>, +pub(crate) fn put_tx_data( + conn: &rusqlite::Connection, tx: &Transaction, - fee: Option, + fee: Option, created_at: Option, ) -> Result { - let txid = tx.txid(); + 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)?; - 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) - } + 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_err(SqliteClientError::from) } /// 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>, +pub(crate) fn mark_transparent_utxo_spent( + conn: &rusqlite::Connection, tx_ref: i64, outpoint: &OutPoint, ) -> Result<(), SqliteClientError> { - stmts.stmt_mark_transparent_utxo_spent(tx_ref, outpoint)?; + 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 utxos txo + WHERE txo.prevout_txid = :prevout_txid + AND txo.prevout_idx = :prevout_idx + ON CONFLICT (transparent_received_output_id, transaction_id) DO NOTHING", + )?; + + let sql_args = named_params![ + ":spent_in_tx": &tx_ref, + ":prevout_txid": &outpoint.hash().to_vec(), + ":prevout_idx": &outpoint.n(), + ]; + stmt_mark_transparent_utxo_spent.execute(sql_args)?; 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>, +pub(crate) fn put_received_transparent_utxo( + conn: &rusqlite::Connection, + params: &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(), - )) + let address_str = output.recipient_address().encode(params); + let account_id = conn + .query_row( + "SELECT account_id FROM addresses WHERE cached_transparent_receiver_address = :address", + named_params![":address": &address_str], + |row| Ok(AccountId(row.get(0)?)), + ) + .optional()?; + + if let Some(account) = account_id { + Ok(put_legacy_transparent_utxo(conn, params, output, account)?) + } else { + // 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; if so, insert/update it directly. + get_account_ids(conn)? + .into_iter() + .find_map( + |account| match get_legacy_transparent_address(params, conn, account) { + Ok(Some((legacy_taddr, _))) if &legacy_taddr == output.recipient_address() => { + Some( + put_legacy_transparent_utxo(conn, params, output, account) + .map_err(SqliteClientError::from), + ) } - }) - }) + Ok(_) => None, + Err(e) => Some(Err(e)), + }, + ) + // The UTXO was not for any of the legacy transparent addresses. + .unwrap_or_else(|| { + Err(SqliteClientError::AddressNotRecognized( + *output.recipient_address(), + )) + }) + } } -/// Removes old incremental witnesses up to the given block height. -pub(crate) fn prune_witnesses

( - stmts: &mut DataConnStmtCache<'_, P>, - below_height: BlockHeight, -) -> Result<(), SqliteClientError> { - stmts.stmt_prune_witnesses(below_height) +#[cfg(feature = "transparent-inputs")] +pub(crate) fn put_legacy_transparent_utxo( + conn: &rusqlite::Connection, + params: &P, + output: &WalletTransparentOutput, + received_by_account: AccountId, +) -> Result { + #[cfg(feature = "transparent-inputs")] + let mut stmt_upsert_legacy_transparent_utxo = conn.prepare_cached( + "INSERT INTO utxos ( + prevout_txid, prevout_idx, + received_by_account_id, address, script, + value_zat, height) + VALUES + (:prevout_txid, :prevout_idx, + :received_by_account_id, :address, :script, + :value_zat, :height) + ON CONFLICT (prevout_txid, prevout_idx) DO UPDATE + SET received_by_account_id = :received_by_account_id, + height = :height, + address = :address, + script = :script, + value_zat = :value_zat + RETURNING id", + )?; + + let sql_args = named_params![ + ":prevout_txid": &output.outpoint().hash().to_vec(), + ":prevout_idx": &output.outpoint().n(), + ":received_by_account_id": received_by_account.0, + ":address": &output.recipient_address().encode(params), + ":script": &output.txout().script_pubkey.0, + ":value_zat": &i64::from(Amount::from(output.txout().value)), + ":height": &u32::from(output.height()), + ]; + + stmt_upsert_legacy_transparent_utxo.query_row(sql_args, |row| row.get::<_, i64>(0).map(UtxoId)) } -/// 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, -) -> Result<(), SqliteClientError> { - stmts.stmt_update_expired(height) +// A utility function for creation of parameters for use in `insert_sent_output` +// and `put_sent_output` +fn recipient_params( + to: &Recipient, +) -> (Option, Option, PoolType) { + match to { + Recipient::External(addr, pool) => (Some(addr.encode()), None, *pool), + Recipient::InternalAccount { + receiving_account, + external_address, + note, + } => ( + external_address.as_ref().map(|a| a.encode()), + Some(*receiving_account), + PoolType::Shielded(note.protocol()), + ), + } } /// 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>, +pub(crate) fn insert_sent_output( + conn: &rusqlite::Connection, tx_ref: i64, from_account: AccountId, - output: &SentTransactionOutput, + 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 (to_address, to_account_id, pool_type) = recipient_params(output.recipient()); + let sql_args = named_params![ + ":tx": &tx_ref, + ":output_pool": &pool_code(pool_type), + ":output_index": &i64::try_from(output.output_index()).unwrap(), + ":from_account_id": from_account.0, + ":to_address": &to_address, + ":to_account_id": to_account_id.map(|a| a.0), + ":value": &i64::from(Amount::from(output.value())), + ":memo": memo_repr(output.memo()) + ]; + + stmt_insert_sent_output.execute(sql_args)?; + + 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>, +pub(crate) fn put_sent_output( + conn: &rusqlite::Connection, from_account: AccountId, tx_ref: i64, output_index: usize, - recipient: &Recipient, - value: Amount, + recipient: &Recipient, + value: NonNegativeAmount, 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 = :to_address, + to_account_id = IFNULL(to_account_id, :to_account_id), + value = :value, + memo = IFNULL(:memo, memo)", + )?; + + let (to_address, to_account_id, pool_type) = recipient_params(recipient); + let sql_args = named_params![ + ":tx": &tx_ref, + ":output_pool": &pool_code(pool_type), + ":output_index": &i64::try_from(output_index).unwrap(), + ":from_account_id": from_account.0, + ":to_address": &to_address, + ":to_account_id": &to_account_id.map(|a| a.0), + ":value": &i64::from(Amount::from(value)), + ":memo": memo_repr(memo) + ]; + + stmt_upsert_sent_output.execute(sql_args)?; + + Ok(()) +} + +/// Inserts the given entries into the nullifier map. +/// +/// Returns an error if the new entries conflict with existing ones. This indicates either +/// corrupted data, or that a reorg has occurred and the caller needs to repair the wallet +/// state with [`truncate_to_height`]. +pub(crate) fn insert_nullifier_map>( + conn: &rusqlite::Transaction<'_>, + block_height: BlockHeight, + spend_pool: ShieldedProtocol, + new_entries: &[(TxId, u16, Vec)], +) -> Result<(), SqliteClientError> { + let mut stmt_select_tx_locators = conn.prepare_cached( + "SELECT block_height, tx_index, txid + FROM tx_locator_map + WHERE (block_height = :block_height AND tx_index = :tx_index) OR txid = :txid", + )?; + let mut stmt_insert_tx_locator = conn.prepare_cached( + "INSERT INTO tx_locator_map + (block_height, tx_index, txid) + VALUES (:block_height, :tx_index, :txid)", + )?; + let mut stmt_insert_nullifier_mapping = conn.prepare_cached( + "INSERT INTO nullifier_map + (spend_pool, nf, block_height, tx_index) + VALUES (:spend_pool, :nf, :block_height, :tx_index) + ON CONFLICT (spend_pool, nf) DO UPDATE + SET block_height = :block_height, + tx_index = :tx_index", + )?; + + for (txid, tx_index, nullifiers) in new_entries { + let tx_args = named_params![ + ":block_height": u32::from(block_height), + ":tx_index": tx_index, + ":txid": txid.as_ref(), + ]; + + // We cannot use an upsert here, because we use the tx locator as the foreign key + // in `nullifier_map` instead of `txid` for database size efficiency. If an insert + // into `tx_locator_map` were to conflict, we would need the resulting update to + // cascade into `nullifier_map` as either: + // - an update (if a transaction moved within a block), or + // - a deletion (if the locator now points to a different transaction). + // + // `ON UPDATE` has `CASCADE` to always update, but has no deletion option. So we + // instead set `ON UPDATE RESTRICT` on the foreign key relation, and require the + // caller to manually rewind the database in this situation. + let locator = stmt_select_tx_locators + .query_map(tx_args, |row| { + Ok(( + BlockHeight::from_u32(row.get(0)?), + row.get::<_, u16>(1)?, + TxId::from_bytes(row.get(2)?), + )) + })? + .fold(Ok(None), |acc: Result<_, SqliteClientError>, row| { + match (acc?, row?) { + (None, rhs) => Ok(Some(Some(rhs))), + // If there was more than one row, then due to the uniqueness + // constraints on the `tx_locator_map` table, all of the rows conflict + // with the locator being inserted. + (Some(_), _) => Ok(Some(None)), + } + })?; + + match locator { + // If the locator in the table matches the one being inserted, do nothing. + Some(Some(loc)) if loc == (block_height, *tx_index, *txid) => (), + // If the locator being inserted would conflict, report it. + Some(_) => Err(SqliteClientError::DbError(rusqlite::Error::SqliteFailure( + rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_CONSTRAINT), + Some("UNIQUE constraint failed: tx_locator_map.block_height, tx_locator_map.tx_index".into()), + )))?, + // If the locator doesn't exist, insert it. + None => stmt_insert_tx_locator.execute(tx_args).map(|_| ())?, + } + + for nf in nullifiers { + // Here it is okay to use an upsert, because per above we've confirmed that + // the locator points to the same transaction. + let nf_args = named_params![ + ":spend_pool": pool_code(PoolType::Shielded(spend_pool)), + ":nf": nf.as_ref(), + ":block_height": u32::from(block_height), + ":tx_index": tx_index, + ]; + stmt_insert_nullifier_mapping.execute(nf_args)?; + } } Ok(()) } +/// Returns the row of the `transactions` table corresponding to the transaction in which +/// this nullifier is revealed, if any. +pub(crate) fn query_nullifier_map, S>( + conn: &rusqlite::Transaction<'_>, + spend_pool: ShieldedProtocol, + nf: &N, +) -> Result, SqliteClientError> { + let mut stmt_select_locator = conn.prepare_cached( + "SELECT block_height, tx_index, txid + FROM nullifier_map + LEFT JOIN tx_locator_map USING (block_height, tx_index) + WHERE spend_pool = :spend_pool AND nf = :nf", + )?; + + let sql_args = named_params![ + ":spend_pool": pool_code(PoolType::Shielded(spend_pool)), + ":nf": nf.as_ref(), + ]; + + // Find the locator corresponding to this nullifier, if any. + let locator = stmt_select_locator + .query_row(sql_args, |row| { + Ok(( + BlockHeight::from_u32(row.get(0)?), + row.get(1)?, + TxId::from_bytes(row.get(2)?), + )) + }) + .optional()?; + let (height, index, txid) = match locator { + Some(res) => res, + None => return Ok(None), + }; + + // Find or create a corresponding row in the `transactions` table. Usually a row will + // have been created during the same scan that the locator was added to the nullifier + // map, but it would not happen if the transaction in question spent the note with no + // change or explicit in-wallet recipient. + put_tx_meta( + conn, + &WalletTx::new( + txid, + index, + vec![], + vec![], + #[cfg(feature = "orchard")] + vec![], + #[cfg(feature = "orchard")] + vec![], + ), + height, + ) + .map(Some) +} + +/// Deletes from the nullifier map any entries with a locator referencing a block height +/// lower than the pruning height. +pub(crate) fn prune_nullifier_map( + conn: &rusqlite::Transaction<'_>, + block_height: BlockHeight, +) -> Result<(), SqliteClientError> { + let mut stmt_delete_locators = conn.prepare_cached( + "DELETE FROM tx_locator_map + WHERE block_height < :block_height", + )?; + + stmt_delete_locators.execute(named_params![":block_height": u32::from(block_height)])?; + + Ok(()) +} + #[cfg(test)] mod tests { - use secrecy::Secret; - use tempfile::NamedTempFile; + use std::num::NonZeroU32; - use zcash_primitives::transaction::components::Amount; - - use zcash_client_backend::data_api::WalletRead; + use sapling::zip32::ExtendedSpendingKey; + use secrecy::{ExposeSecret, SecretVec}; + use zcash_client_backend::data_api::{AccountSource, WalletRead}; + use zcash_primitives::{block::BlockHash, transaction::components::amount::NonNegativeAmount}; use crate::{ - tests, - wallet::{get_current_address, init::init_wallet_db}, - AccountId, WalletDb, + testing::{AddressType, BlockCache, TestBuilder, TestState}, + AccountId, }; - use super::get_balance; + use super::account_birthday; #[cfg(feature = "transparent-inputs")] use { + crate::PRUNING_DEPTH, zcash_client_backend::{ - data_api::WalletWrite, encoding::AddressCodec, wallet::WalletTransparentOutput, + data_api::{wallet::input_selection::GreedyInputSelector, InputSource, WalletWrite}, + encoding::AddressCodec, + fees::{fixed, DustOutputPolicy}, + wallet::WalletTransparentOutput, }, zcash_primitives::{ consensus::BlockHeight, - transaction::components::{OutPoint, TxOut}, + transaction::{ + components::{OutPoint, TxOut}, + fees::fixed::FeeRule as FixedFeeRule, + }, }, }; #[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(); + let st = TestBuilder::new() + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + let account = st.test_account().unwrap(); - // Add an account to the wallet - tests::init_test_accounts_table(&db_data); + // The account should have no summary information + assert_eq!(st.get_wallet_summary(0), None); - // The account should be empty + // We can't get an anchor height, as we have not scanned any blocks. assert_eq!( - get_balance(&db_data, AccountId::from(0)).unwrap(), - Amount::zero() + st.wallet() + .get_target_and_anchor_heights(NonZeroU32::new(10).unwrap()) + .unwrap(), + None ); - // 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); + // The default address is set for the test account + assert_matches!( + st.wallet().get_current_address(account.account_id()), + Ok(Some(_)) + ); - // 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() + // No default address is set for an un-initialized account + assert_matches!( + st.wallet() + .get_current_address(AccountId(account.account_id().0 + 1)), + Ok(None) ); } #[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(); + use crate::testing::TestBuilder; + + let mut st = TestBuilder::new() + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account_id = st.test_account().unwrap().account_id(); + let uaddr = st + .wallet() + .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)) + let height_1 = BlockHeight::from_u32(12345); + let bal_absent = st + .wallet() + .get_transparent_balances(account_id, height_1) .unwrap(); assert!(bal_absent.is_empty()); - 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(); + // Create a fake transparent output. + let value = NonNegativeAmount::const_from_u64(100000); + let outpoint = OutPoint::new([1u8; 32], 1); + let txout = TxOut { + value, + script_pubkey: taddr.script(), + }; - let res0 = super::put_received_transparent_utxo(&mut ops, &utxo); + // Pretend the output's transaction was mined at `height_1`. + let utxo = + WalletTransparentOutput::from_parts(outpoint.clone(), txout.clone(), 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_unspent_transparent_outputs( + taddr, + height_1, + &[] + ).as_deref(), + Ok(&[ref ret]) if (ret.outpoint(), ret.txout(), ret.height()) == (utxo.outpoint(), utxo.txout(), height_1) + ); + assert_matches!( + st.wallet().get_unspent_transparent_output(utxo.outpoint()), + Ok(Some(ret)) if (ret.outpoint(), ret.txout(), ret.height()) == (utxo.outpoint(), utxo.txout(), height_1) + ); + // 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); + // the same `UtxoId`. + let height_2 = BlockHeight::from_u32(34567); + let utxo2 = WalletTransparentOutput::from_parts(outpoint, txout, 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!( - super::get_unspent_transparent_outputs( - &db_data, - taddr, - BlockHeight::from_u32(12345), - &[] - ), - Ok(utxos) if utxos.is_empty() + st.wallet() + .get_unspent_transparent_outputs(taddr, height_1, &[]) + .as_deref(), + Ok(&[]) ); + // We can still look up the specific output, and it has the expected height. assert_matches!( - super::get_unspent_transparent_outputs( - &db_data, - taddr, - BlockHeight::from_u32(34567), - &[] - ), - Ok(utxos) if { - utxos.len() == 1 && - utxos.iter().any(|rutxo| rutxo.height() == utxo2.height()) - } + st.wallet().get_unspent_transparent_output(utxo2.outpoint()), + Ok(Some(ret)) if (ret.outpoint(), ret.txout(), ret.height()) == (utxo2.outpoint(), utxo2.txout(), height_2) + ); + + // If we include `height_2` then the output is returned. + assert_matches!( + st.wallet() + .get_unspent_transparent_outputs(taddr, height_2, &[]) + .as_deref(), + Ok(&[ref ret]) if (ret.outpoint(), ret.txout(), ret.height()) == (utxo.outpoint(), utxo.txout(), height_2) ); 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() + st.wallet().get_transparent_balances(account_id, height_2), + Ok(h) if h.get(taddr) == Some(&value) ); // Artificially delete the address from the addresses table so that // we can ensure the update fails if the join doesn't work. - db_data + st.wallet() .conn .execute( "DELETE FROM addresses WHERE cached_transparent_receiver_address = ?", - [Some(taddr.encode(&db_data.params))], + [Some(taddr.encode(&st.wallet().params))], ) .unwrap(); - let res2 = super::put_received_transparent_utxo(&mut ops, &utxo2); + let res2 = st.wallet_mut().put_received_transparent_utxo(&utxo2); assert_matches!(res2, Err(_)); } + + #[test] + fn get_default_account_index() { + use crate::testing::TestBuilder; + + let st = TestBuilder::new() + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + let account_id = st.test_account().unwrap().account_id(); + let account_parameters = st.wallet().get_account(account_id).unwrap().unwrap(); + + let expected_account_index = zip32::AccountId::try_from(0).unwrap(); + assert_matches!( + account_parameters.kind, + AccountSource::Derived{account_index, ..} if account_index == expected_account_index + ); + } + + #[test] + fn get_account_ids() { + use crate::testing::TestBuilder; + use zcash_client_backend::data_api::WalletWrite; + + let mut st = TestBuilder::new() + .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).unwrap(); + + for acct_id in st.wallet().get_account_ids().unwrap() { + assert_matches!(st.wallet().get_account(acct_id), Ok(Some(_))) + } + } + + #[test] + #[cfg(feature = "transparent-inputs")] + fn transparent_balance_across_shielding() { + use zcash_client_backend::ShieldedProtocol; + + let mut st = TestBuilder::new() + .with_block_cache() + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account = st.test_account().cloned().unwrap(); + let uaddr = st + .wallet() + .get_current_address(account.account_id()) + .unwrap() + .unwrap(); + let taddr = uaddr.transparent().unwrap(); + + // Initialize the wallet with chain data that has no shielded notes for us. + let not_our_key = ExtendedSpendingKey::master(&[]).to_diversifiable_full_viewing_key(); + let not_our_value = NonNegativeAmount::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); + + let check_balance = |st: &TestState<_>, min_confirmations: u32, expected| { + // 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.account_id()) + .unwrap(); + assert_eq!(balance.unshielded(), expected); + + // Check the older APIs for consistency. + let max_height = st.wallet().chain_height().unwrap().unwrap() + 1 - min_confirmations; + assert_eq!( + st.wallet() + .get_transparent_balances(account.account_id(), max_height) + .unwrap() + .get(taddr) + .cloned() + .unwrap_or(NonNegativeAmount::ZERO), + expected, + ); + assert_eq!( + st.wallet() + .get_unspent_transparent_outputs(taddr, max_height, &[]) + .unwrap() + .into_iter() + .map(|utxo| utxo.value()) + .sum::>(), + Some(expected), + ); + }; + + // The wallet starts out with zero balance. + check_balance(&st, 0, NonNegativeAmount::ZERO); + check_balance(&st, 1, NonNegativeAmount::ZERO); + + // Create a fake transparent output. + let value = NonNegativeAmount::from_u64(100000).unwrap(); + let outpoint = OutPoint::new([1u8; 32], 1); + 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, txout, height).unwrap(); + st.wallet_mut() + .put_received_transparent_utxo(&utxo) + .unwrap(); + + // The wallet should detect the balance as having 1 confirmation. + check_balance(&st, 0, value); + check_balance(&st, 1, value); + check_balance(&st, 2, NonNegativeAmount::ZERO); + + // Shield the output. + let input_selector = GreedyInputSelector::new( + fixed::SingleOutputChangeStrategy::new( + FixedFeeRule::non_standard(NonNegativeAmount::ZERO), + None, + ShieldedProtocol::Sapling, + ), + DustOutputPolicy::default(), + ); + let txid = st + .shield_transparent_funds(&input_selector, value, account.usk(), &[*taddr], 1) + .unwrap()[0]; + + // The wallet should have zero transparent balance, because the shielding + // transaction can be mined. + check_balance(&st, 0, NonNegativeAmount::ZERO); + check_balance(&st, 1, NonNegativeAmount::ZERO); + check_balance(&st, 2, NonNegativeAmount::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, 0, NonNegativeAmount::ZERO); + check_balance(&st, 1, NonNegativeAmount::ZERO); + check_balance(&st, 2, NonNegativeAmount::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, 0, NonNegativeAmount::ZERO); + check_balance(&st, 1, NonNegativeAmount::ZERO); + check_balance(&st, 2, NonNegativeAmount::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(); + + // TODO: Making the transparent output spendable in this situation requires + // changes to the transparent data model, so for now the wallet should still have + // zero transparent balance. https://github.com/zcash/librustzcash/issues/986 + check_balance(&st, 0, NonNegativeAmount::ZERO); + check_balance(&st, 1, NonNegativeAmount::ZERO); + check_balance(&st, 2, NonNegativeAmount::ZERO); + + // Roll forward the chain tip until the transaction's expiry height is in the + // stable block range (so a reorg won't make it spendable again). + st.wallet_mut() + .update_chain_tip(expiry_height + PRUNING_DEPTH) + .unwrap(); + + // The transparent output should be spendable again, with more confirmations. + check_balance(&st, 0, value); + check_balance(&st, 1, value); + check_balance(&st, 2, value); + } + + #[test] + fn block_fully_scanned() { + let mut st = TestBuilder::new() + .with_block_cache() + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let block_fully_scanned = |st: &TestState| { + 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 = NonNegativeAmount::const_from_u64(10000); + let start_height = st.sapling_activation_height(); + let _ = st.generate_block_at( + start_height, + BlockHash([0; 32]), + ¬_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_block_cache() + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account_id = st.test_account().unwrap().account_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..9a2efbe51c --- /dev/null +++ b/zcash_client_sqlite/src/wallet/commitment_tree.rs @@ -0,0 +1,1263 @@ +use rusqlite::{self, named_params, OptionalExtension}; +use std::{ + collections::BTreeSet, + error, fmt, + io::{self, Cursor}, + marker::PhantomData, + num::NonZeroU32, + ops::Range, + sync::Arc, +}; +use zcash_client_backend::data_api::chain::CommitmentTreeRoot; + +use incrementalmerkletree::{Address, Hashable, Level, Position, Retention}; +use shardtree::{ + error::ShardTreeError, + store::{Checkpoint, ShardStore, TreeState}, + LocatedPrunableTree, LocatedTree, PrunableTree, RetentionFlags, +}; + +use zcash_primitives::{consensus::BlockHeight, merkle_tree::HashSer}; + +use zcash_client_backend::serialization::shardtree::{read_shard, write_shard}; + +/// 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(&mut self, from: Address) -> Result<(), Self::Error> { + truncate(self.conn, self.table_prefix, from) + } + + 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 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( + &mut self, + checkpoint_id: &Self::CheckpointId, + ) -> Result<(), Self::Error> { + truncate_checkpoints(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(&mut self, from: Address) -> Result<(), Self::Error> { + truncate(&self.conn, self.table_prefix, from) + } + + 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 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( + &mut self, + checkpoint_id: &Self::CheckpointId, + ) -> Result<(), Self::Error> { + let tx = self.conn.transaction().map_err(Error::Query)?; + truncate_checkpoints(&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); + 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)?; + Ok(LocatedPrunableTree::from_parts(shard_root, shard_tree)) + }) + .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( + conn: &rusqlite::Connection, + table_prefix: &'static str, + from: Address, +) -> Result<(), Error> { + conn.execute( + &format!( + "DELETE FROM {}_tree_shards WHERE shard_index >= ?", + table_prefix + ), + [from.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> { + if checkpoint_depth == 0 { + return Ok(None); + } + + 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 - 1], + |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( + 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)?; + + 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)?, + ); + + 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::Checkpoint { + id: (), + is_marked: false, + }, + ) + }), + ) + .map_err(ShardTreeError::Insert)? + .expect("slice of inserted roots was verified to be nonempty"); + drop(insert_into_cap); + + put_cap(conn, table_prefix, cap_result.subtree.take_root()).map_err(ShardTreeError::Storage)?; + + check_shard_discontinuity( + conn, + table_prefix, + start_index..start_index + (roots.len() as u64), + ) + .map_err(ShardTreeError::Storage)?; + + // We want to avoid deserializing the subtree just to annotate its root node, so we simply + // cache the downloaded root alongside of any already-persisted subtree. We will update the + // subtree data itself by reannotating the root node of the tree, handling conflicts, at + // the time that we deserialize the tree. + let mut stmt = conn + .prepare_cached(&format!( + "INSERT INTO {}_tree_shards (shard_index, subtree_end_height, root_hash, shard_data) + VALUES (:shard_index, :subtree_end_height, :root_hash, :shard_data) + ON CONFLICT (shard_index) DO UPDATE + SET subtree_end_height = :subtree_end_height, root_hash = :root_hash", + table_prefix + )) + .map_err(|e| ShardTreeError::Storage(Error::Query(e)))?; + + let put_roots = tracing::info_span!("write_shards").entered(); + for (root, i) in roots.iter().zip(0u64..) { + // The `shard_data` value will only be used in the case that no tree already exists. + let mut shard_data: Vec = vec![]; + let tree = PrunableTree::leaf((root.root_hash().clone(), RetentionFlags::EPHEMERAL)); + write_shard(&mut shard_data, &tree) + .map_err(|e| ShardTreeError::Storage(Error::Serialization(e)))?; + + let mut root_hash_data: Vec = vec![]; + root.root_hash() + .write(&mut root_hash_data) + .map_err(|e| ShardTreeError::Storage(Error::Serialization(e)))?; + + stmt.execute(named_params![ + ":shard_index": start_index + i, + ":subtree_end_height": u32::from(root.subtree_end_height()), + ":root_hash": root_hash_data, + ":shard_data": shard_data, + ]) + .map_err(|e| ShardTreeError::Storage(Error::Query(e)))?; + } + drop(put_roots); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use tempfile::NamedTempFile; + + use incrementalmerkletree::{ + testing::{ + check_append, check_checkpoint_rewind, check_remove_mark, check_rewind_remove_mark, + check_root_hashes, check_witness_consistency, check_witnesses, + }, + Position, Retention, + }; + use shardtree::ShardTree; + use zcash_client_backend::data_api::chain::CommitmentTreeRoot; + use zcash_primitives::consensus::{BlockHeight, Network}; + + use super::SqliteShardStore; + use crate::{ + testing::pool::ShieldedPoolTester, + wallet::{init::init_wallet_db, sapling::tests::SaplingPoolTester}, + WalletDb, + }; + + fn new_tree( + m: usize, + ) -> ShardTree, 4, 3> { + let data_file = NamedTempFile::new().unwrap(); + let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap(); + data_file.keep().unwrap(); + + init_wallet_db(&mut db_data, None).unwrap(); + let store = + SqliteShardStore::<_, String, 3>::from_connection(db_data.conn, T::TABLES_PREFIX) + .unwrap(); + ShardTree::new(store, m) + } + + #[cfg(feature = "orchard")] + mod orchard { + use super::new_tree; + use crate::wallet::orchard::tests::OrchardPoolTester; + + #[test] + fn append() { + super::check_append(new_tree::); + } + + #[test] + fn root_hashes() { + super::check_root_hashes(new_tree::); + } + + #[test] + fn witnesses() { + super::check_witnesses(new_tree::); + } + + #[test] + fn witness_consistency() { + super::check_witness_consistency(new_tree::); + } + + #[test] + fn checkpoint_rewind() { + super::check_checkpoint_rewind(new_tree::); + } + + #[test] + fn remove_mark() { + super::check_remove_mark(new_tree::); + } + + #[test] + fn rewind_remove_mark() { + super::check_rewind_remove_mark(new_tree::); + } + + #[test] + fn put_shard_roots() { + super::put_shard_roots::() + } + } + + #[test] + fn sapling_append() { + check_append(new_tree::); + } + + #[test] + fn sapling_root_hashes() { + check_root_hashes(new_tree::); + } + + #[test] + fn sapling_witnesses() { + check_witnesses(new_tree::); + } + + #[test] + fn sapling_witness_consistency() { + check_witness_consistency(new_tree::); + } + + #[test] + fn sapling_checkpoint_rewind() { + check_checkpoint_rewind(new_tree::); + } + + #[test] + fn sapling_remove_mark() { + check_remove_mark(new_tree::); + } + + #[test] + fn sapling_rewind_remove_mark() { + check_rewind_remove_mark(new_tree::); + } + + #[test] + fn sapling_put_shard_roots() { + put_shard_roots::() + } + + fn put_shard_roots() { + let data_file = NamedTempFile::new().unwrap(); + let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap(); + data_file.keep().unwrap(); + + init_wallet_db(&mut db_data, None).unwrap(); + let tx = db_data.conn.transaction().unwrap(); + let store = + SqliteShardStore::<_, String, 3>::from_connection(&tx, T::TABLES_PREFIX).unwrap(); + + // introduce some roots + let roots = (0u32..4) + .into_iter() + .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').into_iter().map(|c| { + ( + c.to_string(), + match c { + 'c' => Retention::Marked, + 'h' => Retention::Checkpoint { + id: checkpoint_height, + is_marked: false, + }, + _ => 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.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..800d7f660d --- /dev/null +++ b/zcash_client_sqlite/src/wallet/common.rs @@ -0,0 +1,225 @@ +//! Functions common to Sapling and Orchard support in the wallet. + +use rusqlite::{named_params, types::Value, Connection, Row}; +use std::rc::Rc; + +use zcash_client_backend::{wallet::ReceivedNote, ShieldedProtocol}; +use zcash_primitives::transaction::{components::amount::NonNegativeAmount, TxId}; +use zcash_protocol::consensus::{self, BlockHeight}; + +use super::wallet_birthday; +use crate::{error::SqliteClientError, AccountId, ReceivedNoteId, SAPLING_TABLES_PREFIX}; + +#[cfg(feature = "orchard")] +use crate::ORCHARD_TABLES_PREFIX; + +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: AccountId, + target_value: NonNegativeAmount, + 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 {table_prefix}_received_notes.account_id = :account + AND value >= 5000 -- FIXME #1016, allow selection of a 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": 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::>() +} diff --git a/zcash_client_sqlite/src/wallet/init.rs b/zcash_client_sqlite/src/wallet/init.rs index 6650322915..9fff7e3a3f 100644 --- a/zcash_client_sqlite/src/wallet/init.rs +++ b/zcash_client_sqlite/src/wallet/init.rs @@ -1,23 +1,22 @@ //! Functions for initializing the various databases. -use std::collections::HashMap; + use std::fmt; +use std::rc::Rc; -use rusqlite::{self, types::ToSql}; use schemer::{Migrator, MigratorError}; use schemer_rusqlite::RusqliteAdapter; 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}, + keys::AddressGenerationError, }; +use zcash_primitives::{consensus, transaction::components::amount::BalanceError}; -use zcash_client_backend::keys::UnifiedFullViewingKey; - -use crate::{error::SqliteClientError, wallet, WalletDb}; +use super::commitment_tree; +use crate::{error::SqliteClientError, WalletDb}; mod migrations; @@ -26,14 +25,34 @@ pub enum WalletMigrationError { /// The seed is required for the migration. SeedRequired, + /// A seed was provided that is not relevant to any of the accounts within the wallet. + /// + /// Specifically, it is not relevant to any account for which [`Account::source`] is + /// [`AccountSource::Derived`]. We do not check whether the seed is relevant to any + /// imported account, because that would require brute-forcing the ZIP 32 account + /// index space. + /// + /// [`Account::source`]: zcash_client_backend::data_api::Account::source + /// [`AccountSource::Derived`]: zcash_client_backend::data_api::AccountSource::Derived + SeedNotRelevant, + /// Decoding of an existing value from its serialized form has failed. CorruptedData(String), + /// An error occurred in migrating a Zcash address or key. + AddressGeneration(AddressGenerationError), + /// Wrapper for rusqlite errors. DbError(rusqlite::Error), /// Wrapper for amount balance violations BalanceError(BalanceError), + + /// Wrapper for commitment tree invariant violations + CommitmentTree(ShardTreeError), + + /// Reverting the specified migration is not supported. + CannotRevert(Uuid), } impl From for WalletMigrationError { @@ -48,6 +67,18 @@ impl From for WalletMigrationError { } } +impl From> for WalletMigrationError { + fn from(e: ShardTreeError) -> Self { + WalletMigrationError::CommitmentTree(e) + } +} + +impl From for WalletMigrationError { + fn from(e: AddressGenerationError) -> Self { + WalletMigrationError::AddressGeneration(e) + } +} + impl fmt::Display for WalletMigrationError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self { @@ -57,11 +88,24 @@ impl fmt::Display for WalletMigrationError { "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) + } } } } @@ -70,11 +114,69 @@ 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), _ => 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::HdwalletError(e) => WalletMigrationError::CorruptedData(e.to_string()), + #[cfg(feature = "transparent-inputs")] + SqliteClientError::TransparentAddress(e) => { + WalletMigrationError::CorruptedData(e.to_string()) + } + SqliteClientError::DbError(e) => WalletMigrationError::DbError(e), + SqliteClientError::Io(e) => WalletMigrationError::CorruptedData(e.to_string()), + SqliteClientError::InvalidMemo(e) => WalletMigrationError::CorruptedData(e.to_string()), + SqliteClientError::AddressGeneration(e) => WalletMigrationError::AddressGeneration(e), + SqliteClientError::BadAccountData(e) => WalletMigrationError::CorruptedData(e), + SqliteClientError::CommitmentTree(e) => WalletMigrationError::CommitmentTree(e), + SqliteClientError::UnsupportedPoolType(pool) => WalletMigrationError::CorruptedData( + format!("Wallet DB contains unsupported pool type {}", pool), + ), + SqliteClientError::BalanceError(e) => WalletMigrationError::BalanceError(e), + SqliteClientError::TableNotEmpty => unreachable!("wallet already initialized"), + SqliteClientError::BlockConflict(_) + | SqliteClientError::NonSequentialBlocks + | SqliteClientError::RequestedRewindInvalid(_, _) + | SqliteClientError::KeyDerivationError(_) + | SqliteClientError::AccountIdDiscontinuity + | SqliteClientError::AccountIdOutOfRange + | 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") + } + } +} + /// Sets up the internal structure of the data database. /// /// This procedure will automatically perform migration operations to update the wallet database to @@ -83,26 +185,67 @@ 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(schemer::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(schemer::MigratorError::Migration { error: WalletMigrationError::SeedNotRelevant, .. })` +/// or `Err(schemer::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, imported +/// accounts are not migrated. +/// /// 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 std::error::Error; +/// # use secrecy::SecretVec; +/// # use tempfile::NamedTempFile; /// use zcash_primitives::consensus::Network; /// use zcash_client_sqlite::{ /// WalletDb, -/// wallet::init::init_wallet_db, +/// 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)?; +/// 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 @@ -111,17 +254,20 @@ impl std::error::Error for WalletMigrationError { // 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

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

, + wdb: &mut WalletDb, seed: Option>, target_migrations: &[Uuid], + verify_seed_relevance: bool, ) -> Result<(), MigratorError> { + let seed = seed.map(Rc::new); + // Turn off foreign keys, and ensure that table replacement/modification // does not break views wdb.conn @@ -135,7 +281,7 @@ fn init_wallet_db_internal( let mut migrator = Migrator::new(adapter); migrator - .register_multiple(migrations::all_migrations(&wdb.params, seed)) + .register_multiple(migrations::all_migrations(&wdb.params, seed.clone())) .expect("Wallet migration registration should have been successful."); if target_migrations.is_empty() { migrator.up(None)?; @@ -147,267 +293,276 @@ fn init_wallet_db_internal( wdb.conn .execute("PRAGMA foreign_keys = ON", []) .map_err(|e| MigratorError::Adapter(WalletMigrationError::from(e)))?; - Ok(()) -} - -/// Initialises the data database with the given set of account [`UnifiedFullViewingKey`]s. -/// -/// **WARNING** This method should be used with care, and should ordinarily be unnecessary. -/// Prefer to use [`WalletWrite::create_account`] instead. -/// -/// [`WalletWrite::create_account`]: zcash_client_backend::data_api::WalletWrite::create_account -/// -/// 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) -/// -/// # Examples -/// -/// ``` -/// # #[cfg(feature = "transparent-inputs")] -/// # { -/// use tempfile::NamedTempFile; -/// use secrecy::Secret; -/// use std::collections::HashMap; -/// -/// use zcash_primitives::{ -/// consensus::{Network, Parameters}, -/// zip32::{AccountId, ExtendedSpendingKey} -/// }; -/// -/// use zcash_client_backend::{ -/// keys::{ -/// sapling, -/// UnifiedFullViewingKey -/// }, -/// }; -/// -/// use zcash_client_sqlite::{ -/// WalletDb, -/// wallet::init::{init_accounts_table, init_wallet_db} -/// }; -/// -/// 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(); -/// # } -/// ``` -/// -/// [`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); - } - // 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); + // 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()) + } + } } } - // Insert accounts atomically - wdb.conn.execute("BEGIN IMMEDIATE", [])?; - for (account, key) in keys.iter() { - wallet::add_account(wdb, *account, key)?; - } - wdb.conn.execute("COMMIT", [])?; - - Ok(()) -} - -/// Initialises the data database with the given block. -/// -/// This enables a newly-created database to be immediately-usable, without needing to -/// synchronise historic blocks. -/// -/// # Examples -/// -/// ``` -/// use tempfile::NamedTempFile; -/// use zcash_primitives::{ -/// block::BlockHash, -/// consensus::{BlockHeight, Network}, -/// }; -/// use zcash_client_sqlite::{ -/// WalletDb, -/// wallet::init::init_blocks_table, -/// }; -/// -/// // The block height. -/// let height = BlockHeight::from_u32(500_000); -/// // The hash of the block header. -/// let hash = BlockHash([0; 32]); -/// // The nTime field from the block header. -/// let time = 12_3456_7890; -/// // The serialized Sapling commitment tree as of this block. -/// // Pre-compute and hard-code, or obtain from a service. -/// let sapling_tree = &[]; -/// -/// let data_file = NamedTempFile::new().unwrap(); -/// let db = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap(); -/// init_blocks_table(&db, height, hash, time, sapling_tree); -/// ``` -pub fn init_blocks_table

( - wdb: &WalletDb

, - height: BlockHeight, - hash: BlockHash, - time: u32, - sapling_tree: &[u8], -) -> Result<(), SqliteClientError> { - let mut empty_check = wdb.conn.prepare("SELECT * FROM blocks LIMIT 1")?; - if empty_check.exists([])? { - return Err(SqliteClientError::TableNotEmpty); - } - - 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()?, - ], - )?; - Ok(()) } #[cfg(test)] #[allow(deprecated)] mod tests { - use rusqlite::{self, ToSql}; + use rusqlite::{self, named_params, ToSql}; use secrecy::Secret; - use std::collections::HashMap; + use tempfile::NamedTempFile; use zcash_client_backend::{ - address::RecipientAddress, - data_api::WalletRead, + address::Address, + data_api::scanning::ScanPriority, encoding::{encode_extended_full_viewing_key, encode_payment_address}, - keys::{sapling, UnifiedFullViewingKey, UnifiedSpendingKey}, + keys::{sapling, UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey}, }; + use ::sapling::zip32::ExtendedFullViewingKey; use zcash_primitives::{ - block::BlockHash, - consensus::{BlockHeight, BranchId, Parameters}, + consensus::{ + self, BlockHeight, BranchId, Network, NetworkConstants, NetworkUpgrade, Parameters, + }, transaction::{TransactionData, TxVersion}, - zip32::sapling::ExtendedFullViewingKey, + zip32::AccountId, }; - use crate::{ - error::SqliteClientError, - tests::{self, network}, - AccountId, WalletDb, - }; + use crate::{testing::TestBuilder, wallet::scanning::priority_code, WalletDb, UA_TRANSPARENT}; - use super::{init_accounts_table, init_blocks_table, init_wallet_db}; + use super::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, + zcash_primitives::zip32::DiversifierIndex, }; #[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().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, + r#"CREATE TABLE "accounts" ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + account_kind INTEGER NOT NULL DEFAULT 0, + 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 = 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 + AND hd_account_index IS NULL + ) + ) + )"#, + r#"CREATE TABLE "addresses" ( + account_id INTEGER NOT NULL, diversifier_index_be BLOB NOT NULL, address TEXT NOT NULL, cached_transparent_receiver_address TEXT, - FOREIGN KEY (account) REFERENCES accounts(account), - CONSTRAINT diversification UNIQUE (account, diversifier_index_be) - )", + FOREIGN KEY (account_id) REFERENCES accounts(id), + CONSTRAINT diversification UNIQUE (account_id, diversifier_index_be) + )"#, "CREATE TABLE blocks ( height INTEGER PRIMARY KEY, hash BLOB NOT NULL, time INTEGER NOT NULL, - sapling_tree BLOB 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)", + "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 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) )", - "CREATE TABLE sapling_received_notes ( - id_note INTEGER PRIMARY KEY, + "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 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 + )", + "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) + )", + "CREATE TABLE orchard_tree_checkpoints ( + checkpoint_id INTEGER PRIMARY KEY, + position INTEGER + )", + "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 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) + )", + r#"CREATE TABLE "sapling_received_notes" ( + id INTEGER PRIMARY KEY, tx INTEGER NOT NULL, output_index INTEGER NOT NULL, - account 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, - spent INTEGER, + commitment_tree_position INTEGER, + recipient_key_scope INTEGER, FOREIGN KEY (tx) REFERENCES transactions(id_tx), - FOREIGN KEY (account) REFERENCES accounts(account), - FOREIGN KEY (spent) REFERENCES transactions(id_tx), + FOREIGN KEY (account_id) REFERENCES accounts(id), CONSTRAINT tx_output UNIQUE (tx, output_index) + )"#, + "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 + )", + "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) + )", + "CREATE TABLE sapling_tree_checkpoints ( + checkpoint_id INTEGER PRIMARY KEY, + position INTEGER + )", + "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_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 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 + ) )", "CREATE TABLE schemer_migrations ( id blob PRIMARY KEY )", - "CREATE TABLE \"sent_notes\" ( - id_note INTEGER PRIMARY KEY, + 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 INTEGER NOT NULL, + from_account_id INTEGER NOT NULL, to_address TEXT, - to_account INTEGER, + to_account_id 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), + 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) != (to_account IS NOT NULL) + (to_address IS NOT NULL) OR (to_account_id IS NOT NULL) ) - )", + )"#, + // Internal table created by SQLite when we started using `AUTOINCREMENT`. + "CREATE TABLE sqlite_sequence(name,seq)", "CREATE TABLE transactions ( id_tx INTEGER PRIMARY KEY, txid BLOB NOT NULL UNIQUE, @@ -419,23 +574,39 @@ mod tests { fee INTEGER, FOREIGN KEY (block) REFERENCES blocks(height) )", - "CREATE TABLE \"utxos\" ( - id_utxo INTEGER PRIMARY KEY, - received_by_account INTEGER NOT NULL, + "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) + )", + "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) + )", + r#"CREATE TABLE "utxos" ( + 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, - spent_in_tx INTEGER, - FOREIGN KEY (received_by_account) REFERENCES accounts(account), - FOREIGN KEY (spent_in_tx) REFERENCES transactions(id_tx), + FOREIGN KEY (received_by_account_id) REFERENCES accounts(id), CONSTRAINT tx_outpoint UNIQUE (prevout_txid, prevout_idx) - )", + )"#, ]; - let mut tables_query = db_data + let mut tables_query = st + .wallet() .conn .prepare("SELECT sql FROM sqlite_schema WHERE type = 'table' ORDER BY tbl_name") .unwrap(); @@ -450,144 +621,390 @@ mod tests { expected_idx += 1; } + let expected_indices = vec![ + r#"CREATE UNIQUE INDEX accounts_ufvk ON "accounts" (ufvk)"#, + r#"CREATE UNIQUE INDEX accounts_uivk ON "accounts" (uivk)"#, + r#"CREATE UNIQUE INDEX hd_account ON "accounts" (hd_seed_fingerprint, hd_account_index)"#, + r#"CREATE INDEX "addresses_accounts" ON "addresses" ( + "account_id" ASC + )"#, + r#"CREATE INDEX nf_map_locator_idx ON nullifier_map(block_height, tx_index)"#, + r#"CREATE INDEX orchard_received_notes_account ON orchard_received_notes ( + account_id ASC + )"#, + r#"CREATE INDEX orchard_received_notes_tx ON orchard_received_notes ( + tx ASC + )"#, + r#"CREATE INDEX "sapling_received_notes_account" ON "sapling_received_notes" ( + "account_id" ASC + )"#, + r#"CREATE INDEX "sapling_received_notes_tx" ON "sapling_received_notes" ( + "tx" ASC + )"#, + r#"CREATE INDEX sent_notes_from_account ON "sent_notes" (from_account_id)"#, + r#"CREATE INDEX sent_notes_to_account ON "sent_notes" (to_account_id)"#, + r#"CREATE INDEX sent_notes_tx ON "sent_notes" (tx)"#, + r#"CREATE INDEX utxos_received_by_account ON "utxos" (received_by_account_id)"#, + ]; + let mut indices_query = st + .wallet() + .conn + .prepare("SELECT sql FROM sqlite_master WHERE type = 'index' AND sql != '' ORDER BY tbl_name, name") + .unwrap(); + let mut rows = indices_query.query([]).unwrap(); + let mut expected_idx = 0; + while let Some(row) = rows.next().unwrap() { + let sql: String = row.get(0).unwrap(); + assert_eq!( + re.replace_all(&sql, " "), + re.replace_all(expected_indices[expected_idx], " ") + ); + expected_idx += 1; + } + let expected_views = vec![ + // v_orchard_shard_scan_ranges + 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(st.network().activation_height(NetworkUpgrade::Nu5).unwrap()), + ), + //v_orchard_shard_unscanned_ranges + 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), + ), + // v_orchard_shards_scan_state + "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".to_owned(), + // v_received_note_spends + "CREATE VIEW v_received_note_spends AS + SELECT + 2 AS pool, + sapling_received_note_id AS received_note_id, + transaction_id + FROM sapling_received_note_spends + UNION + SELECT + 3 AS pool, + orchard_received_note_id AS received_note_id, + transaction_id + FROM orchard_received_note_spends".to_owned(), + // v_received_notes + "CREATE VIEW v_received_notes AS + SELECT + sapling_received_notes.id AS id_within_pool_table, + sapling_received_notes.tx, + 2 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, 2, sapling_received_notes.output_index) + UNION + SELECT + orchard_received_notes.id AS id_within_pool_table, + orchard_received_notes.tx, + 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)".to_owned(), + // v_sapling_shard_scan_ranges + 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(st.network().activation_height(NetworkUpgrade::Sapling).unwrap()), + ), + // v_sapling_shard_unscanned_ranges + 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) + ), + // v_sapling_shards_scan_state + "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".to_owned(), // 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 + 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, + 0 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, + 0 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".to_owned(), + // 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.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 + 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 - 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 + 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 - 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" + 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".to_owned(), ]; - let mut views_query = db_data + let mut views_query = st + .wallet() .conn .prepare("SELECT sql FROM sqlite_schema WHERE type = 'view' ORDER BY tbl_name") .unwrap(); @@ -597,7 +1014,7 @@ 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], " ") ); expected_idx += 1; } @@ -605,8 +1022,8 @@ mod tests { #[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 +1106,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 +1126,25 @@ mod tests { Ok(()) } + let data_file = NamedTempFile::new().unwrap(); + let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap(); + let seed = [0xab; 32]; - let account = AccountId::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); 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 +1243,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,7 +1262,7 @@ 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')", [], )?; @@ -860,8 +1282,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 +1297,25 @@ mod tests { Ok(()) } + let data_file = NamedTempFile::new().unwrap(); + let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap(); + let seed = [0xab; 32]; - let account = AccountId::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); 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 +1414,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_new(false, true, 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 +1438,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( @@ -1020,157 +1464,90 @@ mod tests { 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(); + let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork).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(); + let seed = [0xab; 32]; + let account = AccountId::ZERO; + let secret_key = UnifiedSpendingKey::from_seed(&db_data.params, &seed, account).unwrap(); - #[cfg(feature = "transparent-inputs")] - let ufvk = UnifiedFullViewingKey::new( - Some( - transparent::AccountPrivKey::from_seed(&network(), &seed, account) - .unwrap() - .to_account_pubkey(), - ), - Some(dfvk), - None, + init_main( + &mut db_data, + &secret_key.to_unified_full_viewing_key(), + account, ) .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) + init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))), + Ok(_) ); - - // should succeed if there are no gaps - assert!(init_accounts_table(&db_data, &ufvks(&[0, 1, 2])).is_ok()); - } - - #[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, - &[], - ) - .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); } #[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).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) + .unwrap(); + assert_matches!( + db_data.get_account(account_id), + Ok(Some(account)) if matches!( + account.kind, + AccountSource::Derived{account_index, ..} if 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(schemer::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"); + let (ua, di) = + wallet::get_current_address(&db_data.conn, &db_data.params, account_id) + .unwrap() + .expect("create_account generated the first address"); assert_eq!(DiversifierIndex::from(tv.diversifier_index), di); assert_eq!(tvua.transparent(), ua.transparent()); assert_eq!(tvua.sapling(), ua.sapling()); + #[cfg(not(feature = "orchard"))] assert_eq!(tv.unified_addr, ua.encode(&Network::MainNetwork)); - ops.get_next_available_address(account) + // hardcoded with knowledge of what's coming next + let ua_request = UnifiedAddressRequest::unsafe_new(false, true, true); + 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..0cfba40e93 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations.rs @@ -1,40 +1,76 @@ +mod add_account_birthdays; mod add_transaction_views; mod add_utxo_account; mod addresses_table; +mod ensure_orchard_ua_receiver; +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 ufvk_support; mod utxos_table; +mod v_sapling_shard_unscanned_ranges; mod v_transactions_net; +mod v_transactions_note_uniqueness; +mod v_transactions_shielding_balance; +mod v_transactions_transparent_history; +mod v_tx_outputs_use_legacy_false; +mod wallet_summaries; + +use std::rc::Rc; use schemer_rusqlite::RusqliteMigration; use secrecy::SecretVec; -use zcash_primitives::consensus; +use zcash_protocol::consensus; use super::WalletMigrationError; pub(super) fn all_migrations( params: &P, - seed: Option>, + 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 + // | + // ensure_orchard_ua_receiver 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 +82,37 @@ 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(), + }), ] } 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..34fc2eb498 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/add_account_birthdays.rs @@ -0,0 +1,74 @@ +//! This migration adds a birthday height to each account record. + +use std::collections::HashSet; + +use schemer_rusqlite::RusqliteMigration; +use uuid::Uuid; +use zcash_primitives::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); + +pub(super) struct Migration

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

schemer::Migration for Migration

{ + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + [shardtree_support::MIGRATION_ID].into_iter().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 foreign_keys=OFF; + PRAGMA legacy_alter_table = ON; + DROP TABLE accounts; + ALTER TABLE accounts_new RENAME TO accounts; + PRAGMA legacy_alter_table = OFF; + PRAGMA foreign_keys=ON;", + u32::from( + self.params + .activation_height(NetworkUpgrade::Sapling) + .unwrap() + ) + ))?; + + Ok(()) + } + + fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), Self::Error> { + Err(WalletMigrationError::CannotRevert(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..a84f99a05b 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,7 +2,6 @@ use std::collections::HashSet; use rusqlite::{self, types::ToSql, OptionalExtension}; -use schemer::{self}; use schemer_rusqlite::RusqliteMigration; use uuid::Uuid; @@ -17,12 +16,7 @@ use zcash_primitives::{ 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); pub(crate) struct Migration; @@ -272,8 +266,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)) } } @@ -283,10 +276,9 @@ mod tests { use tempfile::NamedTempFile; use zcash_client_backend::keys::UnifiedSpendingKey; - use zcash_primitives::zip32::AccountId; + use zcash_primitives::{consensus::Network, zip32::AccountId}; use crate::{ - tests, wallet::init::{init_wallet_db_internal, migrations::addresses_table}, WalletDb, }; @@ -310,24 +302,24 @@ mod tests { #[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).unwrap(); + init_wallet_db_internal(&mut db_data, None, &[addresses_table::MIGRATION_ID], false) + .unwrap(); + let usk = UnifiedSpendingKey::from_seed(&network, &[0u8; 32][..], AccountId::ZERO).unwrap(); let ufvk = usk.to_unified_full_viewing_key(); db_data .conn .execute( "INSERT INTO accounts (account, ufvk) VALUES (0, ?)", - params![ufvk.encode(&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 +337,7 @@ mod tests { VALUES (0, 4, 0, '', 7, '', 'c', true, X'63');", ).unwrap(); - init_wallet_db_internal(&mut db_data, None, &[super::MIGRATION_ID]).unwrap(); + init_wallet_db_internal(&mut db_data, None, &[super::MIGRATION_ID], false).unwrap(); let mut q = db_data .conn @@ -402,12 +394,21 @@ mod tests { #[test] #[cfg(feature = "transparent-inputs")] fn migrate_from_wm2() { + use zcash_client_backend::keys::UnifiedAddressRequest; + use zcash_primitives::{ + legacy::keys::NonHardenedChildIndex, transaction::components::amount::NonNegativeAmount, + }; + + 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(); + let mut db_data = WalletDb::for_path(data_file.path(), network).unwrap(); init_wallet_db_internal( &mut db_data, None, &[utxos_table::MIGRATION_ID, ufvk_support::MIGRATION_ID], + false, ) .unwrap(); @@ -424,7 +425,7 @@ mod tests { sequence: 0, }], vout: vec![TxOut { - value: Amount::from_i64(1100000000).unwrap(), + value: NonNegativeAmount::const_from_u64(1100000000), script_pubkey: Script(vec![]), }], authorization: Authorized, @@ -439,28 +440,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_new( + false, + true, + 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,7 +481,7 @@ mod tests { ) .unwrap(); - init_wallet_db_internal(&mut db_data, None, &[super::MIGRATION_ID]).unwrap(); + init_wallet_db_internal(&mut db_data, None, &[super::MIGRATION_ID], false).unwrap(); let fee = db_data .conn 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..fc64ff26c4 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,11 +1,9 @@ //! 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 std::collections::HashSet; use uuid::Uuid; - use zcash_primitives::consensus; use super::{addresses_table, utxos_table}; @@ -13,21 +11,22 @@ 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, + rusqlite::{named_params, OptionalExtension}, + std::collections::HashMap, + zcash_client_backend::{ + encoding::AddressCodec, keys::UnifiedFullViewingKey, wallet::TransparentAddressMetadata, + }, + zcash_keys::address::Address, + zcash_primitives::legacy::{ + keys::{IncomingViewingKey, NonHardenedChildIndex}, + TransparentAddress, + }, + zip32::{AccountId, DiversifierIndex, 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); pub(super) struct Migration

{ pub(super) _params: P, @@ -65,23 +64,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 +125,108 @@ 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> { + let mut ret: HashMap> = HashMap::new(); + + // Get all UAs derived + let mut ua_query = conn + .prepare("SELECT address, diversifier_index_be FROM addresses WHERE account = :account")?; + let mut rows = ua_query.query(named_params![":account": u32::from(account)])?; + + while let Some(row) = rows.next()? { + let ua_str: String = row.get(0)?; + let di_vec: Vec = row.get(1)?; + let mut di: [u8; 11] = di_vec.try_into().map_err(|_| { + SqliteClientError::CorruptedData("Diversifier index is not an 11-byte value".to_owned()) + })?; + di.reverse(); // BE -> LE conversion + + let ua = Address::decode(params, &ua_str) + .ok_or_else(|| { + SqliteClientError::CorruptedData("Not a valid Zcash recipient address".to_owned()) + }) + .and_then(|addr| match addr { + Address::Unified(ua) => Ok(ua), + _ => Err(SqliteClientError::CorruptedData(format!( + "Addresses table contains {} which is not a unified address", + ua_str, + ))), + })?; + + if let Some(taddr) = ua.transparent() { + let index = NonHardenedChildIndex::from_index( + DiversifierIndex::from(di).try_into().map_err(|_| { + SqliteClientError::CorruptedData( + "Unable to get diversifier for transparent address.".to_owned(), + ) + })?, + ) + .ok_or_else(|| { + SqliteClientError::CorruptedData( + "Unexpected hardened index for transparent address.".to_owned(), + ) + })?; + + ret.insert( + *taddr, + Some(TransparentAddressMetadata::new( + Scope::External.into(), + index, + )), + ); + } + } + + if let Some((taddr, child_index)) = get_legacy_transparent_address(params, conn, account)? { + ret.insert( + taddr, + Some(TransparentAddressMetadata::new( + Scope::External.into(), + child_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::HdwalletError) + }) + .transpose() + } else { + Ok(None) } } 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..5846791644 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/addresses_table.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/addresses_table.rs @@ -1,13 +1,15 @@ use std::collections::HashSet; -use rusqlite::Transaction; +use rusqlite::{named_params, Transaction}; use schemer; use schemer_rusqlite::RusqliteMigration; use uuid::Uuid; -use zcash_client_backend::{address::RecipientAddress, keys::UnifiedFullViewingKey}; -use zcash_primitives::{consensus, zip32::AccountId}; +use zcash_client_backend::{address::Address, keys::UnifiedFullViewingKey}; +use zcash_keys::{address::UnifiedAddress, encoding::AddressCodec, keys::UnifiedAddressRequest}; +use zcash_primitives::consensus; +use zip32::{AccountId, DiversifierIndex}; -use crate::wallet::{add_account_internal, init::WalletMigrationError}; +use crate::{wallet::init::WalletMigrationError, UA_TRANSPARENT}; #[cfg(feature = "transparent-inputs")] use zcash_primitives::legacy::keys::IncomingViewingKey; @@ -16,14 +18,7 @@ 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); pub(crate) struct Migration { pub(crate) params: P, @@ -67,8 +62,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 +72,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_new(false, true, 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 +100,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 +148,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_new( + false, + true, + UA_TRANSPARENT, + ))?; + insert_address(transaction, &self.params, account, d_idx, &address)?; } transaction.execute_batch( @@ -170,7 +174,42 @@ 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(()) +} 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..8818b5f76c --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/ensure_orchard_ua_receiver.rs @@ -0,0 +1,200 @@ +//! This migration ensures that an Orchard receiver exists in the wallet's default Unified address. +use std::collections::HashSet; + +use rusqlite::named_params; +use schemer_rusqlite::RusqliteMigration; +use uuid::Uuid; + +use zcash_client_backend::keys::{ + UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedIncomingViewingKey, +}; +use zcash_primitives::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); + +pub(super) struct Migration

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

schemer::Migration for Migration

{ + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + [orchard_received_notes::MIGRATION_ID].into_iter().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( + r#" + 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_new(UA_ORCHARD, true, 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_client_backend::keys::{UnifiedAddressRequest, UnifiedSpendingKey}; + use zcash_keys::address::Address; + use zcash_primitives::consensus::Network; + + use crate::{ + wallet::init::{init_wallet_db, init_wallet_db_internal, migrations::addresses_table}, + WalletDb, UA_ORCHARD, UA_TRANSPARENT, + }; + + #[test] + fn init_migrate_add_orchard_receiver() { + let data_file = NamedTempFile::new().unwrap(); + let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap(); + + let seed = vec![0x10; 32]; + let account_id = 0u32; + let ufvk = UnifiedSpendingKey::from_seed( + &db_data.params, + &seed, + zip32::AccountId::try_from(account_id).unwrap(), + ) + .unwrap() + .to_unified_full_viewing_key(); + + assert_matches!( + init_wallet_db_internal( + &mut db_data, + Some(SecretVec::new(seed.clone())), + &[addresses_table::MIGRATION_ID], + false + ), + Ok(_) + ); + + // Manually create an entry in the addresses table for an address that lacks an Orchard + // receiver. + db_data + .conn + .execute( + "INSERT INTO accounts (account, ufvk) VALUES (:account_id, :ufvk)", + named_params![ + ":account_id": account_id, + ":ufvk": ufvk.encode(&db_data.params) + ], + ) + .unwrap(); + + let (addr, diversifier_index) = ufvk + .default_address(UnifiedAddressRequest::unsafe_new( + false, + true, + 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.orchard().is_none()); + assert!(ua.sapling().is_some()); + assert_eq!(ua.transparent().is_some(), UA_TRANSPARENT); + } + other => panic!("Unexpected result from address decoding: {:?}", other), + } + + assert_matches!( + init_wallet_db(&mut db_data, Some(SecretVec::new(seed))), + Ok(_) + ); + + match db_data + .conn + .query_row("SELECT address FROM addresses", [], |row| { + Ok(Address::decode(&db_data.params, &row.get::<_, String>(0)?).unwrap()) + }) { + Ok(Address::Unified(ua)) => { + assert_eq!(ua.orchard().is_some(), UA_ORCHARD); + assert!(ua.sapling().is_some()); + assert_eq!(ua.transparent().is_some(), UA_TRANSPARENT); + } + other => panic!("Unexpected result from address decoding: {:?}", other), + } + } +} 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..851a21ff2b --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/full_account_ids.rs @@ -0,0 +1,572 @@ +use std::{collections::HashSet, rc::Rc}; + +use crate::wallet::{account_kind_code, init::WalletMigrationError}; +use rusqlite::{named_params, OptionalExtension, Transaction}; +use schemer_rusqlite::RusqliteMigration; +use secrecy::{ExposeSecret, SecretVec}; +use uuid::Uuid; +use zcash_client_backend::{data_api::AccountSource, keys::UnifiedSpendingKey}; +use zcash_keys::keys::UnifiedFullViewingKey; +use zcash_primitives::consensus; +use zip32::fingerprint::SeedFingerprint; + +use super::{ + add_account_birthdays, receiving_key_scopes, v_transactions_note_uniqueness, wallet_summaries, +}; + +/// 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, +} + +impl schemer::Migration for Migration

{ + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + [ + receiving_key_scopes::MIGRATION_ID, + add_account_birthdays::MIGRATION_ID, + v_transactions_note_uniqueness::MIGRATION_ID, + wallet_summaries::MIGRATION_ID, + ] + .into_iter() + .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 { + seed_fingerprint: SeedFingerprint::from_bytes([0; 32]), + account_index: zip32::AccountId::ZERO, + }); + let account_kind_imported = account_kind_code(AccountSource::Imported); + transaction.execute_batch(&format!( + r#" + PRAGMA foreign_keys = OFF; + PRAGMA legacy_alter_table = ON; + + 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 = "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": ufvk_parsed.orchard().map(|k| k.to_bytes()), + ":sapling_fvk_item_cache": ufvk_parsed.sapling().map(|k| k.to_bytes()), + ":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#" + 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; + "#, + )?; + + // 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; + ")?; + + transaction.execute_batch( + r#" + PRAGMA legacy_alter_table = OFF; + PRAGMA foreign_keys = ON; + "#, + )?; + + Ok(()) + } + + fn down(&self, _transaction: &Transaction) -> Result<(), WalletMigrationError> { + Err(WalletMigrationError::CannotRevert(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..74141e3afc 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/initial_setup.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/initial_setup.rs @@ -9,14 +9,7 @@ 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; @@ -111,6 +104,6 @@ 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)) } } 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..06300e5875 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/nullifier_map.rs @@ -0,0 +1,73 @@ +//! This migration adds a table for storing mappings from nullifiers to the transaction +//! they are revealed in. + +use std::collections::HashSet; + +use schemer_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); + +pub(super) struct Migration; + +impl schemer::Migration for Migration { + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + [received_notes_nullable_nf::MIGRATION_ID] + .into_iter() + .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(()) + } +} 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..550dc14f43 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/orchard_received_notes.rs @@ -0,0 +1,314 @@ +//! This migration adds tables to the wallet database that are needed to persist Orchard received +//! notes. + +use std::collections::HashSet; + +use schemer_rusqlite::RusqliteMigration; +use uuid::Uuid; +use zcash_client_backend::{PoolType, ShieldedProtocol}; + +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); + +pub(super) struct Migration; + +impl schemer::Migration for Migration { + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + [full_account_ids::MIGRATION_ID].into_iter().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::Shielded(ShieldedProtocol::Sapling)); + let orchard_pool_code = pool_code(PoolType::Shielded(ShieldedProtocol::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::Shielded(ShieldedProtocol::Sapling)); + let orchard_pool_code = pool_code(PoolType::Shielded(ShieldedProtocol::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)) + } +} 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..10e2796b8f --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/orchard_shardtree.rs @@ -0,0 +1,216 @@ +//! 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 schemer_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::{init::WalletMigrationError, scan_queue_extrema, scanning::priority_code}; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0x3a6487f7_e068_42bb_9d12_6bb8dbe6da00); + +pub(super) struct Migration

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

schemer::Migration for Migration

{ + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + [shardtree_support::MIGRATION_ID].into_iter().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) = scan_queue_extrema(transaction)?.and_then(|r| { + self.params + .activation_height(NetworkUpgrade::Nu5) + .map(|orchard_activation| std::cmp::max(orchard_activation, *r.end())) + }) { + // 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)) + } +} 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..e5800cdccf 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 @@ -11,12 +11,7 @@ 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); pub(crate) struct Migration; @@ -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)) } } @@ -233,10 +227,9 @@ mod tests { use tempfile::NamedTempFile; use zcash_client_backend::keys::UnifiedSpendingKey; - use zcash_primitives::zip32::AccountId; + use zcash_primitives::{consensus::Network, zip32::AccountId}; use crate::{ - tests, wallet::init::{init_wallet_db_internal, migrations::v_transactions_net}, WalletDb, }; @@ -244,25 +237,30 @@ mod tests { #[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).unwrap(); + init_wallet_db_internal( + &mut db_data, + None, + &[v_transactions_net::MIGRATION_ID], + false, + ) + .unwrap(); // Create an account in the wallet - let usk0 = - UnifiedSpendingKey::from_seed(&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 +269,7 @@ 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(); + init_wallet_db_internal(&mut db_data, None, &[super::MIGRATION_ID], false).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..1c1dd6bcc3 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/receiving_key_scopes.rs @@ -0,0 +1,748 @@ +//! 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::{self, named_params}; +use schemer; +use schemer_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, keys::UnifiedFullViewingKey}; +use zcash_primitives::{ + consensus::{self, BlockHeight, BranchId}, + transaction::{ + components::{amount::NonNegativeAmount, sapling::zip212_enforcement}, + Transaction, + }, + zip32::Scope, +}; + +use crate::{ + wallet::{ + commitment_tree::SqliteShardStore, + init::{migrations::shardtree_support, WalletMigrationError}, + scan_queue_extrema, scope_code, + }, + PRUNING_DEPTH, SAPLING_TABLES_PREFIX, +}; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0xee89ed2b_c1c2_421e_9e98_c1e3e54a7fc2); + +pub(super) struct Migration

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

schemer::Migration for Migration

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

{ + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + transaction.execute_batch( + &format!( + "ALTER TABLE sapling_received_notes ADD COLUMN recipient_key_scope INTEGER NOT NULL DEFAULT {};", + scope_code(Scope::External) + ) + )?; + + // For all notes we have to determine whether they were actually sent to the internal key + // or the external key for the account, so we trial-decrypt the original output with the + // internal IVK and update the persisted scope value if necessary. We check all notes, + // rather than just change notes, because shielding notes may not have been considered + // change. + let mut stmt_select_notes = transaction.prepare( + "SELECT + id_note, + output_index, + transactions.raw, + transactions.block, + transactions.expiry_height, + accounts.ufvk, + diversifier, + value, + rcm, + commitment_tree_position + FROM sapling_received_notes + INNER JOIN accounts on accounts.account = sapling_received_notes.account + INNER JOIN transactions ON transactions.id_tx = sapling_received_notes.tx", + )?; + + // In the case that we don't have the raw transaction + let mut commitment_tree = ShardTree::new( + SqliteShardStore::<_, _, SAPLING_SHARD_HEIGHT>::from_connection( + transaction, + SAPLING_TABLES_PREFIX, + )?, + PRUNING_DEPTH as usize, + ); + + let mut rows = stmt_select_notes.query([])?; + while let Some(row) = rows.next()? { + let note_id: i64 = row.get(0)?; + let output_index: usize = row.get(1)?; + let tx_data_opt: Option> = row.get(2)?; + + let tx_height = row.get::<_, Option>(3)?.map(BlockHeight::from); + let tx_expiry = row.get::<_, Option>(4)?; + let zip212_height = tx_height.map_or_else( + || { + tx_expiry.filter(|h| *h != 0).map_or_else( + || scan_queue_extrema(transaction).map(|extrema| extrema.map(|r| *r.end())), + |h| Ok(Some(BlockHeight::from(h))), + ) + }, + |h| Ok(Some(h)), + )?; + + let zip212_enforcement = zip212_height.map_or_else( + || { + // If the transaction has not been mined and the expiry height is set to 0 (no + // expiry) an no chain tip information is available, then we assume it can only + // be mined under ZIP 212 enforcement rules, so we default to `On` + Zip212Enforcement::On + }, + |h| zip212_enforcement(&self.params, h), + ); + + let ufvk_str: String = row.get(5)?; + let ufvk = UnifiedFullViewingKey::decode(&self.params, &ufvk_str).map_err(|e| { + WalletMigrationError::CorruptedData(format!("Stored UFVK was invalid: {:?}", e)) + })?; + + let dfvk = ufvk.sapling().ok_or_else(|| { + WalletMigrationError::CorruptedData( + "UFVK must have a Sapling component to have received Sapling notes.".to_owned(), + ) + })?; + + // We previously set the default to external scope, so we now verify whether the output + // is decryptable using the intenally-scoped IVK and, if so, mark it as such. + if let Some(tx_data) = tx_data_opt { + let tx = Transaction::read(&tx_data[..], BranchId::Canopy).map_err(|e| { + WalletMigrationError::CorruptedData(format!( + "Unable to parse raw transaction: {:?}", + e + )) + })?; + let output = tx + .sapling_bundle() + .and_then(|b| b.shielded_outputs().get(output_index)) + .unwrap_or_else(|| { + panic!("A Sapling output must exist at index {}", output_index) + }); + + let pivk = PreparedIncomingViewingKey::new(&dfvk.to_ivk(Scope::Internal)); + if try_sapling_note_decryption(&pivk, output, zip212_enforcement).is_some() { + transaction.execute( + "UPDATE sapling_received_notes SET recipient_key_scope = :scope + WHERE id_note = :note_id", + named_params! {":scope": scope_code(Scope::Internal), ":note_id": note_id}, + )?; + } + } else { + let diversifier = { + let d: Vec<_> = row.get(6)?; + Diversifier(d[..].try_into().map_err(|_| { + WalletMigrationError::CorruptedData( + "Invalid diversifier length".to_string(), + ) + })?) + }; + + let note_value = + NonNegativeAmount::from_nonnegative_i64(row.get(7)?).map_err(|_e| { + WalletMigrationError::CorruptedData( + "Note values must be nonnegative".to_string(), + ) + })?; + + let rseed = { + let rcm_bytes: [u8; 32] = + row.get::<_, Vec>(8)?[..].try_into().map_err(|_| { + WalletMigrationError::CorruptedData(format!( + "Note {} is invalid", + note_id + )) + })?; + + let rcm = Option::from(jubjub::Fr::from_repr(rcm_bytes)).ok_or_else(|| { + WalletMigrationError::CorruptedData(format!("Note {} is invalid", note_id)) + })?; + + // The wallet database always stores the `rcm` value, and not `rseed`, + // so for note reconstruction we always use `BeforeZip212`. + Rseed::BeforeZip212(rcm) + }; + + let note_commitment_tree_position = + Position::from(u64::try_from(row.get::<_, i64>(9)?).map_err(|_| { + WalletMigrationError::CorruptedData( + "Note commitment tree position invalid.".to_string(), + ) + })?); + + let scope = select_note_scope( + &mut commitment_tree, + dfvk, + &diversifier, + &sapling::value::NoteValue::from_raw(note_value.into_u64()), + &rseed, + note_commitment_tree_position, + )?; + + if scope == Some(Scope::Internal) { + transaction.execute( + "UPDATE sapling_received_notes SET recipient_key_scope = :scope + WHERE id_note = :note_id", + named_params! {":scope": scope_code(Scope::Internal), ":note_id": note_id}, + )?; + } + } + } + + Ok(()) + } + + fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) + } +} + +#[cfg(feature = "transparent-inputs")] +#[cfg(test)] +mod tests { + use std::convert::Infallible; + + use incrementalmerkletree::Position; + use maybe_rayon::{ + iter::{IndexedParallelIterator, ParallelIterator}, + slice::ParallelSliceMut, + }; + use rand_core::OsRng; + use rusqlite::{named_params, params, Connection}; + use tempfile::NamedTempFile; + use zcash_client_backend::{ + data_api::{BlockMetadata, WalletCommitmentTrees, SAPLING_SHARD_HEIGHT}, + decrypt_transaction, + proto::compact_formats::{CompactBlock, CompactTx}, + scanning::{scan_block, Nullifiers, ScanningKeys}, + TransferType, + }; + use zcash_keys::keys::{UnifiedFullViewingKey, UnifiedSpendingKey}; + use zcash_primitives::{ + block::BlockHash, + consensus::{BlockHeight, Network, NetworkUpgrade, Parameters}, + legacy::keys::{IncomingViewingKey, NonHardenedChildIndex}, + memo::MemoBytes, + transaction::{ + builder::{BuildConfig, BuildResult, Builder}, + components::{amount::NonNegativeAmount, transparent}, + fees::fixed, + }, + zip32::{self, Scope}, + }; + use zcash_proofs::prover::LocalTxProver; + + use crate::{ + error::SqliteClientError, + wallet::{ + init::{ + init_wallet_db_internal, + migrations::{add_account_birthdays, shardtree_support, wallet_summaries}, + }, + memo_repr, parse_scope, + sapling::ReceivedSaplingOutput, + }, + AccountId, 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, + }, + ); + builder + .add_transparent_input( + usk0.transparent() + .derive_external_secret_key(NonHardenedChildIndex::ZERO) + .unwrap(), + transparent::OutPoint::new([1; 32], 0), + transparent::TxOut { + value: NonNegativeAmount::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, + NonNegativeAmount::const_from_u64(EXTERNAL_VALUE), + MemoBytes::empty(), + ) + .unwrap(); + builder + .add_sapling_output::( + Some(ovk), + internal_addr, + NonNegativeAmount::const_from_u64(INTERNAL_VALUE), + MemoBytes::empty(), + ) + .unwrap(); + let prover = LocalTxProver::bundled(); + let res = builder + .build( + OsRng, + &prover, + &prover, + &fixed::FeeRule::non_standard(NonNegativeAmount::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.as_ref(), + ":value": output.note().value().inner(), + ":rcm": &rcm.as_ref(), + ":nf": output.nullifier().map(|nf| nf.0.as_ref()), + ":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(()) + } + + #[test] + fn receiving_key_scopes_migration_enhanced() { + let params = Network::TestNetwork; + + // Create wallet upgraded to just before the current migration. + let data_file = NamedTempFile::new().unwrap(); + let mut db_data = WalletDb::for_path(data_file.path(), params).unwrap(); + init_wallet_db_internal( + &mut db_data, + None, + &[ + add_account_birthdays::MIGRATION_ID, + shardtree_support::MIGRATION_ID, + ], + false, + ) + .unwrap(); + + let (ufvk0, height, res) = prepare_wallet_state(&mut db_data); + let tx = res.transaction(); + let account_id = AccountId(0); + + // We can't use `decrypt_and_store_transaction` because we haven't migrated yet. + // Replicate its relevant innards here. + let d_tx = decrypt_transaction( + ¶ms, + height, + tx, + &[(account_id, ufvk0)].into_iter().collect(), + ); + + db_data + .transactionally::<_, _, rusqlite::Error>(|wdb| { + let tx_ref = crate::wallet::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, 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, None) + .unwrap(); + } + } + } + + Ok(()) + }) + .unwrap(); + + // Apply the current migration + init_wallet_db_internal(&mut db_data, None, &[super::MIGRATION_ID], false).unwrap(); + + // There should be two rows in the `sapling_received_notes` table with correct scopes. + let mut q = db_data + .conn + .prepare( + "SELECT value, recipient_key_scope + FROM sapling_received_notes", + ) + .unwrap(); + let mut rows = q.query([]).unwrap(); + let mut row_count = 0; + while let Some(row) = rows.next().unwrap() { + row_count += 1; + let value: u64 = row.get(0).unwrap(); + let scope = parse_scope(row.get(1).unwrap()); + match value { + EXTERNAL_VALUE => assert_eq!(scope, Some(Scope::External)), + INTERNAL_VALUE => assert_eq!(scope, Some(Scope::Internal)), + _ => { + panic!( + "(Value, Scope) pair {:?} is not expected to exist in the wallet.", + (value, scope), + ); + } + } + } + assert_eq!(row_count, 2); + } + + #[test] + fn receiving_key_scopes_migration_non_enhanced() { + let params = Network::TestNetwork; + + // Create wallet upgraded to just before the current migration. + let data_file = NamedTempFile::new().unwrap(); + let mut db_data = WalletDb::for_path(data_file.path(), params).unwrap(); + init_wallet_db_internal( + &mut db_data, + None, + &[ + wallet_summaries::MIGRATION_ID, + shardtree_support::MIGRATION_ID, + ], + false, + ) + .unwrap(); + + let (ufvk0, height, res) = prepare_wallet_state(&mut db_data); + let tx = res.transaction(); + + let mut compact_tx = CompactTx { + hash: tx.txid().as_ref()[..].into(), + ..Default::default() + }; + for output in tx.sapling_bundle().unwrap().shielded_outputs() { + compact_tx.outputs.push(output.into()); + } + let prev_hash = BlockHash([4; 32]); + let mut block = CompactBlock { + height: height.into(), + hash: vec![7; 32], + prev_hash: prev_hash.0[..].into(), + ..Default::default() + }; + block.vtx.push(compact_tx); + let scanning_keys = ScanningKeys::from_account_ufvks([(AccountId(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. + crate::wallet::put_block( + wdb.conn.0, + block.height(), + block.block_hash(), + block.block_time(), + block.sapling().final_tree_size(), + block.sapling().commitments().len().try_into().unwrap(), + #[cfg(feature = "orchard")] + block.orchard().final_tree_size(), + #[cfg(feature = "orchard")] + block.orchard().commitments().len().try_into().unwrap(), + )?; + + for tx in block.transactions() { + let tx_row = crate::wallet::put_tx_meta(wdb.conn.0, tx, block.height())?; + + for output in tx.sapling_outputs() { + put_received_note_before_migration(wdb.conn.0, output, tx_row, None)?; + } + } + + note_positions.extend(block.transactions().iter().flat_map(|wtx| { + wtx.sapling_outputs() + .iter() + .map(|out| out.note_commitment_tree_position()) + })); + + last_scanned_height = Some(block.height()); + let block_commitments = block.into_commitments(); + sapling_commitments.extend(block_commitments.sapling.into_iter().map(Some)); + } + + // We will have a start position and a last scanned height in all cases where + // `blocks` is non-empty. + if let Some(((_, start_position), _)) = start_positions.zip(last_scanned_height) { + // Create subtrees from the note commitments in parallel. + const CHUNK_SIZE: usize = 1024; + let subtrees = sapling_commitments + .par_chunks_mut(CHUNK_SIZE) + .enumerate() + .filter_map(|(i, chunk)| { + let start = start_position + (i * CHUNK_SIZE) as u64; + let end = start + chunk.len() as u64; + + shardtree::LocatedTree::from_iter( + start..end, + SAPLING_SHARD_HEIGHT.into(), + chunk.iter_mut().map(|n| n.take().expect("always Some")), + ) + }) + .map(|res| (res.subtree, res.checkpoints)) + .collect::>(); + + // Update the Sapling note commitment tree with all newly read note commitments + let mut subtrees = subtrees.into_iter(); + wdb.with_sapling_tree_mut::<_, _, SqliteClientError>(move |sapling_tree| { + for (tree, checkpoints) in &mut subtrees { + sapling_tree.insert_tree(tree, checkpoints)?; + } + + Ok(()) + })?; + } + + Ok(()) + }) + .unwrap(); + + // Apply the current migration + init_wallet_db_internal(&mut db_data, None, &[super::MIGRATION_ID], false).unwrap(); + + // There should be two rows in the `sapling_received_notes` table with correct scopes. + let mut q = db_data + .conn + .prepare( + "SELECT value, recipient_key_scope + FROM sapling_received_notes", + ) + .unwrap(); + let mut rows = q.query([]).unwrap(); + let mut row_count = 0; + while let Some(row) = rows.next().unwrap() { + row_count += 1; + let value: u64 = row.get(0).unwrap(); + let scope = parse_scope(row.get(1).unwrap()); + match value { + EXTERNAL_VALUE => assert_eq!(scope, Some(Scope::External)), + INTERNAL_VALUE => assert_eq!(scope, Some(Scope::Internal)), + _ => { + panic!( + "(Value, Scope) pair {:?} is not expected to exist in the wallet.", + (value, scope), + ); + } + } + } + assert_eq!(row_count, 2); + } +} 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..1831c8356c --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/sapling_memo_consistency.rs @@ -0,0 +1,223 @@ +//! 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 schemer_rusqlite::RusqliteMigration; +use uuid::Uuid; +use zcash_client_backend::{decrypt_transaction, keys::UnifiedFullViewingKey}; +use zcash_primitives::{consensus, transaction::TxId, 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); + +pub(super) struct Migration

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

schemer::Migration for Migration

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

{ + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), Self::Error> { + let mut stmt_raw_tx = transaction.prepare( + "SELECT DISTINCT + transactions.id_tx, transactions.txid, + accounts.account, accounts.ufvk + FROM sent_notes + JOIN accounts ON sent_notes.from_account = accounts.account + JOIN transactions ON transactions.id_tx = sent_notes.tx + WHERE transactions.raw IS NOT NULL", + )?; + + let mut rows = stmt_raw_tx.query([])?; + + let mut tx_sent_notes: BTreeMap<(i64, TxId), HashMap> = + BTreeMap::new(); + while let Some(row) = rows.next()? { + let id_tx: i64 = row.get(0)?; + let txid = row.get(1).map(TxId::from_bytes)?; + let account: u32 = row.get(2)?; + let ufvk_str: String = row.get(3)?; + let ufvk = UnifiedFullViewingKey::decode(&self.params, &ufvk_str).map_err(|e| { + WalletMigrationError::CorruptedData(format!( + "Could not decode unified full viewing key for account {}: {:?}", + account, e + )) + })?; + + tx_sent_notes.entry((id_tx, txid)).or_default().insert( + AccountId::try_from(account).map_err(|_| { + WalletMigrationError::CorruptedData("Account ID is invalid".to_owned()) + })?, + ufvk, + ); + } + + let mut stmt_update_sent_memo = transaction.prepare( + "UPDATE sent_notes + SET memo = :memo + WHERE tx = :id_tx + AND output_index = :output_index", + )?; + + for ((id_tx, txid), ufvks) in tx_sent_notes { + let (block_height, tx) = get_transaction(transaction, &self.params, txid) + .map_err(|err| match err { + SqliteClientError::CorruptedData(msg) => { + WalletMigrationError::CorruptedData(msg) + } + SqliteClientError::DbError(err) => WalletMigrationError::DbError(err), + other => WalletMigrationError::CorruptedData(format!( + "An error was encountered decoding transaction data: {:?}", + other + )), + })? + .ok_or_else(|| { + WalletMigrationError::CorruptedData(format!( + "Transaction not found for id {:?}", + txid + )) + })?; + + let decrypted_outputs = decrypt_transaction(&self.params, block_height, &tx, &ufvks); + + // Orchard outputs were not supported as of the wallet states that could require this + // migration. + for d_out in decrypted_outputs.sapling_outputs() { + stmt_update_sent_memo.execute(named_params![ + ":id_tx": id_tx, + ":output_index": d_out.index(), + ":memo": memo_repr(Some(d_out.memo())) + ])?; + } + } + + // Update the `v_transactions` view to avoid counting the empty memo as a memo + transaction.execute_batch( + "DROP VIEW v_transactions; + CREATE VIEW v_transactions AS + WITH + notes AS ( + SELECT sapling_received_notes.account AS account_id, + sapling_received_notes.tx AS id_tx, + 2 AS pool, + sapling_received_notes.value AS value, + CASE + WHEN sapling_received_notes.is_change THEN 1 + ELSE 0 + END AS is_change, + CASE + WHEN sapling_received_notes.is_change THEN 0 + ELSE 1 + END AS received_count, + CASE + WHEN (sapling_received_notes.memo IS NULL OR sapling_received_notes.memo = X'F6') + THEN 0 + ELSE 1 + END AS memo_present + FROM sapling_received_notes + UNION + SELECT utxos.received_by_account AS account_id, + transactions.id_tx AS id_tx, + 0 AS pool, + utxos.value_zat AS value, + 0 AS is_change, + 1 AS received_count, + 0 AS memo_present + FROM utxos + JOIN transactions + ON transactions.txid = utxos.prevout_txid + UNION + SELECT sapling_received_notes.account AS account_id, + sapling_received_notes.spent AS id_tx, + 2 AS pool, + -sapling_received_notes.value AS value, + 0 AS is_change, + 0 AS received_count, + 0 AS memo_present + FROM sapling_received_notes + WHERE sapling_received_notes.spent IS NOT NULL + ), + sent_note_counts AS ( + SELECT sent_notes.from_account AS account_id, + sent_notes.tx AS id_tx, + COUNT(DISTINCT sent_notes.id_note) as sent_notes, + SUM( + CASE + WHEN (sent_notes.memo IS NULL OR sent_notes.memo = X'F6') + THEN 0 + ELSE 1 + END + ) AS memo_count + FROM sent_notes + LEFT JOIN sapling_received_notes + ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) = + (sapling_received_notes.tx, 2, sapling_received_notes.output_index) + WHERE sapling_received_notes.is_change IS NULL + OR sapling_received_notes.is_change = 0 + GROUP BY account_id, id_tx + ), + blocks_max_height AS ( + SELECT MAX(blocks.height) as max_height FROM blocks + ) + SELECT notes.account_id AS account_id, + transactions.id_tx AS id_tx, + transactions.block AS mined_height, + transactions.tx_index AS tx_index, + transactions.txid AS txid, + transactions.expiry_height AS expiry_height, + transactions.raw AS raw, + SUM(notes.value) AS account_balance_delta, + transactions.fee AS fee_paid, + SUM(notes.is_change) > 0 AS has_change, + MAX(COALESCE(sent_note_counts.sent_notes, 0)) AS sent_note_count, + SUM(notes.received_count) AS received_note_count, + SUM(notes.memo_present) + MAX(COALESCE(sent_note_counts.memo_count, 0)) AS memo_count, + blocks.time AS block_time, + ( + blocks.height IS NULL + AND transactions.expiry_height <= blocks_max_height.max_height + ) AS expired_unmined + FROM transactions + JOIN notes ON notes.id_tx = transactions.id_tx + JOIN blocks_max_height + LEFT JOIN blocks ON blocks.height = transactions.block + LEFT JOIN sent_note_counts + ON sent_note_counts.account_id = notes.account_id + AND sent_note_counts.id_tx = notes.id_tx + GROUP BY notes.account_id, transactions.id_tx", + )?; + + Ok(()) + } + + fn down(&self, _: &rusqlite::Transaction) -> Result<(), Self::Error> { + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) + } +} 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..be86cb7d23 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 @@ -11,14 +11,7 @@ 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); pub(super) struct Migration; @@ -81,7 +74,6 @@ 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)) } } 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..924bcb0189 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/shardtree_support.rs @@ -0,0 +1,282 @@ +//! 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::Retention; +use rusqlite::{self, named_params, params}; +use schemer; +use schemer_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::{ + consensus::{self, BlockHeight, NetworkUpgrade}, + merkle_tree::{read_commitment_tree, read_incremental_witness}, +}; + +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); + +pub(super) struct Migration

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

schemer::Migration for Migration

{ + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + [received_notes_nullable_nf::MIGRATION_ID] + .into_iter() + .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, + is_marked: false, + }, + ) + .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)) + } +} 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..b4af042352 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs @@ -1,5 +1,5 @@ //! 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; @@ -8,8 +8,9 @@ use secrecy::{ExposeSecret, SecretVec}; use uuid::Uuid; use zcash_client_backend::{ - address::RecipientAddress, data_api::PoolType, keys::UnifiedSpendingKey, + address::Address, keys::UnifiedSpendingKey, PoolType, ShieldedProtocol, }; +use zcash_keys::keys::UnifiedAddressRequest; use zcash_primitives::{consensus, zip32::AccountId}; #[cfg(feature = "transparent-inputs")] @@ -18,21 +19,19 @@ use zcash_primitives::legacy::keys::IncomingViewingKey; #[cfg(feature = "transparent-inputs")] use zcash_client_backend::encoding::AddressCodec; -use crate::wallet::{ - init::{migrations::initial_setup, WalletMigrationError}, - pool_code, +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); pub(super) struct Migration

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

schemer::Migration for Migration

{ @@ -69,6 +68,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_new(false, true, 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 +91,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(_) => { 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 +255,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::Shielded(ShieldedProtocol::Sapling))) + } + Address::Transparent(_) => 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 +294,6 @@ 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)) } } 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..083393e677 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/utxos_table.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/utxos_table.rs @@ -8,12 +8,7 @@ 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); pub(super) struct Migration; 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..2191944fec --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/v_sapling_shard_unscanned_ranges.rs @@ -0,0 +1,98 @@ +//! This migration adds a view that returns the un-scanned ranges associated with each sapling note +//! commitment tree shard. + +use std::collections::HashSet; + +use schemer_rusqlite::RusqliteMigration; +use uuid::Uuid; +use zcash_client_backend::data_api::{scanning::ScanPriority, SAPLING_SHARD_HEIGHT}; +use zcash_primitives::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); + +pub(super) struct Migration

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

schemer::Migration for Migration

{ + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + [add_account_birthdays::MIGRATION_ID].into_iter().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(()) + } +} 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..c94d08a677 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 @@ -6,16 +6,12 @@ use rusqlite::{self, named_params}; use schemer; use schemer_rusqlite::RusqliteMigration; use uuid::Uuid; +use zcash_client_backend::{PoolType, ShieldedProtocol}; 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_fields( - 0x2aa4d24f, - 0x51aa, - 0x4a4c, - b"\x8d\x9b\xe5\xb8\xa7\x62\x86\x5f", -); +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0x2aa4d24f_51aa_4a4c_8d9b_e5b8a762865f); pub(crate) struct Migration; @@ -48,7 +44,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::Shielded(ShieldedProtocol::Sapling)) ] )?; @@ -200,8 +196,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)) } } @@ -211,10 +206,9 @@ mod tests { use tempfile::NamedTempFile; use zcash_client_backend::keys::UnifiedSpendingKey; - use zcash_primitives::zip32::AccountId; + use zcash_primitives::{consensus::Network, zip32::AccountId}; use crate::{ - tests, wallet::init::{init_wallet_db_internal, migrations::add_transaction_views}, WalletDb, }; @@ -222,38 +216,45 @@ mod tests { #[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]) - .unwrap(); + let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap(); + init_wallet_db_internal( + &mut db_data, + None, + &[add_transaction_views::MIGRATION_ID], + false, + ) + .unwrap(); // Create two accounts in the wallet. - let usk0 = - UnifiedSpendingKey::from_seed(&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 +266,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 +280,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 +298,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 +388,7 @@ mod tests { } // Run this migration - init_wallet_db_internal(&mut db_data, None, &[super::MIGRATION_ID]).unwrap(); + init_wallet_db_internal(&mut db_data, None, &[super::MIGRATION_ID], false).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..3b507d8170 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/v_transactions_note_uniqueness.rs @@ -0,0 +1,255 @@ +//! This migration fixes a bug in `v_transactions` where distinct but otherwise identical notes +//! were being incorrectly deduplicated. + +use std::collections::HashSet; + +use schemer_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); + +pub(super) struct Migration; + +impl schemer::Migration for Migration { + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + [v_transactions_shielding_balance::MIGRATION_ID] + .into_iter() + .collect() + } + + fn description(&self) -> &'static str { + "Fixes a bug in v_transactions that was omitting value from identically-valued notes." + } +} + +impl RusqliteMigration for Migration { + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), Self::Error> { + transaction.execute_batch( + "DROP VIEW v_transactions; + CREATE VIEW v_transactions AS + WITH + notes AS ( + SELECT sapling_received_notes.id_note AS id, + sapling_received_notes.account AS account_id, + transactions.block AS block, + transactions.txid AS txid, + 2 AS pool, + sapling_received_notes.value AS value, + CASE + WHEN sapling_received_notes.is_change THEN 1 + ELSE 0 + END AS is_change, + CASE + WHEN sapling_received_notes.is_change THEN 0 + ELSE 1 + END AS received_count, + CASE + WHEN (sapling_received_notes.memo IS NULL OR sapling_received_notes.memo = X'F6') + THEN 0 + ELSE 1 + END AS memo_present + FROM sapling_received_notes + JOIN transactions + ON transactions.id_tx = sapling_received_notes.tx + UNION + SELECT utxos.id_utxo AS id, + utxos.received_by_account AS account_id, + utxos.height AS block, + utxos.prevout_txid AS txid, + 0 AS pool, + utxos.value_zat AS value, + 0 AS is_change, + 1 AS received_count, + 0 AS memo_present + FROM utxos + UNION + SELECT sapling_received_notes.id_note AS id, + sapling_received_notes.account AS account_id, + transactions.block AS block, + transactions.txid AS txid, + 2 AS pool, + -sapling_received_notes.value AS value, + 0 AS is_change, + 0 AS received_count, + 0 AS memo_present + FROM sapling_received_notes + JOIN transactions + ON transactions.id_tx = sapling_received_notes.spent + UNION + SELECT utxos.id_utxo AS id, + utxos.received_by_account AS account_id, + transactions.block AS block, + transactions.txid AS txid, + 0 AS pool, + -utxos.value_zat AS value, + 0 AS is_change, + 0 AS received_count, + 0 AS memo_present + FROM utxos + JOIN transactions + ON transactions.id_tx = utxos.spent_in_tx + ), + sent_note_counts AS ( + SELECT sent_notes.from_account AS account_id, + transactions.txid AS txid, + COUNT(DISTINCT sent_notes.id_note) as sent_notes, + SUM( + CASE + WHEN (sent_notes.memo IS NULL OR sent_notes.memo = X'F6' OR sapling_received_notes.tx IS NOT NULL) + THEN 0 + ELSE 1 + END + ) AS memo_count + FROM sent_notes + JOIN transactions + ON transactions.id_tx = sent_notes.tx + LEFT JOIN sapling_received_notes + ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) = + (sapling_received_notes.tx, 2, sapling_received_notes.output_index) + WHERE COALESCE(sapling_received_notes.is_change, 0) = 0 + GROUP BY account_id, txid + ), + blocks_max_height AS ( + SELECT MAX(blocks.height) as max_height FROM blocks + ) + SELECT notes.account_id AS account_id, + notes.block AS mined_height, + notes.txid AS txid, + transactions.tx_index AS tx_index, + transactions.expiry_height AS expiry_height, + transactions.raw AS raw, + SUM(notes.value) AS account_balance_delta, + transactions.fee AS fee_paid, + SUM(notes.is_change) > 0 AS has_change, + MAX(COALESCE(sent_note_counts.sent_notes, 0)) AS sent_note_count, + SUM(notes.received_count) AS received_note_count, + SUM(notes.memo_present) + MAX(COALESCE(sent_note_counts.memo_count, 0)) AS memo_count, + blocks.time AS block_time, + ( + blocks.height IS NULL + AND transactions.expiry_height BETWEEN 1 AND blocks_max_height.max_height + ) AS expired_unmined + FROM notes + LEFT JOIN transactions + ON notes.txid = transactions.txid + JOIN blocks_max_height + LEFT JOIN blocks ON blocks.height = notes.block + LEFT JOIN sent_note_counts + ON sent_note_counts.account_id = notes.account_id + AND sent_note_counts.txid = notes.txid + GROUP BY notes.account_id, notes.txid;" + )?; + + Ok(()) + } + + fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), Self::Error> { + Err(WalletMigrationError::CannotRevert(MIGRATION_ID)) + } +} + +#[cfg(test)] +mod tests { + use rusqlite::{self, params}; + use tempfile::NamedTempFile; + + use zcash_client_backend::keys::UnifiedSpendingKey; + use zcash_primitives::{consensus::Network, zip32::AccountId}; + + use crate::{ + wallet::init::{init_wallet_db_internal, migrations::v_transactions_net}, + WalletDb, + }; + + #[test] + fn v_transactions_note_uniqueness_migration() { + let data_file = NamedTempFile::new().unwrap(); + let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap(); + init_wallet_db_internal( + &mut db_data, + None, + &[v_transactions_net::MIGRATION_ID], + false, + ) + .unwrap(); + + // Create an account in the wallet + let usk0 = UnifiedSpendingKey::from_seed(&db_data.params, &[0u8; 32][..], AccountId::ZERO) + .unwrap(); + let ufvk0 = usk0.to_unified_full_viewing_key(); + db_data + .conn + .execute( + "INSERT INTO accounts (account, ufvk) VALUES (0, ?)", + params![ufvk0.encode(&db_data.params)], + ) + .unwrap(); + + // Tx 0 contains two received notes, both of 2 zatoshis, that are controlled by account 0. + db_data.conn.execute_batch( + "INSERT INTO blocks (height, hash, time, sapling_tree) VALUES (0, 0, 0, x'00'); + INSERT INTO transactions (block, id_tx, txid) VALUES (0, 0, 'tx0'); + + INSERT INTO received_notes (tx, output_index, account, diversifier, value, rcm, nf, is_change) + VALUES (0, 0, 0, '', 2, '', 'nf_a', false); + INSERT INTO received_notes (tx, output_index, account, diversifier, value, rcm, nf, is_change) + VALUES (0, 3, 0, '', 2, '', 'nf_b', false);").unwrap(); + + let check_balance_delta = |db_data: &mut WalletDb, + expected_notes: i64| { + let mut q = db_data + .conn + .prepare( + "SELECT account_id, account_balance_delta, has_change, memo_count, sent_note_count, received_note_count + FROM v_transactions", + ) + .unwrap(); + let mut rows = q.query([]).unwrap(); + let mut row_count = 0; + while let Some(row) = rows.next().unwrap() { + row_count += 1; + let account: i64 = row.get(0).unwrap(); + let account_balance_delta: i64 = row.get(1).unwrap(); + let has_change: bool = row.get(2).unwrap(); + let memo_count: i64 = row.get(3).unwrap(); + let sent_note_count: i64 = row.get(4).unwrap(); + let received_note_count: i64 = row.get(5).unwrap(); + match account { + 0 => { + assert_eq!(account_balance_delta, 2 * expected_notes); + assert!(!has_change); + assert_eq!(memo_count, 0); + assert_eq!(sent_note_count, 0); + assert_eq!(received_note_count, expected_notes); + } + other => { + panic!( + "Account {:?} is not expected to exist in the wallet.", + other + ); + } + } + } + assert_eq!(row_count, 1); + }; + + // Check for the bug (#1020). + check_balance_delta(&mut db_data, 1); + + // Apply the current migration. + init_wallet_db_internal(&mut db_data, None, &[super::MIGRATION_ID], false).unwrap(); + + // Now it should be correct. + check_balance_delta(&mut db_data, 2); + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/v_transactions_shielding_balance.rs b/zcash_client_sqlite/src/wallet/init/migrations/v_transactions_shielding_balance.rs new file mode 100644 index 0000000000..c7b64b2d7d --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/v_transactions_shielding_balance.rs @@ -0,0 +1,155 @@ +//! This migration reworks transaction history views to correctly include spent transparent utxo +//! value. + +use std::collections::HashSet; + +use schemer_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); + +pub(super) struct Migration; + +impl schemer::Migration for Migration { + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + [v_tx_outputs_use_legacy_false::MIGRATION_ID] + .into_iter() + .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)) + } +} 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..70a9566f4c --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/v_transactions_transparent_history.rs @@ -0,0 +1,191 @@ +//! 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 schemer_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); + +pub(super) struct Migration; + +impl schemer::Migration for Migration { + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + [sapling_memo_consistency::MIGRATION_ID] + .into_iter() + .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)) + } +} 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..380542747a --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/v_tx_outputs_use_legacy_false.rs @@ -0,0 +1,92 @@ +//! 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 schemer_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); + +pub(super) struct Migration; + +impl schemer::Migration for Migration { + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + [v_transactions_transparent_history::MIGRATION_ID] + .into_iter() + .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)) + } +} 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..6f9884c363 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/wallet_summaries.rs @@ -0,0 +1,89 @@ +//! This migration adds views and database changes required to provide accurate wallet summaries. + +use std::collections::HashSet; + +use schemer_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); + +pub(super) struct Migration; + +impl schemer::Migration for Migration { + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + [v_sapling_shard_unscanned_ranges::MIGRATION_ID] + .into_iter() + .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)) + } +} diff --git a/zcash_client_sqlite/src/wallet/orchard.rs b/zcash_client_sqlite/src/wallet/orchard.rs new file mode 100644 index 0000000000..373d76c014 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/orchard.rs @@ -0,0 +1,674 @@ +use std::{collections::HashSet, rc::Rc}; + +use incrementalmerkletree::Position; +use orchard::{ + keys::Diversifier, + note::{Note, Nullifier, RandomSeed, Rho}, +}; +use rusqlite::{named_params, types::Value, Connection, Row, Transaction}; + +use zcash_client_backend::{ + data_api::NullifierQuery, + wallet::{ReceivedNote, WalletOrchardOutput}, + DecryptedOutput, ShieldedProtocol, TransferType, +}; +use zcash_keys::keys::UnifiedFullViewingKey; +use zcash_primitives::transaction::TxId; +use zcash_protocol::{ + consensus::{self, BlockHeight}, + memo::MemoBytes, + value::Zatoshis, +}; +use zip32::Scope; + +use crate::{error::SqliteClientError, AccountId, ReceivedNoteId}; + +use super::{memo_repr, parse_scope, scope_code}; + +/// This trait provides a generalization over shielded output representations. +pub(crate) trait ReceivedOrchardOutput { + fn index(&self) -> usize; + fn account_id(&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 { + fn index(&self) -> usize { + self.index() + } + fn account_id(&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 { + fn index(&self) -> usize { + self.index() + } + fn account_id(&self) -> AccountId { + *self.account() + } + fn note(&self) -> &orchard::note::Note { + self.note() + } + fn memo(&self) -> Option<&MemoBytes> { + Some(self.memo()) + } + fn is_change(&self) -> bool { + self.transfer_type() == TransferType::WalletInternal + } + fn nullifier(&self) -> Option<&Nullifier> { + None + } + fn note_commitment_tree_position(&self) -> Option { + None + } + fn recipient_key_scope(&self) -> Option { + if self.transfer_type() == TransferType::WalletInternal { + Some(Scope::Internal) + } else { + Some(Scope::External) + } + } +} + +fn to_spendable_note( + params: &P, + row: &Row, +) -> Result>, SqliteClientError> { + let note_id = ReceivedNoteId(ShieldedProtocol::Orchard, row.get("id")?); + let txid = row.get::<_, [u8; 32]>("txid").map(TxId::from_bytes)?; + let action_index = row.get("action_index")?; + let diversifier = { + let d: Vec<_> = row.get("diversifier")?; + if d.len() != 11 { + return Err(SqliteClientError::CorruptedData( + "Invalid diversifier length".to_string(), + )); + } + let mut tmp = [0; 11]; + tmp.copy_from_slice(&d); + Diversifier::from_bytes(tmp) + }; + + let note_value: u64 = row.get::<_, i64>("value")?.try_into().map_err(|_e| { + SqliteClientError::CorruptedData("Note values must be nonnegative".to_string()) + })?; + + let rho = { + let rho_bytes: [u8; 32] = row.get("rho")?; + Option::from(Rho::from_bytes(&rho_bytes)) + .ok_or_else(|| SqliteClientError::CorruptedData("Invalid rho.".to_string())) + }?; + + let rseed = { + let rseed_bytes: [u8; 32] = row.get("rseed")?; + Option::from(RandomSeed::from_bytes(rseed_bytes, &rho)).ok_or_else(|| { + SqliteClientError::CorruptedData("Invalid Orchard random seed.".to_string()) + }) + }?; + + let note_commitment_tree_position = Position::from( + u64::try_from(row.get::<_, i64>("commitment_tree_position")?).map_err(|_| { + SqliteClientError::CorruptedData("Note commitment tree position invalid.".to_string()) + })?, + ); + + let ufvk_str: Option = row.get("ufvk")?; + let scope_code: Option = row.get("recipient_key_scope")?; + + // If we don't have information about the recipient key scope or the ufvk we can't determine + // which spending key to use. This may be because the received note was associated with an + // imported viewing key, so we treat such notes as not spendable. Although this method is + // presently only called using the results of queries where both the ufvk and + // recipient_key_scope columns are checked to be non-null, this is method is written + // defensively to account for the fact that both of these are nullable columns in case it + // is used elsewhere in the future. + ufvk_str + .zip(scope_code) + .map(|(ufvk_str, scope_code)| { + let ufvk = UnifiedFullViewingKey::decode(params, &ufvk_str) + .map_err(SqliteClientError::CorruptedData)?; + + let spending_key_scope = parse_scope(scope_code).ok_or_else(|| { + SqliteClientError::CorruptedData(format!("Invalid key scope code {}", scope_code)) + })?; + let recipient = ufvk + .orchard() + .map(|fvk| fvk.to_ivk(spending_key_scope).address(diversifier)) + .ok_or_else(|| { + SqliteClientError::CorruptedData("Diversifier invalid.".to_owned()) + })?; + + let note = Option::from(Note::from_parts( + recipient, + orchard::value::NoteValue::from_raw(note_value), + rho, + rseed, + )) + .ok_or_else(|| SqliteClientError::CorruptedData("Invalid Orchard note.".to_string()))?; + + Ok(ReceivedNote::from_parts( + note_id, + txid, + action_index, + note, + spending_key_scope, + note_commitment_tree_position, + )) + }) + .transpose() +} + +pub(crate) fn get_spendable_orchard_note( + conn: &Connection, + params: &P, + txid: &TxId, + index: u32, +) -> Result>, SqliteClientError> { + super::common::get_spendable_note( + conn, + params, + txid, + index, + ShieldedProtocol::Orchard, + to_spendable_note, + ) +} + +pub(crate) fn select_spendable_orchard_notes( + conn: &Connection, + params: &P, + account: AccountId, + target_value: Zatoshis, + anchor_height: BlockHeight, + exclude: &[ReceivedNoteId], +) -> Result>, SqliteClientError> { + super::common::select_spendable_notes( + conn, + params, + account, + target_value, + anchor_height, + exclude, + ShieldedProtocol::Orchard, + to_spendable_note, + ) +} + +/// Records the specified shielded output as having been received. +/// +/// This implementation relies on the facts that: +/// - A transaction will not contain more than 2^63 shielded outputs. +/// - A note value will never exceed 2^63 zatoshis. +pub(crate) fn put_received_note( + conn: &Transaction, + output: &T, + tx_ref: i64, + spent_in: Option, +) -> Result<(), SqliteClientError> { + let mut stmt_upsert_received_note = conn.prepare_cached( + "INSERT INTO orchard_received_notes + ( + tx, action_index, account_id, + diversifier, value, rho, rseed, memo, nf, + is_change, commitment_tree_position, + recipient_key_scope + ) + VALUES ( + :tx, :action_index, :account_id, + :diversifier, :value, :rho, :rseed, :memo, :nf, + :is_change, :commitment_tree_position, + :recipient_key_scope + ) + ON CONFLICT (tx, action_index) DO UPDATE + SET account_id = :account_id, + diversifier = :diversifier, + value = :value, + rho = :rho, + rseed = :rseed, + nf = IFNULL(:nf, nf), + memo = IFNULL(:memo, memo), + is_change = IFNULL(: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, + ":action_index": i64::try_from(output.index()).expect("output indices are representable as i64"), + ":account_id": output.account_id().0, + ":diversifier": diversifier.as_array(), + ":value": output.note().value().inner(), + ":rho": output.note().rho().to_bytes(), + ":rseed": &rseed.as_bytes(), + ":nf": output.nullifier().map(|nf| nf.to_bytes()), + ":memo": memo_repr(output.memo()), + ":is_change": output.is_change(), + ":commitment_tree_position": output.note_commitment_tree_position().map(u64::from), + ":recipient_key_scope": output.recipient_key_scope().map(scope_code), + ]; + + let received_note_id = stmt_upsert_received_note + .query_row(sql_args, |row| row.get::<_, i64>(0)) + .map_err(SqliteClientError::from)?; + + if let Some(spent_in) = spent_in { + conn.execute( + "INSERT INTO orchard_received_note_spends (orchard_received_note_id, transaction_id) + VALUES (:orchard_received_note_id, :transaction_id) + ON CONFLICT (orchard_received_note_id, transaction_id) DO NOTHING", + named_params![ + ":orchard_received_note_id": received_note_id, + ":transaction_id": spent_in + ], + )?; + } + Ok(()) +} + +/// Retrieves the set of nullifiers for "potentially spendable" Orchard notes that the +/// wallet is tracking. +/// +/// "Potentially spendable" means: +/// - The transaction in which the note was created has been observed as mined. +/// - No transaction in which the note's nullifier appears has been observed as mined. +pub(crate) fn get_orchard_nullifiers( + conn: &Connection, + query: NullifierQuery, +) -> Result, SqliteClientError> { + // Get the nullifiers for the notes we are tracking + let mut stmt_fetch_nullifiers = match query { + NullifierQuery::Unspent => conn.prepare( + "SELECT rn.account_id, rn.nf + FROM orchard_received_notes rn + 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 rn.account_id, rn.nf + FROM orchard_received_notes rn + WHERE nf IS NOT NULL", + )?, + }; + + let nullifiers = stmt_fetch_nullifiers.query_and_then([], |row| { + let account = AccountId(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 rn.account_id + FROM orchard_received_notes rn + 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::<_, u32>(0).map(AccountId) + })? + .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: i64, + 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 => Ok(false), + 1 => Ok(true), + _ => unreachable!("nf column is marked as UNIQUE"), + } +} + +#[cfg(test)] +pub(crate) mod tests { + use incrementalmerkletree::{Hashable, Level}; + use orchard::{ + keys::{FullViewingKey, SpendingKey}, + note_encryption::OrchardDomain, + tree::MerkleHashOrchard, + }; + use shardtree::error::ShardTreeError; + use zcash_client_backend::{ + data_api::{ + chain::CommitmentTreeRoot, DecryptedTransaction, WalletCommitmentTrees, WalletSummary, + }, + wallet::{Note, ReceivedNote}, + }; + 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::BlockHeight, memo::MemoBytes, ShieldedProtocol}; + + use super::select_spendable_orchard_notes; + use crate::{ + error::SqliteClientError, + testing::{ + self, + pool::{OutputRecoveryError, ShieldedPoolTester}, + TestState, + }, + wallet::{commitment_tree, sapling::tests::SaplingPoolTester}, + ORCHARD_TABLES_PREFIX, + }; + + pub(crate) struct OrchardPoolTester; + impl ShieldedPoolTester for OrchardPoolTester { + const SHIELDED_PROTOCOL: ShieldedProtocol = ShieldedProtocol::Orchard; + const TABLES_PREFIX: &'static str = ORCHARD_TABLES_PREFIX; + // 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() + } + + 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> { + 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: crate::AccountId, + target_value: zcash_protocol::value::Zatoshis, + anchor_height: BlockHeight, + exclude: &[crate::ReceivedNoteId], + ) -> Result>, SqliteClientError> + { + select_spendable_orchard_notes( + &st.wallet().conn, + &st.wallet().params, + account, + target_value, + anchor_height, + exclude, + ) + } + + fn decrypted_pool_outputs_count( + d_tx: &DecryptedTransaction<'_, crate::AccountId>, + ) -> usize { + d_tx.orchard_outputs().len() + } + + fn with_decrypted_pool_memos( + d_tx: &DecryptedTransaction<'_, crate::AccountId>, + mut f: impl FnMut(&MemoBytes), + ) { + for output in d_tx.orchard_outputs() { + f(output.memo()); + } + } + + fn try_output_recovery( + _: &TestState, + _: BlockHeight, + tx: &Transaction, + fvk: &Self::Fvk, + ) -> Result, OutputRecoveryError> { + 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 Ok(result.map(|(note, addr, memo)| { + ( + Note::Orchard(note), + UnifiedAddress::from_receivers(Some(addr), None, None) + .unwrap() + .into(), + MemoBytes::from_bytes(&memo).expect("correct length"), + ) + })); + } + } + + Ok(None) + } + + fn received_note_count( + summary: &zcash_client_backend::data_api::chain::ScanSummary, + ) -> usize { + summary.received_orchard_note_count() + } + } + + #[test] + fn send_single_step_proposed_transfer() { + testing::pool::send_single_step_proposed_transfer::() + } + + #[test] + #[cfg(feature = "transparent-inputs")] + fn send_multi_step_proposed_transfer() { + testing::pool::send_multi_step_proposed_transfer::() + } + + #[test] + #[allow(deprecated)] + fn create_to_address_fails_on_incorrect_usk() { + testing::pool::create_to_address_fails_on_incorrect_usk::() + } + + #[test] + #[allow(deprecated)] + 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 pool_crossing_required() { + testing::pool::pool_crossing_required::() + } + + #[test] + fn fully_funded_fully_private() { + testing::pool::fully_funded_fully_private::() + } + + #[test] + 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::() + } +} diff --git a/zcash_client_sqlite/src/wallet/sapling.rs b/zcash_client_sqlite/src/wallet/sapling.rs index a18281dc87..63dc4188bc 100644 --- a/zcash_client_sqlite/src/wallet/sapling.rs +++ b/zcash_client_sqlite/src/wallet/sapling.rs @@ -1,42 +1,49 @@ //! Functions for Sapling support in the wallet. + +use std::{collections::HashSet, rc::Rc}; + use group::ff::PrimeField; -use rusqlite::{named_params, types::Value, OptionalExtension, Row}; -use std::rc::Rc; +use incrementalmerkletree::Position; +use rusqlite::{named_params, types::Value, Connection, Row, Transaction}; -use zcash_primitives::{ - consensus::BlockHeight, +use sapling::{self, Diversifier, Nullifier, Rseed}; +use zcash_client_backend::{ + data_api::NullifierQuery, + wallet::{ReceivedNote, WalletSaplingOutput}, + DecryptedOutput, ShieldedProtocol, TransferType, +}; +use zcash_keys::keys::UnifiedFullViewingKey; +use zcash_primitives::transaction::{components::amount::NonNegativeAmount, TxId}; +use zcash_protocol::{ + consensus::{self, BlockHeight}, memo::MemoBytes, - merkle_tree::{read_commitment_tree, read_incremental_witness}, - sapling::{self, Diversifier, Note, Nullifier, Rseed}, - transaction::components::Amount, - zip32::AccountId, }; +use zip32::Scope; -use zcash_client_backend::{ - wallet::{ReceivedSaplingNote, WalletSaplingOutput}, - DecryptedOutput, TransferType, -}; +use crate::{error::SqliteClientError, AccountId, ReceivedNoteId}; -use crate::{error::SqliteClientError, DataConnStmtCache, NoteId, WalletDb}; +use super::{memo_repr, parse_scope, scope_code}; /// This trait provides a generalization over shielded output representations. pub(crate) trait ReceivedSaplingOutput { fn index(&self) -> usize; - fn account(&self) -> AccountId; - fn note(&self) -> &Note; + fn account_id(&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 { fn index(&self) -> usize { self.index() } - fn account(&self) -> AccountId { - WalletSaplingOutput::account(self) + fn account_id(&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 +52,57 @@ 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 { fn index(&self) -> usize { - self.index + self.index() } - fn account(&self) -> AccountId { - self.account + fn account_id(&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 +113,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 +132,101 @@ 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 = parse_scope(scope_code).ok_or_else(|| { + SqliteClientError::CorruptedData(format!("Invalid key scope code {}", scope_code)) + })?; + + let recipient = match spending_key_scope { + Scope::Internal => ufvk + .sapling() + .and_then(|dfvk| dfvk.diversified_change_address(diversifier)), + Scope::External => ufvk + .sapling() + .and_then(|dfvk| dfvk.diversified_address(diversifier)), + } + .ok_or_else(|| SqliteClientError::CorruptedData("Diversifier invalid.".to_owned()))?; + + Ok(ReceivedNote::from_parts( + note_id, + txid, + output_index, + sapling::Note::from_parts( + recipient, + sapling::value::NoteValue::from_raw(note_value), + rseed, + ), + spending_key_scope, + note_commitment_tree_position, + )) + }) + .transpose() } -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

, +/// 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: AccountId, - target_value: Amount, + target_value: NonNegativeAmount, 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) + ) } /// Retrieves the set of nullifiers for "potentially spendable" Sapling notes that the @@ -292,51 +235,61 @@ 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

, +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 rn.account_id, rn.nf + FROM sapling_received_notes rn + 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 rn.account_id, rn.nf + FROM sapling_received_notes rn + WHERE nf IS NOT NULL", + ), + }?; + + let nullifiers = stmt_fetch_nullifiers.query_and_then([], |row| { + let account = AccountId(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 rn.account_id + FROM sapling_received_notes rn + 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::<_, u32>(0).map(AccountId) + })? + .collect::, _>>()?; + Ok(res) } @@ -345,13 +298,25 @@ 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>, +pub(crate) fn mark_sapling_note_spent( + conn: &Connection, tx_ref: i64, - nf: &Nullifier, -) -> Result<(), SqliteClientError> { - stmts.stmt_mark_sapling_note_spent(tx_ref, nf)?; - Ok(()) + 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 => Ok(false), + 1 => Ok(true), + _ => unreachable!("nf column is marked as UNIQUE"), + } } /// Records the specified shielded output as having been received. @@ -359,891 +324,372 @@ 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>, +pub(crate) fn put_received_note( + conn: &Transaction, output: &T, tx_ref: i64, -) -> Result { + spent_in: Option, +) -> Result<(), SqliteClientError> { + let mut stmt_upsert_received_note = conn.prepare_cached( + "INSERT INTO sapling_received_notes + (tx, output_index, account_id, diversifier, value, rcm, memo, nf, + is_change, commitment_tree_position, + recipient_key_scope) + VALUES ( + :tx, + :output_index, + :account_id, + :diversifier, + :value, + :rcm, + :memo, + :nf, + :is_change, + :commitment_tree_position, + :recipient_key_scope + ) + ON CONFLICT (tx, output_index) DO UPDATE + SET account_id = :account_id, + diversifier = :diversifier, + value = :value, + rcm = :rcm, + nf = IFNULL(:nf, nf), + memo = IFNULL(:memo, memo), + is_change = IFNULL(: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, + ":output_index": i64::try_from(output.index()).expect("output indices are representable as i64"), + ":account_id": output.account_id().0, + ":diversifier": &diversifier.0.as_ref(), + ":value": output.note().value().inner(), + ":rcm": &rcm.as_ref(), + ":nf": output.nullifier().map(|nf| nf.0.as_ref()), + ":memo": memo_repr(output.memo()), + ":is_change": output.is_change(), + ":commitment_tree_position": output.note_commitment_tree_position().map(u64::from), + ":recipient_key_scope": output.recipient_key_scope().map(scope_code) + ]; + + let received_note_id = stmt_upsert_received_note + .query_row(sql_args, |row| row.get::<_, i64>(0)) + .map_err(SqliteClientError::from)?; + + if let Some(spent_in) = spent_in { + conn.execute( + "INSERT INTO sapling_received_note_spends (sapling_received_note_id, transaction_id) + VALUES (:sapling_received_note_id, :transaction_id) + ON CONFLICT (sapling_received_note_id, transaction_id) DO NOTHING", + named_params![ + ":sapling_received_note_id": received_note_id, + ":transaction_id": spent_in + ], + )?; } + + Ok(()) } #[cfg(test)] -#[allow(deprecated)] -mod tests { - use rusqlite::Connection; - use secrecy::Secret; - use tempfile::NamedTempFile; - +pub(crate) mod tests { + use incrementalmerkletree::{Hashable, Level}; + use shardtree::error::ShardTreeError; use zcash_proofs::prover::LocalTxProver; + use sapling::{ + self, + note_encryption::try_sapling_output_recovery, + prover::{OutputProver, SpendProver}, + zip32::{DiversifiableFullViewingKey, ExtendedSpendingKey}, + }; 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}, + consensus::BlockHeight, + memo::MemoBytes, + transaction::{ + components::{amount::NonNegativeAmount, sapling::zip212_enforcement}, + Transaction, + }, + zip32::Scope, }; use zcash_client_backend::{ - address::RecipientAddress, + address::Address, data_api::{ - self, - chain::scan_cached_blocks, - error::Error, - wallet::{create_spend_to_address, input_selection::GreedyInputSelector, spend}, - WalletRead, WalletWrite, + chain::CommitmentTreeRoot, DecryptedTransaction, WalletCommitmentTrees, WalletSummary, }, - fees::{zip317, DustOutputPolicy}, keys::UnifiedSpendingKey, - wallet::OvkPolicy, - zip321::{Payment, TransactionRequest}, + wallet::{Note, ReceivedNote}, + ShieldedProtocol, }; 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}, + error::SqliteClientError, + testing::{ + self, + pool::{OutputRecoveryError, ShieldedPoolTester}, + TestState, }, - AccountId, BlockDb, DataConnStmtCache, WalletDb, + wallet::{commitment_tree, sapling::select_spendable_sapling_notes}, + AccountId, ReceivedNoteId, SAPLING_TABLES_PREFIX, }; - #[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, - }, - }, - }; + pub(crate) struct SaplingPoolTester; + impl ShieldedPoolTester for SaplingPoolTester { + const SHIELDED_PROTOCOL: ShieldedProtocol = ShieldedProtocol::Sapling; + const TABLES_PREFIX: &'static str = SAPLING_TABLES_PREFIX; + // const MERKLE_TREE_DEPTH: u8 = sapling::NOTE_COMMITMENT_TREE_DEPTH; - 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."); - } + 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() } - } - #[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) - ); - } + fn usk_to_sk(usk: &UnifiedSpendingKey) -> &Self::Sk { + usk.sapling() + } - #[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) - ); - } + fn sk(seed: &[u8]) -> Self::Sk { + ExtendedSpendingKey::master(seed) + } - #[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 sk_to_fvk(sk: &Self::Sk) -> Self::Fvk { + sk.to_diversifiable_full_viewing_key() + } - #[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); + fn sk_default_address(sk: &Self::Sk) -> Address { + sk.default_address().1.into() } - 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(_) - ); - } - #[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); + fn fvk_default_address(fvk: &Self::Fvk) -> Address { + fvk.default_address().1.into() } - 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(); - } - #[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, + 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> { + 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: NonNegativeAmount, + anchor_height: BlockHeight, + exclude: &[ReceivedNoteId], + ) -> Result>, SqliteClientError> { + select_spendable_sapling_notes( + &st.wallet().conn, + &st.wallet().params, + account, + target_value, + anchor_height, + exclude, ) - .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(); + } + fn decrypted_pool_outputs_count(d_tx: &DecryptedTransaction<'_, AccountId>) -> usize { + d_tx.sapling_outputs().len() + } + + fn with_decrypted_pool_memos( + d_tx: &DecryptedTransaction<'_, AccountId>, + mut f: impl FnMut(&MemoBytes), + ) { + for output in d_tx.sapling_outputs() { + f(output.memo()); + } + } + + fn try_output_recovery( + st: &TestState, + height: BlockHeight, + tx: &Transaction, + fvk: &Self::Fvk, + ) -> Result, OutputRecoveryError> { 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), + &fvk.to_ovk(Scope::External), output, + zip212_enforcement(&st.network(), height), ); if result.is_some() { - return result; + return Ok(result.map(|(note, addr, memo)| { + ( + Note::Sapling(note), + addr.into(), + MemoBytes::from_bytes(&memo).expect("correct length"), + ) + })); } } - 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); + Ok(None) } - scan_cached_blocks(&network, &db_cache, &mut db_write, None).unwrap(); - // Send the funds again, discarding history. - // Neither transaction output is decryptable by the sender. - assert!(send_and_recover_with_policy(&mut db_write, OvkPolicy::Discard).is_none()); + fn received_note_count( + summary: &zcash_client_backend::data_api::chain::ScanSummary, + ) -> usize { + summary.received_sapling_note_count() + } + } + + pub(crate) fn test_prover() -> impl SpendProver + OutputProver { + LocalTxProver::bundled() } #[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 send_single_step_proposed_transfer() { + testing::pool::send_single_step_proposed_transfer::() } #[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(_) - ); + #[cfg(feature = "transparent-inputs")] + fn send_multi_step_proposed_transfer() { + testing::pool::send_multi_step_proposed_transfer::() } #[test] - 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); - } + #[allow(deprecated)] + fn create_to_address_fails_on_incorrect_usk() { + testing::pool::create_to_address_fails_on_incorrect_usk::() + } + + #[test] + #[allow(deprecated)] + 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::() + } - 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(_) - ); + #[test] + fn ovk_policy_prevents_recovery_from_chain() { + testing::pool::ovk_policy_prevents_recovery_from_chain::() + } + + #[test] + fn spend_succeeds_to_t_addr_zero_change() { + testing::pool::spend_succeeds_to_t_addr_zero_change::() + } + + #[test] + fn change_note_spends_succeed() { + testing::pool::change_note_spends_succeed::() + } + + #[test] + fn external_address_change_spends_detected_in_restore_from_seed() { + testing::pool::external_address_change_spends_detected_in_restore_from_seed::< + SaplingPoolTester, + >() + } + + #[test] + #[ignore] // FIXME: #1316 This requires support for dust outputs. + #[cfg(not(feature = "expensive-tests"))] + fn zip317_spend() { + testing::pool::zip317_spend::() } #[test] #[cfg(feature = "transparent-inputs")] fn shield_transparent() { - 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] + #[cfg(feature = "orchard")] + fn pool_crossing_required() { + use crate::wallet::orchard::tests::OrchardPoolTester; + + testing::pool::pool_crossing_required::() + } + + #[test] + #[cfg(feature = "orchard")] + fn fully_funded_fully_private() { + use crate::wallet::orchard::tests::OrchardPoolTester; + + testing::pool::fully_funded_fully_private::() + } + + #[test] + #[cfg(feature = "orchard")] + fn fully_funded_send_to_t() { + use crate::wallet::orchard::tests::OrchardPoolTester; + + testing::pool::fully_funded_send_to_t::() + } + + #[test] + #[cfg(feature = "orchard")] + fn multi_pool_checkpoint() { + use crate::wallet::orchard::tests::OrchardPoolTester; + + testing::pool::multi_pool_checkpoint::() + } + + #[test] + #[cfg(feature = "orchard")] + fn multi_pool_checkpoints_with_pruning() { + use crate::wallet::orchard::tests::OrchardPoolTester; + + testing::pool::multi_pool_checkpoints_with_pruning::() } } diff --git a/zcash_client_sqlite/src/wallet/scanning.rs b/zcash_client_sqlite/src/wallet/scanning.rs new file mode 100644 index 0000000000..0c3750174f --- /dev/null +++ b/zcash_client_sqlite/src/wallet/scanning.rs @@ -0,0 +1,1478 @@ +use incrementalmerkletree::{Address, Position}; +use rusqlite::{self, named_params, types::Value, OptionalExtension}; +use shardtree::error::ShardTreeError; +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, + }, + ShieldedProtocol, +}; +use zcash_primitives::consensus::{self, BlockHeight, NetworkUpgrade}; + +use crate::{ + error::SqliteClientError, + wallet::{block_height_extrema, commitment_tree, 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_client_backend::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; + fn chain_height_unknown() -> Self; + fn commitment_tree(err: ShardTreeError) -> Self; +} + +impl WalletError for SqliteClientError { + fn db_error(err: rusqlite::Error) -> Self { + SqliteClientError::DbError(err) + } + + fn corrupt(message: String) -> Self { + SqliteClientError::CorruptedData(message) + } + + fn chain_height_unknown() -> Self { + SqliteClientError::ChainHeightUnknown + } + + fn commitment_tree(err: ShardTreeError) -> Self { + SqliteClientError::CommitmentTree(err) + } +} + +impl WalletError for WalletMigrationError { + fn db_error(err: rusqlite::Error) -> Self { + WalletMigrationError::DbError(err) + } + + fn corrupt(message: String) -> Self { + WalletMigrationError::CorruptedData(message) + } + + fn chain_height_unknown() -> Self { + WalletMigrationError::CorruptedData( + "Wallet migration requires a valid account birthday.".to_owned(), + ) + } + + fn commitment_tree(err: ShardTreeError) -> Self { + WalletMigrationError::CommitmentTree(err) + } +} + +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}, + AccountBirthday, Ratio, WalletRead, WalletWrite, SAPLING_SHARD_HEIGHT, + }; + use zcash_primitives::{ + block::BlockHash, + consensus::{BlockHeight, NetworkUpgrade, Parameters}, + transaction::components::amount::NonNegativeAmount, + }; + + use crate::{ + error::SqliteClientError, + testing::{ + pool::ShieldedPoolTester, AddressType, BlockCache, InitialChainState, TestBuilder, + TestState, + }, + wallet::{ + sapling::tests::SaplingPoolTester, + scanning::{insert_queue_entries, replace_queue_entries, suggest_scan_ranges}, + }, + VERIFY_LOOKAHEAD, + }; + + #[cfg(feature = "orchard")] + use {crate::wallet::orchard::tests::OrchardPoolTester, orchard::tree::MerkleHashOrchard}; + + #[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_block_cache() + .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 = NonNegativeAmount::const_from_u64(50000); + let initial_height = sapling_activation_height + initial_height_offset; + st.generate_block_at( + initial_height, + prior_block_hash, + &dfvk, + AddressType::DefaultExternal, + value, + initial_sapling_tree_size, + initial_orchard_tree_size, + false, + ); + + for _ in 1..=10 { + st.generate_next_block( + &dfvk, + AddressType::DefaultExternal, + NonNegativeAmount::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!( + st.wallet().suggest_scan_ranges(), + 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!( + st.wallet().suggest_scan_ranges(), + 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!( + st.wallet().suggest_scan_ranges(), + 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), + ); + } + + pub(crate) fn test_with_nu5_birthday_offset( + birthday_offset: u32, + prior_block_hash: BlockHash, + ) -> (TestState, T::Fvk, AccountBirthday, u32) { + let st = TestBuilder::new() + .with_block_cache() + .with_account_birthday(|rng, network, initial_chain_state| { + // We're constructing the birthday without adding any chain data. + assert!(initial_chain_state.is_none()); + + // 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 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 (_, sapling_initial_tree) = Frontier::random_with_prior_subtree_roots( + rng, + (frontier_position + 1).into(), + NonZeroU8::new(16).unwrap(), + ); + #[cfg(feature = "orchard")] + let (_, orchard_initial_tree) = Frontier::random_with_prior_subtree_roots( + rng, + (frontier_position + 1).into(), + NonZeroU8::new(16).unwrap(), + ); + + AccountBirthday::from_parts( + ChainState::new( + birthday_height, + prior_block_hash, + sapling_initial_tree, + #[cfg(feature = "orchard")] + orchard_initial_tree, + ), + None, + ) + }) + .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::(76, BlockHash([0; 32])); + 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_block_cache().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, + ), + ) + .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::(76, BlockHash([0; 32])); + + // 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, BlockHash([0; 32])); + + // Set up the following situation: + // + // last_shard_start prior_tip new_tip + // |<----- 1000 ----->|<--- 500 --->| + // wallet_birthday + let prior_tip_height = birthday.height(); + + // Set up some shard root history before the wallet birthday. + let last_shard_start = birthday.height() - 1000; + T::put_subtree_roots( + &mut st, + 0, + &[CommitmentTreeRoot::from_parts( + last_shard_start, + // fake a hash, the value doesn't matter + T::empty_tree_leaf(), + )], + ) + .unwrap(); + + // 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_block_cache() + .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]), + &dfvk, + AddressType::DefaultExternal, + // 1235 notes into into the second shard + NonNegativeAmount::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_block_cache() + .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(); + + // We have scan ranges and a subtree, but have scanned no blocks. + let summary = st.get_wallet_summary(1); + assert_eq!(summary.and_then(|s| s.scan_progress()), None); + + // 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]), + &dfvk, + AddressType::DefaultExternal, + NonNegativeAmount::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)); + + // Progress denominator depends on which pools are enabled (which changes the + // initial tree states). Here we compute the denominator based upon the fact that + // the trees are the same size at present. + let expected_denom = (1 << SAPLING_SHARD_HEIGHT) * 2 - frontier_tree_size; + #[cfg(feature = "orchard")] + let expected_denom = expected_denom * 2; + assert_eq!( + summary.and_then(|s| s.scan_progress()), + Some(Ratio::new(1, u64::from(expected_denom))) + ); + + // Now simulate shutting down, and then restarting 70 blocks later, after a shard + // has been completed in one pool. This shard will have index 2, as our birthday + // was in shard 1. + let last_shard_start = prior_tip + 50; + T::put_subtree_roots( + &mut st, + 2, + &[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() + .conn + .prepare("SELECT shard_index, subtree_end_height FROM sapling_tree_shards") + .unwrap(); + (shard_stmt + .query_and_then::<_, rusqlite::Error, _, _>([], |row| { + Ok((row.get::<_, u32>(0)?, row.get::<_, Option>(1)?)) + }) + .unwrap() + .collect::, _>>()) + .unwrap(); + } + + { + let mut shard_stmt = st + .wallet_mut() + .conn + .prepare("SELECT shard_index, subtree_end_height FROM orchard_tree_shards") + .unwrap(); + (shard_stmt + .query_and_then::<_, rusqlite::Error, _, _>([], |row| { + Ok((row.get::<_, u32>(0)?, row.get::<_, Option>(1)?)) + }) + .unwrap() + .collect::, _>>()) + .unwrap(); + } + + 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. We still only have one scanned + // note but in the pool where we crossed the subtree boundary we have two shards worth of + // notes to scan. + let expected_denom = expected_denom + (1 << 16); + let summary = st.get_wallet_summary(1); + assert_eq!( + summary.and_then(|s| s.scan_progress()), + Some(Ratio::new(1, u64::from(expected_denom))) + ); + } + + #[test] + fn replace_queue_entries_merges_previous_range() { + use ScanPriority::*; + + let mut st = TestBuilder::new().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.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.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().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.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.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); + } +} diff --git a/zcash_client_sqlite/src/wallet/transparent.rs b/zcash_client_sqlite/src/wallet/transparent.rs new file mode 100644 index 0000000000..63761a7100 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/transparent.rs @@ -0,0 +1,34 @@ +//! Functions for transparent input support in the wallet. +use std::collections::HashSet; + +use rusqlite::{named_params, Connection}; +use zcash_primitives::transaction::components::OutPoint; + +use crate::AccountId; + +pub(crate) fn detect_spending_accounts<'a>( + conn: &Connection, + spent: impl Iterator, +) -> Result, rusqlite::Error> { + let mut account_q = conn.prepare_cached( + "SELECT received_by_account_id + FROM utxos + WHERE prevout_txid = :prevout_txid + AND prevout_idx = :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::<_, u32>(0).map(AccountId), + )? { + acc.insert(account?); + } + } + + Ok(acc) +} diff --git a/zcash_extensions/Cargo.toml b/zcash_extensions/Cargo.toml index 912d78c7fa..009a16606a 100644 --- a/zcash_extensions/Cargo.toml +++ b/zcash_extensions/Cargo.toml @@ -4,21 +4,28 @@ description = "Zcash Extension implementations & consensus node integration laye version = "0.0.0" authors = ["Jack Grigg ", "Kris Nuttycombe "] homepage = "https://github.com/zcash/librustzcash" -repository = "https://github.com/zcash/librustzcash" -license = "MIT OR Apache-2.0" -edition = "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"] [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 [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 +zcash_address.workspace = true +zcash_proofs.workspace = true [features] transparent-inputs = [] 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..40389cbb31 100644 --- a/zcash_extensions/src/transparent/demo.rs +++ b/zcash_extensions/src/transparent/demo.rs @@ -476,26 +476,26 @@ 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 zcash_primitives::{ - consensus::{BlockHeight, BranchId, NetworkUpgrade, Parameters}, - constants, + consensus::{BlockHeight, BranchId, NetworkType, NetworkUpgrade, Parameters}, extensions::transparent::{self as tze, Extension, FromPayload, ToPayload}, legacy::TransparentAddress, - sapling::{self, Node, Rseed}, transaction::{ - builder::Builder, + builder::{BuildConfig, Builder}, components::{ - amount::Amount, + amount::{Amount, NonNegativeAmount}, tze::{Authorized, Bundle, OutPoint, TzeIn, TzeOut}, }, fees::fixed, Transaction, TransactionData, TxVersion, }, - zip32::ExtendedSpendingKey, }; use zcash_proofs::prover::LocalTxProver; @@ -513,38 +513,17 @@ 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)), + #[cfg(zcash_unstable = "nu6")] + 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 +615,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, } } @@ -796,11 +785,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 @@ -815,7 +800,10 @@ mod tests { // create some inputs to spend let extsk = ExtendedSpendingKey::master(&[]); let to = extsk.default_address().1; - let note1 = to.create_note(110000, Rseed::BeforeZip212(jubjub::Fr::random(&mut rng))); + 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 @@ -823,48 +811,54 @@ mod tests { tree.append(cm1).unwrap(); let witness1 = sapling::IncrementalWitness::from_tree(tree); - 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::(&extsk, note1, witness1.path().unwrap()) .unwrap(); - let value = Amount::from_u64(100000).unwrap(); + let value = NonNegativeAmount::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(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(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 +866,31 @@ 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(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..e6abf3b64a 100644 --- a/zcash_history/CHANGELOG.md +++ b/zcash_history/CHANGELOG.md @@ -6,6 +6,8 @@ and this library adheres to Rust's notion of [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +## [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..591c24f224 100644 --- a/zcash_history/Cargo.toml +++ b/zcash_history/Cargo.toml @@ -1,26 +1,26 @@ [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 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..8fa13729bc 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) 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..b53022695c --- /dev/null +++ b/zcash_keys/CHANGELOG.md @@ -0,0 +1,97 @@ +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_keys::address::Address::try_from_zcash_address` +- `zcash_keys::address::Receiver` + +## [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..15cc254ab8 --- /dev/null +++ b/zcash_keys/Cargo.toml @@ -0,0 +1,100 @@ +[package] +name = "zcash_keys" +description = "Zcash key and address management" +version = "0.2.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_primitives.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 + +# - Transparent protocols +hdwallet = { workspace = true, optional = 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 + +# - 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 +zcash_address = { workspace = true, features = ["test-dependencies"] } +zcash_primitives = { workspace = true, features = ["test-dependencies"] } + +[features] +## Enables use of transparent key parts and addresses +transparent-inputs = ["dep:hdwallet", "zcash_primitives/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", + "zcash_primitives/test-dependencies", +] + +#! ### Experimental features + +## Exposes unstable APIs. Their behaviour may change at any time. +unstable = ["dep:byteorder"] + +[badges] +maintenance = { status = "actively-developed" } 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..8d3e0e4609 --- /dev/null +++ b/zcash_keys/src/address.rs @@ -0,0 +1,547 @@ +//! Structs for handling supported address types. + +use zcash_address::{ + unified::{self, Container, Encoding, Typecode}, + ConversionError, ToAddress, TryFromRawAddress, ZcashAddress, +}; +use zcash_primitives::legacy::TransparentAddress; +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 = std::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 { + #[cfg(feature = "sapling")] + Sapling(PaymentAddress), + Transparent(TransparentAddress), + Unified(UnifiedAddress), +} + +#[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()) + } +} + +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), + } + } + + /// 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 send funds to the specified pool. + pub fn has_receiver(&self, pool_type: PoolType) -> bool { + match self { + #[cfg(feature = "sapling")] + Address::Sapling(_) => { + matches!(pool_type, PoolType::Shielded(ShieldedProtocol::Sapling)) + } + Address::Transparent(_) => matches!(pool_type, PoolType::Transparent), + Address::Unified(ua) => match pool_type { + PoolType::Transparent => ua.transparent().is_some(), + PoolType::Shielded(ShieldedProtocol::Sapling) => { + #[cfg(feature = "sapling")] + return ua.sapling().is_some(); + + #[cfg(not(feature = "sapling"))] + return false; + } + PoolType::Shielded(ShieldedProtocol::Orchard) => { + #[cfg(feature = "orchard")] + return ua.orchard().is_some(); + + #[cfg(not(feature = "orchard"))] + return false; + } + }, + } + } +} + +#[cfg(all( + any( + feature = "orchard", + feature = "sapling", + feature = "transparent-inputs" + ), + any(test, feature = "test-dependencies") +))] +pub mod testing { + use proptest::prelude::*; + use zcash_primitives::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 zcash_primitives::legacy::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), + ] + } + + #[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), + ]; + } +} + +#[cfg(test)] +mod tests { + use zcash_address::test_vectors; + use zcash_primitives::consensus::MAIN_NETWORK; + + use super::{Address, UnifiedAddress}; + + #[cfg(feature = "sapling")] + use crate::keys::sapling; + + #[cfg(any(feature = "orchard", feature = "sapling"))] + use zcash_primitives::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.transparent().is_some(), + tv.p2pkh_bytes.is_some() || tv.p2sh_bytes.is_some() + ); + #[cfg(feature = "sapling")] + assert_eq!(_ua.sapling().is_some(), tv.sapling_raw_addr.is_some()); + #[cfg(feature = "orchard")] + 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/encoding.rs b/zcash_keys/src/encoding.rs similarity index 82% rename from zcash_client_backend/src/encoding.rs rename to zcash_keys/src/encoding.rs index 9484122c7e..8de7125a05 100644 --- a/zcash_client_backend/src/encoding.rs +++ b/zcash_keys/src/encoding.rs @@ -1,23 +1,24 @@ //! 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_primitives::constants] module. use crate::address::UnifiedAddress; -use bech32::{self, Error, FromBase32, ToBase32, Variant}; use bs58::{self, decode::Error as Bs58Error}; use std::fmt; -use std::io::{self, Write}; +use zcash_primitives::consensus::NetworkConstants; + use zcash_address::unified::{self, Encoding}; -use zcash_primitives::{ - consensus, - legacy::TransparentAddress, - sapling, - zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, +use zcash_primitives::{consensus, legacy::TransparentAddress}; + +#[cfg(feature = "sapling")] +use { + bech32::{self, Error, FromBase32, ToBase32, Variant}, + sapling::zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, + std::io::{self, Write}, }; +#[cfg(feature = "sapling")] fn bech32_encode(hrp: &str, write: F) -> String where F: Fn(&mut dyn Write) -> io::Result<()>, @@ -28,6 +29,7 @@ where } #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg(feature = "sapling")] pub enum Bech32DecodeError { Bech32Error(Error), IncorrectVariant(Variant), @@ -35,12 +37,14 @@ pub enum Bech32DecodeError { HrpMismatch { expected: String, actual: String }, } +#[cfg(feature = "sapling")] impl From for Bech32DecodeError { fn from(err: Error) -> Self { Bech32DecodeError::Bech32Error(err) } } +#[cfg(feature = "sapling")] impl fmt::Display for Bech32DecodeError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self { @@ -62,6 +66,7 @@ impl fmt::Display for Bech32DecodeError { } } +#[cfg(feature = "sapling")] fn bech32_decode(hrp: &str, s: &str, read: F) -> Result where F: Fn(Vec) -> Option, @@ -79,13 +84,24 @@ where } } +/// A trait for encoding and decoding Zcash addresses. pub trait AddressCodec

where Self: std::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; } @@ -134,6 +150,7 @@ impl AddressCodec

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

for sapling::PaymentAddress { type Error = Bech32DecodeError; @@ -157,7 +174,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!( @@ -178,22 +195,24 @@ impl AddressCodec

for UnifiedAddress { /// constants::testnet::{COIN_TYPE, HRP_SAPLING_EXTENDED_SPENDING_KEY}, /// zip32::AccountId, /// }; -/// use zcash_client_backend::{ +/// 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 +225,30 @@ pub fn decode_extended_spending_key( /// # Examples /// /// ``` +/// use ::sapling::zip32::ExtendedFullViewingKey; /// use zcash_primitives::{ /// constants::testnet::{COIN_TYPE, HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY}, /// zip32::AccountId, /// }; -/// use zcash_client_backend::{ +/// 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 +/// [`ExtendedFullViewingKey`]: sapling::zip32::ExtendedFullViewingKey +#[cfg(feature = "sapling")] pub fn decode_extended_full_viewing_key( hrp: &str, s: &str, @@ -241,12 +262,12 @@ pub fn decode_extended_full_viewing_key( /// /// ``` /// 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}, /// }; /// /// let pa = PaymentAddress::from_bytes(&[ @@ -262,7 +283,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 +293,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,12 +308,12 @@ 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}, +/// consensus::{TEST_NETWORK, NetworkConstants, Parameters}, /// }; /// /// let pa = PaymentAddress::from_bytes(&[ @@ -309,7 +332,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,11 +354,11 @@ pub fn decode_payment_address( /// # Examples /// /// ``` -/// use zcash_client_backend::{ +/// use zcash_keys::{ /// encoding::encode_transparent_address, /// }; /// use zcash_primitives::{ -/// consensus::{TEST_NETWORK, Parameters}, +/// consensus::{TEST_NETWORK, NetworkConstants, Parameters}, /// legacy::TransparentAddress, /// }; /// @@ -342,7 +366,7 @@ pub fn decode_payment_address( /// 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,7 +375,7 @@ 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", /// ); @@ -363,13 +387,13 @@ pub fn encode_transparent_address( 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); @@ -399,12 +423,12 @@ pub fn encode_transparent_address_p( /// /// ``` /// use zcash_primitives::{ -/// consensus::{TEST_NETWORK, Parameters}, +/// consensus::{TEST_NETWORK, NetworkConstants, Parameters}, +/// legacy::TransparentAddress, /// }; -/// use zcash_client_backend::{ +/// use zcash_keys::{ /// encoding::decode_transparent_address, /// }; -/// use zcash_primitives::legacy::TransparentAddress; /// /// assert_eq!( /// decode_transparent_address( @@ -412,7 +436,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,7 +445,7 @@ 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 @@ -435,12 +459,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 +472,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_primitives::constants; #[test] fn extended_spending_key() { @@ -504,7 +529,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 +553,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 +586,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 +607,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 +621,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..0115e15283 --- /dev/null +++ b/zcash_keys/src/keys.rs @@ -0,0 +1,1682 @@ +//! Helper functions for managing light client key material. +use std::{ + error, + 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 { + std::convert::TryInto, + zcash_primitives::legacy::keys::{self as legacy, IncomingViewingKey, NonHardenedChildIndex}, +}; + +#[cfg(all( + feature = "transparent-inputs", + any(test, feature = "test-dependencies") +))] +use zcash_primitives::legacy::TransparentAddress; + +#[cfg(feature = "unstable")] +use { + byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}, + std::convert::TryFrom, + std::io::{Read, Write}, + zcash_encoding::CompactSize, + zcash_primitives::consensus::BranchId, +}; + +#[cfg(feature = "orchard")] +use orchard::{self, keys::Scope}; + +#[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_primitives::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(hdwallet::error::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) + } + } + } +} + +/// 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 std::fmt::Display for DecodingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::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 = "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: legacy::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")] + legacy::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: legacy::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) -> &legacy::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 = 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)); + } + + #[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 != 64 { + return Err(DecodingError::LengthMismatch(Typecode::P2pkh, len)); + } + + let mut key = [0u8; 64]; + source + .read_exact(&mut key) + .map_err(|_| DecodingError::InsufficientData(Typecode::P2pkh))?; + + #[cfg(feature = "transparent-inputs")] + { + transparent = Some( + legacy::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.") + } + } + } +} + +impl error::Error for AddressGenerationError {} + +/// Specification for how a unified address should be generated from a unified viewing key. +#[derive(Clone, Copy, Debug)] +pub struct UnifiedAddressRequest { + has_orchard: bool, + has_sapling: bool, + has_p2pkh: bool, +} + +impl UnifiedAddressRequest { + /// Construct a new unified address request from its constituent parts. + /// + /// Returns `None` if the resulting unified address would not include at least one shielded receiver. + pub fn new(has_orchard: bool, has_sapling: bool, has_p2pkh: bool) -> Option { + let has_shielded_receiver = has_orchard || has_sapling; + + if !has_shielded_receiver { + None + } else { + Some(Self { + has_orchard, + has_sapling, + has_p2pkh, + }) + } + } + + /// Constructs a new unified address request that includes a request for a receiver of each + /// type that is supported given the active feature flags. + pub fn all() -> Option { + let _has_orchard = false; + #[cfg(feature = "orchard")] + let _has_orchard = true; + + let _has_sapling = false; + #[cfg(feature = "sapling")] + let _has_sapling = true; + + let _has_p2pkh = false; + #[cfg(feature = "transparent-inputs")] + let _has_p2pkh = true; + + Self::new(_has_orchard, _has_sapling, _has_p2pkh) + } + + /// Construct a new unified address request from its constituent parts. + /// + /// Panics: at least one of `has_orchard` or `has_sapling` must be `true`. + pub const fn unsafe_new(has_orchard: bool, has_sapling: bool, has_p2pkh: bool) -> Self { + if !(has_orchard || has_sapling) { + panic!("At least one shielded receiver must be requested.") + } + + Self { + has_orchard, + has_sapling, + has_p2pkh, + } + } +} + +#[cfg(feature = "transparent-inputs")] +impl From for DerivationError { + fn from(e: hdwallet::error::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, + #[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![], + ) + } + + /// Construct a UFVK from its constituent parts, after verifying that UIVK derivation can + /// succeed. + fn from_checked_parts( + #[cfg(feature = "transparent-inputs")] transparent: Option, + #[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) => legacy::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 = std::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<&legacy::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. + /// + /// 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. + /// + /// Returns an `Err(AddressGenerationError)` if no valid diversifier exists or if the features + /// required to satisfy the unified address request are not properly enabled. + #[allow(unused_mut)] + pub fn find_address( + &self, + mut 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. + /// + /// 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< + zcash_primitives::legacy::keys::ExternalIvk, + >, + #[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( + legacy::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 = std::iter::empty().chain(self.unknown.iter().map(|(typecode, data)| { + unified::Ivk::Unknown { + typecode: *typecode, + data: data.clone(), + } + })); + #[cfg(feature = "orchard")] + let items = items.chain( + self.orchard + .as_ref() + .map(|ivk| ivk.to_bytes()) + .map(unified::Ivk::Orchard), + ); + #[cfg(feature = "sapling")] + let items = items.chain( + self.sapling + .as_ref() + .map(|divk| divk.to_bytes()) + .map(unified::Ivk::Sapling), + ); + #[cfg(feature = "transparent-inputs")] + let items = items.chain( + self.transparent + .as_ref() + .map(|tivk| tivk.serialize().try_into().unwrap()) + .map(unified::Ivk::P2pkh), + ); + + unified::Uivk::try_from_items(items.collect()) + .expect("UnifiedIncomingViewingKey should only be constructed safely.") + } + + /// Returns the Transparent external IVK, if present. + #[cfg(feature = "transparent-inputs")] + pub fn transparent(&self) -> &Option { + &self.transparent + } + + /// Returns the Sapling IVK, if present. + #[cfg(feature = "sapling")] + pub fn sapling(&self) -> &Option<::sapling::zip32::IncomingViewingKey> { + &self.sapling + } + + /// Returns the Orchard IVK, if present. + #[cfg(feature = "orchard")] + pub fn orchard(&self) -> &Option { + &self.orchard + } + + /// Attempts to derive the Unified Address for the given diversifier index and + /// receiver types. + /// + /// Returns `None` if the specified index does not produce a valid diversifier. + pub fn address( + &self, + _j: DiversifierIndex, + request: UnifiedAddressRequest, + ) -> Result { + #[cfg(feature = "orchard")] + let mut orchard = None; + if request.has_orchard { + #[cfg(not(feature = "orchard"))] + 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 { + return Err(AddressGenerationError::KeyNotAvailable(Typecode::Orchard)); + } + } + + #[cfg(feature = "sapling")] + let mut sapling = None; + if request.has_sapling { + #[cfg(not(feature = "sapling"))] + 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 = Some( + divk.address_at(_j) + .ok_or(AddressGenerationError::InvalidSaplingDiversifierIndex(_j))?, + ); + } else { + return Err(AddressGenerationError::KeyNotAvailable(Typecode::Sapling)); + } + } + + #[cfg(feature = "transparent-inputs")] + let mut transparent = None; + if request.has_p2pkh { + #[cfg(not(feature = "transparent-inputs"))] + 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 transparent_j = to_transparent_child_index(_j) + .ok_or(AddressGenerationError::InvalidTransparentChildIndex(_j))?; + + transparent = Some( + tivk.derive_address(transparent_j) + .map_err(|_| AddressGenerationError::InvalidTransparentChildIndex(_j))?, + ); + } else { + 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 diversifier, and return the Unified Address constructed using that + /// diversifier along with the index at which the valid diversifier was found. + /// + /// Returns an `Err(AddressGenerationError)` if no valid diversifier exists or if the features + /// required to satisfy the unified address request are not properly enabled. + #[allow(unused_mut)] + pub fn find_address( + &self, + mut j: DiversifierIndex, + request: UnifiedAddressRequest, + ) -> Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError> { + // 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.has_p2pkh + && self.transparent.is_some() + && to_transparent_child_index(j).is_none() + { + return Err(AddressGenerationError::InvalidTransparentChildIndex(j)); + } + + // Find a working diversifier and construct the associated address. + loop { + let res = self.address(j, 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. + /// + /// 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) + } +} + +#[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::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_primitives::consensus::MAIN_NETWORK, 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}, + zcash_address::test_vectors, + zcash_primitives::legacy::keys::{AccountPrivKey, IncomingViewingKey}, + 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 zcash_primitives::legacy::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::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_new(false, true, true)) + .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.transparent().is_some() { + assert_eq!(tvua.transparent(), ua.transparent()); + } + #[cfg(feature = "sapling")] + if tvua.sapling().is_some() { + 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_primitives::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::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_new(false, true, true)) + .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.transparent().is_some() { + assert_eq!(tvua.transparent(), ua.transparent()); + } + #[cfg(feature = "sapling")] + 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(zcash_protocol::consensus::Network::MainNetwork)) { + let encoded = usk.to_bytes(Era::Orchard); + + let encoded_len = { + let len = 4; + + #[cfg(feature = "orchard")] + let len = len + 2 + 32; + + let len = len + 2 + 169; + + #[cfg(feature = "transparent-inputs")] + let len = len + 2 + 64; + + 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..eb881a80f8 --- /dev/null +++ b/zcash_keys/src/lib.rs @@ -0,0 +1,25 @@ +//! *A crate for Zcash key and address management.* +//! +//! `zcash_keys` contains Rust structs, traits and functions for creating Zcash spending +//! and viewing keys and addresses. +//! +//! ## Feature flags +#![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 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..9e9c557493 100644 --- a/zcash_primitives/CHANGELOG.md +++ b/zcash_primitives/CHANGELOG.md @@ -7,6 +7,258 @@ and this library adheres to Rust's notion of ## [Unreleased] +## [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 +279,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 +341,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 +791,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..b8f24b84cf 100644 --- a/zcash_primitives/Cargo.toml +++ b/zcash_primitives/Cargo.toml @@ -1,109 +1,130 @@ [package] name = "zcash_primitives" description = "Rust implementations of the Zcash primitives" -version = "0.12.0" +version = "0.15.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 # - 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, features = ["wnaf-memuse"] } +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 inputs # - `Error` type exposed -hdwallet = { version = "0.4", optional = true } +hdwallet = { workspace = true, optional = true } # - `SecretKey` and `PublicKey` types exposed -secp256k1 = { version = "0.26", optional = true } +secp256k1 = { workspace = true, optional = true } # - ZIP 339 bip0039 = { version = "0.10", features = ["std", "all-languages"] } # Dependencies used internally: # (Breaking upgrades to these are usually backwards-compatible, but check MSRVs.) +# - Documentation +document-features.workspace = true + # - Encodings -byteorder = "1" -hex = "0.4" +byteorder.workspace = true +hex.workspace = true # - Shielded protocols -bitvec = "1" -blake2s_simd = "1" +redjubjub = "0.7" # - Transparent inputs -ripemd = { version = "0.1", optional = true } +ripemd = { workspace = true, optional = true } # - ZIP 32 -aes = "0.8" -fpe = "0.6" +aes.workspace = true +fpe.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 +sapling = { workspace = true, features = ["test-dependencies"] } +orchard = { workspace = true, features = ["test-dependencies"] } [target.'cfg(unix)'.dev-dependencies] pprof = { version = "0.11", features = ["criterion", "flamegraph"] } # MSRV 1.56 [features] default = ["multicore"] -multicore = ["orchard/multicore"] -transparent-inputs = ["hdwallet", "ripemd", "secp256k1"] + +## Enables multithreading support for creating proofs. +multicore = ["orchard/multicore", "sapling/multicore"] + +## Enables spending transparent notes with the transaction builder. +transparent-inputs = ["dep:hdwallet", "dep:ripemd", "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", + "orchard/test-dependencies", + "sapling/test-dependencies", + "zcash_protocol/test-dependencies", +] + +## A feature used to isolate tests that are expensive to run. Test-only. +expensive-tests = [] [lib] bench = false @@ -112,9 +133,5 @@ bench = false name = "note_decryption" harness = false -[[bench]] -name = "pedersen_hash" -harness = false - [badges] maintenance = { status = "actively-developed" } 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..99048cb843 100644 --- a/zcash_primitives/benches/note_decryption.rs +++ b/zcash_primitives/benches/note_decryption.rs @@ -3,22 +3,20 @@ use std::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::{ 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, - }, + transaction::components::{sapling::zip212_enforcement, Amount}, }; #[cfg(unix)] @@ -27,27 +25,28 @@ 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(), - ) + .add_output(None, pa, NoteValue::from_raw(100), None) .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 +60,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 +92,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..e24dc1d2d3 100644 --- a/zcash_primitives/src/block.rs +++ b/zcash_primitives/src/block.rs @@ -10,6 +10,9 @@ 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 +42,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 +73,7 @@ impl Deref for BlockHeader { } } +/// The information contained in a Zcash block header. pub struct BlockHeaderData { pub version: i32, pub prev_block: BlockHash, 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/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 index e1d12a766f..398d9ab970 100644 --- a/zcash_primitives/src/legacy.rs +++ b/zcash_primitives/src/legacy.rs @@ -1,6 +1,8 @@ //! Support for legacy transparent addresses and scripts. use byteorder::{ReadBytesExt, WriteBytesExt}; + +use std::fmt; use std::io::{self, Read, Write}; use std::ops::Shl; @@ -9,29 +11,307 @@ use zcash_encoding::Vector; #[cfg(feature = "transparent-inputs")] pub mod keys; -/// Minimal subset of script opcodes. +/// Defined script opcodes. +/// +/// Most of the opcodes are unused by this crate, but we define them so that the alternate +/// `Debug` impl for [`Script`] renders correctly for unexpected scripts. +#[derive(Debug)] enum OpCode { // push value + Op0 = 0x00, // False PushData1 = 0x4c, PushData2 = 0x4d, PushData4 = 0x4e, + Negative1 = 0x4f, + Reserved = 0x50, + Op1 = 0x51, // True + Op2 = 0x52, + Op3 = 0x53, + Op4 = 0x54, + Op5 = 0x55, + Op6 = 0x56, + Op7 = 0x57, + Op8 = 0x58, + Op9 = 0x59, + Op10 = 0x5a, + Op11 = 0x5b, + Op12 = 0x5c, + Op13 = 0x5d, + Op14 = 0x5e, + Op15 = 0x5f, + Op16 = 0x60, + + // control + Nop = 0x61, + Ver = 0x62, + If = 0x63, + NotIf = 0x64, + VerIf = 0x65, + VerNotIf = 0x66, + Else = 0x67, + EndIf = 0x68, + Verify = 0x69, + Return = 0x6a, // stack ops + ToAltStack = 0x6b, + FromAltStack = 0x6c, + Drop2 = 0x6d, + Dup2 = 0x6e, + Dup3 = 0x6f, + Over2 = 0x70, + Rot2 = 0x71, + Swap2 = 0x72, + IfDup = 0x73, + Depth = 0x74, + Drop = 0x75, Dup = 0x76, + Nip = 0x77, + Over = 0x78, + Pick = 0x79, + Roll = 0x7a, + Rot = 0x7b, + Swap = 0x7c, + Tuck = 0x7d, + + // splice ops + Cat = 0x7e, // Disabled + Substr = 0x7f, // Disabled + Left = 0x80, // Disabled + Right = 0x81, // Disabled + Size = 0x82, // bit logic + Invert = 0x83, // Disabled + And = 0x84, // Disabled + Or = 0x85, // Disabled + Xor = 0x86, // Disabled Equal = 0x87, EqualVerify = 0x88, + Reserved1 = 0x89, + Reserved2 = 0x8a, + + // numeric + Add1 = 0x8b, + Sub1 = 0x8c, + Mul2 = 0x8d, // Disabled + Div2 = 0x8e, // Disabled + Negate = 0x8f, + Abs = 0x90, + Not = 0x91, + NotEqual0 = 0x92, + + Add = 0x93, + Sub = 0x94, + Mul = 0x95, // Disabled + Div = 0x96, // Disabled + Mod = 0x97, // Disabled + LShift = 0x98, // Disabled + RShift = 0x99, // Disabled + + BoolAnd = 0x9a, + BoolOr = 0x9b, + NumEqual = 0x9c, + NumEqualVerify = 0x9d, + NumNotEqual = 0x9e, + LessThan = 0x9f, + GreaterThan = 0xa0, + LessThanOrEqual = 0xa1, + GreaterThanOrEqual = 0xa2, + Min = 0xa3, + Max = 0xa4, + + Within = 0xa5, // crypto + Ripemd160 = 0xa6, + Sha1 = 0xa7, + Sha256 = 0xa8, Hash160 = 0xa9, + Hash256 = 0xaa, + CodeSeparator = 0xab, // Disabled CheckSig = 0xac, + CheckSigVerify = 0xad, + CheckMultisig = 0xae, + CheckMultisigVerify = 0xaf, + + // expansion + Nop1 = 0xb0, + CheckLockTimeVerify = 0xb1, + Nop3 = 0xb2, + Nop4 = 0xb3, + Nop5 = 0xb4, + Nop6 = 0xb5, + Nop7 = 0xb6, + Nop8 = 0xb7, + Nop9 = 0xb8, + Nop10 = 0xb9, + + InvalidOpCode = 0xff, +} + +impl OpCode { + fn parse(b: u8) -> Option { + match b { + 0x00 => Some(OpCode::Op0), + 0x4c => Some(OpCode::PushData1), + 0x4d => Some(OpCode::PushData2), + 0x4e => Some(OpCode::PushData4), + 0x4f => Some(OpCode::Negative1), + 0x50 => Some(OpCode::Reserved), + 0x51 => Some(OpCode::Op1), + 0x52 => Some(OpCode::Op2), + 0x53 => Some(OpCode::Op3), + 0x54 => Some(OpCode::Op4), + 0x55 => Some(OpCode::Op5), + 0x56 => Some(OpCode::Op6), + 0x57 => Some(OpCode::Op7), + 0x58 => Some(OpCode::Op8), + 0x59 => Some(OpCode::Op9), + 0x5a => Some(OpCode::Op10), + 0x5b => Some(OpCode::Op11), + 0x5c => Some(OpCode::Op12), + 0x5d => Some(OpCode::Op13), + 0x5e => Some(OpCode::Op14), + 0x5f => Some(OpCode::Op15), + 0x60 => Some(OpCode::Op16), + 0x61 => Some(OpCode::Nop), + 0x62 => Some(OpCode::Ver), + 0x63 => Some(OpCode::If), + 0x64 => Some(OpCode::NotIf), + 0x65 => Some(OpCode::VerIf), + 0x66 => Some(OpCode::VerNotIf), + 0x67 => Some(OpCode::Else), + 0x68 => Some(OpCode::EndIf), + 0x69 => Some(OpCode::Verify), + 0x6a => Some(OpCode::Return), + 0x6b => Some(OpCode::ToAltStack), + 0x6c => Some(OpCode::FromAltStack), + 0x6d => Some(OpCode::Drop2), + 0x6e => Some(OpCode::Dup2), + 0x6f => Some(OpCode::Dup3), + 0x70 => Some(OpCode::Over2), + 0x71 => Some(OpCode::Rot2), + 0x72 => Some(OpCode::Swap2), + 0x73 => Some(OpCode::IfDup), + 0x74 => Some(OpCode::Depth), + 0x75 => Some(OpCode::Drop), + 0x76 => Some(OpCode::Dup), + 0x77 => Some(OpCode::Nip), + 0x78 => Some(OpCode::Over), + 0x79 => Some(OpCode::Pick), + 0x7a => Some(OpCode::Roll), + 0x7b => Some(OpCode::Rot), + 0x7c => Some(OpCode::Swap), + 0x7d => Some(OpCode::Tuck), + 0x7e => Some(OpCode::Cat), + 0x7f => Some(OpCode::Substr), + 0x80 => Some(OpCode::Left), + 0x81 => Some(OpCode::Right), + 0x82 => Some(OpCode::Size), + 0x83 => Some(OpCode::Invert), + 0x84 => Some(OpCode::And), + 0x85 => Some(OpCode::Or), + 0x86 => Some(OpCode::Xor), + 0x87 => Some(OpCode::Equal), + 0x88 => Some(OpCode::EqualVerify), + 0x89 => Some(OpCode::Reserved1), + 0x8a => Some(OpCode::Reserved2), + 0x8b => Some(OpCode::Add1), + 0x8c => Some(OpCode::Sub1), + 0x8d => Some(OpCode::Mul2), + 0x8e => Some(OpCode::Div2), + 0x8f => Some(OpCode::Negate), + 0x90 => Some(OpCode::Abs), + 0x91 => Some(OpCode::Not), + 0x92 => Some(OpCode::NotEqual0), + 0x93 => Some(OpCode::Add), + 0x94 => Some(OpCode::Sub), + 0x95 => Some(OpCode::Mul), + 0x96 => Some(OpCode::Div), + 0x97 => Some(OpCode::Mod), + 0x98 => Some(OpCode::LShift), + 0x99 => Some(OpCode::RShift), + 0x9a => Some(OpCode::BoolAnd), + 0x9b => Some(OpCode::BoolOr), + 0x9c => Some(OpCode::NumEqual), + 0x9d => Some(OpCode::NumEqualVerify), + 0x9e => Some(OpCode::NumNotEqual), + 0x9f => Some(OpCode::LessThan), + 0xa0 => Some(OpCode::GreaterThan), + 0xa1 => Some(OpCode::LessThanOrEqual), + 0xa2 => Some(OpCode::GreaterThanOrEqual), + 0xa3 => Some(OpCode::Min), + 0xa4 => Some(OpCode::Max), + 0xa5 => Some(OpCode::Within), + 0xa6 => Some(OpCode::Ripemd160), + 0xa7 => Some(OpCode::Sha1), + 0xa8 => Some(OpCode::Sha256), + 0xa9 => Some(OpCode::Hash160), + 0xaa => Some(OpCode::Hash256), + 0xab => Some(OpCode::CodeSeparator), + 0xac => Some(OpCode::CheckSig), + 0xad => Some(OpCode::CheckSigVerify), + 0xae => Some(OpCode::CheckMultisig), + 0xaf => Some(OpCode::CheckMultisigVerify), + 0xb0 => Some(OpCode::Nop1), + 0xb1 => Some(OpCode::CheckLockTimeVerify), + 0xb2 => Some(OpCode::Nop3), + 0xb3 => Some(OpCode::Nop4), + 0xb4 => Some(OpCode::Nop5), + 0xb5 => Some(OpCode::Nop6), + 0xb6 => Some(OpCode::Nop7), + 0xb7 => Some(OpCode::Nop8), + 0xb8 => Some(OpCode::Nop9), + 0xb9 => Some(OpCode::Nop10), + 0xff => Some(OpCode::InvalidOpCode), + _ => None, + } + } } /// A serialized script, used inside transparent inputs and outputs of a transaction. -#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Default, PartialEq, Eq)] pub struct Script(pub Vec); +impl fmt::Debug for Script { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + struct ScriptPrinter<'s>(&'s [u8]); + impl<'s> fmt::Debug for ScriptPrinter<'s> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut l = f.debug_list(); + let mut unknown: Option = None; + for b in self.0 { + if let Some(opcode) = OpCode::parse(*b) { + if let Some(s) = unknown.take() { + l.entry(&s); + } + l.entry(&opcode); + } else { + let encoded = format!("{:02x}", b); + if let Some(s) = &mut unknown { + s.push_str(&encoded); + } else { + unknown = Some(encoded); + } + } + } + l.finish() + } + } + + if f.alternate() { + f.debug_tuple("Script") + .field(&ScriptPrinter(&self.0)) + .finish() + } else { + f.debug_tuple("Script") + .field(&hex::encode(&self.0)) + .finish() + } + } +} + impl Script { pub fn read(mut reader: R) -> io::Result { let script = Vector::read(&mut reader, |r| r.read_u8())?; @@ -50,14 +330,14 @@ impl Script { { let mut hash = [0; 20]; hash.copy_from_slice(&self.0[3..23]); - Some(TransparentAddress::PublicKey(hash)) + Some(TransparentAddress::PublicKeyHash(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)) + Some(TransparentAddress::ScriptHash(hash)) } else { None } @@ -97,15 +377,15 @@ impl Shl<&[u8]> for Script { /// 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 + PublicKeyHash([u8; 20]), + ScriptHash([u8; 20]), } impl TransparentAddress { /// Generate the `scriptPubKey` corresponding to this address. pub fn script(&self) -> Script { match self { - TransparentAddress::PublicKey(key_id) => { + TransparentAddress::PublicKeyHash(key_id) => { // P2PKH script Script::default() << OpCode::Dup @@ -114,7 +394,7 @@ impl TransparentAddress { << OpCode::EqualVerify << OpCode::CheckSig } - TransparentAddress::Script(script_id) => { + TransparentAddress::ScriptHash(script_id) => { // P2SH script Script::default() << OpCode::Hash160 << &script_id[..] << OpCode::Equal } @@ -130,7 +410,7 @@ pub mod testing { prop_compose! { pub fn arb_transparent_addr()(v in proptest::array::uniform20(any::())) -> TransparentAddress { - TransparentAddress::PublicKey(v) + TransparentAddress::PublicKeyHash(v) } } } @@ -181,7 +461,7 @@ mod tests { #[test] fn p2pkh() { - let addr = TransparentAddress::PublicKey([4; 20]); + let addr = TransparentAddress::PublicKeyHash([4; 20]); assert_eq!( &addr.script().0, &[ @@ -194,7 +474,7 @@ mod tests { #[test] fn p2sh() { - let addr = TransparentAddress::Script([7; 20]); + let addr = TransparentAddress::ScriptHash([7; 20]); assert_eq!( &addr.script().0, &[ diff --git a/zcash_primitives/src/legacy/keys.rs b/zcash_primitives/src/legacy/keys.rs index fead35a812..fbed55028e 100644 --- a/zcash_primitives/src/legacy/keys.rs +++ b/zcash_primitives/src/legacy/keys.rs @@ -1,23 +1,114 @@ +//! Transparent key components. + use hdwallet::{ traits::{Deserialize, Serialize}, ExtendedPrivKey, ExtendedPubKey, KeyIndex, }; use secp256k1::PublicKey; use sha2::{Digest, Sha256}; +use subtle::{Choice, ConstantTimeEq}; -use crate::{consensus, keys::prf_expand_vec, zip32::AccountId}; +use zcash_protocol::consensus::{self, NetworkConstants}; +use zcash_spec::PrfExpand; +use zip32::AccountId; use super::TransparentAddress; -const MAX_TRANSPARENT_CHILD_INDEX: u32 = 0x7FFFFFFF; +/// The scope of a transparent key. +/// +/// This type can represent [`zip32`] internal and external scopes, as well as custom scopes that +/// may be used in non-hardened derivation at the `change` level of the BIP 44 key path. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TransparentKeyScope(u32); + +impl TransparentKeyScope { + pub fn custom(i: u32) -> Option { + if i < (1 << 31) { + Some(TransparentKeyScope(i)) + } else { + None + } + } +} + +impl From for TransparentKeyScope { + fn from(value: zip32::Scope) -> Self { + match value { + zip32::Scope::External => TransparentKeyScope(0), + zip32::Scope::Internal => TransparentKeyScope(1), + } + } +} + +impl From for KeyIndex { + fn from(value: TransparentKeyScope) -> Self { + KeyIndex::Normal(value.0) + } +} + +/// A child index for a derived transparent address. +/// +/// Only NON-hardened derivation is supported. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct NonHardenedChildIndex(u32); + +impl ConstantTimeEq for NonHardenedChildIndex { + fn ct_eq(&self, other: &Self) -> Choice { + self.0.ct_eq(&other.0) + } +} + +impl NonHardenedChildIndex { + pub const ZERO: NonHardenedChildIndex = NonHardenedChildIndex(0); + + /// Parses the given ZIP 32 child index. + /// + /// Returns `None` if the hardened bit is set. + pub fn from_index(i: u32) -> Option { + if i < (1 << 31) { + Some(NonHardenedChildIndex(i)) + } else { + None + } + } + + /// Returns the index as a 32-bit integer. + pub fn index(&self) -> u32 { + self.0 + } + + pub fn next(&self) -> Option { + // overflow cannot happen because self.0 is 31 bits, and the next index is at most 32 bits + // which in that case would lead from_index to return None. + Self::from_index(self.0 + 1) + } +} + +impl TryFrom for NonHardenedChildIndex { + type Error = (); -/// A type representing a BIP-44 private key at the account path level -/// `m/44'/'/' + fn try_from(value: KeyIndex) -> Result { + match value { + KeyIndex::Normal(i) => NonHardenedChildIndex::from_index(i).ok_or(()), + KeyIndex::Hardened(_) => Err(()), + } + } +} + +impl From for KeyIndex { + fn from(value: NonHardenedChildIndex) -> Self { + Self::Normal(value.index()) + } +} + +/// A [BIP44] private key at the account path level `m/44'/'/'`. +/// +/// [BIP44]: https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki #[derive(Clone, Debug)] pub struct AccountPrivKey(ExtendedPrivKey); impl AccountPrivKey { - /// Performs derivation of the extended private key for the BIP-44 path: + /// Performs derivation of the extended private key for the BIP44 path: /// `m/44'/'/'`. /// /// This produces the root of the derivation tree for transparent @@ -42,28 +133,35 @@ impl AccountPrivKey { 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( + /// Derives the BIP44 private spending key for the child path + /// `m/44'/'/'//`. + pub fn derive_secret_key( &self, - child_index: u32, + scope: TransparentKeyScope, + child_index: NonHardenedChildIndex, ) -> Result { self.0 - .derive_private_key(KeyIndex::Normal(0))? - .derive_private_key(KeyIndex::Normal(child_index)) + .derive_private_key(scope.into())? + .derive_private_key(child_index.into()) .map(|k| k.private_key) } - /// Derives the BIP-44 private spending key for the internal (change) child path + /// Derives the BIP44 private spending key for the external (incoming payment) child path + /// `m/44'/'/'/0/`. + pub fn derive_external_secret_key( + &self, + child_index: NonHardenedChildIndex, + ) -> Result { + self.derive_secret_key(zip32::Scope::External.into(), child_index) + } + + /// Derives the BIP44 private spending key for the internal (change) child path /// `m/44'/'/'/1/`. pub fn derive_internal_secret_key( &self, - child_index: u32, + child_index: NonHardenedChildIndex, ) -> Result { - self.0 - .derive_private_key(KeyIndex::Normal(1))? - .derive_private_key(KeyIndex::Normal(child_index)) - .map(|k| k.private_key) + self.derive_secret_key(zip32::Scope::Internal.into(), child_index) } /// Returns the `AccountPrivKey` serialized using the encoding for a @@ -81,16 +179,17 @@ impl AccountPrivKey { } } -/// A type representing a BIP-44 public key at the account path level -/// `m/44'/'/'`. +/// A [BIP44] 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. +/// +/// [BIP44]: https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki #[derive(Clone, Debug)] pub struct AccountPubKey(ExtendedPubKey); impl AccountPubKey { - /// Derives the BIP-44 public key at the external "change level" path + /// Derives the BIP44 public key at the external "change level" path /// `m/44'/'/'/0`. pub fn derive_external_ivk(&self) -> Result { self.0 @@ -98,7 +197,7 @@ impl AccountPubKey { .map(ExternalIvk) } - /// Derives the BIP-44 public key at the internal "change level" path + /// Derives the BIP44 public key at the internal "change level" path /// `m/44'/'/'/1`. pub fn derive_internal_ivk(&self) -> Result { self.0 @@ -111,11 +210,8 @@ impl AccountPubKey { /// /// [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 i_ovk = PrfExpand::TRANSPARENT_ZIP316_OVK + .with(&self.0.chain_code, &self.0.public_key.serialize()); let ovk_external = ExternalOvk(i_ovk[..32].try_into().unwrap()); let ovk_internal = InternalOvk(i_ovk[32..].try_into().unwrap()); @@ -151,7 +247,7 @@ impl AccountPubKey { /// 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( + TransparentAddress::PublicKeyHash( *ripemd::Ripemd160::digest(Sha256::digest(pubkey.serialize())).as_ref(), ) } @@ -164,35 +260,51 @@ pub(crate) mod private { } } +/// Trait representing a transparent "incoming viewing key". +/// +/// Unlike the Sapling and Orchard shielded protocols (which have viewing keys built into +/// their key trees and bound to specific spending keys), the transparent protocol has no +/// "viewing key" concept. Transparent viewing keys are instead emulated by making two +/// observations: +/// +/// - [BIP32] hierarchical derivation is structured as a tree. +/// - The [BIP44] key paths use non-hardened derivation below the account level. +/// +/// A transparent viewing key for an account is thus defined as the root of a specific +/// non-hardened subtree underneath the account's path. +/// +/// [BIP32]: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki +/// [BIP44]: https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki 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, + child_index: NonHardenedChildIndex, ) -> Result { let child_key = self .extended_pubkey() - .derive_public_key(KeyIndex::Normal(child_index))?; + .derive_public_key(child_index.into())?; 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 { + fn default_address(&self) -> (TransparentAddress, NonHardenedChildIndex) { + let mut child_index = NonHardenedChildIndex::ZERO; + loop { match self.derive_address(child_index) { Ok(addr) => { return (addr, child_index); } Err(_) => { - child_index += 1; + child_index = child_index.next().unwrap_or_else(|| { + panic!("Exhausted child index space attempting to find a default address."); + }); } } } - panic!("Exhausted child index space attempting to find a default address."); } fn serialize(&self) -> Vec { @@ -212,9 +324,12 @@ pub trait IncomingViewingKey: private::SealedChangeLevelKey + std::marker::Sized } } -/// 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. +/// An incoming viewing key at the [BIP44] "external" path +/// `m/44'/'/'/0`. +/// +/// This allows derivation of child addresses that may be provided to external parties. +/// +/// [BIP44]: https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki #[derive(Clone, Debug)] pub struct ExternalIvk(ExtendedPubKey); @@ -230,10 +345,13 @@ impl private::SealedChangeLevelKey for ExternalIvk { 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 +/// An incoming viewing key at the [BIP44] "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. +/// +/// [BIP44]: https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki #[derive(Clone, Debug)] pub struct InternalIvk(ExtendedPubKey); @@ -249,7 +367,7 @@ impl private::SealedChangeLevelKey for InternalIvk { impl IncomingViewingKey for InternalIvk {} -/// Internal ovk used for autoshielding. +/// Internal outgoing viewing key used for autoshielding. pub struct InternalOvk([u8; 32]); impl InternalOvk { @@ -258,7 +376,7 @@ impl InternalOvk { } } -/// External ovk used by zcashd for transparent -> shielded spends to +/// External outgoing viewing key used by `zcashd` for transparent-to-shielded spends to /// external receivers. pub struct ExternalOvk([u8; 32]); @@ -270,7 +388,11 @@ impl ExternalOvk { #[cfg(test)] mod tests { + use hdwallet::KeyIndex; + use subtle::ConstantTimeEq; + use super::AccountPubKey; + use super::NonHardenedChildIndex; #[test] fn check_ovk_test_vectors() { @@ -517,4 +639,54 @@ mod tests { assert_eq!(tv.external_ovk, external.as_bytes()); } } + + #[test] + fn nonhardened_indexes_accepted() { + assert_eq!(0, NonHardenedChildIndex::from_index(0).unwrap().index()); + assert_eq!( + 0x7fffffff, + NonHardenedChildIndex::from_index(0x7fffffff) + .unwrap() + .index() + ); + } + + #[test] + fn hardened_indexes_rejected() { + assert!(NonHardenedChildIndex::from_index(0x80000000).is_none()); + assert!(NonHardenedChildIndex::from_index(0xffffffff).is_none()); + } + + #[test] + fn nonhardened_index_next() { + assert_eq!(1, NonHardenedChildIndex::ZERO.next().unwrap().index()); + assert!(NonHardenedChildIndex::from_index(0x7fffffff) + .unwrap() + .next() + .is_none()); + } + + #[test] + fn nonhardened_index_ct_eq() { + assert!(check( + NonHardenedChildIndex::ZERO, + NonHardenedChildIndex::ZERO + )); + assert!(!check( + NonHardenedChildIndex::ZERO, + NonHardenedChildIndex::ZERO.next().unwrap() + )); + + fn check(v1: T, v2: T) -> bool { + v1.ct_eq(&v2).into() + } + } + + #[test] + fn nonhardened_index_tryfrom_keyindex() { + let nh: NonHardenedChildIndex = KeyIndex::Normal(0).try_into().unwrap(); + assert_eq!(nh.index(), 0); + + assert!(NonHardenedChildIndex::try_from(KeyIndex::Hardened(0)).is_err()); + } } diff --git a/zcash_primitives/src/lib.rs b/zcash_primitives/src/lib.rs index 87ea2baaf0..f514fb1777 100644 --- a/zcash_primitives/src/lib.rs +++ b/zcash_primitives/src/lib.rs @@ -2,27 +2,30 @@ //! //! `zcash_primitives` is a library that provides the core structs and functions necessary //! for working with Zcash. +//! +//! ## 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)] +// Present to reduce refactoring noise from changing all the imports inside this crate for +// the `sapling` crate extraction. +#![allow(clippy::single_component_path_imports)] pub mod block; -pub mod consensus; -pub mod constants; -pub mod keys; +pub use zcash_protocol::consensus; +pub use zcash_protocol::constants; pub mod legacy; -pub mod memo; +pub use zcash_protocol::memo; pub mod merkle_tree; -pub mod sapling; +use sapling; pub mod transaction; -pub mod zip32; -pub mod zip339; - -#[cfg(feature = "zfuture")] +pub use zip32; +#[cfg(zcash_unstable = "zfuture")] pub mod extensions; - -#[cfg(test)] -mod test_vectors; +pub mod zip339; diff --git a/zcash_primitives/src/merkle_tree.rs b/zcash_primitives/src/merkle_tree.rs index 176d3b4375..e365b90629 100644 --- a/zcash_primitives/src/merkle_tree.rs +++ b/zcash_primitives/src/merkle_tree.rs @@ -1,4 +1,4 @@ -//! 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 incrementalmerkletree::{ @@ -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 @@ -98,7 +115,7 @@ 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 @@ -292,6 +309,7 @@ pub mod testing { use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use incrementalmerkletree::frontier::testing::TestNode; use std::io::{self, Read, Write}; + use zcash_encoding::Vector; use super::HashSer; @@ -304,6 +322,23 @@ pub mod testing { writer.write_u64::(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, + format!("Not a valid utf8 string: {:?}", e), + ) + }) + }) + } + + fn write(&self, writer: W) -> io::Result<()> { + Vector::write(writer, self.as_bytes(), |w, b| w.write_u8(*b)) + } + } } #[cfg(test)] @@ -797,7 +832,7 @@ 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)); 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 06f7b1480e..0000000000 --- a/zcash_primitives/src/sapling/note_encryption.rs +++ /dev/null @@ -1,1640 +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, - OutPlaintextBytes, OutgoingCipherKey, ShieldedOutput, AEAD_TAG_SIZE, MEMO_SIZE, - OUT_PLAINTEXT_SIZE, -}; - -/// 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`] for V2. -pub const NOTE_PLAINTEXT_SIZE: usize = COMPACT_NOTE_SIZE + MEMO_SIZE; -/// The size of an encrypted note plaintext. -pub const ENC_CIPHERTEXT_SIZE: usize = NOTE_PLAINTEXT_SIZE + AEAD_TAG_SIZE; - -/// a type to represent the raw bytes of a note plaintext. -#[derive(Clone, Debug)] -pub struct NotePlaintextBytes(pub [u8; NOTE_PLAINTEXT_SIZE]); - -/// a type to represent the raw bytes of an encrypted note plaintext. -#[derive(Clone, Debug)] -pub struct NoteCiphertextBytes(pub [u8; ENC_CIPHERTEXT_SIZE]); - -/// a type to represent the raw bytes of a compact note. -#[derive(Clone, Debug)] -pub struct CompactNotePlaintextBytes(pub [u8; COMPACT_NOTE_SIZE]); - -/// a type to represent the raw bytes of an encrypted compact note. -#[derive(Clone, Debug)] -pub struct CompactNoteCiphertextBytes(pub [u8; COMPACT_NOTE_SIZE]); - -impl AsMut<[u8]> for NotePlaintextBytes { - fn as_mut(&mut self) -> &mut [u8] { - self.0.as_mut() - } -} - -impl From<&[u8]> for NotePlaintextBytes { - fn from(s: &[u8]) -> Self - where - Self: Sized, - { - NotePlaintextBytes(s.try_into().unwrap()) - } -} - -impl AsRef<[u8]> for NoteCiphertextBytes { - fn as_ref(&self) -> &[u8] { - self.0.as_ref() - } -} - -impl From<&[u8]> for NoteCiphertextBytes { - fn from(s: &[u8]) -> Self - where - Self: Sized, - { - NoteCiphertextBytes(s.try_into().unwrap()) - } -} - -impl AsMut<[u8]> for CompactNotePlaintextBytes { - fn as_mut(&mut self) -> &mut [u8] { - self.0.as_mut() - } -} - -impl From<&[u8]> for CompactNotePlaintextBytes { - fn from(s: &[u8]) -> Self - where - Self: Sized, - { - CompactNotePlaintextBytes(s.try_into().unwrap()) - } -} - -impl AsRef<[u8]> for CompactNoteCiphertextBytes { - fn as_ref(&self) -> &[u8] { - self.0.as_ref() - } -} - -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; - - type NotePlaintextBytes = NotePlaintextBytes; - type NoteCiphertextBytes = NoteCiphertextBytes; - type CompactNotePlaintextBytes = CompactNotePlaintextBytes; - type CompactNoteCiphertextBytes = CompactNoteCiphertextBytes; - - 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: &CompactNotePlaintextBytes, - ) -> Option<(Self::Note, Self::Recipient)> { - sapling_parse_note_plaintext_without_memo(self, &plaintext.0, |diversifier| { - DiversifiedTransmissionKey::derive(ivk, diversifier) - }) - } - - fn parse_note_plaintext_without_memo_ovk( - &self, - pk_d: &Self::DiversifiedTransmissionKey, - plaintext: &CompactNotePlaintextBytes, - ) -> 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::CompactNotePlaintextBytes, Self::Memo) { - let (compact, memo) = plaintext.0.split_at(COMPACT_NOTE_SIZE); - ( - compact.try_into().unwrap(), - MemoBytes::from_bytes(memo).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>, ->( - 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>, ->( - 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, 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, - note_encryption::{ENC_CIPHERTEXT_SIZE, NOTE_PLAINTEXT_SIZE}, - 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().0, - 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..6e43bc4612 100644 --- a/zcash_primitives/src/transaction/builder.rs +++ b/zcash_primitives/src/transaction/builder.rs @@ -5,35 +5,38 @@ use std::error; use std::fmt; use std::sync::mpsc::Sender; -use rand::{rngs::OsRng, CryptoRng, RngCore}; +use rand::{CryptoRng, RngCore}; use crate::{ - consensus::{self, BlockHeight, BranchId}, - keys::OutgoingViewingKey, + consensus::{self, BlockHeight, BranchId, NetworkUpgrade}, legacy::TransparentAddress, memo::MemoBytes, - sapling::{self, prover::TxProver, value::NoteValue, Diversifier, Note, PaymentAddress}, + sapling::{ + self, + builder::SaplingMetadata, + prover::{OutputProver, SpendProver}, + Note, PaymentAddress, + }, transaction::{ components::{ amount::{Amount, BalanceError}, - sapling::{ - builder::{self as sapling_builder, SaplingBuilder, SaplingMetadata}, - fees as sapling_fees, - }, - transparent::{self, builder::TransparentBuilder}, + transparent::{self, builder::TransparentBuilder, TxOut}, }, fees::FeeRule, sighash::{signature_hash, SignableInput}, txid::TxIdDigester, Transaction, TransactionData, TxVersion, Unauthorized, }, - zip32::ExtendedSpendingKey, }; #[cfg(feature = "transparent-inputs")] -use crate::transaction::components::transparent::TxOut; +use crate::transaction::components::transparent::builder::TransparentInputInfo; + +use orchard::note::AssetBase; +#[cfg(not(feature = "transparent-inputs"))] +use std::convert::Infallible; -#[cfg(feature = "zfuture")] +#[cfg(zcash_unstable = "zfuture")] use crate::{ extensions::transparent::{ExtensionTxBuilder, ToPayload}, transaction::{ @@ -45,13 +48,32 @@ use crate::{ }, }; +use super::components::amount::NonNegativeAmount; +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; +/// 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), @@ -59,15 +81,27 @@ pub enum Error { /// add a change output. ChangeRequired(Amount), /// 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,7 +122,18 @@ 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), } } @@ -102,6 +147,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 +173,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 +199,100 @@ 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_VANILLA, *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 + } +} + /// 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, transparent_builder: TransparentBuilder, - sapling_builder: SaplingBuilder

, - #[cfg(feature = "zfuture")] + sapling_builder: Option, + orchard_builder: Option, + // TODO: In the future, instead of taking the spending keys as arguments when calling + // `add_sapling_spend` or `add_orchard_spend`, we will build an unauthorized, unproven + // transaction, and then the caller will be responsible for using the spending keys or their + // derivatives for proving and signing to complete transaction creation. + sapling_asks: Vec, + orchard_saks: Vec, + #[cfg(zcash_unstable = "zfuture")] tze_builder: TzeBuilder<'a, TransactionData>, - #[cfg(not(feature = "zfuture"))] + #[cfg(not(zcash_unstable = "zfuture"))] tze_builder: std::marker::PhantomData<&'a ()>, - progress_notifier: Option>, + progress_notifier: U, } -impl<'a, P, R> Builder<'a, P, R> { +impl<'a, P, U: sapling::builder::ProverProgress> Builder<'a, P, U> { /// Returns the network parameters that the builder has been configured for. pub fn params(&self) -> &P { &self.params @@ -159,114 +305,191 @@ 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, transparent_builder: TransparentBuilder::empty(), - sapling_builder: SaplingBuilder::new(params, target_height), - #[cfg(feature = "zfuture")] + sapling_builder, + orchard_builder, + sapling_asks: vec![], + orchard_saks: Vec::new(), + #[cfg(zcash_unstable = "zfuture")] tze_builder: TzeBuilder::empty(), - #[cfg(not(feature = "zfuture"))] + #[cfg(not(zcash_unstable = "zfuture"))] tze_builder: std::marker::PhantomData, - progress_notifier: None, + 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. + pub fn with_progress_notifier( + self, + progress_notifier: Sender, + ) -> Builder<'a, P, Sender> { + Builder { + params: self.params, + build_config: self.build_config, + target_height: self.target_height, + expiry_height: self.expiry_height, + transparent_builder: self.transparent_builder, + sapling_builder: self.sapling_builder, + orchard_builder: self.orchard_builder, + sapling_asks: self.sapling_asks, + orchard_saks: self.orchard_saks, + tze_builder: self.tze_builder, + progress_notifier, + } + } +} + +impl<'a, P: consensus::Parameters, U: sapling::builder::ProverProgress> Builder<'a, 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, + sk: &orchard::keys::SpendingKey, + note: orchard::Note, + merkle_path: orchard::tree::MerklePath, + ) -> Result<(), Error> { + if let Some(builder) = self.orchard_builder.as_mut() { + builder.add_spend(orchard::keys::FullViewingKey::from(sk), note, merkle_path)?; + + self.orchard_saks + .push(orchard::keys::SpendAuthorizingKey::from(sk)); + + 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), + AssetBase::native(), + Some(*memo.as_array()), + ) + .map_err(Error::OrchardRecipient) + } + /// Adds a Sapling note to be spent in this transaction. /// /// Returns an error if the given Merkle path does not have the same anchor as the /// paths for previous Sapling notes. - pub fn add_sapling_spend( + pub fn add_sapling_spend( &mut self, - extsk: ExtendedSpendingKey, - diversifier: Diversifier, + extsk: &sapling::zip32::ExtendedSpendingKey, 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(extsk, note, merkle_path)?; + + self.sapling_asks.push(extsk.expsk.ask.clone()); + 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: NonNegativeAmount, 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()), + Some(*memo.as_array()), + ) + .map_err(Error::SaplingBuild) } /// Adds a transparent coin to be spent in this transaction. #[cfg(feature = "transparent-inputs")] - #[cfg_attr(docsrs, doc(cfg(feature = "transparent-inputs")))] pub fn add_transparent_input( &mut self, sk: secp256k1::SecretKey, @@ -280,27 +503,27 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { pub fn add_transparent_output( &mut self, to: &TransparentAddress, - value: Amount, + value: NonNegativeAmount, ) -> 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. + /// Returns the sum of the transparent, Sapling, Orchard, and TZE value balances. fn value_balance(&self) -> Result { let value_balances = [ self.transparent_builder.value_balance()?, - self.sapling_builder.value_balance(), - #[cfg(feature = "zfuture")] + self.sapling_builder + .as_ref() + .map_or_else(Amount::zero, |builder| builder.value_balance::()), + self.orchard_builder.as_ref().map_or_else( + || Ok(Amount::zero()), + |builder| { + builder + .value_balance::() + .map_err(|_| BalanceError::Overflow) + }, + )?, + #[cfg(zcash_unstable = "zfuture")] self.tze_builder.value_balance()?, ]; @@ -310,59 +533,140 @@ 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, + /// 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<(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( &self.params, self.target_height, - self.transparent_builder.inputs(), + transparent_inputs, self.transparent_builder.outputs(), - self.sapling_builder.inputs().len(), - self.sapling_builder.bundle_output_count(), + 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(), + transparent_inputs, self.transparent_builder.outputs(), - self.sapling_builder.inputs().len(), - self.sapling_builder.bundle_output_count(), + 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) + } - 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. + pub fn build( + self, + 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(rng, spend_prover, output_prover, fee) } - fn build_internal( + /// 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, + 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(rng, spend_prover, output_prover, fee) + } + + fn build_internal( self, - prover: &impl TxProver, - fee: Amount, - ) -> Result<(Transaction, SaplingMetadata), Error> { + mut rng: R, + spend_prover: &SP, + output_prover: &OP, + fee: NonNegativeAmount, + ) -> Result> { let consensus_branch_id = BranchId::for_height(&self.params, self.target_height); // determine transaction version @@ -373,7 +677,8 @@ 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()) { Ordering::Less => { @@ -387,20 +692,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::(&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 { @@ -411,8 +747,8 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { transparent_bundle, sprout_bundle: None, sapling_bundle, - orchard_bundle: None, - #[cfg(feature = "zfuture")] + orchard_bundle, + #[cfg(zcash_unstable = "zfuture")] tze_bundle, }; @@ -430,7 +766,7 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { ) }); - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] let tze_bundle = unauthed_tx .tze_bundle .clone() @@ -444,17 +780,32 @@ 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_bundle = unauthed_tx .sapling_bundle .map(|b| { - b.apply_signatures(prover, &mut ctx, &mut rng, shielded_sig_commitment.as_ref()) + b.apply_signatures( + &mut rng, + *shielded_sig_commitment.as_ref(), + &self.sapling_asks, + ) }) .transpose() - .map_err(Error::SaplingBuild)? - { - Some((bundle, meta)) => (Some(bundle), meta), - None => (None, SaplingMetadata::empty()), - }; + .map_err(Error::SaplingBuild)?; + + let orchard_bundle = unauthed_tx + .orchard_bundle + .map(|b| { + b.create_proof(&orchard::circuit::ProvingKey::build(), &mut rng) + .and_then(|b| { + b.apply_signatures( + &mut rng, + *shielded_sig_commitment.as_ref(), + &self.orchard_saks, + ) + }) + }) + .transpose() + .map_err(Error::OrchardBuild)?; let authorized_tx = TransactionData { version: unauthed_tx.version, @@ -464,20 +815,24 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { 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, + }) } } -#[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; @@ -511,38 +866,59 @@ impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> ExtensionTxBuilder<'a #[cfg(any(test, feature = "test-dependencies"))] mod testing { use rand::RngCore; + use rand_core::CryptoRng; use std::convert::Infallible; - use super::{Builder, Error, SaplingMetadata}; + use super::{BuildResult, Builder, Error}; use crate::{ - consensus::{self, BlockHeight}, - sapling::prover::mock::MockTxProver, - transaction::{fees::fixed, Transaction}, + consensus, + sapling::{ + self, + prover::mock::{MockOutputProver, MockSpendProver}, + }, + transaction::fees::fixed, }; - 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) - } + impl<'a, P: consensus::Parameters, U: sapling::builder::ProverProgress> Builder<'a, P, U> { + /// Build the transaction using mocked randomness and proving capabilities. + /// DO NOT USE EXCEPT FOR UNIT TESTING. + pub fn mock_build(self, 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( + FakeCryptoRng(rng), + &MockSpendProver, + &MockOutputProver, + &fixed::FeeRule::standard(), + ) } } } #[cfg(test)] mod tests { + use std::convert::Infallible; + + use assert_matches::assert_matches; use ff::Field; use incrementalmerkletree::{frontier::CommitmentTree, witness::IncrementalWitness}; use rand_core::OsRng; @@ -551,60 +927,33 @@ mod tests { 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}, + sapling::{self, zip32::ExtendedSpendingKey, Node, Rseed}, + transaction::{ + builder::BuildConfig, + components::amount::{Amount, BalanceError, NonNegativeAmount}, }, - zip32::ExtendedSpendingKey, }; 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, - }, + transaction::{builder::DEFAULT_TX_EXPIRY_DELTA, OutPoint, TxOut}, 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::legacy::keys::NonHardenedChildIndex; use crate::transaction::builder::{self, TransparentBuilder}; let sapling_activation_height = TEST_NETWORK @@ -614,32 +963,39 @@ 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, 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"))] + #[cfg(not(zcash_unstable = "zfuture"))] tze_builder: std::marker::PhantomData, - progress_notifier: None, + progress_notifier: (), + orchard_builder: None, + sapling_asks: vec![], + orchard_saks: Vec::new(), }; - let tsk = AccountPrivKey::from_seed(&TEST_NETWORK, &[0u8; 32], AccountId::from(0)).unwrap(); + let tsk = AccountPrivKey::from_seed(&TEST_NETWORK, &[0u8; 32], AccountId::ZERO).unwrap(); let prev_coin = TxOut { - value: Amount::from_u64(50000).unwrap(), + value: NonNegativeAmount::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(), + tsk.derive_external_secret_key(NonHardenedChildIndex::ZERO) + .unwrap(), OutPoint::new([0u8; 32], 1), prev_coin, ) @@ -648,14 +1004,14 @@ mod tests { // 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]), + NonNegativeAmount::const_from_u64(40000), ) .unwrap(); - let (tx, _) = builder.mock_build().unwrap(); + let res = builder.mock_build(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,7 +1022,10 @@ 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(); @@ -675,41 +1034,28 @@ mod tests { 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::(&extsk, note1, witness1.path().unwrap()) .unwrap(); builder .add_transparent_output( - &TransparentAddress::PublicKey([0; 20]), - Amount::from_u64(40000).unwrap(), + &TransparentAddress::PublicKeyHash([0; 20]), + NonNegativeAmount::const_from_u64(40000), ) .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) - .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) - ); + // A binding signature (and bundle) is present because there is a Sapling spend. + let res = builder.mock_build(OsRng).unwrap(); + assert!(res.transaction().sapling_bundle().is_some()); } #[test] @@ -727,10 +1073,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(OsRng), + Err(Error::InsufficientFunds(expected)) if expected == MINIMUM_FEE.into() ); } @@ -741,42 +1091,51 @@ mod tests { // 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(), + NonNegativeAmount::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(OsRng), + Err(Error::InsufficientFunds(expected)) if + expected == (NonNegativeAmount::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]), + NonNegativeAmount::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(OsRng), + Err(Error::InsufficientFunds(expected)) if expected == + (NonNegativeAmount::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(); @@ -785,36 +1144,38 @@ mod tests { // 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 { - 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(), - note1.clone(), - witness1.path().unwrap(), - ) + .add_sapling_spend::(&extsk, note1.clone(), witness1.path().unwrap()) .unwrap(); builder - .add_sapling_output( + .add_sapling_output::( ovk, to, - Amount::from_u64(30000).unwrap(), + NonNegativeAmount::const_from_u64(30000), MemoBytes::empty(), ) .unwrap(); builder .add_transparent_output( - &TransparentAddress::PublicKey([0; 20]), - Amount::from_u64(20000).unwrap(), + &TransparentAddress::PublicKeyHash([0; 20]), + NonNegativeAmount::const_from_u64(20000), ) .unwrap(); - assert_eq!( - builder.mock_build(), - Err(Error::InsufficientFunds(Amount::from_i64(1).unwrap())) + assert_matches!( + builder.mock_build(OsRng), + Err(Error::InsufficientFunds(expected)) if expected == Amount::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(); @@ -822,40 +1183,36 @@ mod tests { // 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.) { - 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(), - note1, - witness1.path().unwrap(), - ) + .add_sapling_spend::(&extsk, note1, witness1.path().unwrap()) .unwrap(); builder - .add_sapling_spend(extsk, *to.diversifier(), note2, witness2.path().unwrap()) + .add_sapling_spend::(&extsk, note2, witness2.path().unwrap()) .unwrap(); builder - .add_sapling_output( + .add_sapling_output::( ovk, to, - Amount::from_u64(30000).unwrap(), + NonNegativeAmount::const_from_u64(30000), MemoBytes::empty(), ) .unwrap(); builder .add_transparent_output( - &TransparentAddress::PublicKey([0; 20]), - Amount::from_u64(20000).unwrap(), + &TransparentAddress::PublicKeyHash([0; 20]), + NonNegativeAmount::const_from_u64(20000), ) .unwrap(); - assert_eq!( - builder.mock_build(), - Err(Error::SaplingBuild(sapling_builder::Error::BindingSig)) - ) + assert_matches!( + builder.mock_build(OsRng), + Ok(res) if res.transaction().fee_paid(|_| Err(BalanceError::Overflow)).unwrap() == Amount::const_from_i64(10_000) + ); } } } diff --git a/zcash_primitives/src/transaction/components.rs b/zcash_primitives/src/transaction/components.rs index 759d85c73b..f239b0ba01 100644 --- a/zcash_primitives/src/transaction/components.rs +++ b/zcash_primitives/src/transaction/components.rs @@ -1,19 +1,32 @@ //! Structs representing the components within Zcash transactions. +pub mod amount { + pub use zcash_protocol::value::{ + BalanceError, ZatBalance as Amount, Zatoshis as NonNegativeAmount, COIN, + }; -pub mod amount; + #[cfg(feature = "test-dependencies")] + pub mod testing { + pub use zcash_protocol::value::testing::{ + arb_positive_zat_balance as arb_positive_amount, arb_zat_balance as arb_amount, + arb_zatoshis as arb_nonnegative_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}, }; +pub use crate::sapling::bundle::{OutputDescription, SpendDescription}; -#[cfg(feature = "zfuture")] +#[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 3360ec9c49..180466323c 100644 --- a/zcash_primitives/src/transaction/components/orchard.rs +++ b/zcash_primitives/src/transaction/components/orchard.rs @@ -20,14 +20,6 @@ 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; diff --git a/zcash_primitives/src/transaction/components/sapling.rs b/zcash_primitives/src/transaction/components/sapling.rs index 8d52557990..62e493865d 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 ff::PrimeField; -use memuse::DynamicUsage; +use redjubjub::SpendAuth; +use sapling::note_encryption::Zip212Enforcement; +use zcash_protocol::consensus::{BlockHeight, NetworkUpgrade, Parameters, ZIP212_GRACE_PERIOD}; use std::io::{self, Read, Write}; -use zcash_note_encryption::{EphemeralKeyBytes, ShieldedOutput}; +use zcash_encoding::{Array, CompactSize, Vector}; +use zcash_note_encryption::{EphemeralKeyBytes, ENC_CIPHERTEXT_SIZE, OUT_CIPHERTEXT_SIZE}; use crate::{ - consensus, sapling::{ - note::ExtractedNoteCommitment, - note_encryption::{ - CompactNoteCiphertextBytes, NoteCiphertextBytes, SaplingDomain, COMPACT_NOTE_SIZE, + bundle::{ + Authorization, Authorized, Bundle, GrothProofBytes, OutputDescription, + OutputDescriptionV5, SpendDescription, SpendDescriptionV5, }, - redjubjub::{self, PublicKey, Signature}, + note::ExtractedNoteCommitment, value::ValueCommitment, Nullifier, }, + transaction::Transaction, }; -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; - -impl Authorization for Unproven { - type SpendProof = (); - type OutputProof = (); - type AuthSig = (); -} +use super::{Amount, GROTH_PROOF_SIZE}; -/// 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, -} +/// 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; -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,10 +110,10 @@ 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), @@ -340,508 +133,399 @@ 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, +fn read_nullifier(mut reader: R) -> io::Result { + let mut nullifier = Nullifier([0u8; 32]); + reader.read_exact(&mut nullifier.0)?; + Ok(nullifier) } -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, - } - } -} - -#[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(|e| io::Error::new(io::ErrorKind::InvalidData, e)) } -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<( + Amount, + 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<( + Amount, + 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((Amount::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(Amount::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> 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 { + Amount::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) -> Option { - Some(NoteCiphertextBytes(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))?; - fn enc_ciphertext_compact(&self) -> CompactNoteCiphertextBytes { - CompactNoteCiphertextBytes( - self.enc_ciphertext[0..COMPACT_NOTE_SIZE] - .try_into() - .unwrap(), - ) - } -} + 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 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 shielded_spends = sd_v5s + .into_iter() + .zip( + v_spend_proofs + .into_iter() + .zip(v_spend_auth_sigs.into_iter()), ) - } -} - -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, + .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.into_iter()) + .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> for CompactOutputDescription { - fn ephemeral_key(&self) -> EphemeralKeyBytes { - self.ephemeral_key.clone() - } - fn cmstar_bytes(&self) -> [u8; 32] { - self.cmu.to_bytes() - } - - fn enc_ciphertext(&self) -> Option { - None + 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_compact(&self) -> CompactNoteCiphertextBytes { - CompactNoteCiphertextBytes(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, - }, + sapling::bundle::{testing as t_sap, Authorized, Bundle}, transaction::{ - components::{amount::testing::arb_amount, GROTH_PROOF_SIZE}, + components::{amount::testing::arb_amount, Amount}, 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, - } - } - } - prop_compose! { - pub fn arb_bundle()( - n_spends in 0usize..30, - n_outputs in 0usize..30, + fn arb_bundle()( + value_balance in arb_amount() )( - 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 fdc5b6a9d0..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: enc_ciphertext.0, - 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

{ - 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/transparent.rs b/zcash_primitives/src/transaction/components/transparent.rs index 988174e2f9..b7049fb3e9 100644 --- a/zcash_primitives/src/transaction/components/transparent.rs +++ b/zcash_primitives/src/transaction/components/transparent.rs @@ -7,10 +7,9 @@ use std::io::{self, Read, Write}; use crate::legacy::{Script, TransparentAddress}; -use super::amount::{Amount, BalanceError}; +use super::amount::{Amount, BalanceError, NonNegativeAmount}; pub mod builder; -pub mod fees; pub trait Authorization: Debug { type ScriptSig: Debug + Clone + PartialEq; @@ -82,7 +81,7 @@ impl Bundle { let output_sum = self .vout .iter() - .map(|p| p.value) + .map(|p| Amount::from(p.value)) .sum::>() .ok_or(BalanceError::Overflow)?; @@ -159,7 +158,7 @@ impl TxIn { #[derive(Clone, Debug, PartialEq, Eq)] pub struct TxOut { - pub value: Amount, + pub value: NonNegativeAmount, pub script_pubkey: Script, } @@ -168,7 +167,7 @@ impl TxOut { let value = { let mut tmp = [0u8; 8]; reader.read_exact(&mut tmp)?; - Amount::from_nonnegative_i64_le_bytes(tmp) + NonNegativeAmount::from_nonnegative_i64_le_bytes(tmp) } .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "value out of range"))?; let script_pubkey = Script::read(&mut reader)?; diff --git a/zcash_primitives/src/transaction/components/transparent/builder.rs b/zcash_primitives/src/transaction/components/transparent/builder.rs index a65645fc93..c275065fe1 100644 --- a/zcash_primitives/src/transaction/components/transparent/builder.rs +++ b/zcash_primitives/src/transaction/components/transparent/builder.rs @@ -6,11 +6,10 @@ use crate::{ legacy::{Script, TransparentAddress}, transaction::{ components::{ - amount::{Amount, BalanceError}, - transparent::{self, fees, Authorization, Authorized, Bundle, TxIn, TxOut}, + amount::{Amount, BalanceError, NonNegativeAmount}, + transparent::{self, Authorization, Authorized, Bundle, TxIn, TxOut}, }, sighash::TransparentAuthorizingContext, - OutPoint, }, }; @@ -18,6 +17,7 @@ use crate::{ use { crate::transaction::{ self as tx, + components::transparent::OutPoint, sighash::{signature_hash, SignableInput, SIGHASH_ALL}, TransactionData, TxDigests, }, @@ -40,24 +40,9 @@ impl fmt::Display for Error { } } -/// 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 { +pub struct TransparentInputInfo { sk: secp256k1::SecretKey, pubkey: [u8; secp256k1::constants::PUBLIC_KEY_SIZE], utxo: OutPoint, @@ -65,12 +50,12 @@ struct TransparentInputInfo { } #[cfg(feature = "transparent-inputs")] -impl fees::InputView for TransparentInputInfo { - fn outpoint(&self) -> &OutPoint { +impl TransparentInputInfo { + pub fn outpoint(&self) -> &OutPoint { &self.utxo } - fn coin(&self) -> &TxOut { + pub fn coin(&self) -> &TxOut { &self.coin } } @@ -109,19 +94,13 @@ impl TransparentBuilder { /// 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 - } + #[cfg(feature = "transparent-inputs")] + pub fn inputs(&self) -> &[TransparentInputInfo] { + &self.inputs } /// Returns the transparent outputs that will be produced by the transaction being constructed. - pub fn outputs(&self) -> &[impl fees::OutputView] { + pub fn outputs(&self) -> &[TxOut] { &self.vout } @@ -133,16 +112,12 @@ impl TransparentBuilder { 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)) => { + Some(TransparentAddress::PublicKeyHash(hash)) => { use ripemd::Ripemd160; use sha2::Sha256; @@ -163,11 +138,11 @@ impl TransparentBuilder { Ok(()) } - pub fn add_output(&mut self, to: &TransparentAddress, value: Amount) -> Result<(), Error> { - if value.is_negative() { - return Err(Error::InvalidAmount); - } - + pub fn add_output( + &mut self, + to: &TransparentAddress, + value: NonNegativeAmount, + ) -> Result<(), Error> { self.vout.push(TxOut { value, script_pubkey: to.script(), @@ -182,20 +157,20 @@ impl TransparentBuilder { .inputs .iter() .map(|input| input.coin.value) - .sum::>() + .sum::>() .ok_or(BalanceError::Overflow)?; #[cfg(not(feature = "transparent-inputs"))] - let input_sum = Amount::zero(); + let input_sum = NonNegativeAmount::ZERO; let output_sum = self .vout .iter() .map(|vo| vo.value) - .sum::>() + .sum::>() .ok_or(BalanceError::Overflow)?; - (input_sum - output_sum).ok_or(BalanceError::Underflow) + (Amount::from(input_sum) - Amount::from(output_sum)).ok_or(BalanceError::Underflow) } pub fn build(self) -> Option> { @@ -228,7 +203,6 @@ impl TransparentBuilder { impl TxIn { #[cfg(feature = "transparent-inputs")] - #[cfg_attr(docsrs, doc(cfg(feature = "transparent-inputs")))] pub fn new(prevout: OutPoint) -> Self { TxIn { prevout, @@ -240,7 +214,7 @@ impl TxIn { #[cfg(not(feature = "transparent-inputs"))] impl TransparentAuthorizingContext for Unauthorized { - fn input_amounts(&self) -> Vec { + fn input_amounts(&self) -> Vec { vec![] } @@ -251,7 +225,7 @@ impl TransparentAuthorizingContext for Unauthorized { #[cfg(feature = "transparent-inputs")] impl TransparentAuthorizingContext for Unauthorized { - fn input_amounts(&self) -> Vec { + fn input_amounts(&self) -> Vec { return self.inputs.iter().map(|txin| txin.coin.value).collect(); } diff --git a/zcash_primitives/src/transaction/components/tze.rs b/zcash_primitives/src/transaction/components/tze.rs index e766d2f9f5..26e849cbc5 100644 --- a/zcash_primitives/src/transaction/components/tze.rs +++ b/zcash_primitives/src/transaction/components/tze.rs @@ -1,5 +1,4 @@ //! Structs representing the TZE components within Zcash transactions. -#![cfg(feature = "zfuture")] use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use std::convert::TryFrom; @@ -12,7 +11,6 @@ use super::amount::Amount; use crate::{extensions::transparent as tze, transaction::TxId}; pub mod builder; -pub mod fees; fn to_io_error(_: std::num::TryFromIntError) -> io::Error { io::Error::new(io::ErrorKind::InvalidData, "value out of range") @@ -253,7 +251,7 @@ pub mod testing { prop_compose! { pub fn arb_tzeout()(value in arb_nonnegative_amount(), precondition in arb_precondition()) -> TzeOut { - TzeOut { value, precondition } + TzeOut { value: value.into(), precondition } } } diff --git a/zcash_primitives/src/transaction/components/tze/builder.rs b/zcash_primitives/src/transaction/components/tze/builder.rs index 82bd1f2e0d..974dac9ce8 100644 --- a/zcash_primitives/src/transaction/components/tze/builder.rs +++ b/zcash_primitives/src/transaction/components/tze/builder.rs @@ -1,5 +1,4 @@ //! Types and functions for building TZE transaction components -#![cfg(feature = "zfuture")] use std::fmt; @@ -9,7 +8,7 @@ use crate::{ self as tx, components::{ amount::{Amount, BalanceError}, - tze::{fees, Authorization, Authorized, Bundle, OutPoint, TzeIn, TzeOut}, + tze::{Authorization, Authorized, Bundle, OutPoint, TzeIn, TzeOut}, }, }, }; @@ -36,16 +35,16 @@ pub struct TzeSigner<'a, BuildCtx> { } #[derive(Clone)] -struct TzeBuildInput { +pub struct TzeBuildInput { tzein: TzeIn<()>, coin: TzeOut, } -impl fees::InputView for TzeBuildInput { - fn outpoint(&self) -> &OutPoint { +impl TzeBuildInput { + pub fn outpoint(&self) -> &OutPoint { &self.tzein.prevout } - fn coin(&self) -> &TzeOut { + pub fn coin(&self) -> &TzeOut { &self.coin } } @@ -72,11 +71,11 @@ impl<'a, BuildCtx> TzeBuilder<'a, BuildCtx> { } } - pub fn inputs(&self) -> &[impl fees::InputView] { + pub fn inputs(&self) -> &[TzeBuildInput] { &self.vin } - pub fn outputs(&self) -> &[impl fees::OutputView] { + pub fn outputs(&self) -> &[TzeOut] { &self.vout } diff --git a/zcash_primitives/src/transaction/fees.rs b/zcash_primitives/src/transaction/fees.rs index e29913acc1..0dc82acf2b 100644 --- a/zcash_primitives/src/transaction/fees.rs +++ b/zcash_primitives/src/transaction/fees.rs @@ -2,15 +2,16 @@ use crate::{ consensus::{self, BlockHeight}, - transaction::components::{amount::Amount, transparent::fees as transparent}, + transaction::components::amount::NonNegativeAmount, }; -#[cfg(feature = "zfuture")] -use crate::transaction::components::tze::fees as tze; - pub mod fixed; +pub mod transparent; pub mod zip317; +#[cfg(zcash_unstable = "zfuture")] +pub mod tze; + /// A trait that represents the ability to compute the fees that must be paid /// by a transaction having a specified set of inputs and outputs. pub trait FeeRule { @@ -21,6 +22,7 @@ pub trait FeeRule { /// Implementations of this method should compute the fee amount given exactly the inputs and /// outputs specified, and should NOT compute speculative fees given any additional change /// outputs that may need to be created in order for inputs and outputs to balance. + #[allow(clippy::too_many_arguments)] fn fee_required( &self, params: &P, @@ -29,12 +31,13 @@ pub trait FeeRule { transparent_outputs: &[impl transparent::OutputView], sapling_input_count: usize, sapling_output_count: usize, - ) -> Result; + orchard_action_count: usize, + ) -> Result; } /// A trait that represents the ability to compute the fees that must be paid by a transaction /// having a specified set of inputs and outputs, for use when experimenting with the TZE feature. -#[cfg(feature = "zfuture")] +#[cfg(zcash_unstable = "zfuture")] pub trait FutureFeeRule: FeeRule { /// Computes the total fee required for a transaction given the provided inputs and outputs. /// @@ -50,7 +53,52 @@ pub trait FutureFeeRule: FeeRule { transparent_outputs: &[impl transparent::OutputView], sapling_input_count: usize, sapling_output_count: usize, + orchard_action_count: usize, tze_inputs: &[impl tze::InputView], tze_outputs: &[impl tze::OutputView], - ) -> Result; + ) -> Result; +} + +/// An enumeration of the standard fee rules supported by the wallet. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum StandardFeeRule { + #[deprecated( + note = "Using this fee rule violates ZIP 317, and might cause transactions built with it to fail. Use `StandardFeeRule::Zip317` instead." + )] + PreZip313, + #[deprecated( + note = "Using this fee rule violates ZIP 317, and might cause transactions built with it to fail. Use `StandardFeeRule::Zip317` instead." + )] + Zip313, + Zip317, +} + +impl FeeRule for StandardFeeRule { + type Error = zip317::FeeError; + + fn fee_required( + &self, + params: &P, + target_height: BlockHeight, + transparent_inputs: &[impl transparent::InputView], + transparent_outputs: &[impl transparent::OutputView], + sapling_input_count: usize, + sapling_output_count: usize, + orchard_action_count: usize, + ) -> Result { + #[allow(deprecated)] + match self { + Self::PreZip313 => Ok(zip317::MINIMUM_FEE), + Self::Zip313 => Ok(NonNegativeAmount::const_from_u64(1000)), + Self::Zip317 => zip317::FeeRule::standard().fee_required( + params, + target_height, + transparent_inputs, + transparent_outputs, + sapling_input_count, + sapling_output_count, + orchard_action_count, + ), + } + } } diff --git a/zcash_primitives/src/transaction/fees/fixed.rs b/zcash_primitives/src/transaction/fees/fixed.rs index c3028dcace..1bb1b38527 100644 --- a/zcash_primitives/src/transaction/fees/fixed.rs +++ b/zcash_primitives/src/transaction/fees/fixed.rs @@ -1,22 +1,22 @@ use crate::{ consensus::{self, BlockHeight}, - transaction::components::{amount::Amount, transparent::fees as transparent}, - transaction::fees::zip317, + transaction::components::amount::NonNegativeAmount, + transaction::fees::{transparent, zip317}, }; -#[cfg(feature = "zfuture")] -use crate::transaction::components::tze::fees as tze; +#[cfg(zcash_unstable = "zfuture")] +use crate::transaction::fees::tze; /// A fee rule that always returns a fixed fee, irrespective of the structure of /// the transaction being constructed. #[derive(Clone, Copy, Debug)] pub struct FeeRule { - fixed_fee: Amount, + fixed_fee: NonNegativeAmount, } impl FeeRule { /// Creates a new nonstandard fixed fee rule with the specified fixed fee. - pub fn non_standard(fixed_fee: Amount) -> Self { + pub fn non_standard(fixed_fee: NonNegativeAmount) -> Self { Self { fixed_fee } } @@ -40,7 +40,7 @@ impl FeeRule { } /// Returns the fixed fee amount which which this rule was configured. - pub fn fixed_fee(&self) -> Amount { + pub fn fixed_fee(&self) -> NonNegativeAmount { self.fixed_fee } } @@ -56,12 +56,13 @@ impl super::FeeRule for FeeRule { _transparent_outputs: &[impl transparent::OutputView], _sapling_input_count: usize, _sapling_output_count: usize, - ) -> Result { + _orchard_action_count: usize, + ) -> Result { Ok(self.fixed_fee) } } -#[cfg(feature = "zfuture")] +#[cfg(zcash_unstable = "zfuture")] impl super::FutureFeeRule for FeeRule { fn fee_required_zfuture( &self, @@ -71,9 +72,10 @@ impl super::FutureFeeRule for FeeRule { _transparent_outputs: &[impl transparent::OutputView], _sapling_input_count: usize, _sapling_output_count: usize, + _orchard_action_count: usize, _tze_inputs: &[impl tze::InputView], _tze_outputs: &[impl tze::OutputView], - ) -> Result { + ) -> Result { Ok(self.fixed_fee) } } diff --git a/zcash_primitives/src/transaction/components/transparent/fees.rs b/zcash_primitives/src/transaction/fees/transparent.rs similarity index 52% rename from zcash_primitives/src/transaction/components/transparent/fees.rs rename to zcash_primitives/src/transaction/fees/transparent.rs index 1e63e16c33..e5c5916040 100644 --- a/zcash_primitives/src/transaction/components/transparent/fees.rs +++ b/zcash_primitives/src/transaction/fees/transparent.rs @@ -1,32 +1,56 @@ //! Types related to computation of fees and change related to the transparent components //! of a transaction. -use super::TxOut; +use std::convert::Infallible; + use crate::{ legacy::Script, - transaction::{components::amount::Amount, OutPoint}, + transaction::components::{amount::NonNegativeAmount, transparent::TxOut, OutPoint}, }; +#[cfg(feature = "transparent-inputs")] +use crate::transaction::components::transparent::builder::TransparentInputInfo; + /// This trait provides a minimized view of a transparent input suitable for use in /// fee and change computation. -pub trait InputView { +pub trait InputView: std::fmt::Debug { /// The outpoint to which the input refers. fn outpoint(&self) -> &OutPoint; /// The previous output being spent. fn coin(&self) -> &TxOut; } +#[cfg(feature = "transparent-inputs")] +impl InputView for TransparentInputInfo { + fn outpoint(&self) -> &OutPoint { + self.outpoint() + } + + fn coin(&self) -> &TxOut { + self.coin() + } +} + +impl InputView for Infallible { + fn outpoint(&self) -> &OutPoint { + unreachable!() + } + fn coin(&self) -> &TxOut { + unreachable!() + } +} + /// This trait provides a minimized view of a transparent output suitable for use in /// fee and change computation. -pub trait OutputView { +pub trait OutputView: std::fmt::Debug { /// Returns the value of the output being created. - fn value(&self) -> Amount; + fn value(&self) -> NonNegativeAmount; /// Returns the script corresponding to the newly created output. fn script_pubkey(&self) -> &Script; } impl OutputView for TxOut { - fn value(&self) -> Amount { + fn value(&self) -> NonNegativeAmount { self.value } diff --git a/zcash_primitives/src/transaction/components/tze/fees.rs b/zcash_primitives/src/transaction/fees/tze.rs similarity index 78% rename from zcash_primitives/src/transaction/components/tze/fees.rs rename to zcash_primitives/src/transaction/fees/tze.rs index 1a72d6cc32..1a603f2065 100644 --- a/zcash_primitives/src/transaction/components/tze/fees.rs +++ b/zcash_primitives/src/transaction/fees/tze.rs @@ -1,10 +1,10 @@ //! Abstractions and types related to fee calculations for TZE components of a transaction. use crate::{ - extensions::transparent::{self as tze}, + extensions::transparent as tze, transaction::components::{ amount::Amount, - tze::{OutPoint, TzeOut}, + tze::{builder::TzeBuildInput, OutPoint, TzeOut}, }, }; @@ -17,6 +17,15 @@ pub trait InputView { fn coin(&self) -> &TzeOut; } +impl InputView for TzeBuildInput { + fn outpoint(&self) -> &OutPoint { + self.outpoint() + } + fn coin(&self) -> &TzeOut { + self.coin() + } +} + /// This trait provides a minimized view of a TZE output suitable for use in /// fee computation. pub trait OutputView { diff --git a/zcash_primitives/src/transaction/fees/zip317.rs b/zcash_primitives/src/transaction/fees/zip317.rs index 60fcd767f9..ee25c8fa38 100644 --- a/zcash_primitives/src/transaction/fees/zip317.rs +++ b/zcash_primitives/src/transaction/fees/zip317.rs @@ -7,30 +7,52 @@ use core::cmp::max; use crate::{ consensus::{self, BlockHeight}, legacy::TransparentAddress, - transaction::components::{ - amount::{Amount, BalanceError}, - transparent::{fees as transparent, OutPoint}, + transaction::{ + components::{ + amount::{BalanceError, NonNegativeAmount}, + transparent::OutPoint, + }, + fees::transparent, }, }; -/// The minimum conventional fee using the standard [ZIP 317] constants. Equivalent to -/// `(FeeRule::standard().marginal_fee() * FeeRule::standard().grace_actions()).unwrap()`, -/// but as a constant. +/// The standard [ZIP 317] marginal fee. /// /// [ZIP 317]: https//zips.z.cash/zip-0317 -pub const MINIMUM_FEE: Amount = Amount::const_from_i64(10_000); +pub const MARGINAL_FEE: NonNegativeAmount = NonNegativeAmount::const_from_u64(5_000); + +/// The minimum number of logical actions that must be paid according to [ZIP 317]. +/// +/// [ZIP 317]: https//zips.z.cash/zip-0317 +pub const GRACE_ACTIONS: usize = 2; + +/// The standard size of a P2PKH input, in bytes, according to [ZIP 317]. +/// +/// [ZIP 317]: https//zips.z.cash/zip-0317 +pub const P2PKH_STANDARD_INPUT_SIZE: usize = 150; + +/// The standard size of a P2PKH output, in bytes, according to [ZIP 317]. +/// +/// [ZIP 317]: https//zips.z.cash/zip-0317 +pub const P2PKH_STANDARD_OUTPUT_SIZE: usize = 34; + +/// The minimum conventional fee computed from the standard [ZIP 317] constants. Equivalent to +/// `MARGINAL_FEE * GRACE_ACTIONS`. +/// +/// [ZIP 317]: https//zips.z.cash/zip-0317 +pub const MINIMUM_FEE: NonNegativeAmount = NonNegativeAmount::const_from_u64(10_000); /// A [`FeeRule`] implementation that implements the [ZIP 317] fee rule. /// -/// This fee rule supports only P2pkh transparent inputs; an error will be returned if a coin -/// containing a non-p2pkh script is provided as an input. This fee rule may slightly overestimate -/// fees in case where the user is attempting to spend more than ~150 transparent inputs. +/// This fee rule supports Orchard, Sapling, and (P2PKH only) transparent inputs. +/// Returns an error if a coin containing a non-p2pkh script is provided as an input. +/// This fee rule may slightly overestimate fees in case where the user is attempting to spend more than ~150 transparent inputs. /// /// [`FeeRule`]: crate::transaction::fees::FeeRule /// [ZIP 317]: https//zips.z.cash/zip-0317 #[derive(Clone, Debug)] pub struct FeeRule { - marginal_fee: Amount, + marginal_fee: NonNegativeAmount, grace_actions: usize, p2pkh_standard_input_size: usize, p2pkh_standard_output_size: usize, @@ -42,10 +64,10 @@ impl FeeRule { /// [ZIP 317]: https//zips.z.cash/zip-0317 pub fn standard() -> Self { Self { - marginal_fee: Amount::from_u64(5000).unwrap(), - grace_actions: 2, - p2pkh_standard_input_size: 150, - p2pkh_standard_output_size: 34, + marginal_fee: MARGINAL_FEE, + grace_actions: GRACE_ACTIONS, + p2pkh_standard_input_size: P2PKH_STANDARD_INPUT_SIZE, + p2pkh_standard_output_size: P2PKH_STANDARD_OUTPUT_SIZE, } } @@ -54,7 +76,7 @@ impl FeeRule { /// Returns `None` if either `p2pkh_standard_input_size` or `p2pkh_standard_output_size` are /// zero. pub fn non_standard( - marginal_fee: Amount, + marginal_fee: NonNegativeAmount, grace_actions: usize, p2pkh_standard_input_size: usize, p2pkh_standard_output_size: usize, @@ -72,7 +94,7 @@ impl FeeRule { } /// Returns the ZIP 317 marginal fee. - pub fn marginal_fee(&self) -> Amount { + pub fn marginal_fee(&self) -> NonNegativeAmount { self.marginal_fee } /// Returns the ZIP 317 number of grace actions @@ -129,11 +151,12 @@ impl super::FeeRule for FeeRule { transparent_outputs: &[impl transparent::OutputView], sapling_input_count: usize, sapling_output_count: usize, - ) -> Result { + orchard_action_count: usize, + ) -> Result { let non_p2pkh_inputs: Vec<_> = transparent_inputs .iter() .filter_map(|t_in| match t_in.coin().script_pubkey.address() { - Some(TransparentAddress::PublicKey(_)) => None, + Some(TransparentAddress::PublicKeyHash(_)) => None, _ => Some(t_in.outpoint()), }) .cloned() @@ -151,7 +174,8 @@ impl super::FeeRule for FeeRule { let logical_actions = max( ceildiv(t_in_total_size, self.p2pkh_standard_input_size), ceildiv(t_out_total_size, self.p2pkh_standard_output_size), - ) + max(sapling_input_count, sapling_output_count); + ) + max(sapling_input_count, sapling_output_count) + + orchard_action_count; (self.marginal_fee * max(self.grace_actions, logical_actions)) .ok_or_else(|| BalanceError::Overflow.into()) diff --git a/zcash_primitives/src/transaction/mod.rs b/zcash_primitives/src/transaction/mod.rs index a500b54bb7..b7e9943295 100644 --- a/zcash_primitives/src/transaction/mod.rs +++ b/zcash_primitives/src/transaction/mod.rs @@ -13,27 +13,23 @@ mod tests; use blake2b_simd::Hash as Blake2bHash; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; -use ff::PrimeField; use memuse::DynamicUsage; use std::convert::TryFrom; use std::fmt; use std::fmt::Debug; use std::io::{self, Read, Write}; use std::ops::Deref; -use zcash_encoding::{Array, CompactSize, Vector}; +use zcash_encoding::{CompactSize, Vector}; use crate::{ consensus::{BlockHeight, BranchId}, - sapling::redjubjub, + sapling::{self, builder as sapling_builder}, }; use self::{ components::{ amount::{Amount, BalanceError}, - orchard as orchard_serialization, - sapling::{ - self, OutputDescription, OutputDescriptionV5, SpendDescription, SpendDescriptionV5, - }, + orchard as orchard_serialization, sapling as sapling_serialization, sprout::{self, JsDescription}, transparent::{self, TxIn, TxOut}, OutPoint, @@ -42,7 +38,7 @@ use self::{ util::sha256d::{HashReader, HashWriter}, }; -#[cfg(feature = "zfuture")] +#[cfg(zcash_unstable = "zfuture")] use self::components::tze::{self, TzeIn, TzeOut}; const OVERWINTER_VERSION_GROUP_ID: u32 = 0x03C48270; @@ -59,11 +55,18 @@ const V5_VERSION_GROUP_ID: u32 = 0x26A7270A; /// using these constants should be inspected, and use of these constants /// should be removed as appropriate in favor of the new consensus /// transaction version and group. -#[cfg(feature = "zfuture")] +#[cfg(zcash_unstable = "zfuture")] const ZFUTURE_VERSION_GROUP_ID: u32 = 0xFFFFFFFF; -#[cfg(feature = "zfuture")] +#[cfg(zcash_unstable = "zfuture")] const ZFUTURE_TX_VERSION: u32 = 0x0000FFFF; +/// 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]); @@ -92,6 +95,12 @@ impl AsRef<[u8; 32]> for TxId { } } +impl From for [u8; 32] { + fn from(value: TxId) -> Self { + value.0 + } +} + impl TxId { pub fn from_bytes(bytes: [u8; 32]) -> Self { TxId(bytes) @@ -122,7 +131,7 @@ pub enum TxVersion { Overwinter, Sapling, Zip225, - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] ZFuture, } @@ -137,7 +146,7 @@ impl TxVersion { (OVERWINTER_TX_VERSION, OVERWINTER_VERSION_GROUP_ID) => Ok(TxVersion::Overwinter), (SAPLING_TX_VERSION, SAPLING_VERSION_GROUP_ID) => Ok(TxVersion::Sapling), (V5_TX_VERSION, V5_VERSION_GROUP_ID) => Ok(TxVersion::Zip225), - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] (ZFUTURE_TX_VERSION, ZFUTURE_VERSION_GROUP_ID) => Ok(TxVersion::ZFuture), _ => Err(io::Error::new( io::ErrorKind::InvalidInput, @@ -167,7 +176,7 @@ impl TxVersion { TxVersion::Overwinter => OVERWINTER_TX_VERSION, TxVersion::Sapling => SAPLING_TX_VERSION, TxVersion::Zip225 => V5_TX_VERSION, - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] TxVersion::ZFuture => ZFUTURE_TX_VERSION, } } @@ -178,7 +187,7 @@ impl TxVersion { TxVersion::Overwinter => OVERWINTER_VERSION_GROUP_ID, TxVersion::Sapling => SAPLING_VERSION_GROUP_ID, TxVersion::Zip225 => V5_VERSION_GROUP_ID, - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] TxVersion::ZFuture => ZFUTURE_VERSION_GROUP_ID, } } @@ -191,12 +200,13 @@ impl TxVersion { } } + /// Returns `true` if this transaction version supports the Sprout protocol. pub fn has_sprout(&self) -> bool { match self { TxVersion::Sprout(v) => *v >= 2u32, TxVersion::Overwinter | TxVersion::Sapling => true, TxVersion::Zip225 => false, - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] TxVersion::ZFuture => true, } } @@ -205,30 +215,33 @@ impl TxVersion { !matches!(self, TxVersion::Sprout(_)) } + /// Returns `true` if this transaction version supports the Sapling protocol. pub fn has_sapling(&self) -> bool { match self { TxVersion::Sprout(_) | TxVersion::Overwinter => false, TxVersion::Sapling => true, TxVersion::Zip225 => true, - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] TxVersion::ZFuture => true, } } + /// Returns `true` if this transaction version supports the Orchard protocol. pub fn has_orchard(&self) -> bool { match self { TxVersion::Sprout(_) | TxVersion::Overwinter | TxVersion::Sapling => false, TxVersion::Zip225 => true, - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] TxVersion::ZFuture => true, } } - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] pub fn has_tze(&self) -> bool { matches!(self, TxVersion::ZFuture) } + /// Suggests the transaction version that should be used in the given Zcash epoch. pub fn suggested_for_branch(consensus_branch_id: BranchId) -> Self { match consensus_branch_id { BranchId::Sprout => TxVersion::Sprout(2), @@ -237,7 +250,9 @@ impl TxVersion { TxVersion::Sapling } BranchId::Nu5 => TxVersion::Zip225, - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "nu6")] + BranchId::Nu6 => TxVersion::Zip225, + #[cfg(zcash_unstable = "zfuture")] BranchId::ZFuture => TxVersion::ZFuture, } } @@ -246,33 +261,40 @@ impl TxVersion { /// Authorization state for a bundle of transaction data. pub trait Authorization { type TransparentAuth: transparent::Authorization; - type SaplingAuth: sapling::Authorization; + type SaplingAuth: sapling::bundle::Authorization; type OrchardAuth: orchard::bundle::Authorization; - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] type TzeAuth: tze::Authorization; } +/// [`Authorization`] marker type for fully-authorized transactions. #[derive(Debug)] pub struct Authorized; impl Authorization for Authorized { type TransparentAuth = transparent::Authorized; - type SaplingAuth = sapling::Authorized; + type SaplingAuth = sapling::bundle::Authorized; type OrchardAuth = orchard::bundle::Authorized; - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] type TzeAuth = tze::Authorized; } +/// [`Authorization`] marker type for transactions without authorization data. +/// +/// Currently this includes Sapling proofs because the types in this crate support v4 +/// transactions, which commit to the Sapling proofs in the transaction digest. pub struct Unauthorized; impl Authorization for Unauthorized { type TransparentAuth = transparent::builder::Unauthorized; - type SaplingAuth = sapling::builder::Unauthorized; - type OrchardAuth = orchard_serialization::Unauthorized; + type SaplingAuth = + sapling_builder::InProgress; + type OrchardAuth = + orchard::builder::InProgress; - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] type TzeAuth = tze::builder::Unauthorized; } @@ -297,6 +319,7 @@ impl PartialEq for Transaction { } } +/// The information contained in a Zcash transaction. #[derive(Debug)] pub struct TransactionData { version: TxVersion, @@ -305,13 +328,14 @@ pub struct TransactionData { expiry_height: BlockHeight, transparent_bundle: Option>, sprout_bundle: Option, - sapling_bundle: Option>, + sapling_bundle: Option>, orchard_bundle: Option>, - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] tze_bundle: Option>, } impl TransactionData { + /// Constructs a `TransactionData` from its constituent parts. #[allow(clippy::too_many_arguments)] pub fn from_parts( version: TxVersion, @@ -320,7 +344,7 @@ impl TransactionData { expiry_height: BlockHeight, transparent_bundle: Option>, sprout_bundle: Option, - sapling_bundle: Option>, + sapling_bundle: Option>, orchard_bundle: Option>, ) -> Self { TransactionData { @@ -332,12 +356,14 @@ impl TransactionData { sprout_bundle, sapling_bundle, orchard_bundle, - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] tze_bundle: None, } } - #[cfg(feature = "zfuture")] + /// Constructs a `TransactionData` from its constituent parts, including speculative + /// future parts that are not in the current Zcash consensus rules. + #[cfg(zcash_unstable = "zfuture")] #[allow(clippy::too_many_arguments)] pub fn from_parts_zfuture( version: TxVersion, @@ -346,7 +372,7 @@ impl TransactionData { expiry_height: BlockHeight, transparent_bundle: Option>, sprout_bundle: Option, - sapling_bundle: Option>, + sapling_bundle: Option>, orchard_bundle: Option>, tze_bundle: Option>, ) -> Self { @@ -363,10 +389,12 @@ impl TransactionData { } } + /// Returns the transaction version. pub fn version(&self) -> TxVersion { self.version } + /// Returns the Zcash epoch that this transaction can be mined in. pub fn consensus_branch_id(&self) -> BranchId { self.consensus_branch_id } @@ -387,7 +415,7 @@ impl TransactionData { self.sprout_bundle.as_ref() } - pub fn sapling_bundle(&self) -> Option<&sapling::Bundle> { + pub fn sapling_bundle(&self) -> Option<&sapling::Bundle> { self.sapling_bundle.as_ref() } @@ -395,7 +423,7 @@ impl TransactionData { self.orchard_bundle.as_ref() } - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] pub fn tze_bundle(&self) -> Option<&tze::Bundle> { self.tze_bundle.as_ref() } @@ -441,7 +469,7 @@ impl TransactionData { digester.digest_transparent(self.transparent_bundle.as_ref()), digester.digest_sapling(self.sapling_bundle.as_ref()), digester.digest_orchard(self.orchard_bundle.as_ref()), - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] digester.digest_tze(self.tze_bundle.as_ref()), ) } @@ -456,14 +484,15 @@ impl TransactionData { Option>, ) -> Option>, f_sapling: impl FnOnce( - Option>, - ) -> Option>, + Option>, + ) -> Option>, f_orchard: impl FnOnce( Option>, ) -> Option>, - #[cfg(feature = "zfuture")] f_tze: impl FnOnce( + #[cfg(zcash_unstable = "zfuture")] f_tze: impl FnOnce( Option>, - ) -> Option>, + ) + -> Option>, ) -> TransactionData { TransactionData { version: self.version, @@ -474,7 +503,7 @@ impl TransactionData { sprout_bundle: self.sprout_bundle, sapling_bundle: f_sapling(self.sapling_bundle), orchard_bundle: f_orchard(self.orchard_bundle), - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] tze_bundle: f_tze(self.tze_bundle), } } @@ -482,9 +511,9 @@ impl TransactionData { pub fn map_authorization( self, f_transparent: impl transparent::MapAuth, - f_sapling: impl sapling::MapAuth, + mut f_sapling: impl sapling_serialization::MapAuth, mut f_orchard: impl orchard_serialization::MapAuth, - #[cfg(feature = "zfuture")] f_tze: impl tze::MapAuth, + #[cfg(zcash_unstable = "zfuture")] f_tze: impl tze::MapAuth, ) -> TransactionData { TransactionData { version: self.version, @@ -495,7 +524,15 @@ impl TransactionData { .transparent_bundle .map(|b| b.map_authorization(f_transparent)), sprout_bundle: self.sprout_bundle, - sapling_bundle: self.sapling_bundle.map(|b| b.map_authorization(f_sapling)), + sapling_bundle: self.sapling_bundle.map(|b| { + b.map_authorization( + &mut f_sapling, + |f, p| f.map_spend_proof(p), + |f, p| f.map_output_proof(p), + |f, s| f.map_auth_sig(s), + |f, a| f.map_authorization(a), + ) + }), orchard_bundle: self.orchard_bundle.map(|b| { b.map_authorization( &mut f_orchard, @@ -503,7 +540,7 @@ impl TransactionData { |f, a| f.map_authorization(a), ) }), - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] tze_bundle: self.tze_bundle.map(|b| b.map_authorization(f_tze)), } } @@ -530,7 +567,7 @@ impl Transaction { Self::from_data_v4(data) } TxVersion::Zip225 => Ok(Self::from_data_v5(data)), - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] TxVersion::ZFuture => Ok(Self::from_data_v5(data)), } } @@ -573,7 +610,7 @@ impl Transaction { Self::read_v4(reader, version, consensus_branch_id) } TxVersion::Zip225 => Self::read_v5(reader.into_base_reader(), version), - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] TxVersion::ZFuture => Self::read_v5(reader.into_base_reader(), version), } } @@ -593,18 +630,8 @@ impl Transaction { 0u32.into() }; - let (value_balance, shielded_spends, shielded_outputs) = if version.has_sapling() { - let vb = Self::read_amount(&mut reader)?; - #[allow(clippy::redundant_closure)] - let ss: Vec> = - Vector::read(&mut reader, |r| SpendDescription::read(r))?; - #[allow(clippy::redundant_closure)] - let so: Vec> = - Vector::read(&mut reader, |r| OutputDescription::read(r))?; - (vb, ss, so) - } else { - (Amount::zero(), vec![], vec![]) - }; + let (value_balance, shielded_spends, shielded_outputs) = + sapling_serialization::read_v4_components(&mut reader, version.has_sapling())?; let sprout_bundle = if version.has_sprout() { let joinsplits = Vector::read(&mut reader, |r| { @@ -630,7 +657,9 @@ impl Transaction { let binding_sig = if version.has_sapling() && !(shielded_spends.is_empty() && shielded_outputs.is_empty()) { - Some(redjubjub::Signature::read(&mut reader)?) + let mut sig = [0; 64]; + reader.read_exact(&mut sig)?; + Some(redjubjub::Signature::from(sig)) } else { None }; @@ -648,16 +677,16 @@ impl Transaction { expiry_height, transparent_bundle, sprout_bundle, - sapling_bundle: binding_sig.map(|binding_sig| { + sapling_bundle: binding_sig.and_then(|binding_sig| { sapling::Bundle::from_parts( shielded_spends, shielded_outputs, value_balance, - sapling::Authorized { binding_sig }, + sapling::bundle::Authorized { binding_sig }, ) }), orchard_bundle: None, - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] tze_bundle: None, }, }) @@ -690,10 +719,10 @@ impl Transaction { let (consensus_branch_id, lock_time, expiry_height) = Self::read_v5_header_fragment(&mut reader)?; let transparent_bundle = Self::read_transparent(&mut reader)?; - let sapling_bundle = Self::read_v5_sapling(&mut reader)?; + let sapling_bundle = sapling_serialization::read_v5_bundle(&mut reader)?; let orchard_bundle = orchard_serialization::read_v5_bundle(&mut reader)?; - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] let tze_bundle = if version.has_tze() { Self::read_tze(&mut reader)? } else { @@ -709,7 +738,7 @@ impl Transaction { sprout_bundle: None, sapling_bundle, orchard_bundle, - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] tze_bundle, }; @@ -733,72 +762,11 @@ impl Transaction { #[cfg(feature = "temporary-zcashd")] pub fn temporary_zcashd_read_v5_sapling( reader: R, - ) -> io::Result>> { - Self::read_v5_sapling(reader) + ) -> io::Result>> { + sapling_serialization::read_v5_bundle(reader) } - #[allow(clippy::redundant_closure)] - fn read_v5_sapling( - mut reader: R, - ) -> io::Result>> { - let sd_v5s = Vector::read(&mut reader, SpendDescriptionV5::read)?; - let od_v5s = Vector::read(&mut reader, OutputDescriptionV5::read)?; - let n_spends = sd_v5s.len(); - let n_outputs = od_v5s.len(); - let value_balance = if n_spends > 0 || n_outputs > 0 { - Self::read_amount(&mut reader)? - } else { - Amount::zero() - }; - - let anchor = if n_spends > 0 { - Some(sapling::read_base(&mut reader, "anchor")?) - } else { - None - }; - - let v_spend_proofs = Array::read(&mut reader, n_spends, |r| sapling::read_zkproof(r))?; - let v_spend_auth_sigs = Array::read(&mut reader, n_spends, |r| { - SpendDescription::read_spend_auth_sig(r) - })?; - let v_output_proofs = Array::read(&mut reader, n_outputs, |r| sapling::read_zkproof(r))?; - - let binding_sig = if n_spends > 0 || n_outputs > 0 { - Some(redjubjub::Signature::read(&mut reader)?) - } else { - None - }; - - let shielded_spends = sd_v5s - .into_iter() - .zip( - v_spend_proofs - .into_iter() - .zip(v_spend_auth_sigs.into_iter()), - ) - .map(|(sd_5, (zkproof, spend_auth_sig))| { - // the following `unwrap` is safe because we know n_spends > 0. - sd_5.into_spend_description(anchor.unwrap(), zkproof, spend_auth_sig) - }) - .collect(); - - let shielded_outputs = od_v5s - .into_iter() - .zip(v_output_proofs.into_iter()) - .map(|(od_5, zkproof)| od_5.into_output_description(zkproof)) - .collect(); - - Ok(binding_sig.map(|binding_sig| { - sapling::Bundle::from_parts( - shielded_spends, - shielded_outputs, - value_balance, - sapling::Authorized { binding_sig }, - ) - })) - } - - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] fn read_tze(mut reader: &mut R) -> io::Result>> { let vin = Vector::read(&mut reader, TzeIn::read)?; let vout = Vector::read(&mut reader, TzeOut::read)?; @@ -819,7 +787,7 @@ impl Transaction { self.write_v4(writer) } TxVersion::Zip225 => self.write_v5(writer), - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] TxVersion::ZFuture => self.write_v5(writer), } } @@ -833,34 +801,11 @@ impl Transaction { writer.write_u32::(u32::from(self.expiry_height))?; } - if self.version.has_sapling() { - writer.write_all( - &self - .sapling_bundle - .as_ref() - .map_or(Amount::zero(), |b| *b.value_balance()) - .to_i64_le_bytes(), - )?; - Vector::write( - &mut writer, - self.sapling_bundle - .as_ref() - .map_or(&[], |b| b.shielded_spends()), - |w, e| e.write_v4(w), - )?; - Vector::write( - &mut writer, - self.sapling_bundle - .as_ref() - .map_or(&[], |b| b.shielded_outputs()), - |w, e| e.write_v4(w), - )?; - } else if self.sapling_bundle.is_some() { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Sapling components may not be present if Sapling is not active.", - )); - } + sapling_serialization::write_v4_components( + &mut writer, + self.sapling_bundle.as_ref(), + self.version.has_sapling(), + )?; if self.version.has_sprout() { if let Some(bundle) = self.sprout_bundle.as_ref() { @@ -874,7 +819,7 @@ impl Transaction { if self.version.has_sapling() { if let Some(bundle) = self.sapling_bundle.as_ref() { - bundle.authorization().binding_sig.write(&mut writer)?; + writer.write_all(&<[u8; 64]>::from(bundle.authorization().binding_sig))?; } } @@ -911,7 +856,7 @@ impl Transaction { self.write_transparent(&mut writer)?; self.write_v5_sapling(&mut writer)?; orchard_serialization::write_v5_bundle(self.orchard_bundle.as_ref(), &mut writer)?; - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] self.write_tze(&mut writer)?; Ok(()) } @@ -926,65 +871,17 @@ impl Transaction { #[cfg(feature = "temporary-zcashd")] pub fn temporary_zcashd_write_v5_sapling( - sapling_bundle: Option<&sapling::Bundle>, + sapling_bundle: Option<&sapling::Bundle>, writer: W, ) -> io::Result<()> { - Self::write_v5_sapling_inner(sapling_bundle, writer) + sapling_serialization::write_v5_bundle(writer, sapling_bundle) } pub fn write_v5_sapling(&self, writer: W) -> io::Result<()> { - Self::write_v5_sapling_inner(self.sapling_bundle.as_ref(), writer) - } - - fn write_v5_sapling_inner( - sapling_bundle: Option<&sapling::Bundle>, - mut writer: W, - ) -> io::Result<()> { - if let Some(bundle) = sapling_bundle { - Vector::write(&mut writer, bundle.shielded_spends(), |w, e| { - e.write_v5_without_witness_data(w) - })?; - - Vector::write(&mut writer, bundle.shielded_outputs(), |w, e| { - e.write_v5_without_proof(w) - })?; - - if !(bundle.shielded_spends().is_empty() && bundle.shielded_outputs().is_empty()) { - writer.write_all(&bundle.value_balance().to_i64_le_bytes())?; - } - if !bundle.shielded_spends().is_empty() { - writer.write_all(bundle.shielded_spends()[0].anchor().to_repr().as_ref())?; - } - - Array::write( - &mut writer, - bundle.shielded_spends().iter().map(|s| &s.zkproof()[..]), - |w, e| w.write_all(e), - )?; - Array::write( - &mut writer, - bundle.shielded_spends().iter().map(|s| s.spend_auth_sig()), - |w, e| e.write(w), - )?; - - 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()) { - bundle.authorization().binding_sig.write(&mut writer)?; - } - } else { - CompactSize::write(&mut writer, 0)?; - CompactSize::write(&mut writer, 0)?; - } - - Ok(()) + sapling_serialization::write_v5_bundle(writer, self.sapling_bundle.as_ref()) } - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] pub fn write_tze(&self, mut writer: W) -> io::Result<()> { if let Some(bundle) = &self.tze_bundle { Vector::write(&mut writer, &bundle.vin, |w, e| e.write(w))?; @@ -1023,7 +920,7 @@ pub struct TxDigests { pub transparent_digests: Option>, pub sapling_digest: Option, pub orchard_digest: Option, - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] pub tze_digests: Option>, } @@ -1033,7 +930,7 @@ pub trait TransactionDigest { type SaplingDigest; type OrchardDigest; - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] type TzeDigest; type Digest; @@ -1053,7 +950,7 @@ pub trait TransactionDigest { fn digest_sapling( &self, - sapling_bundle: Option<&sapling::Bundle>, + sapling_bundle: Option<&sapling::Bundle>, ) -> Self::SaplingDigest; fn digest_orchard( @@ -1061,7 +958,7 @@ pub trait TransactionDigest { orchard_bundle: Option<&orchard::Bundle>, ) -> Self::OrchardDigest; - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] fn digest_tze(&self, tze_bundle: Option<&tze::Bundle>) -> Self::TzeDigest; fn combine( @@ -1070,7 +967,7 @@ pub trait TransactionDigest { transparent_digest: Self::TransparentDigest, sapling_digest: Self::SaplingDigest, orchard_digest: Self::OrchardDigest, - #[cfg(feature = "zfuture")] tze_digest: Self::TzeDigest, + #[cfg(zcash_unstable = "zfuture")] tze_digest: Self::TzeDigest, ) -> Self::Digest; } @@ -1093,7 +990,7 @@ pub mod testing { Authorized, Transaction, TransactionData, TxId, TxVersion, }; - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] use super::components::tze::testing::{self as tze}; pub fn arb_txid() -> impl Strategy { @@ -1108,12 +1005,14 @@ pub mod testing { Just(TxVersion::Sapling).boxed() } BranchId::Nu5 => Just(TxVersion::Zip225).boxed(), - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "nu6")] + BranchId::Nu6 => Just(TxVersion::Zip225).boxed(), + #[cfg(zcash_unstable = "zfuture")] BranchId::ZFuture => Just(TxVersion::ZFuture).boxed(), } } - #[cfg(not(feature = "zfuture"))] + #[cfg(not(zcash_unstable = "zfuture"))] prop_compose! { pub fn arb_txdata(consensus_branch_id: BranchId)( version in arb_tx_version(consensus_branch_id), @@ -1138,7 +1037,7 @@ pub mod testing { } } - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] prop_compose! { pub fn arb_txdata(consensus_branch_id: BranchId)( version in arb_tx_version(consensus_branch_id), diff --git a/zcash_primitives/src/transaction/sighash.rs b/zcash_primitives/src/transaction/sighash.rs index 3fd9b8b2ea..4be0a7454b 100644 --- a/zcash_primitives/src/transaction/sighash.rs +++ b/zcash_primitives/src/transaction/sighash.rs @@ -1,18 +1,18 @@ -use crate::legacy::Script; use blake2b_simd::Hash as Blake2bHash; use super::{ - components::{ - sapling::{self, GrothProofBytes}, - transparent, Amount, - }, + components::{amount::NonNegativeAmount, transparent}, sighash_v4::v4_signature_hash, sighash_v5::v5_signature_hash, Authorization, TransactionData, TxDigests, TxVersion, }; +use crate::{ + legacy::Script, + sapling::{self, bundle::GrothProofBytes}, +}; -#[cfg(feature = "zfuture")] -use crate::extensions::transparent::Precondition; +#[cfg(zcash_unstable = "zfuture")] +use {super::components::Amount, crate::extensions::transparent::Precondition}; pub const SIGHASH_ALL: u8 = 0x01; pub const SIGHASH_NONE: u8 = 0x02; @@ -27,9 +27,9 @@ pub enum SignableInput<'a> { index: usize, script_code: &'a Script, script_pubkey: &'a Script, - value: Amount, + value: NonNegativeAmount, }, - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] Tze { index: usize, precondition: &'a Precondition, @@ -42,7 +42,7 @@ impl<'a> SignableInput<'a> { match self { SignableInput::Shielded => SIGHASH_ALL, SignableInput::Transparent { hash_type, .. } => *hash_type, - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] SignableInput::Tze { .. } => SIGHASH_ALL, } } @@ -63,7 +63,7 @@ pub trait TransparentAuthorizingContext: transparent::Authorization { /// so that wallets can commit to the transparent input breakdown /// without requiring the full data of the previous transactions /// providing these inputs. - fn input_amounts(&self) -> Vec; + fn input_amounts(&self) -> Vec; /// Returns the list of all transparent input scriptPubKeys, provided /// so that wallets can commit to the transparent input breakdown /// without requiring the full data of the previous transactions @@ -76,13 +76,12 @@ pub trait TransparentAuthorizingContext: transparent::Authorization { /// set of precomputed hashes produced in the construction of the /// transaction ID. pub fn signature_hash< - 'a, TA: TransparentAuthorizingContext, - SA: sapling::Authorization, + SA: sapling::bundle::Authorization, A: Authorization, >( tx: &TransactionData, - signable_input: &SignableInput<'a>, + signable_input: &SignableInput<'_>, txid_parts: &TxDigests, ) -> SignatureHash { SignatureHash(match tx.version { @@ -92,7 +91,7 @@ pub fn signature_hash< TxVersion::Zip225 => v5_signature_hash(tx, signable_input, txid_parts), - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] TxVersion::ZFuture => v5_signature_hash(tx, signable_input, txid_parts), }) } diff --git a/zcash_primitives/src/transaction/sighash_v4.rs b/zcash_primitives/src/transaction/sighash_v4.rs index b5eeed7b1c..ad136e5f8d 100644 --- a/zcash_primitives/src/transaction/sighash_v4.rs +++ b/zcash_primitives/src/transaction/sighash_v4.rs @@ -1,12 +1,17 @@ use blake2b_simd::{Hash as Blake2bHash, Params as Blake2bParams}; -use byteorder::{LittleEndian, WriteBytesExt}; use ff::PrimeField; -use crate::consensus::BranchId; +use crate::{ + consensus::BranchId, + sapling::{ + self, + bundle::{GrothProofBytes, OutputDescription, SpendDescription}, + }, +}; use super::{ components::{ - sapling::{self, GrothProofBytes, OutputDescription, SpendDescription}, + sapling as sapling_serialization, sprout::JsDescription, transparent::{self, TxIn, TxOut}, }, @@ -22,13 +27,6 @@ const ZCASH_JOINSPLITS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashJSplitsHash"; const ZCASH_SHIELDED_SPENDS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashSSpendsHash"; const ZCASH_SHIELDED_OUTPUTS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashSOutputHash"; -macro_rules! update_u32 { - ($h:expr, $value:expr, $tmp:expr) => { - (&mut $tmp[..4]).write_u32::($value).unwrap(); - $h.update(&$tmp[..4]); - }; -} - macro_rules! update_hash { ($h:expr, $cond:expr, $value:expr) => { if $cond { @@ -53,7 +51,7 @@ fn prevout_hash(vin: &[TxIn]) -> Blake2bHash fn sequence_hash(vin: &[TxIn]) -> Blake2bHash { let mut data = Vec::with_capacity(vin.len() * 4); for t_in in vin { - data.write_u32::(t_in.sequence).unwrap(); + data.extend_from_slice(&t_in.sequence.to_le_bytes()); } Blake2bParams::new() .hash_length(32) @@ -105,7 +103,7 @@ fn joinsplits_hash( } fn shielded_spends_hash< - A: sapling::Authorization, + A: sapling::bundle::Authorization, >( shielded_spends: &[SpendDescription], ) -> Blake2bHash { @@ -114,7 +112,7 @@ fn shielded_spends_hash< data.extend_from_slice(&s_spend.cv().to_bytes()); data.extend_from_slice(s_spend.anchor().to_repr().as_ref()); data.extend_from_slice(s_spend.nullifier().as_ref()); - s_spend.rk().write(&mut data).unwrap(); + data.extend_from_slice(&<[u8; 32]>::from(*s_spend.rk())); data.extend_from_slice(s_spend.zkproof()); } Blake2bParams::new() @@ -126,7 +124,7 @@ fn shielded_spends_hash< fn shielded_outputs_hash(shielded_outputs: &[OutputDescription]) -> Blake2bHash { let mut data = Vec::with_capacity(shielded_outputs.len() * 948); for s_out in shielded_outputs { - s_out.write_v4(&mut data).unwrap(); + sapling_serialization::write_output_v4(&mut data, s_out).unwrap(); } Blake2bParams::new() .hash_length(32) @@ -135,7 +133,7 @@ fn shielded_outputs_hash(shielded_outputs: &[OutputDescription] } pub fn v4_signature_hash< - SA: sapling::Authorization, + SA: sapling::bundle::Authorization, A: Authorization, >( tx: &TransactionData, @@ -145,18 +143,15 @@ pub fn v4_signature_hash< if tx.version.has_overwinter() { let mut personal = [0; 16]; personal[..12].copy_from_slice(ZCASH_SIGHASH_PERSONALIZATION_PREFIX); - (&mut personal[12..]) - .write_u32::(tx.consensus_branch_id.into()) - .unwrap(); + personal[12..].copy_from_slice(&u32::from(tx.consensus_branch_id).to_le_bytes()); let mut h = Blake2bParams::new() .hash_length(32) .personal(&personal) .to_state(); - let mut tmp = [0; 8]; - update_u32!(h, tx.version.header(), tmp); - update_u32!(h, tx.version.version_group_id(), tmp); + h.update(&tx.version.header().to_le_bytes()); + h.update(&tx.version.version_group_id().to_le_bytes()); update_hash!( h, hash_type & SIGHASH_ANYONECANPAY == 0, @@ -231,12 +226,12 @@ pub fn v4_signature_hash< shielded_outputs_hash(tx.sapling_bundle.as_ref().unwrap().shielded_outputs()) ); } - update_u32!(h, tx.lock_time, tmp); - update_u32!(h, tx.expiry_height.into(), tmp); + h.update(&tx.lock_time.to_le_bytes()); + h.update(&u32::from(tx.expiry_height).to_le_bytes()); if tx.version.has_sapling() { h.update(&tx.sapling_value_balance().to_i64_le_bytes()); } - update_u32!(h, hash_type.into(), tmp); + h.update(&u32::from(hash_type).to_le_bytes()); match signable_input { SignableInput::Shielded => (), @@ -251,8 +246,7 @@ pub fn v4_signature_hash< bundle.vin[*index].prevout.write(&mut data).unwrap(); script_code.write(&mut data).unwrap(); data.extend_from_slice(&value.to_i64_le_bytes()); - data.write_u32::(bundle.vin[*index].sequence) - .unwrap(); + data.extend_from_slice(&bundle.vin[*index].sequence.to_le_bytes()); h.update(&data); } else { panic!( @@ -261,7 +255,7 @@ pub fn v4_signature_hash< } } - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] SignableInput::Tze { .. } => { panic!("A request has been made to sign a TZE input, but the transaction version is not ZFuture"); } diff --git a/zcash_primitives/src/transaction/sighash_v5.rs b/zcash_primitives/src/transaction/sighash_v5.rs index aebe5f9193..60b02b8b75 100644 --- a/zcash_primitives/src/transaction/sighash_v5.rs +++ b/zcash_primitives/src/transaction/sighash_v5.rs @@ -1,7 +1,6 @@ use std::io::Write; use blake2b_simd::{Hash as Blake2bHash, Params, State}; -use byteorder::{LittleEndian, WriteBytesExt}; use zcash_encoding::Array; use crate::transaction::{ @@ -17,17 +16,18 @@ use crate::transaction::{ Authorization, TransactionData, TransparentDigests, TxDigests, }; -#[cfg(feature = "zfuture")] -use zcash_encoding::{CompactSize, Vector}; - -#[cfg(feature = "zfuture")] -use crate::transaction::{components::tze, TzeDigests}; +#[cfg(zcash_unstable = "zfuture")] +use { + crate::transaction::{components::tze, TzeDigests}, + byteorder::WriteBytesExt, + zcash_encoding::{CompactSize, Vector}, +}; const ZCASH_TRANSPARENT_INPUT_HASH_PERSONALIZATION: &[u8; 16] = b"Zcash___TxInHash"; const ZCASH_TRANSPARENT_AMOUNTS_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxTrAmountsHash"; const ZCASH_TRANSPARENT_SCRIPTS_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxTrScriptsHash"; -#[cfg(feature = "zfuture")] +#[cfg(zcash_unstable = "zfuture")] const ZCASH_TZE_INPUT_HASH_PERSONALIZATION: &[u8; 16] = b"Zcash__TzeInHash"; fn hasher(personal: &[u8; 16]) -> State { @@ -121,7 +121,7 @@ fn transparent_sig_digest( txin.prevout.write(&mut ch).unwrap(); ch.write_all(&value.to_i64_le_bytes()).unwrap(); script_pubkey.write(&mut ch).unwrap(); - ch.write_u32::(txin.sequence).unwrap(); + ch.write_all(&txin.sequence.to_le_bytes()).unwrap(); } let txin_sig_digest = ch.finalize(); @@ -138,7 +138,7 @@ fn transparent_sig_digest( } } -#[cfg(feature = "zfuture")] +#[cfg(zcash_unstable = "zfuture")] fn tze_input_sigdigests( bundle: &tze::Bundle, input: &SignableInput<'_>, @@ -195,7 +195,7 @@ pub fn v5_signature_hash< ), txid_parts.sapling_digest, txid_parts.orchard_digest, - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] tx.tze_bundle .as_ref() .zip(txid_parts.tze_digests.as_ref()) diff --git a/zcash_primitives/src/transaction/tests.rs b/zcash_primitives/src/transaction/tests.rs index 9335d89840..6aa63b29c8 100644 --- a/zcash_primitives/src/transaction/tests.rs +++ b/zcash_primitives/src/transaction/tests.rs @@ -3,10 +3,11 @@ use std::ops::Deref; use proptest::prelude::*; -use crate::{consensus::BranchId, legacy::Script}; +use crate::{ + consensus::BranchId, legacy::Script, transaction::components::amount::NonNegativeAmount, +}; use super::{ - components::Amount, sapling, sighash::{ SignableInput, TransparentAuthorizingContext, SIGHASH_ALL, SIGHASH_ANYONECANPAY, @@ -20,7 +21,7 @@ use super::{ Authorization, Transaction, TransactionData, TxDigests, TxIn, }; -#[cfg(feature = "zfuture")] +#[cfg(zcash_unstable = "zfuture")] use super::components::tze; #[test] @@ -43,7 +44,7 @@ fn check_roundtrip(tx: Transaction) -> Result<(), TestCaseError> { let txo = Transaction::read(&txn_bytes[..], tx.consensus_branch_id).unwrap(); prop_assert_eq!(tx.version, txo.version); - #[cfg(feature = "zfuture")] + #[cfg(zcash_unstable = "zfuture")] prop_assert_eq!(tx.tze_bundle.as_ref(), txo.tze_bundle.as_ref()); prop_assert_eq!(tx.lock_time, txo.lock_time); prop_assert_eq!( @@ -61,6 +62,7 @@ fn check_roundtrip(tx: Transaction) -> Result<(), TestCaseError> { proptest! { #[test] #[ignore] + #[cfg(feature = "expensive-tests")] fn tx_serialization_roundtrip_sprout(tx in arb_tx(BranchId::Sprout)) { check_roundtrip(tx)?; } @@ -69,6 +71,7 @@ proptest! { proptest! { #[test] #[ignore] + #[cfg(feature = "expensive-tests")] fn tx_serialization_roundtrip_overwinter(tx in arb_tx(BranchId::Overwinter)) { check_roundtrip(tx)?; } @@ -77,6 +80,7 @@ proptest! { proptest! { #[test] #[ignore] + #[cfg(feature = "expensive-tests")] fn tx_serialization_roundtrip_sapling(tx in arb_tx(BranchId::Sapling)) { check_roundtrip(tx)?; } @@ -85,6 +89,7 @@ proptest! { proptest! { #[test] #[ignore] + #[cfg(feature = "expensive-tests")] fn tx_serialization_roundtrip_blossom(tx in arb_tx(BranchId::Blossom)) { check_roundtrip(tx)?; } @@ -93,6 +98,7 @@ proptest! { proptest! { #[test] #[ignore] + #[cfg(feature = "expensive-tests")] fn tx_serialization_roundtrip_heartwood(tx in arb_tx(BranchId::Heartwood)) { check_roundtrip(tx)?; } @@ -114,10 +120,11 @@ proptest! { } } -#[cfg(feature = "zfuture")] +#[cfg(zcash_unstable = "zfuture")] proptest! { #[test] #[ignore] + #[cfg(feature = "expensive-tests")] fn tx_serialization_roundtrip_future(tx in arb_tx(BranchId::ZFuture)) { check_roundtrip(tx)?; } @@ -134,7 +141,7 @@ fn zip_0143() { index: n as usize, script_code: &tv.script_code, script_pubkey: &tv.script_code, - value: Amount::from_nonnegative_i64(tv.amount).unwrap(), + value: NonNegativeAmount::from_nonnegative_i64(tv.amount).unwrap(), }, _ => SignableInput::Shielded, }; @@ -156,7 +163,7 @@ fn zip_0243() { index: n as usize, script_code: &tv.script_code, script_pubkey: &tv.script_code, - value: Amount::from_nonnegative_i64(tv.amount).unwrap(), + value: NonNegativeAmount::from_nonnegative_i64(tv.amount).unwrap(), }, _ => SignableInput::Shielded, }; @@ -170,7 +177,7 @@ fn zip_0243() { #[derive(Debug)] struct TestTransparentAuth { - input_amounts: Vec, + input_amounts: Vec, input_scriptpubkeys: Vec