diff --git a/.cargo/config.toml b/.cargo/config.toml index 3c801ff8f56..ea6b21a77bb 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,6 +3,12 @@ # Flags that apply to all Zebra crates and configurations [target.'cfg(all())'] rustflags = [ + # Enable tx_v6 everywhere by default + "--cfg", 'feature="tx_v6"', + + # TODO: Remove when ZSA is stable + "--cfg", "zcash_unstable=\"nu7\"", + # Zebra standard lints for Rust 1.65+ # High-risk code @@ -82,6 +88,12 @@ rustflags = [ [build] rustdocflags = [ + # Enable tx_v6 everywhere by default + "--cfg", 'feature="tx_v6"', + + # TODO: Remove when ZSA is stable + "--cfg", "zcash_unstable=\"nu7\"", + # The -A and -W settings must be the same as the `RUSTDOCFLAGS` in: # https://github.com/ZcashFoundation/zebra/blob/main/.github/workflows/docs-deploy-firebase.yml#L68 diff --git a/.dockerignore b/.dockerignore index 567fee9decd..f017360b3e2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -22,4 +22,4 @@ !zebra-* !zebrad !docker/entrypoint.sh -!docker/default-zebra-config.toml +!testnet-single-node-deploy diff --git a/.github/workflows/ci-basic.yml b/.github/workflows/ci-basic.yml new file mode 100644 index 00000000000..ff00f174da2 --- /dev/null +++ b/.github/workflows/ci-basic.yml @@ -0,0 +1,67 @@ +name: Basic checks + +on: [push] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + nu7: + - true + - false + + env: + # Use system-installed RocksDB library instead of building from scratch + ROCKSDB_LIB_DIR: /usr/lib + # Use system-installed Snappy library for compression in RocksDB + SNAPPY_LIB_DIR: /usr/lib/x86_64-linux-gnu + + steps: + - uses: actions/checkout@v4 + - name: Show system resource summary (before cleanup) + run: | + df -h + free -h + lscpu | egrep 'Model name|Socket|Thread|Core|CPU\(s\)' + + - name: Free disk space (safe cleanup for Rust CI) + run: | + # Remove heavy preinstalled SDKs and toolchains + sudo rm -rf /usr/local/lib/android || true + sudo rm -rf "$AGENT_TOOLSDIRECTORY" || true # preinstalled tool caches + df -h + + - name: Install dependencies on Ubuntu + run: sudo apt-get update && sudo apt-get install -y protobuf-compiler librocksdb-dev + - name: Install formatting & linting tools + run: rustup component add rustfmt clippy + + - name: Verify working directory is clean + run: git diff --exit-code + + - name: Strip nu7/tx_v6 flags from config + if: ${{ !matrix.nu7 }} + run: | + sed -i 's|.*"--cfg", .feature="tx_v6".*|# &|' .cargo/config.toml + sed -i 's|.*"--cfg", "zcash_unstable=\\"nu7\\"".*|# &|' .cargo/config.toml + + - name: Run tests + run: timeout --preserve-status 1h cargo test --verbose --locked + - name: Run doc check + run: cargo doc --workspace --no-deps --all-features --document-private-items --locked + - name: Run format check + run: cargo fmt -- --check + - name: Run clippy + run: cargo clippy --workspace --all-targets --features "default-release-binaries proptest-impl lightwalletd-grpc-tests zebra-checkpoints" + - name: Restore cargo config + run: git checkout -- .cargo/config.toml + - name: Verify working directory is clean + run: git diff --exit-code + + - name: Show system resource summary + run: | + df -h + free -h + lscpu | egrep 'Model name|Socket|Thread|Core|CPU\(s\)' diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ac2c57c5cc1..c579b190529 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -67,7 +67,8 @@ jobs: cache-on-failure: true - uses: ./.github/actions/setup-zebra-build - name: Run clippy - run: cargo clippy ${{ matrix.args }} --features "${{ matrix.features }}" + # TODO: Temporary QED-it fork exception for clippy::manual_option_zip; remove before merging this branch upstream. + run: cargo clippy ${{ matrix.args }} --features "${{ matrix.features }}" -- -A unknown-lints -A clippy::manual_option_zip crate-checks: permissions: diff --git a/.github/workflows/tests-unit.yml b/.github/workflows/tests-unit.yml index 375b24d02a7..0ced5e4230a 100644 --- a/.github/workflows/tests-unit.yml +++ b/.github/workflows/tests-unit.yml @@ -66,6 +66,9 @@ jobs: - uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c #v1.15.2 with: toolchain: ${{ matrix.rust-version }} + # Mirror the cfgs in .cargo/config.toml; the action's RUSTFLAGS export + # would otherwise replace them. + rustflags: '-D warnings --cfg zcash_unstable="nu7" --cfg feature="tx_v6"' cache-key: unit-tests-${{ matrix.os }}-${{ matrix.rust-version }}-${{ matrix.features }} cache-on-failure: true - uses: taiki-e/install-action@305bebabd4457bed9b82541755f034994382465b #v2.68.10 diff --git a/Cargo.lock b/Cargo.lock index 454d3831875..28bf2828a34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -165,7 +165,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -176,7 +176,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -547,13 +547,13 @@ dependencies = [ [[package]] name = "blake2b_simd" -version = "1.0.4" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b79834656f71332577234b50bfc009996f7449e0c056884e6a02492ded0ca2f3" +checksum = "3c2f0dc9a68c6317d884f97cc36cf5a3d20ba14ce404227df55e1af708ab04bc" dependencies = [ "arrayref", "arrayvec", - "constant_time_eq", + "constant_time_eq 0.2.6", ] [[package]] @@ -564,7 +564,7 @@ checksum = "ee29928bad1e3f94c9d1528da29e07a1d3d04817ae8332de1e8b846c8439f4b3" dependencies = [ "arrayref", "arrayvec", - "constant_time_eq", + "constant_time_eq 0.4.2", ] [[package]] @@ -1063,6 +1063,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "constant_time_eq" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6" + [[package]] name = "constant_time_eq" version = "0.4.2" @@ -1436,7 +1442,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1594,8 +1600,7 @@ dependencies = [ [[package]] name = "equihash" version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca4f333d4ccc9d23c06593733673026efa71a332e028b00f12cf427b9677dce9" +source = "git+https://github.com/QED-it/librustzcash?rev=0ea737548f7aea6124056df54d55f6c5a35ef914#0ea737548f7aea6124056df54d55f6c5a35ef914" dependencies = [ "blake2b_simd", "cc", @@ -1627,7 +1632,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1643,8 +1648,7 @@ dependencies = [ [[package]] name = "f4jumble" version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d42773cb15447644d170be20231a3268600e0c4cea8987d013b93ac973d3cf7" +source = "git+https://github.com/QED-it/librustzcash?rev=0ea737548f7aea6124056df54d55f6c5a35ef914#0ea737548f7aea6124056df54d55f6c5a35ef914" dependencies = [ "blake2b_simd", ] @@ -1999,9 +2003,9 @@ dependencies = [ [[package]] name = "halo2_gadgets" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73a5e510d58a07d8ed238a5a8a436fe6c2c79e1bb2611f62688bc65007b4e6e7" +checksum = "45824ce0dd12e91ec0c68ebae2a7ed8ae19b70946624c849add59f1d1a62a143" dependencies = [ "arrayvec", "bitvec", @@ -2026,8 +2030,7 @@ checksum = "47716fe1ae67969c5e0b2ef826f32db8c3be72be325e1aa3c1951d06b5575ec5" [[package]] name = "halo2_poseidon" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa3da60b81f02f9b33ebc6252d766f843291fb4d2247a07ae73d20b791fc56f" +source = "git+https://github.com/zcash/halo2?rev=2308caf68c48c02468b66cfc452dad54e355e32f#2308caf68c48c02468b66cfc452dad54e355e32f" dependencies = [ "bitvec", "ff", @@ -2037,9 +2040,8 @@ dependencies = [ [[package]] name = "halo2_proofs" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05713f117155643ce10975e0bee44a274bcda2f4bb5ef29a999ad67c1fa8d4d3" +version = "0.3.1" +source = "git+https://github.com/zcash/halo2?rev=2308caf68c48c02468b66cfc452dad54e355e32f#2308caf68c48c02468b66cfc452dad54e355e32f" dependencies = [ "blake2b_simd", "ff", @@ -2614,7 +2616,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi 0.5.2", "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2803,7 +2805,7 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "770919970f7d2f74fea948900d35e2ef64f44129e8ae4015f59de1f0aca7c2a5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3136,7 +3138,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3514,9 +3516,8 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "orchard" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1ef66fcf99348242a20d582d7434da381a867df8dc155b3a980eca767c56137" +version = "0.12.0" +source = "git+https://github.com/QED-it/orchard?rev=77d3274cb1f4620e9a1b86477c490fa123dff6bd#77d3274cb1f4620e9a1b86477c490fa123dff6bd" dependencies = [ "aes", "bitvec", @@ -3535,8 +3536,11 @@ dependencies = [ "memuse", "nonempty", "pasta_curves", + "proptest", "rand 0.8.5", + "rand_core 0.6.4", "reddsa", + "secp256k1", "serde", "sinsemilla", "subtle", @@ -3986,16 +3990,17 @@ dependencies = [ [[package]] name = "proptest" -version = "1.10.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" +checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" dependencies = [ "bit-set", "bit-vec", "bitflags 2.11.0", + "lazy_static", "num-traits", - "rand 0.9.2", - "rand_chacha 0.9.0", + "rand 0.8.5", + "rand_chacha 0.3.1", "rand_xorshift", "regex-syntax", "rusty-fork", @@ -4356,11 +4361,11 @@ dependencies = [ [[package]] name = "rand_xorshift" -version = "0.4.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" dependencies = [ - "rand_core 0.9.5", + "rand_core 0.6.4", ] [[package]] @@ -4674,7 +4679,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4748,9 +4753,8 @@ dependencies = [ [[package]] name = "sapling-crypto" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d3c081c83f1dc87403d9d71a06f52301c0aa9ea4c17da2a3435bbf493ffba4" +version = "0.6.0" +source = "git+https://github.com/QED-it/sapling-crypto?rev=59535fb5d34b5c5cf1b20ef18269f5c65228378c#59535fb5d34b5c5cf1b20ef18269f5c65228378c" dependencies = [ "aes", "bellman", @@ -4828,6 +4832,7 @@ version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ + "rand 0.8.5", "secp256k1-sys", "serde", ] @@ -5154,8 +5159,7 @@ checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "sinsemilla" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d268ae0ea06faafe1662e9967cd4f9022014f5eeb798e0c302c876df8b7af9c" +source = "git+https://github.com/zcash/sinsemilla?rev=aabb707e862bc3d7b803c77d14e5a771bcee3e8c#aabb707e862bc3d7b803c77d14e5a771bcee3e8c" dependencies = [ "group", "pasta_curves", @@ -5414,7 +5418,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -6649,7 +6653,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -7008,9 +7012,9 @@ dependencies = [ [[package]] name = "xdg" -version = "2.5.2" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" +checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5" [[package]] name = "yaml-rust2" @@ -7049,8 +7053,7 @@ dependencies = [ [[package]] name = "zcash_address" version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4491dddd232de02df42481757054dc19c8bc51cf709cfec58feebfef7c3c9a" +source = "git+https://github.com/QED-it/librustzcash?rev=0ea737548f7aea6124056df54d55f6c5a35ef914#0ea737548f7aea6124056df54d55f6c5a35ef914" dependencies = [ "bech32", "bs58", @@ -7063,18 +7066,17 @@ dependencies = [ [[package]] name = "zcash_encoding" version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca38087e6524e5f51a5b0fb3fc18f36d7b84bf67b2056f494ca0c281590953d" +source = "git+https://github.com/QED-it/librustzcash?rev=0ea737548f7aea6124056df54d55f6c5a35ef914#0ea737548f7aea6124056df54d55f6c5a35ef914" dependencies = [ "core2", + "hex", "nonempty", ] [[package]] name = "zcash_history" version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fde17bf53792f9c756b313730da14880257d7661b5bfc69d0571c3a7c11a76d" +source = "git+https://github.com/QED-it/librustzcash?rev=0ea737548f7aea6124056df54d55f6c5a35ef914#0ea737548f7aea6124056df54d55f6c5a35ef914" dependencies = [ "blake2b_simd", "byteorder", @@ -7084,8 +7086,7 @@ dependencies = [ [[package]] name = "zcash_keys" version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c115531caa1b7ca5ccd82dc26dbe3ba44b7542e928a3f77cd04abbe3cde4a4f2" +source = "git+https://github.com/QED-it/librustzcash?rev=0ea737548f7aea6124056df54d55f6c5a35ef914#0ea737548f7aea6124056df54d55f6c5a35ef914" dependencies = [ "bech32", "blake2b_simd", @@ -7096,7 +7097,9 @@ dependencies = [ "group", "memuse", "nonempty", + "orchard", "rand_core 0.6.4", + "sapling-crypto", "secrecy", "subtle", "tracing", @@ -7110,8 +7113,7 @@ dependencies = [ [[package]] name = "zcash_note_encryption" version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77efec759c3798b6e4d829fcc762070d9b229b0f13338c40bf993b7b609c2272" +source = "git+https://github.com/zcash/zcash_note_encryption?rev=9f7e93d42cef839d02b9d75918117941d453f8cb#9f7e93d42cef839d02b9d75918117941d453f8cb" dependencies = [ "chacha20", "chacha20poly1305", @@ -7123,8 +7125,7 @@ dependencies = [ [[package]] name = "zcash_primitives" version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fd9ff256fb298a7e94a73c1adad6c7e0b4b194b902e777ee9f5f2e12c4c4776" +source = "git+https://github.com/QED-it/librustzcash?rev=0ea737548f7aea6124056df54d55f6c5a35ef914#0ea737548f7aea6124056df54d55f6c5a35ef914" dependencies = [ "bip32", "blake2b_simd", @@ -7158,7 +7159,6 @@ dependencies = [ "zcash_note_encryption", "zcash_protocol", "zcash_script", - "zcash_spec", "zcash_transparent", "zip32", ] @@ -7166,8 +7166,7 @@ dependencies = [ [[package]] name = "zcash_proofs" version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a2c13bb673d542608a0e6502ac5494136e7ce4ce97e92dd239489b2523eed9" +source = "git+https://github.com/QED-it/librustzcash?rev=0ea737548f7aea6124056df54d55f6c5a35ef914#0ea737548f7aea6124056df54d55f6c5a35ef914" dependencies = [ "bellman", "blake2b_simd", @@ -7177,7 +7176,6 @@ dependencies = [ "home", "jubjub", "known-folders", - "lazy_static", "rand_core 0.6.4", "redjubjub", "sapling-crypto", @@ -7190,13 +7188,13 @@ dependencies = [ [[package]] name = "zcash_protocol" version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18b1a337bbc9a7d55ae35d31189f03507dbc7934e9a4bee5c1d5c47464860e48" +source = "git+https://github.com/QED-it/librustzcash?rev=0ea737548f7aea6124056df54d55f6c5a35ef914#0ea737548f7aea6124056df54d55f6c5a35ef914" dependencies = [ "core2", "document-features", "hex", "memuse", + "zcash_encoding", ] [[package]] @@ -7219,8 +7217,7 @@ dependencies = [ [[package]] name = "zcash_spec" version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded3f58b93486aa79b85acba1001f5298f27a46489859934954d262533ee2915" +source = "git+https://github.com/QED-it/zcash_spec?rev=d5e84264d2ad0646b587a837f4e2424ca64d3a05#d5e84264d2ad0646b587a837f4e2424ca64d3a05" dependencies = [ "blake2b_simd", ] @@ -7228,8 +7225,7 @@ dependencies = [ [[package]] name = "zcash_transparent" version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9b7b4bc11d8bb20833d1b8ab6807f4dca941b381f1129e5bbd72a84e391991" +source = "git+https://github.com/QED-it/librustzcash?rev=0ea737548f7aea6124056df54d55f6c5a35ef914#0ea737548f7aea6124056df54d55f6c5a35ef914" dependencies = [ "bip32", "blake2b_simd", @@ -7359,6 +7355,7 @@ dependencies = [ "tracing-error", "tracing-futures", "tracing-subscriber", + "zcash_primitives", "zcash_proofs", "zcash_protocol", "zebra-chain", diff --git a/Cargo.toml b/Cargo.toml index 34d7c73c301..ffc1c5c2e49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,23 +23,25 @@ license = "MIT OR Apache-2.0" repository = "https://github.com/ZcashFoundation/zebra" homepage = "https://zfnd.org/zebra/" keywords = ["zebra", "zcash"] -rust-version = "1.85.0" +rust-version = "1.85.1" edition = "2021" # `cargo release` settings [workspace.dependencies] incrementalmerkletree = { version = "0.8.2", features = ["legacy-api"] } -orchard = "0.11" -sapling-crypto = "0.5" -zcash_address = "0.10" -zcash_encoding = "0.3" -zcash_history = "0.4" -zcash_keys = "0.12" -zcash_primitives = "0.26" -zcash_proofs = "0.26" -zcash_transparent = "0.6" -zcash_protocol = "0.7" +# TODO: Remove the `temporary-zebra` feature once upstream `orchard` exposes the APIs +# Zebra needs without this transition feature. +orchard = { version = "0.12", features = ["zsa-issuance", "temporary-zebra"] } +sapling-crypto = "0.6" +zcash_address = "0.10.1" +zcash_encoding = "0.3.0" +zcash_history = "0.4.0" +zcash_keys = "0.12.0" +zcash_primitives = { version = "0.26.4", features = ["zsa-issuance", "zip-233"] } +zcash_proofs = "0.26.1" +zcash_transparent = "0.6.3" +zcash_protocol = "0.7.2" zip32 = "0.2" abscissa_core = "0.7" atty = "0.2.14" @@ -314,3 +316,21 @@ unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(tokio_unstable)', # Used by tokio-console 'cfg(zcash_unstable, values("zfuture", "nu6.1", "nu7"))' # Used in Zebra and librustzcash ] } + +[patch.crates-io] +halo2_proofs = { version = "0.3.0", git = "https://github.com/zcash/halo2", rev = "2308caf68c48c02468b66cfc452dad54e355e32f" } +halo2_poseidon = { version = "0.1.0", git = "https://github.com/zcash/halo2", rev = "2308caf68c48c02468b66cfc452dad54e355e32f" } +sinsemilla = { git = "https://github.com/zcash/sinsemilla", rev = "aabb707e862bc3d7b803c77d14e5a771bcee3e8c" } +zcash_note_encryption = { version = "0.4.1", git = "https://github.com/zcash/zcash_note_encryption", rev = "9f7e93d42cef839d02b9d75918117941d453f8cb" } +sapling-crypto = { package = "sapling-crypto", version = "0.6", git = "https://github.com/QED-it/sapling-crypto", rev = "59535fb5d34b5c5cf1b20ef18269f5c65228378c" } +orchard = { version = "0.12.0", git = "https://github.com/QED-it/orchard", rev = "77d3274cb1f4620e9a1b86477c490fa123dff6bd" } +zcash_primitives = { version = "0.26.1", git = "https://github.com/QED-it/librustzcash", rev = "0ea737548f7aea6124056df54d55f6c5a35ef914" } +zcash_protocol = { version = "0.7.2", git = "https://github.com/QED-it/librustzcash", rev = "0ea737548f7aea6124056df54d55f6c5a35ef914" } +zcash_address = { version = "0.10.1", git = "https://github.com/QED-it/librustzcash", rev = "0ea737548f7aea6124056df54d55f6c5a35ef914" } +zcash_encoding = { version = "0.3.0", git = "https://github.com/QED-it/librustzcash", rev = "0ea737548f7aea6124056df54d55f6c5a35ef914" } +zcash_history = { version = "0.4.0", git = "https://github.com/QED-it/librustzcash", rev = "0ea737548f7aea6124056df54d55f6c5a35ef914" } +zcash_keys = { version = "0.12.0", git = "https://github.com/QED-it/librustzcash", rev = "0ea737548f7aea6124056df54d55f6c5a35ef914" } +zcash_transparent = { version = "0.6.3", git = "https://github.com/QED-it/librustzcash", rev = "0ea737548f7aea6124056df54d55f6c5a35ef914" } +zcash_proofs = { version = "0.26.1", git = "https://github.com/QED-it/librustzcash", rev = "0ea737548f7aea6124056df54d55f6c5a35ef914" } +equihash = { version = "0.2.2", git = "https://github.com/QED-it/librustzcash", rev = "0ea737548f7aea6124056df54d55f6c5a35ef914" } +zcash_spec = { git = "https://github.com/QED-it/zcash_spec", rev = "d5e84264d2ad0646b587a837f4e2424ca64d3a05" } diff --git a/deny.toml b/deny.toml index 0eefca407cc..84a17aa4651 100644 --- a/deny.toml +++ b/deny.toml @@ -143,7 +143,17 @@ allow-git = [ "https://github.com/zcash/librustzcash.git", "https://github.com/zcash/incrementalmerkletree", "https://github.com/zcash/orchard", - "https://github.com/zcash/sapling-crypto" + "https://github.com/zcash/sapling-crypto", + + "https://github.com/zcash/halo2", + "https://github.com/zcash/sinsemilla", + "https://github.com/zcash/zcash_note_encryption", + + # TODO: remove these QED-it fork entries once the required changes are merged upstream. + "https://github.com/QED-it/librustzcash", + "https://github.com/QED-it/orchard", + "https://github.com/QED-it/sapling-crypto", + "https://github.com/QED-it/zcash_spec", ] [sources.allow-org] diff --git a/testnet-single-node-deploy/dockerfile b/testnet-single-node-deploy/dockerfile new file mode 100644 index 00000000000..c15475224ca --- /dev/null +++ b/testnet-single-node-deploy/dockerfile @@ -0,0 +1,30 @@ +FROM rust:1.85.1 + +# Accept build arguments for Git information +ARG GIT_COMMIT +ARG GIT_TAG + +# Set up Rust and cargo +RUN apt-get update && apt-get install git build-essential clang -y + +# Set the working directory to the repo root +WORKDIR /app + +# Copy files +COPY . . + +# Set Git environment variables for the build +# These will be used by the build.rs script +ENV GIT_COMMIT_FULL=$GIT_COMMIT +ENV GIT_TAG=$GIT_TAG + +# Validate the presence of the config file +RUN test -f testnet-single-node-deploy/regtest-config.toml + +# Build zebrad with the required features +RUN cargo build --release --package zebrad --bin zebrad + +EXPOSE 18232 + +# Run the zebra node +ENTRYPOINT ["target/release/zebrad", "-c", "/app/testnet-single-node-deploy/regtest-config.toml"] diff --git a/testnet-single-node-deploy/regtest-config.toml b/testnet-single-node-deploy/regtest-config.toml new file mode 100644 index 00000000000..5e2322674d9 --- /dev/null +++ b/testnet-single-node-deploy/regtest-config.toml @@ -0,0 +1,24 @@ +[mining] +miner_address = 'tmLTZegcJN5zaufWQBARHkvqC62mTumm3jR' + +[network] +network = "Regtest" + +# This section may be omitted when testing only Canopy +[network.testnet_parameters.activation_heights] +# Configured activation heights must be greater than or equal to 1, +# block height 0 is reserved for the Genesis network upgrade in Zebra +NU5 = 1 +NU6 = 1 +NU7 = 1 + +# This section may be omitted if a persistent Regtest chain state is desired +[state] +ephemeral = true + +# This section may be omitted if it's not necessary to send transactions to Zebra's mempool +[rpc] +listen_addr = "0.0.0.0:18232" + +# disable cookie auth +enable_cookie_auth = false diff --git a/zebra-chain/Cargo.toml b/zebra-chain/Cargo.toml index 79e908f50f0..067885c97d1 100644 --- a/zebra-chain/Cargo.toml +++ b/zebra-chain/Cargo.toml @@ -45,6 +45,7 @@ proptest-impl = [ "rand_chacha", "tokio/tracing", "zebra-test", + "orchard/test-dependencies", ] bench = ["zebra-test"] @@ -85,7 +86,7 @@ zcash_script.workspace = true # ECC deps halo2 = { package = "halo2_proofs", version = "0.3" } -orchard.workspace = true +orchard = { workspace = true } zcash_encoding.workspace = true zcash_history.workspace = true zcash_note_encryption = { workspace = true } diff --git a/zebra-chain/src/block.rs b/zebra-chain/src/block.rs index 2fa724939d1..a67d297613c 100644 --- a/zebra-chain/src/block.rs +++ b/zebra-chain/src/block.rs @@ -179,7 +179,7 @@ impl Block { } /// Access the [orchard note commitments](pallas::Base) from all transactions in this block. - pub fn orchard_note_commitments(&self) -> impl Iterator { + pub fn orchard_note_commitments(&self) -> impl Iterator + '_ { self.transactions .iter() .flat_map(|transaction| transaction.orchard_note_commitments()) diff --git a/zebra-chain/src/block/arbitrary.rs b/zebra-chain/src/block/arbitrary.rs index 3dbad2387a7..6c4811005c5 100644 --- a/zebra-chain/src/block/arbitrary.rs +++ b/zebra-chain/src/block/arbitrary.rs @@ -466,7 +466,7 @@ impl Block { sapling_tree.append(*sapling_note_commitment).unwrap(); } for orchard_note_commitment in transaction.orchard_note_commitments() { - orchard_tree.append(*orchard_note_commitment).unwrap(); + orchard_tree.append(orchard_note_commitment).unwrap(); } } new_transactions.push(Arc::new(transaction)); diff --git a/zebra-chain/src/block/height.rs b/zebra-chain/src/block/height.rs index d2aec338f5f..3a38155e4c5 100644 --- a/zebra-chain/src/block/height.rs +++ b/zebra-chain/src/block/height.rs @@ -2,7 +2,7 @@ use std::ops::{Add, Sub}; use thiserror::Error; -use zcash_primitives::consensus::BlockHeight; +use zcash_protocol::consensus::BlockHeight; use crate::{serialization::SerializationError, BoxError}; diff --git a/zebra-chain/src/lib.rs b/zebra-chain/src/lib.rs index 03b88397454..9bc94234c9d 100644 --- a/zebra-chain/src/lib.rs +++ b/zebra-chain/src/lib.rs @@ -42,6 +42,9 @@ pub mod transparent; pub mod value_balance; pub mod work; +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +pub mod orchard_zsa; + pub use bounded_vec::BoundedVec; pub use error::Error; diff --git a/zebra-chain/src/orchard.rs b/zebra-chain/src/orchard.rs index be96644c8c9..5724199634a 100644 --- a/zebra-chain/src/orchard.rs +++ b/zebra-chain/src/orchard.rs @@ -6,6 +6,7 @@ mod action; mod address; mod commitment; mod note; +mod shielded_data_flavor; mod sinsemilla; #[cfg(any(test, feature = "proptest-impl"))] @@ -23,3 +24,7 @@ pub use commitment::{CommitmentRandomness, NoteCommitment, ValueCommitment}; pub use keys::Diversifier; pub use note::{EncryptedNote, Note, Nullifier, WrappedNoteKey}; pub use shielded_data::{AuthorizedAction, Flags, ShieldedData}; +pub use shielded_data_flavor::{OrchardVanilla, ShieldedDataFlavor}; + +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +pub use shielded_data_flavor::OrchardZSA; diff --git a/zebra-chain/src/orchard/action.rs b/zebra-chain/src/orchard/action.rs index ae7690def7a..f9e9ad9c0ec 100644 --- a/zebra-chain/src/orchard/action.rs +++ b/zebra-chain/src/orchard/action.rs @@ -11,6 +11,7 @@ use super::{ commitment::{self, ValueCommitment}, keys, note::{self, Nullifier}, + ShieldedDataFlavor, }; /// An Action description, as described in the [Zcash specification §7.3][actiondesc]. @@ -21,7 +22,7 @@ use super::{ /// /// [actiondesc]: https://zips.z.cash/protocol/nu5.pdf#actiondesc #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct Action { +pub struct Action { /// A value commitment to net value of the input note minus the output note pub cv: commitment::ValueCommitment, /// The nullifier of the input note being spent. @@ -35,14 +36,14 @@ pub struct Action { /// encrypted private key in `out_ciphertext`. pub ephemeral_key: keys::EphemeralPublicKey, /// A ciphertext component for the encrypted output note. - pub enc_ciphertext: note::EncryptedNote, + pub enc_ciphertext: Flavor::EncryptedNote, /// A ciphertext component that allows the holder of a full viewing key to /// recover the recipient diversified transmission key and the ephemeral /// private key (and therefore the entire note plaintext). pub out_ciphertext: note::WrappedNoteKey, } -impl ZcashSerialize for Action { +impl ZcashSerialize for Action { fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { self.cv.zcash_serialize(&mut writer)?; writer.write_all(&<[u8; 32]>::from(self.nullifier)[..])?; @@ -55,7 +56,7 @@ impl ZcashSerialize for Action { } } -impl ZcashDeserialize for Action { +impl ZcashDeserialize for Action { fn zcash_deserialize(mut reader: R) -> Result { // # Consensus // @@ -93,7 +94,7 @@ impl ZcashDeserialize for Action { // https://zips.z.cash/protocol/protocol.pdf#concretesym but fixed to // 580 bytes in https://zips.z.cash/protocol/protocol.pdf#outputencodingandconsensus // See [`note::EncryptedNote::zcash_deserialize`]. - enc_ciphertext: note::EncryptedNote::zcash_deserialize(&mut reader)?, + enc_ciphertext: Flavor::EncryptedNote::zcash_deserialize(&mut reader)?, // Type is `Sym.C`, i.e. `𝔹^Y^{\[N\]}`, i.e. arbitrary-sized byte arrays // https://zips.z.cash/protocol/protocol.pdf#concretesym but fixed to // 80 bytes in https://zips.z.cash/protocol/protocol.pdf#outputencodingandconsensus diff --git a/zebra-chain/src/orchard/arbitrary.rs b/zebra-chain/src/orchard/arbitrary.rs index 7a6544606f8..7b491de3742 100644 --- a/zebra-chain/src/orchard/arbitrary.rs +++ b/zebra-chain/src/orchard/arbitrary.rs @@ -10,17 +10,21 @@ use reddsa::{orchard::SpendAuth, Signature, SigningKey, VerificationKey, Verific use proptest::{array, collection::vec, prelude::*}; use super::{ - keys::*, note, tree, Action, AuthorizedAction, Flags, NoteCommitment, ValueCommitment, + keys::*, note, tree, Action, AuthorizedAction, Flags, NoteCommitment, ShieldedDataFlavor, + ValueCommitment, }; -impl Arbitrary for Action { +impl Arbitrary for Action +where + ::Strategy: 'static, +{ type Parameters = (); fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { ( any::(), any::(), - any::(), + any::(), any::(), ) .prop_map(|(nullifier, rk, enc_ciphertext, out_ciphertext)| Self { @@ -54,11 +58,14 @@ impl Arbitrary for note::Nullifier { type Strategy = BoxedStrategy; } -impl Arbitrary for AuthorizedAction { +impl Arbitrary for AuthorizedAction +where + ::Strategy: 'static, +{ type Parameters = (); fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - (any::(), any::()) + (any::>(), any::()) .prop_map(|(action, spend_auth_sig)| Self { action, spend_auth_sig: spend_auth_sig.0, @@ -119,7 +126,15 @@ impl Arbitrary for Flags { type Parameters = (); fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - (any::()).prop_map(Self::from_bits_truncate).boxed() + (any::()) + .prop_map(|byte| { + // Clear ENABLE_ZSA: it is only allowed in V6, and this generator is + // also used for V5 cases where the flag would make deserialization fail. + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + let byte = byte & !(Flags::ENABLE_ZSA.bits()); + Self::from_bits_truncate(byte) + }) + .boxed() } type Strategy = BoxedStrategy; diff --git a/zebra-chain/src/orchard/commitment.rs b/zebra-chain/src/orchard/commitment.rs index 4e69258c4e5..98d07716abf 100644 --- a/zebra-chain/src/orchard/commitment.rs +++ b/zebra-chain/src/orchard/commitment.rs @@ -14,6 +14,12 @@ use halo2::{ use lazy_static::lazy_static; use rand_core::{CryptoRng, RngCore}; +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +use orchard::{ + note::AssetBase, + value::{ValueCommitTrapdoor, ValueSum}, +}; + use crate::{ amount::Amount, error::RandError, @@ -236,7 +242,13 @@ impl ValueCommitment { { let rcv = generate_trapdoor(csprng)?; - Ok(Self::new(rcv, value)) + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + let vc = Self::new(rcv, ValueSum::from_raw(value.into()), AssetBase::zatoshi()); + + #[cfg(not(all(zcash_unstable = "nu7", feature = "tx_v6")))] + let vc = Self::new(rcv, value); + + Ok(vc) } /// Generate a new `ValueCommitment` from an existing `rcv on a `value`. @@ -244,11 +256,29 @@ impl ValueCommitment { /// ValueCommit^Orchard(v) := /// /// - #[allow(non_snake_case)] + #[cfg(not(all(zcash_unstable = "nu7", feature = "tx_v6")))] pub fn new(rcv: pallas::Scalar, value: Amount) -> Self { let v = pallas::Scalar::from(value); Self::from(*V * v + *R * rcv) } + + /// Generate a new `ValueCommitment` accounting for `rcv`, `value` and `assetBase`. + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + pub fn new(rcv: pallas::Scalar, value: ValueSum, asset: AssetBase) -> Self { + // TODO: Can be simplified if `ValueCommitTrapdoor` and `ValueCommitment` are exposed in Orchard. + Self( + pallas::Affine::from_bytes( + &orchard::value::ValueCommitment::derive( + value, + ValueCommitTrapdoor::from_bytes(rcv.to_repr()) + .expect("canonical scalar representation round-trip"), + asset, + ) + .to_bytes(), + ) + .expect("valid commitment point round-trip"), + ) + } } lazy_static! { diff --git a/zebra-chain/src/orchard/note/arbitrary.rs b/zebra-chain/src/orchard/note/arbitrary.rs index e9365de80c1..88e34618170 100644 --- a/zebra-chain/src/orchard/note/arbitrary.rs +++ b/zebra-chain/src/orchard/note/arbitrary.rs @@ -2,13 +2,13 @@ use proptest::{collection::vec, prelude::*}; use super::*; -impl Arbitrary for EncryptedNote { +impl Arbitrary for EncryptedNote { type Parameters = (); fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - (vec(any::(), 580)) + (vec(any::(), SIZE)) .prop_map(|v| { - let mut bytes = [0; 580]; + let mut bytes = [0; SIZE]; bytes.copy_from_slice(v.as_slice()); Self(bytes) }) diff --git a/zebra-chain/src/orchard/note/ciphertexts.rs b/zebra-chain/src/orchard/note/ciphertexts.rs index 8f857cf1444..32584963701 100644 --- a/zebra-chain/src/orchard/note/ciphertexts.rs +++ b/zebra-chain/src/orchard/note/ciphertexts.rs @@ -9,58 +9,45 @@ use crate::serialization::{SerializationError, ZcashDeserialize, ZcashSerialize} /// A ciphertext component for encrypted output notes. /// /// Corresponds to the Orchard 'encCiphertext's -#[derive(Deserialize, Serialize)] -pub struct EncryptedNote(#[serde(with = "BigArray")] pub(crate) [u8; 580]); - -// These impls all only exist because of array length restrictions. -// TODO: use const generics https://github.com/ZcashFoundation/zebra/issues/2042 +#[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)] +pub struct EncryptedNote(#[serde(with = "BigArray")] pub(crate) [u8; SIZE]); -impl Copy for EncryptedNote {} - -impl Clone for EncryptedNote { - fn clone(&self) -> Self { - *self +impl From<[u8; SIZE]> for EncryptedNote { + fn from(bytes: [u8; SIZE]) -> Self { + Self(bytes) } } -impl fmt::Debug for EncryptedNote { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_tuple("EncryptedNote") - .field(&hex::encode(&self.0[..])) - .finish() +impl From> for [u8; SIZE] { + fn from(enc_ciphertext: EncryptedNote) -> Self { + enc_ciphertext.0 } } -impl Eq for EncryptedNote {} +impl TryFrom<&[u8]> for EncryptedNote { + type Error = std::array::TryFromSliceError; -impl From<[u8; 580]> for EncryptedNote { - fn from(bytes: [u8; 580]) -> Self { - EncryptedNote(bytes) + fn try_from(bytes: &[u8]) -> Result { + Ok(Self(bytes.try_into()?)) } } -impl From for [u8; 580] { - fn from(enc_ciphertext: EncryptedNote) -> Self { - enc_ciphertext.0 +impl AsRef<[u8]> for EncryptedNote { + fn as_ref(&self) -> &[u8] { + &self.0 } } -impl PartialEq for EncryptedNote { - fn eq(&self, other: &Self) -> bool { - self.0[..] == other.0[..] - } -} - -impl ZcashSerialize for EncryptedNote { +impl ZcashSerialize for EncryptedNote { fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { writer.write_all(&self.0[..])?; Ok(()) } } -impl ZcashDeserialize for EncryptedNote { +impl ZcashDeserialize for EncryptedNote { fn zcash_deserialize(mut reader: R) -> Result { - let mut bytes = [0; 580]; + let mut bytes = [0; SIZE]; reader.read_exact(&mut bytes[..])?; Ok(Self(bytes)) } @@ -126,33 +113,56 @@ impl ZcashDeserialize for WrappedNoteKey { } #[cfg(test)] -use proptest::prelude::*; -#[cfg(test)] -proptest! { +mod tests { + use crate::{ + orchard::{OrchardVanilla, ShieldedDataFlavor, WrappedNoteKey}, + serialization::{ZcashDeserialize, ZcashSerialize}, + }; + + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + use crate::orchard::OrchardZSA; - #[test] - fn encrypted_ciphertext_roundtrip(ec in any::()) { - let _init_guard = zebra_test::init(); + use proptest::prelude::*; + fn roundtrip_encrypted_note(note: &EncryptedNote) -> EncryptedNote + where + EncryptedNote: ZcashSerialize + ZcashDeserialize, + { let mut data = Vec::new(); + note.zcash_serialize(&mut data) + .expect("EncryptedNote should serialize"); + EncryptedNote::zcash_deserialize(&data[..]) + .expect("randomized EncryptedNote should deserialize") + } - ec.zcash_serialize(&mut data).expect("EncryptedNote should serialize"); + proptest! { + #[test] + fn encrypted_ciphertext_roundtrip_orchard_vanilla(ec in any::<::EncryptedNote>()) { + let _init_guard = zebra_test::init(); + let ec2 = roundtrip_encrypted_note(&ec); + prop_assert_eq![ec, ec2]; + } - let ec2 = EncryptedNote::zcash_deserialize(&data[..]).expect("randomized EncryptedNote should deserialize"); - prop_assert_eq![ec, ec2]; - } + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + #[test] + fn encrypted_ciphertext_roundtrip_orchard_zsa(ec in any::<::EncryptedNote>()) { + let _init_guard = zebra_test::init(); + let ec2 = roundtrip_encrypted_note(&ec); + prop_assert_eq![ec, ec2]; + } - #[test] - fn out_ciphertext_roundtrip(oc in any::()) { - let _init_guard = zebra_test::init(); + #[test] + fn out_ciphertext_roundtrip(oc in any::()) { + let _init_guard = zebra_test::init(); - let mut data = Vec::new(); + let mut data = Vec::new(); - oc.zcash_serialize(&mut data).expect("WrappedNoteKey should serialize"); + oc.zcash_serialize(&mut data).expect("WrappedNoteKey should serialize"); - let oc2 = WrappedNoteKey::zcash_deserialize(&data[..]).expect("randomized WrappedNoteKey should deserialize"); + let oc2 = WrappedNoteKey::zcash_deserialize(&data[..]).expect("randomized WrappedNoteKey should deserialize"); - prop_assert_eq![oc, oc2]; + prop_assert_eq![oc, oc2]; + } } } diff --git a/zebra-chain/src/orchard/shielded_data.rs b/zebra-chain/src/orchard/shielded_data.rs index fc261718b8c..c1355d5a8c9 100644 --- a/zebra-chain/src/orchard/shielded_data.rs +++ b/zebra-chain/src/orchard/shielded_data.rs @@ -20,9 +20,28 @@ use crate::{ }, }; +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +use crate::orchard_zsa::compute_burn_value_commitment; + +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +use orchard::{note::AssetBase, value::ValueSum}; + +use super::{OrchardVanilla, ShieldedDataFlavor}; + /// A bundle of [`Action`] descriptions and signature data. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] -pub struct ShieldedData { +#[cfg_attr( + not(all(zcash_unstable = "nu7", feature = "tx_v6")), + serde(bound(serialize = "Flavor::EncryptedNote: serde::Serialize")) +)] +#[cfg_attr( + all(zcash_unstable = "nu7", feature = "tx_v6"), + serde(bound( + serialize = "Flavor::EncryptedNote: serde::Serialize, Flavor::BurnType: serde::Serialize", + deserialize = "Flavor::BurnType: serde::Deserialize<'de>" + )) +)] +pub struct ShieldedData { /// The orchard flags for this transaction. /// Denoted as `flagsOrchard` in the spec. pub flags: Flags, @@ -37,13 +56,18 @@ pub struct ShieldedData { pub proof: Halo2Proof, /// The Orchard Actions, in the order they appear in the transaction. /// Denoted as `vActionsOrchard` and `vSpendAuthSigsOrchard` in the spec. - pub actions: AtLeastOne, + pub actions: AtLeastOne>, /// A signature on the transaction `sighash`. /// Denoted as `bindingSigOrchard` in the spec. pub binding_sig: Signature, + + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + /// Assets intended for burning + /// Denoted as `vAssetBurn` in the spec (ZIP 230). + pub burn: Flavor::BurnType, } -impl fmt::Display for ShieldedData { +impl fmt::Display for ShieldedData { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut fmter = f.debug_struct("orchard::ShieldedData"); @@ -59,10 +83,10 @@ impl fmt::Display for ShieldedData { } } -impl ShieldedData { +impl ShieldedData { /// Iterate over the [`Action`]s for the [`AuthorizedAction`]s in this /// transaction, in the order they appear in it. - pub fn actions(&self) -> impl Iterator { + pub fn actions(&self) -> impl Iterator> { self.actions.actions() } @@ -98,10 +122,26 @@ impl ShieldedData { /// pub fn binding_verification_key(&self) -> reddsa::VerificationKeyBytes { let cv: ValueCommitment = self.actions().map(|action| action.cv).sum(); - let cv_balance: ValueCommitment = - ValueCommitment::new(pallas::Scalar::zero(), self.value_balance); - let key_bytes: [u8; 32] = (cv - cv_balance).into(); + #[cfg(not(all(zcash_unstable = "nu7", feature = "tx_v6")))] + let key = { + let cv_balance = ValueCommitment::new(pallas::Scalar::zero(), self.value_balance); + cv - cv_balance + }; + + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + let key = { + let cv_balance = ValueCommitment::new( + pallas::Scalar::zero(), + ValueSum::from_raw(self.value_balance.into()), + AssetBase::zatoshi(), + ); + let burn_value_commitment = compute_burn_value_commitment(self.burn.as_ref()); + cv - cv_balance - burn_value_commitment + }; + + let key_bytes: [u8; 32] = key.into(); + key_bytes.into() } @@ -120,14 +160,19 @@ impl ShieldedData { } /// A trait for types that can provide Orchard actions. -pub trait OrchardActions { +pub trait OrchardActions { /// Returns an iterator over the actions in this type. - fn actions(&self) -> impl Iterator + '_; + fn actions<'a>(&'a self) -> impl Iterator> + 'a + where + Flavor: 'a; } -impl OrchardActions for AtLeastOne { +impl OrchardActions for AtLeastOne> { /// Iterate over the [`Action`]s of each [`AuthorizedAction`]. - fn actions(&self) -> impl Iterator + '_ { + fn actions<'a>(&'a self) -> impl Iterator> + 'a + where + Flavor: 'a, + { self.iter() .map(|authorized_action| &authorized_action.action) } @@ -137,23 +182,82 @@ impl OrchardActions for AtLeastOne { /// /// Every authorized Orchard `Action` must have a corresponding `SpendAuth` signature. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] -pub struct AuthorizedAction { +#[serde(bound = "Flavor::EncryptedNote: serde::Serialize")] +pub struct AuthorizedAction { /// The action description of this Action. - pub action: Action, + pub action: Action, /// The spend signature. pub spend_auth_sig: Signature, } -impl AuthorizedAction { +impl AuthorizedAction { + /// The size of a single Action description. + /// + /// Computed as: + /// ```text + /// 5 × 32 (fields for nullifier, output commitment, etc.) + /// + ENC_CIPHERTEXT_SIZE (580 bytes for OrchardVanilla / Nu5–Nu6, + /// 612 bytes for OrchardZSA / Nu7) + /// + 80 (authentication tag) + /// = 820 bytes (OrchardVanilla) + /// = 852 bytes (OrchardZSA) + /// ``` + /// + /// - For OrchardVanilla (Nu5/Nu6), ENC_CIPHERTEXT_SIZE = 580; see + /// [§ 7.5 Action Description Encoding and Consensus][nu5_pdf] and + /// [ZIP-0225 § “Orchard Action Description”][zip225]. + /// - For OrchardZSA (Nu7), ENC_CIPHERTEXT_SIZE = 612; see + /// [ZIP-0230 § “OrchardZSA Action Description”][zip230]. + /// + /// [nu5_pdf]: https://zips.z.cash/protocol/nu5.pdf#actionencodingandconsen + /// [zip225]: https://zips.z.cash/zip-0225#orchard-action-description-orchardaction + /// [zip230]: https://zips.z.cash/zip-0230#orchardzsa-action-description-orchardzsaaction + pub const ACTION_SIZE: u64 = 5 * 32 + (Flavor::ENC_CIPHERTEXT_SIZE as u64) + 80; + + /// The size of a single `Signature`. + /// + /// Each Signature is 64 bytes. + /// [7.1 Transaction Encoding and Consensus][ps] + /// + /// [ps]: + pub const SPEND_AUTH_SIG_SIZE: u64 = 64; + + /// The size of a single AuthorizedAction + /// + /// Each serialized `Action` has a corresponding `Signature`. + pub const AUTHORIZED_ACTION_SIZE: u64 = Self::ACTION_SIZE + Self::SPEND_AUTH_SIG_SIZE; + + /// The maximum number of actions allowed in a transaction. + /// + /// A serialized `Vec` requires at least one byte for its length, + /// and each action must include a signature. Therefore, the maximum allocation + /// is constrained by these factors and cannot exceed this calculated size. + pub const ACTION_MAX_ALLOCATION: u64 = (MAX_BLOCK_BYTES - 1) / Self::AUTHORIZED_ACTION_SIZE; + + /// Enforce consensus limit at compile time: + /// + /// # Consensus + /// + /// > [NU5 onward] nSpendsSapling, nOutputsSapling, and nActionsOrchard MUST all be less than 2^16. + /// + /// + /// + /// This check works because if `ACTION_MAX_ALLOCATION` were ≥ 2^16, the subtraction below + /// would underflow for `u64`, causing a compile-time error. + const _ACTION_MAX_ALLOCATION_OK: u64 = (1 << 16) - Self::ACTION_MAX_ALLOCATION; + /// Split out the action and the signature for V5 transaction /// serialization. - pub fn into_parts(self) -> (Action, Signature) { + pub fn into_parts(self) -> (Action, Signature) { (self.action, self.spend_auth_sig) } // Combine the action and the spend auth sig from V5 transaction /// deserialization. - pub fn from_parts(action: Action, spend_auth_sig: Signature) -> AuthorizedAction { + pub fn from_parts( + action: Action, + spend_auth_sig: Signature, + ) -> AuthorizedAction { AuthorizedAction { action, spend_auth_sig, @@ -161,56 +265,20 @@ impl AuthorizedAction { } } -/// The size of a single Action -/// -/// Actions are 5 * 32 + 580 + 80 bytes so the total size of each Action is 820 bytes. -/// [7.5 Action Description Encoding and Consensus][ps] -/// -/// [ps]: -pub const ACTION_SIZE: u64 = 5 * 32 + 580 + 80; - -/// The size of a single `Signature`. -/// -/// Each Signature is 64 bytes. -/// [7.1 Transaction Encoding and Consensus][ps] -/// -/// [ps]: -pub const SPEND_AUTH_SIG_SIZE: u64 = 64; - -/// The size of a single AuthorizedAction -/// -/// Each serialized `Action` has a corresponding `Signature`. -pub const AUTHORIZED_ACTION_SIZE: u64 = ACTION_SIZE + SPEND_AUTH_SIG_SIZE; - /// The maximum number of orchard actions in a valid Zcash on-chain transaction V5. /// /// If a transaction contains more actions than can fit in maximally large block, it might be /// valid on the network and in the mempool, but it can never be mined into a block. So /// rejecting these large edge-case transactions can never break consensus. -impl TrustedPreallocate for Action { +impl TrustedPreallocate for Action { fn max_allocation() -> u64 { - // Since a serialized Vec uses at least one byte for its length, - // and the signature is required, - // a valid max allocation can never exceed this size - const MAX: u64 = (MAX_BLOCK_BYTES - 1) / AUTHORIZED_ACTION_SIZE; - // # Consensus - // - // > [NU5 onward] nSpendsSapling, nOutputsSapling, and nActionsOrchard MUST all be less than 2^16. - // - // https://zips.z.cash/protocol/protocol.pdf#txnconsensus - // - // This acts as nActionsOrchard and is therefore subject to the rule. - // The maximum value is actually smaller due to the block size limit, - // but we ensure the 2^16 limit with a static assertion. - static_assertions::const_assert!(MAX < (1 << 16)); - MAX + AuthorizedAction::::ACTION_MAX_ALLOCATION } } impl TrustedPreallocate for Signature { fn max_allocation() -> u64 { - // Each signature must have a corresponding action. - Action::max_allocation() + Action::::max_allocation() } } @@ -223,7 +291,7 @@ bitflags! { /// # Consensus /// /// > [NU5 onward] In a version 5 transaction, the reserved bits 2..7 of the flagsOrchard - /// > field MUST be zero. + /// > field MUST be zero. Bit 2 (ENABLE_ZSA) is introduced in V6 (NU7, ZIP 230). /// /// /// @@ -237,6 +305,9 @@ bitflags! { const ENABLE_SPENDS = 0b00000001; /// Enable creating new non-zero valued Orchard notes. const ENABLE_OUTPUTS = 0b00000010; + /// Enable ZSA transaction (otherwise all notes within actions must use native asset). + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + const ENABLE_ZSA = 0b00000100; } } diff --git a/zebra-chain/src/orchard/shielded_data_flavor.rs b/zebra-chain/src/orchard/shielded_data_flavor.rs new file mode 100644 index 00000000000..4317530aaca --- /dev/null +++ b/zebra-chain/src/orchard/shielded_data_flavor.rs @@ -0,0 +1,75 @@ +//! This module defines traits and structures for supporting the Orchard Shielded Protocol +//! for `V5` and `V6` versions of the transaction. +use std::fmt::Debug; + +use serde::{de::DeserializeOwned, Serialize}; + +use orchard::{flavor::OrchardFlavor, primitives::OrchardPrimitives}; + +pub use orchard::flavor::OrchardVanilla; + +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +pub use orchard::{flavor::OrchardZSA, note::AssetBase, value::NoteValue}; + +use crate::serialization::{ZcashDeserialize, ZcashSerialize}; + +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +use crate::orchard_zsa::{Burn, BurnItem, NoBurn}; + +use super::note; + +// When testing or with the proptest-impl feature, enforce Arbitrary. +#[cfg(any(test, feature = "proptest-impl"))] +mod test_arbitrary { + use proptest::prelude::Arbitrary; + + pub trait TestArbitrary: Arbitrary {} + impl TestArbitrary for T {} +} + +// Otherwise, no extra requirement. +#[cfg(not(any(test, feature = "proptest-impl")))] +mod test_arbitrary { + pub trait TestArbitrary {} + impl TestArbitrary for T {} +} + +/// A trait representing compile-time settings of ShieldedData of Orchard Shielded Protocol +/// used in the transactions `V5` and `V6`. +pub trait ShieldedDataFlavor: OrchardFlavor { + /// A type representing an encrypted note for this protocol version. + type EncryptedNote: Clone + + Debug + + PartialEq + + Eq + + DeserializeOwned + + Serialize + + ZcashDeserialize + + ZcashSerialize + + AsRef<[u8]> + + for<'a> TryFrom<&'a [u8], Error = std::array::TryFromSliceError> + + test_arbitrary::TestArbitrary; + + /// A type representing a burn field for this protocol version. + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + type BurnType: Clone + + Default + + Debug + + ZcashDeserialize + + ZcashSerialize + + AsRef<[BurnItem]> + + for<'a> From<&'a [(AssetBase, NoteValue)]> + + test_arbitrary::TestArbitrary; +} + +impl ShieldedDataFlavor for OrchardVanilla { + type EncryptedNote = note::EncryptedNote<{ OrchardVanilla::ENC_CIPHERTEXT_SIZE }>; + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + type BurnType = NoBurn; +} + +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +impl ShieldedDataFlavor for OrchardZSA { + type EncryptedNote = note::EncryptedNote<{ OrchardZSA::ENC_CIPHERTEXT_SIZE }>; + type BurnType = Burn; +} diff --git a/zebra-chain/src/orchard/tests/preallocate.rs b/zebra-chain/src/orchard/tests/preallocate.rs index 79f6a16e7d9..6b1fadfce29 100644 --- a/zebra-chain/src/orchard/tests/preallocate.rs +++ b/zebra-chain/src/orchard/tests/preallocate.rs @@ -4,10 +4,7 @@ use reddsa::{orchard::SpendAuth, Signature}; use crate::{ block::MAX_BLOCK_BYTES, - orchard::{ - shielded_data::{ACTION_SIZE, AUTHORIZED_ACTION_SIZE}, - Action, AuthorizedAction, - }, + orchard::{Action, AuthorizedAction, OrchardVanilla}, serialization::{arbitrary::max_allocation_is_big_enough, TrustedPreallocate, ZcashSerialize}, }; @@ -17,16 +14,16 @@ proptest! { /// Confirm that each `AuthorizedAction` takes exactly AUTHORIZED_ACTION_SIZE /// bytes when serialized. #[test] - fn authorized_action_size_is_small_enough(authorized_action in ::arbitrary_with(())) { + fn authorized_action_size_is_small_enough(authorized_action in >::arbitrary_with(())) { let (action, spend_auth_sig) = authorized_action.into_parts(); let mut serialized_len = action.zcash_serialize_to_vec().expect("Serialization to vec must succeed").len(); serialized_len += spend_auth_sig.zcash_serialize_to_vec().expect("Serialization to vec must succeed").len(); - prop_assert!(serialized_len as u64 == AUTHORIZED_ACTION_SIZE) + prop_assert!(serialized_len as u64 == AuthorizedAction::::AUTHORIZED_ACTION_SIZE) } /// Verify trusted preallocation for `AuthorizedAction` and its split fields #[test] - fn authorized_action_max_allocation_is_big_enough(authorized_action in ::arbitrary_with(())) { + fn authorized_action_max_allocation_is_big_enough(authorized_action in >::arbitrary_with(())) { let (action, spend_auth_sig) = authorized_action.into_parts(); let ( @@ -37,12 +34,14 @@ proptest! { ) = max_allocation_is_big_enough(action); // Calculate the actual size of all required Action fields - prop_assert!((smallest_disallowed_serialized_len as u64)/ACTION_SIZE*AUTHORIZED_ACTION_SIZE >= MAX_BLOCK_BYTES); - prop_assert!((largest_allowed_serialized_len as u64)/ACTION_SIZE*AUTHORIZED_ACTION_SIZE <= MAX_BLOCK_BYTES); + prop_assert!((smallest_disallowed_serialized_len as u64)/AuthorizedAction::::ACTION_SIZE* + AuthorizedAction::::AUTHORIZED_ACTION_SIZE >= MAX_BLOCK_BYTES); + prop_assert!((largest_allowed_serialized_len as u64)/AuthorizedAction::::ACTION_SIZE* + AuthorizedAction::::AUTHORIZED_ACTION_SIZE <= MAX_BLOCK_BYTES); // Check the serialization limits for `Action` - prop_assert!(((smallest_disallowed_vec_len - 1) as u64) == Action::max_allocation()); - prop_assert!((largest_allowed_vec_len as u64) == Action::max_allocation()); + prop_assert!(((smallest_disallowed_vec_len - 1) as u64) == Action::::max_allocation()); + prop_assert!((largest_allowed_vec_len as u64) == Action::::max_allocation()); prop_assert!((largest_allowed_serialized_len as u64) <= MAX_BLOCK_BYTES); let ( diff --git a/zebra-chain/src/orchard_zsa.rs b/zebra-chain/src/orchard_zsa.rs new file mode 100644 index 00000000000..a8ea0f5924f --- /dev/null +++ b/zebra-chain/src/orchard_zsa.rs @@ -0,0 +1,18 @@ +//! OrchardZSA related functionality. + +#[cfg(any(test, feature = "proptest-impl"))] +mod arbitrary; + +mod asset_state; +mod burn; +mod issuance; + +pub(crate) use burn::{compute_burn_value_commitment, Burn, NoBurn}; +pub(crate) use issuance::IssueData; + +pub use burn::BurnItem; + +pub use asset_state::{AssetBase, AssetState, AssetStateError, IssuedAssetChanges}; + +#[cfg(any(test, feature = "proptest-impl"))] +pub use asset_state::testing::{mock_asset_base, mock_asset_state}; diff --git a/zebra-chain/src/orchard_zsa/arbitrary.rs b/zebra-chain/src/orchard_zsa/arbitrary.rs new file mode 100644 index 00000000000..7e68d40248b --- /dev/null +++ b/zebra-chain/src/orchard_zsa/arbitrary.rs @@ -0,0 +1,58 @@ +//! Randomised data generation for OrchardZSA types. + +use proptest::prelude::*; + +use orchard::{bundle::testing::BundleArb, issuance::testing::arb_signed_issue_bundle}; + +use crate::transaction::arbitrary::MAX_ARBITRARY_ITEMS; + +use super::{ + burn::{Burn, BurnItem, NoBurn}, + issuance::IssueData, +}; + +impl Arbitrary for BurnItem { + type Parameters = (); + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + BundleArb::::arb_asset_to_burn() + .prop_map(|(asset_base, value)| BurnItem::from((asset_base, value))) + .boxed() + } + + type Strategy = BoxedStrategy; +} + +impl Arbitrary for NoBurn { + type Parameters = (); + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + Just(Self).boxed() + } + + type Strategy = BoxedStrategy; +} + +impl Arbitrary for Burn { + type Parameters = (); + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + prop::collection::vec(any::(), 0..MAX_ARBITRARY_ITEMS) + .prop_map(|inner| inner.into()) + .boxed() + } + + type Strategy = BoxedStrategy; +} + +impl Arbitrary for IssueData { + type Parameters = (); + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + arb_signed_issue_bundle(MAX_ARBITRARY_ITEMS) + .prop_map(|bundle| bundle.into()) + .boxed() + } + + type Strategy = BoxedStrategy; +} diff --git a/zebra-chain/src/orchard_zsa/asset_state.rs b/zebra-chain/src/orchard_zsa/asset_state.rs new file mode 100644 index 00000000000..688dbd90836 --- /dev/null +++ b/zebra-chain/src/orchard_zsa/asset_state.rs @@ -0,0 +1,424 @@ +//! Defines and implements the issued asset state types + +use byteorder::{ReadBytesExt, WriteBytesExt}; +use std::{ + collections::{BTreeMap, HashMap}, + io, + sync::Arc, +}; +use thiserror::Error; + +pub use orchard::note::AssetBase; +use orchard::{ + bundle::burn_validation::{validate_bundle_burn, BurnError}, + issuance::{ + check_issue_bundle_without_sighash, verify_issue_bundle, AssetRecord, Error as IssueError, + }, + note::Nullifier, + value::NoteValue, + Note, +}; + +use zcash_primitives::transaction::components::issuance::{read_note, write_note}; + +use crate::transaction::{SigHash, Transaction}; + +/// Wraps orchard's AssetRecord for use in zebra state management. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AssetState(AssetRecord); + +impl AssetState { + /// Creates a new [`AssetRecord`] instance. + pub fn new(amount: NoteValue, is_finalized: bool, reference_note: Note) -> Self { + Self(AssetRecord::new(amount, is_finalized, reference_note)) + } + + /// Deserializes a new [`AssetState`] from its canonical byte encoding. + pub fn from_bytes(bytes: &[u8]) -> Result { + use std::io::{Cursor, Read}; + + let mut reader = Cursor::new(bytes); + let mut amount_bytes = [0; 8]; + reader.read_exact(&mut amount_bytes)?; + + let is_finalized = match reader.read_u8()? { + 0 => false, + 1 => true, + _ => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Invalid is_finalized", + )) + } + }; + + let mut asset_bytes = [0u8; 32]; + reader.read_exact(&mut asset_bytes)?; + let asset = Option::from(AssetBase::from_bytes(&asset_bytes)) + .ok_or(io::Error::new(io::ErrorKind::InvalidData, "Invalid asset"))?; + + let reference_note = read_note(reader, asset)?; + + Ok(AssetState(AssetRecord::new( + NoteValue::from_bytes(amount_bytes), + is_finalized, + reference_note, + ))) + } + + /// Serializes [`AssetState`] to its canonical byte encoding. + pub fn to_bytes(&self) -> Result, io::Error> { + use std::io::Write; + + let mut bytes = Vec::new(); + bytes.write_all(&self.0.amount.to_bytes())?; + bytes.write_u8(self.0.is_finalized as u8)?; + bytes.write_all(&self.0.reference_note.asset().to_bytes())?; + write_note(&mut bytes, &self.0.reference_note)?; + Ok(bytes) + } + + /// Returns whether the asset is finalized. + #[cfg(any(test, feature = "proptest-impl"))] + pub fn is_finalized(&self) -> bool { + self.0.is_finalized + } + + /// Returns the total supply. + #[cfg(any(test, feature = "proptest-impl"))] + pub fn total_supply(&self) -> u64 { + self.0.amount.inner() + } +} + +impl From for AssetState { + fn from(record: AssetRecord) -> Self { + Self(record) + } +} + +// Needed for the new `getassetstate` RPC endpoint in `zebra-rpc`. +// Can't derive `Serialize` here as `orchard::AssetRecord` doesn't implement it. +impl serde::Serialize for AssetState { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::{Error as _, SerializeStruct}; + + // "3" is the expected number of struct fields (a hint for pre-allocation). + let mut st = serializer.serialize_struct("AssetState", 3)?; + + let inner = &self.0; + st.serialize_field("amount", &inner.amount.inner())?; + st.serialize_field("is_finalized", &inner.is_finalized)?; + + let mut note_bytes = Vec::::new(); + write_note(&mut note_bytes, &inner.reference_note).map_err(S::Error::custom)?; + st.serialize_field("reference_note", &hex::encode(note_bytes))?; + + st.end() + } +} + +/// Errors returned when validating asset state updates. +#[derive(Debug, Error, Clone, PartialEq, Eq)] +#[allow(missing_docs)] +pub enum AssetStateError { + #[error("issuance validation failed: {0}")] + Issue(IssueError), + + #[error("burn validation failed: {0}")] + Burn(BurnError), + + #[error("invalid input: {0}")] + InvalidInput(String), +} + +/// A map of asset state changes for assets modified in a block or transaction set. +/// Contains `(old_state, new_state)` pairs for each modified asset. +#[derive(Clone, Debug, PartialEq, Eq, Default)] +pub struct IssuedAssetChanges(HashMap, AssetState)>); + +/// Apply validator output to the mutable state map. +fn apply_updates( + states: &mut HashMap, AssetState)>, + updates: BTreeMap, +) { + use std::collections::hash_map::Entry; + + for (asset, record) in updates { + match states.entry(asset) { + Entry::Occupied(mut entry) => entry.get_mut().1 = AssetState::from(record), + Entry::Vacant(entry) => { + entry.insert((None, AssetState::from(record))); + } + } + } +} + +impl IssuedAssetChanges { + /// Validates burns and issuances across transactions, returning the map of changes. + /// + /// # Signature Verification Modes + /// + /// - **With `transaction_sighashes` (Some)**: Full validation for Contextually Verified Blocks + /// from the consensus workflow. Performs signature verification using `verify_issue_bundle`. + /// + /// - **Without `transaction_sighashes` (None)**: Trusted validation for Checkpoint Verified Blocks + /// loaded during bootstrap/startup from disk. These blocks are within checkpoint ranges and + /// are considered trusted, so signature verification is skipped using `check_issue_bundle_without_sighash`. + #[allow(clippy::unwrap_in_result)] + pub fn validate_and_get_changes( + transactions: &[Arc], + transaction_sighashes: Option<&[SigHash]>, + get_state: impl Fn(&AssetBase) -> Option, + ) -> Result { + if let Some(sighashes) = transaction_sighashes { + if transactions.len() != sighashes.len() { + return Err(AssetStateError::InvalidInput(format!( + "transaction count ({}) does not match sighash count ({})", + transactions.len(), + sighashes.len(), + ))); + } + } + + // Track old and current states - old_state is None for newly created assets + let mut states = HashMap::, AssetState)>::new(); + + for (i, tx) in transactions.iter().enumerate() { + // Validate and apply burns + if let Some(burn) = tx.orchard_burns() { + let burn_records = validate_bundle_burn( + burn.iter() + .map(|burn_item| <(AssetBase, NoteValue)>::from(*burn_item)), + |asset| Self::get_or_cache_record(&mut states, asset, &get_state), + ) + .map_err(AssetStateError::Burn)?; + apply_updates(&mut states, burn_records); + } + + // Validate and apply issuances + if let Some(issue_data) = tx.orchard_zsa_issue_data() { + // ZIP-0227 defines issued-note rho as DeriveIssuedRho(nf_{0,0}, i_action, i_note), + // so we must pass the first Action nullifier (nf_{0,0}). We rely on + // `orchard_nullifiers()` preserving Action order, so `.next()` returns nf_{0,0}. + // Nullifier type conversion via bytes: both types wrap pallas::Point + // but lack a direct conversion path in the current orchard API. + let raw_nullifier = tx.orchard_nullifiers().next().ok_or_else(|| { + AssetStateError::InvalidInput( + "issuance bundle has no orchard actions".to_string(), + ) + })?; + let first_nullifier = &Nullifier::from_bytes(&<[u8; 32]>::from(*raw_nullifier)) + .expect("valid zebra nullifier bytes convert to orchard nullifier"); + + let issue_records = match transaction_sighashes { + Some(sighashes) => { + // Full verification with signature check (Contextually Verified Block) + verify_issue_bundle( + issue_data.inner(), + *sighashes[i].as_ref(), + |asset| Self::get_or_cache_record(&mut states, asset, &get_state), + first_nullifier, + ) + .map_err(AssetStateError::Issue)? + } + None => { + // Trusted verification without signature check (Checkpoint Verified Block) + check_issue_bundle_without_sighash( + issue_data.inner(), + |asset| Self::get_or_cache_record(&mut states, asset, &get_state), + first_nullifier, + ) + .map_err(AssetStateError::Issue)? + } + }; + + apply_updates(&mut states, issue_records); + } + } + + Ok(IssuedAssetChanges(states)) + } + + /// Gets current record from cache or fetches and caches it. + fn get_or_cache_record( + states: &mut HashMap, AssetState)>, + asset: &AssetBase, + get_state: &impl Fn(&AssetBase) -> Option, + ) -> Option { + use std::collections::hash_map::Entry; + + match states.entry(*asset) { + Entry::Occupied(entry) => Some(entry.get().1 .0), + Entry::Vacant(entry) => { + let state = get_state(asset)?; + entry.insert((Some(state), state)); + Some(state.0) + } + } + } + + /// Gets an iterator over `IssuedAssetChanges` inner `HashMap` elements. + pub fn iter(&self) -> impl Iterator, AssetState))> { + self.0.iter() + } +} + +impl From> for IssuedAssetChanges { + fn from(issued: HashMap) -> Self { + IssuedAssetChanges( + issued + .into_iter() + .map(|(base, state)| (base, (None, state))) + .collect(), + ) + } +} + +#[cfg(any(test, feature = "proptest-impl"))] +/// Test utilities for creating mock asset states and bases, used in zebra-rpc tests. +pub mod testing { + use super::AssetState; + + use orchard::{ + issuance::{ + auth::{IssueAuthKey, IssueValidatingKey, ZSASchnorr}, + compute_asset_desc_hash, IssueBundle, + }, + note::{AssetBase, AssetId, Nullifier}, + value::NoteValue, + }; + + use group::{ff::PrimeField, Curve, Group}; + use halo2::{arithmetic::CurveAffine, pasta::pallas}; + use rand::{RngCore, SeedableRng}; + use rand_chacha::ChaChaRng; + + const TEST_RNG_SEED: u64 = 0; + + fn hash_asset_desc(desc: &[u8]) -> [u8; 32] { + let (first, rest) = desc + .split_first() + .expect("asset description must be non-empty"); + compute_asset_desc_hash(&(*first, rest.to_vec()).into()) + } + + fn random_bytes(rng: &mut impl RngCore) -> [u8; N] { + let mut bytes = [0u8; N]; + rng.fill_bytes(&mut bytes); + bytes + } + + // Coordinate extractor for Pallas (nu5.pdf, § 5.4.9.7), used to create a nullifier. + fn extract_p(point: &pallas::Point) -> pallas::Base { + point + .to_affine() + .coordinates() + .map(|c| *c.x()) + .unwrap_or_else(pallas::Base::zero) + } + + fn dummy_nullifier(rng: impl RngCore) -> Nullifier { + Nullifier::from_bytes(&extract_p(&pallas::Point::random(rng)).to_repr()) + .expect("pallas x-coordinate is a valid nullifier") + } + + fn create_issue_keys( + rng: &mut (impl RngCore + rand::CryptoRng), + ) -> (IssueAuthKey, IssueValidatingKey) { + let isk = IssueAuthKey::::random(rng); + let ik = IssueValidatingKey::::from(&isk); + (isk, ik) + } + + // Creates a reference note whose `rho` is set, making it serializable via `AssetState::to_bytes`. + fn create_reference_note_with_rho( + asset_desc: &[u8], + rng: &mut (impl RngCore + rand::CryptoRng), + ) -> orchard::Note { + let (isk, ik) = create_issue_keys(&mut *rng); + let desc_hash = hash_asset_desc(asset_desc); + + let sighash = random_bytes::<32>(rng); + let first_nullifier = dummy_nullifier(&mut *rng); + let (bundle, _) = IssueBundle::new(ik, desc_hash, None, true, &mut *rng); + + let signed_bundle = bundle + .update_rho(&first_nullifier, rng) + .prepare(sighash) + .sign(&isk) + .expect("signing a freshly-created bundle must succeed"); + + *signed_bundle + .actions() + .first() + .get_reference_note() + .expect("first action of IssueBundle always has a reference note") + } + + /// Returns a deterministic [`AssetBase`] for the given description. + pub fn mock_asset_base(desc: &[u8]) -> AssetBase { + let mut rng = ChaChaRng::seed_from_u64(TEST_RNG_SEED); + let (_, ik) = create_issue_keys(&mut rng); + AssetBase::custom(&AssetId::new_v0(&ik, &hash_asset_desc(desc))) + } + + /// Returns a deterministic [`AssetState`] for use in tests. + pub fn mock_asset_state( + asset_desc: &[u8], + total_supply: u64, + is_finalized: bool, + ) -> AssetState { + let mut rng = ChaChaRng::seed_from_u64(TEST_RNG_SEED); + let reference_note = create_reference_note_with_rho(asset_desc, &mut rng); + AssetState::new( + NoteValue::from_bytes(total_supply.to_le_bytes()), + is_finalized, + reference_note, + ) + } +} + +#[cfg(test)] +mod tests { + use super::{testing::mock_asset_state, *}; + + #[test] + fn asset_state_roundtrip_serialization() { + let state = mock_asset_state(b"test_asset", 1000, false); + + let bytes = state.to_bytes().unwrap(); + let decoded = AssetState::from_bytes(&bytes).unwrap(); + + assert_eq!(state, decoded); + } + + #[test] + fn asset_state_finalized_roundtrip() { + let state = mock_asset_state(b"finalized", 5000, true); + + let bytes = state.to_bytes().unwrap(); + let decoded = AssetState::from_bytes(&bytes).unwrap(); + + assert!(decoded.is_finalized()); + assert_eq!(decoded.total_supply(), 5000); + } + + #[test] + fn read_asset_state_invalid_finalized_byte() { + let mut bytes = vec![0u8; 8]; // amount + bytes.push(2); // invalid is_finalized (not 0 or 1) + + let result = AssetState::from_bytes(&bytes); + assert!(result.is_err()); + } + + #[test] + fn issued_asset_changes_empty() { + let changes = IssuedAssetChanges::default(); + assert_eq!(changes.iter().count(), 0); + } +} diff --git a/zebra-chain/src/orchard_zsa/burn.rs b/zebra-chain/src/orchard_zsa/burn.rs new file mode 100644 index 00000000000..68916afac92 --- /dev/null +++ b/zebra-chain/src/orchard_zsa/burn.rs @@ -0,0 +1,171 @@ +//! OrchardZSA burn related functionality. + +use std::io; + +use halo2::pasta::pallas; + +use orchard::{note::AssetBase, value::NoteValue}; + +use zcash_primitives::transaction::components::orchard::{read_burn, write_burn}; + +use crate::{ + orchard::ValueCommitment, + serialization::{SerializationError, ZcashDeserialize, ZcashSerialize}, +}; + +/// OrchardZSA burn item. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct BurnItem(AssetBase, NoteValue); + +impl BurnItem { + /// Returns [`AssetBase`] being burned. + pub fn asset(&self) -> AssetBase { + self.0 + } + + /// Returns the amount being burned. + pub fn amount(&self) -> NoteValue { + self.1 + } + + /// Returns the raw [`u64`] amount being burned. + pub fn raw_amount(&self) -> u64 { + self.1.inner() + } +} + +// Convert from burn item type used in `orchard` crate +impl From<(AssetBase, NoteValue)> for BurnItem { + fn from(item: (AssetBase, NoteValue)) -> Self { + Self(item.0, item.1) + } +} + +// Convert to burn item type used in `orchard` crate +impl From for (AssetBase, NoteValue) { + fn from(item: BurnItem) -> Self { + (item.0, item.1) + } +} + +impl serde::Serialize for BurnItem { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + (self.0.to_bytes(), &self.1.inner()).serialize(serializer) + } +} + +impl<'de> serde::Deserialize<'de> for BurnItem { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let (asset_base_bytes, amount) = <([u8; 32], u64)>::deserialize(deserializer)?; + Ok(BurnItem( + Option::from(AssetBase::from_bytes(&asset_base_bytes)) + .ok_or_else(|| serde::de::Error::custom("Invalid orchard_zsa AssetBase"))?, + NoteValue::from_raw(amount), + )) + } +} + +/// A special marker type indicating the absence of a burn field in Orchard ShieldedData for `V5` +/// transactions. It is unifying handling and serialization of ShieldedData across various Orchard +/// protocol variants. +#[derive(Default, Clone, Debug, PartialEq, Eq, Serialize)] +pub struct NoBurn; + +impl From<&[(AssetBase, NoteValue)]> for NoBurn { + fn from(bundle_burn: &[(AssetBase, NoteValue)]) -> Self { + assert!( + bundle_burn.is_empty(), + "Burn must be empty for OrchardVanilla" + ); + Self + } +} + +impl AsRef<[BurnItem]> for NoBurn { + fn as_ref(&self) -> &[BurnItem] { + &[] + } +} + +impl ZcashSerialize for NoBurn { + fn zcash_serialize(&self, mut _writer: W) -> Result<(), io::Error> { + Ok(()) + } +} + +impl ZcashDeserialize for NoBurn { + fn zcash_deserialize(mut _reader: R) -> Result { + Ok(Self) + } +} + +/// OrchardZSA burn items. +#[derive(Default, Clone, Debug, PartialEq, Eq, Serialize)] +pub struct Burn(Vec); + +impl From> for Burn { + fn from(inner: Vec) -> Self { + Self(inner) + } +} + +impl From<&[(AssetBase, NoteValue)]> for Burn { + fn from(bundle_burn: &[(AssetBase, NoteValue)]) -> Self { + Self( + bundle_burn + .iter() + .map(|bundle_burn_item| BurnItem::from(*bundle_burn_item)) + .collect(), + ) + } +} + +impl AsRef<[BurnItem]> for Burn { + fn as_ref(&self) -> &[BurnItem] { + &self.0 + } +} + +impl ZcashSerialize for Burn { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + write_burn( + &mut writer, + &self.0.iter().map(|item| (*item).into()).collect::>(), + ) + } +} + +impl ZcashDeserialize for Burn { + fn zcash_deserialize(mut reader: R) -> Result { + Ok(Burn( + read_burn(&mut reader)? + .into_iter() + .map(|item| item.into()) + .collect(), + )) + } +} + +/// Computes the value commitment for a list of burns. +/// +/// For burns, the public trapdoor is always zero. +pub(crate) fn compute_burn_value_commitment(burn: &[BurnItem]) -> ValueCommitment { + burn.iter() + .map(|&BurnItem(asset, amount)| { + ValueCommitment::new( + pallas::Scalar::zero(), + // TODO: `amount - NoteValue::from_raw(0)` is an obscure way to widen + // `NoteValue` into `ValueSum`. Replace with a direct/explicit conversion + // once the upstream Orchard API exposes one. + amount - NoteValue::from_raw(0), + asset, + ) + }) + .sum() +} diff --git a/zebra-chain/src/orchard_zsa/issuance.rs b/zebra-chain/src/orchard_zsa/issuance.rs new file mode 100644 index 00000000000..41701a0f625 --- /dev/null +++ b/zebra-chain/src/orchard_zsa/issuance.rs @@ -0,0 +1,59 @@ +//! OrchardZSA issuance related functionality. + +use std::{fmt::Debug, io}; + +use halo2::pasta::pallas; + +use orchard::issuance::{IssueAction, IssueBundle, Signed}; + +use zcash_primitives::transaction::components::issuance::{read_bundle, write_bundle}; + +use crate::serialization::{SerializationError, ZcashDeserialize, ZcashSerialize}; + +/// Wrapper for `IssueBundle` used in the context of Transaction V6. This allows the implementation of +/// a Serde serializer for unit tests within this crate. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct IssueData(IssueBundle); + +impl IssueData { + /// Returns a reference to the inner `IssueBundle`. + pub fn inner(&self) -> &IssueBundle { + &self.0 + } +} + +impl From> for IssueData { + fn from(inner: IssueBundle) -> Self { + Self(inner) + } +} + +impl IssueData { + pub(crate) fn note_commitments(&self) -> impl Iterator + '_ { + self.0.note_commitments() + } + + /// Returns issuance actions + pub fn actions(&self) -> impl Iterator { + self.0.actions().iter() + } +} + +impl ZcashSerialize for Option { + fn zcash_serialize(&self, writer: W) -> Result<(), io::Error> { + write_bundle(self.as_ref().map(|issue_data| &issue_data.0), writer) + } +} + +impl ZcashDeserialize for Option { + fn zcash_deserialize(reader: R) -> Result { + Ok(read_bundle(reader)?.map(IssueData)) + } +} + +#[cfg(any(test, feature = "proptest-impl", feature = "elasticsearch"))] +impl serde::Serialize for IssueData { + fn serialize(&self, _serializer: S) -> Result { + unimplemented!("Serde serialization for IssueData functionality is not needed for Zebra"); + } +} diff --git a/zebra-chain/src/parallel/tree.rs b/zebra-chain/src/parallel/tree.rs index 94cbc7d9bc3..84506894815 100644 --- a/zebra-chain/src/parallel/tree.rs +++ b/zebra-chain/src/parallel/tree.rs @@ -73,7 +73,7 @@ impl NoteCommitmentTrees { let sprout_note_commitments: Vec<_> = block.sprout_note_commitments().cloned().collect(); let sapling_note_commitments: Vec<_> = block.sapling_note_commitments().cloned().collect(); - let orchard_note_commitments: Vec<_> = block.orchard_note_commitments().cloned().collect(); + let orchard_note_commitments: Vec<_> = block.orchard_note_commitments().collect(); let mut sprout_result = None; let mut sapling_result = None; diff --git a/zebra-chain/src/parameters/network.rs b/zebra-chain/src/parameters/network.rs index 4fd856ad2c0..475094d4a03 100644 --- a/zebra-chain/src/parameters/network.rs +++ b/zebra-chain/src/parameters/network.rs @@ -66,9 +66,9 @@ impl NetworkKind { /// pay-to-public-key-hash payment addresses for the network. pub fn b58_pubkey_address_prefix(self) -> [u8; 2] { match self { - Self::Mainnet => zcash_primitives::constants::mainnet::B58_PUBKEY_ADDRESS_PREFIX, + Self::Mainnet => zcash_protocol::constants::mainnet::B58_PUBKEY_ADDRESS_PREFIX, Self::Testnet | Self::Regtest => { - zcash_primitives::constants::testnet::B58_PUBKEY_ADDRESS_PREFIX + zcash_protocol::constants::testnet::B58_PUBKEY_ADDRESS_PREFIX } } } @@ -77,9 +77,9 @@ impl NetworkKind { /// payment addresses for the network. pub fn b58_script_address_prefix(self) -> [u8; 2] { match self { - Self::Mainnet => zcash_primitives::constants::mainnet::B58_SCRIPT_ADDRESS_PREFIX, + Self::Mainnet => zcash_protocol::constants::mainnet::B58_SCRIPT_ADDRESS_PREFIX, Self::Testnet | Self::Regtest => { - zcash_primitives::constants::testnet::B58_SCRIPT_ADDRESS_PREFIX + zcash_protocol::constants::testnet::B58_SCRIPT_ADDRESS_PREFIX } } } diff --git a/zebra-chain/src/parameters/network_upgrade.rs b/zebra-chain/src/parameters/network_upgrade.rs index 3111eaa7359..4b24bde5bb8 100644 --- a/zebra-chain/src/parameters/network_upgrade.rs +++ b/zebra-chain/src/parameters/network_upgrade.rs @@ -198,11 +198,11 @@ impl fmt::Display for ConsensusBranchId { } } -impl TryFrom for zcash_primitives::consensus::BranchId { +impl TryFrom for zcash_protocol::consensus::BranchId { type Error = crate::Error; fn try_from(id: ConsensusBranchId) -> Result { - zcash_primitives::consensus::BranchId::try_from(u32::from(id)) + zcash_protocol::consensus::BranchId::try_from(u32::from(id)) .map_err(|_| Self::Error::InvalidConsensusBranchId) } } @@ -226,9 +226,9 @@ pub(crate) const CONSENSUS_BRANCH_IDS: &[(NetworkUpgrade, ConsensusBranchId)] = (Nu5, ConsensusBranchId(0xc2d6d0b4)), (Nu6, ConsensusBranchId(0xc8e71055)), (Nu6_1, ConsensusBranchId(0x4dec4df0)), - // TODO: set below to (Nu7, ConsensusBranchId(0x77190ad8)), once the same value is set in librustzcash - #[cfg(any(test, feature = "zebra-test"))] - (Nu7, ConsensusBranchId(0xffffffff)), + // Registered unconditionally so chain history works in builds without + // `zcash_unstable="nu7"`; Nu7-specific code paths remain cfg-gated elsewhere. + (Nu7, ConsensusBranchId(0x77190ad8)), #[cfg(zcash_unstable = "zfuture")] (ZFuture, ConsensusBranchId(0xffffffff)), ]; @@ -520,7 +520,7 @@ impl From for NetworkUpgrade { zcash_protocol::consensus::NetworkUpgrade::Nu5 => Self::Nu5, zcash_protocol::consensus::NetworkUpgrade::Nu6 => Self::Nu6, zcash_protocol::consensus::NetworkUpgrade::Nu6_1 => Self::Nu6_1, - #[cfg(zcash_unstable = "nu7")] + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] zcash_protocol::consensus::NetworkUpgrade::Nu7 => Self::Nu7, #[cfg(zcash_unstable = "zfuture")] zcash_protocol::consensus::NetworkUpgrade::ZFuture => Self::ZFuture, diff --git a/zebra-chain/src/parameters/transaction.rs b/zebra-chain/src/parameters/transaction.rs index 621355abb04..fbdcadb47ec 100644 --- a/zebra-chain/src/parameters/transaction.rs +++ b/zebra-chain/src/parameters/transaction.rs @@ -14,4 +14,5 @@ pub const TX_V5_VERSION_GROUP_ID: u32 = 0x26A7_270A; /// The version group ID for version 6 transactions. /// TODO: update this after it's chosen -pub const TX_V6_VERSION_GROUP_ID: u32 = 0xFFFF_FFFF; +/// FIMXE: upstream Zebra uses 0xFFFF_FFFF +pub const TX_V6_VERSION_GROUP_ID: u32 = 0x7777_7777; diff --git a/zebra-chain/src/primitives/address.rs b/zebra-chain/src/primitives/address.rs index c0052f45f45..aa7023c5771 100644 --- a/zebra-chain/src/primitives/address.rs +++ b/zebra-chain/src/primitives/address.rs @@ -3,7 +3,7 @@ //! Usage: use zcash_address::unified::{self, Container}; -use zcash_primitives::consensus::NetworkType; +use zcash_protocol::consensus::NetworkType; use crate::{parameters::NetworkKind, transparent, BoxError}; diff --git a/zebra-chain/src/primitives/zcash_note_encryption.rs b/zebra-chain/src/primitives/zcash_note_encryption.rs index ae802beb0f7..c4055d82187 100644 --- a/zebra-chain/src/primitives/zcash_note_encryption.rs +++ b/zebra-chain/src/primitives/zcash_note_encryption.rs @@ -7,7 +7,14 @@ use crate::{ transaction::Transaction, }; -/// Returns true if all Sapling or Orchard outputs, if any, decrypt successfully with +use orchard::{ + bundle::{Authorization, Bundle}, + primitives::OrchardPrimitives, +}; + +use zcash_primitives::transaction::OrchardBundle; + +/// Returns true if **all** Sapling or Orchard outputs decrypt successfully with /// an all-zeroes outgoing viewing key. pub fn decrypts_successfully(tx: &Transaction, network: &Network, height: Height) -> bool { let nu = NetworkUpgrade::current(network, height); @@ -40,20 +47,32 @@ pub fn decrypts_successfully(tx: &Transaction, network: &Network, height: Height } if let Some(bundle) = tx.orchard_bundle() { - for act in bundle.actions() { - if zcash_note_encryption::try_output_recovery_with_ovk( - &orchard::note_encryption::OrchardDomain::for_action(act), - &orchard::keys::OutgoingViewingKey::from([0u8; 32]), - act, - act.cv_net(), - &act.encrypted_note().out_ciphertext, - ) - .is_none() - { - return false; - } + let is_decrypted_successfully = match bundle { + OrchardBundle::OrchardVanilla(bundle) => orchard_bundle_decrypts_successfully(bundle), + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + OrchardBundle::OrchardZSA(bundle) => orchard_bundle_decrypts_successfully(bundle), + }; + + if !is_decrypted_successfully { + return false; } } true } + +/// Checks if all actions in an Orchard bundle decrypt successfully. +fn orchard_bundle_decrypts_successfully( + bundle: &Bundle, +) -> bool { + bundle.actions().iter().all(|act| { + zcash_note_encryption::try_output_recovery_with_ovk( + &orchard::primitives::OrchardDomain::for_action(act), + &orchard::keys::OutgoingViewingKey::from([0u8; 32]), + act, + act.cv_net(), + &act.encrypted_note().out_ciphertext, + ) + .is_some() + }) +} diff --git a/zebra-chain/src/primitives/zcash_primitives.rs b/zebra-chain/src/primitives/zcash_primitives.rs index 27314292ae2..48282ea8ee2 100644 --- a/zebra-chain/src/primitives/zcash_primitives.rs +++ b/zebra-chain/src/primitives/zcash_primitives.rs @@ -135,6 +135,15 @@ impl zp_tx::components::orchard::MapAuth + for IdentityMap +{ + fn map_issue_authorization(&self, s: orchard::issuance::Signed) -> orchard::issuance::Signed { + s + } +} + #[derive(Debug)] struct PrecomputedAuth {} @@ -143,6 +152,9 @@ impl zp_tx::Authorization for PrecomputedAuth { type SaplingAuth = sapling_crypto::bundle::Authorized; type OrchardAuth = orchard::bundle::Authorized; + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + type IssueAuth = orchard::issuance::Signed; + #[cfg(zcash_unstable = "zfuture")] type TzeAuth = zp_tx::components::tze::Authorized; } @@ -268,6 +280,8 @@ impl PrecomputedTxData { f_transparent, IdentityMap, IdentityMap, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + IdentityMap, #[cfg(zcash_unstable = "zfuture")] (), ); @@ -282,7 +296,7 @@ impl PrecomputedTxData { /// Returns the Orchard bundle in `tx_data`. pub fn orchard_bundle( &self, - ) -> Option> { + ) -> Option> { self.tx_data.orchard_bundle().cloned() } diff --git a/zebra-chain/src/tests/vectors.rs b/zebra-chain/src/tests/vectors.rs index f5029c9805d..f72d2e5a8f4 100644 --- a/zebra-chain/src/tests/vectors.rs +++ b/zebra-chain/src/tests/vectors.rs @@ -8,7 +8,7 @@ use crate::{ block::Block, parameters::Network, serialization::ZcashDeserializeInto, - transaction::{UnminedTx, VerifiedUnminedTx}, + transaction::{SigHash, UnminedTx, VerifiedUnminedTx}, }; use zebra_test::vectors::{ @@ -75,6 +75,7 @@ impl Network { Amount::try_from(1_000_000).expect("valid amount"), 0, std::sync::Arc::new(vec![]), + SigHash([0; 32]), ) .ok() }) diff --git a/zebra-chain/src/transaction.rs b/zebra-chain/src/transaction.rs index 6e6b9c240b0..2db214de532 100644 --- a/zebra-chain/src/transaction.rs +++ b/zebra-chain/src/transaction.rs @@ -60,6 +60,12 @@ use crate::{ Error, }; +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +use crate::orchard_zsa; + +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +pub mod versioned_sig; + /// A Zcash transaction. /// /// A transaction is an encoded data structure that facilitates the transfer of @@ -147,9 +153,10 @@ pub enum Transaction { /// The sapling shielded data for this transaction, if any. sapling_shielded_data: Option>, /// The orchard data for this transaction, if any. - orchard_shielded_data: Option, + orchard_shielded_data: Option>, }, /// A `version = 6` transaction, which is reserved for current development. + // TODO: Add ZIP-230 fee support once our librustzcash/orchard dependencies support it. #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] V6 { /// The Network Upgrade for this transaction. @@ -170,7 +177,9 @@ pub enum Transaction { /// The sapling shielded data for this transaction, if any. sapling_shielded_data: Option>, /// The orchard data for this transaction, if any. - orchard_shielded_data: Option, + orchard_shielded_data: Option>, + /// The OrchardZSA issuance data for this transaction, if any. + orchard_zsa_issue_data: Option, }, } @@ -199,7 +208,7 @@ impl fmt::Display for Transaction { fmter.field("sprout_joinsplits", &self.joinsplit_count()); fmter.field("sapling_spends", &self.sapling_spends_per_anchor().count()); fmter.field("sapling_outputs", &self.sapling_outputs().count()); - fmter.field("orchard_actions", &self.orchard_actions().count()); + fmter.field("orchard_actions", &self.orchard_action_count()); fmter.field("unmined_id", &self.unmined_id()); @@ -317,7 +326,7 @@ impl Transaction { pub fn has_shielded_inputs(&self) -> bool { self.joinsplit_count() > 0 || self.sapling_spends_per_anchor().count() > 0 - || (self.orchard_actions().count() > 0 + || (self.orchard_action_count() > 0 && self .orchard_flags() .unwrap_or_else(orchard::Flags::empty) @@ -335,7 +344,7 @@ impl Transaction { pub fn has_shielded_outputs(&self) -> bool { self.joinsplit_count() > 0 || self.sapling_outputs().count() > 0 - || (self.orchard_actions().count() > 0 + || (self.orchard_action_count() > 0 && self .orchard_flags() .unwrap_or_else(orchard::Flags::empty) @@ -349,7 +358,7 @@ impl Transaction { /// Does this transaction has at least one flag when we have at least one orchard action? pub fn has_enough_orchard_flags(&self) -> bool { - if self.version() < 5 || self.orchard_actions().count() == 0 { + if self.version() < 5 || self.orchard_action_count() == 0 { return true; } self.orchard_flags() @@ -1066,64 +1075,188 @@ impl Transaction { // orchard - /// Access the [`orchard::ShieldedData`] in this transaction, - /// regardless of version. - pub fn orchard_shielded_data(&self) -> Option<&orchard::ShieldedData> { + /// Iterate over the [`orchard::Action`]s in this transaction. + pub fn orchard_action_count(&self) -> usize { match self { - // Maybe Orchard shielded data + Transaction::V1 { .. } + | Transaction::V2 { .. } + | Transaction::V3 { .. } + | Transaction::V4 { .. } => 0, + Transaction::V5 { orchard_shielded_data, .. - } => orchard_shielded_data.as_ref(), + } => orchard_shielded_data + .iter() + .flat_map(orchard::ShieldedData::actions) + .count(), + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] Transaction::V6 { orchard_shielded_data, .. - } => orchard_shielded_data.as_ref(), + } => orchard_shielded_data + .iter() + .flat_map(orchard::ShieldedData::actions) + .count(), + } + } - // No Orchard shielded data + /// Access the [`orchard::Nullifier`]s in this transaction. + pub fn orchard_nullifiers(&self) -> Box + '_> { + match self { Transaction::V1 { .. } | Transaction::V2 { .. } | Transaction::V3 { .. } - | Transaction::V4 { .. } => None, + | Transaction::V4 { .. } => Box::new(std::iter::empty()), + + Transaction::V5 { + orchard_shielded_data, + .. + } => Box::new( + orchard_shielded_data + .iter() + .flat_map(orchard::ShieldedData::nullifiers), + ), + + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + Transaction::V6 { + orchard_shielded_data, + .. + } => Box::new( + orchard_shielded_data + .iter() + .flat_map(orchard::ShieldedData::nullifiers), + ), } } - /// Iterate over the [`orchard::Action`]s in this transaction, if there are any, - /// regardless of version. - pub fn orchard_actions(&self) -> impl Iterator { - self.orchard_shielded_data() - .into_iter() - .flat_map(orchard::ShieldedData::actions) + /// Access the note commitments in this transaction. + pub fn orchard_note_commitments(&self) -> Box + '_> { + match self { + Transaction::V1 { .. } + | Transaction::V2 { .. } + | Transaction::V3 { .. } + | Transaction::V4 { .. } => Box::new(std::iter::empty()), + + Transaction::V5 { + orchard_shielded_data, + .. + } => Box::new( + orchard_shielded_data + .iter() + .flat_map(orchard::ShieldedData::note_commitments) + .cloned(), + ), + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + Transaction::V6 { + orchard_shielded_data, + orchard_zsa_issue_data, + .. + } => Box::new( + orchard_shielded_data + .iter() + .flat_map(orchard::ShieldedData::note_commitments) + .cloned() + .chain( + orchard_zsa_issue_data + .iter() + .flat_map(orchard_zsa::IssueData::note_commitments), + ), + ), + } } - /// Access the [`orchard::Nullifier`]s in this transaction, if there are any, + /// Access the Orchard issue data in this transaction, if any, /// regardless of version. - pub fn orchard_nullifiers(&self) -> impl Iterator { - self.orchard_shielded_data() - .into_iter() - .flat_map(orchard::ShieldedData::nullifiers) + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + pub fn orchard_zsa_issue_data(&self) -> Option<&orchard_zsa::IssueData> { + match self { + Transaction::V1 { .. } + | Transaction::V2 { .. } + | Transaction::V3 { .. } + | Transaction::V4 { .. } + | Transaction::V5 { .. } => None, + + Transaction::V6 { + orchard_zsa_issue_data, + .. + } => orchard_zsa_issue_data.as_ref(), + } } - /// Access the note commitments in this transaction, if there are any, + /// Access the Orchard asset burns in this transaction, if there are any, /// regardless of version. - pub fn orchard_note_commitments(&self) -> impl Iterator { - self.orchard_shielded_data() - .into_iter() - .flat_map(orchard::ShieldedData::note_commitments) + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + pub fn orchard_burns(&self) -> Option<&'_ [orchard_zsa::BurnItem]> { + match self { + Transaction::V1 { .. } + | Transaction::V2 { .. } + | Transaction::V3 { .. } + | Transaction::V4 { .. } + | Transaction::V5 { .. } => None, + + Transaction::V6 { + orchard_shielded_data, + .. + } => orchard_shielded_data + .as_ref() + .map(|data| data.burn.as_ref()), + } } - /// Access the [`orchard::Flags`] in this transaction, if there is any, + /// Access the Orchard flags in this transaction, if there is any, /// regardless of version. pub fn orchard_flags(&self) -> Option { - self.orchard_shielded_data() - .map(|orchard_shielded_data| orchard_shielded_data.flags) + match self { + Transaction::V1 { .. } + | Transaction::V2 { .. } + | Transaction::V3 { .. } + | Transaction::V4 { .. } => None, + + Transaction::V5 { + orchard_shielded_data, + .. + } => orchard_shielded_data.as_ref().map(|data| data.flags), + + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + Transaction::V6 { + orchard_shielded_data, + .. + } => orchard_shielded_data.as_ref().map(|data| data.flags), + } + } + + /// Access the [`orchard::tree::Root`] in this transaction, if there is any, + /// regardless of version. + pub fn orchard_shared_anchor(&self) -> Option { + match self { + Transaction::V1 { .. } + | Transaction::V2 { .. } + | Transaction::V3 { .. } + | Transaction::V4 { .. } => None, + + Transaction::V5 { + orchard_shielded_data, + .. + } => orchard_shielded_data + .as_ref() + .map(|data| data.shared_anchor), + + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + Transaction::V6 { + orchard_shielded_data, + .. + } => orchard_shielded_data + .as_ref() + .map(|data| data.shared_anchor), + } } /// Return if the transaction has any Orchard shielded data, /// regardless of version. pub fn has_orchard_shielded_data(&self) -> bool { - self.orchard_shielded_data().is_some() + self.orchard_flags().is_some() } // value balances @@ -1451,10 +1584,28 @@ impl Transaction { /// /// pub fn orchard_value_balance(&self) -> ValueBalance { - let orchard_value_balance = self - .orchard_shielded_data() - .map(|shielded_data| shielded_data.value_balance) - .unwrap_or_else(Amount::zero); + let orchard_value_balance = match self { + Transaction::V1 { .. } + | Transaction::V2 { .. } + | Transaction::V3 { .. } + | Transaction::V4 { .. } => None, + + Transaction::V5 { + orchard_shielded_data, + .. + } => orchard_shielded_data + .as_ref() + .map(|data| data.value_balance), + + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + Transaction::V6 { + orchard_shielded_data, + .. + } => orchard_shielded_data + .as_ref() + .map(|data| data.value_balance), + } + .unwrap_or_else(Amount::zero); ValueBalance::from_orchard_amount(orchard_value_balance) } @@ -1641,8 +1792,31 @@ impl Transaction { /// /// See `orchard_value_balance` for details. pub fn orchard_value_balance_mut(&mut self) -> Option<&mut Amount> { - self.orchard_shielded_data_mut() - .map(|shielded_data| &mut shielded_data.value_balance) + match self { + Transaction::V5 { + orchard_shielded_data: Some(orchard_shielded_data), + .. + } => Some(&mut orchard_shielded_data.value_balance), + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + Transaction::V6 { + orchard_shielded_data: Some(orchard_shielded_data), + .. + } => Some(&mut orchard_shielded_data.value_balance), + + Transaction::V1 { .. } + | Transaction::V2 { .. } + | Transaction::V3 { .. } + | Transaction::V4 { .. } + | Transaction::V5 { + orchard_shielded_data: None, + .. + } => None, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + Transaction::V6 { + orchard_shielded_data: None, + .. + } => None, + } } /// Modify the `value_balance` field from the `sapling::ShieldedData` in this transaction, @@ -1792,17 +1966,40 @@ impl Transaction { /// Modify the [`orchard::ShieldedData`] in this transaction, /// regardless of version. - pub fn orchard_shielded_data_mut(&mut self) -> Option<&mut orchard::ShieldedData> { + pub fn v5_orchard_shielded_data_mut( + &mut self, + ) -> Option<&mut orchard::ShieldedData> { match self { Transaction::V5 { orchard_shielded_data: Some(orchard_shielded_data), .. } => Some(orchard_shielded_data), + + Transaction::V1 { .. } + | Transaction::V2 { .. } + | Transaction::V3 { .. } + | Transaction::V4 { .. } + | Transaction::V5 { + orchard_shielded_data: None, + .. + } => None, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + Transaction::V6 { .. } => None, + } + } + + /// Modify the Orchard flags in this transaction, regardless of version. + pub fn orchard_flags_mut(&mut self) -> Option<&mut orchard::shielded_data::Flags> { + match self { + Transaction::V5 { + orchard_shielded_data: Some(orchard_shielded_data), + .. + } => Some(&mut orchard_shielded_data.flags), #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] Transaction::V6 { orchard_shielded_data: Some(orchard_shielded_data), .. - } => Some(orchard_shielded_data), + } => Some(&mut orchard_shielded_data.flags), Transaction::V1 { .. } | Transaction::V2 { .. } diff --git a/zebra-chain/src/transaction/arbitrary.rs b/zebra-chain/src/transaction/arbitrary.rs index 58d37e43a97..7aec9f9da77 100644 --- a/zebra-chain/src/transaction/arbitrary.rs +++ b/zebra-chain/src/transaction/arbitrary.rs @@ -15,11 +15,16 @@ use crate::{ primitives::{Bctv14Proof, Groth16Proof, Halo2Proof, ZkSnarkProof}, sapling::{self, AnchorVariant, PerSpendAnchor, SharedAnchor}, serialization::{self, ZcashDeserializeInto}, - sprout, transparent, + sprout, + transaction::SigHash, + transparent, value_balance::{ValueBalance, ValueBalanceError}, LedgerState, }; +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +use crate::orchard_zsa::IssueData; + use itertools::Itertools; use super::{ @@ -132,8 +137,21 @@ impl Transaction { .boxed() } - /// Generate a proptest strategy for V5 Transactions - pub fn v5_strategy(ledger_state: LedgerState) -> BoxedStrategy { + /// Helper function to generate the common transaction fields. + /// This function is generic over the Orchard shielded data type. + fn v5_v6_strategy_common( + ledger_state: LedgerState, + ) -> impl Strategy< + Value = ( + NetworkUpgrade, + LockTime, + block::Height, + Vec, + Vec, + Option>, + Option>, + ), + > + 'static { ( NetworkUpgrade::branch_id_strategy(), any::(), @@ -141,7 +159,7 @@ impl Transaction { transparent::Input::vec_strategy(&ledger_state, MAX_ARBITRARY_ITEMS), vec(any::(), 0..MAX_ARBITRARY_ITEMS), option::of(any::>()), - option::of(any::()), + option::of(any::>()), ) .prop_map( move |( @@ -153,29 +171,99 @@ impl Transaction { sapling_shielded_data, orchard_shielded_data, )| { - Transaction::V5 { - network_upgrade: if ledger_state.transaction_has_valid_network_upgrade() { - ledger_state.network_upgrade() - } else { - network_upgrade - }, + // Apply conditional logic based on ledger_state + let network_upgrade = if ledger_state.transaction_has_valid_network_upgrade() { + ledger_state.network_upgrade() + } else { + network_upgrade + }; + + let sapling_shielded_data = if ledger_state.height.is_min() { + // The genesis block should not contain any shielded data. + None + } else { + sapling_shielded_data + }; + + let orchard_shielded_data = if ledger_state.height.is_min() { + // The genesis block should not contain any shielded data. + None + } else { + orchard_shielded_data + }; + + ( + network_upgrade, lock_time, expiry_height, inputs, outputs, - sapling_shielded_data: if ledger_state.height.is_min() { - // The genesis block should not contain any shielded data. - None - } else { - sapling_shielded_data - }, - orchard_shielded_data: if ledger_state.height.is_min() { - // The genesis block should not contain any shielded data. - None - } else { - orchard_shielded_data - }, - } + sapling_shielded_data, + orchard_shielded_data, + ) + }, + ) + } + + /// Generate a proptest strategy for V5 Transactions + pub fn v5_strategy(ledger_state: LedgerState) -> BoxedStrategy { + Self::v5_v6_strategy_common::(ledger_state) + .prop_map( + move |( + network_upgrade, + lock_time, + expiry_height, + inputs, + outputs, + sapling_shielded_data, + orchard_shielded_data, + )| Transaction::V5 { + network_upgrade, + lock_time, + expiry_height, + inputs, + outputs, + sapling_shielded_data, + orchard_shielded_data, + }, + ) + .boxed() + } + + /// Generate a proptest strategy for V6 Transactions + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + pub fn v6_strategy(ledger_state: LedgerState) -> BoxedStrategy { + Self::v5_v6_strategy_common::(ledger_state) + .prop_flat_map(|common_fields| { + option::of(any::()) + .prop_map(move |issue_data| (common_fields.clone(), issue_data)) + }) + .prop_filter_map( + "orchard_shielded_data can not be None for V6", + |( + ( + network_upgrade, + lock_time, + expiry_height, + inputs, + outputs, + sapling_shielded_data, + orchard_shielded_data, + ), + orchard_zsa_issue_data, + )| { + orchard_shielded_data.is_some().then_some(Transaction::V6 { + network_upgrade, + lock_time, + expiry_height, + // TODO: Consider generating a real arbitrary zip233_amount + zip233_amount: Default::default(), + inputs, + outputs, + sapling_shielded_data, + orchard_shielded_data, + orchard_zsa_issue_data, + }) }, ) .boxed() @@ -697,7 +785,7 @@ impl Arbitrary for sapling::TransferData { type Strategy = BoxedStrategy; } -impl Arbitrary for orchard::ShieldedData { +impl Arbitrary for orchard::ShieldedData { type Parameters = (); fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { @@ -707,13 +795,22 @@ impl Arbitrary for orchard::ShieldedData { any::(), any::(), vec( - any::(), + any::>(), 1..MAX_ARBITRARY_ITEMS, ), any::(), + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + any::(), ) - .prop_map( - |(flags, value_balance, shared_anchor, proof, actions, binding_sig)| Self { + .prop_map(|props| { + #[cfg(not(all(zcash_unstable = "nu7", feature = "tx_v6")))] + let (flags, value_balance, shared_anchor, proof, actions, binding_sig) = props; + + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + let (flags, value_balance, shared_anchor, proof, actions, binding_sig, burn) = + props; + + Self { flags, value_balance, shared_anchor, @@ -722,8 +819,10 @@ impl Arbitrary for orchard::ShieldedData { .try_into() .expect("arbitrary vector size range produces at least one action"), binding_sig: binding_sig.0, - }, - ) + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + burn, + } + }) .boxed() } @@ -765,6 +864,8 @@ impl Arbitrary for Transaction { Some(3) => return Self::v3_strategy(ledger_state), Some(4) => return Self::v4_strategy(ledger_state), Some(5) => return Self::v5_strategy(ledger_state), + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + Some(6) => return Self::v6_strategy(ledger_state), Some(_) => unreachable!("invalid transaction version in override"), None => {} } @@ -778,19 +879,36 @@ impl Arbitrary for Transaction { NetworkUpgrade::Blossom | NetworkUpgrade::Heartwood | NetworkUpgrade::Canopy => { Self::v4_strategy(ledger_state) } - NetworkUpgrade::Nu5 - | NetworkUpgrade::Nu6 - | NetworkUpgrade::Nu6_1 - | NetworkUpgrade::Nu7 => prop_oneof![ + NetworkUpgrade::Nu5 | NetworkUpgrade::Nu6 | NetworkUpgrade::Nu6_1 => prop_oneof![ Self::v4_strategy(ledger_state.clone()), Self::v5_strategy(ledger_state) ] .boxed(), + NetworkUpgrade::Nu7 => { + #[cfg(not(all(zcash_unstable = "nu7", feature = "tx_v6")))] + { + prop_oneof![ + Self::v4_strategy(ledger_state.clone()), + Self::v5_strategy(ledger_state.clone()), + ] + .boxed() + } + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + { + prop_oneof![ + Self::v4_strategy(ledger_state.clone()), + Self::v5_strategy(ledger_state.clone()), + Self::v6_strategy(ledger_state), + ] + .boxed() + } + } + #[cfg(zcash_unstable = "zfuture")] NetworkUpgrade::ZFuture => prop_oneof![ Self::v4_strategy(ledger_state.clone()), - Self::v5_strategy(ledger_state) + Self::v5_strategy(ledger_state) // FIXME: Add v6_strategy here? ] .boxed(), } @@ -826,6 +944,7 @@ impl Arbitrary for VerifiedUnminedTx { any::(), serialization::arbitrary::datetime_u32(), any::(), + any::<[u8; 32]>().prop_map(SigHash), ) .prop_map( |( @@ -836,6 +955,7 @@ impl Arbitrary for VerifiedUnminedTx { fee_weight_ratio, time, height, + tx_sighash, )| { if unpaid_actions > conventional_actions { unpaid_actions = conventional_actions; @@ -854,6 +974,7 @@ impl Arbitrary for VerifiedUnminedTx { time: Some(time), height: Some(height), spent_outputs: std::sync::Arc::new(vec![]), + tx_sighash, } }, ) @@ -1042,21 +1163,11 @@ pub fn transactions_from_blocks<'a>( }) } -/// Modify a V5 transaction to insert fake Orchard shielded data. -/// /// Creates a fake instance of [`orchard::ShieldedData`] with one fake action. Note that both the /// action and the shielded data are invalid and shouldn't be used in tests that require them to be /// valid. -/// -/// A mutable reference to the inserted shielded data is returned, so that the caller can further -/// customize it if required. -/// -/// # Panics -/// -/// Panics if the transaction to be modified is not V5. -pub fn insert_fake_orchard_shielded_data( - transaction: &mut Transaction, -) -> &mut orchard::ShieldedData { +pub fn create_fake_orchard_shielded_data( +) -> orchard::ShieldedData { // Create a dummy action let mut runner = TestRunner::default(); let dummy_action = orchard::Action::arbitrary() @@ -1071,22 +1182,36 @@ pub fn insert_fake_orchard_shielded_data( }; // Place the dummy action inside the Orchard shielded data - let dummy_shielded_data = orchard::ShieldedData { + orchard::ShieldedData:: { flags: orchard::Flags::empty(), value_balance: Amount::try_from(0).expect("invalid transaction amount"), shared_anchor: orchard::tree::Root::default(), proof: Halo2Proof(vec![]), actions: at_least_one![dummy_authorized_action], binding_sig: Signature::from([0u8; 64]), - }; + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + burn: Default::default(), + } +} +/// Modify a V5 transaction to insert fake Orchard shielded data. +/// +/// A mutable reference to the inserted shielded data is returned, so that the caller can further +/// customize it if required. +/// +/// # Panics +/// +/// Panics if the transaction to be modified is not V5. +pub fn insert_fake_v5_orchard_shielded_data( + transaction: &mut Transaction, +) -> &mut orchard::ShieldedData { // Replace the shielded data in the transaction match transaction { Transaction::V5 { orchard_shielded_data, .. } => { - *orchard_shielded_data = Some(dummy_shielded_data); + *orchard_shielded_data = Some(create_fake_orchard_shielded_data()); orchard_shielded_data .as_mut() @@ -1095,3 +1220,31 @@ pub fn insert_fake_orchard_shielded_data( _ => panic!("Fake V5 transaction is not V5"), } } + +/// Modify a V6 transaction to insert fake Orchard shielded data. +/// +/// A mutable reference to the inserted shielded data is returned, so that the caller can further +/// customize it if required. +/// +/// # Panics +/// +/// Panics if the transaction to be modified is not V6. +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +pub fn insert_fake_v6_orchard_shielded_data( + transaction: &mut Transaction, +) -> &mut orchard::ShieldedData { + // Replace the shielded data in the transaction + match transaction { + Transaction::V6 { + orchard_shielded_data, + .. + } => { + *orchard_shielded_data = Some(create_fake_orchard_shielded_data()); + + orchard_shielded_data + .as_mut() + .expect("shielded data was just inserted") + } + _ => panic!("Fake V6 transaction is not V6"), + } +} diff --git a/zebra-chain/src/transaction/builder.rs b/zebra-chain/src/transaction/builder.rs index 9ba21b0b8ba..99c9517b5e0 100644 --- a/zebra-chain/src/transaction/builder.rs +++ b/zebra-chain/src/transaction/builder.rs @@ -63,7 +63,7 @@ impl Transaction { // let outputs: Vec<_> = outputs .into_iter() - .map(|(amount, lock_script)| transparent::Output::new_coinbase(amount, lock_script)) + .map(|(amount, lock_script)| transparent::Output::new(amount, lock_script)) .collect(); assert!( !outputs.is_empty(), @@ -101,6 +101,7 @@ impl Transaction { // See the Zcash spec for additional shielded coinbase consensus rules. sapling_shielded_data: None, orchard_shielded_data: None, + orchard_zsa_issue_data: None, } } diff --git a/zebra-chain/src/transaction/serialize.rs b/zebra-chain/src/transaction/serialize.rs index e67df823fcf..600cfd05459 100644 --- a/zebra-chain/src/transaction/serialize.rs +++ b/zebra-chain/src/transaction/serialize.rs @@ -9,8 +9,8 @@ use hex::FromHex; use reddsa::{orchard::Binding, orchard::SpendAuth, Signature}; use crate::{ - amount, block::MAX_BLOCK_BYTES, + orchard::{OrchardVanilla, ShieldedDataFlavor}, parameters::{OVERWINTER_VERSION_GROUP_ID, SAPLING_VERSION_GROUP_ID, TX_V5_VERSION_GROUP_ID}, primitives::{Halo2Proof, ZkSnarkProof}, serialization::{ @@ -21,11 +21,17 @@ use crate::{ }; #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] -use crate::parameters::TX_V6_VERSION_GROUP_ID; +use crate::{ + orchard::OrchardZSA, orchard_zsa::NoBurn, parameters::TX_V6_VERSION_GROUP_ID, + serialization::CompactSizeMessage, +}; use super::*; use crate::sapling; +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +use versioned_sig::{SighashInfoV0, VersionedSigV0}; + impl ZcashDeserialize for jubjub::Fq { fn zcash_deserialize(mut reader: R) -> Result { let possible_scalar = jubjub::Fq::from_bytes(&reader.read_32_bytes()?); @@ -326,7 +332,10 @@ impl ZcashDeserialize for Option> { } } -impl ZcashSerialize for Option { +impl ZcashSerialize for Option> +where + orchard::ShieldedData: ZcashSerialize, +{ fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { match self { None => { @@ -342,14 +351,18 @@ impl ZcashSerialize for Option { orchard_shielded_data.zcash_serialize(&mut writer)?; } } + Ok(()) } } -impl ZcashSerialize for orchard::ShieldedData { +impl ZcashSerialize for orchard::ShieldedData { fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { // Split the AuthorizedAction - let (actions, sigs): (Vec, Vec>) = self + let (actions, sigs): ( + Vec>, + Vec>, + ) = self .actions .iter() .cloned() @@ -381,12 +394,71 @@ impl ZcashSerialize for orchard::ShieldedData { } } +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +#[allow(clippy::unwrap_in_result)] +impl ZcashSerialize for orchard::ShieldedData { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + // Denoted as `nActionGroupsOrchard` in the spec (ZIP 230). + // TxV6 currently supports only one action group. + CompactSizeMessage::try_from(1) + .unwrap_or_else(|_| unreachable!()) + .zcash_serialize(&mut writer)?; + + // Split the AuthorizedAction + let (actions, sigs): ( + Vec>, + Vec>>, + ) = self + .actions + .iter() + .cloned() + .map(orchard::AuthorizedAction::into_parts) + .map(|(action, sig)| (action, VersionedSigV0::new(sig))) + .unzip(); + + // Denoted as `nActionsOrchard` and `vActionsOrchard` in the spec. + actions.zcash_serialize(&mut writer)?; + + // Denoted as `flagsOrchard` in the spec. + self.flags.zcash_serialize(&mut writer)?; + + // Denoted as `anchorOrchard` in the spec. + self.shared_anchor.zcash_serialize(&mut writer)?; + + // Denoted as `nAGExpiryHeight` in the spec (ZIP 230) (must be zero for V6/NU7). + writer.write_u32::(0)?; + + // Denoted as `vAssetBurn` in the spec (ZIP 230). + self.burn.zcash_serialize(&mut writer)?; + + // Denoted as `sizeProofsOrchard` and `proofsOrchard` in the spec. + self.proof.zcash_serialize(&mut writer)?; + + // Denoted as `vSpendAuthSigsOrchard` in the spec. + zcash_serialize_external_count(&sigs, &mut writer)?; + + // Denoted as `valueBalanceOrchard` in the spec. + // FIXME: `valueBalanceOrchard` and `bindingSigOrchard` are per-transaction fields, + // not per-action-group fields. They are serialized here because today the V6 + // transaction has exactly one action group. Once multi-action-group support is + // added (ZIP-230), move this serialization up to the caller in the V6 branch of + // `Transaction::zcash_serialize`. + self.value_balance.zcash_serialize(&mut writer)?; + + // Denoted as `bindingSigOrchard` in the spec. + VersionedSigV0::new(self.binding_sig).zcash_serialize(&mut writer)?; + + Ok(()) + } +} + // we can't split ShieldedData out of Option deserialization, // because the counts are read along with the arrays. -impl ZcashDeserialize for Option { +impl ZcashDeserialize for Option> { fn zcash_deserialize(mut reader: R) -> Result { // Denoted as `nActionsOrchard` and `vActionsOrchard` in the spec. - let actions: Vec = (&mut reader).zcash_deserialize_into()?; + let actions: Vec> = + (&mut reader).zcash_deserialize_into()?; // "The fields flagsOrchard, valueBalanceOrchard, anchorOrchard, sizeProofsOrchard, // proofsOrchard , and bindingSigOrchard are present if and only if nActionsOrchard > 0." @@ -408,8 +480,16 @@ impl ZcashDeserialize for Option { // in [`Flags::zcash_deserialized`]. let flags: orchard::Flags = (&mut reader).zcash_deserialize_into()?; + // `ENABLE_ZSA` is introduced in V6 (ZIP 230) and must be zero in V5. + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + if flags.contains(orchard::Flags::ENABLE_ZSA) { + return Err(SerializationError::Parse( + "ENABLE_ZSA is not allowed in V5 transactions", + )); + } + // Denoted as `valueBalanceOrchard` in the spec. - let value_balance: amount::Amount = (&mut reader).zcash_deserialize_into()?; + let value_balance: Amount = (&mut reader).zcash_deserialize_into()?; // Denoted as `anchorOrchard` in the spec. // Consensus: type is `{0 .. 𝑞_ℙ − 1}`. See [`orchard::tree::Root::zcash_deserialize`]. @@ -432,7 +512,7 @@ impl ZcashDeserialize for Option { let binding_sig: Signature = (&mut reader).zcash_deserialize_into()?; // Create the AuthorizedAction from deserialized parts - let authorized_actions: Vec = actions + let authorized_actions: Vec> = actions .into_iter() .zip(sigs) .map(|(action, spend_auth_sig)| { @@ -440,11 +520,96 @@ impl ZcashDeserialize for Option { }) .collect(); - let actions: AtLeastOne = authorized_actions.try_into()?; + let actions: AtLeastOne> = + authorized_actions.try_into()?; - Ok(Some(orchard::ShieldedData { + Ok(Some(orchard::ShieldedData:: { flags, value_balance, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + burn: NoBurn, + shared_anchor, + proof, + actions, + binding_sig, + })) + } +} + +// FIXME: Try to avoid duplication with OrchardVanilla version +// we can't split ShieldedData out of Option deserialization, +// because the counts are read along with the arrays. +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +impl ZcashDeserialize for Option> { + fn zcash_deserialize(mut reader: R) -> Result { + // Denoted as `nActionGroupsOrchard` in the spec (ZIP 230). + // TxV6 currently supports only one action group. + let n_action_groups: usize = (&mut reader) + .zcash_deserialize_into::()? + .into(); + if n_action_groups == 0 { + return Ok(None); + } else if n_action_groups != 1 { + return Err(SerializationError::Parse( + "V6 transaction must contain exactly one action group", + )); + } + + // Denoted as `nActionsOrchard` and `vActionsOrchard` in the spec. + let actions: Vec> = (&mut reader).zcash_deserialize_into()?; + + // Denoted as `flagsOrchard` in the spec. + // Consensus: type of each flag is 𝔹, i.e. a bit. This is enforced implicitly + // in [`Flags::zcash_deserialized`]. + let flags: orchard::Flags = (&mut reader).zcash_deserialize_into()?; + + // Denoted as `anchorOrchard` in the spec. + // Consensus: type is `{0 .. 𝑞_ℙ − 1}`. See [`orchard::tree::Root::zcash_deserialize`]. + let shared_anchor: orchard::tree::Root = (&mut reader).zcash_deserialize_into()?; + + // Denoted as `nAGExpiryHeight` in the spec (ZIP 230) (must be zero for V6/NU7). + let n_ag_expiry_height = reader.read_u32::()?; + if n_ag_expiry_height != 0 { + return Err(SerializationError::Parse( + "nAGExpiryHeight must be zero for NU7", + )); + } + + // Denoted as `vAssetBurn` in the spec (ZIP 230). + let burn = (&mut reader).zcash_deserialize_into()?; + + // Denoted as `sizeProofsOrchard` and `proofsOrchard` in the spec. + // Consensus: type is `ZKAction.Proof`, i.e. a byte sequence. + // https://zips.z.cash/protocol/protocol.pdf#halo2encoding + let proof: Halo2Proof = (&mut reader).zcash_deserialize_into()?; + + // Denoted as `vSpendAuthSigsOrchard` in the spec. + let spend_sigs: Vec>> = + zcash_deserialize_external_count(actions.len(), &mut reader)?; + + // Denoted as `valueBalanceOrchard` in the spec. + let value_balance: Amount = (&mut reader).zcash_deserialize_into()?; + + // Denoted as `bindingSigOrchard` in the spec. + let binding_sig: Signature = + VersionedSigV0::zcash_deserialize(&mut reader)?.into_signature(); + + // Create the AuthorizedAction from deserialized parts + let authorized_actions: Vec> = actions + .into_iter() + .zip(spend_sigs) + .map(|(action, spend_sig)| { + orchard::AuthorizedAction::from_parts(action, spend_sig.into_signature()) + }) + .collect(); + + let actions: AtLeastOne> = + authorized_actions.try_into()?; + + Ok(Some(orchard::ShieldedData:: { + flags, + value_balance, + burn, shared_anchor, proof, actions, @@ -686,6 +851,7 @@ impl ZcashSerialize for Transaction { outputs, sapling_shielded_data, orchard_shielded_data, + orchard_zsa_issue_data, } => { // Transaction V6 spec: // https://zips.z.cash/zip-0230#specification @@ -707,6 +873,9 @@ impl ZcashSerialize for Transaction { writer.write_u32::(expiry_height.0)?; // Denoted as `zip233_amount` in the spec. + // FIXME: ZIP-230 places this *after* the 8-byte `fee` field which is currently + // missing (see TODO on the Transaction::V6 variant). Once `fee` is added, the + // byte offset of zip233_amount will shift to match the spec. zip233_amount.zcash_serialize(&mut writer)?; // Denoted as `tx_in_count` and `tx_in` in the spec. @@ -715,16 +884,26 @@ impl ZcashSerialize for Transaction { // Denoted as `tx_out_count` and `tx_out` in the spec. outputs.zcash_serialize(&mut writer)?; + // Denoted as `vSighashInfo` in the spec. + // There is one sighash info per transparent input. For now, only V0 is supported. + for _ in inputs { + SighashInfoV0.zcash_serialize(&mut writer)?; + } + // A bundle of fields denoted in the spec as `nSpendsSapling`, `vSpendsSapling`, // `nOutputsSapling`,`vOutputsSapling`, `valueBalanceSapling`, `anchorSapling`, // `vSpendProofsSapling`, `vSpendAuthSigsSapling`, `vOutputProofsSapling` and // `bindingSigSapling`. - sapling_shielded_data.zcash_serialize(&mut writer)?; + sapling_v6::zcash_serialize_v6(sapling_shielded_data, &mut writer)?; // A bundle of fields denoted in the spec as `nActionsOrchard`, `vActionsOrchard`, // `flagsOrchard`,`valueBalanceOrchard`, `anchorOrchard`, `sizeProofsOrchard`, // `proofsOrchard`, `vSpendAuthSigsOrchard`, and `bindingSigOrchard`. orchard_shielded_data.zcash_serialize(&mut writer)?; + + // A bundle of OrchardZSA issuance fields denoted in the spec as `nIssueActions`, + // `vIssueActions`, `ik`, and `issueAuthSig`. + orchard_zsa_issue_data.zcash_serialize(&mut writer)?; } } Ok(()) @@ -1004,17 +1183,27 @@ impl ZcashDeserialize for Transaction { // Denoted as `tx_out_count` and `tx_out` in the spec. let outputs = Vec::zcash_deserialize(&mut limited_reader)?; + // Denoted as `vSighashInfo` in the spec (ZIP-230). + // There is one `TransparentSighashInfo` per transparent input (tx_in_count entries). + // For now, only V0 is supported, which must decode to a Vector == [0x00]. + for _ in &inputs { + SighashInfoV0::zcash_deserialize(&mut limited_reader)?; + } + // A bundle of fields denoted in the spec as `nSpendsSapling`, `vSpendsSapling`, // `nOutputsSapling`,`vOutputsSapling`, `valueBalanceSapling`, `anchorSapling`, // `vSpendProofsSapling`, `vSpendAuthSigsSapling`, `vOutputProofsSapling` and // `bindingSigSapling`. - let sapling_shielded_data = (&mut limited_reader).zcash_deserialize_into()?; + let sapling_shielded_data = sapling_v6::zcash_deserialize_v6(&mut limited_reader)?; // A bundle of fields denoted in the spec as `nActionsOrchard`, `vActionsOrchard`, // `flagsOrchard`,`valueBalanceOrchard`, `anchorOrchard`, `sizeProofsOrchard`, // `proofsOrchard`, `vSpendAuthSigsOrchard`, and `bindingSigOrchard`. let orchard_shielded_data = (&mut limited_reader).zcash_deserialize_into()?; + // OrchardZSA Issuance Fields. + let orchard_zsa_issue_data = (&mut limited_reader).zcash_deserialize_into()?; + Ok(Transaction::V6 { network_upgrade, lock_time, @@ -1024,6 +1213,7 @@ impl ZcashDeserialize for Transaction { outputs, sapling_shielded_data, orchard_shielded_data, + orchard_zsa_issue_data, }) } (_, _) => Err(SerializationError::Parse("bad tx header")), @@ -1173,3 +1363,173 @@ impl FromHex for SerializedTransaction { Ok(bytes.into()) } } + +// TODO: After tx-v6 merge, refactor to share common serialization logic with V5. +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +mod sapling_v6 { + use super::*; + + use redjubjub::{Binding, Signature, SpendAuth}; + + type SaplingShieldedData = sapling::ShieldedData; + + impl ZcashSerialize for Signature { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + writer.write_all(&<[u8; 64]>::from(*self)[..])?; + Ok(()) + } + } + + impl ZcashDeserialize for Signature { + fn zcash_deserialize(mut reader: R) -> Result { + Ok(reader.read_64_bytes()?.into()) + } + } + + pub(super) fn zcash_serialize_v6( + shielded_data: &Option, + mut writer: W, + ) -> Result<(), io::Error> { + match shielded_data { + None => { + // Same as V5: empty spend and output lists + zcash_serialize_empty_list(&mut writer)?; + zcash_serialize_empty_list(&mut writer)?; + } + Some(sapling_shielded_data) => { + zcash_serialize_v6_inner(sapling_shielded_data, &mut writer)?; + } + } + Ok(()) + } + + fn zcash_serialize_v6_inner( + shielded_data: &SaplingShieldedData, + mut writer: W, + ) -> Result<(), io::Error> { + // V6 difference: wrap spend auth signatures with VersionedSigV0 + let (spend_prefixes, spend_proofs_sigs): (Vec<_>, Vec<_>) = shielded_data + .spends() + .cloned() + .map(sapling::Spend::::into_v5_parts) + .map(|(prefix, proof, sig)| (prefix, (proof, VersionedSigV0::new(sig)))) + .unzip(); + let (spend_proofs, spend_sigs) = spend_proofs_sigs.into_iter().unzip(); + + // Same as V5: collect output parts + let (output_prefixes, output_proofs): (Vec<_>, _) = shielded_data + .outputs() + .cloned() + .map(sapling::Output::into_v5_parts) + .unzip(); + + // Same as V5: serialize spend/output prefixes + spend_prefixes.zcash_serialize(&mut writer)?; + output_prefixes.zcash_serialize(&mut writer)?; + + // Same as V5: value balance + shielded_data.value_balance.zcash_serialize(&mut writer)?; + + // Same as V5: shared anchor (if spends present) + if let Some(shared_anchor) = shielded_data.shared_anchor() { + writer.write_all(&<[u8; 32]>::from(shared_anchor)[..])?; + } + + // Same as V5: spend proofs + zcash_serialize_external_count(&spend_proofs, &mut writer)?; + + // V6 difference: versioned spend auth signatures + zcash_serialize_external_count(&spend_sigs, &mut writer)?; + + // Same as V5: output proofs + zcash_serialize_external_count(&output_proofs, &mut writer)?; + + // V6 difference: versioned binding signature + VersionedSigV0::new(shielded_data.binding_sig).zcash_serialize(&mut writer)?; + + Ok(()) + } + + #[allow(clippy::unwrap_in_result)] + pub(super) fn zcash_deserialize_v6( + mut reader: R, + ) -> Result, SerializationError> { + // Same as V5: deserialize spend/output prefixes + let spend_prefixes: Vec<_> = (&mut reader).zcash_deserialize_into()?; + let output_prefixes: Vec<_> = (&mut reader).zcash_deserialize_into()?; + + let spends_count = spend_prefixes.len(); + let outputs_count = output_prefixes.len(); + + // Same as V5: return None if no spends or outputs + if spend_prefixes.is_empty() && output_prefixes.is_empty() { + return Ok(None); + } + + // Same as V5: value balance + let value_balance = (&mut reader).zcash_deserialize_into()?; + + // Same as V5: shared anchor (if spends present) + let shared_anchor = if spends_count > 0 { + Some((&mut reader).zcash_deserialize_into()?) + } else { + None + }; + + // Same as V5: spend proofs + let spend_proofs = zcash_deserialize_external_count(spends_count, &mut reader)?; + + // V6 difference: deserialize versioned spend auth signatures + let spend_sigs: Vec>> = + zcash_deserialize_external_count(spends_count, &mut reader)?; + + // Same as V5: output proofs + let output_proofs = zcash_deserialize_external_count(outputs_count, &mut reader)?; + + // V6 difference: deserialize versioned binding signature + let binding_sig = VersionedSigV0::zcash_deserialize(&mut reader)?.into_signature(); + + // V6 difference: unwrap versioned spend auth signatures + let spends: Vec<_> = spend_prefixes + .into_iter() + .zip(spend_proofs) + .zip(spend_sigs) + .map(|((prefix, proof), spend_sig)| { + sapling::Spend::::from_v5_parts( + prefix, + proof, + spend_sig.into_signature(), + ) + }) + .collect(); + + // Same as V5: create outputs from parts + let outputs = output_prefixes + .into_iter() + .zip(output_proofs) + .map(|(prefix, proof)| sapling::Output::from_v5_parts(prefix, proof)) + .collect(); + + // Same as V5: create transfers from spends/outputs + let transfers = match shared_anchor { + Some(shared_anchor) => sapling::TransferData::SpendsAndMaybeOutputs { + shared_anchor, + spends: spends + .try_into() + .expect("checked spends when parsing shared anchor"), + maybe_outputs: outputs, + }, + None => sapling::TransferData::JustOutputs { + outputs: outputs + .try_into() + .expect("checked spends or outputs and returned early"), + }, + }; + + Ok(Some(sapling::ShieldedData { + value_balance, + transfers, + binding_sig, + })) + } +} diff --git a/zebra-chain/src/transaction/sighash.rs b/zebra-chain/src/transaction/sighash.rs index 1cfb2a33335..d1bae9c0ced 100644 --- a/zebra-chain/src/transaction/sighash.rs +++ b/zebra-chain/src/transaction/sighash.rs @@ -127,7 +127,7 @@ impl SigHasher { /// Returns the Orchard bundle in the precomputed transaction data. pub fn orchard_bundle( &self, - ) -> Option<::orchard::bundle::Bundle<::orchard::bundle::Authorized, ZatBalance>> { + ) -> Option> { self.precomputed_tx_data.orchard_bundle() } diff --git a/zebra-chain/src/transaction/txid.rs b/zebra-chain/src/transaction/txid.rs index ba506128113..445b51113d2 100644 --- a/zebra-chain/src/transaction/txid.rs +++ b/zebra-chain/src/transaction/txid.rs @@ -41,7 +41,7 @@ impl<'a> TxIdBuilder<'a> { Some(Hash(hash_writer.finish())) } - /// Compute the Transaction ID for a V5 transaction in the given network upgrade. + /// Compute the Transaction ID for transactions V5 to V6. /// In this case it's the hash of a tree of hashes of specific parts of the /// transaction, as specified in ZIP-244 and ZIP-225. fn txid_v5(self) -> Option { diff --git a/zebra-chain/src/transaction/unmined.rs b/zebra-chain/src/transaction/unmined.rs index 1e1c5fe5897..403f6647168 100644 --- a/zebra-chain/src/transaction/unmined.rs +++ b/zebra-chain/src/transaction/unmined.rs @@ -22,7 +22,7 @@ use crate::{ block::Height, serialization::ZcashSerialize, transaction::{ - AuthDigest, Hash, + AuthDigest, Hash, SigHash, Transaction::{self, *}, WtxId, }, @@ -379,6 +379,9 @@ pub struct VerifiedUnminedTx { /// Used by mempool policy checks (`AreInputsStandard`, `GetP2SHSigOpCount`). /// Empty for transactions with no transparent inputs or in test contexts. pub spent_outputs: Arc>, + + /// The shielded sighash for this transaction. + pub tx_sighash: SigHash, } impl fmt::Debug for VerifiedUnminedTx { @@ -407,6 +410,7 @@ impl VerifiedUnminedTx { miner_fee: Amount, legacy_sigop_count: u32, spent_outputs: Arc>, + tx_sighash: SigHash, ) -> Result { let fee_weight_ratio = zip317::conventional_fee_weight_ratio(&transaction, miner_fee); let conventional_actions = zip317::conventional_actions(&transaction.transaction); @@ -424,6 +428,7 @@ impl VerifiedUnminedTx { time: None, height: None, spent_outputs, + tx_sighash, }) } diff --git a/zebra-chain/src/transaction/unmined/zip317.rs b/zebra-chain/src/transaction/unmined/zip317.rs index b7c75ef1e6b..e094090b803 100644 --- a/zebra-chain/src/transaction/unmined/zip317.rs +++ b/zebra-chain/src/transaction/unmined/zip317.rs @@ -153,15 +153,52 @@ pub fn conventional_actions(transaction: &Transaction) -> u32 { let n_join_split = transaction.joinsplit_count(); let n_spends_sapling = transaction.sapling_spends_per_anchor().count(); let n_outputs_sapling = transaction.sapling_outputs().count(); - let n_actions_orchard = transaction.orchard_actions().count(); + let n_actions_orchard = transaction.orchard_action_count(); let tx_in_logical_actions = div_ceil(tx_in_total_size, P2PKH_STANDARD_INPUT_SIZE); let tx_out_logical_actions = div_ceil(tx_out_total_size, P2PKH_STANDARD_OUTPUT_SIZE); + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + let n_issue_actions = { + use zcash_primitives::transaction::fees::zip317::CREATION_COST; + + let n_issue_notes = transaction + .orchard_zsa_issue_data() + .map_or(0, |issue_data| issue_data.inner().get_all_notes().len()); + + // TODO: Calculate the real `n_asset_creations`. + // + // This is simplified to zero for now because ZIP-317 defines `nAssetCreations` using + // the Global Issuance State, so it cannot be calculated from the transaction alone. + // Doing the exact calculation requires additional logic for querying the state task + // from the consensus task. + // + // See also the ZIP-317 fee calculation in librustzcash: + // zcash_primitives/src/transaction/fees/zip317.rs, FeeRule::fee_required + let n_asset_creations = 0; + + n_issue_notes + (CREATION_COST * n_asset_creations) + }; + + #[cfg(not(all(zcash_unstable = "nu7", feature = "tx_v6")))] + let n_issue_actions = 0; + let logical_actions = max(tx_in_logical_actions, tx_out_logical_actions) + 2 * n_join_split + max(n_spends_sapling, n_outputs_sapling) - + n_actions_orchard; + + n_actions_orchard + + n_issue_actions; + + // TODO: Add ZSA issuance-related ZIP-317 terms to logical_actions formula, like: + // + // let logical_actions = + // ... + // + n_zsa_issue_notes + // + CREATION_COST * n_asset_creations; + // + // librustzcash already includes these; see: + // zcash_primitives/src/transaction/fees/zip317.rs, FeeRule::fee_required + let logical_actions: u32 = logical_actions .try_into() .expect("transaction items are limited by serialized size limit"); diff --git a/zebra-chain/src/transaction/versioned_sig.rs b/zebra-chain/src/transaction/versioned_sig.rs new file mode 100644 index 00000000000..8529be93c6c --- /dev/null +++ b/zebra-chain/src/transaction/versioned_sig.rs @@ -0,0 +1,162 @@ +//! Versioned signature types for transaction V6+. +//! +//! ZIP 246 introduces sighash versioning to allow future signature algorithm upgrades. +//! Signatures are prefixed with sighash metadata (version + optional associated data). + +use std::io; + +use byteorder::{ReadBytesExt, WriteBytesExt}; + +use crate::serialization::{ + CompactSizeMessage, SerializationError, TrustedPreallocate, ZcashDeserialize, ZcashSerialize, +}; + +/// Sighash version 0 for V6 transactions (zero-sized type as V0 has no metadata). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct SighashInfoV0; + +#[allow(clippy::unwrap_in_result)] +impl ZcashSerialize for SighashInfoV0 { + fn zcash_serialize(&self, mut writer: W) -> io::Result<()> { + // Sighash V0 has no associated data, so length is always 1 (just the version byte) + CompactSizeMessage::try_from(1) + .expect("1 is always a valid CompactSize") + .zcash_serialize(&mut writer)?; + + // Version 0 + writer.write_u8(0)?; + + Ok(()) + } +} + +impl ZcashDeserialize for SighashInfoV0 { + fn zcash_deserialize(mut reader: R) -> Result { + let length = usize::from(CompactSizeMessage::zcash_deserialize(&mut reader)?); + + if length != 1 { + return Err(SerializationError::Parse( + "invalid sighash V0: length must be 1", + )); + } + + let version = reader.read_u8()?; + if version != 0 { + return Err(SerializationError::Parse( + "invalid sighash V0: version byte must be 0", + )); + } + + Ok(Self) + } +} + +/// A signature with sighash version 0 prefix for V6 transactions. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct VersionedSigV0(Sig); + +impl VersionedSigV0 { + /// Wrap a signature with sighash V0 metadata. + pub(crate) fn new(signature: Sig) -> Self { + Self(signature) + } + + /// Extract the underlying signature. + pub(crate) fn into_signature(self) -> Sig { + self.0 + } +} + +impl ZcashSerialize for VersionedSigV0 +where + Sig: ZcashSerialize, +{ + fn zcash_serialize(&self, mut writer: W) -> io::Result<()> { + SighashInfoV0.zcash_serialize(&mut writer)?; + self.0.zcash_serialize(&mut writer) + } +} + +impl ZcashDeserialize for VersionedSigV0 +where + Sig: ZcashDeserialize, +{ + fn zcash_deserialize(mut reader: R) -> Result { + SighashInfoV0::zcash_deserialize(&mut reader)?; + let signature = Sig::zcash_deserialize(&mut reader)?; + Ok(Self(signature)) + } +} + +impl TrustedPreallocate for VersionedSigV0 +where + Sig: TrustedPreallocate, +{ + fn max_allocation() -> u64 { + // Sighash info adds only 2 bytes overhead, so signature's max allocation is safe + Sig::max_allocation() + } +} + +#[cfg(test)] +mod tests { + use redjubjub::{Signature, SpendAuth}; + + use super::*; + + #[test] + fn sighash_info_v0_roundtrip() { + let info = SighashInfoV0; + + let bytes = info.zcash_serialize_to_vec().unwrap(); + assert_eq!(bytes, &[0x01, 0x00]); // CompactSize(1), version 0 + + let parsed = SighashInfoV0::zcash_deserialize(&bytes[..]).unwrap(); + assert_eq!(parsed, info); + } + + #[test] + fn sighash_info_v0_rejects_wrong_length() { + let bytes = [0x02, 0x00]; // CompactSize(2) - wrong length + assert!(SighashInfoV0::zcash_deserialize(&bytes[..]).is_err()); + } + + #[test] + fn sighash_info_v0_rejects_wrong_version() { + let bytes = [0x01, 0x01]; // CompactSize(1), version 1 - wrong version + assert!(SighashInfoV0::zcash_deserialize(&bytes[..]).is_err()); + } + + #[test] + fn versioned_sig_v0_roundtrip() { + // Create a test signature using real Sapling SpendAuth signature type (64 bytes) + // Using fixed bytes for deterministic testing (not a cryptographically valid signature) + let sig_bytes = [0x11u8; 64]; // Arbitrary 64-byte pattern + let original_sig = Signature::::from(sig_bytes); + + let versioned_sig = VersionedSigV0::new(original_sig); + let serialized_bytes = versioned_sig.zcash_serialize_to_vec().unwrap(); + + // Expected format: [CompactSize(1), version(0), sig_bytes...] + // 0x01 = CompactSize encoding of length 1 (just the version byte) + // 0x00 = sighash version 0 + // followed by 64 bytes of the signature + assert_eq!(serialized_bytes.len(), 1 + 1 + 64); // CompactSize + version + sig + assert_eq!(serialized_bytes[0], 0x01); // CompactSize(1) + assert_eq!(serialized_bytes[1], 0x00); // version 0 + assert_eq!(&serialized_bytes[2..], &sig_bytes[..]); // signature bytes + + let deserialized_sig = + VersionedSigV0::>::zcash_deserialize(&serialized_bytes[..]) + .unwrap(); + assert_eq!(deserialized_sig.into_signature(), original_sig); + } + + #[test] + fn versioned_sig_v0_rejects_invalid_sighash() { + let mut bytes = vec![0x01, 0x01]; // Invalid: CompactSize(1), version 1 + bytes.extend_from_slice(&[0u8; 64]); // Add dummy signature + + assert!(VersionedSigV0::>::zcash_deserialize(&bytes[..]).is_err()); + } +} diff --git a/zebra-chain/src/transparent/address.rs b/zebra-chain/src/transparent/address.rs index 8ab18caf0b1..ccadc67544a 100644 --- a/zebra-chain/src/transparent/address.rs +++ b/zebra-chain/src/transparent/address.rs @@ -142,12 +142,12 @@ impl std::str::FromStr for Address { hash_bytes.copy_from_slice(&payload); match hrp.as_str() { - zcash_primitives::constants::mainnet::HRP_TEX_ADDRESS => Ok(Address::Tex { + zcash_protocol::constants::mainnet::HRP_TEX_ADDRESS => Ok(Address::Tex { network_kind: NetworkKind::Mainnet, validating_key_hash: hash_bytes, }), - zcash_primitives::constants::testnet::HRP_TEX_ADDRESS => Ok(Address::Tex { + zcash_protocol::constants::testnet::HRP_TEX_ADDRESS => Ok(Address::Tex { network_kind: NetworkKind::Testnet, validating_key_hash: hash_bytes, }), @@ -196,25 +196,25 @@ impl ZcashDeserialize for Address { reader.read_exact(&mut hash_bytes)?; match version_bytes { - zcash_primitives::constants::mainnet::B58_SCRIPT_ADDRESS_PREFIX => { + zcash_protocol::constants::mainnet::B58_SCRIPT_ADDRESS_PREFIX => { Ok(Address::PayToScriptHash { network_kind: NetworkKind::Mainnet, script_hash: hash_bytes, }) } - zcash_primitives::constants::testnet::B58_SCRIPT_ADDRESS_PREFIX => { + zcash_protocol::constants::testnet::B58_SCRIPT_ADDRESS_PREFIX => { Ok(Address::PayToScriptHash { network_kind: NetworkKind::Testnet, script_hash: hash_bytes, }) } - zcash_primitives::constants::mainnet::B58_PUBKEY_ADDRESS_PREFIX => { + zcash_protocol::constants::mainnet::B58_PUBKEY_ADDRESS_PREFIX => { Ok(Address::PayToPublicKeyHash { network_kind: NetworkKind::Mainnet, pub_key_hash: hash_bytes, }) } - zcash_primitives::constants::testnet::B58_PUBKEY_ADDRESS_PREFIX => { + zcash_protocol::constants::testnet::B58_PUBKEY_ADDRESS_PREFIX => { Ok(Address::PayToPublicKeyHash { network_kind: NetworkKind::Testnet, pub_key_hash: hash_bytes, diff --git a/zebra-consensus/Cargo.toml b/zebra-consensus/Cargo.toml index de5143f729d..d8d07f40ce8 100644 --- a/zebra-consensus/Cargo.toml +++ b/zebra-consensus/Cargo.toml @@ -57,6 +57,8 @@ derive-getters = { workspace = true, features = ["auto_copy_getters"] } sapling-crypto = { workspace = true, features = ["multicore"]} orchard.workspace = true +zcash_primitives.workspace = true + zcash_proofs = { workspace = true, features = ["multicore", "bundled-prover"] } tower-fallback = { path = "../tower-fallback/", version = "0.2.41" } diff --git a/zebra-consensus/src/block.rs b/zebra-consensus/src/block.rs index e75ad458ad6..0db32173c58 100644 --- a/zebra-consensus/src/block.rs +++ b/zebra-consensus/src/block.rs @@ -8,7 +8,7 @@ //! verification, where it may be accepted or rejected. use std::{ - collections::HashSet, + collections::{HashMap, HashSet}, future::Future, pin::Pin, sync::Arc, @@ -281,6 +281,10 @@ where let mut sigops = 0; let mut block_miner_fees = Ok(Amount::zero()); + // Collect tx sighashes during verification and later emit them in block order (for ZSA issuance auth). + let mut tx_sighash_by_tx_id: HashMap = + HashMap::with_capacity(block.transactions.len()); + use futures::StreamExt; while let Some(result) = async_checks.next().await { tracing::trace!(?result, remaining = async_checks.len()); @@ -300,6 +304,13 @@ where if let Some(miner_fee) = response.miner_fee() { block_miner_fees += miner_fee; } + + if let crate::transaction::Response::Block { + tx_id, tx_sighash, .. + } = response + { + tx_sighash_by_tx_id.insert(tx_id, tx_sighash); + } } // Check the summed block totals @@ -332,12 +343,24 @@ where let new_outputs = Arc::into_inner(known_utxos) .expect("all verification tasks using known_utxos are complete"); + // Rebuild sighashes in block order to align with `block.transactions` indexing. + let transaction_sighashes: Arc<[transaction::SigHash]> = block + .transactions + .iter() + .map(|tx| { + *tx_sighash_by_tx_id + .get(&tx.unmined_id()) + .expect("every verified tx must return a sighash") + }) + .collect(); + let prepared_block = zs::SemanticallyVerifiedBlock { block, hash, height, new_outputs, transaction_hashes, + transaction_sighashes: Some(transaction_sighashes), deferred_pool_balance_change: Some(deferred_pool_balance_change), }; diff --git a/zebra-consensus/src/block/check.rs b/zebra-consensus/src/block/check.rs index 0bfaa7d3dc6..11811a1a984 100644 --- a/zebra-consensus/src/block/check.rs +++ b/zebra-consensus/src/block/check.rs @@ -261,25 +261,32 @@ pub fn subsidy_is_valid( if Some(height) == NetworkUpgrade::Nu6_1.activation_height(net) { let lockbox_disbursements = net.lockbox_disbursements(height); - if lockbox_disbursements.is_empty() { - Err(BlockError::Other( - "missing lockbox disbursements for NU6.1 activation block".to_string(), - ))?; + // FIXME: Temporary workaround for configured non-standard test networks, + // including Regtest. After syncing with upstream Zebra v4.2.0, tests + // started failing here when NU6.1 activates with an empty configured + // lockbox disbursement list. The upstream `nu7_nsm_transactions` test + // also hits this when run with `tx_v6`/`nu7` enabled. Revisit later. + // if lockbox_disbursements.is_empty() { + // Err(BlockError::Other( + // "missing lockbox disbursements for NU6.1 activation block".to_string(), + // ))?; + // } + + if !lockbox_disbursements.is_empty() { + deferred_pool_balance_change = lockbox_disbursements.into_iter().try_fold( + deferred_pool_balance_change, + |balance, (addr, expected_amount)| { + if !has_amount(&addr, expected_amount) { + Err(SubsidyError::OneTimeLockboxDisbursementNotFound)?; + } + + balance + .checked_sub(expected_amount) + .ok_or(SubsidyError::Underflow) + }, + )?; } - - deferred_pool_balance_change = lockbox_disbursements.into_iter().try_fold( - deferred_pool_balance_change, - |balance, (addr, expected_amount)| { - if !has_amount(&addr, expected_amount) { - Err(SubsidyError::OneTimeLockboxDisbursementNotFound)?; - } - - balance - .checked_sub(expected_amount) - .ok_or(SubsidyError::Underflow) - }, - )?; - }; + } // Check each funding stream output. funding_streams.into_iter().try_for_each( diff --git a/zebra-consensus/src/error.rs b/zebra-consensus/src/error.rs index d4b97af7ffb..5e4072b6850 100644 --- a/zebra-consensus/src/error.rs +++ b/zebra-consensus/src/error.rs @@ -47,6 +47,10 @@ pub enum TransactionError { #[error("coinbase transaction MUST NOT have the EnableSpendsOrchard flag set")] CoinbaseHasEnableSpendsOrchard, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + #[error("coinbase transaction MUST NOT have the EnableZSA flag set")] + CoinbaseHasEnableZSA, + #[error("coinbase transaction Sapling or Orchard outputs MUST be decryptable with an all-zero outgoing viewing key")] CoinbaseOutputsNotDecryptable, @@ -296,6 +300,9 @@ impl TransactionError { | WrongConsensusBranchId | MissingConsensusBranchId => 100, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + CoinbaseHasEnableZSA => 100, + _other => 0, } } diff --git a/zebra-consensus/src/lib.rs b/zebra-consensus/src/lib.rs index 0c2f3e1f9b1..9a93106ffbf 100644 --- a/zebra-consensus/src/lib.rs +++ b/zebra-consensus/src/lib.rs @@ -56,3 +56,6 @@ pub use router::RouterError; /// A boxed [`std::error::Error`]. pub type BoxError = Box; + +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +mod orchard_zsa; diff --git a/zebra-consensus/src/orchard_zsa.rs b/zebra-consensus/src/orchard_zsa.rs new file mode 100644 index 00000000000..87c2771955a --- /dev/null +++ b/zebra-consensus/src/orchard_zsa.rs @@ -0,0 +1,2 @@ +#[cfg(test)] +mod tests; diff --git a/zebra-consensus/src/orchard_zsa/tests.rs b/zebra-consensus/src/orchard_zsa/tests.rs new file mode 100644 index 00000000000..04b0b355a39 --- /dev/null +++ b/zebra-consensus/src/orchard_zsa/tests.rs @@ -0,0 +1,282 @@ +//! Simulates a full Zebra node’s block-processing pipeline on a predefined Orchard/ZSA workflow. +//! +//! This integration test reads a sequence of serialized regtest blocks (including Orchard burns +//! and ZSA issuance), feeds them through the node’s deserialization, consensus router, and state +//! service exactly as if they arrived from the network, and verifies that each block is accepted +//! (or fails at the injected point) and that the final state is updated correctly. + +use std::{ + collections::{hash_map, HashMap}, + sync::Arc, +}; + +use color_eyre::eyre::{eyre, Report}; +use tokio::time::{timeout, Duration}; +use tower::ServiceExt; + +use orchard::{ + issuance::{ + auth::{IssueValidatingKey, ZSASchnorr}, + {AssetRecord, IssueAction}, + }, + note::{AssetBase, AssetId}, + value::NoteValue, +}; + +use zebra_chain::{ + block::{genesis::regtest_genesis_block, Block, Hash}, + parameters::{testnet::ConfiguredActivationHeights, Network}, + serialization::ZcashDeserialize, +}; + +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +use zebra_chain::orchard_zsa::{AssetState, BurnItem}; + +use zebra_state::{ReadRequest, ReadResponse, ReadStateService}; + +use zebra_test::{ + transcript::{ExpectedTranscriptError, Transcript}, + vectors::{OrchardWorkflowBlock, ORCHARD_ZSA_WORKFLOW_BLOCKS}, +}; + +use crate::{block::Request, Config}; + +type AssetRecords = HashMap; + +type TranscriptItem = (Request, Result); + +#[derive(Debug)] +enum AssetRecordsError { + BurnAssetMissing, + AssetMismatch, + EmptyActionNotFinalized, + AmountOverflow, + AmountUnderflow, + MissingRefNote, + ModifyFinalized, +} + +/// Processes orchard burns, decreasing asset supply. +fn process_burns<'a, I: IntoIterator>( + asset_records: &mut AssetRecords, + burns: I, +) -> Result<(), AssetRecordsError> { + for burn in burns { + let asset_record = asset_records + .get_mut(&burn.asset()) + .ok_or(AssetRecordsError::BurnAssetMissing)?; + + asset_record.amount = NoteValue::from_raw( + asset_record + .amount + .inner() + .checked_sub(burn.amount().inner()) + .ok_or(AssetRecordsError::AmountUnderflow)?, + ); + } + + Ok(()) +} + +/// Processes orchard issue actions, increasing asset supply. +fn process_issue_actions<'a, I: Iterator>( + asset_records: &mut AssetRecords, + ik: &IssueValidatingKey, + actions: I, +) -> Result<(), AssetRecordsError> { + for action in actions { + let action_asset = AssetBase::custom(&AssetId::new_v0(ik, action.asset_desc_hash())); + let reference_note = action.get_reference_note(); + let is_finalized = action.is_finalized(); + + let mut note_amounts = action.notes().iter().map(|note| { + if note.asset() == action_asset { + Ok(note.value()) + } else { + Err(AssetRecordsError::AssetMismatch) + } + }); + + let first_note_amount = match note_amounts.next() { + Some(note_amount) => note_amount, + None => { + if is_finalized { + Ok(NoteValue::from_raw(0)) + } else { + Err(AssetRecordsError::EmptyActionNotFinalized) + } + } + }; + + for amount_result in std::iter::once(first_note_amount).chain(note_amounts) { + let amount = amount_result?; + + match asset_records.entry(action_asset) { + hash_map::Entry::Occupied(mut entry) => { + let asset_record = entry.get_mut(); + asset_record.amount = asset_record + .amount + .inner() + .checked_add(amount.inner()) + .map(NoteValue::from_raw) + .ok_or(AssetRecordsError::AmountOverflow)?; + if asset_record.is_finalized { + return Err(AssetRecordsError::ModifyFinalized); + } + asset_record.is_finalized = is_finalized; + } + + hash_map::Entry::Vacant(entry) => { + entry.insert(AssetRecord { + amount, + is_finalized, + reference_note: *reference_note.ok_or(AssetRecordsError::MissingRefNote)?, + }); + } + } + } + } + + Ok(()) +} + +/// Builds assets records for the given blocks. +fn build_asset_records<'a, I: IntoIterator>( + blocks: I, +) -> Result { + blocks + .into_iter() + .filter_map(|(request, result)| match (request, result) { + (Request::Commit(block), Ok(_)) => Some(&block.transactions), + _ => None, + }) + .flatten() + .try_fold(HashMap::new(), |mut asset_records, tx| { + if let Some(burns) = tx.orchard_burns() { + process_burns(&mut asset_records, burns.iter())?; + } + + if let Some(issue_data) = tx.orchard_zsa_issue_data() { + process_issue_actions( + &mut asset_records, + issue_data.inner().ik(), + issue_data.actions(), + )?; + } + + Ok(asset_records) + }) +} + +/// Creates transcript data from predefined workflow blocks. +fn create_transcript_data<'a, I: IntoIterator>( + serialized_blocks: I, +) -> impl Iterator + use<'a, I> { + let workflow_blocks = serialized_blocks.into_iter().map( + |OrchardWorkflowBlock { + height: _, + bytes, + is_valid, + }| { + ( + Arc::new(Block::zcash_deserialize(&bytes[..]).expect("block should deserialize")), + *is_valid, + ) + }, + ); + + std::iter::once((regtest_genesis_block(), true)) + .chain(workflow_blocks) + .map(|(block, is_valid)| { + ( + Request::Commit(block.clone()), + if is_valid { + Ok(block.hash()) + } else { + Err(ExpectedTranscriptError::Any) + }, + ) + }) +} + +/// Queries the state service for the asset state of the given asset. +async fn request_asset_state( + read_state_service: &ReadStateService, + asset_base: AssetBase, +) -> Option { + let request = ReadRequest::AssetState { + asset_base, + include_non_finalized: true, + }; + + match read_state_service.clone().oneshot(request).await { + Ok(ReadResponse::AssetState(asset_state)) => asset_state, + _ => unreachable!("The state service returned an unexpected response."), + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn check_orchard_zsa_workflow() -> Result<(), Report> { + let _init_guard = zebra_test::init(); + + let network = Network::new_regtest( + ConfiguredActivationHeights { + canopy: Some(1), + nu7: Some(1), + ..Default::default() + } + .into(), + ); + + let (state_service, read_state_service, _, _) = zebra_state::init_test_services(&network).await; + + let (block_verifier_router, ..) = + crate::router::init_test(Config::default(), &network, state_service).await; + + let transcript_data = + create_transcript_data(ORCHARD_ZSA_WORKFLOW_BLOCKS.iter()).collect::>(); + + let asset_records = + build_asset_records(&transcript_data).expect("should calculate asset_records"); + + // Before applying the blocks, ensure that none of the assets exist in the state. + for &asset_base in asset_records.keys() { + assert!( + request_asset_state(&read_state_service, asset_base) + .await + .is_none(), + "State should initially have no info about this asset." + ); + } + + // Verify all blocks in the transcript against the consensus and the state. + timeout( + Duration::from_secs(15), + Transcript::from(transcript_data).check(block_verifier_router), + ) + .await + .map_err(|_| eyre!("Task timed out"))??; + + // After processing the transcript blocks, verify that the state matches the expected supply info. + for (&asset_base, asset_record) in &asset_records { + let asset_state = request_asset_state(&read_state_service, asset_base) + .await + .expect("State should contain this asset now."); + + assert_eq!( + asset_state.is_finalized(), + asset_record.is_finalized, + "Finalized state does not match for asset {:?}.", + asset_base + ); + + assert_eq!( + asset_state.total_supply(), + asset_record.amount.inner(), + "Total supply mismatch for asset {:?}.", + asset_base + ); + } + + Ok(()) +} diff --git a/zebra-consensus/src/primitives/halo2.rs b/zebra-consensus/src/primitives/halo2.rs index aa8fed710d9..a11131d8bfc 100644 --- a/zebra-consensus/src/primitives/halo2.rs +++ b/zebra-consensus/src/primitives/halo2.rs @@ -10,11 +10,14 @@ use std::{ use futures::{future::BoxFuture, FutureExt}; use once_cell::sync::Lazy; -use orchard::{bundle::BatchValidator, circuit::VerifyingKey}; +use orchard::{bundle::BatchValidator, circuit::VerifyingKey, flavor::OrchardVanilla}; use rand::thread_rng; -use zcash_protocol::value::ZatBalance; +use zcash_primitives::transaction::OrchardBundle; use zebra_chain::transaction::SigHash; +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +use orchard::flavor::OrchardZSA; + use crate::BoxError; use thiserror::Error; use tokio::sync::watch; @@ -50,29 +53,38 @@ pub type BatchVerifyingKey = ItemVerifyingKey; pub type ItemVerifyingKey = VerifyingKey; lazy_static::lazy_static! { - /// The halo2 proof verifying key. - pub static ref VERIFYING_KEY: ItemVerifyingKey = ItemVerifyingKey::build(); + /// The halo2 proof verifying key for OrchardVanilla. + pub static ref VERIFYING_KEY_VANILLA: ItemVerifyingKey = + ItemVerifyingKey::build::(); +} + +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +lazy_static::lazy_static! { + /// The halo2 proof verifying key for OrchardZSA. + pub static ref VERIFYING_KEY_ZSA: ItemVerifyingKey = + ItemVerifyingKey::build::(); } /// A Halo2 verification item, used as the request type of the service. #[derive(Clone, Debug)] pub struct Item { - bundle: orchard::bundle::Bundle, + bundle: OrchardBundle, sighash: SigHash, } impl RequestWeight for Item { fn request_weight(&self) -> usize { - self.bundle.actions().len() + match &self.bundle { + OrchardBundle::OrchardVanilla(b) => b.actions().len(), + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + OrchardBundle::OrchardZSA(b) => b.actions().len(), + } } } impl Item { /// Creates a new [`Item`] from a bundle and sighash. - pub fn new( - bundle: orchard::bundle::Bundle, - sighash: SigHash, - ) -> Self { + pub fn new(bundle: OrchardBundle, sighash: SigHash) -> Self { Self { bundle, sighash } } @@ -93,7 +105,11 @@ trait QueueBatchVerify { impl QueueBatchVerify for BatchValidator { fn queue(&mut self, Item { bundle, sighash }: Item) { - self.add_bundle(&bundle, sighash.0); + match bundle { + OrchardBundle::OrchardVanilla(b) => self.add_bundle(&b, sighash.0), + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + OrchardBundle::OrchardZSA(b) => self.add_bundle(&b, sighash.0), + } } } @@ -130,7 +146,7 @@ impl From for Halo2Error { /// Note that making a `Service` call requires mutable access to the service, so /// you should call `.clone()` on the global handle to create a local, mutable /// handle. -pub static VERIFIER: Lazy< +pub static VERIFIER_VANILLA: Lazy< Fallback< Batch, ServiceFn BoxFuture<'static, Result<(), BoxError>>>, @@ -138,7 +154,7 @@ pub static VERIFIER: Lazy< > = Lazy::new(|| { Fallback::new( Batch::new( - Verifier::new(&VERIFYING_KEY), + Verifier::new(&VERIFYING_KEY_VANILLA), HALO2_MAX_BATCH_SIZE, None, super::MAX_BATCH_LATENCY, @@ -153,7 +169,29 @@ pub static VERIFIER: Lazy< // to erase the result type. // (We can't use BoxCloneService to erase the service type, because it is !Sync.) tower::service_fn( - (|item: Item| Verifier::verify_single_spawning(item, &VERIFYING_KEY).boxed()) + (|item: Item| Verifier::verify_single_spawning(item, &VERIFYING_KEY_VANILLA).boxed()) + as fn(_) -> _, + ), + ) +}); + +/// Like [`VERIFIER_VANILLA`], but for OrchardZSA proofs. +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +pub static VERIFIER_ZSA: Lazy< + Fallback< + Batch, + ServiceFn BoxFuture<'static, Result<(), BoxError>>>, + >, +> = Lazy::new(|| { + Fallback::new( + Batch::new( + Verifier::new(&VERIFYING_KEY_ZSA), + HALO2_MAX_BATCH_SIZE, + None, + super::MAX_BATCH_LATENCY, + ), + tower::service_fn( + (|item: Item| Verifier::verify_single_spawning(item, &VERIFYING_KEY_ZSA).boxed()) as fn(_) -> _, ), ) diff --git a/zebra-consensus/src/transaction.rs b/zebra-consensus/src/transaction.rs index ab90d90354a..b7fd6135e29 100644 --- a/zebra-consensus/src/transaction.rs +++ b/zebra-consensus/src/transaction.rs @@ -23,6 +23,8 @@ use tower::{ }; use tracing::Instrument; +use zcash_primitives::transaction::OrchardBundle; + use zcash_protocol::value::ZatBalance; use zebra_chain::{ @@ -204,6 +206,9 @@ pub enum Response { /// The number of legacy signature operations in this transaction's /// transparent inputs and outputs. sigops: u32, + + /// Shielded sighash for this transaction. + tx_sighash: SigHash, }, /// A response to a mempool transaction verification request. @@ -407,7 +412,8 @@ where return Ok(Response::Block { tx_id, miner_fee: Some(verified_tx.miner_fee), - sigops: verified_tx.legacy_sigop_count + sigops: verified_tx.legacy_sigop_count, + tx_sighash: verified_tx.tx_sighash, }); } @@ -494,7 +500,7 @@ where tracing::trace!(?tx_id, "got state UTXOs"); - let mut async_checks = match tx.as_ref() { + let (mut async_checks, tx_sighash) = match tx.as_ref() { Transaction::V1 { .. } | Transaction::V2 { .. } | Transaction::V3 { .. } => { tracing::debug!(?tx, "got transaction with wrong version"); return Err(TransactionError::WrongVersion); @@ -584,6 +590,7 @@ where tx_id, miner_fee, sigops, + tx_sighash }, Request::Mempool { transaction: tx, .. } => { // TODO: `spent_outputs` may not align with `tx.inputs()` when a transaction @@ -597,6 +604,7 @@ where miner_fee.expect("fee should have been checked earlier"), sigops, spent_outputs.into(), + tx_sighash, )?; if let Some(mut mempool) = mempool { @@ -896,7 +904,7 @@ where script_verifier: script::Verifier, cached_ffi_transaction: Arc, joinsplit_data: &Option>, - ) -> Result { + ) -> Result<(AsyncChecks, SigHash), TransactionError> { let tx = request.transaction(); let nu = request.upgrade(network); @@ -908,13 +916,15 @@ where .sighasher() .sighash(HashType::ALL, None); - Ok(Self::verify_transparent_inputs_and_outputs( + let async_check = Self::verify_transparent_inputs_and_outputs( request, script_verifier, cached_ffi_transaction, )? .and(Self::verify_sprout_shielded_data(joinsplit_data, &sighash)?) - .and(Self::verify_sapling_bundle(sapling_bundle, &sighash))) + .and(Self::verify_sapling_bundle(sapling_bundle, &sighash)); + + Ok((async_check, sighash)) } /// Verifies if a V4 `transaction` is supported by `network_upgrade`. @@ -944,7 +954,8 @@ where | NetworkUpgrade::Canopy | NetworkUpgrade::Nu5 | NetworkUpgrade::Nu6 - | NetworkUpgrade::Nu6_1 => Ok(()), + | NetworkUpgrade::Nu6_1 + | NetworkUpgrade::Nu7 => Ok(()), #[cfg(zcash_unstable = "zfuture")] NetworkUpgrade::ZFuture => Ok(()), @@ -952,8 +963,7 @@ where // Does not support V4 transactions NetworkUpgrade::Genesis | NetworkUpgrade::BeforeOverwinter - | NetworkUpgrade::Overwinter - | NetworkUpgrade::Nu7 => Err(TransactionError::UnsupportedByNetworkUpgrade( + | NetworkUpgrade::Overwinter => Err(TransactionError::UnsupportedByNetworkUpgrade( transaction.version(), network_upgrade, )), @@ -985,7 +995,7 @@ where network: &Network, script_verifier: script::Verifier, cached_ffi_transaction: Arc, - ) -> Result { + ) -> Result<(AsyncChecks, SigHash), TransactionError> { let transaction = request.transaction(); let nu = request.upgrade(network); @@ -998,13 +1008,15 @@ where .sighasher() .sighash(HashType::ALL, None); - Ok(Self::verify_transparent_inputs_and_outputs( + let async_check = Self::verify_transparent_inputs_and_outputs( request, script_verifier, cached_ffi_transaction, )? .and(Self::verify_sapling_bundle(sapling_bundle, &sighash)) - .and(Self::verify_orchard_bundle(orchard_bundle, &sighash))) + .and(Self::verify_orchard_bundle(orchard_bundle, &sighash)); + + Ok((async_check, sighash)) } /// Verifies if a V5 `transaction` is supported by `network_upgrade`. @@ -1047,15 +1059,98 @@ where } } - /// Passthrough to verify_v5_transaction, but for V6 transactions. + /// Verify a V5 transaction. + /// + /// Returns a set of asynchronous checks that must all succeed for the transaction to be + /// considered valid. These checks include: + /// + /// - transaction support by the considered network upgrade (see [`Request::upgrade`]) + /// - transparent transfers + /// - sapling shielded data + /// - orchard shielded data + /// + /// The parameters of this method are: + /// + /// - the `request` to verify (that contains the transaction and other metadata, see [`Request`] + /// for more information) + /// - the `network` to consider when verifying + /// - the `script_verifier` to use for verifying the transparent transfers + /// - the prepared `cached_ffi_transaction` used by the script verifier + /// - the sapling shielded data of the transaction, if any + /// - the orchard shielded data of the transaction, if any + // FIXME: This function performs no V6-specific issuance or burn semantic checks + // (ZIP-226 / ZIP-227). Those rules are enforced only in `zebra-state` via + // `IssuedAssetChanges::validate_and_get_changes`. Either move that validation here + // or document the contract that the state layer cannot be bypassed. #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] fn verify_v6_transaction( request: &Request, network: &Network, script_verifier: script::Verifier, cached_ffi_transaction: Arc, - ) -> Result { - Self::verify_v5_transaction(request, network, script_verifier, cached_ffi_transaction) + ) -> Result<(AsyncChecks, SigHash), TransactionError> { + let transaction = request.transaction(); + let nu = request.upgrade(network); + + Self::verify_v6_transaction_network_upgrade(&transaction, nu)?; + + let sapling_bundle = cached_ffi_transaction.sighasher().sapling_bundle(); + let orchard_bundle = cached_ffi_transaction.sighasher().orchard_bundle(); + + let sighash = cached_ffi_transaction + .sighasher() + .sighash(HashType::ALL, None); + + let async_check = Self::verify_transparent_inputs_and_outputs( + request, + script_verifier, + cached_ffi_transaction, + )? + .and(Self::verify_sapling_bundle(sapling_bundle, &sighash)) + .and(Self::verify_orchard_bundle(orchard_bundle, &sighash)); + + Ok((async_check, sighash)) + } + + /// Verifies if a V6 `transaction` is supported by `network_upgrade`. + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + fn verify_v6_transaction_network_upgrade( + transaction: &Transaction, + network_upgrade: NetworkUpgrade, + ) -> Result<(), TransactionError> { + match network_upgrade { + // TODO: update V6 group ID in the comment below after it's chosen + // Supports V6 transactions + // + // # Consensus + // + // > [NU7 onward] The transaction version number MUST be 4, 5, or 6. + // > If the transaction version number is 4 then the version group ID MUST be 0x892F2085. + // > If the transaction version number is 5 then the version group ID MUST be 0x26A7270A. + // > If the transaction version number is 6 then the version group ID MUST be 0x77777777. + // + // Note: Here we verify the transaction version number of the above rule. The version + // group ID is checked in the zebra-chain crate during transaction deserialization. + NetworkUpgrade::Nu7 => Ok(()), + + #[cfg(zcash_unstable = "zfuture")] + NetworkUpgrade::ZFuture => Ok(()), + + // Does not support V6 transactions + NetworkUpgrade::Genesis + | NetworkUpgrade::BeforeOverwinter + | NetworkUpgrade::Overwinter + | NetworkUpgrade::Sapling + | NetworkUpgrade::Blossom + | NetworkUpgrade::Heartwood + | NetworkUpgrade::Canopy + | NetworkUpgrade::Nu5 + | NetworkUpgrade::Nu6 + | NetworkUpgrade::Nu6_1 => Err(TransactionError::UnsupportedByNetworkUpgrade( + transaction.version(), + network_upgrade, + )), + } } /// Verifies if a transaction's transparent inputs are valid using the provided @@ -1225,9 +1320,11 @@ where /// Verifies a transaction's Orchard shielded data. fn verify_orchard_bundle( - bundle: Option<::orchard::bundle::Bundle<::orchard::bundle::Authorized, ZatBalance>>, + bundle: Option>, sighash: &SigHash, ) -> AsyncChecks { + use zcash_primitives::transaction::OrchardBundle; + let mut async_checks = AsyncChecks::new(); if let Some(bundle) = bundle { @@ -1242,11 +1339,20 @@ where // aggregated Halo2 proof per transaction, even with multiple // Actions in one transaction. So we queue it for verification // only once instead of queuing it up for every Action description. - async_checks.push( - primitives::halo2::VERIFIER + let item = primitives::halo2::Item::new(bundle.clone(), *sighash); + let check = match &bundle { + OrchardBundle::OrchardVanilla(_) => primitives::halo2::VERIFIER_VANILLA .clone() - .oneshot(primitives::halo2::Item::new(bundle, *sighash)), - ); + .oneshot(item) + .boxed(), + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + OrchardBundle::OrchardZSA(_) => primitives::halo2::VERIFIER_ZSA + .clone() + .oneshot(item) + .boxed(), + }; + + async_checks.push(check); } async_checks diff --git a/zebra-consensus/src/transaction/check.rs b/zebra-consensus/src/transaction/check.rs index ecdc66197bc..411d0857a61 100644 --- a/zebra-consensus/src/transaction/check.rs +++ b/zebra-consensus/src/transaction/check.rs @@ -178,10 +178,15 @@ pub fn coinbase_tx_no_prevout_joinsplit_spend(tx: &Transaction) -> Result<(), Tr return Err(TransactionError::CoinbaseHasSpend); } - if let Some(orchard_shielded_data) = tx.orchard_shielded_data() { - if orchard_shielded_data.flags.contains(Flags::ENABLE_SPENDS) { + if let Some(orchard_flags) = tx.orchard_flags() { + if orchard_flags.contains(Flags::ENABLE_SPENDS) { return Err(TransactionError::CoinbaseHasEnableSpendsOrchard); } + // ZIP-230: coinbase must not set enableZSA. + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + if orchard_flags.contains(Flags::ENABLE_ZSA) { + return Err(TransactionError::CoinbaseHasEnableZSA); + } } } diff --git a/zebra-consensus/src/transaction/tests.rs b/zebra-consensus/src/transaction/tests.rs index 6a1890241f2..0e52fb91d62 100644 --- a/zebra-consensus/src/transaction/tests.rs +++ b/zebra-consensus/src/transaction/tests.rs @@ -20,7 +20,7 @@ use tower::{buffer::Buffer, service_fn, ServiceExt}; use zebra_chain::{ amount::{Amount, NonNegative}, block::{self, Block, Height}, - orchard::{Action, AuthorizedAction, Flags}, + orchard::{Action, AuthorizedAction, Flags, OrchardVanilla}, parameters::{testnet::ConfiguredActivationHeights, Network, NetworkUpgrade}, primitives::{ed25519, x25519, Groth16Proof}, sapling, @@ -28,7 +28,7 @@ use zebra_chain::{ sprout, transaction::{ arbitrary::{ - insert_fake_orchard_shielded_data, test_transactions, transactions_from_blocks, + insert_fake_v5_orchard_shielded_data, test_transactions, transactions_from_blocks, v5_transactions, }, zip317, Hash, HashType, JoinSplitData, LockTime, Transaction, @@ -36,6 +36,9 @@ use zebra_chain::{ transparent::{self, CoinbaseData, CoinbaseSpendRestriction}, }; +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +use zebra_chain::transaction::arbitrary::insert_fake_v6_orchard_shielded_data; + use zebra_node_services::mempool; use zebra_state::ValidateContextError; use zebra_test::mock_service::MockService; @@ -92,7 +95,7 @@ fn v5_transaction_with_orchard_actions_has_inputs_and_outputs() { }) .expect("V5 tx with only Orchard shielded data"); - tx.orchard_shielded_data_mut().unwrap().flags = Flags::empty(); + *tx.orchard_flags_mut().unwrap() = Flags::empty(); // The check will fail if the transaction has no flags assert_eq!( @@ -101,7 +104,7 @@ fn v5_transaction_with_orchard_actions_has_inputs_and_outputs() { ); // If we add ENABLE_SPENDS flag it will pass the inputs check but fails with the outputs - tx.orchard_shielded_data_mut().unwrap().flags = Flags::ENABLE_SPENDS; + *tx.orchard_flags_mut().unwrap() = Flags::ENABLE_SPENDS; assert_eq!( check::has_inputs_and_outputs(&tx), @@ -109,7 +112,7 @@ fn v5_transaction_with_orchard_actions_has_inputs_and_outputs() { ); // If we add ENABLE_OUTPUTS flag it will pass the outputs check but fails with the inputs - tx.orchard_shielded_data_mut().unwrap().flags = Flags::ENABLE_OUTPUTS; + *tx.orchard_flags_mut().unwrap() = Flags::ENABLE_OUTPUTS; assert_eq!( check::has_inputs_and_outputs(&tx), @@ -117,8 +120,7 @@ fn v5_transaction_with_orchard_actions_has_inputs_and_outputs() { ); // Finally make it valid by adding both required flags - tx.orchard_shielded_data_mut().unwrap().flags = - Flags::ENABLE_SPENDS | Flags::ENABLE_OUTPUTS; + *tx.orchard_flags_mut().unwrap() = Flags::ENABLE_SPENDS | Flags::ENABLE_OUTPUTS; assert!(check::has_inputs_and_outputs(&tx).is_ok()); } @@ -137,7 +139,7 @@ fn v5_transaction_with_orchard_actions_has_flags() { }) .expect("V5 tx with only Orchard actions"); - tx.orchard_shielded_data_mut().unwrap().flags = Flags::empty(); + *tx.orchard_flags_mut().unwrap() = Flags::empty(); // The check will fail if the transaction has no flags assert_eq!( @@ -146,20 +148,19 @@ fn v5_transaction_with_orchard_actions_has_flags() { ); // If we add ENABLE_SPENDS flag it will pass. - tx.orchard_shielded_data_mut().unwrap().flags = Flags::ENABLE_SPENDS; + *tx.orchard_flags_mut().unwrap() = Flags::ENABLE_SPENDS; assert!(check::has_enough_orchard_flags(&tx).is_ok()); - tx.orchard_shielded_data_mut().unwrap().flags = Flags::empty(); + *tx.orchard_flags_mut().unwrap() = Flags::empty(); // If we add ENABLE_OUTPUTS flag instead, it will pass. - tx.orchard_shielded_data_mut().unwrap().flags = Flags::ENABLE_OUTPUTS; + *tx.orchard_flags_mut().unwrap() = Flags::ENABLE_OUTPUTS; assert!(check::has_enough_orchard_flags(&tx).is_ok()); - tx.orchard_shielded_data_mut().unwrap().flags = Flags::empty(); + *tx.orchard_flags_mut().unwrap() = Flags::empty(); // If we add BOTH ENABLE_SPENDS and ENABLE_OUTPUTS flags it will pass. - tx.orchard_shielded_data_mut().unwrap().flags = - Flags::ENABLE_SPENDS | Flags::ENABLE_OUTPUTS; + *tx.orchard_flags_mut().unwrap() = Flags::ENABLE_SPENDS | Flags::ENABLE_OUTPUTS; assert!(check::has_enough_orchard_flags(&tx).is_ok()); } } @@ -1220,7 +1221,7 @@ fn v5_coinbase_transaction_without_enable_spends_flag_passes_validation() { .find(|transaction| transaction.is_coinbase()) .expect("V5 coinbase tx"); - let shielded_data = insert_fake_orchard_shielded_data(&mut tx); + let shielded_data = insert_fake_v5_orchard_shielded_data(&mut tx); assert!(!shielded_data.flags.contains(Flags::ENABLE_SPENDS)); @@ -1235,7 +1236,7 @@ fn v5_coinbase_transaction_with_enable_spends_flag_fails_validation() { .find(|transaction| transaction.is_coinbase()) .expect("V5 coinbase tx"); - let shielded_data = insert_fake_orchard_shielded_data(&mut tx); + let shielded_data = insert_fake_v5_orchard_shielded_data(&mut tx); assert!(!shielded_data.flags.contains(Flags::ENABLE_SPENDS)); @@ -1248,6 +1249,40 @@ fn v5_coinbase_transaction_with_enable_spends_flag_fails_validation() { } } +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +#[test] +fn v6_coinbase_transaction_with_enable_zsa_flag_fails_validation() { + let network = Network::new_regtest( + ConfiguredActivationHeights { + canopy: Some(1), + nu7: Some(1), + ..Default::default() + } + .into(), + ); + + let outputs = vec![(Amount::zero(), transparent::Script::new(Default::default()))]; + + let mut tx = Transaction::new_v6_coinbase( + &network, + Height(1), + outputs, + Vec::new(), + Some(Amount::zero()), + ); + + let shielded_data = insert_fake_v6_orchard_shielded_data(&mut tx); + + assert!(!shielded_data.flags.contains(Flags::ENABLE_ZSA)); + + shielded_data.flags = Flags::ENABLE_ZSA; + + assert_eq!( + check::coinbase_tx_no_prevout_joinsplit_spend(&tx), + Err(TransactionError::CoinbaseHasEnableZSA) + ); +} + #[tokio::test] async fn v5_transaction_is_rejected_before_nu5_activation() { let sapling = NetworkUpgrade::Sapling; @@ -2822,7 +2857,7 @@ async fn v5_with_duplicate_orchard_action() { let height = tx.expiry_height().expect("expiry height"); let orchard_shielded_data = tx - .orchard_shielded_data_mut() + .v5_orchard_shielded_data_mut() .expect("tx without transparent, Sprout, or Sapling outputs must have Orchard actions"); // Enable spends @@ -3438,9 +3473,9 @@ fn coinbase_outputs_are_decryptable() -> Result<(), Report> { /// Given an Orchard action as a base, fill fields related to note encryption /// from the given test vector and returned the modified action. fn fill_action_with_note_encryption_test_vector( - action: &Action, + action: &Action, v: &zebra_test::vectors::TestVector, -) -> Action { +) -> Action { let mut action = action.clone(); action.cv = v.cv_net.try_into().expect("test vector must be valid"); action.cm_x = pallas::Base::from_repr(v.cmx).unwrap(); @@ -3463,7 +3498,7 @@ fn coinbase_outputs_are_decryptable_for_fake_v5_blocks() { .find(|tx| tx.is_coinbase()) .expect("coinbase V5 tx"); - let shielded_data = insert_fake_orchard_shielded_data(&mut transaction); + let shielded_data = insert_fake_v5_orchard_shielded_data(&mut transaction); shielded_data.flags = Flags::ENABLE_OUTPUTS; let action = fill_action_with_note_encryption_test_vector( @@ -3496,7 +3531,7 @@ fn shielded_outputs_are_not_decryptable_for_fake_v5_blocks() { .find(|tx| tx.is_coinbase()) .expect("V5 coinbase tx"); - let shielded_data = insert_fake_orchard_shielded_data(&mut tx); + let shielded_data = insert_fake_v5_orchard_shielded_data(&mut tx); shielded_data.flags = Flags::ENABLE_OUTPUTS; let action = fill_action_with_note_encryption_test_vector( diff --git a/zebra-consensus/src/transaction/tests/prop.rs b/zebra-consensus/src/transaction/tests/prop.rs index 02b0c4b0c5f..09633682b8c 100644 --- a/zebra-consensus/src/transaction/tests/prop.rs +++ b/zebra-consensus/src/transaction/tests/prop.rs @@ -333,6 +333,7 @@ fn mock_transparent_transaction( zip233_amount, sapling_shielded_data: None, orchard_shielded_data: None, + orchard_zsa_issue_data: None, network_upgrade, }, invalid_version => unreachable!("invalid transaction version: {}", invalid_version), diff --git a/zebra-rpc/src/methods.rs b/zebra-rpc/src/methods.rs index 76ee6b3e2bc..df8d1645c7b 100644 --- a/zebra-rpc/src/methods.rs +++ b/zebra-rpc/src/methods.rs @@ -59,7 +59,7 @@ use tower::ServiceExt; use tracing::Instrument; use zcash_address::{unified::Encoding, TryFromAddress}; -use zcash_primitives::consensus::Parameters; +use zcash_protocol::consensus::Parameters; use zebra_chain::{ amount::{Amount, NegativeAllowed}, @@ -167,10 +167,23 @@ pub(super) const PARAM_VERBOSITY_DESC: &str = "Whether to include verbose output pub(super) const PARAM_N_DESC: &str = "The output index in the transaction."; pub(super) const PARAM_INCLUDE_MEMPOOL_DESC: &str = "Whether to include mempool transactions in the response."; +pub(super) const PARAM_ASSET_BASE_DESC: &str = + "The asset base as 32 bytes encoded as 64 hex characters."; +pub(super) const PARAM_INCLUDE_NON_FINALIZED_DESC: &str = + "Whether to query the best chain tip including non-finalized state. Defaults to true."; #[cfg(test)] mod tests; +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +use zebra_chain::orchard_zsa::AssetState; + +/// Dummy type to allow `get_asset_state` to be declared in the `Rpc` trait unconditionally, +/// since `zebra_chain::orchard_zsa::AssetState` is not available without these flags. +#[cfg(not(all(zcash_unstable = "nu7", feature = "tx_v6")))] +#[derive(Clone, serde::Serialize, serde::Deserialize)] +pub struct AssetState; + #[rpc(server)] /// RPC method signatures. pub trait Rpc { @@ -459,6 +472,23 @@ pub trait Rpc { request: GetAddressUtxosRequest, ) -> Result; + /// Returns the asset state of the provided asset base at the best chain tip or finalized chain tip. + /// + /// # Parameters + /// + /// - `asset_base`: hex-encoded 32-byte asset base to query. + /// - `include_non_finalized`: if `true`, query the best chain tip, including non-finalized state; + /// if `false`, query only the finalized chain tip. + /// + /// method: post + /// tags: blockchain + #[method(name = "getassetstate")] + async fn get_asset_state( + &self, + asset_base: String, + include_non_finalized: Option, + ) -> Result; + /// Stop the running zebrad process. /// /// # Notes @@ -1879,7 +1909,7 @@ where let time = u32::try_from(block.header.time.timestamp()) .expect("Timestamps of valid blocks always fit into u32."); - let sapling_nu = zcash_primitives::consensus::NetworkUpgrade::Sapling; + let sapling_nu = zcash_protocol::consensus::NetworkUpgrade::Sapling; let sapling = if network.is_nu_active(sapling_nu, height.into()) { match read_state .ready() @@ -1900,7 +1930,7 @@ where let (sapling_tree, sapling_root) = sapling.map_or((None, None), |(tree, root)| (Some(tree), Some(root))); - let orchard_nu = zcash_primitives::consensus::NetworkUpgrade::Nu5; + let orchard_nu = zcash_protocol::consensus::NetworkUpgrade::Nu5; let orchard = if network.is_nu_active(orchard_nu, height.into()) { match read_state .ready() @@ -2123,6 +2153,60 @@ where } } + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + async fn get_asset_state( + &self, + asset_base: String, + include_non_finalized: Option, + ) -> Result { + let read_state = self.read_state.clone(); + let include_non_finalized = include_non_finalized.unwrap_or(true); + + if asset_base.len() != 64 { + return Err("expected 32 bytes (64 hex chars)") + .map_error(server::error::LegacyCode::InvalidParameter); + } + + let asset_base_bytes: [u8; 32] = hex::decode(&asset_base) + .map_error_with_prefix( + server::error::LegacyCode::InvalidParameter, + "invalid hex encoding", + )? + .try_into() + .expect("length already checked above"); + + let asset_base = zebra_chain::orchard_zsa::AssetBase::from_bytes(&asset_base_bytes) + .into_option() + .ok_or_error( + server::error::LegacyCode::InvalidParameter, + "invalid asset base", + )?; + + let request = zebra_state::ReadRequest::AssetState { + asset_base, + include_non_finalized, + }; + + let zebra_state::ReadResponse::AssetState(asset_state) = + read_state.oneshot(request).await.map_misc_error()? + else { + unreachable!("unexpected response from state service"); + }; + + asset_state.ok_or_misc_error("asset base not found") + } + + // Dummy implementation required to satisfy the `Rpc` trait when the real + // `AssetState` type and implementation are not compiled in. + #[cfg(not(all(zcash_unstable = "nu7", feature = "tx_v6")))] + async fn get_asset_state( + &self, + _asset_base: String, + _include_non_finalized: Option, + ) -> Result { + Err(ErrorCode::MethodNotFound.into()) + } + fn stop(&self) -> Result { #[cfg(not(target_os = "windows"))] if self.network.is_regtest() { diff --git a/zebra-rpc/src/methods/tests/snapshot.rs b/zebra-rpc/src/methods/tests/snapshot.rs index 44dafc3fae2..e85d063a0b9 100644 --- a/zebra-rpc/src/methods/tests/snapshot.rs +++ b/zebra-rpc/src/methods/tests/snapshot.rs @@ -35,6 +35,10 @@ use zebra_chain::{ transaction::Transaction, work::difficulty::CompactDifficulty, }; + +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +use zebra_chain::orchard_zsa::{mock_asset_base, mock_asset_state}; + use zebra_consensus::Request; use zebra_network::{ address_book_peers::MockAddressBookPeers, @@ -691,6 +695,94 @@ async fn test_mocked_rpc_response_data_for_network(network: &Network) { settings.bind(|| { insta::assert_json_snapshot!(format!("z_get_subtrees_by_index_for_orchard"), subtrees) }); + + // Test the response format from `getassetstate`. + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + { + // Wrong length: asset base hex must be exactly 64 characters. + let req = rpc.get_asset_state("abcd".to_string(), None); + let wrong_length = req + .await + .expect_err("The RPC response should be an error for wrong-length asset base"); + + settings + .bind(|| insta::assert_json_snapshot!("get_asset_state_wrong_length", wrong_length)); + + // Invalid hex: 64 chars but not valid hex. + let req = rpc.get_asset_state("zz".repeat(32), None); + let invalid_hex = req + .await + .expect_err("The RPC response should be an error for invalid hex"); + + settings.bind(|| insta::assert_json_snapshot!("get_asset_state_invalid_hex", invalid_hex)); + + // Invalid curve point: valid 64-char hex, but not a valid Pallas point. + let req = rpc.get_asset_state("ff".repeat(32), None); + let invalid_asset_base = req + .await + .expect_err("The RPC response should be an error for invalid asset base"); + + settings.bind(|| { + insta::assert_json_snapshot!("get_asset_state_invalid_asset_base", invalid_asset_base) + }); + + // Prepare the state response and make the RPC request. + let asset_base = mock_asset_base(b"Asset1"); + let rsp = read_state + .expect_request_that(|req| matches!(req, ReadRequest::AssetState { .. })) + .map(|responder| responder.respond(ReadResponse::AssetState(None))); + let req = rpc.get_asset_state(hex::encode(asset_base.to_bytes()), None); + + // Get the RPC error response. + let (asset_state_rsp, ..) = tokio::join!(req, rsp); + let asset_state = asset_state_rsp.expect_err("The RPC response should be an error"); + + // Check the error response. + settings.bind(|| { + insta::assert_json_snapshot!(format!("get_asset_state_not_found"), asset_state) + }); + + // Query the same asset with include_non_finalized=false, verifying it only checks + // finalized state. + let asset_base = mock_asset_base(b"Asset1"); + let rsp = read_state + .expect_request_that(|req| { + matches!( + req, + ReadRequest::AssetState { + asset_base: _, + include_non_finalized: false, + } + ) + }) + .map(|responder| responder.respond(ReadResponse::AssetState(None))); + let req = rpc.get_asset_state(hex::encode(asset_base.to_bytes()), Some(false)); + + let (asset_state_rsp, ..) = tokio::join!(req, rsp); + let asset_state = asset_state_rsp.expect_err( + "The RPC response should be an error when the asset is absent from finalized state", + ); + + settings.bind(|| { + insta::assert_json_snapshot!("get_asset_state_not_found_finalized_only", asset_state) + }); + + // Prepare the state response and make the RPC request. + let asset_base = mock_asset_base(b"Asset2"); + let asset_state = mock_asset_state(b"Asset2", 1000, true); + let rsp = read_state + .expect_request_that(|req| matches!(req, ReadRequest::AssetState { .. })) + .map(|responder| responder.respond(ReadResponse::AssetState(Some(asset_state)))); + let req = rpc.get_asset_state(hex::encode(asset_base.to_bytes()), None); + + // Get the RPC response. + let (asset_state_rsp, ..) = tokio::join!(req, rsp); + let asset_state = + asset_state_rsp.expect("The RPC response should contain a `AssetState` struct."); + + // Check the response. + settings.bind(|| insta::assert_json_snapshot!(format!("get_asset_state"), asset_state)); + } } /// Snapshot `getinfo` response, using `cargo insta` and JSON serialization. diff --git a/zebra-rpc/src/methods/tests/snapshots/get_asset_state@mainnet.snap b/zebra-rpc/src/methods/tests/snapshots/get_asset_state@mainnet.snap new file mode 100644 index 00000000000..d253ced0039 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_asset_state@mainnet.snap @@ -0,0 +1,9 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: asset_state +--- +{ + "amount": 1000, + "is_finalized": true, + "reference_note": "cc36601959213b6b0cdb96a75c17c3a668a97f0d6a8c5ce164a518ea9ba9a50ea75191fd861b0ff10e62b000000000000000007dd2affe87ca0930d034f1434442d34621b68c579d60cfb7b87988329c9e262d447ea48052db6f5fcd7668cee86b48ed21b4e5d03a01f7aba7a9dcd95803491d" +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_asset_state@testnet.snap b/zebra-rpc/src/methods/tests/snapshots/get_asset_state@testnet.snap new file mode 100644 index 00000000000..d253ced0039 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_asset_state@testnet.snap @@ -0,0 +1,9 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: asset_state +--- +{ + "amount": 1000, + "is_finalized": true, + "reference_note": "cc36601959213b6b0cdb96a75c17c3a668a97f0d6a8c5ce164a518ea9ba9a50ea75191fd861b0ff10e62b000000000000000007dd2affe87ca0930d034f1434442d34621b68c579d60cfb7b87988329c9e262d447ea48052db6f5fcd7668cee86b48ed21b4e5d03a01f7aba7a9dcd95803491d" +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_asset_state_invalid_asset_base@mainnet.snap b/zebra-rpc/src/methods/tests/snapshots/get_asset_state_invalid_asset_base@mainnet.snap new file mode 100644 index 00000000000..af592f97be1 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_asset_state_invalid_asset_base@mainnet.snap @@ -0,0 +1,9 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +assertion_line: 732 +expression: invalid_asset_base +--- +{ + "code": -8, + "message": "invalid asset base" +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_asset_state_invalid_asset_base@testnet.snap b/zebra-rpc/src/methods/tests/snapshots/get_asset_state_invalid_asset_base@testnet.snap new file mode 100644 index 00000000000..af592f97be1 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_asset_state_invalid_asset_base@testnet.snap @@ -0,0 +1,9 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +assertion_line: 732 +expression: invalid_asset_base +--- +{ + "code": -8, + "message": "invalid asset base" +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_asset_state_invalid_hex@mainnet.snap b/zebra-rpc/src/methods/tests/snapshots/get_asset_state_invalid_hex@mainnet.snap new file mode 100644 index 00000000000..e23ef4ca54c --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_asset_state_invalid_hex@mainnet.snap @@ -0,0 +1,9 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +assertion_line: 720 +expression: invalid_hex +--- +{ + "code": -8, + "message": "invalid hex encoding: Invalid character 'z' at position 0" +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_asset_state_invalid_hex@testnet.snap b/zebra-rpc/src/methods/tests/snapshots/get_asset_state_invalid_hex@testnet.snap new file mode 100644 index 00000000000..e23ef4ca54c --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_asset_state_invalid_hex@testnet.snap @@ -0,0 +1,9 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +assertion_line: 720 +expression: invalid_hex +--- +{ + "code": -8, + "message": "invalid hex encoding: Invalid character 'z' at position 0" +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_asset_state_not_found@mainnet.snap b/zebra-rpc/src/methods/tests/snapshots/get_asset_state_not_found@mainnet.snap new file mode 100644 index 00000000000..0082f7209c9 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_asset_state_not_found@mainnet.snap @@ -0,0 +1,9 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +assertion_line: 711 +expression: asset_state +--- +{ + "code": -1, + "message": "asset base not found" +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_asset_state_not_found@testnet.snap b/zebra-rpc/src/methods/tests/snapshots/get_asset_state_not_found@testnet.snap new file mode 100644 index 00000000000..0082f7209c9 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_asset_state_not_found@testnet.snap @@ -0,0 +1,9 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +assertion_line: 711 +expression: asset_state +--- +{ + "code": -1, + "message": "asset base not found" +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_asset_state_not_found_finalized_only@mainnet.snap b/zebra-rpc/src/methods/tests/snapshots/get_asset_state_not_found_finalized_only@mainnet.snap new file mode 100644 index 00000000000..68237f065e9 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_asset_state_not_found_finalized_only@mainnet.snap @@ -0,0 +1,9 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +assertion_line: 774 +expression: asset_state +--- +{ + "code": -1, + "message": "asset base not found" +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_asset_state_not_found_finalized_only@testnet.snap b/zebra-rpc/src/methods/tests/snapshots/get_asset_state_not_found_finalized_only@testnet.snap new file mode 100644 index 00000000000..68237f065e9 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_asset_state_not_found_finalized_only@testnet.snap @@ -0,0 +1,9 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +assertion_line: 774 +expression: asset_state +--- +{ + "code": -1, + "message": "asset base not found" +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_asset_state_wrong_length@mainnet.snap b/zebra-rpc/src/methods/tests/snapshots/get_asset_state_wrong_length@mainnet.snap new file mode 100644 index 00000000000..43c8a3cffa4 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_asset_state_wrong_length@mainnet.snap @@ -0,0 +1,9 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +assertion_line: 710 +expression: wrong_length +--- +{ + "code": -8, + "message": "expected 32 bytes (64 hex chars)" +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_asset_state_wrong_length@testnet.snap b/zebra-rpc/src/methods/tests/snapshots/get_asset_state_wrong_length@testnet.snap new file mode 100644 index 00000000000..43c8a3cffa4 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_asset_state_wrong_length@testnet.snap @@ -0,0 +1,9 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +assertion_line: 710 +expression: wrong_length +--- +{ + "code": -8, + "message": "expected 32 bytes (64 hex chars)" +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_2@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_2@mainnet_10.snap index 3392fb6ab50..5f77942899a 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_2@mainnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_2@mainnet_10.snap @@ -1,5 +1,6 @@ --- source: zebra-rpc/src/methods/tests/snapshot.rs +assertion_line: 867 expression: block --- { diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_2@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_2@testnet_10.snap index f262aa44b00..ccc78aad123 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_2@testnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_2@testnet_10.snap @@ -1,5 +1,6 @@ --- source: zebra-rpc/src/methods/tests/snapshot.rs +assertion_line: 867 expression: block --- { diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_2@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_2@mainnet_10.snap index 3392fb6ab50..5f77942899a 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_2@mainnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_2@mainnet_10.snap @@ -1,5 +1,6 @@ --- source: zebra-rpc/src/methods/tests/snapshot.rs +assertion_line: 867 expression: block --- { diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_2@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_2@testnet_10.snap index f262aa44b00..ccc78aad123 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_2@testnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_2@testnet_10.snap @@ -1,5 +1,6 @@ --- source: zebra-rpc/src/methods/tests/snapshot.rs +assertion_line: 867 expression: block --- { diff --git a/zebra-rpc/src/methods/tests/snapshots/getrawtransaction_verbosity=1@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/getrawtransaction_verbosity=1@mainnet_10.snap index eb954bf134e..4cc359fdeeb 100644 --- a/zebra-rpc/src/methods/tests/snapshots/getrawtransaction_verbosity=1@mainnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/getrawtransaction_verbosity=1@mainnet_10.snap @@ -1,5 +1,6 @@ --- source: zebra-rpc/src/methods/tests/snapshot.rs +assertion_line: 529 expression: rsp --- { diff --git a/zebra-rpc/src/methods/tests/snapshots/getrawtransaction_verbosity=1@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/getrawtransaction_verbosity=1@testnet_10.snap index e29b6f2344a..421a7e40bbb 100644 --- a/zebra-rpc/src/methods/tests/snapshots/getrawtransaction_verbosity=1@testnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/getrawtransaction_verbosity=1@testnet_10.snap @@ -1,5 +1,6 @@ --- source: zebra-rpc/src/methods/tests/snapshot.rs +assertion_line: 529 expression: rsp --- { diff --git a/zebra-rpc/src/methods/tests/vectors.rs b/zebra-rpc/src/methods/tests/vectors.rs index a1f26083fe0..c73197899ee 100644 --- a/zebra-rpc/src/methods/tests/vectors.rs +++ b/zebra-rpc/src/methods/tests/vectors.rs @@ -23,7 +23,7 @@ use zebra_chain::{ NetworkKind, }, serialization::{DateTime32, ZcashDeserializeInto, ZcashSerialize}, - transaction::{zip317, UnminedTxId, VerifiedUnminedTx}, + transaction::{zip317, SigHash, UnminedTxId, VerifiedUnminedTx}, work::difficulty::{CompactDifficulty, ExpandedDifficulty, ParameterDifficulty as _, U256}, }; use zebra_consensus::MAX_BLOCK_SIGOPS; @@ -2323,6 +2323,7 @@ async fn gbt_with(net: Network, addr: ZcashAddress) { time: None, height: None, spent_outputs: std::sync::Arc::new(vec![]), + tx_sighash: SigHash([0; 32]), }; let next_fake_tip_hash = diff --git a/zebra-rpc/src/methods/types/get_block_template/zip317.rs b/zebra-rpc/src/methods/types/get_block_template/zip317.rs index 829030840f8..5747cf18e88 100644 --- a/zebra-rpc/src/methods/types/get_block_template/zip317.rs +++ b/zebra-rpc/src/methods/types/get_block_template/zip317.rs @@ -27,10 +27,10 @@ use zebra_node_services::mempool::TransactionDependencies; use crate::methods::types::transaction::TransactionTemplate; #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] -use crate::methods::{Amount, NonNegative}; +use crate::methods::Amount; #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] -use zebra_chain::parameters::NetworkUpgrade; +use zebra_chain::{amount::NonNegative, parameters::NetworkUpgrade}; #[cfg(test)] mod tests; diff --git a/zebra-rpc/src/methods/types/transaction.rs b/zebra-rpc/src/methods/types/transaction.rs index 3c1e835defb..4e0e40e289f 100644 --- a/zebra-rpc/src/methods/types/transaction.rs +++ b/zebra-rpc/src/methods/types/transaction.rs @@ -10,7 +10,7 @@ use hex::ToHex; use zcash_script::script::Asm; use zebra_chain::{ - amount::{self, Amount, NegativeOrZero, NonNegative}, + amount::{self, Amount, NegativeAllowed, NegativeOrZero, NonNegative}, block::{self, merkle::AUTH_DIGEST_PLACEHOLDER, Height}, orchard, parameters::Network, @@ -299,6 +299,25 @@ pub struct TransactionObject { #[serde(rename = "blocktime", skip_serializing_if = "Option::is_none")] #[getter(copy)] pub(crate) block_time: Option, + + /// The burn amount for this transaction, if any. + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + #[serde( + rename = "zip233amount", + skip_serializing_if = "Option::is_none", + default + )] + #[getter(copy)] + pub(crate) zip233_amount: Option>, + + /// Whether this transaction contains OrchardZSA issuance data. + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + #[serde( + rename = "issuanceexists", + default, + skip_serializing_if = "Option::is_none" + )] + pub(crate) issuance_exists: Option, } /// The transparent input of a transaction. @@ -558,7 +577,7 @@ impl ShieldedOutput { /// Object with Orchard-specific information. #[serde_with::serde_as] -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)] +#[derive(Clone, Debug, PartialEq, Default, serde::Serialize, serde::Deserialize, Getters, new)] pub struct Orchard { /// Array of Orchard actions. actions: Vec, @@ -586,6 +605,64 @@ pub struct Orchard { #[serde_as(as = "Option")] #[getter(copy)] binding_sig: Option<[u8; 64]>, + /// Whether OrchardZSA burn data is present. + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + #[serde( + rename = "burnexists", + default, + skip_serializing_if = "Option::is_none" + )] + pub(crate) burn_exists: Option, +} + +impl Orchard { + /// Constructs an [`Orchard`] from [`orchard::ShieldedData`]. + /// Works for both `OrchardVanilla` and `OrchardZSA`. + fn from_shielded_data( + shielded_data: &orchard::ShieldedData, + value_balance: Amount, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] burn_exists: Option, + ) -> Self { + Self { + actions: shielded_data + .actions + .iter() + .map(|authorized| { + let spend_auth_sig: [u8; 64] = authorized.spend_auth_sig.into(); + let cv: [u8; 32] = authorized.action.cv.into(); + let nullifier: [u8; 32] = authorized.action.nullifier.into(); + let rk: [u8; 32] = authorized.action.rk.into(); + let cm_x: [u8; 32] = authorized.action.cm_x.into(); + let ephemeral_key: [u8; 32] = authorized.action.ephemeral_key.into(); + let enc_ciphertext = authorized.action.enc_ciphertext.as_ref().to_vec(); + let out_ciphertext: [u8; 80] = authorized.action.out_ciphertext.into(); + OrchardAction { + cv, + nullifier, + rk, + cm_x, + ephemeral_key, + enc_ciphertext, + spend_auth_sig, + out_ciphertext, + } + }) + .collect(), + value_balance: Zec::from(value_balance).lossy_zec(), + value_balance_zat: value_balance.zatoshis(), + flags: Some(OrchardFlags::new( + shielded_data.flags.contains(orchard::Flags::ENABLE_OUTPUTS), + shielded_data.flags.contains(orchard::Flags::ENABLE_SPENDS), + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + shielded_data.flags.contains(orchard::Flags::ENABLE_ZSA), + )), + anchor: Some(shielded_data.shared_anchor.bytes_in_display_order()), + proof: Some(shielded_data.proof.bytes_in_display_order()), + binding_sig: Some(shielded_data.binding_sig.into()), + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + burn_exists, + } + } } /// Object with Orchard-specific information. @@ -597,6 +674,10 @@ pub struct OrchardFlags { /// Whether Orchard spends are enabled. #[serde(rename = "enableSpends")] enable_spends: bool, + /// Whether OrchardZSA is enabled. + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + #[serde(rename = "enableZSA")] + enable_zsa: bool, } /// The Orchard action of a transaction. @@ -619,8 +700,8 @@ pub struct OrchardAction { #[serde(rename = "ephemeralKey", with = "hex")] ephemeral_key: [u8; 32], /// The output note encrypted to the recipient. - #[serde(rename = "encCiphertext", with = "arrayhex")] - enc_ciphertext: [u8; 580], + #[serde(rename = "encCiphertext", with = "hex")] + enc_ciphertext: Vec, /// A ciphertext enabling the sender to recover the output note. #[serde(rename = "spendAuthSig", with = "hex")] spend_auth_sig: [u8; 64], @@ -660,6 +741,10 @@ impl Default for TransactionObject { expiry_height: None, block_hash: None, block_time: None, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + zip233_amount: None, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + issuance_exists: None, } } } @@ -859,63 +944,34 @@ impl TransactionObject { .collect(), value_balance: Some(Zec::from(tx.sapling_value_balance().sapling_amount()).lossy_zec()), value_balance_zat: Some(tx.sapling_value_balance().sapling_amount().zatoshis()), - orchard: Some(Orchard { - actions: tx - .orchard_actions() - .collect::>() - .iter() - .map(|action| { - let spend_auth_sig: [u8; 64] = tx - .orchard_shielded_data() - .and_then(|shielded_data| { - shielded_data - .actions - .iter() - .find(|authorized_action| authorized_action.action == **action) - .map(|authorized_action| { - authorized_action.spend_auth_sig.into() - }) - }) - .unwrap_or([0; 64]); - - let cv: [u8; 32] = action.cv.into(); - let nullifier: [u8; 32] = action.nullifier.into(); - let rk: [u8; 32] = action.rk.into(); - let cm_x: [u8; 32] = action.cm_x.into(); - let ephemeral_key: [u8; 32] = action.ephemeral_key.into(); - let enc_ciphertext: [u8; 580] = action.enc_ciphertext.into(); - let out_ciphertext: [u8; 80] = action.out_ciphertext.into(); - - OrchardAction { - cv, - nullifier, - rk, - cm_x, - ephemeral_key, - enc_ciphertext, - spend_auth_sig, - out_ciphertext, - } - }) - .collect(), - value_balance: Zec::from(tx.orchard_value_balance().orchard_amount()).lossy_zec(), - value_balance_zat: tx.orchard_value_balance().orchard_amount().zatoshis(), - flags: tx.orchard_shielded_data().map(|data| { - OrchardFlags::new( - data.flags.contains(orchard::Flags::ENABLE_OUTPUTS), - data.flags.contains(orchard::Flags::ENABLE_SPENDS), - ) - }), - anchor: tx - .orchard_shielded_data() - .map(|data| data.shared_anchor.bytes_in_display_order()), - proof: tx - .orchard_shielded_data() - .map(|data| data.proof.bytes_in_display_order()), - binding_sig: tx - .orchard_shielded_data() - .map(|data| data.binding_sig.into()), - }), + orchard: Some( + (match tx.as_ref() { + Transaction::V5 { + orchard_shielded_data, + .. + } => orchard_shielded_data.as_ref().map(|data| { + Orchard::from_shielded_data( + data, + tx.orchard_value_balance().orchard_amount(), + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + None, + ) + }), + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + Transaction::V6 { + orchard_shielded_data, + .. + } => orchard_shielded_data.as_ref().map(|data| { + Orchard::from_shielded_data( + data, + tx.orchard_value_balance().orchard_amount(), + Some(!data.burn.as_ref().is_empty()), + ) + }), + _ => None, + }) + .unwrap_or_default(), + ), binding_sig: tx.sapling_binding_sig().map(|raw_sig| raw_sig.into()), joinsplit_pub_key: tx.joinsplit_pub_key().map(|raw_key| { // Display order is reversed in the RPC output. @@ -942,6 +998,19 @@ impl TransactionObject { }, block_hash, block_time, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + zip233_amount: match tx.as_ref() { + Transaction::V6 { zip233_amount, .. } => Some(*zip233_amount), + _ => None, + }, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + issuance_exists: match tx.as_ref() { + Transaction::V6 { + orchard_zsa_issue_data, + .. + } => Some(orchard_zsa_issue_data.is_some()), + _ => None, + }, } } } diff --git a/zebra-rpc/src/tests/vectors.rs b/zebra-rpc/src/tests/vectors.rs index fb899f2d121..ff6863f13bb 100644 --- a/zebra-rpc/src/tests/vectors.rs +++ b/zebra-rpc/src/tests/vectors.rs @@ -38,6 +38,10 @@ pub fn test_transaction_serialization() { expiry_height: None, block_hash: None, block_time: None, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + zip233_amount: None, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + issuance_exists: None, })); assert_eq!( @@ -73,6 +77,10 @@ pub fn test_transaction_serialization() { expiry_height: None, block_hash: None, block_time: None, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + zip233_amount: None, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + issuance_exists: None, })); assert_eq!( diff --git a/zebra-rpc/tests/serialization_tests.rs b/zebra-rpc/tests/serialization_tests.rs index 5edb4a44326..4db5254bf47 100644 --- a/zebra-rpc/tests/serialization_tests.rs +++ b/zebra-rpc/tests/serialization_tests.rs @@ -787,7 +787,7 @@ fn test_get_raw_transaction_true() -> Result<(), Box> { let rk = action.rk(); let cm_x = action.cm_x(); let ephemeral_key = action.ephemeral_key(); - let enc_ciphertext = action.enc_ciphertext(); + let enc_ciphertext = action.enc_ciphertext().clone(); let spend_auth_sig = action.spend_auth_sig(); let out_ciphertext = action.out_ciphertext(); OrchardAction::new( @@ -806,17 +806,32 @@ fn test_get_raw_transaction_true() -> Result<(), Box> { let value_balance_zat = bundle.value_balance_zat(); let spends_flag = bundle.flags().as_ref().map(|f| f.enable_spends()); let outputs_flag = bundle.flags().as_ref().map(|f| f.enable_outputs()); + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + let enable_zsa_flag = bundle.flags().as_ref().map(|f| f.enable_zsa()); let anchor = bundle.anchor(); let proof = bundle.proof().clone(); let binding_sig = bundle.binding_sig(); + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + let burn_exists = *bundle.burn_exists(); Orchard::new( actions, value_balance, value_balance_zat, + #[cfg(not(all(zcash_unstable = "nu7", feature = "tx_v6")))] spends_flag.map(|_| OrchardFlags::new(spends_flag.unwrap(), outputs_flag.unwrap())), + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + spends_flag.map(|_| { + OrchardFlags::new( + spends_flag.unwrap(), + outputs_flag.unwrap(), + enable_zsa_flag.unwrap(), + ) + }), anchor, proof, binding_sig, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + burn_exists, ) }); let binding_sig = tx.binding_sig(); @@ -836,6 +851,10 @@ fn test_get_raw_transaction_true() -> Result<(), Box> { let expiry_height = tx.expiry_height(); let block_hash = tx.block_hash(); let block_time = tx.block_time(); + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + let zip233_amount = tx.zip233_amount(); + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + let issuance_exists = *tx.issuance_exists(); let new_obj = GetRawTransactionResponse::Object(Box::new(TransactionObject::new( in_active_chain, @@ -864,6 +883,10 @@ fn test_get_raw_transaction_true() -> Result<(), Box> { expiry_height, block_hash, block_time, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + zip233_amount, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + issuance_exists, ))); assert_eq!(obj, new_obj); diff --git a/zebra-state/src/arbitrary.rs b/zebra-state/src/arbitrary.rs index 1dc9b7ce33d..b7566dc4a1a 100644 --- a/zebra-state/src/arbitrary.rs +++ b/zebra-state/src/arbitrary.rs @@ -82,8 +82,13 @@ impl ContextuallyVerifiedBlock { .map(|outpoint| (outpoint, zero_utxo.clone())) .collect(); - ContextuallyVerifiedBlock::with_block_and_spent_utxos(block, zero_spent_utxos) - .expect("all UTXOs are provided with zero values") + ContextuallyVerifiedBlock::with_block_and_spent_utxos( + block, + zero_spent_utxos, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + Default::default(), + ) + .expect("all UTXOs are provided with zero values") } /// Create a [`ContextuallyVerifiedBlock`] from a [`Block`] or [`SemanticallyVerifiedBlock`], @@ -97,6 +102,7 @@ impl ContextuallyVerifiedBlock { height, new_outputs, transaction_hashes, + transaction_sighashes, deferred_pool_balance_change: _, } = block.into(); @@ -110,7 +116,10 @@ impl ContextuallyVerifiedBlock { // TODO: fix the tests, and stop adding unrelated inputs and outputs. spent_outputs: new_outputs, transaction_hashes, + transaction_sighashes, chain_value_pool_change: ValueBalance::zero(), + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + issued_asset_changes: Default::default(), } } } diff --git a/zebra-state/src/error.rs b/zebra-state/src/error.rs index 5b4504f57f3..54fda445ee1 100644 --- a/zebra-state/src/error.rs +++ b/zebra-state/src/error.rs @@ -15,6 +15,9 @@ use zebra_chain::{ work::difficulty::CompactDifficulty, }; +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +use zebra_chain::orchard_zsa; + use crate::{constants::MIN_TRANSPARENT_COINBASE_MATURITY, HashOrHeight, KnownBlock}; /// A wrapper for type erased errors that is itself clonable and implements the @@ -383,6 +386,10 @@ pub enum ValidateContextError { tx_index_in_block: Option, transaction_hash: transaction::Hash, }, + + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + #[error("error updating issued asset state")] + InvalidIssuedAsset(#[from] orchard_zsa::AssetStateError), } impl From for ValidateContextError { diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index 82620468285..d3469627089 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -24,6 +24,9 @@ use zebra_chain::{ value_balance::{ValueBalance, ValueBalanceError}, }; +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +use zebra_chain::orchard_zsa::{AssetBase, IssuedAssetChanges}; + /// Allow *only* these unused imports, so that rustdoc link resolution /// will work with inline links. #[allow(unused_imports)] @@ -258,6 +261,9 @@ pub struct SemanticallyVerifiedBlock { /// A precomputed list of the hashes of the transactions in this block, /// in the same order as `block.transactions`. pub transaction_hashes: Arc<[transaction::Hash]>, + /// A precomputed list of the sighashes of the transactions in this block, + /// in the same order as `block.transactions`. + pub transaction_sighashes: Option>, /// This block's deferred pool value balance change. pub deferred_pool_balance_change: Option, } @@ -318,8 +324,19 @@ pub struct ContextuallyVerifiedBlock { /// in the same order as `block.transactions`. pub(crate) transaction_hashes: Arc<[transaction::Hash]>, + /// A precomputed list of the sighashes of the transactions in this block, + /// in the same order as `block.transactions`. + pub transaction_sighashes: Option>, + /// The sum of the chain value pool changes of all transactions in this block. pub(crate) chain_value_pool_change: ValueBalance, + + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + /// Asset state changes for assets modified in this block. + /// Maps asset_base -> (old_state, new_state) where: + /// - old_state: the state before this block was applied + /// - new_state: the state after this block was applied + pub(crate) issued_asset_changes: IssuedAssetChanges, } /// Wraps note commitment trees and the history tree together. @@ -392,12 +409,22 @@ pub struct FinalizedBlock { pub(super) treestate: Treestate, /// This block's deferred pool value balance change. pub(super) deferred_pool_balance_change: Option, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + /// Asset state changes to be applied to the finalized state. + /// Contains (old_state, new_state) pairs for assets modified in this block. + /// If `None`, the changes will be recalculated from the block's transactions. + pub issued_asset_changes: Option, } impl FinalizedBlock { /// Constructs [`FinalizedBlock`] from [`CheckpointVerifiedBlock`] and its [`Treestate`]. pub fn from_checkpoint_verified(block: CheckpointVerifiedBlock, treestate: Treestate) -> Self { - Self::from_semantically_verified(SemanticallyVerifiedBlock::from(block), treestate) + Self::from_semantically_verified( + SemanticallyVerifiedBlock::from(block), + treestate, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + None, + ) } /// Constructs [`FinalizedBlock`] from [`ContextuallyVerifiedBlock`] and its [`Treestate`]. @@ -405,11 +432,24 @@ impl FinalizedBlock { block: ContextuallyVerifiedBlock, treestate: Treestate, ) -> Self { - Self::from_semantically_verified(SemanticallyVerifiedBlock::from(block), treestate) + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + let issued_asset_changes = Some(block.issued_asset_changes.clone()); + Self::from_semantically_verified( + SemanticallyVerifiedBlock::from(block), + treestate, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + issued_asset_changes, + ) } /// Constructs [`FinalizedBlock`] from [`SemanticallyVerifiedBlock`] and its [`Treestate`]. - fn from_semantically_verified(block: SemanticallyVerifiedBlock, treestate: Treestate) -> Self { + fn from_semantically_verified( + block: SemanticallyVerifiedBlock, + treestate: Treestate, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] issued_asset_changes: Option< + IssuedAssetChanges, + >, + ) -> Self { Self { block: block.block, hash: block.hash, @@ -418,6 +458,8 @@ impl FinalizedBlock { transaction_hashes: block.transaction_hashes, treestate, deferred_pool_balance_change: block.deferred_pool_balance_change, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + issued_asset_changes, } } } @@ -483,6 +525,8 @@ impl ContextuallyVerifiedBlock { pub fn with_block_and_spent_utxos( semantically_verified: SemanticallyVerifiedBlock, mut spent_outputs: HashMap, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + issued_asset_changes: IssuedAssetChanges, ) -> Result { let SemanticallyVerifiedBlock { block, @@ -490,6 +534,7 @@ impl ContextuallyVerifiedBlock { height, new_outputs, transaction_hashes, + transaction_sighashes, deferred_pool_balance_change, } = semantically_verified; @@ -506,10 +551,13 @@ impl ContextuallyVerifiedBlock { new_outputs, spent_outputs: spent_outputs.clone(), transaction_hashes, + transaction_sighashes, chain_value_pool_change: block.chain_value_pool_change( &utxos_from_ordered_utxos(spent_outputs), deferred_pool_balance_change, )?, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + issued_asset_changes, }) } } @@ -526,6 +574,7 @@ impl CheckpointVerifiedBlock { block.deferred_pool_balance_change = deferred_pool_balance_change; block } + /// Creates a block that's ready to be committed to the finalized state, /// using a precalculated [`block::Hash`]. /// @@ -551,6 +600,8 @@ impl SemanticallyVerifiedBlock { height, new_outputs, transaction_hashes, + // Not used in checkpoint paths. + transaction_sighashes: None, deferred_pool_balance_change: None, } } @@ -567,7 +618,7 @@ impl SemanticallyVerifiedBlock { impl From> for CheckpointVerifiedBlock { fn from(block: Arc) -> Self { - CheckpointVerifiedBlock(SemanticallyVerifiedBlock::from(block)) + Self(SemanticallyVerifiedBlock::from(block)) } } @@ -586,6 +637,7 @@ impl From> for SemanticallyVerifiedBlock { height, new_outputs, transaction_hashes, + transaction_sighashes: None, deferred_pool_balance_change: None, } } @@ -599,6 +651,7 @@ impl From for SemanticallyVerifiedBlock { height: valid.height, new_outputs: valid.new_outputs, transaction_hashes: valid.transaction_hashes, + transaction_sighashes: valid.transaction_sighashes, deferred_pool_balance_change: Some(DeferredPoolBalanceChange::new( valid.chain_value_pool_change.deferred_amount(), )), @@ -606,19 +659,6 @@ impl From for SemanticallyVerifiedBlock { } } -impl From for SemanticallyVerifiedBlock { - fn from(finalized: FinalizedBlock) -> Self { - Self { - block: finalized.block, - hash: finalized.hash, - height: finalized.height, - new_outputs: finalized.new_outputs, - transaction_hashes: finalized.transaction_hashes, - deferred_pool_balance_change: finalized.deferred_pool_balance_change, - } - } -} - impl From for SemanticallyVerifiedBlock { fn from(checkpoint_verified: CheckpointVerifiedBlock) -> Self { checkpoint_verified.0 @@ -1410,6 +1450,17 @@ pub enum ReadRequest { /// Returns `true` if the transparent output is spent in the best chain, /// or `false` if it is unspent. IsTransparentOutputSpent(transparent::OutPoint), + + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + /// Returns [`ReadResponse::AssetState`] with an [`AssetState`](zebra_chain::orchard_zsa::AssetState) + /// of the provided [`AssetBase`] if it exists for the best chain tip or finalized chain tip (depending + /// on the `include_non_finalized` flag). + AssetState { + /// The [`AssetBase`] to return the asset state for. + asset_base: AssetBase, + /// Whether to include the issued asset state changes in the non-finalized state. + include_non_finalized: bool, + }, } impl ReadRequest { @@ -1454,6 +1505,8 @@ impl ReadRequest { ReadRequest::TipBlockSize => "tip_block_size", ReadRequest::NonFinalizedBlocksListener => "non_finalized_blocks_listener", ReadRequest::IsTransparentOutputSpent(_) => "is_transparent_output_spent", + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + ReadRequest::AssetState { .. } => "asset_state", } } diff --git a/zebra-state/src/response.rs b/zebra-state/src/response.rs index 6ca5be13de3..80c0b9466c1 100644 --- a/zebra-state/src/response.rs +++ b/zebra-state/src/response.rs @@ -18,6 +18,9 @@ use zebra_chain::{ value_balance::ValueBalance, }; +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +use zebra_chain::orchard_zsa::AssetState; + use zebra_chain::work::difficulty::CompactDifficulty; // Allow *only* these unused imports, so that rustdoc link resolution @@ -303,6 +306,7 @@ impl Eq for NonFinalizedBlocksListener {} #[derive(Clone, Debug, PartialEq, Eq)] /// A response to a read-only /// [`ReadStateService`](crate::service::ReadStateService)'s [`ReadRequest`]. +#[allow(clippy::large_enum_variant)] pub enum ReadResponse { /// Response to [`ReadRequest::UsageInfo`] with the current best chain tip. UsageInfo(u64), @@ -453,6 +457,10 @@ pub enum ReadResponse { /// Response to [`ReadRequest::IsTransparentOutputSpent`] IsTransparentOutputSpent(bool), + + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + /// Response to [`ReadRequest::AssetState`] + AssetState(Option), } /// A structure with the information needed from the state to build a `getblocktemplate` RPC response. @@ -560,6 +568,9 @@ impl TryFrom for Response { ReadResponse::SolutionRate(_) | ReadResponse::TipBlockSize(_) => { Err("there is no corresponding Response for this ReadResponse") } + + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + ReadResponse::AssetState(_) => Err("there is no corresponding Response for this ReadResponse"), } } } diff --git a/zebra-state/src/service.rs b/zebra-state/src/service.rs index ac58d04650f..04b6408935b 100644 --- a/zebra-state/src/service.rs +++ b/zebra-state/src/service.rs @@ -1720,6 +1720,20 @@ impl Service for ReadStateService { let is_spent = read::unspent_utxo(state.latest_best_chain(), &state.db, outpoint); Ok(ReadResponse::IsTransparentOutputSpent(is_spent.is_none())) } + + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + ReadRequest::AssetState { + asset_base, + include_non_finalized, + } => { + let best_chain = include_non_finalized + .then(|| state.latest_best_chain()) + .flatten(); + + let response = read::asset_state(best_chain, &state.db, &asset_base); + + Ok(ReadResponse::AssetState(response)) + } }; timed_span.spawn_blocking(request_handler) diff --git a/zebra-state/src/service/chain_tip.rs b/zebra-state/src/service/chain_tip.rs index 40d67d67929..854f76b0c83 100644 --- a/zebra-state/src/service/chain_tip.rs +++ b/zebra-state/src/service/chain_tip.rs @@ -115,6 +115,9 @@ impl From for ChainTipBlock { height, new_outputs: _, transaction_hashes, + // Sighashes are not needed here - they are only used during block validation, + // which has already completed before a block reaches `ChainTipBlock`. + transaction_sighashes: _, deferred_pool_balance_change: _, } = prepared; diff --git a/zebra-state/src/service/check/anchors.rs b/zebra-state/src/service/check/anchors.rs index 83ad3ce8e10..3bb2dd8a876 100644 --- a/zebra-state/src/service/check/anchors.rs +++ b/zebra-state/src/service/check/anchors.rs @@ -88,25 +88,21 @@ fn sapling_orchard_anchors_refer_to_final_treestates( // > earlier block’s final Orchard treestate. // // - if let Some(orchard_shielded_data) = transaction.orchard_shielded_data() { + if let Some(shared_anchor) = transaction.orchard_shared_anchor() { tracing::debug!( - ?orchard_shielded_data.shared_anchor, + ?shared_anchor, ?tx_index_in_block, ?height, "observed orchard anchor", ); if !parent_chain - .map(|chain| { - chain - .orchard_anchors - .contains(&orchard_shielded_data.shared_anchor) - }) + .map(|chain| chain.orchard_anchors.contains(&shared_anchor)) .unwrap_or(false) - && !finalized_state.contains_orchard_anchor(&orchard_shielded_data.shared_anchor) + && !finalized_state.contains_orchard_anchor(&shared_anchor) { return Err(ValidateContextError::UnknownOrchardAnchor { - anchor: orchard_shielded_data.shared_anchor, + anchor: shared_anchor, height, tx_index_in_block, transaction_hash, @@ -114,7 +110,7 @@ fn sapling_orchard_anchors_refer_to_final_treestates( } tracing::debug!( - ?orchard_shielded_data.shared_anchor, + ?shared_anchor, ?tx_index_in_block, ?height, "validated orchard anchor", diff --git a/zebra-state/src/service/check/tests.rs b/zebra-state/src/service/check/tests.rs index 5c99d1218bf..eae5f07d356 100644 --- a/zebra-state/src/service/check/tests.rs +++ b/zebra-state/src/service/check/tests.rs @@ -6,3 +6,6 @@ mod anchors; mod nullifier; mod utxo; mod vectors; + +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +mod issuance; diff --git a/zebra-state/src/service/check/tests/issuance.rs b/zebra-state/src/service/check/tests/issuance.rs new file mode 100644 index 00000000000..b7a54a41944 --- /dev/null +++ b/zebra-state/src/service/check/tests/issuance.rs @@ -0,0 +1,115 @@ +use std::sync::Arc; + +use zebra_chain::{ + block::{self, genesis::regtest_genesis_block, Block}, + orchard_zsa::{AssetBase, IssuedAssetChanges}, + parameters::{testnet::ConfiguredActivationHeights, Network}, + serialization::ZcashDeserialize, +}; + +use zebra_test::vectors::{OrchardWorkflowBlock, ORCHARD_ZSA_WORKFLOW_BLOCKS}; + +use crate::{ + check::Chain, + service::{finalized_state::FinalizedState, read, write::validate_and_commit_non_finalized}, + CheckpointVerifiedBlock, Config, NonFinalizedState, +}; + +#[test] +fn check_burns_and_issuance() { + let _init_guard = zebra_test::init(); + + let network = Network::new_regtest( + ConfiguredActivationHeights { + canopy: Some(1), + nu7: Some(1), + ..Default::default() + } + .into(), + ); + + let mut finalized_state = FinalizedState::new_with_debug( + &Config::ephemeral(), + &network, + true, + #[cfg(feature = "elasticsearch")] + false, + false, + ); + + let mut non_finalized_state = NonFinalizedState::new(&network); + + finalized_state + .commit_finalized_direct(regtest_genesis_block().into(), None, "test") + .expect("unexpected invalid genesis block test vector"); + + let empty_chain = Arc::new(Chain::new( + &network, + finalized_state + .db + .finalized_tip_height() + .unwrap_or(block::Height::MIN), + finalized_state.db.sprout_tree_for_tip(), + finalized_state.db.sapling_tree_for_tip(), + finalized_state.db.orchard_tree_for_tip(), + finalized_state.db.history_tree(), + finalized_state.db.finalized_value_pool(), + )); + + for OrchardWorkflowBlock { + height, + bytes, + is_valid, + } in ORCHARD_ZSA_WORKFLOW_BLOCKS.iter() + { + let block = + Arc::new(Block::zcash_deserialize(&bytes[..]).expect("block should deserialize")); + + let chain = non_finalized_state + .best_chain() + .cloned() + .unwrap_or_else(|| empty_chain.clone()); + + let issued_asset_changes_result = IssuedAssetChanges::validate_and_get_changes( + &block.transactions, + None, + |asset_base: &AssetBase| { + read::asset_state(Some(&chain), &finalized_state.db, asset_base) + }, + ); + + let CheckpointVerifiedBlock(block) = CheckpointVerifiedBlock::new(block, None, None); + + let commit_result = + validate_and_commit_non_finalized(&finalized_state.db, &mut non_finalized_state, block); + + if !is_valid { + assert!( + issued_asset_changes_result.is_err() || commit_result.is_err(), + "invalid workflow block at height {height} should fail issued-asset validation or commit" + ); + + // Later workflow blocks depend on this one, so stop after rejecting it. + break; + } + + issued_asset_changes_result.unwrap_or_else(|error| { + panic!( + "valid workflow block at height {height} should have valid issued-asset changes: {error:?}" + ) + }); + + commit_result.unwrap_or_else(|error| { + panic!("valid workflow block at height {height} should commit: {error:?}") + }); + + let best_chain = non_finalized_state + .best_chain() + .expect("should have a non-finalized chain"); + + assert!( + !best_chain.issued_assets.is_empty(), + "issued assets should not be empty after workflow block {height}" + ); + } +} diff --git a/zebra-state/src/service/check/tests/nullifier.rs b/zebra-state/src/service/check/tests/nullifier.rs index 7ca829e6038..4a8063eac14 100644 --- a/zebra-state/src/service/check/tests/nullifier.rs +++ b/zebra-state/src/service/check/tests/nullifier.rs @@ -693,8 +693,8 @@ proptest! { /// (And that the test infrastructure generally works.) #[test] fn accept_distinct_arbitrary_orchard_nullifiers_in_one_block( - authorized_action in TypeNameToDebug::::arbitrary(), - orchard_shielded_data in TypeNameToDebug::::arbitrary(), + authorized_action in TypeNameToDebug::>::arbitrary(), + orchard_shielded_data in TypeNameToDebug::>::arbitrary(), use_finalized_state in any::(), ) { let _init_guard = zebra_test::init(); @@ -752,9 +752,9 @@ proptest! { /// if they come from different AuthorizedActions in the same orchard::ShieldedData/Transaction. #[test] fn reject_duplicate_orchard_nullifiers_in_transaction( - authorized_action1 in TypeNameToDebug::::arbitrary(), - mut authorized_action2 in TypeNameToDebug::::arbitrary(), - orchard_shielded_data in TypeNameToDebug::::arbitrary(), + authorized_action1 in TypeNameToDebug::>::arbitrary(), + mut authorized_action2 in TypeNameToDebug::>::arbitrary(), + orchard_shielded_data in TypeNameToDebug::>::arbitrary(), ) { let _init_guard = zebra_test::init(); @@ -804,10 +804,10 @@ proptest! { /// if they come from different transactions in the same block. #[test] fn reject_duplicate_orchard_nullifiers_in_block( - authorized_action1 in TypeNameToDebug::::arbitrary(), - mut authorized_action2 in TypeNameToDebug::::arbitrary(), - orchard_shielded_data1 in TypeNameToDebug::::arbitrary(), - orchard_shielded_data2 in TypeNameToDebug::::arbitrary(), + authorized_action1 in TypeNameToDebug::>::arbitrary(), + mut authorized_action2 in TypeNameToDebug::>::arbitrary(), + orchard_shielded_data1 in TypeNameToDebug::>::arbitrary(), + orchard_shielded_data2 in TypeNameToDebug::>::arbitrary(), ) { let _init_guard = zebra_test::init(); @@ -863,10 +863,10 @@ proptest! { /// if they come from different blocks in the same chain. #[test] fn reject_duplicate_orchard_nullifiers_in_chain( - authorized_action1 in TypeNameToDebug::::arbitrary(), - mut authorized_action2 in TypeNameToDebug::::arbitrary(), - orchard_shielded_data1 in TypeNameToDebug::::arbitrary(), - orchard_shielded_data2 in TypeNameToDebug::::arbitrary(), + authorized_action1 in TypeNameToDebug::>::arbitrary(), + mut authorized_action2 in TypeNameToDebug::>::arbitrary(), + orchard_shielded_data1 in TypeNameToDebug::>::arbitrary(), + orchard_shielded_data2 in TypeNameToDebug::>::arbitrary(), duplicate_in_finalized_state in any::(), ) { let _init_guard = zebra_test::init(); @@ -1116,8 +1116,8 @@ fn transaction_v4_with_sapling_shielded_data( /// /// If there are no `AuthorizedAction`s in `authorized_actions`. fn transaction_v5_with_orchard_shielded_data( - orchard_shielded_data: impl Into>, - authorized_actions: impl IntoIterator, + orchard_shielded_data: impl Into>>, + authorized_actions: impl IntoIterator>, ) -> Transaction { let mut orchard_shielded_data = orchard_shielded_data.into(); let authorized_actions: Vec<_> = authorized_actions.into_iter().collect(); diff --git a/zebra-state/src/service/check/utxo.rs b/zebra-state/src/service/check/utxo.rs index 4f1a307c402..6abc32cbebf 100644 --- a/zebra-state/src/service/check/utxo.rs +++ b/zebra-state/src/service/check/utxo.rs @@ -65,7 +65,7 @@ pub fn transparent_spend( // The state service returns UTXOs from pending blocks, // which can be rejected by later contextual checks. - // This is a particular issue for v5 transactions, + // This is a particular issue for v5 and v6 transactions, // because their authorizing data is only bound to the block data // during contextual validation (#2336). // diff --git a/zebra-state/src/service/finalized_state.rs b/zebra-state/src/service/finalized_state.rs index 3937f9997dc..7ce8b348100 100644 --- a/zebra-state/src/service/finalized_state.rs +++ b/zebra-state/src/service/finalized_state.rs @@ -93,6 +93,7 @@ pub const STATE_COLUMN_FAMILIES_IN_CODE: &[&str] = &[ "orchard_anchors", "orchard_note_commitment_tree", "orchard_note_commitment_subtree", + "orchard_issued_assets", // Chain "history_tree", "tip_chain_value_pool", diff --git a/zebra-state/src/service/finalized_state/disk_format/shielded.rs b/zebra-state/src/service/finalized_state/disk_format/shielded.rs index a845cda2c30..dd75e78bd65 100644 --- a/zebra-state/src/service/finalized_state/disk_format/shielded.rs +++ b/zebra-state/src/service/finalized_state/disk_format/shielded.rs @@ -13,6 +13,9 @@ use zebra_chain::{ subtree::{NoteCommitmentSubtreeData, NoteCommitmentSubtreeIndex}, }; +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +use zebra_chain::orchard_zsa::{AssetBase, AssetState}; + use crate::service::finalized_state::disk_format::{FromDisk, IntoDisk}; use super::block::HEIGHT_DISK_BYTES; @@ -213,3 +216,43 @@ impl FromDisk for NoteCommitmentSubtreeData { ) } } + +// TODO: Replace `.unwrap()`s with `.expect()`s + +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +impl IntoDisk for AssetState { + type Bytes = Vec; + + fn as_bytes(&self) -> Self::Bytes { + self.to_bytes() + .expect("asset state should serialize successfully") + } +} + +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +impl FromDisk for AssetState { + fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { + Self::from_bytes(bytes.as_ref()).expect("asset state should deserialize successfully") + } +} + +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +impl IntoDisk for AssetBase { + type Bytes = [u8; 32]; + + fn as_bytes(&self) -> Self::Bytes { + self.to_bytes() + } +} + +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +impl FromDisk for AssetBase { + fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { + bytes + .as_ref() + .try_into() + .ok() + .and_then(|asset_bytes| Option::from(Self::from_bytes(asset_bytes))) + .expect("asset base should deserialize successfully") + } +} diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/column_family_names.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/column_family_names.snap index d061f4dd38f..0c6a65e61b2 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/column_family_names.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/column_family_names.snap @@ -12,6 +12,7 @@ expression: cf_names "height_by_hash", "history_tree", "orchard_anchors", + "orchard_issued_assets", "orchard_note_commitment_subtree", "orchard_note_commitment_tree", "orchard_nullifiers", diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_0.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_0.snap index f04da90d0b9..56bd8e6bf72 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_0.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_0.snap @@ -5,6 +5,7 @@ expression: empty_column_families [ "balance_by_transparent_addr: no entries", "history_tree: no entries", + "orchard_issued_assets: no entries", "orchard_note_commitment_subtree: no entries", "orchard_nullifiers: no entries", "sapling_note_commitment_subtree: no entries", diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_1.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_1.snap index 8fcb84c844f..0076744f5b4 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_1.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_1.snap @@ -4,6 +4,7 @@ expression: empty_column_families --- [ "history_tree: no entries", + "orchard_issued_assets: no entries", "orchard_note_commitment_subtree: no entries", "orchard_nullifiers: no entries", "sapling_note_commitment_subtree: no entries", diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_2.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_2.snap index 8fcb84c844f..0076744f5b4 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_2.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_2.snap @@ -4,6 +4,7 @@ expression: empty_column_families --- [ "history_tree: no entries", + "orchard_issued_assets: no entries", "orchard_note_commitment_subtree: no entries", "orchard_nullifiers: no entries", "sapling_note_commitment_subtree: no entries", diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@no_blocks.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@no_blocks.snap index e4c682a7270..933ade2bca0 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@no_blocks.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@no_blocks.snap @@ -11,6 +11,7 @@ expression: empty_column_families "height_by_hash: no entries", "history_tree: no entries", "orchard_anchors: no entries", + "orchard_issued_assets: no entries", "orchard_note_commitment_subtree: no entries", "orchard_note_commitment_tree: no entries", "orchard_nullifiers: no entries", diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_0.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_0.snap index f04da90d0b9..56bd8e6bf72 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_0.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_0.snap @@ -5,6 +5,7 @@ expression: empty_column_families [ "balance_by_transparent_addr: no entries", "history_tree: no entries", + "orchard_issued_assets: no entries", "orchard_note_commitment_subtree: no entries", "orchard_nullifiers: no entries", "sapling_note_commitment_subtree: no entries", diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_1.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_1.snap index 8fcb84c844f..0076744f5b4 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_1.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_1.snap @@ -4,6 +4,7 @@ expression: empty_column_families --- [ "history_tree: no entries", + "orchard_issued_assets: no entries", "orchard_note_commitment_subtree: no entries", "orchard_nullifiers: no entries", "sapling_note_commitment_subtree: no entries", diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_2.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_2.snap index 8fcb84c844f..0076744f5b4 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_2.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_2.snap @@ -4,6 +4,7 @@ expression: empty_column_families --- [ "history_tree: no entries", + "orchard_issued_assets: no entries", "orchard_note_commitment_subtree: no entries", "orchard_nullifiers: no entries", "sapling_note_commitment_subtree: no entries", diff --git a/zebra-state/src/service/finalized_state/disk_format/upgrade/add_subtrees.rs b/zebra-state/src/service/finalized_state/disk_format/upgrade/add_subtrees.rs index edaa93ed579..9b9598fcef9 100644 --- a/zebra-state/src/service/finalized_state/disk_format/upgrade/add_subtrees.rs +++ b/zebra-state/src/service/finalized_state/disk_format/upgrade/add_subtrees.rs @@ -834,7 +834,6 @@ fn calculate_orchard_subtree( let orchard_note_commitments = block .orchard_note_commitments() .take(prev_remaining_notes) - .cloned() .collect(); // This takes less than 1 second per tree, so we don't need to make it cancellable. diff --git a/zebra-state/src/service/finalized_state/zebra_db/arbitrary.rs b/zebra-state/src/service/finalized_state/zebra_db/arbitrary.rs index 0d81cefb44d..e837eda199f 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/arbitrary.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/arbitrary.rs @@ -66,8 +66,8 @@ impl ZebraDb { } // Orchard - if let Some(orchard_shielded_data) = transaction.orchard_shielded_data() { - batch.zs_insert(&orchard_anchors, orchard_shielded_data.shared_anchor, ()); + if let Some(shared_anchor) = transaction.orchard_shared_anchor() { + batch.zs_insert(&orchard_anchors, shared_anchor, ()); } } diff --git a/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs b/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs index 706f4d06666..444d496b5f0 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs @@ -136,6 +136,7 @@ fn test_block_db_round_trip_with( height: Height(0), new_outputs, transaction_hashes, + transaction_sighashes: None, deferred_pool_balance_change: None, }) }; diff --git a/zebra-state/src/service/finalized_state/zebra_db/shielded.rs b/zebra-state/src/service/finalized_state/zebra_db/shielded.rs index e07b482fd9b..97a0422f7e3 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/shielded.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/shielded.rs @@ -26,6 +26,9 @@ use zebra_chain::{ transaction::Transaction, }; +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +use zebra_chain::orchard_zsa::{AssetBase, AssetState, IssuedAssetChanges}; + use crate::{ request::{FinalizedBlock, Treestate}, service::finalized_state::{ @@ -36,11 +39,29 @@ use crate::{ TransactionLocation, }; +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +use crate::service::finalized_state::TypedColumnFamily; + // Doc-only items #[allow(unused_imports)] use zebra_chain::subtree::NoteCommitmentSubtree; +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +/// The name of the chain value pools column family. +pub const ISSUED_ASSETS: &str = "orchard_issued_assets"; + +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +/// The type for reading value pools from the database. +pub type IssuedAssetsCf<'cf> = TypedColumnFamily<'cf, AssetBase, AssetState>; + impl ZebraDb { + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + /// Returns a typed handle to the `history_tree` column family. + pub(crate) fn issued_assets_cf(&self) -> IssuedAssetsCf<'_> { + IssuedAssetsCf::new(&self.db, ISSUED_ASSETS) + .expect("column family was created when database was created") + } + // Read shielded methods /// Returns `true` if the finalized state contains `sprout_nullifier`. @@ -446,6 +467,12 @@ impl ZebraDb { Some(subtree_data.with_index(index)) } + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + /// Get the orchard issued asset state for the finalized tip. + pub fn issued_asset(&self, asset_base: &AssetBase) -> Option { + self.issued_assets_cf().zs_get(asset_base) + } + /// Returns the shielded note commitment trees of the finalized tip /// or the empty trees if the state is empty. /// Additionally, returns the sapling and orchard subtrees for the finalized tip if @@ -486,6 +513,9 @@ impl DiskWriteBatch { for transaction in &finalized.block.transactions { self.prepare_nullifier_batch(zebra_db, transaction); } + + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + self.prepare_issued_assets_batch(zebra_db, finalized); } /// Prepare a database batch containing `finalized.block`'s nullifiers, @@ -523,6 +553,34 @@ impl DiskWriteBatch { } } + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + /// Prepare a database batch containing `finalized.block`'s asset issuance + /// and return it (without actually writing anything). + /// + /// # Errors + /// + /// - Returns an error if asset state changes cannot be calculated from the block's transactions + #[allow(clippy::unwrap_in_result)] + pub fn prepare_issued_assets_batch(&mut self, zebra_db: &ZebraDb, finalized: &FinalizedBlock) { + let mut batch = zebra_db.issued_assets_cf().with_batch_for_writing(self); + let asset_changes = if let Some(asset_changes) = finalized.issued_asset_changes.as_ref() { + asset_changes.clone() + } else { + // Recalculate changes from transactions if not provided. + // This happens for Checkpoint Verified Blocks loaded during startup. + IssuedAssetChanges::validate_and_get_changes( + &finalized.block.transactions, + None, // No sighashes - uses trusted validation without signature checks + |asset_base| zebra_db.issued_asset(asset_base), + ) + .expect("valid issued assets changes") + }; + // Add only the new states to the batch. + for (asset_base, (_old_state, new_state)) in asset_changes.iter() { + batch = batch.zs_insert(asset_base, new_state); + } + } + /// Prepare a database batch containing the note commitment and history tree updates /// from `finalized.block`, and return it (without actually writing anything). /// diff --git a/zebra-state/src/service/non_finalized_state.rs b/zebra-state/src/service/non_finalized_state.rs index d9b6a4be0d9..0ee720a0957 100644 --- a/zebra-state/src/service/non_finalized_state.rs +++ b/zebra-state/src/service/non_finalized_state.rs @@ -18,6 +18,9 @@ use zebra_chain::{ transparent, }; +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +use zebra_chain::orchard_zsa::{AssetBase, IssuedAssetChanges}; + use crate::{ constants::{MAX_INVALIDATED_BLOCKS, MAX_NON_FINALIZED_CHAIN_FORKS}, error::ReconsiderError, @@ -26,6 +29,9 @@ use crate::{ SemanticallyVerifiedBlock, ValidateContextError, WatchReceiver, }; +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +use crate::service::read; + mod backup; mod chain; @@ -574,6 +580,15 @@ impl NonFinalizedState { finalized_state, )?; + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + let issued_assets = IssuedAssetChanges::validate_and_get_changes( + &prepared.block.transactions, + prepared.transaction_sighashes.as_deref(), + |asset_base: &AssetBase| { + read::asset_state(Some(&new_chain), finalized_state, asset_base) + }, + )?; + // Reads from disk check::anchors::block_sapling_orchard_anchors_refer_to_final_treestates( finalized_state, @@ -592,6 +607,9 @@ impl NonFinalizedState { let contextual = ContextuallyVerifiedBlock::with_block_and_spent_utxos( prepared.clone(), spent_utxos.clone(), + // TODO: Refactor this into repeated `With::with()` calls, see http_request_compatibility module. + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + issued_assets, ) .map_err(|value_balance_error| { ValidateContextError::CalculateBlockChainValueChange { diff --git a/zebra-state/src/service/non_finalized_state/chain.rs b/zebra-state/src/service/non_finalized_state/chain.rs index 49c96fc84d1..498b5a2cf69 100644 --- a/zebra-state/src/service/non_finalized_state/chain.rs +++ b/zebra-state/src/service/non_finalized_state/chain.rs @@ -34,6 +34,9 @@ use zebra_chain::{ work::difficulty::PartialCumulativeWork, }; +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +use zebra_chain::orchard_zsa::{AssetBase, AssetState, IssuedAssetChanges}; + use crate::{ request::Treestate, service::check, ContextuallyVerifiedBlock, HashOrHeight, OutputLocation, TransactionLocation, ValidateContextError, @@ -193,6 +196,10 @@ pub struct ChainInner { pub(crate) orchard_subtrees: BTreeMap>, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + /// The ZIP-0227 `issued_assets` state for this chain. + pub(crate) issued_assets: HashMap, + // Nullifiers // /// The Sprout nullifiers revealed by `blocks` and, if the `indexer` feature is selected, @@ -261,6 +268,8 @@ impl Chain { orchard_anchors_by_height: Default::default(), orchard_trees_by_height: Default::default(), orchard_subtrees: Default::default(), + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + issued_assets: Default::default(), sprout_nullifiers: Default::default(), sapling_nullifiers: Default::default(), orchard_nullifiers: Default::default(), @@ -988,6 +997,49 @@ impl Chain { } } + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + /// Returns the Orchard issued asset state if one is present in + /// the chain for the provided asset base. + pub fn issued_asset(&self, asset_base: &AssetBase) -> Option { + self.issued_assets.get(asset_base).cloned() + } + + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + /// Revert the issued-asset state changes recorded for a block. + /// + /// At `RevertPosition::Tip`, restores `issued_assets` to the per-asset `old_state` + /// captured when the block was applied. At `RevertPosition::Root` this is a no-op + /// because finalized issuance state lives in the on-disk column family. + fn revert_issued_assets( + &mut self, + position: RevertPosition, + issued_asset_changes: &IssuedAssetChanges, + ) { + if position == RevertPosition::Root { + // `issued_assets` grows monotonically (no eviction on finalization). + // At ~112 bytes per asset this is negligible for realistic asset counts. + // Revisit if ZSA adoption reaches hundreds of thousands of unique assets. + } else { + trace!( + ?position, + "restoring previous issued asset states for tip block" + ); + // Simply restore the old states + for (asset_base, (old_state, new_state)) in issued_asset_changes.iter() { + assert_eq!( + self.issued_assets.get(asset_base), + Some(new_state), + "tip revert: current state differs from recorded new_state for {:?}", + asset_base + ); + match old_state { + Some(state) => self.issued_assets.insert(*asset_base, *state), + None => self.issued_assets.remove(asset_base), + }; + } + } + } + /// Adds the Orchard `tree` to the tree and anchor indexes at `height`. /// /// `height` can be either: @@ -1502,6 +1554,29 @@ impl Chain { self.add_history_tree(height, history_tree); + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + for (asset_base, (old_state_from_block, new_state)) in + contextually_valid.issued_asset_changes.iter() + { + self.issued_assets + .entry(*asset_base) + .and_modify(|current_state| { + assert_eq!( + old_state_from_block.as_ref(), + Some(&*current_state), + "issued asset state mismatch for {:?}", + asset_base + ); + *current_state = *new_state; + }) + .or_insert_with(|| { + // When `old_state_from_block` is `Some` but the asset is missing from + // the in-memory map, it means the entry was evicted during finalization + // and lives in the finalized DB. + *new_state + }); + } + Ok(()) } @@ -1555,21 +1630,23 @@ impl Chain { .zip(transaction_hashes.iter().cloned()) .enumerate() { - let ( - inputs, - outputs, - joinsplit_data, - sapling_shielded_data_per_spend_anchor, - sapling_shielded_data_shared_anchor, - orchard_shielded_data, - ) = match transaction.deref() { + let transaction_data = match transaction.deref() { V4 { inputs, outputs, joinsplit_data, sapling_shielded_data, .. - } => (inputs, outputs, joinsplit_data, sapling_shielded_data, &None, &None), + } => ( + inputs, + outputs, + joinsplit_data, + sapling_shielded_data, + &None, + &None, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + &None + ), V5 { inputs, outputs, @@ -1583,6 +1660,8 @@ impl Chain { &None, sapling_shielded_data, orchard_shielded_data, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + &None, ), #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] V6 { @@ -1597,6 +1676,7 @@ impl Chain { &None, &None, sapling_shielded_data, + &None, orchard_shielded_data, ), @@ -1605,6 +1685,27 @@ impl Chain { ), }; + #[cfg(not(all(zcash_unstable = "nu7", feature = "tx_v6")))] + let ( + inputs, + outputs, + joinsplit_data, + sapling_shielded_data_per_spend_anchor, + sapling_shielded_data_shared_anchor, + orchard_shielded_data_vanilla, + ) = transaction_data; + + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + let ( + inputs, + outputs, + joinsplit_data, + sapling_shielded_data_per_spend_anchor, + sapling_shielded_data_shared_anchor, + orchard_shielded_data_vanilla, + orchard_shielded_data_zsa, + ) = transaction_data; + // add key `transaction.hash` and value `(height, tx_index)` to `tx_loc_by_hash` let transaction_location = TransactionLocation::from_usize(height, transaction_index); let prior_pair = self @@ -1631,7 +1732,9 @@ impl Chain { &transaction_hash, ))?; self.update_chain_tip_with(&(sapling_shielded_data_shared_anchor, &transaction_hash))?; - self.update_chain_tip_with(&(orchard_shielded_data, &transaction_hash))?; + self.update_chain_tip_with(&(orchard_shielded_data_vanilla, &transaction_hash))?; + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + self.update_chain_tip_with(&(orchard_shielded_data_zsa, &transaction_hash))?; } // update the chain value pool balances @@ -1721,6 +1824,9 @@ impl UpdateWith for Chain { &contextually_valid.chain_value_pool_change, ); + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + let issued_asset_changes = &contextually_valid.issued_asset_changes; + // remove the blocks hash from `height_by_hash` assert!( self.height_by_hash.remove(&hash).is_some(), @@ -1740,21 +1846,22 @@ impl UpdateWith for Chain { for (transaction, transaction_hash) in block.transactions.iter().zip(transaction_hashes.iter()) { - let ( - inputs, - outputs, - joinsplit_data, - sapling_shielded_data_per_spend_anchor, - sapling_shielded_data_shared_anchor, - orchard_shielded_data, - ) = match transaction.deref() { + let transaction_data = match transaction.deref() { V4 { inputs, outputs, joinsplit_data, sapling_shielded_data, .. - } => (inputs, outputs, joinsplit_data, sapling_shielded_data, &None, &None), + } => ( + inputs, + outputs, + joinsplit_data, + sapling_shielded_data, + &None, + &None, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + &None), V5 { inputs, outputs, @@ -1768,6 +1875,8 @@ impl UpdateWith for Chain { &None, sapling_shielded_data, orchard_shielded_data, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + &None, ), #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] V6 { @@ -1782,6 +1891,7 @@ impl UpdateWith for Chain { &None, &None, sapling_shielded_data, + &None, orchard_shielded_data, ), @@ -1790,6 +1900,27 @@ impl UpdateWith for Chain { ), }; + #[cfg(not(all(zcash_unstable = "nu7", feature = "tx_v6")))] + let ( + inputs, + outputs, + joinsplit_data, + sapling_shielded_data_per_spend_anchor, + sapling_shielded_data_shared_anchor, + orchard_shielded_data_vanilla, + ) = transaction_data; + + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + let ( + inputs, + outputs, + joinsplit_data, + sapling_shielded_data_per_spend_anchor, + sapling_shielded_data_shared_anchor, + orchard_shielded_data_vanilla, + orchard_shielded_data_zsa, + ) = transaction_data; + // remove the utxos this produced self.revert_chain_with(&(outputs, transaction_hash, new_outputs), position); // reset the utxos this consumed @@ -1816,7 +1947,9 @@ impl UpdateWith for Chain { &(sapling_shielded_data_shared_anchor, transaction_hash), position, ); - self.revert_chain_with(&(orchard_shielded_data, transaction_hash), position); + self.revert_chain_with(&(orchard_shielded_data_vanilla, transaction_hash), position); + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + self.revert_chain_with(&(orchard_shielded_data_zsa, transaction_hash), position); } // TODO: move these to the shielded UpdateWith.revert...()? @@ -1827,6 +1960,10 @@ impl UpdateWith for Chain { // TODO: move this to the history tree UpdateWith.revert...()? self.remove_history_tree(position, height); + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + // In revert_chain_with for ContextuallyVerifiedBlock: + self.revert_issued_assets(position, issued_asset_changes); + // revert the chain value pool balances, if needed // note that size is 0 because it isn't need for reverting self.revert_chain_with(&(*chain_value_pool_change, height, 0), position); @@ -2162,12 +2299,17 @@ where } } -impl UpdateWith<(&Option, &SpendingTransactionId)> for Chain { +impl + UpdateWith<( + &Option>, + &SpendingTransactionId, + )> for Chain +{ #[instrument(skip(self, orchard_shielded_data))] fn update_chain_tip_with( &mut self, &(orchard_shielded_data, revealing_tx_id): &( - &Option, + &Option>, &SpendingTransactionId, ), ) -> Result<(), ValidateContextError> { @@ -2192,7 +2334,7 @@ impl UpdateWith<(&Option, &SpendingTransactionId)> for Ch fn revert_chain_with( &mut self, (orchard_shielded_data, _revealing_tx_id): &( - &Option, + &Option>, &SpendingTransactionId, ), _position: RevertPosition, diff --git a/zebra-state/src/service/non_finalized_state/tests/prop.rs b/zebra-state/src/service/non_finalized_state/tests/prop.rs index f95b518d18c..df579e9196b 100644 --- a/zebra-state/src/service/non_finalized_state/tests/prop.rs +++ b/zebra-state/src/service/non_finalized_state/tests/prop.rs @@ -52,6 +52,8 @@ fn push_genesis_chain() -> Result<()> { ContextuallyVerifiedBlock::with_block_and_spent_utxos( block, only_chain.unspent_utxos(), + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + Default::default(), ) .map_err(|e| (e, chain_values.clone())) .expect("invalid block value pool change"); @@ -148,6 +150,8 @@ fn forked_equals_pushed_genesis() -> Result<()> { let block = ContextuallyVerifiedBlock::with_block_and_spent_utxos( block, partial_chain.unspent_utxos(), + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + Default::default() )?; partial_chain = partial_chain .push(block) @@ -166,8 +170,12 @@ fn forked_equals_pushed_genesis() -> Result<()> { ); for block in chain.iter().cloned() { - let block = - ContextuallyVerifiedBlock::with_block_and_spent_utxos(block, full_chain.unspent_utxos())?; + let block = ContextuallyVerifiedBlock::with_block_and_spent_utxos( + block, + full_chain.unspent_utxos(), + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + Default::default() + )?; // Check some properties of the genesis block and don't push it to the chain. if block.height == block::Height(0) { @@ -210,7 +218,9 @@ fn forked_equals_pushed_genesis() -> Result<()> { // same original full chain. for block in chain.iter().skip(fork_at_count).cloned() { let block = - ContextuallyVerifiedBlock::with_block_and_spent_utxos(block, forked.unspent_utxos())?; + ContextuallyVerifiedBlock::with_block_and_spent_utxos(block, forked.unspent_utxos(), + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + Default::default())?; forked = forked.push(block).expect("forked chain push is valid"); } diff --git a/zebra-state/src/service/read.rs b/zebra-state/src/service/read.rs index c27aee1ee82..56c39d9e675 100644 --- a/zebra-state/src/service/read.rs +++ b/zebra-state/src/service/read.rs @@ -42,6 +42,10 @@ pub use find::{ find_chain_headers, hash_by_height, height_by_hash, next_median_time_past, non_finalized_state_contains_block_hash, tip, tip_height, tip_with_value_balance, }; + +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +pub use find::asset_state; + pub use tree::{orchard_subtrees, orchard_tree, sapling_subtrees, sapling_tree}; #[cfg(any(test, feature = "proptest-impl"))] diff --git a/zebra-state/src/service/read/find.rs b/zebra-state/src/service/read/find.rs index d3f6052ba59..d79d8c795d1 100644 --- a/zebra-state/src/service/read/find.rs +++ b/zebra-state/src/service/read/find.rs @@ -25,6 +25,9 @@ use zebra_chain::{ value_balance::ValueBalance, }; +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +use zebra_chain::orchard_zsa::{AssetBase, AssetState}; + use crate::{ constants, service::{ @@ -696,3 +699,14 @@ pub(crate) fn calculate_median_time_past(relevant_chain: Vec>) -> Dat DateTime32::try_from(median_time_past).expect("valid blocks have in-range times") } + +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +/// Return the [`AssetState`] for the provided [`AssetBase`], if it exists in the provided chain. +pub fn asset_state(chain: Option, db: &ZebraDb, asset_base: &AssetBase) -> Option +where + C: AsRef, +{ + chain + .and_then(|chain| chain.as_ref().issued_asset(asset_base)) + .or_else(|| db.issued_asset(asset_base)) +} diff --git a/zebra-test/src/mock_service.rs b/zebra-test/src/mock_service.rs index cf5c2da0db7..eae1fe6d296 100644 --- a/zebra-test/src/mock_service.rs +++ b/zebra-test/src/mock_service.rs @@ -82,7 +82,7 @@ const DEFAULT_PROXY_CHANNEL_SIZE: usize = 100; /// /// We've seen delays up to 67ms on busy Linux and macOS machines, /// and some other timeout failures even with a 150ms timeout. -pub const DEFAULT_MAX_REQUEST_DELAY: Duration = Duration::from_millis(300); +pub const DEFAULT_MAX_REQUEST_DELAY: Duration = Duration::from_millis(1000); /// An internal type representing the item that's sent in the [`broadcast`] channel. /// diff --git a/zebra-test/src/vectors.rs b/zebra-test/src/vectors.rs index f581c80f37d..dbe5b324e9c 100644 --- a/zebra-test/src/vectors.rs +++ b/zebra-test/src/vectors.rs @@ -6,9 +6,15 @@ use lazy_static::lazy_static; mod block; mod orchard_note_encryption; +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +mod orchard_zsa_workflow_blocks; + pub use block::*; pub use orchard_note_encryption::*; +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +pub use orchard_zsa_workflow_blocks::*; + /// A testnet transaction test vector /// /// Copied from librustzcash diff --git a/zebra-test/src/vectors/orchard-zsa-workflow-block-1.txt b/zebra-test/src/vectors/orchard-zsa-workflow-block-1.txt new file mode 100644 index 00000000000..074c05cf6e4 --- /dev/null +++ b/zebra-test/src/vectors/orchard-zsa-workflow-block-1.txt @@ -0,0 +1 @@ +0400000027e30134d620e9fe61f719938320bab63e7e72c91b5e23025676f90ed8119f020a70bb32bfd219e26833d128fb172e93760aa1e93a4c5bb73b897214109cf1ceab56c2db11e453e8e8eff9e22eb826f5a76a4a2d60a3a0a6ec9d49794ec7fc43f2fa494d3fa60c200202020202020202020202020202020202020202020202020202020202020202fd4005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020400008085202f89010000000000000000000000000000000000000000000000000000000000000000ffffffff025100ffffffff0140be4025000000001976a91475dd6d7f4bef95aa1ff1a711e5bfd853b4c6aaf888ac000000000100000000000000000000000000000600008077777777d80a1977000000001c1d1c0000000000000000000000000001022a46968d3e18126e043a75a29ee62b013a509f75e15848f5b7076f8a8515232a8f1936228aa3f6dbb6b3c0b4d064928da244d786cd8e0c36c99d71d9a69ac530f35cc933e1d989161f712fc7e03144eb3953f031e75ff6298eb6146209bfbab31d705a6a2e0e281dd63ea34025999c441b020d0fd1488f4fc409d44cd4048d35d9b408f975fee94626bbdb03d1ce88e7111649fbc25d653e642c5a6cf00ea12daffd4754cc102f274cb5cfe8597bd7733e3253123c58b3646b7dab1df3ff17c038a2d7eff8d099beb8d8749a5f8046afece919f950a276c9f31290f2d3e5f7d1ec67eb3c1ba82b91bc987e71d5b561b6ee060bdd5b709fad62256e464c0e85ea34271d40b52ac47b51189672e59b52f5209ecd82ac738d13ea812aa3d7b6f2037448ab3813ea9627dd2114ace4808cd8019d8dfaf41d6c09cc3628088ac842d812559364f9ff08540283366aa4a2ecd8ebf86db6cabb000d882276fdac5205f29e89574f35c2d7a14eb38974a85588d8d6f99f88d7df2fd5d034f2fe9e5e8b6325ab445c8b257968e570caabfbe85a056d3cce83b465f976149045bf96d649d75f82d1aeddde3824d3ea9cdb84cc13521f25e82e6f2454647f4cdb20a6d6bc34a075f9f582376f751ab380860d5cdd16527234a2337aceacc9cba19ec781ae908416e329d611f4741df7415d2a0b4a9b231c8c902c154d459c89840fda5db847771c873503d61d742f75ee036893da820f7c44169e1f820d34db133018ffc537803e192f399adb97b7cb1d7bebaad3c413b226218077003a61cfbccce8f64600b352ede772e4795e7c809229aaf28c936edf328f78cb6c242a385e6d86cbad4765954e67ad5cbbd933bba82efc5296a142af9d8fcc05912d8b2f8ff12b65c5b34c55970d558aa72aa7cc01a104ba0a91cf2dd44c54f9d4a53c9fa0c5dab0ac5cc0ceba2d97c75d0e66e64b1a6de8ec186a620135cde4a5c5449ecfdbe7832e811f3c2b0aa730b7bf24d356a000c722e6a84821aa079a15d6f7cffbb49e1852ec3e005a17393c0f118e7713128d21cebf23d47627ec32006c828cc18652a0595622d18ab6980742df9049f0803b762d1fc409e5825c6c6ef082f176a475e16d8d32819cbb608bdbe2fb64bb3eb5e8de977efafef1a63cd0006ccff6f0761560aff5255fc62dfd8fc29bf6b6fdecac10592dfd035495f59b531f8bb221bf4e963d3387704f61c309ae782cad00c1539783580ae89e5f1791ab4384341f198b6c901a3bedcc04c22c865f5100aa5d41e1c3a28e333f75601b5079988d004ce889854b975e6fec8b2cfc5f9ab82eec606c152c89f1aea80db68e87736d28dbde05f7f3fd427c3cb00d5616029da2c5bbb5f0a4b334151d449b882a74674f3ccd6214c77e082333220719d20beea6670026a7d53c65025c661be152f63a1eea74128da1942585e1d12ae4a2de89e670480872ca54190f188fad7dbb6ed0efbbc5e948917ffdb5ecec9994d2fe370d2c62fcc6399c75b06494c4476f5c284073dc0f447fb5ed5e4e5c91e71c73118cfd75d5f38bcd537214f10bde70b7b69f09655007b9b1e89413fb24ce6018008292c0789d63d2a1ba9090db6b2e1340b9ba40838f38e90e20c2300dfe0224ba34a75c397b52ed6295c2f99eb672e7e80ce4c395364a5b2ccbe073eb9f571765bddb49c947fa64a2c314514e9782dfc67faff3be254c3cd11842e9d6f4889d5c9cc5e9401024b6499d2c8bbbe70738e44b2dcd0318aeb6ea3dd6fcb5901eb2b85225297fd38f4a1ef0cbe9069b757bf9bba06914340beb3725d21230a1eca312ab7e3f7e504baac85b31260223363835b032f422d35bea9bc21fc6a4f0f1f1af1c3a0d4a1b7f90c13384e790f7853a9d224e2b9ae612ea45f32d15db80a554fa12f33fa94bbb03c8c1ef4408cf9c3a84089d1683037796d547acea1984952e24f6958fc03c61e829665b270bb7e1d8f96e8e3114f861c518677ab733dd6e24e5944026b2e25c56baaf48b4b4f2bd0e78816381ea43cd87611ecc1ee3d132ec5bf420bcac7c008b57362fd77da833e56bfae146b27f5b1bcf9a1c18b243fce9c8e86033f0c91cbdaf26abae936510baac066677933cb979f1436218e51a22a01312a83ef305054eaf4c59a70f1528f22e614e25f9330b03621580af3858c67dd7856b07585c314babb54b681efa732d9509808a743e5a632f828daccaa5e2e96fbc93d44f4d7bf50b606247d1eaf64ffb084bf2b3dff7c730b9d12ba591bb058c51df5ebabb9b1a13932329d135d04b0a6087bfbaa57610ab647f5a2fdf214028370e3b305a622ce2e29631bd8b464bcad80053a922bdd8e36bb1e404fe412481e400c38d606887f4e09ce06b57a5210a25c5ab3c336e4849f76d70e49707ae2935f1dfd8a24aed7c70df7de3a668eb7a49b1319880dde2bbd9031ae5d82f0000000000fde01c32e80a1d7be5cd020f55d70237eda1c9ce87ffeb801ac6ba06fc87f14af7ee001f71367e7c399bd5af9c4321eb8c9b9b40f0b864113fac97dec99817e0863eae150b7473246a404791e8df7dd87b891f4898f4b62a4b4e8a082df0308bc9ed246779248ebfe250c90ae494ce65bd4301aad926c3381b6502a271051bc924d883b45946fdb0d0aecc8016cbddf8416479646852b5dc7bd4171f2be349cef6d631d593db44de0110a7285436ee91b7f614793048f59f7643005f80c98418e7503c5d3d2dd66877e556618303b288e4ed6173be39045e72520881b6c8a03ecd4c0a338fc252f38f0155cc614e42e69f6623d9148764c1650cfedd235648677be68f1a0c3ad6dca507bce497191f654e0a8d122823fcc44b875941639faacccd1e36f881520f9e73baa1a785b410a30695e20e2921560b1dde27097f64dfe29f80a36ea47392454dc56c11eaf29c69e2f14aed2ddaa15356e072979ff22f7793380f0ea8e6c69ca4bee3556fd7a8177a991b3acaf3ee6343b544902ef0bfa7fc8224c3c2f72857cc1d0385def61edf058cb83f5d5f4b8551a827e740855672918ba5e89c33c49a25bc7fec3d8555bf75934dcf455a114fcf9aad62ef9d630c8e971c8dc03573d32f177ac1a88ccdc32cdac46bc0da79afbc03e46b90ffa0a4485b8ae89197e28b4305780218e3ad88e2aff514d9831e5ecd3b8fa4f63b1f5934531e4e4a61d39fbea70d790b2553cebc7787b1b725efd6a7a5997dfeb8cd1cd64b9592e17f8da680967f83200ffdd1f9a6761a401a68ad4a8333a89826f7eaf8ac86a1d10a90ccfd1f6e39dbaca8d3f36e6bd26cd19f39c3de7e12339097a36d31a95c513f3b429f47b2cfa51b092d6e656e89cd4ab951621e0a464be157cb2b32863697620a018835dd4d26617d14615b2f8dede57f3c1ef76db65ae99f0e634e8c92c9c2d23fe33ef41326c80de7ba895358d9c905bb902a33c2cc9ab83a8114253c0b71f2db8f6686f57a8eeb1c8d931649767a92946043da9b56d0bd108202ac4618680ea828f731fdffa842f0da822f7b3d938cefda7236f87244e7d6e45330af9e7fc6969efa1a48d8d4e2fabc4b048b996dd483a0b095fa1dfde3df2e4e9ca643528292ccef9e18e393abce26ce80dafd197f71acb86ae668d60a8f248990a73a034051f83d98b52aac97c280810a497b2e369f9a98e66e43f3a420b6c5bcf64fd1df00310d0f2332b8c1b48b0f1a7bf905388663ea87a17bc351a25e1f823cf252c4ed6bac6a0befba7299229720f55dea4cfed0878ee992f14070b80eaf0950673174e680a8ea2725cc6844c2cbaa5340aeac5861491c962791b29cea2c10ef3c4fbea57002ec0b724701098808798f015ecdcbee8f557796d0f948f7b134f613537e27d5189a233dd93845f57e7ee32b2fc58f00829cf943222628d28ad37d7e8698c0b43683dfcc79a0fe795aea76363056c4a2df20cbb6ea1a94ca98429906c517b4499ada8ca7c33f92099c24706569ccea7d80176015020e8875ac5e804d158f0e6cc77390960ac846eba694fbcb154353ce31ac0f39e7c140421f32eb5113a4a8a82ae5714d7eeb6dfe172a114e82520a3324e2e0eb57170e4d1ca0f71e17bb5039379ab74c82bf1e59d4e18d6f8219a019e1830f403db9203611a7425c19eca17335f8b2402fe724178eae4873bc0d670c86c35a678a94028e8fe15a81800975fc633d86311b84bd817dc4823e555da13d8324b688522a33ba071add3b52029e71f53ee24560005461cae8646032d5a7647a051ca9173e79ff0684b179ddaaa5315c63c809777cff4b9087da9d13fd70229060cacbc4f6a5173d9e5bd2236239a8f3f1b35731b0a8eee4e1dc351c45633b381319ace4f8e67480439f8ba094e4dd6f5f2302701cceea42fb092459a110115dbc4b4a23848aaa865d256dd49c65e2f3e78598c0a676eb92689ff84ba3c62658f17be076913bac3ca6730d7b8fddc085f0f293598e6eecf7777d476c50f2732df4abc81e448b68bf6a89b8550ca99fef0733a99d6aa6819eebdc3d0be749f3902b7a9468fc1def14bdcc78195cf37a56aa47fa97017e6051c221875ab74e5af730ed6c781d09a090efc0fb39013e193c1cd6d511010ef87ea4d415cf27e519f1910b2ec08e0f96271919991d8f49cfa308c2ab1256517186628f2b68fc08fbd40c083d0e3e76c3998f7ba7565b4d821d43e9f6a6e131ba11c988581af34fa1a5c2f38e560828362ffbc98b532ab54436634d51442f91b3a5c270ecd05d6fe23ccbc6b8e6d162b0b052f2e8c5d757854e82b930aae39b08ea2933ea1c24eca311a27d1423a6991fb632e82556a01141078a6e0d926540a82d038b91fc3292d655713739a103ccbc2d00c2cfd3db76e059e9681f522d20b53aa08075ad64770577c8f62f1fd5fbc035e8ab1adbd422691bf8fca969b5602ff113fc584cabcdd9b09abc611c5d018e1e01b6e9bc81d05dcb2b925446a3c99eb7d6f33906b0bbaf6d54df9544d3d45008aec204af4737705d53c00e28289e9f5039b7c10bb124486c78026f45d6f86232ad8758658501756732f7eb25b7942ef8e1ff8001aa3fb223282292e2a232701ce204a40ef5d33d56ea348788dc75d2b189f0e654662f3fac48248728a99e3c383bcb90a155c975853e60142694ff51a99c69e58e17fe128400196c2491493f132cd894b4713aafdd51ba62cf9b9c0a5780b907df4e8c3651f61d332b27dab4101e686cf000975f51ad9714f34a55fd8559de60146baa51bdbac23d7f52d0572de1f2606d2740cbaefe4bf7c481f01ed4241b0fb11e876878b2146d35936d373e600fbc01af91081622126f8bea375d7d4d4bb0798c6351fc76740122cfbd2b37a2deb1fc8c3d1418c5b372474579e74579bf586c3576507ac5a2403f35365b1138b23a91ae42b452c695154bb0e9f207f8167a55055dfe777f2d2f02ca6e7c1d058f30ebbb3478a6214ff5d4408e5bd7c68fa2e2e72b34031026283537f5553ed0ec745934ad9d80494d45f70445265e0c7a44b8f623cf0f1e3829c430f14c189d21529d08de9c2edf644f1c73f31288ba01b473a0b60f5438610e9fbc0588344da1adf7bdf8de2b7f673b1092293a57863e1e57c2de648661ecb464d3f5ad14b16de9a8d7670910645d6774365543808a1120f6b8853b2744d5813e1638f5009eb80b4f80ec542dd2d26f38be0bfa39004e571d0d9fa317e82fc8bcfcfcc22c290ea08a04c7b23f374e2618282618b563ad6823d04d782db515eaf6c7ccff0925a31841087d6564b94ad4881b27e1bf5130e8b125783e416501aace48fcef33bfa0a021f45133af6f587d4f9db7e1124238934cab6c201aa9b3a2509f47b912ee8c08b6e26360e1881a191674f1fd4562b083e046b309e0188d46ec0ba1eb3ad663e8da09606816e71ebedf98474f332d0db48feb611d9bb8c9c9278c805127e5afeab725ec1e277bf0f6c39579a62c4fc5af28a08fb689f532f70a97379c1d01b5773e66a8ede53515681fa99e705875ee753e3fe1a5d1dd4fabcacda6e6240028869bec9a2f0c318973a47efb55dfddefc344b52a352bab66d16be34bca355d93c8f796ee45ed489c202806e57ab7504752c77f22f371026a1c659c9b8a049ee00a52cb513ca264b58bee06ef3760d30a03035768ac5060e0f10e83f1b323f9e52d5ed7ef4f520b04c8131824d98edc957f955f4f56c97e27bfc6ebcac914bd2df66f625e6cf8180f71b6950b831bc2144a7eed754373ccba95c11904e90c769b891a8104cc43e675266c2ab34e5341869340bcad8f0e03274a33069de50a132f6996401d5054107d036b8954fee7ad59c8f066d9a28b8e3e38081c46cb2cdd03462ca6bc3ffb68e694269e6664a7e78eebbd079db252de1a5e4d163f651e95ae1a364c95a713910bb10f93e48c39288aa7d96a962a16624de9a6a6d17c0b4aa76db1c1d384ef9584685f92f6d89d9d7c76a90f5f2bc5c5ad8f6e23a57330c99bf1a28b08bc6865d5bd7fa5df1fd97b016c52a82153eb51e18adba7306a38cd57ce9d086a85ebd8451207f0adbfe103f78e62652bc415ac7ff100ca3b6a2a54c24e1a1e2ce09d1da16064a791b50fe4e7fcadfec2fa4126b36f18ca19da22734de293bd5a46594de528a2b1729b28db97f8cd9253c643819e8cdaa7a9490412f97d31fdbe291dab8235fbadb8e6d572d01730ab6e8a1a4d85c4abcc45952b66556b70e25b761d13b41d1fb89dfe0f4a4258c471401be464aac77e77a05e0003c9b29891b5320d4b4c10605d76bdde0b68f3bf7dcac410275289d8efa4813c0e0771438e98e5033a14d9c024db5871a365ddc1b574efe2498fa5ded028bb1a7298cc34e9cbb97e367d1e3e43f0f12e18108989bf6780f17410b6d18cd04506ac1ab3b9c5723b9f43523254ff36c27410d75fe0bfbee904079125c56627db186aacbb5983a9684bd59788563370724326940b5c8e235bc3dcf39095f6c73333591c92353c1c1eff96c7cf8d63c14e8599af2d3ec4e802a2da1a912097cab62d280c4fe934a87819f5ddef95e3b1419e8aa3064b1da8c071684ecdc61666523cd8152192821a3e11b0a561a0fd6b8941b61697c986b0a39c67c917d2dfc3c017a7f65977039c3ffa967583285201a75d291db2d2e05636d143977efd8bc62d304acc37c182a22429db7df3ad5099a3ae55d34d52b0a49e86737228504f2f31302604e981484aee7f2c96730e0477c909d2783b944c694811f16d18acd2bf88048fbe1424ba57bb1cf99d9b3a4c0160a69f2f62e1ba92616e68a9579ebfef073a5754dcd79fd7d639ccdcb579091442ee3d41366f6bca7c3ee3b55e3bef69082a8a2f88355cd4ea9f563d934247c09b06e626e7c879c241c1b894636a7d7baa0765596bac635c2a4c26bc6be3ef0d7473cb93a627a9fd9133d1910d72214cac07375b4233987bc113e40545991ea6bcbe77e9332d567da5a9b4078ba90a4f4708203c2ddb2e7fa846014f7f147dd27b1dca90339781e866e3b2afb433f45b303f3b1d8f5e0770f42b7feb90accba31ddb21b1fb6f5cc9f3290633faacd008a50ac589e35caa71f221836352f7d249e125e0cb624f8da953900f26fc129024d81b5e0a7e9b9b3805f8d1b1ed0aaeeca905cd8951cefed311718e3963932cbfdf1a7e521b7ae0066361edea58d435431d51df47d9415b73f5d4d5cfed2ec0da9033182a30ae6e3cec2373000070804f5a09d208e2b640f6b69609a888915230232714a622de83c22ef36376a3f6bfaf3514f58363010001145a576b275e87377018d2897a5a51bf8acfd93e3ffaa670424e1b727531b24511399c8bc193f3f0a331e9f7c231aeab65a960ea013e33360e8fe8a72a326e5396d56307677e497b5630b30cdd259337b86d69ced041e50b2b1e3bac8ea04a790d2e4222ee8270492219f821925b9e53532e4a8febef12c979c06056adb024437fedb6258f657a1191352aeb18ee8285a78088f48d70872b06f1af2a82a1b6da0794603a192c3f641826d979a9129294d86c1a5a1750e0794eb4e55cdf04abfc69bacf314289c31a1e0cc6579513f387a15b7cbe01ba090e58b83a0128efebeaca87d7e26690ccd24436df55f91df89db8802cc6f5bd9a83e0f16c12db9ef714157e6cb64bd30b6e0f37451b7be69029c41b504316fb773d0303d3f7abee79db307ccca10ddebe27582604b49b6501a353cf4f46b3c41125d3edf1b75a7fe2b5b9e4bce8c16b607c932cc274c9b88e6fa57df70967b6c585e12371c0da219948f5e8698e8a12f6498128bc037e588aa13a238c349923b39ec867f33447ee6aef1aaea4a3a59531d3351c9d0024d4e343890b3c81b4082f898c192ff9d24aa2d67ffd398627deb446930862596b07eb0e053bf8343d2b473b02f30d58d97f3731bbfd642fd7238e78e73061e7551a95ed8dc9ed4dde9b2e4820524c9a2acf06c7c8c3d9dfb6011a9b553db9752d19f935a3b54169170651ea381e7b581c4777ce9065a351a05f02899512e5abff3c1e19829e38a9589138121d91c3c8d264ce45291bb81d4cce6f773232b475fa37a5239028762bdd940b7c57ca64ce6a80eabf4a3d4f8825162ad71e0fc5ecfaf1b4e477fdcfea2560a5d261e17ac6bbaf1bad39863d047c7a2565b02c3448a5d98c8381439a3c266dea59bdb7344c0d6d5b6efe69816eb78a53ec182a1527164c44799981b22b19f33e9815a57b9a8c5246f706a95aff9ef46d315e09077814e1b4cae5a95a6b91d10afa3c00d82e92a2b06ba032f41b82bb536ff806436b55bcf6f294e9e483bdf89cf3d3800e2dd5241f38894581c48bde1c03762bf356f91fc1709c4cb256b44e9d3dc797944448e8ff26ff5fa4a143db0b8fbc046667cf116446e9e96c33c920511d50cffa998efef3c31f60c54642d4e74eba2835fa316bc0b744fc0c6a1c1f417ebe94e8efee9a9a81faa92398ed8d4e391c2de56b5becf7f88f9ff92c397fda0cffb200224c5989e24e6b5b8586750c94cc3816380bf3bd78b0819b6a061029d04daaf4e7209b01809a96d9faf171f2f0051f66c9f209957727299378077bb7993d5cd8498e26482f728805422b7ee3663a2e83ac02c139e71c1db030d34c8b47c5749b2f024933529a900d3f3479525cbf042e9c56ab27a5337f01990cc9fc857f0109d1066a538d11c798b4d1620963561063f5d5550b5e8e0273ef4c75f449d16e880184afed92d85692cf41027d426e2263442c109b9a57700dcd13ed2bfc300e11ae68ecf15a63b93027e4ad349afa243010282c3500844e11c7e70ef7ca543d0866c42a2bca6d32f16c0dd2c7d387304b9f2348e28f4a1d7cf8c2f916baca3c56f3679737aeea1a186fdf1149c512366973b68cdf025525fc138fe27545cb61afd1403cb0d86d0027e3bb4f9f37d534f0594fa68409cd566d869e47a86338ea1b9583dfd09a5fbb9f14409e8c44cb1c2cbe14c2fe1496515612ac80bd613e5fec353f1d54be561ab3ec48901dc054162c0ca4b2260efa5f4d40fdc1437afdf12711d7e47e1f2ecdf1d2db1acb489c1ad72a643561cc67990069406259da6b8a1be0b0dcedba67fe860b162c2578a01c7bdb6079151e050ff8c408f03f01f2e539e937c559c68826cbd9e0f812c51524c977f470513bb7c5e6a6ab4ab2f0f3fdef09aa68bf646ac41b5ff24efdc802237d869f8c61cca9b5ae1a6e714682d0413c013321513f5185d1270f1be317ef28ff49b7d957c9f57d97b073cad269d868e5220cb17d498dd87b08e04e1ecf08114ce16af5fa9a5c6ed64c4a1ed089874efd697b43874b315ba68bbd569f4ab30bf3bcfaf0cf4d7a1d6bfc7ad8e745113145ffee4ff65c2f45e3a33d6ce6836504e46b2504793544bdb13c7554ca60d1ed538f406c1736728191e1a0c7fb493c2cc9f50bc43cd6f1ec2ef121ceca053ee304ced8e06670738fe8558942de4a432f489b5500672ecf401405d3d4a8d1dc87afbae76dcd5019c17b130166778fc9262b440178d153d1db762c915a044531cef94b4463e3f37f0895eb640649ceff3d6064e129c4fb4fee145e3db488163588a447da804b5fcd24853479cb66654e3296e3fb14162f81b96305a258aeb3e41e0f95580a7c3b550490bc92fcbc885c1c08d4aa33fadc78b31cb1dc6b6de873912be2d4a975e75cbf6e268478166b3f3aff82ca941f72008e5d0825b4aacf97d2c8e8e91803e80d707d3f2c247f9c222e75b625276777df11966cb6306e764d2ad9918a09746d78246318ee2ba0ce753eeebfc81f0537b9fbf54a33bf44af3d33ba0ecff50a2e6b5015903b796a3873245c684ac869cb0b496d3a1fde2822ae3399b5a462d19bfc437864cb37dba8503671defaea24361c253916f686903d82ec0114457501831abc5b843ca8b905a318ee0a19d261d91104abd055ba419f6392f8ce47b7319b47846a1e9484767d722d246f38f578c77047c07d0dcbb0d174655500eaf0ded2696c4e0e286268b70d355cdd61c75c937fc1a5c1a49cb178cf0b79443afb3e6d2fe89d33e8cecc4836068ce0337d7c40f1669f797482a05adf4a66f7d6d0f5736b250db0771cb53e232d457ee0a38d78339dbc13603ad3cce223afa6ff228ed4ca72512a8c87b6b5de090b84ab3162a2e0ddf2d0bf9594c0d7c7ae1964a7ca457ce1775d3d8f73bdfb2c11b91392feed8e546cd4ee795b27456f0e83bb69d58c327325302b3b377200272b0a1e6c59f83bbf3c9b7f51d6910c1aeb6a0b9730618950bc233eb1cd0e9c3c3f56385dd45bf162595620738d40659c2332e0b2a5493bd9cacfd89354a2d21a82537412c4ff77cccdd07fe64dd81c292110ec3b549cf598003a2f5620b62e05f1fb78099f0d71907ff2cb57499a8d43e9a3a5259cfe868ce6a13617b465671101b4ca9f8fac4e96444b699c62e4d719fd9b9b984d635854fb4ee6ec4fa05d2e6439b83b99b60baa348ade0d8c6cc61856453294a4113d53048bafbaa960e51385fc86d0103bfa92e18786638b6086dac70e4c4910071fa2e8dd516701cd1e1e9f1a505f71ca754e4148ce1ab3f1703de0927c3b0ed27680eef3559bc487b93cdf87bc797b10d40f00ed621e51c5f0bf0c0c37e4ecd2a5200ddecc6b3711c7307e0f92a267cf786d86dd0d9862019a06a3e46f4a830f376ec9feb7ccea52e907fb9e07bc5bb6d0b65f37a457b61099ef719c930a8668e4f8d6532849c629bb081cfa34c225e04a81300fa9ff89d1adcf8cda6baac27498949d120925775e3a2555db999c6193623eaa31484cbd03fa2c95b0e243d90c6230b5bdc03f96cbea3aef61b96c73a070cd55bc23d5ea9330ce30b18a92a26060b058be34ce5e9a52379851e978d11688cee2af6afc85354cfd7372a5de96734ada61af62f3e1ff96009ea7080e6d507c228fa0e14630e5dc86160ab0e7a7f53a172fd405750a6f5d285fcd58f1ae55178128aceeccce7e54214d420f3554f6eb50ac449d227d590a264b435ed77d38a8553dc11960db070ac6dddf3eb70b568746186e181b4075dc0725d74d812aaaaf1a49162e4922664e6ffc13da33b96080f36fe2af3e67e66a382caf2fb726e0d9ea24a4cf7dd19b13e54cd4e15a6f15d0a275367dace48cee1f783e16b434be9ba34ba96bf57faf099ea46c2b1a869c747e4d4f173a9051ef0791b3769ec6006fb05ebd151fd7f0c02103fcf82f0b36aeea1e2ffd8413ae651f1d39e8a007e0740d3b7f3f3179e7c00d787633bf94947c4c8ef1159e800ba43fccd3e2b51fe7658545e99f8a2279ebe5a9775441efac8c34dafeaa6248678a81f2bdb6aac2836395d2ae7cfef990f1f93c8fe77990e19dd09ffc9c694c00a93f9c888ce384d013c3eec0f2de539e32e24405f0a85d9e942dfcf08b1a9b9d418922dce3fac4ac146d15e267a9bed5407239058dd4bb0f26936930fad9e95f189d59551e5b5436dbd000d6d51f666674ffdd56a8d7d773acb79889796a67c79401521e4fef3e1c13f336aa3730f8176d5595a212c340520f67c60d581964354e08570ab76eb1b4ccb239a065af838e7e12cfe38b1eeb576a08ca8e80870a28423d9ba0eb266fa1452246873308b04991fe3d0a018638a7ea504d91904a9c1ab40d91a6252d0362619fce83eb13c2c381d0f0051e9c8fe5b864c00ed5c559beaebfdf408d60c7431d88ad093c17f95ab80aadfad695d4a5bf7e8d5de9ce28ad161fe61a78967ebbf60299703b64302e40ef312013d8ae59437d6d1e50a69791c70e149c098db71154e841fe7d42afd894595bc1a7bf6c5291fe8539d8f9e61b7aa7eb89e397985470d467a0956a841b46a7afdc32f8098013608a49a8cf3aad71121e7e75a0fc3877f058214534d4884e1cc290afac643541386d1775c40d02d5a24cb415b13e687373b0f9279b2a71bdb5e935e12d5ec025287dbb0ae92f0eea99712d7d961bd8650386f078e0402cdfc184731b8f69ae58bf41660bb3577ba8ba0f39c0dc5d37c16743a1d1d3048afdf79bed75d144fcbf84bf31d31a49bdbb09adc9d47ec4af826fb59cce63e573ceceb7cb65986d6705096563ae56f4d888b8b35e9a417f07e134223ba9920a6da3c3338846d83d680ade617cd73d01d1e7b02bc1f38e2f9aa3d72ca38ca6b8b2b047a4ab4179f9a2486a98d2c47f8489e19660d967e832667326c5ed19e0b6c371185049e236809746519b3fd0b2da81bb047581751e5f000c27f90af1e9c6f94122a52882b25124c774dd2bb9dcf0cd1119b849e40e62f4fac94e984caee31dece427ff8e82d60f1662906336a28904968bae02dbd55144148f7fc0fdee5de5f28641e6927ff9fc05a6bab50f7ce11d0130ba201dd1507c8dab4430f37b0259fed76e7d7c2eea8e711f4c9f7e8e9e22540201003c3bcdcba1cc88821416ab7f84f8ec7d39de552f12dda67ee591a5e7428e72a041fc192d0763ef7250c1b2cb7b28691dfabbb1107d1428a48eb70b8c4786821f01008efa775c2dc56e24c14b0e3c699b3f87750f15898eb03ecb05c1cf418cec83b445f823ca7d9b5c6d2f59d2babc66fa45ef25699c6334df7096d63dc863dc5c0c00000000000000000100a66e571f0aab1e56f1ba167ae5c70372d798117b27a1fb90ec0e0c420a39be295775ab7d796f04c1eb5bfd26dbea6c289dd71d3a9eb5ecba47ec7beb2f0ac82d2100333473f16d482c8725d51b4cef4e26ec24b4681f9ef8523b160fc0880c6bc1cd01ce2c6d87d0184c1799a254ed03047cf57a3bfd49b8a4438d496d741cf5aca6bd02cc36601959213b6b0cdb96a75c17c3a668a97f0d6a8c5ce164a518ea9ba9a50ea75191fd861b0ff10e62b0000000000000000014bdddfd920a81d4344b290ccf81563cd88d228bf514ff6c10a14146853746190d63dce655fed7940453fcf78f23bb58b1a7bb3a389bac35d05cf6becb7fa512db82ac459e02256acaf1a8ccac2c4b5c581c3f178dcc4b047e32fa1a7e751bdcfc6f42d46381f89dedaa04e8030000000000000afa43a02ed891bab3de4fb88ac8edcc540d7c23085c43c821e1dbf7e42d710e15b2810a40cce7baffe1fd1a1c9334b8dc2e54fbeac600ff17a771ab8d7a4c850001004100829788fbd48367fbf6850ccd87dbca6e0e9eae376e4c320704bf2b392916ce5bba296d83ca3e3f8d5dbc9c34b273750ef1fda0b50495393b096a9206ae3c4ab5 diff --git a/zebra-test/src/vectors/orchard-zsa-workflow-block-2.txt b/zebra-test/src/vectors/orchard-zsa-workflow-block-2.txt new file mode 100644 index 00000000000..7ea2bb750ee --- /dev/null +++ b/zebra-test/src/vectors/orchard-zsa-workflow-block-2.txt @@ -0,0 +1 @@ +040000006a62d6711f0c8d014eccdf985c96bc787caca4ed0f096ced10a9ae649376218434e8f49e6438bc002559469cd420ccd702a954e2c475eea628d80d247ac4d35872415e2d784e68e2e5ee7c588e967c3c710ef416435b915be2ccac452718ec7f0a104a4d0f0f0f200202020202020202020202020202020202020202020202020202020202020202fd4005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020400008085202f89010000000000000000000000000000000000000000000000000000000000000000ffffffff025200ffffffff0140be4025000000001976a91475dd6d7f4bef95aa1ff1a711e5bfd853b4c6aaf888ac000000000200000000000000000000000000000600008077777777d80a1977000000001c1d1c000000000000000000000000000102aea196ea3bce51dcdbd01bdd60b7e83a294cdebe9f92eff7bc393b13d4946712e31abf3f6f4fefb3419f01a558351692e812eef2e3fbc7d9d8b6e3f8d42d701447436a4a4e8bd2db1ff5ec328bc6801e25f273260e8053031f5a4f94c960b21e94204708321590dcbebf2cffa69b1668a5b78c961eb4ab7632a77b429b7db53a430f838a7439f44f7cb585d4d97e39f44eb87e1009b9eea0407db8cc328e282119031d11888d38ac2c26b50cae1c7e9f8b354cf63648ca3bf4cf532a4a519273bdb96c8ef842611cdc1e99cb7efef97597de545a910952aa9ab4ae8ecd7698fbdc8bbe853a8feff9c963f7f39d26b8a20db3740dc5394a23b1297457d344faa3aeb20bf1ecc59d646e29d417644bcd8f342775d7fd0c50ef957cb381e823c353139cb2fa037c08ab15202707a273baf8c17b203e255ea89ce435b3a7a8ae3af04a854e4b6cb10f3d3080d389e73eef9c27c06f26d66063d719e8e7c8af8f7df014ca9fb93b31557c3859550bbf8906665a95254a2461cc5dd802ab015505c4e42bf25f0053d34328723564f4eb70f1de4c1c2e1efdfb3855279302e8bbb5d6461fab8062e5ca07da30d264ada980ac3bc071eb6d38aebd33ba08e6fae8b96e86066dcfd0e8367dd3ca297d5899090955b03a10222f8bdfdd40a312ab7018526b2f305d2c66d0f5d8a917e1f64cb81ddc19a9ee9ff045ad11f586a553237a901e52647d7c9dae9371ae4b873c449f3bae6878e7e0abd1de435148d7228c191400e26715ab45a05c6552397c894a4adfde6971b2b1608d77cc7f2033aea85a9ca823f1118ea389b98715153530ce0174bb306d6781488ab4c3b3cbdec949ec5da16cf4ed4bf24b29ebf2c07e47ea1696daab14153a296ac3edca0107cb6c6d0411e22fe9ce68ecd81d04d0618690a4838f755f7ff6d7738d2dce4d18f56180eebe3e34d4735051fe99fd251e3b50eb44201a89e329b5d4b92db0c1b05592fe78d32b7621b6996587200a6429969e5b6713ba1d5e75ad93e6971b81a5d7dad55fbe5bbfbffa450110d02010f43464d01cd8627e2367b54c9f415de80531d73622dd7fe1944bc0ce0c52439c5a6ec568015d3438e5684c2789895c7ccd339f52960e5b8fc5cce08a6511fe128328e711798c06429a78eff80fb88e818196da49b3ce1ef0c2d2d4b2dfb7e5e8642cc65706af6014c4723cf2acb0497cda82086d9c259419a060043443f2e7086649d059f573fdb911934b443675f5534ec98a7ec473926c645baaeb2de5cb35b0bf2ab95f18a6872437f456223d010960757a608e27d267905bc8733efeb3f3798f27322c127fac739bd591a4f59d12844768564518e154e38b165a749feca251d2e93afbb9b89cd33bcb8a8a88727cb88a2ba44c7f41ba8fcca793bd983ba05c9f154567c289e946ba8740e8aa3f8371076c0ef8ee811b5b707e822b8346014056114c636588fba327be66e50517400a2c56219369059e5bc1474e296323e66fc93456c1a213780d6d6b35d8f0de89196de8e3472bdb03d00999d78bb81d2239c9f0e9f72a4eada7f2c7b6cbb6f04c9601350c76b65d93882eda8aac4ea0ae1e9a66298171749071c2668fbd57584c769446e37ada616718ea737051d9c8fdc43ceba9e6c61c93c24a47cbc0e427ea6136a9a181556a0d65cfc247c5f45950dcb88ad3d482c62acb38c22c858d226fba53de2b43778864ed38683c4eb1dec2c628c4716be2c52f84c1f74b6622213afe3a371bd7a365c7a279828ee9fcc9df447d34b9f4e1ae8b025fcdc77e8c860f654bde77a0439bb365f29294cc17cbf40118f7801061c94d38b74049e0f41431216c9f8ffa67ee940d5433bd61887b21cd8a7736d20c22bbf63192c65e76c7bb67472c68d810e4adb590ec44a1277aa2062cc4d11e4bb168e7bb12d549b6bcecb93e10dd90a85812b2948e6d300badee2a5eb116e62715b57b2a8b695698bf9c26b5c378c9509f2cfb2cd1ad6ac0f1affe78822f7a44f9dc5c371875cc485592a30c4b640253a4e2ba18c616a19b4837655aead9aef26cc6fe5e7a5091699a837ccdb2d17404f28bc6a3044f5c5cef2ab5deb089194b4501a8ed869850e307f9721dbcffdb871c20cfa56ae350c431c6f9aac6dba65efb48d3177ddc5cdb5c77a6084a91e839a7a3a723954dbe8fd0151949da50a025a718bf6c3eae12818c40c64a3598335b82bc13e33ccae355b8c371934e04530a74b149855c8377c9d2111c36df6a25caebf2ee988e6799298f1c49e025c09a82c5b01be12b232623333a518665fc7a3bcd3e24b5cb492b4d764fc49c22e941db838318a64447dccf501cd22c3d4bb9ca569ceeeb65be2aa6814d8741713566e738c1bc3007a734d3bf2f8eb411175df180a1d8b51d2496be07f43cd4fffb9a37c4a8778594669315770899bfd43a76556223562127e33a450a0000000000fde01c754b7fc76e1b32f20d876d4c0aa771ce2c30f20d99a6aaebda6efaf1b52017a22336afee15806267d128443d6174621bb0886342f804f661ce1ecb329ce9f3067bf1c0a33e62d2b539f4e93529b7021e04317a7b55eec94ee9bb5aa0761bce27774af8e9b388a13f2b5632832c309537698d7627e638ec3e0d6216c6f6ebef9a19ad53ba916a40bf1f9f584f78e1dabcab46a3d4074cebc0aefc626bc9327abe4f7769fe35a43ae90d38ffb88d4beb973cc2170e6c6c92ef9282222ef47e749c5b0a4325be1046ab492450ce15e742e2ddb543592829dbaae427e56d1e281dbe6d4c5a25c92e87c915c32474b947060f59ad463790ce2ab5e53c6e6e5c674a19e31e2a46e63d61355036befa328c02031d8a1c4b5d89fc0377695668b9dae329f316c39505276faaa02d2aeb2bfcbc51b627103a23d479c8da69cff875e71fbcde0a61fd75aa9f42eda310b868ac90555c51f98785cc42bf6e521f7d4f1f74870227c09b4980a12910fc91c76c3586f23ed083a3f3d906a92f1e9aeb52820b8789c88639f758ed23a4a9621fa4c76a0b34b48b73411b8d1ab3f39550d05dcb0e3cbdb55d34bb3c34a716f7f1a38479cf84694f0572874fb101999e7216df278bbafcee6958d69c064e5fc9b03a9e67030bffb59421f7f400f9f07aa03231bd1754b7cdb9835f2f0c2e63c63185772614106defde3ee216ad5947e20d9aa8c08b614026e5dfa9d2cdcbd1199974ba4b2227d846f1397d3b84b9e7d51bdce73f185b9b0e1e14d3681d57eb6d67c62677ceb5a3cc1da5f830077a9dcbeae3b17a96afe725ff0b356872b4429c0bdfa8589f7dfb37287c5a473c16c73b08bf7954b6ddbe88861c729a6e4b5f320cc10ab8ab9182ee267b7135d78497a1806e657c0faacb86874fcd4b23cf71b597d7440725bc77a118f995bf0773d4fd4f3e4de31fd5d521f1076ab72aae628c7b58af32756291fd7e3b272b0ed6860bd7eca6ae2aed965edf044aa5784735b781ea69f9a19133e09c7b8f7a6e21ac4658d2ecd89aa720af5730e9eaa5cee76883272ed485276a5eb16f3446833406c691113595ab200a70af285b3b1d28c057b3554edad652fbd5d60a271f6fbad47b5231079e05e26e2bc990f573403a4223207a993f2c2d80bb21bdfb03ef279cfe68ef5eb1bedeb71a6f8c268e8dd45344e0b2b2d37aa6041b99250795529d01a3547b8a373bff62665bd1f3cbe70ca3b2797a688d400ca5f1734b938dee2a128b9b6c081b0aebf1b1c3104a0260642c75a6a507d2ba82a1fb4e285c450c360b31170545b211715ea68556c9603fff8467277e1fe3ddc86d2b832647f66c8fa99d0ee49e3b900d003ce041ec6ddbcacb4655ec71ae6566d0e057e80d3af00ac928144bbd48bca2089e76923b354c3873a96a73c5ce0169d67198e6754aa9d810c2d52c62af0bfc25e6a7752466671eb1c60f472ab871a2807159abb991aef22b4e23e066ec83b1ee8b1fd3b8b49e4d1234922c984e4150642b3bab2037ca1a8ea8e6d983d2a864132cce10c23ca0b20f94e4e9de7784dfedff7cc51021b22b1e385f4126699df2fa3ead6bd1c086d3c1b74642ecf7632af636da3780a91357c8a29cac20bb2fdbd9d9477ae1b2dcca03a61491245fe7f06dab703ac7301f37e8cbaa8ab5b6ba1f88987a9acf187be3c6a74ffa433c7bd961e29946b75868b763df383c4061383053177ed0ef19847c1a27737350a25dda055e0b14d4f17aef727f1e610ec28c2e918dd70cb00b21bef00dcc1045dd7f25379f8ebbedf61147ef9fbc496beb9c68af96dd9f2a2a7a3a59e4c5370a6398aa2647835cba429c72feb7cf2df0a0b6c7fb37d230d7d5c09a442e9d6b49de4f2197d989f31c543fd355a33c88d67103202c27cd4eb69211747aaef2b764146d3ca1664bfbac8c6f784cae3e93fe52816c8fade38210e843a0da7c4cd9f8bad8dbdbce59e3e3754d29478d656d6bca0bccab20db2b300d537ceeb312b2f52c972f87af638bd0e1918447be85dab6279cdd5fcedb419c4e1ea1a799e16021dad3f20c707e686be3819bd02c40822cf7a3b8d1412d7d8a1b9c449d35d8d294993dccfbacc319f42c30ae1dc239223c15a225cff8b05bc098d5dd94a8d10991983801bf293644fe51e321fe958f2bd71ab1ef70d339530b065e4af70fa705615c816db729ea78601bc8c8e86df422ae89bf3ff9edf8f3c81e8eb632f94b82e43580dc7a4e569da11a16cfec70c6a22dcf9c73624ed6f47bd6799a933d35fa152ff61985aadf47be0826690cbef56029860b013f8c1d46e6ffc02ff95d9bdbf738ea5a6999b6aa118fea7cf0ce9af89b4082524d20bdeab5dbd1704983af56f8b27a2b00f2e3adb77a982e041f1d90bb238936c9921f8c8d0a48250fb605de51a110378890d0f7bc7375dc892ec84cb0942184e916d13b6fee5c36b35f22c9bb157a201cdccdf3b2f4ad84b5b9b39b3f7837ba9b052695e9c4f1697fdf647407d84620a5268bab7704920200f136ad1e1b16f3540230d95643b731a1b0bb28df019529cef1c9a2b234523260c86b34e4cd053f7a18f9edfa39b551a03e0c8481cce4c2482f95dd5937d6048b90dfd10cc3359825fa7dbc05adece12ed493d6979327f11fb23676a73abe02094cdd2941f62fe3476554fec806e682aba1e9d85d0fbb749baffaaf2918a849afa55a2078143de02e786f19165a451ace3611e23387194846155abada9cbb845fdcd6ffcd1108554b3ab3a0972a878f1bf6ecab1548175907548ee1341c9efc2f396b10e34f2387c05c7570e883951d89ea65ae53185b585fe2a377f87e2f06be9f860ec0bd1489057fa0adc562d3c1da073d21e285cc50a3355b638308202fbf641149bc7e38647714603dd3894d423c1db16fe190aeaddabec9858d0a84b31518d62e97ac0ec579ef0d93e487abb5b300b411cbae81027a1da3fb507108fd3adf00a7feee2cb0fd4f44bbdc67e5c7f18cbfb72d7ca39e6462b5be148f56e7c3c8c30c2be72161df2b51b9d65177e4d467733ba0ed385b04a36f89edc2c91c44980053ceb60694025f50bd5fad161857f2ed942bf1643874e999aaf1f6fb152ab51e461e06208df5ec065ace1fe18968eb2d96c1a8f880b3a5d41379e96604e421d64e388a35119fc646950ac88a083b797a83cacf80484d465e0d42806633cb0ffc1e3d6c119e4dcf3d5d7f928da116920dee25ec1b3cb0e764af9b403518d3e74f79974e2e91b3c7375f054726320bfc42ef968d5978ecd9274d7c7e28fdf4e98969a56031318cfcb20bf2077bbaa85fbabe36e1f998eb233260b94c7f9735498efb153e2a2980e9827f8966eba09388ce0fa7b933a704c7ee1057686b74cd03259e03dc32d165be77b256c4ed4bde320375631e35607a65c0d8a25b520509badb2b14b434116118e545ceaf1b9e84440fa7014a3fe66c9ace40898e372060a037c1e858306f62ac1b9168f23224bac1dfd0cf6a1eb182914bc127c10d4358bd6ad37b5c274309c856169b9fe2f18cb1b5efc570b1817bb6ed904c5957260b4f234b400410e16d9673bfa70c5507ab36e6c1d12603a11a9b615a657f4a0f098c86604b91204c688e7eb67d8973fbe7c449d7923538f707aca7c5bcbf2f12ad6ffbf073150b71748f176fcada737687b17d6e99f4788ff3ae6cb90d1904fb50bb5dc5618d123c7b8862640c2639194531acd3e5a3775e2ebdb5445ce19d7bbcddeb97f8b3281f7eead74663e512435f855d38ccde468ff6dec4845de26ef9f89ed8fceb02307d2338e7ba1a91d86ce287849ab64ebd95268e44ef092dedb0cb9c43c992921ddd13365b2fc6ddaa69fb01f11567724b92603b7391c0d1019737e2f5ddae752cc87b1fde6f03efd0d1301607d5f912a5ed7eb085316bdd99c0eb6c072845e429572b827c365af84ddd29c1d21daa5dd238fdd3589049dce3f68e414a5c954d288c27706275f0c1ad5a1f7aa28d4330e75a0d3c700fd8a7ee7073e0b11cb4aa15d3a67bda1ec50b0e2fb79c7578970fc3433fee13a25b7c4c7f23993ae5a85e06272677f3cd6bfe702b816eab4f920431129fe60a8f43ca0ccde1ce14615c6a13659286c1af1217c9bcac0ddf59fb8dfd259a014fda40acd6da51c8ce9b9473048fb14724012a34d1e55af44c568072c722dfe47b63321b5704f51b711d1b471c7f1d42c7c4c1d62986da17cfaf6c2e0e8261b5ea1264101292c9d74ad5e1ce210c24027cb96827523c1f05b1e1f4f2964769f305ef4dfdd292ce9b56cf29671bdb590f1f146c199f42c16e6eca232688947be34ef0960503e6990c2bd6562a027c518650472f2d7ccd0b4c2c0e48782aa898c867b0ed35a5ee2b9a49969cc60a6b0f7eb6d62f3784a0f786aa31b98421c2e53619b072f40f640dc4eac9348f297d45fef993d8efac40d54313bdb6b62b4a21cddef3f6d2de94baaa5a1a01dc1930d5c730838cbb3114699eb00e5e05513779cdc8a72d0adad0555286073c4f3df4bb49c69c863ec93975936f7db753311a2136d21b12f20b979d065fc203343316db6d4af99b8e03a4fb392b542a61ac2a8feb029f791dac934e3dc19a974202d4cb9aca89d552ba451bb1ece399032fe3aa471fa38f642039fc7460a95179179abc3ebcacd6e439e5167466cc8642d3ee2434b6212cee8be60673309e7ece17ddadfc190c2c76424883d8d163d8fbaa746581e3e20657fe191c2d8e029c830e96d77d2bd3c905ed5e7dc539861ad5834903715ea09cdf45f16a1b548cb24a3ff3732bfc33cd730235bcb5a12b9b44e3b899ff4b7a7fd898ff31b1ed4c53cd012ad221d6c31572df1aa12f1e5738a0a85b3b4e2bba85659c9bf18d9d4025fd31ffb2978c453388f437d81d3a852f5b4fca2d6de35d289ef231c15bd93cbfb306e665caa31aece93c2c17924bcd599f6971b03c938cb80f182085686d3b3c52250c28212a73375966dba08720397498b86913738e849fa5a7d4eb836e7526393c4c8f67d5c11d9a636ea6c0a80dd5f375ba2394a3d67fbb7bb99d16d5e7a01b21a17a8ca1ef752461e87ec68e84d2b96e39af5894d217f93c77a90e53b962691b1e4d666d2069377b067262c7fc9a89a722943ac1ea8bf474afa52d05f924fb089caf9d8c0f8b62ec664a2a9623f5636a45f70f7ee13d0f1a438c527b948c830cbfab2b5e8d7790e1d05fefd844975aeb1518025ada3e48fe06b5a50262bad72453b679fbff44014248839e528f94737b0a3509ddcb6af0ba6054779526951c2f17b56a2fb5fb762886a536f16d35ad4470eaec941467f62bb3c65d902158b538a02b8c5f7739255598b9f7193a15fc62b66da64ad54f7cecdc38c03bbe0b0138ab76a11c375c1b41b513d090ed060c8b49380e39da7160ddcc546bea7b32971ca686de44aae91d223e9ef643e97d6838a7b784175f23826ef53d7c854038a204f996e274f6861dad6946add3f9b28749792a745647a602f95b73e29f18a2f12aa3bb957ac65a39f262491f64415857073c2e609b1972d4fd98bef1d81702ae000f4c04df495fda7167efb1961da2e73e14d26ea129245e7cc28b40554c72193aa07dbaf7796a5ab5a9e923ef6108d80644646404b39775c54eb9b69f8d5ad332c10573e209f6f1e4724466446069cf9de84fb1982535ae987d79eee3948c462395f2fe5dfdab85f34289af9154ea22007b0ffa4c80de37561ce5a43718062739c92da7f14126c6bb30ca37ca4e75b24d1af5138c12e51396f27beee5cfbdfd33f4603903540e6bd85f7ad422fea491dce38a67ca731c796667a17b92f2f4990cb677f902dc737ff44912783497b29d142b0f310e042a447b14d1fb357e405b07bade56dbce01240cfa877199feaf15854cfd2647ab17a4522eedd764b9299607ded8993386667c391b630603646978e103592d542167adacd3c2f79a51d32918f72d9a79eecae2228bbc4aac88e5000d4a042e2e8e478e58f68c34ccb0ba7920da12baef1144212bc291555b2ba0a54be304026f884f58ff0f151f73c2b5ca2624923891d841a2ba35a54527adbb605a9c7c34225316df163c39756ef54519331c114d65a8b693f3565b54d82a7b2d6c6ebbe81e31a161b6aa0ae9823cf1f0075c315f7cffc524f26deda1bd90b436b7651a94ada84d8744b53ebce1130cac1dc86e04c266059f8d130fb8b72670ae4217cb64be4247eaa54b865bed9c45790270572393d70f869e8885743c69a6852474e1c8780de41318e2124771a5e76b2fdd72e499e3ba1ae34639918c9adf64484da208a85f0c9e5f2fe061cab4a24716c62b269005a7a973680a6b096a0da98695a7c68879eabdfb02a6efb67c07c119b3964bd1311a48ba62563fbfaaceb60b5709b7121a3c7937bf6f412e847be027d241f8ac4870ce6dc15da3b0bfd1b092ad9a9230129face2bc15d62ae532920098c8b6cb50f2bdf46ec9ae7530ed4b2695fbeae019011f6493ba847d0883e918fdb5793762073be7971f6fd6e04386ac973b7bd6f272d03cd5ce4113833a1e2b8967756af923c1f04bbfdd91761f8118ddd37b116322bddc5c373c46ceffb930b4f879115bbb49884a68b8972f92c109a4181eaee0cbe993143d5e410abeed3cd0a186112030dba501e5bd077e2c760894416c3818997d9fd580301362ab6521460ae67f5d6abfe50d8ba881ca7d9c6a3f0fca20161faa7a10286f9805ea972c4358f4462c91bc20c1ccf0d2f8578602f8b2fda488e93793c064b28b660ce91155e6eb388bc74d35a0fcabbfc26cb2f62a9ad552a5a6e6b071e503fd84237114cd1aba12994167c25b70f27efc6f094d796318ae364ff82e34c2ddd58b5f4d2d3e7f274d047a34eb6611405bfc4d0c72aa0d925cf774a493ba25f6d7e746df2a64e99ca228312af74e2ea16a9fb21a7ccdfd9e126282cc4a5f73cecf80e63400981eaec1c2a506a6278f938cff8529f432b11f7b80ec8532fbc4efe3123da93fa1e3dcb0a491edae4562a6ff0ed10ac5622b8b761a816c47e3c457e35feda228666447f12d11a188a0af3cc0568b5175d56be7d2dbd9ffa4ff5e756fe06d6913ce886616588ef8a3a7474d8599e6bf807d7e2c16fd2fca3aefb7f7e3c6d7370e5a5f06ff4d651f8bfc84c8904b24b942b8fd7f1ad2bf43be2a15a6c3130f103aa743311fd9ef27b1c5e3c94c5e0ee4d7da246836d2bd9d2fc94fbbbf6f39ab23567670d6024bd92c45f8217bc72b968fb645fb7bf0dad94daa205c2b32707114f0d3a2b048e77122570344de0cc5341bd2e9dcf056c8af924fcfe927e00fe90a86db2d1c76507bc7d41049f956288499b93ce3101da31c501b640d85f26b432782d166aabcd52b2c1259f1be64c2fa580755344274060495389e57180cb8303ea85c1be637fec123014099cf8d73778449a8f2f7cbeaf2b9a2276d6c9419632e5e1cc704e04c8f4e931d894e8e72cb9c097ca547ab3e9d65c549e50b3b979f162b52618a8c1ab558a08cf4ffb03ad6fa5be884cd91f36ce7edb3f39d0f153102dcaac8d3eadba968537042ce1d838c4de19b920322d2e3bbbe7e3c32246f9c138be98cee9624bb978244ea5e0770f841908783d1ddde305415279a2a774f3e0867257bbeff179737a121308b7e43f01172c172f458ad05786c2791f21d9ddd241df3e8a61d38874d2c19bb4a1875b536045b991efc0e0f50f4333787c57ba80d393613cf7ff307a002778cb0ef16e137f51df6cff4dbd86a663e1c8bf10ad0134c54f828e936fe07af62aed9ac1d061c4a1be5a4bb3ade5db7f8eb73ce04b621bcd3e37cc67c88fa920b33cabdd4744f760102a0dd5e9a3c483d54d3e98fd70ddf2ab70d0a67fa727fc90fbbf9767a908c1876ff968434e99df92b4a163272228a4bac0bbb522526352ba35355c39256dcd2591c3f121e1eb3a72e1f2146d4231b22fd62a90ab5897d648ba487268290cc514b26e6d1e8efc8f864a33116f7257a918ef693d2b7b42f01f97c97ec9421e6f45744c2a3c498e355efe70e19dd2f236dd11d081f911c11f9e953fc3920d657ecf71bf16bb1c59dcf8fb813143f1c209674b40719ee3b4b340e7d722190d9cdf719855e8b0df6219ff1d14ebafd12e78fa701123ac389379281164b4e910d31715070ab76b188d69fef0c76d6b92c8a7057906306131ee222c074423d653442b0b807aeacf10eae54b643c0c2380103a03ceb139e202a546107916120156c2f5f558840b9ccd30f0c049e5a260326920d2c2a71f439aa33abc373e05f094db3f22e4f345aac051a49f3c25f4257148688e88ac05f9929c160a3c0151b12849962a86dc5f52eb3dde805334d3b5a3a41ce506b927cc046fdb32ee9747429c6cd232294a2494572be5890744ca45836450b16c9cf38d392d66623f4bbf7983083ecf4eddc26bd0a981029378a80b8346a65d97b21346fb695db709cabe0aed5317250fb8c1b84a7f0f25a83edbce11db67be1875b1256022d9ff144930e007c1fb625b28f49dfc726c62838bb54962e18b5cfc57908c1e96f17da2200170be357a5f0de62f3066449f0657183235822a3df69c44efd10d554e5ec6e3d6b50503c59525158a002f64e5bfd186bb5c00953063471ed1619994f436765d3323b1bd823453c22250207679df5c833667f37ea203c4015ab161bd1d818fdc724863eadeab76438e5dd8c17ff12289d15bf2ababd5b1f11d99956068ff66a3c7db86de45408f533e710369617c7939e2fa30f774a885e7dd4dc9a9821d6b8e361f2ecdfa26658b380eb9a5a5c6761aa8ec009e7e640c0b2e11b239ffbf6254e4c85310d42635d6fc4bad0b816d1da7ff59712835a33328a2085d63020f5d7a3bb79e100f65802d1e0a220db9f9012d4865c3df6c077c6349970fbfb7af8c02f7bef83605e44a39ba2987b27f1c5e833ebf530e4a44bd368b8400d9c15a296ee0237e80bbc63e4307d6898f88b0237bea353021d1dfe372ec7551b39dc3516221b5de4b6dcf73061f6e330963d3a333952641e203f5cdf835b491eb7b4d88590546e19ad7b825572259eca0b2eb694537b010eb049befa627f2e04b27252a2e3a470ecaf839c75823cc17fe178170bd09f620b6dce0abfe69ca0a1a72b8b481700dbef6cc54fe6f9bbb510608eb6e2b11fe52239f9ccbba8c4e8c173703789275ef3c7d46d1d74efdd56b8271b589f7946121db433f7c65f46c698a4088741bb94930eede7ca711c5ad1cec93142d644922d118492a2c87ef47515b178fff9f4c4b0dd17566c6c7c0a8b896dc37af0941921211e0479234d0cd880650b37458d1c182923da9757b6a3281375a8a1967a37ac8493f3edc69f9f6cd92733b4c650e16cfc84f11e7a370eb029311c00eb6f41390cbbfe4499cfb519ca0b3f3fc03fd12fcd933686f54e206cd211755ab51af642b3d638cc76c30c4852c57810de8a88af7971bd800056419daa30ff4ee9606987b72cf401a63c73d4e0266ee0059abf8d8040cf9b1cc0e03f20c6ab4f81b0656c0f70122c0a1e1817361d4f7871bb8b49803dd66afb9bbbef42a02b60e97b05f917c9f0cc44dd609e2c47f8cb40b25546f9b40a6cc85626714419e5d99c2a19f6aea34ff1aa68150dd843cf09d281a536b110174b72d46492a4cecb645639418b0b83a2181e1e3d3cac90608049c0315bdb22f427d5147cd500b9513c0b11946bbf7ffdeba33ad07c7cca8e12a70dad173e95a3235fc19290d5df255e56eb421faf99218f8d5933093a76ef50badcafe4aad4c8dbb46d6f41349fc644de5a67932e138e785a4dd6c41356c33b5d62c8d20997c021f5edbd62b2d7dd64866c8029abcfb433d9e86ed96bfe23854dc8f1ca9798a03263fc7908d993160e4ee5380dae6d928aae08f4f0385e1ee2144a1d72de4f3adcd65bd46839567b5044f1df941a79e74bc380719f94a5a62fc25636d0df6bb744810afeb61bcbfb7db683b86d305ea6ee173a7f3c60c836a455d46abaf5c4afda70e98dcf7c769b8b0e591f7c0002ce392a0ae74217f17ded37207d523019b9cefddb63d0cf48208ad358982c37e54b8e43a86f09d1ece53ece8b13f07fbf03d8952339dcfa593abc3eb8e04106c4d29b4387edd6987f5597bc8934fd4be0bed1017baa2dc5c6c3d9078de5812c058b068ce350ec2d47796e43efb6e633c58d1876e81cd3341045c883fc990ca83a68ae2b4739cce3e8f2bb4e7e9fe843a00211124a4b36808bf3d2763f0f61109e11074da88823ecc7ec4ac91ad7b702aa17e60eeeb14f1824ea79c143043a3762472da7e1085476d98d713a8add40eaa5aa01d3e391cb0ad27b9db1953faa08335fa95d2ae399545218bcd2d79ccf0f5d2c41932f95164594584ce83db0e838115c71c536f4a653b33ade923ad419a04051d90e4aba9a15ab553447422b5515010037641587161fc476726c0770b756dc632ca272ef713724876c7ad3763c2e7d9457ebe7fbf6099c04e05990a8455386561d5fb81861fcec46ed8db4704f74202001000aa0458a92597a590b936e887db45d498b249a3736018ebcf12271839aa2842cefeff60b4733779d839b207efda828cc5b55076647084c640bcf43b568e9312100000000000000000100386ab6ce82fe7a9ef925dd5d1837967771ba1cf00e39a5b291e6553a3d322e01c3ce6039ee91d184cc93325ccca36e09d1e6d3bb2bcc006cba274b26c8bc031d0000 diff --git a/zebra-test/src/vectors/orchard-zsa-workflow-block-3.txt b/zebra-test/src/vectors/orchard-zsa-workflow-block-3.txt new file mode 100644 index 00000000000..760d9f30419 --- /dev/null +++ b/zebra-test/src/vectors/orchard-zsa-workflow-block-3.txt @@ -0,0 +1 @@ +04000000328c8759c836c829ba9aaaa763cfa6f26ad6a3b9ebabdd8c85657163e528f440f7fc5f9d2ae75417af8906f6125232ee50554d5a2f938c7549d611b795acf0b36eeeecce566e140155d7d9869c14b07367a58cb45e592816370b58b04b9d14c40a104a4d0f0f0f200202020202020202020202020202020202020202020202020202020202020202fd4005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030400008085202f89010000000000000000000000000000000000000000000000000000000000000000ffffffff025300ffffffff0140be4025000000001976a91475dd6d7f4bef95aa1ff1a711e5bfd853b4c6aaf888ac000000000300000000000000000000000000000600008077777777d80a1977000000001c1d1c00000000000000000000000000010208179941cc27efd1424eb3178d809f951ee46d60459ec07be830d494d3ddb13138d97ec19c34475c0834f97cdaeabc410ea9da96a2cac7e6077dc69b42866512b4d2aeda73dfa5378463e2b47b50c277bbbb915b423ec24a8e9d20b7ad7c462ce5c551a752b68a35a69be03b4c292055da834daa37bed577a3fb5cec4b06e02bae7d0b195ec859f6b3499e0d35e765aa78297da8f49c2b3330f455e487c3062cfcabf1dc7b781df617c58319562d32e55a28672808650c6a1a7e7fe2fac1ccc97a9a59e44c2c816c56286d5e2656a520b7d834f0de839cb7004dcd7d9622e60690000d5c8da7724a404bb34d1531ee3444158cac680a85ad9fb9cd543f4dcbf458a5b01201addc876ba118d591c464bb70b05787887f678c06a1afd45421ec49eb33eee084f961ed57050fdd2edab16c5132a6267f79210ee2240111c1b8e556866da678d61a54ad2845e60d0da280b6f6e190d8a0fd2e7afcbb29b07d5ea65bbdd23fd6b6bc3fd618e751e4967fed651711c4affe68f0ec18a4fac8ce2c2f9dda72ae4776d4112e3a19af7e56560f95977d12d9733decd02c4edcb49e705bf2176d8b7be72c43fca605e9adab0a0c4032de71943d16fa8027cae086af29b4ec906e44210e2e96bc011f54dc13cd0eb69b4859486c0d5c105b3ff9d0e5fd60831eed98c823bcee4d4626956b394dc5686fc88fefd4c952f8edd127d4035d7caabd1dcabd4a15d8b2bc7a8e03343d54b9393d72925bdf185f33c731453f4a6ce9d181623e8d1b1de61a64b19b52bc0bd335d7a43ceeb7a4dc70a4fdd27457cc48adc864a170ac183127336123a7cc21f8143381ee07afe23be23f0081e134809c73487689c6248b526da8b4469f3657f4362de37201277665226258773d36a496cc167bcdb042dea3290c03c0d29858fce8f0efc5924caa85720659f4e3c299e54d3f65afc1375604128e9ea08c41cc29b27b39de885852b6a4caa2b06cb707f269ee74bf826e882a62a4a09526c147cf8d5b0dffc81cebb40935b0c44afd306e3ef46418d641286528ec6f7927e00884a78ce8ba041c22754c5d43aee834e4324071d8122dfa8121036ca977de96ce1409b2d5f6d3d52dade45dc9438a2e009b4ebc1070b076ced0a0346125440cfcb97e9a11dbf89b2ef17453265c491e7caf3c73c67c3ba0df0aaf55a3097c2c1969cfa87a5023b7fc1ff11cdf6ea575d74e00136231303282a03dc25b79876bb7192e21b2a436f8df3db93e89c44850e43ae841962bdce38414164e91b6ea84121e233b501531d7c88ea8250af3d92a6d6652266b736fa0aeb33b153d8087f665cf396638081d88de61f56fb720237c650bfe1628fe69df60328fecfd63b40051c331910c18b5b28a788bf377e6d9537e19e4b7875fac433b7e1f40359a1537fcd434480786cd73c546590822d9280bc6d9fdbb3fb553dd391369c88ab2005780b94bcc3f73819b625d9ee702de518b42dce1ef631648f4b3e186c6a7fb6687efe3fe575ba1ffb33d8a00d7d2a7aea45a9bc69eb4eadf572903d6378b8a09dc8dfaf9f93e8216e867dd2b133d176d55df57adc982d720a586d95fe9d827b6c65b2a99c7b82f4c3762f18f042fbc3d6f3dc696d3170f8b194689ee52986a46ca2b12b2301992bbb79abe9e5efd1a09a59100821b30d2884216f80fa5de5915cdee82b7a9380c9af24f89eefc02460a7af55bce079da6bd098d87638dcaf9175f6091ecab3b60568763b5afbb42bf90528f2fb75458eb805e5209c3f13aadf24a02ee054bfa1c718438e3b1c95ca9149a6ffce2f94826dd84efba0abe3dea656e842d21760305fb1ad4f24c624e9e17f97bc9589501e47c5b94022e6ba6bc495206b41f3014a348cdc2c620afdd2a37f03597f1f160a002789a75cede7331cd301eaec28c3433f962ef3c0eb45dd9ee891618f4b55bd85cac72141680f5aed64bc97f30e5891c797fa2ce38b6d9edbbf2bda94fd9b6f776fa2c4be93e02d9e100fb3784d89a823e16cf8660504b52d793ce38db405b2bba53c0739a0ab29e99774d8bde3e9a0c002c1eb15cdcff21fc3f642a695fa5b027213b2f0ded88566a00bcd0a932f8313ecf74428935ea92d7f7b7ea346fa2f3232c4eb7ee8ca26affcd1a39f5219950189ddde8a824038957852969d42e2b368e332b8d6bc87ade80e467ec2ff4577db9858b3d43c7e45b6dcd81098579008ce79e58f1efc816198bf84a56527e8baa2f64f8229331de79f19c3e775eeaa56df3916843c15c3153d421e29b94262e35e1000c73570011e4a4d7901e5693c81b50bb22032bd62d389e2467da4ea820318038de1387e32e8eac8ebaea73956620a51ff071e12b363ce21c9fbe4649a07c09ae8e3eb752fdc0753e2b332739270e0fcf73fe07192e5f7b7d311cf9f8c782e6b52cdab23d4f127c4a1cd24c4c3dacd108a5073b000000000189417040ec111f12210b709548f9e1cc72c8573b22b4fe8ebc2b367fba44619b0700000000000000fde01c8058e7742a96de3335e593337703e088bc001e07057194958b355776108b2a03b6aee92575ac396842b4885628906dc34aa3231642ace87bbcd339c100a33aa835b0d7c9ddd32f743a3ed75aa780b46b3a6e6fe904448918842144774846bc858eae4d5ebe546e829728c2d5e9054b5364c2162f56db8c417e2fc3c68e0b218b7a87ac2d72f4764016d279338adae53d94a46b323474d65b5eed8dff240facbed9e30f5a8489f4bb7e9a9967af53fb52050ff4266850cb5695cd4aed169fcd37e3517b98150a05b7e890a18bb7d8464785b11e8ba977119491179fe35d408f87922b59c979adb9333cc16a2ba0802c51b3654b61101a29ca875d3ed6391aa0080c22fb3a8422c0cc49265feb5cf11a6d9af2ea78aa71dd64c5a49d573e6bfe1a1889a1c33568ee8e079d0939d6f82345f429588991f538b0cf5ffd141237d63a1f46a21018d0ee67b806dd6d84c787453708e54d77ef56335e406930917cd59e2fcd7894c0874e9f33d810b4d2a3f7a9a1777842bc83895c7272315dba98b60f43e00ff33eb461c481023a7d5952c8c3126a3d8e7717e1bc25e98f3c0148de9c0fb393269fbdd6cb6525cbfe3986b514f6dc8a84b0db284a174627ed685359ae81f47aacbec39396a6b22371bdce3baedaa26712a3a5fc10e6b736e855010ab111a09671488927f7c37dfdd21b117248175f7ca11191c01f288c554ff8a73da675f8a0eb442c7bcc9db9586e74f8aafe4b53dffdcebb91861d7d5880f7309937ba80477d7b0f024fa698ef32526b0f77a7b57b62c9f90ea42cda9306174953bf7d3d6a7982fd2a15188e3eb173ceeb794d161af8e3ada8cf8d110ba90bb903a7b9468f2060d272c74e4041ccff905e5063e219439da71a414bef4ed7ae3659161db17deb30afe85c81a9d240190ffec960125d760ce460e0bedb86787d3dd780db4665c5d868d7cfa19cb93510532336d789a3646cb0f0135ebca4ccdf8e71a5ff81ebef8d688b98350713059571dcd61f48ea85a51bf31b63810c514ca9f086717776d3562c2de47ba8df330d863330e20d83c31abb2cc7b9d2057984814615f931da172f6fd213832842b1d9f1c91315ebf0929f0b5aa8c1cd12f8efccd921e7c38347d1f3e1d9a7426ff2b34e888b76099eda31a3eb809d74231d5817580df2f785a2bd5ec755c201a77a64616508c1e5dc0496b2c1133651a9b9c5de7d8adead01772ffc3d2c287fcfdc674309f1938f04a88f071e2ce464395aa0164913924dab231b878601efcbce51cd71dc472b50efd13836ad50faca2c5036cc6a14df4c0218a3b0539db3519fdb3cd7c0ddf8c1cd7c194a01651cb240d81fe0f22b57d66f1c3908872f70f482763dfaf570dd4a245cbcabc24a0f58ed2c1a14c594d40cf0703576bc851b93c301fda7514fe6e08ec9a5d47538c00f526987a4de808b1814c2795099eb81e36565b028f8242922e855726e4021eae5ae4e766bfea6506b3386afebb9ffde59fc73eba6f6fc38e97c3c118b01ce743d99810df8d18e8ee225afdbfa00e0d1e4dd10237125e79d5e6301f055a361c9b63f40c214e730c33b28c2934232b4996812b4fcbb999be95c4ff2b76e59b1c82cb8020f38ae15f1befc6ff684529bd1222f8e767a0c03bc993bd0defdd5637e206014d3404628082b44864e8d1a478c42dec28b8a23c23efa357833df6a8becac6be5eb1be00dd21c4f528fa5ba8b7c753e2df288ffaeb191d42353ddecb142fef90c0b43ea3344910ff498e675139a8e5e1f38949d6a77ef34290e7a6204b12110d11d6b1a2f0b5a3182e5db5aa4ea8cefcdb5bc9f55cb2dc1f38d01fc63f1e601fbf008a995f68a3aee8262f71669d80a7bfaecf4002085468513289ce16012f1c67f1dc9801abd004b2875986ea03c8dbd9ea014acd3e6740de8e2987171e759f6a82eea2dbf1f8ad139a3220ecafa8e66c8a8628d9d73906614bb361a91e28af2ec5d782eae93a0dbca8286f24179a29d91b45f8994dfa640c0c3b773d8032081f132741081b52f3bddc740c1af6234a1ce28bd71f69a0448093ea3f326a5e1baa0dd90b1f62c1ffdcd55e69cae0266e70d2fb072a455e4f17a813c524f1dbd4020a961976dee5de199abd9d9dd57ba296df218d7ecd977e493929f906505ed4b5780dc035c378f7a2e15dcabeac875a56d7a27313aa11e46c0dcea60f89fd3ce9dbdadb6c77c89c917cbef1bc4c5fe5adc0f2cd15a8cece3ebd056c209ad17c9349b9797867a49a89dd6ee20c930d81320570214b8201c5c74068833b483338a6df66107aae6df0ec39327ca20e2ff295825361756eff82724022afdab05b3360640c31d9640598e4dc8d99654bd5ffe6cfe0bcf63e85fff3ef54a716699ec98bbb2d537fd2c5aad0e16281cdea896b1b1bd167ebcef81713addb5fff034044bad920113c36cac545157b5645f2d0b987a7a5f99187a8d27154297178ddfe6166701ac1fcffd819960982eeee766c9f4be5f7063d52711c1157f0d37f462d47b4c02a91aa4d2d50d44c7a8170316c84f7dc32b0127b5262bf7e58068db6952716f3eba13ea09d663e9ad917ac816e349c952d414ab7be5a5b7d7fde519b0531c7aa8e229d58158c7250016b5f20839636e50ab528c6bbee6215f921907cf3f779d804405fca883f2390041714eb2f2270245c5d4d871f39a94e33eaf45757dd49ee3f403730eed9a4a07722fa58b7d1acbdfe429c4329f8836ee7399cb037900399f1a30cf7bc69c2836092585950b8dbbc35a9b2d3117c2fd249e4f6e4c2244418d602cf4976d08376dac912dfe320dac4bff8a9b9116ba49445a9788db03d24b6d200e5fa61069b59e2784885590f52a4fb2a38b1eb9290188798e62ed1c1f94f0240622035d1d812886cb3c2f68f553910771226cc8c8597adc2038fcf58df64eff3e5b5cd8e506c8a28f63645ddb363d3a6e8e999bd7be3bb9653221fb48dc96480ea6db9edf2db3ff03e2530c26a0b3e7e11531b4128d0391fdc4cc1fab14cc8c2dd3de15e254414ec3b2960fc21903ae6ad41af937b386a9a1e10406af81cc0f1c451883008f2341453121c94a7b0aa606862c9f20b8dba452f634ed95a8636622fbe7ad83959d9afee675b43dd118aecaaa9ac56c694fabdf138b3d6fc501052ce50e659efc1693853dddb0c3643b89c74ce882539c02b2881e97020b071a6328078237afe1c85abf5a3a7af8a64303e917001e037a576cd426631d30cb364e0693e087b5b640b45fad1f80a7fc937140e206715499242abfbdc12946604b6b0d37002682035b423965f39beee02d945648ec2e9537319f8c0890ce0c8c2c490cce80ad720763d606751b0906cd2af0a6637f518c6574b79403c4b8291fda5d174bf788076e0f2a21c1e433d7745e9a3292232ae22e6d9855f5a6a08dfd52de3bd67faf6e0c91c41dc9cd34b673ef60d20f5d60591275b37d06718134ffe98a1d1f91f2129ceb9b481e65e79c8a7f76f1b1795211d44e6f08eb9bfd479c85083abfbea53f2c3bc7e1ea28f8883b70c2b3fd3a97ca0aea6991f6735e6508502212e1a869b6b3bf0b3c9b178003914a0e28882c14afe13684c67db80d7e2cf3030aeb6b9c4bb7b527ca02321347a2f8830d3947cd1ddafab7335b9e81c5018783089b15600f95b79b843115d18b6741e9a8920ba2b1636500666cafa0e9231a76009308a85c6ff6e61923baa9ce7ad9217577ce1cbe66d8a99f1faedec5c48fa20a868e192cb7d6bbe4942782d0f46e899a40112d45312f923171d42c9890ebe5053d5004d18ddf153ac75b8e088c05bdb86d5902e188c93a005d1d5288f651f608fb2ae3a6bcaf515d6f962fd675e32833fbfe9efdb2301a8a7182200fdfec8306b69ebb50fe21fd0206a601fc69360205c397cf0394b52cf1ef420e4f2c4b573da0c97863e04a7b2948fbfd8bac5ab68e12b1959998ebdd0cdefef5a6d84a30300c550a64f0549c0c0aab76b02de34f024b8c351233117cc2787a3c53a7cca51f6f8ef2076896c602d29f14e36ca4e60ae9dd00a46acd2bf38d3c11fcb950380d517dbfaa3ac7f31a4dcf4c2d1b902c1bd237aeb7891d7bb9ff08e0e2eba9521201731c8f9617c3492eefa3fadcb30877d485601bd1fe11a2cab2a4cd11c33d2627b1fd02abf30a564fd82afee0f9155daa6583bc51228fad6f05e2ac0356650d77effe8321725b4dfb095ab79975a2eec00ddd47df1d825ab7d0d3fb414da1093772f311b4b783f1513291bccd3634ff67900e6861222f8066e9520344050112603f6584bd077d01f68c61f7d3956013ba6bc359fe2cbf12b8cc80b30a084b0554b66f84d9815534d4e06ced52c7abb7e83f78cf7a96a0841fc31e3614d8543adcc7476b057ec57e868e0e2607d4066ef4887907e8d4c024568c6e1318eee91b14b808a96932d83dfd07e69f559f3cddfff556201ee7616238906f6f5bc3f73976bc41458123ff09740626014271b002ce249dfd0ecc1ffdb5897fe086f85f07f40dc3e0cfc12af882e537092149e249e0417a705a1d9613d6f711c39b6a95386444f25fbff8cc73e27040e3050b4ebe6ce5fa788328979efa4ba3937eecea2bc70402d1b03c0bb305790c70a6da36ac0ebef7b242bbe623d040f84e90728115130494983ef208760d537c20d3f1f29e172639b3ac91fb218ecd13a2c3307516212692c9c4d9f48e138d6fc762917d5926c4f188001c963d7fab414b0042ae30a0aac21991efb96b94db00be63072433376ff194ee9858a1eb9d5365d94b6d1316e54cf35ead5ceb8d7753844a6bccf44e0c6397833ef9aef090e3ba86dd6935b5d866081de2da9ff9217b9f14a17f9a88f4467f7d6eb19dc784d0f3add23e31d61d454932661d709b7a9b744b0296e7dbe67aac9d4f749b836a18f3aef33b1b2d7e238bb4ed0b2b0ff458fb6bcab3257548336385d8e3c9406079c29b18ef066615ae70c6a41c0914c78eefa870d61f72c5f0c53baf242251c751ce0e0b6f3b20b536638d470eb20a995071d3ad53578440d0e861d004b717ba7af4f5066a00eefcb51fc061669e26259409a6fdb6cbe8016232b8f053d9e3aa7b23c5a6981db514e084dd4114ed220f7414734002c97c5c12cd3686d4e1f26a8626265316301f7d855fd9315d5b29ee08884e313d165a1667ed0329119cbd1cf08a64edd638b409439a86f9cfeca82e0cebf2ad5aa2b0d7d3b078f44455f5ce982b51ad8f0b0a3691790094ecc268327456e9b28596649d507127b6d098e987e39f7c0ffe259f4fddbc72923e9b5d9761e960a3b55449d7c8de52a31ea641ecc16729db1d11e12392ac770b667e40c072ed36f47ccc7b18f1f6834c73dd980d0ac0b87e9c1809e42b766f2741c78854a7571b2806fa3f7facd816c75d9be41cc0e56d3f9a183ba1a2953a2aa4cc122e6a7fcd6df5dcbdd12d2d1ccc052a251f59477742c9143abd8907dd878bade670b175c74625ce585eedc068627601dac8d1ad4446b01a19d01bc827c638ab448c16a36ddb6961e4e330b54f35435258b9f10dcd39bb278a3e22727cde5fa688b674e38ab934ee8d592f44297ce6c43d10159a781ee30f61f26dafb42ad4b7efd74fb3bf10f50461845f9695f6a5345924392a52ac4f1f5f5b7de204afff794cab99afa32e49a2791ab9ba36bd899478709e642c52192f40772de7753efdf8eeee89d183749cea7dfe764fbf0ee965a043a445b5d55b0d232b1ba556bf541ce5ea79a02d209b393b1a6e564055e8a2bbec3e6cea7a0e3b2cd2f0a52568fcb39c0bcc28b4262f9a75901e373aaef42cc67839a029f10c12eaf8789bec9b0d6a7aaf59e3db32ba2b4fb210df585ecc987a208425e7eb4f08fb40181be25f0f08b64b557f04bb803ef2f1fe71dfb8397b3af94228ea0f0900ba781239c60cfd6e636233b21fd5befd2d07c03aab39f329428d310efb035c2f1c354afdb336734ec96cb13df5b0d6960b789aa572fddbde28c5f7026ee79f2e5c3ac2ac5861b7b1b0b29007c6b397a9d7c6ec3117fa9b0b32f1369fb59314213c30032dc2cb89222c35c19ce930bc0d093f77b6e6c6a18fe0e47344d304c2306e5109ad861c4cf7897784c5b62a53a465af034a10dadec667a4cbf34c37401ce0fa4d163ffa17920a2ed21e6b58a66c73bfb59d0562f7786ddddb831863b325783f32850a76be9df6583c2a1dd4d1594df57aacd33b5fbf02438de3742377354aafb3e7988392949a9a31651f2779cce74dfb618cf70a391a0e207918f989100e8adc94dd09e0c36924b9abf8041abd66afffeb3895a798367601257d90383072647c6548aff2a8a84cb372f095861404331c02ef5f66ce21bf60d0196f3f3e6080f71d28d9e76a91d373990dc792f81392703e7412c7fec20bf308c4c67d1d560d9f9a186e839e68c0673a9fb21bdeb66ad46523679d72134565cf26ee8231691724b3897351f945137b278a022fac1e67f1fb58f94615a5de51bbddd11a3b4c410aeb977308b4df9e6c6a7ca60d21d0304d499d68de1f19e7f0ef46295e3c9c2999ab214fbe1cac7910d2d006819e9bc345c08ad812094eef6770850b160b1cf9b5a89186fefb431dd693a4e76b54f9a00a3abef0f2aaf074a6a56fd94e12904d9a56ea763896001414216d7ad295547002b913197cd588df3d80baa5ab1ba8e66966b5728cca01dcdfe46a1a004071d7597065b6053fb4c092c9d34734032826e5e33fb1c1aed3acc86a6d40bb8aef7eb865913878efca765cf8ffea0f0eb1a9ebc446fcb9af630ab93911814297a02d824a69d179c8238961112779a616b3910de6206be798c0d6a805175bfee0ab2d81cfa36d360382c5cf5b8a22fd0b92df8adb0fcc8d287c9629831f11d05586781ec446039ec11f247b2911bdc523244f6a5acf9fc1d94a8c6300335c08e147493a67ed3eae9b5dcb308ca4b1013e70363c83d59d015f6b8e56157250342edbe336b94732a476ed76d074b72fa6053c5186cb23dabdd5eb0948045b59c3886d3a9bb8c895d610c2f0014d7ad77f07bccdc21b9dbf11ba601ab24d8a49e2d683864bed45aecfcc535745214e5efe12cfbfd7f6fa4e07d6a99da0fae35c6a43b1bfe0b3b029c158b94c44ba7cabd30ceafcdea59e16384400123550da1ce8a557482d00df3dd10be50bc1d1469a1b2b7e0a92868f5e623d201ad9a3187390cf7126828651e00573419dc71dc8e1a20020e691ac45f94679e4dab4708b6d4789c76a530073a900f850b1c13cb293942738d1c69cb52634c6613969b96a73840b96ba54c6acbaa63995252ba0ae34330bb9870826f5016214313002c2bae9f289e1850cf5faa7a866f8c19f50938e6d248e75581bdd0b5793519413f4c927a1b504192f58a16a419f0ef6b7a756e1d7182e376fe23fc050200508db98c24dc8dfd7e21db222d8fe345198ccf2a18d613ddd78596730a6d62a15e1d89a63e4d760b0210e38a7376c10eab659393e4147312e1eb968ae4b13ffb084a4e9ab407a2a080c3572ce04ba42dcdb0eed473e6f317c6f306dce541e8bd4dc413b56c3ff43260641d07f6cec38b406a205966138382200881aeb74b86f7e868347bc8d222b62f186ae9908e645cf3bbaa163ad2d216db04f67a803d5ef7bdd9f1af3423b09b6e1fa798be91da85706ccc3541ddd022f2f8c37b66569d9d0f2de39831056d9479b99c06ed735090748709d598d720ba3b738018120a7be3f119c9b0d79db949f421d968aba7462736db56ead7843191afc2dc8fc5136a373f9279f803cb7508ade06c0fd69a7a09b0b2c85a7833a1390b5037685dd5461b313224074ef893f6c08d7e4d5c808eb98c32eb103fb1b0cc425f44116168d1be1e72cf59b5a0878440c8316410821c2cff1f63286be803b2c48816326827190456321fd3ce680d3e50bf9ceaf6b1d7ef1dcf75705b6bd2ccef26422c794711f31184e29b161213c653325591530c3ee6aa96cff7806b73093d6f7c5e21860aa5cbdb3c720b6c07c64d2b7e67c46319de33993ad8d5d6e1c194ec0b90405031e6b0f78d309d398203f954855af91e4dc0747ad0bda63800daa5e316156537f47d10db6d940b034cc1de7a004aa7b0ef853f9807e28d58d048b46d2413ba0931fc1d95a09a9fa989ecd0698ab3efa18169db5a8b92d35033607a01af715e16fb13a64c12398f04215e8ca53d71b131fc6896acd89a7a96b2dd30b28dc5c5b8f8eef31cab4c6a2d6ff9971dbb1684fd3d918ccc160383a4206d7794e007aa6e270785e9453f48d2bca001c4acc8d3b7ada3cd405dabfe51f200047fc60f559f5dae8c1e5c6c223257537e37baf9b33ca48109b646d1238520eb5e907cd1d797523ce509d26e3eb8b9bf22207cd2c8ca831784e7d2d012b2324ff4af79734a120f2dac701dd51ef39e3437827bf829f66032a55b729c72ae330c0ba05cceba6fe40d25d1a2b435ffb2935b272088b441cbbab33d7334152ff263c865396e786b7d898c5103c4c5b6ae11355a21a7112fd19e8c8dd29f6c2ca117e48e4524aefe115e25910c6be924a03e75966f4cf11a52045fbb72eac99062e0419500b28aefc640d7e8c6aee30237f6e91cf268f85edcee3fd30a215f7a029cef884bcf5a51e8f9d56168a0a94985967217316f99c3e98edddca4e8c8517075af8a76e4dc17a71941c1d5217c25dd6bed73896a5ebfb3a47f517e2a77055328aa0108078b5312de421e7b8f7d3254224005fccf651f699bd442b9a953d9602f57e030aab33287f4c773b8f50a1b327a83827b3d61fb881745f22451320843c47fa2e359458408b4ed36aa945fb136edf7274b598e9998e85981a5cafbb6723477e774b5093e17cdb9f60b748f3cbd5babcebb4957c4eddbf9c9d0da7e6a41c211cdf0e550ae4f688240d65cbeebd3f63e096d7438091d63a0fbab5fb99fb006b18b4845a6515a0019be6beab1190e36069bc2f029782922445a2d387833b052f2e087fe190073eca8e0cd89790c26587de79288bcb99654ed913b085541a2741bd62fb23c174e923b8bcd5df81bae8eefa6981af6d904f067763153e467d8946919fa53aec059917b7bc0c44f940f1d430f2d5184008a40f6931940641421f81e1a21632b84e290d7988ebf0d21c0b764b2ae5b549eac0e8ca9e067e593306c8267aeaa639423025c2a068adf55cb9771aeaa152576758988deff73b0f20220b5768da1865345c70ea5fbbefb05e89c2563d2bd60dcf29a8e0d211f2491c1f1682efeb83ce0fdf3ff66284b3f0be220920b8d3c2c074cc2c90da1114387613adfcad952a8df5526845f3896d2ba2e6c2f40bdf7d953ea27e33e1602eb37a0fe7df337147bb42bd399603a7572101d306968633878c448100b1c7fcad0e74b890f6e0241b058b28b613eca454e24302bf19c4b24445fef6763f70737561671040eb1807a06cdf574fea5cab324a365b683e344e789d11f3dc8cc7d8cf3bff379192c25a9f2325d32d0da28a2e5fb6bd8f55b9a5332b0839df8756ed077317a4eb1be9ea217307c705a86a18e0aeef02ce66c322975b667dcdd3d218e6c8641416296c1c67b3d927cb0ca8c819a241211ec0b99f058e06116fae7d0481838584cd454f3bf815aad272087fa10057bd50da301ca2cc29ddbff99b7f9e255a4a81fe333ac60e7893dfeb9c2dacbeaef3ebcfc99bbfa0689d966bd5fd58ef07d184c818ffff47813ff294426f5228ae746a2369ccb30dc699c2545f0d5639633e0b4701aecaec57f1ad2deb0a9eeb9d9ccbd7118b6d1f19711e1cf2d345f8056d21a93f246ac5b1958ac321bb198f13b32ae3e4e0746c5d6465fd1a44a004d7268a9141d3b735a3a2b19c8435400dcbc0fa46cd442fce7b53eb33557f9f0b753c86875d1ea0f0512ee35e16d5eeddff9d2ed70c5831544715c41e4b08a469e22b3460f4330eb496e607dc616f4e031ec92f3e262866b2e4445d4268cbdcaf86b5b40a94704f6d75cb42786f8263cfd9511fb5c76c88fa0a56700847af19e9afbe2d440223d3b4b2b9e28b5c3da81c518a0b0ba395fb4fe17f8a5331bcbc09f628802d02ce837b9c9916e0b68bf4f5ae4da2159822e9e1006b9b523844ba58a47ba659f4569773e62a10ee3b091fd37a904bcf22908028620d9dd4e3720b693d2f3c6ded2ca1dd0d049d8d1529eccce31fd6a81fb9c664d278881333ec9208c5bab6603b47c1ac76beb693e2f38420c963937ca144a2c8df46eab52ba142cd719d843c3cb7b356fe19876acd283cf15adbbb465871fa91e55d6109927d045227d8bc21cf187c84569ecd0ab0d1794f56681965fb183763e296e6a16779b470827299056a4dc366419d7f09d6202bacd90772e0f6b747e35f7051d78bc0d3cfc1a215d2c546b39bcb3eca5afb4897dabffc786fa1dcb6a5be62ef6986cef752b24021010095c43a10bab1a6a7699239244905e9a47cfd4f07e5be1dfba7d303387ef0df3e133cc65b3bec5d4d698df47b8843f97c9fa43c96db5192fe0f9728f9abc9831701006ec32520323f319b887b8bd80186235dbed10e7bae006383f1fc0cbd310e463189c096d57ecbff55e386b0256262e5263fba24795ea52e526dbb65955661b613000000000000000001003a3bdb3ab1d76025840c5243e3d8f7d0312568fa3de328917aa9d7e2007443261ae331f41bae85356357ac6a14bbe68c4c5a607bf56d20c7387bd9f90ae9a30700000600008077777777d80a1977000000001c1d1c000000000000000000000000000102acab2d4e10c854609271c50c2c117a099bacd3907a3a58605d5d3e0e8d6e5ea71c0ff65fcedfeaa2ba963479bbaf9b3f746a038c7053f1fe3aab09c47f5afb3df77bc7b1b11f86213729005f2f6f93fc566b449e92024191466dd87ec769e205a5a428342bdbfafbd738eb3ca7ac65be69289a620f123a62e4459c1dbe6fd4297343bf12c53539db2264e828fff0d59e20310b28b29414a47c2fb1f9cc7d8435e333246e6d976712978c71138a9ecdca241b683bcbea9a472880b5d43a830af267eb5656a7687c947710e46a15423f531fd78c52995be0bd29542691590aa500c9a13aeccc00a7baa834d40c66a87178df386f98c880fa335b3a76f97814b4bc632a6e25b2e8278f720ab48af736f8c0c1920ecf942d401156ac9c325569b458bd1d4e63b75b32d6a2395995c3748b32853c1b1a17d16a5ce81acde4f66519d91bf405dac4846b30adb02fa7c049e9a62df8918bcfb68648af95ec147d0a621c403bf99c6a5e5f9cc31a95509bcf494f390e6e13fdd5e22d9cccd857bc18aa79bd3a87ba507b2206539166a47a8ad3d95f04e3abdc8da6253924aeb2d1e863fb93a71aa852413b6fd8f467a51f949d31f3c86d7dd0e68b76a91bd7f3c3088bd3250e5c0694d2dfe26f1fc702ed4f0b448643f1af6e4dbda88710f46dfcc4b9d47fea0305d6a2cbb7598f583d845489fa9e521ad5bc772b300c864da96df21ccdbbbfe954be73f1977c1e83109527c2f7a2a20198921c9cabae12152139c5957d82351a3e80c13d76c920fbcbfc45ace0c6b26649d886cf856e3a007aa56b99da61d9b031fbfb3870b32ba66d5872afdfdd3ecda96c7ef12cf516508f87ec53da419e6b4fd0688c78ab6a9e1a52bc389fca6c45b9f50044ef78192c96a477c1ae001017c836fe2cd6115606bd53a1cc61f0cbe8d755de6485d2ad4dca67ac0e626d4d8d820442b9b05bbf88a16fadeb8bf621d3186e9e891f36311b307681f6c76e148ee30eac97e6e5ccb50c3ffeeb3c5d536fa697a3892cb83f2689cdeb68656f532fade0329fc9ab165a782a4cff262e1cd3523b9f09f9dc4bf56f209e909361e5799072c19dc05d61a402238da4c68d069508860d1f52af17b0a4aeae12a92c7c5bb222b6fca4a8650905c163bcdb59ad763bfbf8ea8c49cdf2154655bc297761f552a69db930e7a9dc68851e33950fbfb6c8d1740b23bdfdd434dce055a1afa8f1f12b4ad56d300479735906e8f39fad353651bb4b21d779748058e94e8e25771e300517b53cab94847aad8c4961f78e9b3bde28eed420cbd0c77395c15a8321d482087bd3b390f5c10e168dd083a076f9bed95aa9a9410392cf904d9da66d27c3865a1b1934057b6c7d5f6be7a47694a90b1646809084fecce659bd57ed10087ad3671a4a32932871eef82021bcf3051b310ae195f322a0909c48356b43b9109f99654fcc58e297c304dbc02a525de21eea717a93f7fdb0013950354ff70e2c0aead2147bf193eb468cf2efa3766f3a860d0774aaa86395dab02a5f53e163b98cff0aba0cfed1928f8a9a79be9e387518cb75a5d66fe37f540cc58df5bbcc86b31edf52c39426fbe4c52c360f643acac10e2743f35620bde3369a08371dc1c2644c659e2bdf5fa9846647554392bf32166d69408d080350130a7eb985027a3fe777fda9c894aa049ff9b670d7436a4f54f900c55946ff7f036d87511432240fa50131d99423f8fcb35ea474b52ae17a0d4a77aeb7764d8e0fa6167632faab10351eb3bc1cd6d617b6857855c2f7fa063edc34bc304e0a17d4411a8af39e61097b19245ad9249dd1762552974cc59e609e821a86d008655f75fc38d23fbd61efcb94ba37d979d9fdd992624384751abbe6124a81d980a419061abfb9426ffe6178aaeae18e2595f42ce3a02817a515980bba234980acfdddd3a9517628fc1e0e707573df5f29d48d6789a7daf36c20a68a60bf29ac3fcc2596a692305377b151a3136742d679cd3a9c6949db3cc787cf05db00a4253837bb0232b73670f508c9494a608adc1e65a67cb60c80444b7f17d5ae8a94d14c8ee6dd62ead17dfbc300347e6767fbdd4007cea3256f193b39df77d756125516cbc718d64cdd655d8b75566a545bc7cab21c04c362e39828fd39d9eb6638be076cb4e1000957423964eed733339a944bede03b33fc0991557542161fa326050b8b12d58a3d996ea02c9540b42b038f0f0673de36eb41a10e6d3fd2a789e0d660747d7261d46fe25adb071e2cac4324deb6e49db15eaa7f5458a578bb40b95f72cef5dbc20084c03d849651b84939094c1f6dabcdb2736ee5e22913e3c0560520f30dcfd1fb69e119ce664fb5126d13e1459499a40873000715ac593376347c124d371ca604375201ca58480995b39eb507192e5f7b7d311cf9f8c782e6b52cdab23d4f127c4a1cd24c4c3dacd108a5073b000000000189417040ec111f12210b709548f9e1cc72c8573b22b4fe8ebc2b367fba44619b0200000000000000fde01cf01e733534b2547c5da63419234833bee33c895c80fb7f91faf4eb65a3343399dd2f754c0c481ef9371bfda08ddd39c10309f01eacd1f476a62fabc23b52d59c2f25f1a15db7017dae141fc0ad9567167b6bb3ab515a15818c54c83fe9ddd1197a8dc868acb6bb50a8512d4786a1d98065f9d2134b39a33adae58062b351c20954cd855fc785db23240c0d1caa57e10d93d95b15682cf812c974de326a3ba914b1ea47273b76091095fa84d16cf8358f2d42081b07a4f8271cea907db167ab2cd8c750eb88054ea7aba8892d6369df064085e916dcb6d54e40a5c9d7a566b283bfb0dcee58daf282542f25d22835167ab75232355c344bddf58d76eb65a9900a6128afba7f0835ab345616973cd538148789ece31fd14b4bc932ccb222e5489dda19e68c36cca6c493b28bf74b3cd32c79096ee56e49d16748eecc698c5bde9de29240635b20f69a521678c20734f45b7c421ed53185d6deda3d36e51949aa89cc64c7de2a5812fb31732114e0dca247e6009f32c5da52dbdb941cb6310e6a85eb56dafa6a3569c7263b68b7e9814c212cb24a056a96a717cc06996f363b04b1baed98a8532bbe1555e2cc1542bdf2897f711f979321e07c5065952be5d4c9ac025f7656091d6d3b3ce05ba889c27e65eed466e6ea8c235965146b0fe32f84190df15d8f7d4fa1a6b3068a5686702fd22432ca2d8ba2c2353213ff47877fb8bc2deefd1f88e54979c68a199a6b018aed5551c194f07832eb36ac78f1fad7dc20fd92fe4ea0581f08af7932cd6cf607f6388476e37460f54ebc7651e7ac7c4511fb2f090354c487487cf66e8272a5e1eab545ce0521ec21b81a7c613cd5c323b835afd0f89405699a98b3a301efb06f00e55d7350d762e4d9b0a395f903a3f11a059806b377a6ec23edf52c90cb559163f7fc829f4f25b4a3a20ed09e9ed2cd02df19f9e13b15b1d485019ce53b17898cbcba27c5b35af24bf69b73eac497c007cc7277c156000468ed0f72cb633a32c4b0557c0b63456d116fcfb21e575dc7b18743a335e65e4a63255880dc309e0975f5a5fb082b8fb75c13f9ced1e09a7719ff3384d2326e825717d2503d3c5767dbc03ffbf864e33f52f84178e86ce482112186ceae89446c8d54dbb9995c482e3529b87820410fbee1f3ebd843ed517d2003d3395fff426a8913e60ea93e4ce2b669e22b45a5977d3a214d52040b1906bee392e3cb6fd2f0e76af1494dadd0e9edd3d1213b3929858369edd3dfbe38f18c96038b4e9b5fe5984d47524bdf6d6963609f114672062d559087da13bce187a1894330ef060ae677a209d5278370d0dda8613e055ecb5824688ad60c1c6f3121be7486f234c9779bc6e72b4095ecc080a2ac042c00962cd9bd573209abd3c80265ca366647c364363312aadb82d62a69b66553904bd3c4c2b731aee10101d4abc799c9c568fac4f2b2dde5e18f72ad038e77c1cbda478811097883d26f75e4872f6e9abccf5036e0a80f426d8e44d0c7085cac3f2c0167d144d22d5e4a5a4926f7cf8f61390bc6ea71483b1e1e233b5c29190c78e3d64eb4d9169faa23bacf10bb6b7b8f9d2ff28b15b3b51d45cba1ef62e1c218176956388fcc1fa7ace34e8b2cae88fc6849ac2489fe05f934106f4ce5f6d49597304a02d729b96fcb7fc93d643ad685eead45820c7c9cec3cb12b71105cabd2e1cbb89fe0e469898c6dcd107dbdbbf017f6285c8879c3c52494a71f49d6231b8664fa33823e9d2d1854c48bf041c012730ac7af06e402ef034c44aea171713aaa7488fa20f9bf9a7926c7825431dfb78867585aa8259379876fcd4bbc260d9036acc0bb3236e073e576070ee8c2b970abbb217065c3b99a11a20432e68feb01545ad84886b763fdce091425b03b8343ac79c6d9b0dc3c46143ca08a12f5f347a4235f6c6cc5eaa15ca0c11dea21e33da84aba1455ef9680169453f54e06e3959623e6758a899e06ae6d0f34dbce4fbfcc0df699dcaf715403d2de4d534d0d65cff96d6b273599a23ee28bac49968c582ad298bd976b92c3fdc25c6b8cb55a6e11316f2f7a75b8af2de49735234fd53271174dd97e8085f20432deb86de8a2dde1bec0476371776c33537d0a793468cbd26dd74587fd65f6d147042cb6a87468208e62204de93dc1ffe460bda9fb188f7d20561147ea2c82d4c6a375f25135b01f50345ba603afcfae114f3e8dc0ae40491a517766e0c1e0d09c3a53ca0a70fde8170abfc3cc41d5b8e86e37b20e6b8566a7da37c85d9421500d664e57ff4ea70e63655e9dec9776581f1f18117474758488ce2c15b11cc9dddb8ede676c14473797a945ff91c88fece3b79f802979c2a51066745052ad61f47fc036c673bf4eb662a1c1bfcd9fbcad1b2e992f382ba75cc0b66bed86699b44250561e2cb8ecb3e3adb9b6b38987ea0782b08b7db10b493e9aa476ec5530f474f48aed45a96c3973e69f243e4cf8d7562dc3ba7478c80c284052cde0430326ade78233df565c1763ebdcca2029c5daca7eb07a1cfffa0facba0de03679e4e2f4901a2e6a4d4a44c5b8ae4b23e118d546c95278e6183b1ea3a584e7195c37ad0b233588b7be5f4628796af90266faab6597837ec4163825e603cb91183a5e6af5e65bdb9dcc6dbdde227626f9bcdaaa0608e2b476f55741ace4daaa88bb5ca512f9834d11b9d067f274db268f790e8f2462729298b32fdd047afaff839418bd8d324474bda31ecd933e8ab6ed8e7a043137f1a7f1e47d8d6de184eeeb0d488d7784b35dde159dcbf12cae8ae47f49aafa28d1b824831117e0e4974f1f9a869e8965c5524a97b72873c9ca5855e8161ff34e33200916831e5cbf7842c6ec4820d562560ef39f0c86954137227d5ccd2e12e3903a3e5ba41007ae1b6e63380e322604562c675eaccc451be6ea10d91f424813e1a53e0898e57668a3587a979b15abe23a8edab51e9291abbb5fcc51d8b64ca280182ef86d03847a03df062dc9a3d6e16af5fb0b5ecaf01a15ddd02273db492a130f091d2ce6c43524b86ad09ca4be1defc563a4ba3e0d81d814492300fcc1e6121757a3d2e66305697a88297caed3d2c96d435ffe4abb10b8c000296f7657f260ba88e270b201355ad5f5738c31df44d6f9df64088f6fa9f8ef6e8e32c44d13e1e9706cb4d9f3e09cbf67d002c181c875757bf28c038f49e16e6f1b18421c5ea1e7bdcaf7f060e32ff66f06926f76f8027535d21e52b7fc50312583377b928d327cfc030dfc5884368c02b8dd93d8ccf42c44542a52a179bcedcd0c277f94c8314b7714be22a22ceaaf14e3b279aa2066fbd4c641ed17184152076505adf7f733004e51df26264722586d65170e5837df78c222d5301906386b6c2b9594a978827275209733e690f664fc0ad6e8a09839e8eb03efe2fbfcfeaf3b6efe14aa90d33a68098b42f0401ad9b80a1f301d6841dba1690f04f420c14996b2790291b091c076d9b683cf59d691607e1fe4cbdc8ac5288f20f047828d594b7bfa93ccb810e4f66d7494cfa8f312a4263630efb9730fcb49c3e25ffec758e19bbc6074c321e7e3e18c074cf1fe78e2b643566086f105f4ea7cc32e8c22bbee2f514a006440ea180a536d1aae7a6754f3f4196af0fe0f3a2a3eb365e66035809653b7eff17061a4bc142bd597361b7777973cb37a02f28a0121c8a6973a1faf48f68407ca737410073cd56b1381dc7022367cdf26ce8716e88659a5876ccfef99da0767be80ae872ef1a03085c3f0bca2c00373aaeafd9caa989f99f47e1d96e34f3918bd426e3070f6d98ee3dafd83647d9919b7e621eb2a3ca573a4657c19864721bc5601f18d41b84803ed57c3a67b4a81c031b13319a785bdec9316536d814cb75beb3016c4444f2ad0401d136cd8a9986c63d5182406de8e399d729cced21fb86aeea0ba629b4386b9a08f8f4962e44e4ec4099b4577b1b8d08ca72f952c612e057eb0f32d6ce1879b8496219e2d52d6fef86f0adabc5f719bb7bea5daf2daa2aaab202ab3b8f1dd82dc36abb4f5a357a0d10de2bad6e7e8409e55cb5697ae3a6678c3c90ac18bedfcc8ce5e7c264d13ba9eb9eda1e7b0a3f484495e3dcef48c494452b778db24d51620fe26ffb3ca6d58d613d489acee30b5afc98b41a2e2f6b82b82d7161164c7da34e55424ed3207c6556c92de6b0da96f41970ec6403dc9b6ebc1ebec5af2e892f5cf00eca29e2445a88aacb827e98ff60c153fd46ece716eac01f801b03f71d608773df4b7e01fa9e4b4e7dd2cd6858a48e5abb7f42cbf2559205aef801d90c13030c369c09cb89a72c2916b3232b44ba7cc43006206165793e134f7d9d605cbb8ea9650dd1bac0ae3b6f24259429ff5cfb6b33554a9827ef731c7631de06a270036d35e5d200eda11f9a5b4e9fae4b0be94240709eab6caa0b192c1931a9c20beb5749b5e9d7bf35e594c9eb715a7d6aeb1a7b799e1b9568e411e3abb55f9886ad36fe77a1addaeda4129f1a7c44889cfb759891438778630c343fbe4e0aa08cac485991d4ceacc6e36e0ff4009b339aaff1c20b995aa7537e181f3acd8b33874149f6948ab877575c4c46cf45fa18940c3b0b01cf8e26f7ef206f31cd3cfbdb50c4895a8b7994071892aa56e5731f79ea813dd68d38dc252a0a448d3476e1861218ebd644690271e2a1df09591c13326bd43dae5ceece57a53b82c6fb37d315e5266d8be1adf11871e9d0b3254757a2b89d212fb1a5aed71c3089d537de9caf3a05836a12ce3f06ba03b1c2e3abe04e20f85a5075f10efb5632dd250a46d36793d81fbbca08078b3cc5344849b2f4777ba96c4c51f50931f210e5cfeaf2fc0dde8c868ecd87b9ff7fc063e869186d9ef8f42ffeee75303634270b02c49f414484223ef6bacb10224472c72a6d2b4a55dd35754beaa51129d5066a4a79108c2707676debb8d8460792be989f6abc32a9955697e86f88b5c70f1324666e021da9b50a2536f90deb21fdd37d412825b6b789183cd7a08855570c249046d5fa1b78a9074c86b2064271956722b5b9b9098b47ab36e1813ae202082f9e4f2fcb1a23cc9b8c65b8eb101ee19bc52fcd59066db23e1c910b4fad04a40191bdbdafd60d411e63bb4be37590a62e6f67031969cfbf2d5c10b05f1e8da33598e1003d3683a2def2751ee9d5ca63d13706782855e30bdf8df5dd7e16f1ac100f6165f40176444d5fe0bad04b9474a0a733463ea02008ee8dbf421048b89528f775a67d8ec4df0c3e70ddd983f7837930ed1c9764ac97dd2cc6335dc961f524086dcc3dd1590766962e0a19bd99a9a0443b334aa44375c0633e1752dcdacf22024c04c7f8f134d66177140e9fe1716464881377cfa5af8fb846ce8d0a892b23ecf6d589e6524bf0d31a5becb0db9b647ffd3cf1f445633e60e2ab740c48e837c6e2f5d152898059437ef5f7ba571687b5d8a19c233c876b6dc86c4bc7712c1ff7638de3a6e2f342b74627bc8562277f2af8e38e25a49698a5cd4d8f30fa4c3ca31a75f1d04f9207cbea2141019262431e51ff0abbefb94d92a8dee90c5cfe12c87c7587072c6972fde6cdf0e5ae87764bfd4bb92084c774605de23ae0c208151dfa4e96a47ce5d37a6279a2ae96ed85e38e691bfbbc791ef6fdc0571b9d0839e626f8a957c350bb3975eb71658eca91eec2016f947993f799839f51a6b16730f96282ae945979795d57a20cbc4f8b8da574ba52a7f33f46debf5868fa942015d8b0321b96553d3b1f9642fff3f8385f7cd57f59990459c87287a652069b9215854090f87b88bd28164b357482208b7acad2e69b59ecc148ecf8fc8260d0d217e29b059ea79ab5528f0ace07e86bd02a19f424ceadde5d4e9da5871acecbb7034c57807c0d3f501546b2cab10032a0b78a43a5c49094bd70cb437cf06aae693069b7e971695966c96ba88d637aa1380a05a5b6b17838d79aed8d451917b8ec39e14a33b4607e0ea05cacf2c1e37a9989d3b7333a791e637ac120067ec6c9b13f485728ec3db798c472d30c8847c7219b01df20a36a7a62174ce521f78f7f631f884d9de89cf367dd64233ce6ce529c8b3510e64e94130f75b7b3840eb74de02b19e8db036f4a35477601122f5b319a473b48c15aa9a17528ad70dcc0c6ae9f30d2a27dd07ba5d7f46fd2434d6dce48d6b9be99c27aa9241cf06a47a91c634a3e80eb1226f947c6263191953508eb81574088df331529c9adadb11000ef88df3afe3e1a94dadbdd9498f25c831a6856ba99d7f562e6418522629eb174a9a1763460c226953bcf3f2cb1d1eeb9901093eb562066ce2b53ebfa99190d40adde5e22d1258650a7017c4eadb306e3a3399161fdc26f324efbe1bdf79d31b670e3f4399052f2abc29e75e54817ef5721b8abaef8859f0b44466e461e2c0e782511b729b9586eae3639a8433666c03efb296d3ef358146bd8149df652684fc77f12ed17cdebaa3e54603cca58549839a18818552a261f910c65880cecc7b71e67bb030ea66de315db35e8ca9195221d5957601473ab3e8b265f33deb6e9830dc8e5b5318534ad86822dc889d07e120e4409dc71299d0563fc7f1e3e42d0bcb5e5546c060c624f25d794c4fc2b9c123eee53dd7433b950dabcdd5df4b6c09f9662636422ce099e1928fffb914ab9677e92a000aac69c8124f2da52ccc7047e8f4548733b7651089b41db72ef606b1d1800db6998e768a7b791f8167a9a8e8fa74a5f6306db79068e7c087b5445f99e4f4e4f2f7e7cb5e2063e040a3576b1fd54489a4d0b5cfdc4ae87529d9dee46272d0f333b890cda54d2445790939a53b4f09ef8f735e4ad5617f92efd672ad348ffbd6d0fb24ca12db8aa231364aa5443b628ae2a1ca25191a3b9dbaf3607151dcb82fbcf0343a004057a7fd92aa65821bff82f89224c1fa32a4b0f95f71003889d29f0294710927c552e268f24cb8d506f83f4b401493bcc8eefd2571b21304588fad0b3bffd452c1afa297241edd5f64a4aad6f063fa52e0debce8199b77b499feaed1b3341a275d7bf0c3c420744b7a731897c142626fd47f092b8dd3591cdadfd5786d19b2cc0d44ae2503e43c914a716d82429b90f1e62a7378a9ecbbcacd6428283961c862bf43337ae4aa88820f40c8d180407c9db4160ecc1f31b75c1a96b203fb12dd679b22d499f9e29972c41634b6c17984dffd55a357e01e0063eb4595539d486e47abcf623cd22e4cda816d972080dc7c49900acab496f4fb5899b1ce857a1e4e050e59c89aef8a1326f391cf875015f92321e786cface98366282b621a5468426fd3ef54a71f65ee42fbb8925743989b88ca3e1fff7c0a099f541dc08379245133a8613e7070b102caa175a1fe60d90d8629e601586b9db1927243a1560cc6169531d1bd2f57f10066ea998f57824bac8e957c8b40443a865b253a3078a3d6ba987b4e7a138b59a9bc5104749df348a9c82a79611b8bcd32ebeeee7dcc5afabcb59017e0ef3eee9833df563f64a2c5f755da6126d509cc7c2efab1cef5d317f5ba4b8d44428f2391ff1f8715a7f2ab1e80d549ba628429bf9229ea0a7311871ea4bff78bf9d9c5545fb0c13bfb437d4844091c048a7e1d9191b66a3c3519c93208ec73943ce6d9ebd6d0b9009a2115a8ae0700ab08a33dc598a7f4e11bdcf254643454863267d52ae8a34fc831d339821febd2befb7825d267b4f762270642e423aa818dc3a9d25dbde3e2391181e8e90090ea3532fefb077b1c5c71ff4d2d73793ef25bba11f5e429ea97c28a815ce4839476c2ae3e41e430c7b32e14a7739f95bc1c71704dd834a7eaf16cdf30508f0f35a4369170f2a8ea3ce2feb07f2c765781c17649f187b6c62b0ae4a000993b0b2769e60268a63e80708fee95924942214b300ea2c694156ba68774488246daee6e11e90eed74afc4cf7aec4f9ed635d852e012e4b383cbee19f9c0a37114b13a04b639e9e203bc8cb74bafc12d7303a6545d55597d083ce2beff0e5e31ca0c075dbd333aa7af34e99cf9023308e7a8b087e3ddfa0716dbf14121ce3d50ac786605ab44375fc7f6d9164c390fa531bd9989aefb38a81e8529226f2e94d1861905c264489f2191de08505b5c65e64fac823c126c03bbaf9f8f0c138251b082b5bc63789c4b83655993e60753a600b82a08a3d862777b492a2050007afd42cb3c06dfce9164caa3e7dc4f32963d41109ca612d864cc0368b62d161f4e61c33d9cece10eeaff277ac07e6982e4bcc634b51eca9318b0275f84eca0507f1cb3d158eab4d7e8bd4e9daa079010cbb254f0c4718431069c3c8b550f1f48132fa3c7cedc60676ce4eefcfdfde4cfaa462a16635aa1223cdb89a69c2cdb5b25c2811bea7794f5aa30cd64df324e4f2b82d6f455e3ee97415b3e798e8ba53282c520045417276b84fff56de4d964cb9577def437c009c4f1ac2009683b933dbc5b707f0f06f6cb08e1878244be4b4a78217c167da249f97501f010b665654b84dc83ee6635445da33169875044a963e19de81ead7325c997c98b518af8774e9e7782bf3346414e72a57cafdf40b12fed84cc1f80112dedd7a88d50f994c21f8fc15022e293bd141feb1cdda6f34f987bcace87047ddc29da7a7d3d51780f600308821368570d9a67b7fee9339bca8b9e5c7e6536023e61c791badb5ec75a32fa57134aad944508a3196d5d1681b86ae4845d94d491625acaa784131f673ac395c051f06a72180cf0f248e328ebfd48a635344790bf8b6ace9f74d6a80b28c00a8bf36bcdade6077f316f57e0556def217f5c7388b20854112da38e280221f61a06b1797038b28fce2950a12a5a5c2398c1d59f22763c9d9013ec9bd3958dc8614eb2dad0c98d4f491620918ec5d6297d6c14e5438a221d4c536b1e3684bdc8a4d200cfdb2ef15c4321fa4d70596d0b7415ac784a5bd998844e292303db2f0da28f139e1f79dc4972e35061a785c98a3e753057be3347593b484fbdc0eee36419bc02a0e125f830947ed869c1d6adbbc7f6d27737b05d4fa0d774092c09640600862138b7eadf16430932d85552e09073dd75c1bffae66cacbf410c8df76a9e70de128890eb8ae0978a786647991d78e042e3d844e144d26dcac8fd7a5c5408aee5f3f6540d44b14a1fd959baea319e875e10f4ab47f5b55b14b68fdcc90b654d6632c648ca67a4f4e30afbbdfa87b374171ddf2674bd17602d694c91a068282dbbd16f56f2ad22d69eb444d49d64e4d4043e5f9774b2d6463b1ea600f0162f20f723474ce358b213422077822e0a5451ce0eb70cd9b584e9a7251e589f90f2de25b233e8252f141a495339f0c15b8c3c8bf449733808b1eda375254d81f4a6ea24282e082e0faf0c2f53ee31dc7200e65a77a5a29256707a86c94413f4df519ecc2821927d559234fe7e4e8fddac39b97aa5b55d81a9f4f56f0254a5a829e211b861e853ce2fff882f76fff4f338692b2b62e41d6aab5acca525b83e3937e7efe472e286833ae910d58aded56ef57e3a3067f18ae7fa7653b93a52aa7ded330b43abd4c7a7fce499fc6efa4b3f51f418157552ea0403dc9247cb647dc8d62570dc732a69c0123125f62f5632b49bca1f51bf1f32d7c0aa6958cb016999ecac58a13071391bad3ea482715057fbaeea04c79f8fb33dc0584db96c5860054a4bbe129b92a4db227e678e197fe78629ab55931876c4079ebb9fa8b5346c2aff2008b85b6005314578e790cbd802a03a80e41511061d41de6e6d6fbb1b142ccc1880a8c948b4804716bf87498b48759f9780cc2a34d8393485b8fc3df1bbe3f2dc248c223dedd696203f4e863e14e4b458c8915fa9594d30308b001d569cecac5e884058d24874f15915f1acd6bcf0ebaabaca6a87071c56642923fe5aee1d93a3701dc8e093441c8d1c664a8e25f4d64328118119df354eaf19f4b7be3edc43068d1aa8f20c3bc2cfee7dae423b4bb29d3acc81ae732a9b5fa555b607febc60172abaf24f5ec3ff2d34f2f5fe893262b418878c76fa829acb3d6e8836148db96ff48f91e55cf7a67da226e2f8eaf1e750fb9b94e2cee55f5d58e26003d87b246ea94a9ba076fb9a33492ec5356ec350b0a3f81aad9036f24d777aa50717e1409d51a092fcddd1eadcc9a60a794739a20860a440564bbe1229cd645f939a8b5ef2393bd85f18c5868e22d0074e754b4178662c0172f7b8a36890133502f0350a6714fa799b195bd4753c6e7fc3febdb9fdbf451e79ce456772ffc12f2f6ec78f568287ab7c7206935af9d97f9677c54716fd7f19f617cb1ab5b3370cf4a827cb0d9668814253cb2ee0a7e3640d4c8c3a9aa7b067bb5f208539857a28c7e88a7ee0f1f593852f4edbca5c10ac64310aed3d02434dba785cf2f31b984dfeb28a9119c5f170484c17a6e8b1ada9cbfa830c5b6cef83f6a40835c6ea58195cac815f40941b02e01004349e72ae81bdc54aeb7d9130d0faf5af0585614f3f015307e3b37ceead2b995fe70ea9b874d4dde076de36e022e52f4076f9947168ad143b45d8c719198951c0100fa8ed39ef7993bdaa5b29da8d6a278cef6575c6e18492c3c81b8da2504c796390c357e8a7d4b921af4150f798756a351eb0d0b8e6cca953d59c955f0bccaf52b000000000000000001008651450b384a1be4f25b05d9b6ac552307a1498473cb4bf377bf17fe90463b3bc3cda080d9c9c4c1d8861a4aa776de4b3887ca857f97d0c151a88577e7d7c9330000 diff --git a/zebra-test/src/vectors/orchard-zsa-workflow-block-4.txt b/zebra-test/src/vectors/orchard-zsa-workflow-block-4.txt new file mode 100644 index 00000000000..4a3fd674a85 --- /dev/null +++ b/zebra-test/src/vectors/orchard-zsa-workflow-block-4.txt @@ -0,0 +1 @@ +04000000293308488fed45512ca0bacc2fe851d3d74b0a35db77040f92ccb405cb79471a642c58f092852382aea7864344d14577b5d2fbf6a33b3757e70d46ac11e2e640e385d9a360c44970f6e027b274a588995c7f5787b332ae78fed3bf6f9e4703c722254a4d0f0f0f200202020202020202020202020202020202020202020202020202020202020202fd4005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020400008085202f89010000000000000000000000000000000000000000000000000000000000000000ffffffff025400ffffffff0140be4025000000001976a91475dd6d7f4bef95aa1ff1a711e5bfd853b4c6aaf888ac000000000400000000000000000000000000000600008077777777d80a1977000000001c1d1c000000000000000000000000000102a61ab77f99d15677db6daeee41bce55515f2b94494edcfbb2c4b245cebd29d81468d2a08ebeb6e2174b0cac2c5146daa73af7939f80a182b9f64fedc4332ed251a476e888fb7112dcd77415de117ed4e84d195e9140b4810eea253f74943550721ac7b54eda526e5bc8e5d9cfdb128129321e1b0f27a9968f8652bb8d3ab0f2e7782aafaabfe4ca1b9206270f571790f1e5ddeb686b24afcdc32c59ac16a029ffe816cc0aa56d7954acc308577706d13aa1fcc93d6863b48daefb3d853bfb3dd8cda43f36c07d64647539f830bf5af5dbffcf4111f1bbf6e41275b9c246482f8fd5adc40ebaf07e8a397b5f5443561361d3550f5753b23994225d1cc41be22397eb3c4b1b4bb7f47c045c21b466eadbdadf96e832c1f7218ab3caeb78ce0165b5b4a9b5bcb77c2db63dc697299d906613aa9208336c08aaa99546aeb9f024b7b182f5473b3c8e2d19812fe359b5a25c52175d3c40219e3769dd032af9d3fed29110784c0921edc1715ef70698b37217e07f1aeb47b44c2e6ffd2fee66cfdbb8780a355fc3884f05e6465a5eedc3c9ac8ccbbaf55d947a8e781a40ea865ccb88272662a46c66faa0450d2e2b92197a655c8a142e8023fcc46c477f93bb9e6dbe635c0044fbdbb7013430c1db33330c57829d694dac21addd26dced5a42193cb8b3f1cea152b262fc7ddd973df487d36b8722e2d2d9fac1677b598253dce0648e948677b818b71d9d8eea4bc7c00c5b4c9a0c3641e58e1215573c9b885185bb698bb892eeab0dd77f43e4d9f7eeefe7c23f308c0453c015bec793dacc16f82444ee4782074bba0998de85f48204fd7cec03e19a474b316c0df4c4f360cf894722283a0a1e2d9bf5c3b3f9dbc8bf57e0d5b1ec70d414e01ae59ee8a26cce1e0f2a230b8a5495e9f5234831df9796e88c19b2865a55fe14e761bfe46eac9a86fa34aa6dc918ffb011cd3f79370eb2b80a53d0e20e29c6e688d2aed78ac60441a7dc8deaf313e6343ccdf16bb920a0d403e1e3bd7231d53217680e730308d7857d31a28b940381eadcfcf6f45f9dc3a312680d54ae75991a678a3e683513ee7b100b0b5cf6353876c60ba2feb4089812d837655c9e85ec75dea73f8a27c82ecfc74a91249149d81013e6c37bd1d23665bb2f95206f3d2c2ec907a2ccc292e0128577a83b80cb30b4911953dc3b8b37e2d5880965c400314ecfe07497bcd910968fb13d3bf80ad52d26e30155ff51878e86916e74e1d3fe26f5f97e8ab22de3ea067f48c20852ba592d297ce13a6a9fa480eb1c4408a0d747015990b036c9438eed6c462484dc0f630c0a9d2558f87dc83b8b58a1a6e0bf4fd59d49717f5337191d0554cd1cefe74d2f73cb8f401b10c5c90ee1f55b83c2fd8ec62738f3e0662bd96479073e4939f1f6cabb21b927836bdffadd0e3b6186caf59e63903d500c5525517e419b4d2d9c7f493d5cf36fd7115d1fef916022111a31db97863077bafb9fe9a0d17175eae1281942c66ffea2c8193319cc32af840e1ad5d2a7999e10ab90f723654760a1e448e9ec1a66e2d039be4081dbe838ba28f0c96340364372f019ddda85290a15c07a72605f85ee2a4b17756a306e60b986169b780acee465839030910811ba00af00b9c408949c7e6d02f342e2db6609bf17c7ba1686b905542121f01412fcd4e40fc7f38d747180c7c197d2c7128612701511262fb6509f4c9b3119abcc57aa6bbf1752b9d6cfcbd16b7a959bb9e1d3c39e32e1b3603cd1d97b8ec564c49fb20f5ee40cdaaefba448f04a577f3d91665db4efc8f51bed235df7f29b1a9fc6e8bfb02ede69eb626ea6ac5eca63a5dc1b07f32acfde517045921718c0e3bb8539dc6557dc3794e0367e5bd47bfd213fef9570818931588a5bb6f1545d79d3bbe36de936e33b6a3c5c31a18e1fb7cb25ec38434f165640be6b20f5ea300114420449e886d8cefeb47d4688fda50a3d5d05aa0ab039be78ae889509c8c885e4be14b756740f7025733e8e42ca77c4bd6dc02cdf33085e83c631f1867d0f5afcd1dbb735ff59737ae2f3e309e86f3e30e629a2a9194f5bcf0e200676752f6821283401ce3a108ba8785d651a4345dd030ef3216135373a16e8c4942167618b68c2ab9036ebb6378df975f00b75e2f907bc343741044ac4b7332677a6bd0b915cebaccb8da0387dcade509be63bdb4b126b4e74d327ee3944bba493f9a607837e7922cbadb8d39a8510e6a806fbc1b648a474dea3b417178faa1274bfd96e0e8ee841cf8ae47a34fe7b0d300e6326e85752f7b04427450d8110226b55695c0795ea866bff9520b84e199bcd58b0628558dc102f3cb17eac9d28b7cc70c651be173d4a12707e15a9e81d1eb5f0255322ea5e1e84ce1e004396c04949873362beaade613ff6cee0779ee33ac03ed7c41cbbcb4346a933b2bd706de42f40832c1af9e5afcc313c7310000000000fde01ceb14769d26edc3e1ec0b6c3cf4f012e5f3bb0699eeda7098e32772a2e7a5789f42bde0a9a3d22ee79679c83b7929ee7d85a9ef790df92ed99f1568f234bf6327a7e6b0b9a9f83864cffedabd0f4b7d05cf38d308466d3c8f615f6dfdb54686988ced6e545fb3dc0e3b667c1b86b529af140add483c97f03e79781b36fdc9c7348b8f6bb5c367ca37d98d9494568a5ba2efe112bbe0a4d8f2985f0685b760e98d4b1b6a310ac4df2f7c35c2c090b340e9570f246844dd1f08408dbf33dc30fc1e5324d0136755e1c972490710fe88e921b82cbcf1ce77f1381198ee399ee8cd36b71e3f76ab846147d753472420b3f95356814b35d3c4a09ddb88017553a6b88eefc75edea98bb62f0ca6c0d96b294cb39c26fdfdc53dcd846923ed6fa4cba81d4c7284325d4cabc834ab07ec5c6315841c1b37971a078785bef4f1b9d4c784824e15580ff4d22ccabe4228308a6e1c8997600573b157a762440c003f7675f9ba84cd61ca6e4cc963fe167930d6dbe248be0b60d702f38298ad32292ca65c0b8c722a11884a85971e85ae6c7ceae43c58a6ca60e73b226375379da8b21edd8f9c62dab681f448f00b805c562f864a9d552e8920d436eb90b03d511df86d2b9007ff38b9bbf2185de7e2f83d40b272c57074cdeb0a4a136b62ae6085c4eea6529ebcc2668df2c833515cf8a3c9f0771777d42d08edd47494d828ccefc8a8218f8be6ca78a7e715940e9ec0b6f4bc61b2273c3f9af3e7ccff5143c502530de51481665313011067846e1db2af82d4968d68d6fcbf6e0d4eabdc26d5afe3914fb60ac2915eb768c03b132977e09f39e1a6762b697c2cf3565248e5792b142ab2b8081b252156e8e7fd697faa78812e06b0cf702122d342918c42de5950c0c655cab82c6e778feaca8fefb135e598016698e42cfdc2cd17b3645c8f9c06cb1178b6282683874f9397cda1a0c4f82342afea89f968acc92273ecfd3abd3060a6ec7c185ff5e595fa77db09731ee7ae847dccdfc730b01f3bd08d2335dd46447e5251a49513eb733557e9598ca8b9194464f98a84dddefd38c8d95fae4c6546e2d60a0a56c5094d45f534bd54e3638586438ed21843edda4ba96652bc0fb6142900b2bac653a1a8e5b47cbbe3c6236123cab2f081cf8869f0c7f6b95d1317c71695dfa66ef1a8f73508c489bf895c77681a67cbc0336289b8d321158aca0ee71c5fd833abda69b736cbe4d3925409b00c1f7aa1c2436b83f0a2eb7f4617f7cbad46f9399afcc67d9cd0a633ee7d8640f60d5cfbcf5a59038f6c35c5b78be9c3b9a20005f22dd003ec32dc887a6f05e9e63c2f2bd55e7e1dc6a16158d18303cc825e0ca3ea9c14b00ce65d179ffa91e9823333b286436c19fe1f6b7fd4e5745fb57dcf304a8181ed986134f8d958c4aeb9aaea0f3c5839f2c2ccc40702e4f2df5d1b373c12fd900e3061d37ab827bb45d8cec4c318e0bf9bb8b1efc28e55c2cf5143a104c2d2f67fe14c7b084662e87ed070b687fafb3c338beb0bdaf156689e97623b388006dbb7001d81c1336b931ca5ee62a217680dc3c5e6a3867227aa3f4b3c0688254a21da00ed13187e94bc3ff8dddaecd22a6f69dfba6faf5f4f1420d7570b01648a634ebb58253d5fbcd5087e9d6c40b58c9888d24cde24c53aa03ac95fd00ac0f3883595259a08e377805185d88251d02cdb1e07d8317dc3c49d5ac7d51f251ffdad6a5924712078df4f978fa698678d5f5cc98aacfc4a1515fcfcc9894c1b93b408c32f570f6d25440d9f54dfd10b04ec332659cc295963be57d966204c825e5ac79c49a07e3fd25eac4420d826e4268c8194458ffb42930fb1fe86504081c609d6dbfddd60476a395aafdf29961d710026cb301045b5afb969eef89eb68da988649589966c5609fa88912dd6922f57a9c4b5585979c635a94a227a9a679c1c12f4c06de51e3cc5d799769917d735755eb6b83c1e62effd850dfc4e97deb5af370caccd79c130aba98b5bfc18d641924def73a094848bc9871eb2c68b56a71de350dd0b1ca1326bf4ea898117a9fd1cba7ff487dccfb1cbaa1dd5260f19988dd5ad54daee2747ecbe3d20c38254ff6114972e80343f17f85dbe5f8121a7a746ef0dc5f4eadfc2516d15103f099c212860163554c4946f05dba082b1a8683bfa8b681fbd07d725ab36b41596d650c89c5879ccf3bfcba4c2dde9151e4c8508bac86b19d6da570d114650a659ef72c8c2a114ce78416b7c92ae8cc04421dc0d38bda88d702942fde9e0d887e30a68d928df94b49839f0cf965376adf852c922df049dd57e6d9426f76d173de491c956930b378544fbce65263323d2a05bad1f5ae4ff2fa4a2c48913647c37bf1f924327a3901736687e3e1c91706b1a9ae8b44178853071df81287c4b35df69348a84d7c7b678dfc7f0285d45b5ec38cc4b06dbfbb8eb1f58a5e032ad73a19c491eb0da69c7c2eefaf5745b1c29c172df460b63ea5ffbed0f5b523f60133b67f9541aea2495222d9d0d85fe1df8e109d976132afacbb839b05177f205c45e66d6601b3af686626f2dc6ad85780470db507617e70a00cfa20096fb48cebedd9f0fced03bb05497d6b5a79e133063e56134b810de9272d45068f155004a2a874cd501d711fd514c3777402de53671e88c08601930ed9e6d290ce94f7ed773bec230a56762dde7ae45663444ee9cfbb8e520fb2e952136ce8aee8c67bf67aa4263c9c9ed27f371e3fad47f54c0fb6bee650ee51c27018eadf25d872b2e778530da23f8902ea1fb8f987ec5a9b8fc5e02d1de942b3160fe1c328d8238160bc72f198b7b755630bde4c8c6e0bafc09878006966712af6eb173accde952176f2ada6e73a57e258a67b63334bf671165891cfbffcf02b72468e3f41306eb6f614ea1574c22552243ac7fbce43fee330539ddd9ad050a65d418391d5dfac66b88af326e9afbed52da3285a1ff37042297fdbf28c234375aae863308d16c18f32c35981bdc25527581e72c950c8b102486ad2443653513babd3304c97af50e0e7c47c3bc4913e3105fadc330955f2aa8457c500352531bf75a81b5c36c3537957c9310129657eeb9764331d8916fb8c4b93d603f2f451df90fe4b18fcff5937f0763f9f9d9e43aa9a48955ca43d721dd958f251ce3c12a65dbed0d6310a2420ee51c1d66c370feb868fb47852107c9c3c2e4a150bf90074938b41ac2cc344302cea6dc108d657c2664b6ac3554ffe290f63f41b88ba62799922c8bd5713c96645d6af1a02ebd99483d4bbb22c349f00096949b8fa72f008a68aebdfef62690f96f0b0690b2f05b8fa06da539da1086add1bdc5e41ca418c8a9e62f75a1d5c3064be3b763d02cbd4ec986a6d4db0b5def1caf3b7e6b2b18cc119fd1ace8ef93b2e67f153248a844c6dd39e955fa7ae2d10ab386ba0e3c09983259aaf80654818421f299ae4f76c1977ea6b22e0117ab8e9327470a0803104c31d9c50f595df4b59457dcbabf610e8efd6ce42e210028ef2a40daeeb7b52b5dee87fbd9edd4812c70d3b03655cf8c527b76470d7a168cd23ac716ec057419d362d88901e093dd6fa5a05279c0fdb23b868da33fd26032a07e0935e3e41e2ff879c07710a5a0554208caab5fd12c707f68998d013f3fd858749c37e34a15366230abddaf53384789df8575856e335083cce50200f35f4f0c01741227f7633f64748dff363f0d5d2741f015b3b0da58e7550c96da8eebdc3ce9010857fe3f1224e8485476f772c4ec3a0b1914bbb953bbe634de98ee085481dde0a5b3dc2d0b9b1c0eeacb67257e295b5871e8b90d5bd4702169999b0caf297fda250c3d67270fe651ed3f13f927ca8dcae6fecb4b14dfd14650724e1d85fa0be29a6126ec1f6d9b4bb5d7d01d132ad4caaa4f5158abe9cc90285c931a747f85ccb444995e3794eabb8a62852bc7c8e4262261118dbd39905e02cc4e87f6309e31dcbd7afe3362cc3902337156eb37c38588e2132969a8b460caa70581932cb872bd5eb62030acf78e5bbe017d7beef6378d53388baf133984d611f9c1ada7415a710c7af40ea8a18b462ec900cccfd48c9c9f6d75a0bec518eea39bd729b6af9a41a3823f088c62ec6e494cb081c011d6f72611d147d7297bda13dc1c5cad5e2f64e0eda824eef11a8e99ac24ad312041b7f16587b2ee5da66751076a45d87a0004cff4830b178d6becbfa7fb0fbb59af805a2bc047ea82308732b7170f5cff6213bad74b34a24a40208eb1c3caf10cdc7f5e075893896a7ad83612f996a5ccdf4439f9be232e0c70c8b4bf1815cacdddf09f391d2daa3f185752edaf3b178daf972d9a0d2075b1e86e6329a4ee9fc3701416246569e6d6b574d3359cc7fdc81b7e7f6e9118a707d9a8e19fba7f0bde9fc9f0d57ad42a4ed8acc888573de8a42deacd3ab60b13a1b8e7b4d6c3889a13c9f83dca28e22882223ce887d3e754c03d13d6435e25f8629b32c45125878379f382124092a85575aae2f7d85c068952e5ee2963c82f0f52dd2659a3dfedfe80345bc6b448371f6e98f89396113bafada3d7d00ad708ccd0aa4da642e47c3c3125e1fb7007fc3428e8039b237784ad373997b21dc11eca74a5c5c76ea0d28016789fd5e71b53b98caa0403518b48fec07d7deda6bd3502f6e845ff4d38f561fffb712ef1574e5c67b18250848cd7b7417e38f9c2762063728b3ff65d3b1308d52384d93ef6106378ca74e73785edcbccb87d190fa42852bc0e7cc674aef23d5c4f6c7f25d3c6406222440f30395bee17078618dec30471cf0f329f7944944d224696b984f06b4d6f38aa012d8f468a379ea694786916fafba3ee538cd5302597aea4b9e36176037fb75b1eb9f00f4e814b10dfa4593eb4de89e8672475715d9ce9f296a37b7cd1c04a7d2ad6adc2313a63650ae6f3243cc519684ee439e62d4f05c9cd1210bca14569501f73bd304100e91c3125ca1856d0f14ba4cf60858ba376e8953401eb0e34ddcc79a3630536c44b51dc9cd709c0a0c56cc995485458f558c9499d70015847975f1b0bca691170a19b00a5ee341b153d382d12f17bfe6e524c8d1c3ddd2ae4e8b820fe3fada07d5f11b0d1cc20d4f3837ca323bc3aca166310afc414b5e6bc9a8b5b14511a3d53ab0651e16f35f71788c25d5699f0e8993d10f5ffed58bb5e0e1fee76c0dbb6bcd3c57742043cfb6af8cb5a939e051bb880e57d450f3b422c4261481e5707d463502fa1a247339e29df438ce7f89e0e589fd38c495e0f18ded68b47dabebf0a524ae202b06a0f3ee6c4f03470327b4de92c3f542653dc56425dfe320f40bdedbdc05119310e35da22b5750cfb30e5cb0ed0716e6db9db5e7e6abb07b92d4a953340bc2e5f190ad4c35e289954c971c727bc15e9b7aabd7f03c20531fea2eae19f8e9d79adcd2f8224bcfe7f439b4f3709c01b3597ef79cae722ecec8031a958b0b8ceaf85121b4a07daf02f8e2cf0b3de383af65691660f59e165837b7cf6bde2dadeed1bd43d3f73ae1698911b0536f7dbf4e2d6c56335726bd1ec5656507382eb988fc02139bc2abd9c4054dead0b8708d1c82bc2522a0dac1b12a4e12482643b52f6dbd336a5c9e18f3e1277108fe3c748f1921b51120a6ef937ac5ec0d7987be5e162330df3051a5d73e1d2d58277003a3816dd7b8b3f959bc6f211d443c38be9ddd3b9300ed7c57b80896ea353979c025686d0131368bb090ddc1b416968888543bf181484b30c117cfd0ba7cb47fbdd9353aca2ad8c3e232ab347c8fcee3da949461d254ebb4efa58323397c7437d14ae41188c1091a1e5c19a812bb9f3c0e3052d703fde44e4bc644eb9868cd3e81be7c593e112e4b16332a0edc158679568d68898215401067e152674e9b08a91f1545039209540ee07d606534d9d3cae8e2ba7c41696d98e81cfb51ac75093cdcecbe4e151e7e11dd2f5acd46c4b81510b058aee02a6443b5ed15eab6ca414f558d717e713fd72a1508344d11dd855b3ddea04a91e9e17e801b1864a11ba7c691faba839181faebea6fedc5e17f73b86c9f79c153de886a466b2bf576b13e20ee04ee7a812f9c282f12ecc84b4664fa8f0d7ba28342e62a5f26636812c85a2fc52e2713821dc14f4d88d6c7c3b04d1ebfeac6fb52c1c3deda7ad9ca81f3198479f8aaec97788c4e36a60faf9f35466ea0817a7580b7e2c1dd7d5c91f825eb390649ff2ef59043a78e81af318ca02d8e51530893e2c476ab939be3fb7f97e7e1c876102db4f933c6515a0ea3815f8692d5882149f27228de5d3ee3a189e442f7aeba9d2f224b4a7350b972ed7804024285cb5ffbf190c658042b34a65f74e2206386d72f58c1971e13ee1618111e4abbbac42a4df018dd5095f5d34f8bedba7e8c016d43d5976b53f64b8ba69bee894bbe6592ba91091b477245b4c7fbb3b117f43e153dd3ce5b8e729db69166667aa8a262f499f1ffcaf96400926b7310e8dad490e172b3a28a1372e8269b700843040145d0fff123e774cdd7d13a107f941e54c3b101a75a7b89123c730b23c5242541160b81b2486f0d43b48b630ebff077349b932d4b1dca86ac7b0eb09ec210c6e60b8023b1bd764f45575aa0ff8cf428e73967b893ed99a3677df98181ce1e747ba05e4d22856602b65940feff1572b85070cbf3f3667d9006ac9e2a823bde9bc6e4802ac245a1a0593c9ed21bb20bd96ed1d6dc372c68fef06bbdcddf157fc986f12592b09b47bd57a93abdebe379afbad3cbcee4fd3d3c06ab7e077c3a592d7f05dfecb21dab193ed2f1cacc73b36f234d19cd9aec0820a5f07d885ab875cf95ae0ea422b479f899185683f450a80bbe3243e675ab78d03ff3c8b892162bb6ed140d6693f23c1db1ce1c44e72bc8cbbd78cfaa7276e307a9790367739bf7ae9c41910690b17da552948128e954d0c0358127a6c5cde7138e4143dfd124879d345bffd973ff61bd59ef05b29b375bf361a91ff36e5d944ebe5eaaa3bd9219192cceb83b03f6e01c03773d0875b125b474eda6b638815df36a9897cd04bb0627875acab6916945cb4ad4a15c7789ecd6545710ce02c4a95511a936dbce1d23f63cf2aa1b4168eb79125c272c84984ff6b90509dca11528b90ebcfec843d8031e1262e44492d91380c1e48fe40b69a7f972a4b65f2a1272e69dc81f9265944e7a7fa5292d716114f81e5e2d85ca8e5bef65cf0c2b48b8de8ecad9666060f8bce33197c9c8d3bd23cbf12d5f38b2bbf558834a1f087e829393f6d894f406d5648fbf02c033c0c9c7c2cb5765bdec28ac013550af841af8dd75daaa595c21604d27b4865791e19764aaef69747f939a26d390cd2a13c5c7393212791d47feb4c7f997825007212ec03106caa9313a057aaede6c32e61b56aa7d2ff6dab413ea0024a52229d3424ca20b13ced2f99eff9ab0ece84c3742825c1b1af21c43b10f92c9b77e860ac2c979d06114976002b556cba9d2b397138802defd025c2eee86981f62a0577e3255e257305c56fd6b36dfe8bdc474c11509b2aa6a6e2d0b0c9940933727683460a9d76ab3a5173ce8c12cbbd642f5aca35f712b0e6a09954347a129a8bd9d7b03c85667a8ab55baaa182630888a363429710fcc3a1053a2f133a5a0ee00801ef0044046c0c83ad2fc7e9004761f23c6c56c68ef820658f10e9548eac148b676b17ba4da920681aa72b7457836312e6b5bc5fa94d432c4f474c6e463cf489cfee3759027eb768ada86f9caaee53e8fd04f38d0d77c2b0162ff6b9526d46f0931e306a7ff8507264fe9a28668e232524b55ab6c4265bb45732328f3ab319cc762920efca7be63c01e45676abcc53fcc8127bf623df583878a6467afbed0107d82f39c0a07a91667c030c1ca68e51efb39b4de79dc884a8c1d3e08f92d79ced7e0726f794cf707804370c03b15798d889386011a398083bc39b8bd025becbda55461144cc64d2a87f66fe36395a7cc04a4d526903c009c7e5b879ad42b55cf100dd2874343a9b5c28f814aa9bdef9da9ddf3e28afb7d7252fcadeddefbf3871266722f4cc91e1cac1ef6d7bf79c8041e9c3bdd67ab98128f846511aa3f3508002b72e266ad7dc1e6d7980bd52080632e8b34517a175ee55d6cdb603482ba3b08494208127e10ca59ae0464b6065129b14c5f64dc094e6b3c58381facb0ac54edeb50551d146501b9e73db713a9dd2f7da7ac59b7127817b6c7e2e967189257d63ba1086acd83077cd1a472739146b872eabbde85a95c7365fe4014b4dceef5c8d162308280291425ce7a154731d7827d00fba81664aa1799117256d3122e3683b453c57a5c89c0ef29f5ff01378502117bb625c06519debb93adaa8fedbc4a5b5c333c44d69506a9defe5ff6316447e14be33ce2b346eeb0ffe9e8eae127934ea47297114a20f1571ff9410b6aae38c103958d9e2f744f96e7a5c863fa5e1f7946435916dfaea569c98e52cbce21f316a386190fc8a8bc758207eaf47418f22cb383a3c0b76ed430803088ac2ea93a59675dfdb5e54acc2a344df2e79edb82a8c353acf494b7875dd61d2c671fb93c9f0705e0c706350ad9a63bd3ccdf5319e72a83570b865e4a16f64ede5fca465f827afd75a94cad5f504af82e6c2d20439f7b9072feaf64a46f0bd37514d5ac23dedd40130eb796b239b20b17d01dd24d103aa1bbd0b34c98a33bfefd305f678153c1da63db75b3519aad211c89866a06e14053fec9ab201a15f0968f5a7351b746ffb59fb9072e1d31c6f6a3893fc4a08c30f2c2f77b6ebbbe541ef66261ea201ee1dc28c9c3fd5331146d17c860c3ecbe6f03d7fc3ab3bec46892b484a30ce4e368e9ce6c7deb8551a62bffb57265999bc0a06d433a147a9455b52dc0206d07ba97461cac5cb1e6af61dbdcfcf224fd383c82d8c3c2db90bdc2a1de556fb17e74d717519ca2c67fade699607baf4584a714f36a9a48619caa3388af28ea027b3e2093ffb100e60b62c7d7bce5389ced95cae2876d22766b9bb1916266b6298803b1d2942f80650dd52e3a6e68a2381b54ae03f8c72b70d74ff8d62664e7feffa4957a7c5ad39d61310db12e67bb689cc1f3020271161dad2e1bea43512eca10f9806a04dfbd5dd2a7cd407d8bb7bba1e8a632f6ef20dfdab100533b29c8ee155390ea7280b1c6f08518cd51a280be4ad60e01bef867332df9dd00bb5855db742193990382f4ab17dec02ea779057ef4a17973395fa42b06a4f025190de6f453db9de88fc3ab30f925e4252a3f068840147f63651ad9fc4dce4a25ef04827475ee32b6323d631d32c3b5e56c2a55f7165ffcb3214171ae35d46238ecc97c6ea7c33b642f464f65e47e207ad3f39db36aa89fd812af4200d822ce12633bc286be2df3fc11fb9a86ec3a1b8e866925d2f3ce9a6832b906f4168107cb6dd819206c526a615de50e7490c51afb22438166d36dd591afcf832c7cd0b00caa086f25ee17510f577b1414bce7b402d89a048705da79fa27d7ba3e1df1bf09a8413fc367764d504480886e0c28d818d53f533ec4bcfe6843f9c8acba036b249e9e9b03094ab9c865178d5e6d368b3600ed0bbbad0cca699e39a6dfc3d7ab8419e4e73befc32bebacf848c72f5ffd1fa33dd0ec763e3028f22b33164f7725fa90bcace4bffc5a1a44c5ead8d7a4ef1312e28665f62e09d3e845ac5eb5322381ccd8dd25d6601cff44954e9cef0e719c7f5945ea91a95ed32a69fe2676e62db8e98bf1ee4cbbfc2270e7f97a3791b7daa2d6e0696a994aea73c9096077da55ea9788947c4ac575933fa3c9ebede2f9d933efe13ccc28762a32f349c72c6243699117f64c8fb498960838c3ed33a6bde0add7043f55d41d925138a216b1244e9c743abf31d38ef74d1e5567dd9cf3d18dfa2013d15feac483f84109cbbe50e021ca914421c870ef33cea73eb8e4bf44c26c9110e75daf3f584ea74715b8fe9517facf675226697e6196be80d476fe6b2c3ed0a6871a7d6c2b449d4b77c76eacc645d8230164eeae097df3039486d5649c859020b4835bcc09eded052f0c81bd98422731cc213f2d4c94b0691311fa5631ff40fa1fb8afcb89cef84c2c098ece64c510e0ec6b0ce3b886dd0af654a8b122051fdc506ce2a489e3de4917dfd6005f7bf17f71ac249df616b7e2ee433592ec122b417d98f1f6880fd9fd99796ba509dc018c7942ac7d2760b757c1fc939bf87ed34c49cb0e0f49eab2dbc1d50151806e97c82a7142eaeed30ac605a05ff4323b5995ec50fe39b861a6974af802b796807030554e3f95ffa95b274dfc015a56b09108ad0f21edfa3fc2316c0320fd5d87ce538bf8ffbf04c4d76c74d1b08c54fccbe4640f0421591d99083b87058e15610aedf2bccf8ef41652f3759634ea9dd2975871579270d082651715490332d03109cb48d788664eacedc3660799f586f66e70c913dd8511d0100ab86b3d84c5e23d9a89735e02d3c43a51649693cc38b69171ca4e6c67642212f986efa43cd2dd4d9fe67fabda4b9eec56deaa85229d2463ec6723fb24eb82b3f01007ce2a4739d42a02bcb331317607dca2fd3b13142053b49d83650bbe8630e379caa7bd1679d6585378b949cecad4d2f43a105c05490c458870bfe6ccefced4d01000000000000000001007a72132d71c9756eb315ba91e59625029e5206e81e4af0ad928025faede51b9ae0be61d9a7b73adefc0d73542658393ebe3cc3bcc20a286e86a7ea68becfe5122100333473f16d482c8725d51b4cef4e26ec24b4681f9ef8523b160fc0880c6bc1cd01ce2c6d87d0184c1799a254ed03047cf57a3bfd49b8a4438d496d741cf5aca6bd000101004100fa525783d544902802f52c822d0e3e842660b577b7e4d74627e34350f1aced59f6ea356aa2a936b6e9ace648eefe288d0e10402de99b8e426d946ddddc2873dc diff --git a/zebra-test/src/vectors/orchard-zsa-workflow-block-5.txt b/zebra-test/src/vectors/orchard-zsa-workflow-block-5.txt new file mode 100644 index 00000000000..435565d3bfd --- /dev/null +++ b/zebra-test/src/vectors/orchard-zsa-workflow-block-5.txt @@ -0,0 +1 @@ +04000000bf2db11854853a8246c563df3ae4b71ab875ca42a7d40ef0915c9b741e0cc8391995c43e4ce9356caf6eb198529f595bff8ff3b1c35943bcc5ca6519a382e8d411f3c2c438a42ab898cff58ac4b6c2c3a97d4f3d7662b71f2c7905aaaa25201522254a4d0f0f0f200202020202020202020202020202020202020202020202020202020202020202fd4005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020400008085202f89010000000000000000000000000000000000000000000000000000000000000000ffffffff025500ffffffff0140be4025000000001976a91475dd6d7f4bef95aa1ff1a711e5bfd853b4c6aaf888ac000000000500000000000000000000000000000600008077777777d80a1977000000001c1d1c0000000000000000000000000001020dae0f6b69ef92d4f64c2e8e5de3c7f75d46817399ab9aa789bb36ff2e9083b77f2557946492cb76ef9a7606b1c7e80ea69eb1965d1a63df175c876560b1712710facb52f1d783eb8937eb8674bf5e64297e07a54fe2588fe12e71a2bd09a9b73f4a58aa3f8c718c4e0f42ddb0dba2652f947cc48a221f834dd58f92d0172716f68c76f9cb4faf8a7f21648c85328c39e0afae0c7c39616fc0d321a8233e48a8a9a9920f6feecd8f115ea0d378cd4c50f6e39b8e48b25e32758bc96f3db5f3d87652d1700430f0a611afc1131415dadbbcdcca83a0edaa0a9b1b4dbb768ce92131b3d98637a9bb58c3566adaeaa42ef767cda79297697f9556e55ebc56b94ab8077efd8deed2436a2ccb9188a3f887e2f3a507b83f8794622574d8ce3a0f5b29f6304e612e44b169aa41a1677ce4074f02618ac400bd24e1c464c7bdf5e6f8c968cc964c62b40a4db30c154babb39c1f9dcab495c5de6c3000a1eecae312595ffd40ca907adbb74f6d76a26192c7ce592f8206a0ee78727ad14b3fb76ce88c225cd6cfda207c8dcd1f90bedf39e7c6f7c1cd569a24b40ca1d243c5abab615475ccf885f7ac1bbb18cf259f9cc48f0546704d790281df2078059b45c25fcf2b989371602621ccd816ce2efedc8022020ba37581156dc18ec25bec578f86a3ede281a114bc18baa2c14931c9be402ae5b847a69c54bded786a02707f3abe602ea974e00d963eb5bc671bbc46793be751345cec2ca9fe409b0e97ad085e9479a99807aceeffa918e1063c6358cf17c01589cb1200d87e79c467637ea7ad32b7f930e8d2d237dc6423167b1a4347928eefaba5f610dc18a72f7821a7569b264837d6d03f442a378bfa38f4f15b9c6fff5c756d169e9c3f647f0a42134d83b5638467fe01c425e6b0addeeca03cccc47c4e8534810c9a2eec4ab3382393fb85c0592f73937aa7cf5231f7aa21550f64d7a026bf645c0537991b267796f7912b15ba325d24f8ce945d016384653024dd1b0e5a40c78026a94ce033f814e942736a0fd10716189fad4c004622262cfc7c023b1610dd1e24b9b65cc0f20fff8ae11e1e258758f95b1e65d67c49c7498605548b2ff77fe72cbcdd2184f2a03de7754bff3b7da0e8b62ff6df10f1b9fe6cdc3329a3ecc7dfdc86ff3324d83457ca4b44ee3de5e20b93fd534acbe5034027bd739fc603f68638a0527562e8a3996ac6d9c997ed037bb6c6bde8d29a0c449602e6fa5ab55c0a9e4e48897fa3208b0de92255439fa7aa00e334de9bed92fd5c5a9a1d41ceb2de0559ab64fc528c361aa76567dd09a77583e306f9e6da86fc8a9739b87c3e7c9f1bbc5266615a722c258ed9c3a494de1c61932d2b612ea3ea5c88b2e2ab6f93591ba63aa2423401aab0e0472333ec5fa360d06cb46b8d11d6fda67389046ca83f8156bcb705bc22cac98f446b727272d29faa4eb02af2bc4b4f1bbfc0d31058e75e631da028b07b7a24942db691c2b9dc46c294d59267025f0ad95bb9a1c52c085b85e499b1ae946fbf41a9210361d75719c588c5c983fe35fa17d7880c49eb35fb42969855fd30f77cb2ddbd872c4f90d76607aabeaa6914f2c349a9da896c6b102e7b17a82b8e6a51b44f29eeb8d0f76ddd3461d47d8a1667b0028aa4ec1c14a12934a337e21990f0d51ac92702af380f38874b5310d17d3a7dcdbdeaa7ebfd8618638ca2448c75cf2ca6499b5f62087a75e0b317ee735b0d2bd37e9ba09025df2a584631ee38abbc27bce96c2628b161209d6519610f55997145cf7d2c62e294e4ba595023b7c1aa297d080a4a0184c5f57e4db23dbc48d29923f49c3422f970ce220e183d80411733ec0771837243ea16306387f6f0ff3e38c88fe8b270287ecb4665079aa1d7b4bd81dc5274ebdfef7bbade1b38d7ea5a0c2e67b0a406202a50b23bfdf46d3ad7cc5853cac4f526f09673bd5d0eaecd32d740f8d0a2e803c457f484cdadfbebfbea9d855fe336a2cc958b8971c4e8a78927759097a31800cc29afae86cb4f80557f0bad1fa47204afd3575121f26462e2a0b7541e39634fde8ad3962ec1488a680babd379aef88b4369e1ee477442494dbe5b6504714c037f4f8f9a53862cb4d7a1370bf65ea317852bf432b4edfe6ac2b7386e0828e1753d2c44ee6aedc06c4e5041fe0d8627d32f7afb6f9da79c3ca35b0bcdd0a522ca7f25d85e2c32c164e4089192e9e28365ae7eed8e34113270ebf8885b34dfa8eaaddb5470d29a04cc0d332434923a9629e56bcce61620b2adb2566b933199d35da08c785dbdfcd6ce0c6a12d38b9567a5e762b184beff3b2e527ed803c157d1d8ba1065d7b39f4a7cb1cb3c53da778fa8998c6eec11e995520773a8c6eef5ae030b2e6ae4d0be533ab45fd0c31d41135c119ef0ecb307e4e4f84d46511eaba518e0910e29c957a57efdc72573aadb8bac36ec3267f9210000000000fde01c188e377136780dfdbe3c75749e3bf53da48b46976c017a66364901a285b1831336d2998204a418553339d6364740ded8308a980219b0ffed2907294d91fec112c22ce8ebdcde3041f1125ea56ebfcff90b662f0d6e964d1dfeec3afdc11f593d51ced59eb2920a9870cb1c37d9f4fcecc6bd60b2bb9d8b51a5318a102da92c11359f2c2f38c91b13183d41568fd05f49002dd77e0d7dda2311db5da850b5628a946d4137d1e951c472cfbb7a848a9a63c62cf9bd06badffd005c1394e13aa5170a51b759cba5533bb20b900830910ab476a4acdb0752905795251d10146f7105b78210f2e942f5c74aceb95b1efb0c6c3093a199a6aae1cb9c104f85a89c3ea5e56ad1c14925cd5b8ee61da55b3de7814648d1d36b422799d03a06f8fb8354a995a988d993ed46462b0548f96d611ecae494d0d5e1ffa060bd0d48afdcdf242966642c57e5d16b6bf54c184c288f2b594da2fb9e04f3c352bf10295d49a3c69319938e84f7ac582b06f7d3256a61cbcccc9adbf4245e0716255a34fd7c71c80a67c77f50784e79ca6eb281275823309a6c7bf5babc0ea0d5943837f96e0dd00cb2bf73d8300f43c9c3f02e6dba291dc30204cf0bee2841995600162376c8372046f8dac7b3f32adc3206498f66907a0482ff70254832c4bb6e1497ab562f89bf20c82bcd2746136ec25ffa0bd1c62c8f44bfb726ff7c49aed573058ab054989b65dc4a6c4cea72584371ceb4d4ef9e56adb6d1bb438d1940b7df1eaed3680711907a9a5c89d3cc2ba7093dabd32bd51c0c0903fbfc0a47847046d5d027b0c30693d46f16db4f24b582c2d7adeb493d5a4f6e88a3b858a22e5b7ab2d3000202a71902986d3784fcfa40ce43c293f48eca6d572a6e3e97a459fc9e3b1e6e0091befb7e1e81998e5b554bff5a98cdad3f62cc63806ff620073d0f91fbdcb0feeea41726dee4d1801c1e8ff87a5be515cd6594173de2fa03932465b2a62905c3ab25830816877924d089b1a508f5490446a0b0b2d3f73e1d6f383ec20d1ce16e312cc73afaeab770aebf479e5f19f7bd1a497f1e891598406258c9181ee7d56f93be0de5d8506b447d5378fb9cb004aaad64a61cf8e993d1e9426f81b614b8a8d42a1d56cd805c86c75afa8c0f4143cbc5df71e61dffee9d5098e6cb83c85d7e41b587bb9412f5606b7db04c6e88d94ce52813f61663544cf6f3ed0b59314bbe422dc0380287fd79af38ea59028812d7ba6680cfd50662bca04151f8692677fe53a305a2e3d22678975d1aec6e9b16d5d10b91e8f555dd5bfec61a2cefbbd811b7a7cd2709d4398c588e0ae78263a7196f917f6b834a1b5251143d92ed10e32966b8b4aa199e4efff6916bc688b17b0c7ab594e1a17a92b62b37cf7f4b44856c5b89cf96414f34bc7acb5a6e5d70c744197f3a4828a0ed2564c3eaaa18e3cc50a4bd365a2a5e00662c905994ad97faf959773409f88d76f8f76b628a621cf1f0901e5091a566940c07044cd4499570872b036c526ad23c24c2276668424f9d97e1016c08ffc320e0f6ee1a0f3c1ef9f5ee7583a0d85b843f380399bfc17178bbf3915805b6cd916f3795762b5213377d5cfee4d3dd572041c7137cffaec61a13100a39751129e6c1376c3e53394cbfba46740ed2a5ccdd475bff0058fc788121bc26d7bb59d5e3fa8e06bae50796aaca2ddd550ee1aaba926fb2b901b0211284203dba4a5cc7ff6fdd9e023836cd54f8383041716e8ba5c620114fba66aab5638e346e4d3fbca3562cbfc0bf3ce75ff8402d3e4e5eac269d296e83a2b7136296dc9597e840bd8435c5df11bc67c485b8907b144bd647cf3d124f712f8a6149dee99cc2dc86cc86b9608ba6d4e9f4d9e09e4b2401141a0a0b842db652e61b8a9a27b2e0af0b609515a0546633e986386ec0d76d297fd4139fabdedb91e30b85016f3db57df2da16ca7f460f9a16f556898ec9cd8a2289fe081cb92d0d4b04316edc3b640d2221d6bf17aff815b9b0d688913feedb0c600bb424bf2c9ebacc4db4529cf6bba509d85d7bbd8b674f2232211687c9f42745ac8269e5905f1ac3a509c52fdb460f68331a5b0bc927fe3196178603b533511f4535748081f6d425df18bfaa941d8abe449b48b5379179dad980fb1b019e8c2c9dc9d54cd0e6c3fd98ba4336a7a7899d5cf8c44f3cc04c9e91eef7f5b6f0222cd317b980aa791fe0fd827baad7316db7ba07f2626cde56e8cc7d1892aa0ea7af0130c6430df01e69c12a4c12e367ffab4bbd8d14f156dfc215a45cc4269e158c7a65a9be76a488890648f69ac60d7fab0b7eb2f639c051e89e4e6026c79d4542b9fdd169ae78a5cae801f81e6870b9912917ec53367a1466695d10cba3293ed61f7f3a0b142b0bd249fcd3acf7ba2d4f60d2861aee378542348d7a491ab8f3ccc7bb493504a472d124298a1c8d6b5abc370cea464da6bd6278d9e50c54314cc04fd9bc05f573faf98694420b71e5dfbee53bbcd1e7361325ec0b11dfb438df925b3009aa3e613e5c382bea27b6c10c210540826c5e789c84ef45b7f352b012416c48c800a5c631dc0739143e75f2623722354380f7e503928314d28dac7082972b76f171260b75cc44e8e31ea09f319d183b487929e34f3bd6414743517c640f7e3016ee92cf91d2b4cf631c232a2264fb6d7c631f9ffcd774cec0060ee9fdc3d913a6f10a236029a89f3b116f511d57ff50933340d1cb645b2522a7a1c4d12b6087d29d2c1a56f2776bd92f20520f7cd5bdecc055ca85312d59fb2d9fc1b8d48da64906c415ad32a2b0ba07d7dd2d8df84d739786167fe9eb5ac42534317d8ec6de56830440179bc7007a0560ad11c153f760576de6c3236a34ca5a88f54271cb23d24b0d60b5ec9e733f1b514ae64a593d91254c7b128ae3b9da73a217fc68133cca8bd5841361cd2fb428a1756a9ccd0d728017acd1cd25988f3467d05bd98b3e3bdae39b696aef2fad097b7dc664229693b7b1e50500d7493e7ad7b286c32a59c583e959e9b9047b1f190b84fa1f2edbbca09a7d9dd36958f089e6ed849a3ff1535fa16eeca51d066229239049b78efa90d3210749285d251184b53ffadb50b05e6f477eb7a899a7533622c64a22f79c6992b01d358ad410644d355f5a8ad5446c58e93217719d28362fe1311e861cc788d00dd0dc1ad027e24ec8e5c0484d1a64fa6418694818114510b75a310b80be2dd00fba25471e8be0a8dd580114ce97aa41fb09b89622c24423821b57fd9bdf6f352e642f11d55d3d7e0ea6fb087f88c1f101a618b3909e123922ce03a66cdff7ced587d7dad1bba0f1d28dd2976f7c63018ff0d6a41b1e6910e0778dc679175a3058edeaadb6d3b61d51e12ddcc612dcf63fe4b77c3e67f92841af883b5821916955734c9b13604f2686aa92a2a7e65fd4599328918fda86022a7e6dea0ba6227a54e4135bb786fea3b12b5e51837073ed8a94a8b9dc65e431f6f87fceac236da4de63a3184d3eb26234a447f27034e4261b4513b666c848086ff04e8bf3bae807c4b07e2c474b4efbbe456eb73d1896609d068a168a811422941264a4620919e43b0ba752ed47595876ef533e9b1ea4a19fdb48baf2afb401d629a6d7575a2cb2f2fae94e5f4b496be23d42075048b518658caf35112ba800eed77ade00462d013cc9847ee0b9fc7d0f9fa248ce0e6dab49a7b86c5ccb0b0909c41c11ea5cb67b90bac37ec93d1b73d1e9bb7f8199126b825a80dd04497f1c6ea27d84d08146134062c03a4959f7f2aceac748b77d2b6df5c55feec6511625adf18900359832ed074c8bfb23db24aeba790ce905a1341718d43a098f923712fb6c8deccf274138268a1917e674c83c21b37193e01ab90bee904aafa55104178f2d3e77e43cb02d34f03178f07b8970a56260964db4646afc8d7c437052663a5116804fcf70c2f420a443e32c6d97d63cb2f3a1c2aa394cb7ae4814ee03c115701e58caa281402eddf60ceed479cda61fe221ff83c58af5357da9bf62cd8821d79ec69c8605c75de6d111dfe875fb31f3b2410326e4d6b407d1a1b827ca9c3cfe8679d77b4ca33f28e69989968350ba670b96cb0a4f4974bd231d1b79baa209e85e0af05c80c56db027e4c646dc88c032bb6fb928b079cb6acf97b51818990523f89fd8aa7261379186ac0e1edc4b0398767c8a4c508045567f81a16561fc11048ab7959969d241407c38283148402597880904db5223cb765e71f668c5182ca947c89b9eecb2afb08e953d90d7c3bdbd978385d661599406243c94b18d2128c24c0b1eebee47bc674fbd75dea65b493cf43c0a582668b294d059e03555962f1540896bdaa7e122ef345c40e5824c538fa0b9473cd34870ee4eaa8e2470cf3ddec01c46097c1a80327afe723bd63b037c1f2b3d8c53246e27b80149ba67f703838ca689dd8c4b103694197b162eb8b13c657a5d2f5972ef9650624ff631170b3813a9ffccd2bda18fb29e3b1686ed313c0ad18a93a40b8607a791384d50e4149c7ad441f4a7c40ad140a2c8d0c80e5355918836c1b493511a5637fde687ef211ab1ad66cce847626208f9d46e3751e4ce66c5888417c8f74d3250a15c1ac234acc49ce674ad2caa17ca1fbe02d13e2cbb53c91139da0c139e6bae21e6298e308bfe05c09c8c0b9b66eab09afedc26683a95d7f69c1d52c12aec4ea73a86f107b2e0c6861663edd25500cef4a3834c8144aa8434091ec6a9a8c17ecac51aa53698aabcedfa598d09e9a85bbb5ceaaa0b19f4614da7d74b94069b469eb09cd805b4876cccea363b8f9ea84b491fa6adefb15bcd753cb935bc5c40bb6933ddc438303102c2f87ad7ca761c18b177f01462be95c43845215035d042b6c8b76fc53ad48f161bc2def2348fe5c63ae39e9b28a5532168e4c1dae52f4603c5f5feb60a66151fff1fd3497c09ee0696a60fc5a9691b348f9e2f75d6e3064532041d79034eab7e6b2b6fb35a47647fd18544e63f3377b91015473da13609b7230a88602dcc86496ead8765f08988d4c12f1477932ee6441ae2ba0c9337e05333ba590136b321fa74c95957365a86a68beb217e43e5abc61b0a36110ebdf334d0282dd12e6aa5124234247007fc46d97bc645fc18f10c91ce5fcec5d53e6631b031e4160eb666d90398b2ccc845e3bd397be544d01c63379d720fb90278f160b2c31fa90151ecff86d4a69f889932546689dec81b54416d19e9a788c78c33ce3ca806743ae1cae0f66a43cd18bd8c43bedae3b2e2dd3c12fd7fa709d7080b7d7567fbb63cb49e3ccb133af45e4ea1195fd0d82aaa613e18d78e140b3438ed98ce39ce8f3814de6494b7807198d757c0a4e285df138908310cb71b63a665793afd9e9f8d1c72f5e453b6695369cc4137317a1a84648469f0dae399b5658471dee594123d076a232883b31baa50a507445baa16b40f678c421cdf377e469d16f8a1e529933c66847b9598f3920c8ed6b50a598fa798616919b5f4d902e986955dbc1792f224932a0cdd8310b1b0d272d722a99b1c4879a39aa0b1ea5e0a1704cf2695863a089f57e7a417a7e58679f27822f57a0331d1a95d56e6d9cbe1b088f89ea98b390534e77fac03bf1664b67a6a580ee418acc34962defcd92cb2a53e8a8557ec0b25eb8403c0cc2a92502d347641b32a1a0c98ed28aae6a9e0f8aea0fb641aa2393c22f41be5c7061affecdee0356232735d4bb49b0a1dbadbdf89628bbf2f77e836c350d7ccc396b10ec6ad5d854a98644e574898ff3c3aa94cffa2b6e9f89a5436a62c4edf93785ee45772c31a0eb5741fc0092226921d13dd79c41f0c2bbe8231956d7f0579d80a301998b525dcadae007786a5fcf45d5fd2b91609cc1d84963b051202d210ce17bbbfda528bbd645da09f5a7bfa7007315b42753390b8836903022916292a760c3443fb17b22830b2229d79280e26b4824bc6c5d7721b70093149434f6e3ddd43f2e3bc65aa38fe53407bbbded59a78978e1ec7bcf66d59ac04e2f0bafc89cdabb25c47b9790bbd6048c711191a1617842ceee0acdb3bf9d20ced32cadafe375c00a1e6725295d84e97ab3be1c29d1d82efaaa882a54ddeab0856e66e15a568c6236a147d6d83a2d432bfa94e04f82420933ead871c3d76fc39c3fea133fd5a7de2498b06946939efe1d77866768d238b7bd3b402c66cadf011269e940f723964f60d60759211c83e95d9fc09c1c1ab486e933d6ed544504f302d26cabfaf0a167861a81c789ab6d8aa403b5d960d61d5bb156895588f914a1a46f7be67a7076a6c0c5b02f67347f927d3e53db46b57db56753bc4d666f0b91933b150b39144e6aa105913306758bb56788562dc655ee4dcded65ef58a91d60ed3ae805c6290d5a8f07c91f461f0cb6b32d8bd6bfa24001edc51f960d07cc22ee32a22163e460c644f65688460dfc12bf411f2cf83ce3acab2698e5ac0093639d233c6c6dc5a1d8906909d342eca52bc76077139f14d5de7d0290cc4b6d9b42ccf0a2ef187dc8e88e252b7159e0b4bd6d21caed0cfbf59dc393e00bb45dab2228ebe2175a43d40b98a4b39129473495b2df4d40233c48528a007a7e180c72716b7bd21c3331d6078f06977a311b2399e41395d0e8211197e133179533a24d420b7e4da12281161e6727c38a99762165bd5a31c285e6beac56852b218c8b96532a45c2e9ff6cf4beabb22a9a2468352456441bb6a1864af1605a28e16175b3b17267db8f0215c708a6d53d6dd3a7ec2cdcea31d8e1f09e9f859873282fe607f29959e06efdaf749120a72145801564da1c874e119014b1b7d84de381fc0dfb63feca99c59154a34d7812e8e4de28f0afc73cf23e10ead4bbb52439757646b991ebd11fbe848029f17b0086828e9c881c6360471057ded13a4658f7a908205b800169023f653e5acaf73472332576ef9335e3aa446afd29106d184edade5a3c2049bcc9ce1cab5303fb398bcb790a94854a8e8b50a96f81d447ab5941dd00ccd040eb3a4c3aba9257fbf381fcaf9447577213550bea19d24ac0f402c6f815f812b48eb31842c7b256a59cf1d5f728157cced8a7b86f1e5f867ec33bc2700b81e23d1c076264c9b57288f1d094a86ee64dbbb9581f8df00ed449278080e9dc9cc2fc2edc65a3bcb1b89a260399483d6af3ffcbc60a5af5f4a4a2e5e5d6038eff308bc63a48c0b9c1cb84c410929fbc5e164141264e30bbce50d2decb8aba967e21e8e8b5da53616a7d01afee1671a69a7893e49e6d933506ec4f6862c71dc397c167222543aa96fce5c2f12e2a296c18786648027bc59b8b6d1310d5d386115e123611cfcc7fcc41ddca1e999b923e9cab5090fac1f5196fd789595c689c660e62f34600c86d1069fe2e5178f72a4f0698b2fd7e4f7ca4ed08a5fcbff29599e011af73a5c98ca62787fdb7fc8eec0c653d4050866a501b237bd51c1f99d375e220ef6df4e3de65b789efc49ccad76bb521f3fdfd2a8aea0ca472d248ef48d2b9b18983cf1e0f1fcc870fdebb73df9a5bc57822cd318aa97e16f7ffdf87cd866071b6d46531872fbaa8688cd365fd503725e3d2d26bd0e2a5d9b7b628abeae09983079ef510098e1ea4f19921f53ba4dcf11eaab49bb27cfef03061258403f770c0349c0d1e63c9f5510df94916d497373235eda172b700a74fd03d1f6c4037a5e26205d4f0ffa4ecfca3e9a5cefd5bb10961910f521a9a7644615233696d77be22667564f128b83382dc02d9dca64735fbdffc71edc2893fb2456028d969cb48238fc4a89f485ea7f75b0e093b92e15713f45e32d8847a0d95c2ac67ba015b9a63c6ac30ec42fa4eb67faf137b4aeb1f08d0cf7f6e54c888941d4cd68808b018017bc9d63d2b73a96010eb766c3b3805fa11b59281edee7f5469162b2fb3d9aac2c36868ebd44dbbbf269d6d41a903cf16f1a5e865859df2602d61eafcde89ec822e23e1c541197fc23ebe6ef7da65aec5881a98e4fe8b4eb121519aba3bd7dd3117c9d72d3ec236edff272bec449343fd75c7fa9ade9684a3f3ee9ec7a647f5504cd2333c3c48e72fb9d750d3d38203f93dcd07499111c3aba25722d67266e0f103187cad58a2dce3aac9ed4b2bac919b65d9600c1d6b9e9ab0adb93cd359b283b65cb21dd79df971c53072927ad4b7ec26344143f04e9026a8c935c619d10833295949348693c6c850303800a82b09c549da3e024c11a4e7d3ec9e590fc92911d93ffa57b9c3b8319add04d2b08d7b0b3ef52962701c5215d670922b26f3457193dcfee7d6cd48ba0da67a08be6b234c34842b0a1919ebe472dbb63d61a6dd22093095aac6f66cf4b4681b169819e0c546266a9e5cb59a444121c597cb37b5728d6ad97a46f062d362cb2554af6c2d38e1c89dbe8128514b18e128a2c5d6458087b80d1ba859b051409d3c81734abc6b38fff1fa8fbf2ccb3d50afef7ecfc7a3ba476f22d4f1504550cf15c9ecf04bde80f4a6b78ac9c6c7b1c818e47b3a24315cd569bef09aebeabf440aa58f696a779a4ad57c5d139bb51d730db1c950b38367e691da2883c76358b953b9e3f1ddfa4a8a43c24f3bc9564bc7885512d2ed41ba876914b60ef4f2f430c4de3e05dd476c88b90b6e4ed301521887f2be0773b015da86f753bb42a1208bcc6ce91109d0b2f867f4c261ff8ee81cc688d11cb9e049e371319fcae9bca3d71ed72e15ebff6dce15ee19d98da8cfb71374e442ceb23a532841bb92cd637bc52cd46a4d6273f4d5ca12e7861cf43d002801aeb2e841a29db4b598489d14cd8ded30b7b5f9b514ab259bd9e4b0d78ab7d9ce3e13565277a76e1e221a0a0669240741c37a680d61fb11499ea422b798f29ddd510390e3cc8935420a52021df741ef1cbf9029952e595e9747624b6949cbb55664e46af0e465ba3a64e240858ce9ae63a8e3057ec26fbb6e80fcd48ef64528cefb2218a1cacbfe0c5ab04b988e6466fd74dcf5b216de799737c4bf6a30ecf0cb3645c830f3b61ea9e1e7a43663d214fe89fddb8865829a013c1eaa389f9792623d061bd3a7dc45902d7927d496cd12451f010926835f24da4323366514cb435cf35309b381125863c8d9a392cc3db81951aa24b17e5e9f5e134a627c526abf4be7cc33b3df0e18e2e9d8a1703f8847d0d8285b11120bb54535115f373628d6f88e49ddc2e523c8548da55ca656045e38c8227dea861f7a9fb5a7c7026fd47f74e9662c015331791732c29d5849337ede4aad274b4106fb34a1256095f443f771a8eb64c14074c332ec6da0f6e920f07d237c70118ebe5347246a5999519c32289d039a6123f8892c5f00443e345cdfb45f1b7fe28ffb2b6a5def67f235920bcfd10bf11b58a6c6bd01b8f700c0ac4f361513258e1093eda26bb055bff436aee4e2df8cb9d25c7b7ca7203e908ba9da64119921813c35984567c5a045f4eece12b5803a6152ef624c2938d025fef0c493444daec4bedf781e7a59d517e1fbd36fdf1355829e5326d3648a6e393247d5a57b041e49fef691af1408fda20ba16d23c8b4db28d7103333fda1a8f2397ff68796d9d940ca11969d8accc76c9e4556d92dc68049c56f7867e550c193d1e1ca8dacdae77d857b460764f66998462d8f29cfeff131afbc6e58bb78db441634d67528a9b2ea82cfc0757bf421bca1ad2b549851f50355e7f04ec9a4a8e07e42ff38034378f64b276753c22ab2f1310619b95c0a870b654a3d17ac6f07782a259fd471c6b21e6566cbd52d1c4c501345c98b8600e88a1d37b8a60f4bd246b700797fb2c072fa44c299bffb965605b0a97b5430a8c2f8fd55c2d38c45410f834757167d59f7aff3ef8e44c356f1b8396d15bd75b986aa63a5d94301b815bcc496cb54316f0ab48e7fd488227aaa8e0975c402113af080853b891129b1fdd7b0c02c768dc39afcf3409a528c91db70e62980b4e9226748147716b4ff70708575c15d7de7119ef30dfa2f98de797efeda0230db6fb75e60cd2c4f85bf875a01e20c751a1beec240cceaa6bd92aff8f640ba699e3a9e15193a1320880aa11a1048c7a6c5696bf351cb69739fb2b26643ee6cb141d5458b40da6264cd9d268e96d4f8b55d876559992e5a454a3154b5a44554f9bb19dd678264f308d3f7d9aa817275a9a092601f07498ffa7a8d0125cee8a7c563c3cb74231fcc9ef586d6bd8d1da774b27267cc2f1e73e4f3e6b3cacc1d0c80da5db15dca5bbc3046b259a830045ea6ac6c7e16321a74342f01a236c43b5fe13c93a33e736f2905452925c0a42816ddb5dd33593071ae71ee87f4e8ab116770f7bf6d3a2860594ab1a30581ccdbb7d9d8ba64f31e8c1635f6bf88c767a265338d0f768b1a9efc89596641d00552ec434495d878197b53932decc451c5cc03d915248d6390da3bf7eb6c68f92ec7b23a7ead609052ba972d649d2b2c8fae788d603674021290100bdd47536e0e9ddd0ed7f6ca78696fdc9354cd66f719116f2d6c79fc16c78ac1cfeccc36ee57eabe610199ddb57a5610d2c57c8ad584f85ea587dfa9e1045112f0100a10624b94bc7e5897857d9c9c6bd7db34f49999ae691af7cae1ec1091934b118989f88cf0648aa0165647cae3943ce67f43a5457b79a9853f4c279a79c87ff1c00000000000000000100a63ee2baec827e97d5bf72d553e9cb96425d9511b028d4dbb9d1bbbe99728b3780afe98c73ac2dcb34a366c34d5a2d07cc1d9e6918791fbacdd81456a3e50e072100333473f16d482c8725d51b4cef4e26ec24b4681f9ef8523b160fc0880c6bc1cd01ce2c6d87d0184c1799a254ed03047cf57a3bfd49b8a4438d496d741cf5aca6bd02cc36601959213b6b0cdb96a75c17c3a668a97f0d6a8c5ce164a518ea9ba9a50ea75191fd861b0ff10e62b00000000000000000ce51b2430a969dcca7f33fc6e3b70e812a62b8c2682661909aaa36c5d956fe3fd69d8d271e71fd393e3b660615f3607584494362271c529492f4a4db06d138f7db82ac459e02256acaf1a8ccac2c4b5c581c3f178dcc4b047e32fa1a7e751bdcfc6f42d46381f89dedaa04d007000000000000508bfc11533594e458bf22848099fc56c00d4c279d02b08201381ae3041c382ebfc9445172a0430d3645bcf95d72d3ba53ccec0f46904d9dcc1a82c3531256a30001004100f1d963f532671dbecd062b0cf1d37a7eeaff9eb90361e021d1f766fca0fc6c1ec38f518dbb2b2d70d03c87cb9bd87034b3334c972d27d1a5a34f7181a9772081 diff --git a/zebra-test/src/vectors/orchard_zsa_workflow_blocks.rs b/zebra-test/src/vectors/orchard_zsa_workflow_blocks.rs new file mode 100644 index 00000000000..8aa976208fb --- /dev/null +++ b/zebra-test/src/vectors/orchard_zsa_workflow_blocks.rs @@ -0,0 +1,75 @@ +//! OrchardZSA workflow test blocks + +#![allow(missing_docs)] + +use hex::FromHex; +use lazy_static::lazy_static; + +/// Represents a serialized block and its validity status. +pub struct OrchardWorkflowBlock { + /// Block height. + pub height: u32, + /// Serialized byte data of the block. + pub bytes: &'static [u8], + /// Indicates whether the block is valid. + pub is_valid: bool, +} + +fn decode_bytes(hex: &str) -> Vec { + >::from_hex(hex.trim()).expect("Block bytes are in valid hex representation") +} + +lazy_static! { + pub static ref ORCHARD_ZSA_WORKFLOW_BLOCK_1_BYTES: Vec = + decode_bytes(include_str!("orchard-zsa-workflow-block-1.txt")); + pub static ref ORCHARD_ZSA_WORKFLOW_BLOCK_2_BYTES: Vec = + decode_bytes(include_str!("orchard-zsa-workflow-block-2.txt")); + pub static ref ORCHARD_ZSA_WORKFLOW_BLOCK_3_BYTES: Vec = + decode_bytes(include_str!("orchard-zsa-workflow-block-3.txt")); + pub static ref ORCHARD_ZSA_WORKFLOW_BLOCK_4_BYTES: Vec = + decode_bytes(include_str!("orchard-zsa-workflow-block-4.txt")); + pub static ref ORCHARD_ZSA_WORKFLOW_BLOCK_5_BYTES: Vec = + decode_bytes(include_str!("orchard-zsa-workflow-block-5.txt")); + + /// Test blocks for a Zcash Shielded Assets (ZSA) workflow. + /// The sequence demonstrates issuing, transferring, and burning a custom + /// asset, then finalizing the issuance and attempting an extra issue. + /// + /// The workflow blocks were generated using `zcash_tx_tool` + pub static ref ORCHARD_ZSA_WORKFLOW_BLOCKS: Vec = vec![ + // Issue: 1000 + OrchardWorkflowBlock { + height: 1, + bytes: ORCHARD_ZSA_WORKFLOW_BLOCK_1_BYTES.as_slice(), + is_valid: true + }, + + // Transfer + OrchardWorkflowBlock { + height: 2, + bytes: ORCHARD_ZSA_WORKFLOW_BLOCK_2_BYTES.as_slice(), + is_valid: true + }, + + // Burn: 7, Burn: 2 + OrchardWorkflowBlock { + height: 3, + bytes: ORCHARD_ZSA_WORKFLOW_BLOCK_3_BYTES.as_slice(), + is_valid: true + }, + + // Issue: finalize + OrchardWorkflowBlock { + height: 4, + bytes: ORCHARD_ZSA_WORKFLOW_BLOCK_4_BYTES.as_slice(), + is_valid: true + }, + + // Try to issue: 2000 + OrchardWorkflowBlock { + height: 5, + bytes: ORCHARD_ZSA_WORKFLOW_BLOCK_5_BYTES.as_slice(), + is_valid: false + }, + ]; +} diff --git a/zebrad/src/components/inbound/tests/fake_peer_set.rs b/zebrad/src/components/inbound/tests/fake_peer_set.rs index f3c3951d256..53650a878f0 100644 --- a/zebrad/src/components/inbound/tests/fake_peer_set.rs +++ b/zebrad/src/components/inbound/tests/fake_peer_set.rs @@ -15,7 +15,7 @@ use zebra_chain::{ fmt::humantime_seconds, parameters::Network::{self, *}, serialization::{DateTime32, ZcashDeserializeInto}, - transaction::{UnminedTx, UnminedTxId, VerifiedUnminedTx}, + transaction::{SigHash, UnminedTx, UnminedTxId, VerifiedUnminedTx}, }; use zebra_consensus::{error::TransactionError, transaction, Config as ConsensusConfig}; use zebra_network::{ @@ -172,6 +172,7 @@ async fn mempool_push_transaction() -> Result<(), crate::BoxError> { Amount::try_from(1_000_000).expect("valid amount"), 0, std::sync::Arc::new(vec![]), + SigHash([0; 32]), ) .expect("verification should pass"), )); @@ -281,6 +282,7 @@ async fn mempool_advertise_transaction_ids() -> Result<(), crate::BoxError> { Amount::try_from(1_000_000).expect("valid amount"), 0, std::sync::Arc::new(vec![]), + SigHash([0; 32]), ) .expect("verification should pass"), )); @@ -384,6 +386,7 @@ async fn mempool_transaction_expiration() -> Result<(), crate::BoxError> { Amount::try_from(1_000_000).expect("valid amount"), 0, std::sync::Arc::new(vec![]), + SigHash([0; 32]), ) .expect("verification should pass"), )); @@ -524,6 +527,7 @@ async fn mempool_transaction_expiration() -> Result<(), crate::BoxError> { Amount::try_from(1_000_000).expect("valid amount"), 0, std::sync::Arc::new(vec![]), + SigHash([0; 32]), ) .expect("verification should pass"), )); diff --git a/zebrad/src/components/mempool/storage/tests/prop.rs b/zebrad/src/components/mempool/storage/tests/prop.rs index 7b864fd50c5..96695611063 100644 --- a/zebrad/src/components/mempool/storage/tests/prop.rs +++ b/zebrad/src/components/mempool/storage/tests/prop.rs @@ -16,7 +16,7 @@ use zebra_chain::{ sapling, serialization::AtLeastOne, sprout, - transaction::{self, JoinSplitData, Transaction, UnminedTxId, VerifiedUnminedTx}, + transaction::{self, JoinSplitData, SigHash, Transaction, UnminedTxId, VerifiedUnminedTx}, transparent, LedgerState, }; @@ -491,6 +491,7 @@ impl SpendConflictTestInput { Amount::try_from(1_000_000).expect("valid amount"), 0, std::sync::Arc::new(vec![]), + SigHash([0; 32]), ) .expect("verification should pass"), VerifiedUnminedTx::new( @@ -499,6 +500,7 @@ impl SpendConflictTestInput { Amount::try_from(1_000_000).expect("valid amount"), 0, std::sync::Arc::new(vec![]), + SigHash([0; 32]), ) .expect("verification should pass"), ) @@ -526,6 +528,7 @@ impl SpendConflictTestInput { Amount::try_from(1_000_000).expect("valid amount"), 0, std::sync::Arc::new(vec![]), + SigHash([0; 32]), ) .expect("verification should pass"), VerifiedUnminedTx::new( @@ -534,6 +537,7 @@ impl SpendConflictTestInput { Amount::try_from(1_000_000).expect("valid amount"), 0, std::sync::Arc::new(vec![]), + SigHash([0; 32]), ) .expect("verification should pass"), ) @@ -755,8 +759,8 @@ impl SpendConflictTestInput { /// present in the `conflicts` set. /// /// This may clear the entire shielded data. - fn remove_orchard_actions_with_conflicts( - maybe_shielded_data: &mut Option, + fn remove_orchard_actions_with_conflicts( + maybe_shielded_data: &mut Option>, conflicts: &HashSet, ) { if let Some(shielded_data) = maybe_shielded_data.take() { @@ -768,7 +772,7 @@ impl SpendConflictTestInput { .collect(); if let Ok(actions) = AtLeastOne::try_from(updated_actions) { - *maybe_shielded_data = Some(orchard::ShieldedData { + *maybe_shielded_data = Some(orchard::ShieldedData:: { actions, ..shielded_data }); @@ -816,7 +820,7 @@ struct SaplingSpendConflict { /// A conflict caused by revealing the same Orchard nullifier. #[derive(Arbitrary, Clone, Debug)] struct OrchardSpendConflict { - new_shielded_data: DisplayToDebug, + new_shielded_data: DisplayToDebug>, } impl SpendConflictForTransactionV4 { @@ -967,7 +971,11 @@ impl OrchardSpendConflict { /// the new action is inserted in the transaction. /// /// The transaction will then conflict with any other transaction with the same new nullifier. - pub fn apply_to(self, orchard_shielded_data: &mut Option) { + // TODO: Consider adding support of OrchardZSA. + pub fn apply_to( + self, + orchard_shielded_data: &mut Option>, + ) { if let Some(shielded_data) = orchard_shielded_data.as_mut() { shielded_data .actions diff --git a/zebrad/src/components/mempool/storage/tests/vectors.rs b/zebrad/src/components/mempool/storage/tests/vectors.rs index 328f23bb94b..a9ebcee590d 100644 --- a/zebrad/src/components/mempool/storage/tests/vectors.rs +++ b/zebrad/src/components/mempool/storage/tests/vectors.rs @@ -11,6 +11,7 @@ use zebra_chain::{ amount::{Amount, NonNegative}, block::{Block, Height}, parameters::Network, + transaction::SigHash, }; use zebra_chain::transparent; @@ -278,6 +279,7 @@ fn mempool_expired_basic_for_network(network: Network) -> Result<()> { Amount::try_from(1_000_000).expect("valid amount"), 0, std::sync::Arc::new(vec![]), + SigHash([0; 32]), ) .expect("verification should pass"), Vec::new(), diff --git a/zebrad/src/components/mempool/tests.rs b/zebrad/src/components/mempool/tests.rs index a8a90f9955a..197c659d85f 100644 --- a/zebrad/src/components/mempool/tests.rs +++ b/zebrad/src/components/mempool/tests.rs @@ -13,7 +13,7 @@ use crate::{ use zebra_chain::{ amount::{Amount, NonNegative}, parameters::NetworkKind, - transaction::{Transaction, UnminedTx, VerifiedUnminedTx}, + transaction::{SigHash, Transaction, UnminedTx, VerifiedUnminedTx}, transparent::{self, Address}, }; @@ -110,8 +110,14 @@ pub fn standard_verified_unmined_tx_strategy() -> BoxedStrategy Result<(), Report> { Amount::try_from(1_000_000).expect("invalid value"), 0, std::sync::Arc::new(vec![]), + SigHash([0; 32]), ) .expect("verification should pass"), )); @@ -1013,6 +1014,7 @@ async fn mempool_reverifies_after_tip_change() -> Result<(), Report> { Amount::try_from(1_000_000).expect("invalid value"), 0, std::sync::Arc::new(vec![]), + SigHash([0; 32]), ) .expect("verification should pass"), )); diff --git a/zebrad/tests/acceptance.rs b/zebrad/tests/acceptance.rs index af36d11a7e2..e996bcac2b3 100644 --- a/zebrad/tests/acceptance.rs +++ b/zebrad/tests/acceptance.rs @@ -3678,15 +3678,19 @@ async fn nu7_nsm_transactions() -> Result<()> { let base_network_params = testnet::Parameters::build() // Regtest genesis hash .with_genesis_hash("029f11d80ef9765602235e1bc9727e3eb6ba20839319f761fee920d63401e327") + .expect("failed to set genesis hash") .with_checkpoints(false) + .expect("failed to verify checkpoints") .with_target_difficulty_limit(U256::from_big_endian(&[0x0f; 32])) + .expect("failed to set target difficulty limit") .with_disable_pow(true) .with_slow_start_interval(Height::MIN) .with_lockbox_disbursements(vec![]) .with_activation_heights(ConfiguredActivationHeights { nu7: Some(1), ..Default::default() - }); + }) + .expect("failed to set activation heights"); let network = base_network_params .clone() @@ -3696,7 +3700,8 @@ async fn nu7_nsm_transactions() -> Result<()> { // Use default post-NU6 recipients recipients: None, }]) - .to_network(); + .to_network() + .expect("failed to build configured network"); tracing::info!("built configured Testnet, starting state service and block verifier");